# 技术架构设计

## 一、核心架构决策

### 1.1 配置驱动：一套代码 = 一个垂直科室

每个专科（肿瘤/心血管/神经...）是独立部署实例。一套代码通过配置文件切换专科，实例间完全隔离。

```yaml
# config/specialties/oncology.yaml
specialty:
  name: "肿瘤科"
  slug: "oncology"
  
pubmed_filter:
  mesh_include_categories: ["C04"]
  mesh_include_subcategories: ["C04.557", "C04.588", "C04.697"]
  mesh_cross_include: ["E02.319", "E02.815", "D27.505"]
  
journals:
  tier_config:
    tier1:  # 🔴 四大综合顶刊
      issns: ["0028-4793", "0140-6736", "0098-7484", "0959-8138"]
    tier2:  # 🟠 肿瘤顶刊
      issns: ["0732-183X", "1474-5488", "2374-2437", "2159-8290"]

tag_engine:
  tag_tree_file: "oncology_tags.json"
  mesh_to_tag_mapping: "oncology_mesh_mapping.json"

ui:
  theme:
    primary_color: "#1a5276"
    logo_text: "肿瘤科文献中心"
```

### 1.2 技术栈

| 层级 | 选型 | 依据 |
|------|------|------|
| **后端框架** | FastAPI (async) | async SQLAlchemy + asyncpg 比同步快 4.1x |
| **ORM** | SQLAlchemy 2.0 (async) | 原生 async，避免事件循环阻塞 |
| **数据库** | PostgreSQL 16 + pgvector | pgvector 为未来 AI 向量搜索预留 |
| **缓存/队列** | Redis + ARQ 任务队列 | ARQ 比 Celery 轻量，原生 async |
| **搜索** | PostgreSQL tsvector（当前）/ Elasticsearch 8.x（未来） | 当前阶段 PG 触发器维护 tsvector + GIN 索引；ES 配置为可选，ES_URL 为空时自动回退 PG |
| **文件存储** | MinIO (dev) / COS (prod) | S3 兼容 MinIO + 腾讯云 COS 双存储支持 |
| **任务队列** | Redis + ARQ | 原生 async，比 Celery 轻量，定时任务 + 异步任务均由其管理 |
| **前端** | Vue 3 + Naive UI + Pinia + Vite | 80+ 组件，TypeScript 支持好 |
| **计费** | Stripe + 本地最小化缓存 | Stripe 为计费真相来源 |
| **可观测** | structlog + Sentry | 全链路日志+错误追踪 |

### 1.3 日期时间类型

**全部使用 `TIMESTAMPTZ`**（内部存 UTC，读取时自动转时区）。特定纯日期字段（pub_date、approval_date）用 `DATE`。

- 存储 8 bytes
- 数据库自动处理时区转换
- SQL 中日期运算自然
- 禁止用整数时间戳代替日期类型

---

## 二、多租户架构

### 2.1 策略：共享表 + PostgreSQL RLS 双层防御

```
应用层：ContextVar + Repository 自动过滤
数据库层：PostgreSQL RLS 强制执行

双层保险，防止开发者遗漏 tenant_id 过滤或 SQL 注入绕过
```

### 2.2 请求生命周期

```
客户端请求
  → CORS / RateLimit / CSRF / Trace 中间件
  → FastAPI 路由
  → get_current_user (依赖注入，从 JWT 解析 user + tenant_id + is_superuser)
  → TenantContext (ContextVar 设置，非中间件—避免连接池竞争)
  → Service 层
  → Model 层 (SQLAlchemy 自动加 tenant_id 过滤)
  → PostgreSQL RLS (数据库层强制检查，仅生产启用)
```

`get_current_user` 而不是中间件设置 `tenant_ctx` 的原因：中间件在连接池层面可能跨请求污染 ContextVar，依赖注入方式确保每个请求独立。JWT access token 携带 `tid`（tenant_id）和 `is_superuser`。Admin 路由（`/admin/*`）通过路由器级依赖 `require_superuser` 强制验证。Demo 用户访问 admin 返回 403。

---

## 三、RBAC 权限模型

### 3.1 角色层级

```
owner (4)
 └─ admin (3)
      └─ editor (2)
           └─ viewer (1)
```

### 3.2 核心权限矩阵

| 权限 | owner | admin | editor | viewer |
|---|---:|---:|:---:|:---:|
| 查看文献 | ✅ | ✅ | ✅ | ✅ |
| 创建/导入文献 | ✅ | ✅ | ✅ | ❌ |
| 编辑文献 | ✅ | ✅ | ✅ | ❌ |
| 删除文献 | ✅ | ✅ | 仅自己 | ❌ |
| 管理标签 | ✅ | ✅ | ✅ | ❌ |
| 导出引用 | ✅ | ✅ | ✅ | ✅ |
| 邀请/移除成员 | ✅ | ✅ | ❌ | ❌ |
| 管理团队 | ✅ | ✅ | ❌ | ❌ |
| 配置审批流 | ✅ | ✅ | ❌ | ❌ |
| SSO配置 | ✅ | ❌ | ❌ | ❌ |
| 计费管理 | ✅ | ❌ | ❌ | ❌ |

### 3.3 代码实现

```python
# app/core/permissions.py
def require_role(minimum_role: TenantRole):
    """FastAPI 依赖注入：强制检查当前租户内最低角色"""
    async def dependency(current_user=Depends(get_current_user), db=Depends(get_db)):
        user_tenant = await db.execute(
            select(UserTenant).where(
                UserTenant.user_id == current_user.id,
                UserTenant.tenant_id == tenant_ctx.get(),
            )
        )
        if ROLE_HIERARCHY[user_tenant.role] < ROLE_HIERARCHY[minimum_role]:
            raise HTTPException(403)
        return user_tenant
    return dependency
```

---

## 四、JWT 认证设计

```
Access Token（15分钟）:
{
    "sub": "<user_id>",
    "tid": "<tenant_id>",
    "role": "editor",
    "exp": 1680000000
}

Refresh Token（30天，Redis存储）:
- Key: refresh_token:<jti>
- Value: {user_id, tenant_id, issued_at}
- 支持轮换（rotation），防重放
```

---

## 五、Stripe 计费集成

### 5.1 原则

- Stripe 是计费真相来源，本地只存 customer_id + subscription_id
- Webhook 立即返回 200 → 队列异步处理 → event.id 做幂等
- 监听最小事件集：`checkout.session.completed`、`customer.subscription.*`、`invoice.*`

### 5.2 方案功能矩阵

| 功能 | Free | Pro | Team | Enterprise |
|------|:---:|:---:|:---:|:---:|
| 存储 | 500MB | 10GB | 无限 | 无限 |
| 文献 | 1000篇 | 10000篇 | 无限 | 无限 |
| API日配额 | 100 | 1000 | 5000 | 无限 |
| 团队协作 | ❌ | ❌ | ✅ | ✅ |
| SSO | ❌ | ❌ | ❌ | ✅ |
| 审批流 | ❌ | ❌ | ✅ | ✅ |
| 品牌定制 | ❌ | ❌ | ❌ | ✅ |
| 最大成员 | 1 | 1 | 50 | 无限 |
| 价格 | $0 | $5/月 | $10/人/月 | 定制 |

---

## 六、前端架构

### 6.1 技术栈

- Vue 3 (Composition API + `<script setup>`) + TypeScript
- Vite5 + Naive UI + Pinia + Vue Router
- Axios 封装（JWT 自动刷新拦截器 + 租户头注入）

### 6.2 Pinia 状态管理

- **auth store**：认证状态、用户信息、权限
- **tenant store**：当前租户上下文
- **literature store**：文献列表、筛选条件
- **ui store**：侧边栏、主题、多标签

### 6.3 组件分层

```
业务组件 (LiteratureCard, SearchPanel...)
  └─ 基础组件 (BasicTable, BasicForm, BasicModal...)
       └─ Naive UI (n-button, n-table, n-card...)
```

---

## 七、基础设施

### 7.1 Docker Compose 核心服务

| 服务 | 镜像 | 端口 |
|------|------|------|
| PostgreSQL 16 | pgvector/pgvector:pg16 | 5432 |
| Redis | redis:7-alpine | 6379 |
| Elasticsearch 8（可选） | elasticsearch:8.11.0 | 9200 |
| MinIO | minio/minio | 9000, 9001 |
| Backend | FastAPI via Uvicorn (4 workers) | 8000 |
| Worker | ARQ | — |
| Frontend | Vite dev server (dev) / Nginx (prod) | 5173 / 80 |

> **ES 为可选服务：** dev 和 prod 的 docker-compose 均有 ES 配置，但 `ES_URL` 为空时系统自动回退 PostgreSQL tsvector 搜索。当前生产环境未启动 ES。

### 7.2 Redis 缓存策略

| 缓存 Key | TTL | 用途 |
|------|:---:|------|
| `user:{id}:profile` | 300s | 用户信息 |
| `tenant:{id}:settings` | 600s | 租户设置 |
| `tenant:{id}:plan` | 300s | 方案功能 |
| `ratelimit:{tid}:{route}:{date}` | 1天 | API限流 |
| `refresh_token:{jti}` | 30天 | JWT刷新 |
| `homepage:feed` | 1800s | 首页文献列表预缓存 |
| `hot_articles` | 1800s | 热搜缓存（TOP30 高被引） |

---

## 八、搜索引擎架构

### 8.1 当前阶段：PostgreSQL tsvector（生产可用）

文献搜索使用 PostgreSQL 内置的 tsvector + GIN 索引：

- `global_literature.search_tsv`（TSVECTOR 类型），由 `trg_global_literature_tsv` 触发器自动维护，包含 `title + abstract + authors.family + authors.affiliation`
- GIN 索引 `ix_gl_search_tsv`（重建迁移：`0314f4d28728`），约源数据 30-50%
- 所有搜索（全文搜索 + 高级搜索字段选择）走 `@@ plainto_tsquery('english', term)`，ILIKE 仅做 NULL tsvector 记录兜底
- 中文搜索自动检测 → 匹配 `GlobalTag.name_zh` → 注入 tag_ids 走标签递归检索（`search_engine.py` 中文检测逻辑）
- 600 万记录预期 < 100ms

### 8.2 未来阶段：Elasticsearch（可选，按需启用）

配置文件中有 ES 连接，但 `ES_URL` 为空时自动回退 PG：

```
es_enabled ──True──→ ES search_service (search_service.py)
     ↓ False
   PG AdvancedSearchEngine (fallback)
```

设计要点：
- `search_service.py` 参数与 `AdvancedSearchEngine.search()` 对齐，输出格式一致
- 管道写入 DB 后异步同步到 ES，索引失败只 log 不阻塞
- docker-compose 已有 ES 8.11.0 容器配置，但默认不开启（端口关闭 + healthcheck start_period 30s）

**切换时机：**
- 搜索延迟 > 200ms（当前 1662 行 < 5ms，3M 行预期 < 100ms）
- 需要模糊搜索、同义词扩展、加权排序等高级功能
- 运维团队有余力管理 ES 集群（至少 2GB 内存额外开销）
