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

21 KiB
Raw Blame History

技术债与架构演进 — 主题综合

日期: 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_lettersmain.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 空 Vecmain.rs:425-673 散落的 start_event_cleanup / start_pool_metrics / start_auto_analysis / start_dialysis_workflow_orchestrator / start_outbox_relay / start_timeout_checker 全部下沉到各模块 spawn_workerserp-health 在 spawn_workers 中以 tokio::time::interval(3600s) 驱动 retry_dead_letterserp-ai 驱动 claim_next 消费循环(每 30s 批量 20 条,SELECT FOR UPDATE SKIP LOCKED 防多副本)。
  2. Phase A2防回归0.5 周): 编译期静态断言 + 集成测试grep pub async fn retry_dead_letterspub async fn claim_next 必须在 spawn_workers 实现中被引用;扩展 CronHeartbeatCancellationToken 统一 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_userREVOKE 所有 + 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.rshandle_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_handlersdeprecate 一版后删除),事件订阅统一在 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_letterhealth.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 周,含确定性硬截止解除): 立即补丁迁移 m000170generate_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_eventsidx_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_tenantREFRESH CONCURRENTLY 每 10 分钟(tasks.rs 新增 start_stats_refreshDashboard 读物化视图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 heartbeatstart_event_cleanup 对称注册到 main.rs;补集成测试;修正 wiki 误记。ROI 极高——代码已实现 90%,差最后 10% 接线,解锁危急值告警/积分发放/预约提醒重试链路。
  2. PP-02 分区补建迁移2 天): 立即写 m000170generate_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.containsILIKE '%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 可校验契约。