2026-07-27 07:59:18 +08:00
|
|
|
|
# 技术架构设计
|
|
|
|
|
|
|
|
|
|
|
|
## 一、核心架构决策
|
|
|
|
|
|
|
|
|
|
|
|
### 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"]
|
2026-07-28 11:02:29 +08:00
|
|
|
|
mesh_cross_include: ["E02.319", "E02.815", "D27.505.954.248"]
|
2026-07-27 07:59:18 +08:00
|
|
|
|
|
|
|
|
|
|
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 内存额外开销)
|