Files
backend/docs/02-技术架构设计.md
T
34047007@qq.com 73f9468384
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
fix: 第8轮搜索深度审计修复 — 缓存失效、Redis重试、中文标签、翻页稳定性等12项
CRITICAL:
- invalidate_search_cache 清理 atm:* 缓存(MeSH ATM扩展不再用过期结果)
- Pro 方案 api_quota_per_day 1000→10000(修复低于 Free 的数据错误)
- CacheService/RateLimitMiddleware Redis 连接失败60秒自动重试(原永久降级)
- 普通搜索中文输入自动匹配 GlobalTag.name_zh(如"肺癌"通过MeSH标签关联文献)
- AdvancedPubSearchView resolveQuery 添加 seen Set 检测交替 #N 循环引用

HIGH:
- 限速器 _burst_windows 每500请求清理过期条目(防止内存泄漏)
- cron daily_ftp_update 末尾调用 invalidate_search_cache()(自动管道不再用过期缓存)

MEDIUM:
- _apply_order_by ASC 排序加 id tiebreaker(title/journal/first_author翻页跳行/重复)
- _keyset_condition 所有 is_(None) 加 id tiebreaker + __NULL__ 哨兵值
- _field_condition("all") 默认tsvector路径加 journal/journal_iso ILIKE 兜底
- SearchView restoreFromQuery date_preset/year_from/year_to 优先顺序修复

docs: 更新 12/13 搜索文档,移除 CLAUDE.md 陈旧 SQLite 提及
2026-07-28 11:02:29 +08:00

268 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 技术架构设计
## 一、核心架构决策
### 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.954.248"]
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 内存额外开销)