Files
backend/docs/02-技术架构设计.md
T

268 lines
8.8 KiB
Markdown
Raw Normal View History

# 技术架构设计
## 一、核心架构决策
### 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 Token15分钟):
{
"sub": "<user_id>",
"tid": "<tenant_id>",
"role": "editor",
"exp": 1680000000
}
Refresh Token30天,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 行 < 5ms3M 行预期 < 100ms
- 需要模糊搜索、同义词扩展、加权排序等高级功能
- 运维团队有余力管理 ES 集群(至少 2GB 内存额外开销)