# 技术架构设计 ## 一、核心架构决策 ### 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 Token(15分钟): { "sub": "", "tid": "", "role": "editor", "exp": 1680000000 } Refresh Token(30天,Redis存储): - Key: refresh_token: - 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 + `