OncoLit: a multi-tenant oncology literature search, feed, and collaboration platform. Built with FastAPI + Vue 3 + PostgreSQL. Includes PubMed pipeline, drug approvals, AI summaries, and systematic review tools.
8.8 KiB
8.8 KiB
技术架构设计
一、核心架构决策
1.1 配置驱动:一套代码 = 一个垂直科室
每个专科(肿瘤/心血管/神经...)是独立部署实例。一套代码通过配置文件切换专科,实例间完全隔离。
# 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 代码实现
# 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 内存额外开销)