Files
hms/docs/discussions/2026-06-25-analysis/02-architecture.md
iven 3351c68d10 docs: redact Redis 凭据明文 + 系统分析报告 + wiki 关键数字校正
PP-03 凭据泄露处置:
- 清除 wiki + 2 份历史文档中的 Redis 明文密码与公网 IP(4 文件 5 处)
- wiki 新增安全告警 + 症状导航条目
- 核实降级:泄露旧密码已失效,HMS 连本地 Redis,云端闲置;公网已关闭

系统深度分析(9 维度 + 6 主题多专家组):
- docs/discussions/2026-06-25-analysis/ 新增 7 文件
- 综合 6.8/10,4 CRITICAL,TOP 12 痛点,4 阶段路线图

wiki 关键数字校正(PP-02/05a fix 触发):
- 迁移数 175→176(m20260626_000170)
- 症状导航新增 device_readings 分区硬截止 + claim_next 注入修复条目
2026-06-26 09:07:35 +08:00

154 lines
21 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.
# 技术债与架构演进 — 主题综合
> 日期: 2026-06-25 | 分支: feat/media-library-banner | 阶段: V1 CONDITIONAL GO上线临门一脚
> 视角: 不看上线前 P0/P1由安全/DevOps 主题覆盖),专攻"架构债"——即代码当前能跑,但 6-12 个月后会成为微服务化/SaaS 化拦路虎的结构性缺陷。
> 前序: 决策简报 `00-INDEX.md`(综合 6.9/10TOP 5 痛点含 PP-01/PP-02/PP-07本章节聚焦"结构性演进"而非"上线就绪度"。
> 证据口径: 所有论断附文件路径:行号或 grep 结果,已逐项核验。
---
## 一、主题愿景(专家共识收敛)
HMS 的架构骨架L1/L2/L3 分层 + Outbox 事件总线 + 双层多租户 + ErpModule trait是全系统最扎实的资产决策简报后端架构 8.0 分),**但当前"模块化单体"已部分名义化**:模块边界在组装层被绕过、事件契约仅靠文档约定、后台任务无声明式注册入口、多租户隔离依赖"竞态防护"的反模式、数据库 schema 演进无兼容窗口纪律。这些债现在不疼,但 6-12 个月后微服务化/SaaS 化启动时,**会从"重构"退化为"重写"**。
**主题愿景:** 在保持业务迭代速度的同时,把五类结构性缺陷(事件契约 / 模块边界 / 后台任务 / 多租户隔离 / schema 演进)从"文档约定 + 人肉记忆"升级为"编译期/CI 可校验的类型契约 + 机制层防护",为 2-3 年后的微服务化打下"可演进而非可重写"的架构地基。核心判断标准:**这笔债如果现在不还,拆服务当天是要重写而不是重构**。
---
## 二、专家提案摘要与交叉验证
三位专家(首席架构师 / 后端架构师 / 数据架构师)独立提案,**核心诊断高度一致**,差异仅在实施手法与优先级排序。已核验的关键证据:
| 诊断项 | 专家共识 | 代码证据(已核验) | alreadyKnown |
|--------|---------|-------------------|--------------|
| 死信重试未接线PP-01 | 三人一致认定是"缺少任务注册框架"症状,非单点 bug | `erp-core/events.rs:382` 定义 `retry_dead_letters``main.rs:425-673` + `tasks.rs` 全部 spawn 点无调用 | V2 审计已识别为痛点,但"根因是缺 framework"是新视角 |
| AI 队列死存储PP-05 | claim_next 消费循环未接线,与 PP-01 同源 | 与 PP-01 共享"无 TaskRegistry"根因 | 已识别,归入半成品自动化主题 |
| RLS 无 FORCE + SET 串扰PP-07/PP-08 | 三人一致:是同一缺陷两面,须合并修 | grep `FORCE ROW LEVEL SECURITY` 全仓 **0 命中**`tenant_rls.rs:31` 共享池 `SET app.current_tenant_id`:44 `RESET` | 已识别,但"SET LOCAL + 事务作用域"是机制层新解法 |
| 分区硬编码PP-02 | 确定性硬截止,优先级高于概率性 bug | `m000073:43-46` 硬编码 2026_05~2026_08启动路径外无自动建分区 | 已识别pg_partman + 应用层兜底是增量 |
| 事件 schema 无版本治理 | EVENT_SCHEMA_VERSION="v1" 仅写入 payload无消费者校验 | `events.rs:67` `pub const EVENT_SCHEMA_VERSION: &str = "v1"` | **新发现**(历史只规定命名规范,未约束 schema 演进) |
| ErpModule trait 名实不符 | register_event_handlers 8 模块中 4 空壳 | `module.rs:69` default `{}`auth/config/core/message 全空或 stubhealth:408 空函数体 | **新发现**(认知债,非功能 bug |
| 组装层边界泄漏 | dialysis 业务编排下沉到 erp-server | `erp-server/src/dialysis_workflow.rs` 直接订阅 `dialysis.record.created` 并编排 BPMN违反 §1.3 L2 零直接依赖 | **新发现**(架构铁律被默默绕过) |
---
## 三、战略举措(归并为 5 项)
### 举措 A后台任务声明式注册TaskRegistry + spawn_workers— 偿还"未接线死代码"债
**Rationale** PP-01retry_dead_letters和 PP-05claim_next反复出现的根因不是"忘了接线",而是 ErpModule 没有"后台工作器"这一等公民概念,每个新任务都靠开发者记得在 `main.rs` 手动 spawn。一次性建立 framework 后,未来新增定时任务(分区维护、归档、留存策略)都有标准路径。
**Phases**
1. **Phase A1接线1 周):** `erp-core/src/module.rs` 的 ErpModule trait 新增 `async fn spawn_workers(&self, ctx) -> AppResult<Vec<JoinHandle<()>>>`default 空 Vec`main.rs:425-673` 散落的 `start_event_cleanup / start_pool_metrics / start_auto_analysis / start_dialysis_workflow_orchestrator / start_outbox_relay / start_timeout_checker` 全部下沉到各模块 `spawn_workers`erp-health 在 spawn_workers 中以 `tokio::time::interval(3600s)` 驱动 `retry_dead_letters`erp-ai 驱动 `claim_next` 消费循环(每 30s 批量 20 条,`SELECT FOR UPDATE SKIP LOCKED` 防多副本)。
2. **Phase A2防回归0.5 周):** 编译期静态断言 + 集成测试grep `pub async fn retry_dead_letters``pub async fn claim_next` 必须在 spawn_workers 实现中被引用;扩展 `CronHeartbeat``CancellationToken` 统一 graceful shutdown。
3. **Phase A3可观测0.5 周):** `/health/tasks` 端点暴露每个任务 `last_heartbeat / max_expected_interval`,对接 Alertmanager "任务卡死"告警。
- **effortEstimate** 2 周(含测试 + graceful shutdown 改造)
- **expectedImpact** 兑现"主动关怀引擎"承诺(死信不再永久滞留)+ 消除 2 处死代码认知污染 + 为未来 HA 多副本选主铺路
- **kpis** retry_dead_letters 每小时执行且 heartbeat 可观测claim_next 队列积压 < 100死信表 resolved_at 非空率 > 95%
- **dependencies** PP-04 可观测性三件套Alertmanager配合多副本部署时需 PG advisory lock 选主Phase C 蓝绿引入时)
### 举措 B多租户隔离机制层根治FORCE RLS + SET LOCAL 事务作用域)— 偿还"竞态防护负价值"债
**Rationale** 三位专家一致裁定PP-07无 FORCE和 PP-08共享池 SET/RESET 串扰)是同一架构缺陷两面——"数据库层做应用层的事"SET 会话变量)和"应用层依赖数据库兜底但没真兜底"(无 FORCE互相强化。安全专家"多一层防护更安全"的直觉在此是**负价值**SET/RESET 在共享池上的毫秒级窗口产生的间歇性泄漏比"无 RLS 兜底"更危险,因为它制造了"有防护"的假象。正确做法是机制层消除窗口,而非"小心地 RESET"。
**Phases**
1. **Phase B1FORCE RLS0.5 周):** 新增迁移 `m000170` 遍历所有 tenant_id 业务表 `ALTER TABLE ... FORCE ROW LEVEL SECURITY`;新建非 owner 应用角色 `app_user`REVOKE 所有 + GRANT CRUD + LOGIN运行时连接改用 app_user——否则 FORCE 对 owner 无效。
2. **Phase B2SET LOCAL 事务作用域1.5 周):** 废弃 `tenant_rls.rs:31` 共享池 `SET app.current_tenant_id` 反模式;重构中间件为 `db.transaction(|txn| async { txn.execute(SET LOCAL app.current_tenant_id = $1); next.run(req).await })`——`SET LOCAL` 作用域严格限于事务内commit/rollback 后自动消失,物理上不可能被连接复用读到。同步修订 wiki §4 RLS 描述(消除文档代码漂移)。
3. **Phase B3并发隔离测试0.5 周):** 新增多租户测试 cratewiki §4.1 规划未实现),`#[tokio::test(flavor="multi_thread", worker_threads=8)]` 并发 100 个不同 tenant 请求断言零泄漏,覆盖 FHIR `allowed_patient_ids` 复杂范围过滤端点。
- **effortEstimate** 2.5 周
- **expectedImpact** 从机制层消灭跨租户泄漏窗口(医疗 SaaS 合规红线)+ 为读写分离/PgBouncer transaction pooling 铺路SET LOCAL 是唯一可用租户上下文传递方式)
- **kpis** 并发隔离测试 100 线程零泄漏FORCE RLS 覆盖所有 79 业务表;连接池 max_connections 调优后无等待超时
- **dependencies** 连接池利用率评估(事务包裹增加连接占用时长,可能需调大 max_connections只读端点统一事务路径回归测试
- **调和分歧:** 采纳首席架构师 A 方案 + 后端/数据架构师的 SET LOCAL——不删除 RLS 中间件(保留 tenant_id 注入 extension但删除共享池 SET/RESET改为事务内 SET LOCAL。这是机制层根治安全专家"多一层防护"诉求由 FORCE RLS 兜底满足,竞态窗口由 SET LOCAL 消除。
### 举措 C模块边界复位Saga 下沉 + ErpModule trait 裂变)— 偿还"组装层业务泄漏"债
**Rationale** `erp-server/src/dialysis_workflow.rs` 在 L3 组装层承载透析业务编排,违反 §1.3"L2 间零直接依赖"铁律。ErpModule trait 11 个方法中 `register_event_handlers` 在 8 模块里 4 空壳,是典型的"接口名实不符"——新人会误以为事件注册走这个方法。这两点是微服务化时最痛的债:如果 erp-dialysis 核心逻辑物理上无法脱离 erp-server 运行,"按模块拆服务"就是空话。
**Phases**
1. **Phase C1编排下沉1.5 周):** `erp-core` 定义 `trait DomainSaga { fn name(); async fn handle(&event, &ctx); }` + `SagaContext { db, event_bus }`;把 `dialysis_workflow.rs``handle_dialysis_record_created` 整体迁移到 `erp-dialysis/src/sagas/dialysis_session_saga.rs`,由 erp-dialysis 在 `on_startup` 自注册到 SagaRegistry同理 erp-health 的"告警→AI→推送"链路、erp-ai 的"化验上传→解读→推送"链路各归其位。**验收:** `erp-server/Cargo.toml` 不再直接依赖业务 entity crate 内部类型。
2. **Phase C2trait 裂变1 周):** 废弃 `register_event_handlers`deprecate 一版后删除),事件订阅统一在 `on_startup`;替换为 `fn capabilities(&self) -> &'static [ModuleCapability]` 显式能力声明HttpRoutes / EventConsumer / EventPublisher / BackgroundWorker按需 impl 而非大杂烩 trait。可选把单一 trait 裂变为 LifecycleModule / RoutingModule / EventModule首席架构师提案但为控制 breaking change 范围,建议先做能力声明,裂变留待 V2。
3. **Phase C3archlint0.5 周):** `cargo xtask archlint` 扫描 `erp-server/src/` 下是否有跨 crate 业务 importCI 拦截边界泄漏回归。
- **effortEstimate** 3 周
- **expectedImpact** 模块具备"可独立部署"属性(微服务化前置)+ 消除 erp-server 业务逻辑认知陷阱 + 死 trait 方法清理降低新人认知负载
- **kpis** erp-server 不再 import 业务 entity cratearchlint CI 零违规register_event_handlers 调用点清零
- **dependencies** Saga 间状态机/补偿事务本次只做"位置下沉"不引入完整 Saga 框架避免过度设计erp-dialysis 若需依赖 erp-workflow entity通过 erp-core trait 反向依赖反转
### 举措 D事件契约治理Schema 注册表 + Consumer Manifest + CI 校验)— 偿还"v1 永冻"债
**Rationale** 51 个事件类型声明在 `docs/event-registry.md`,但代码侧无强约束。`events.rs:67` 的 schema_version 仅写入 payload无消费者校验。这正是 PP-01/PP-05事件无消费者/死存储)反复出现的元根因——没有编译期/CI 约束,全靠人记忆。微服务化前不补这课,拆服务当天就要停机重写(微服务间事件 schema 演进是分布式系统头号痛点)。
**Phases**
1. **Phase D1注册表1 周):** `erp-core` 新增 `EventDescriptor { event_type, schema_version_range, payload_json_schema, deprecated_since }` 静态注册表;每个 crate 通过 `inventory` crate编译期收集fallback `build.rs` 代码生成)自动注册本模块发布/消费的事件描述符,生成 consumer manifest。
2. **Phase D2消费校验1 周):** `consume_with_retry` 入口按 event_type 查表schema_version 做语义版本范围匹配(`^v1` 兼容 v1.x不匹配事件进新表 `incompatible_events` 并告警(不进 dead_letter`health.patient.created` / `health.alert.triggered` / `article.published` 等核心事件补 JSON Schema 草稿(`jsonschema` crate 校验,采样率控制 ~0.1ms/事件开销)。
3. **Phase D3CI 拦截0.5 周):** `cargo run --bin verify-event-contracts` 校验每个被 enqueue 的事件必须有至少一个 consumer manifest直接拦截 PP-05 类死存储回归)。
- **effortEstimate** 2.5 周
- **expectedImpact** 从文档约定升级为编译期可校验的类型契约 + CI 直接拦截"事件无消费者"回归 + 为微服务间 schema 演进expand-contract打地基
- **kpis** 51 事件类型 100% 有 consumer manifestCI 零"无消费者"违规;核心事件 JSON Schema 覆盖率 > 60%
- **dependencies** 举措 Aspawn_workers 接线后才有真实消费者可 manifestinventory crate 构建稳定性需验证
### 举措 E可演进 schema 与数据生命周期(独立 migrate + 分区自动化 + 物化视图 + 归档)
**Rationale** `main.rs:233` `Migrator::up` 在应用启动路径执行PP-11破坏性 DDL 在启动瞬间跑多副本并发迁移会冲突device_readings 分区硬编码到 2026-08PP-02 确定性硬截止stats_service 全是实时 count + date_truncDashboard 随患者量增长拖垮 DB CPUdead_letter_events / ai_analysis_queue / audit_logs / domain_events 无归档只增不减。这是 V1 上线后 6-12 个月迭代速度的核心瓶颈。
**Phases**
1. **Phase E1分区自愈 + 死信索引1 周,含确定性硬截止解除):** 立即补丁迁移 `m000170``generate_series` 动态补建 2026_06 起未来 12 个月分区(模板提取自 `m000073:42-55`);安装 pg_partman 5.x + `part_config(premake=3, infinite_time_partitions=true)`erp-health `spawn_workers` 新增 `start_partition_maintenance`(每 6h `SELECT partman.run_maintenance('device_readings')` 应用层兜底,防 pg_cron 缺失)。同步给 `dead_letter_events``idx_dead_letter_unresolved ON (tenant_id) WHERE resolved_at IS NULL` + `idx_dead_letter_created`(配合举措 A 重试扫描性能)。
2. **Phase E2物化视图 + 归档1.5 周):** stats 高频查询建物化视图 `mv_patient_stats_by_tenant` / `mv_consultation_stats_by_tenant``REFRESH CONCURRENTLY` 每 10 分钟(`tasks.rs` 新增 `start_stats_refresh`Dashboard 读物化视图UI 标注"数据更新于 X 分钟前"管理预期。归档函数 `archive_old_rows(table, days)` 把 > 180 天 resolved/completed 数据迁到 `_archive` 表(医疗病历留存 15 年合规要求,冷数据不删除只迁移);明确热(<30 天)/温30-180 天)/冷(>180 天)三级生命周期写入 `wiki/database.md`
3. **Phase E3独立 migrate 子命令1 周):** 拆出 `erp-server migrate` 子命令pre-deploy 阶段独立执行迁移,应用启动不再跑 `Migrator::up`;建立 schema 兼容窗口规则——破坏性变更必须分两次发布v1 加新列双写 / v2 删旧列expand-contract patternCI lint 强制。
4. **Phase E4双副本蓝绿3-4 周,演进式不一步到位):** compose + 双副本 + Nginx upstream 切换 + PG 副本(`alerts.yml:70` pg_replication_lag 告警已定义但无副本);配合举措 A 的 PG advisory lock 选主。**异见采纳:** 医疗 SaaS 早期团队(<10 人K8s 运维成本超过收益compose + 双副本拿 80% HA/回滚收益用 20% 复杂度。
- **effortEstimate** 6.5 周E1-E3 可独立交付E4 演进式)
- **expectedImpact** 解除 2026-09-01 确定性硬截止 + Dashboard DB CPU 降 50%+ + 破坏性迁移可安全回滚 + 为多租户 SaaS 报表打地基
- **kpis** device_readings 分区自动维护至 2027_06Dashboard 物化视图刷新延迟 < 10min破坏性迁移 100% 走 expand-contracterp-server migrate 独立子命令上线
- **dependencies** 举措 Aspawn_workers 注册 partition_maintenance / stats_refresh双副本引入分布式问题需配套选主团队接受 expand-contract 两次迁移纪律CI lint 强制)
---
## 四、速赢1-2 周可落地)
1. **PP-01 死信重试接线2-3 天):**`tasks.rs` 新增 `start_dead_letter_retry`(每小时,调用 `retry_dead_letters` 并 touch heartbeat`start_event_cleanup` 对称注册到 `main.rs`;补集成测试;修正 wiki 误记。**ROI 极高**——代码已实现 90%,差最后 10% 接线,解锁危急值告警/积分发放/预约提醒重试链路。
2. **PP-02 分区补建迁移2 天):** 立即写 `m000170``generate_series` 补建 2026_09~2027_06 分区(确定性硬截止解除,距今 ~10 周。pg_partman 自动化可随后跟进,但手动补建是上线前必须项。
3. **PP-07 FORCE RLS 迁移1 天):** 单个迁移 `ALTER TABLE ... FORCE ROW LEVEL SECURITY` 遍历所有 tenant_id 表(沿用 m000088 DO 块模板),立即堵死 owner-bypass 路径。SET LOCAL 事务改造(举措 B Phase B2随后跟进。
4. **register_event_handlers 死方法清理1 天):** deprecate 标注 + 文档迁移到 on_startup低风险高回报的认知债偿还。
---
## 五、主题级风险
1. **多副本引入分布式问题:** 蓝绿/双副本后,后台任务(举措 A会重复执行需 PG advisory lock 选主session 一致性、token 黑名单(当前 DashMap 非分布式)需迁 Redis。建议举措 A/E4 打包推进。
2. **trait 裂变是 breaking change** ErpModule trait 裂变需一次性迁移 8 个模块,建议放在 V1 上线后第一个迭代archlint 误报可能拖慢迭代,需白名单机制。
3. **SET LOCAL 性能开销:** 每请求 BEGIN/COMMIT 约 1-2ms医疗后台可接受但 device_readings 高频写入路径需评估;连接池利用率可能下降,需调大 max_connections。
4. **加密与搜索契约冲突:** 患者姓名加密PP-12会破坏 `Name.contains`ILIKE '%x%')模糊搜索契约FHIR handler 依赖此契约。gin(trgm) 索引在加密后失效必须在加密方案落地前确定搜索降级策略prefix-only / HMAC 盲索引精确匹配),而非事后补救。
5. **inventory crate 构建稳定性:** 某些构建配置下不稳定,需 fallback 到 build.rs 代码生成JSON Schema 校验增加每事件 ~0.1ms 开销,需采样率控制。
6. **确定性 vs 概率性优先级分歧:** 数据架构师主张 PP-02确定性硬截止优先级高于一切概率性 bug其他专家按影响面排序。**裁定:** Phase 0 两者都做(速赢 1+2 并行),不排序。
---
## 六、调和专家分歧后的最终取舍
| 分歧点 | 安全/DevOps 立场 | 首席/后端/数据架构师立场 | 最终取舍 |
|--------|-----------------|----------------------|---------|
| tenant_rls SET 逻辑 | "多一层防护更安全,保留 SET" | "竞态防护是负价值SET/RESET 窗口比无兜底更危险" | **采纳机制层根治:** FORCE RLS 兜底 + SET LOCAL 事务作用域,删除共享池 SET/RESET。安全诉求由 FORCE 满足,竞态由 SET LOCAL 消除 |
| CD 工具选型 | "直接上 Kubernetes/ArgoCD" | "K8s 运维成本超过收益compose + 双副本够用" | **采纳演进式:** compose + 双副本 + 独立 migrate拿 80% 收益用 20% 复杂度。真正难的是 schema 兼容窗口纪律expand-contract与工具无关 |
| 事件 schema 注册表 | "过度工程,补测试更重要" | "PP-01/PP-05 反复出现的元根因,微服务化前必修" | **采纳注册表:** 但分阶段(注册表 → 消费校验 → CI 拦截),先解决"无消费者"回归JSON Schema 全覆盖可渐进 |
| Repository trait 层 | — | 后端架构师主张抽出(消灭漏 tenant_id 再生土壤) | **暂缓:** 工作量大16+ 处 begin/commit 迁移),且当前 SeaORM Filter + 举措 B 机制层防护已大幅降低泄漏风险。列为 V2 候选先做试点appointment/consultation/follow_up验证抽象价值 |
| trait 裂变 vs 能力声明 | — | 首席主张三 trait 裂变,后端主张能力声明 | **采纳能力声明优先:**`capabilities()` 显式声明 + deprecate 死方法,裂变留待 V2控制 breaking change 范围 |
---
## 七、路线图(与决策简报 Phase 对齐)
| Phase | 时窗 | 本主题举措 | 交付价值 |
|-------|------|-----------|---------|
| Phase 0 护航 | 0-2 周 | 速赢 1-4PP-01 接线 / PP-02 分区补建 / PP-07 FORCE RLS / 死方法清理) | 消灭确定性硬截止 + 兑现死信重试承诺 + 堵死 owner-bypass |
| Phase 1 稳固 | 1-3 月 | 举措 A 全量 + 举措 B 全量 + 举措 D Phase D1-D2 | 后台任务可观测 + 多租户机制层根治 + 事件契约注册表 |
| Phase 2 深化 | 3-6 月 | 举措 C 全量 + 举措 D Phase D3 + 举措 E Phase E1-E3 | 模块边界复位 + CI 拦截无消费者 + schema 兼容窗口纪律 |
| Phase 3 规模化 | 6-12 月 | 举措 E Phase E4双副本蓝绿+ Repository trait 试点 | 多副本 HA + 数据访问契约接缝(微服务化前置) |
---
> 本章节所有论断已附代码证据(文件:行号或 grep 结果)。核心架构债(事件契约 / 模块边界 / 后台任务 / 多租户隔离 / schema 演进)现在偿还成本是"重构级"6-12 个月后微服务化启动时将退化为"重写级"。决策层建议Phase 0 速赢立即启动Phase 1-2 把五类结构性缺陷升级为编译期/CI 可校验契约。