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 注入修复条目
21 KiB
技术债与架构演进 — 主题综合
日期: 2026-06-25 | 分支: feat/media-library-banner | 阶段: V1 CONDITIONAL GO(上线临门一脚) 视角: 不看上线前 P0/P1(由安全/DevOps 主题覆盖),专攻"架构债"——即代码当前能跑,但 6-12 个月后会成为微服务化/SaaS 化拦路虎的结构性缺陷。 前序: 决策简报
00-INDEX.md(综合 6.9/10,TOP 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 全空或 stub;health: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-01(retry_dead_letters)和 PP-05(claim_next)反复出现的根因不是"忘了接线",而是 ErpModule 没有"后台工作器"这一等公民概念,每个新任务都靠开发者记得在 main.rs 手动 spawn。一次性建立 framework 后,未来新增定时任务(分区维护、归档、留存策略)都有标准路径。
Phases:
- 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防多副本)。 - Phase A2(防回归,0.5 周): 编译期静态断言 + 集成测试:grep
pub async fn retry_dead_letters和pub async fn claim_next必须在 spawn_workers 实现中被引用;扩展CronHeartbeat为CancellationToken统一 graceful shutdown。 - 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:
- Phase B1(FORCE RLS,0.5 周): 新增迁移
m000170遍历所有 tenant_id 业务表ALTER TABLE ... FORCE ROW LEVEL SECURITY;新建非 owner 应用角色app_user(REVOKE 所有 + GRANT CRUD + LOGIN),运行时连接改用 app_user——否则 FORCE 对 owner 无效。 - Phase B2(SET 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 描述(消除文档代码漂移)。 - Phase B3(并发隔离测试,0.5 周): 新增多租户测试 crate(wiki §4.1 规划未实现),
#[tokio::test(flavor="multi_thread", worker_threads=8)]并发 100 个不同 tenant 请求断言零泄漏,覆盖 FHIRallowed_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:
- 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 内部类型。 - Phase C2(trait 裂变,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。 - Phase C3(archlint,0.5 周):
cargo xtask archlint扫描erp-server/src/下是否有跨 crate 业务 import,CI 拦截边界泄漏回归。
- effortEstimate: 3 周
- expectedImpact: 模块具备"可独立部署"属性(微服务化前置)+ 消除 erp-server 业务逻辑认知陷阱 + 死 trait 方法清理降低新人认知负载
- kpis: erp-server 不再 import 业务 entity crate;archlint 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:
- Phase D1(注册表,1 周):
erp-core新增EventDescriptor { event_type, schema_version_range, payload_json_schema, deprecated_since }静态注册表;每个 crate 通过inventorycrate(编译期收集,fallbackbuild.rs代码生成)自动注册本模块发布/消费的事件描述符,生成 consumer manifest。 - 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 草稿(jsonschemacrate 校验,采样率控制 ~0.1ms/事件开销)。 - Phase D3(CI 拦截,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 manifest;CI 零"无消费者"违规;核心事件 JSON Schema 覆盖率 > 60%
- dependencies: 举措 A(spawn_workers 接线后才有真实消费者可 manifest);inventory crate 构建稳定性需验证
举措 E:可演进 schema 与数据生命周期(独立 migrate + 分区自动化 + 物化视图 + 归档)
Rationale: main.rs:233 Migrator::up 在应用启动路径执行(PP-11),破坏性 DDL 在启动瞬间跑,多副本并发迁移会冲突;device_readings 分区硬编码到 2026-08(PP-02 确定性硬截止);stats_service 全是实时 count + date_trunc,Dashboard 随患者量增长拖垮 DB CPU;dead_letter_events / ai_analysis_queue / audit_logs / domain_events 无归档只增不减。这是 V1 上线后 6-12 个月迭代速度的核心瓶颈。
Phases:
- 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-healthspawn_workers新增start_partition_maintenance(每 6hSELECT 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 重试扫描性能)。 - 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。 - Phase E3(独立 migrate 子命令,1 周): 拆出
erp-server migrate子命令,pre-deploy 阶段独立执行迁移,应用启动不再跑Migrator::up;建立 schema 兼容窗口规则——破坏性变更必须分两次发布(v1 加新列双写 / v2 删旧列,expand-contract pattern),CI lint 强制。 - Phase E4(双副本蓝绿,3-4 周,演进式不一步到位): compose + 双副本 + Nginx upstream 切换 + PG 副本(
alerts.yml:70pg_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_06;Dashboard 物化视图刷新延迟 < 10min;破坏性迁移 100% 走 expand-contract;erp-server migrate 独立子命令上线
- dependencies: 举措 A(spawn_workers 注册 partition_maintenance / stats_refresh);双副本引入分布式问题需配套选主;团队接受 expand-contract 两次迁移纪律(CI lint 强制)
四、速赢(1-2 周可落地)
- PP-01 死信重试接线(2-3 天): 在
tasks.rs新增start_dead_letter_retry(每小时,调用retry_dead_letters并 touch heartbeat),与start_event_cleanup对称注册到main.rs;补集成测试;修正 wiki 误记。ROI 极高——代码已实现 90%,差最后 10% 接线,解锁危急值告警/积分发放/预约提醒重试链路。 - PP-02 分区补建迁移(2 天): 立即写
m000170用generate_series补建 2026_09~2027_06 分区(确定性硬截止解除,距今 ~10 周)。pg_partman 自动化可随后跟进,但手动补建是上线前必须项。 - PP-07 FORCE RLS 迁移(1 天): 单个迁移
ALTER TABLE ... FORCE ROW LEVEL SECURITY遍历所有 tenant_id 表(沿用 m000088 DO 块模板),立即堵死 owner-bypass 路径。SET LOCAL 事务改造(举措 B Phase B2)随后跟进。 - register_event_handlers 死方法清理(1 天): deprecate 标注 + 文档迁移到 on_startup,低风险高回报的认知债偿还。
五、主题级风险
- 多副本引入分布式问题: 蓝绿/双副本后,后台任务(举措 A)会重复执行,需 PG advisory lock 选主;session 一致性、token 黑名单(当前 DashMap 非分布式)需迁 Redis。建议举措 A/E4 打包推进。
- trait 裂变是 breaking change: ErpModule trait 裂变需一次性迁移 8 个模块,建议放在 V1 上线后第一个迭代;archlint 误报可能拖慢迭代,需白名单机制。
- SET LOCAL 性能开销: 每请求 BEGIN/COMMIT 约 1-2ms,医疗后台可接受,但 device_readings 高频写入路径需评估;连接池利用率可能下降,需调大 max_connections。
- 加密与搜索契约冲突: 患者姓名加密(PP-12)会破坏
Name.contains(ILIKE '%x%')模糊搜索契约,FHIR handler 依赖此契约。gin(trgm) 索引在加密后失效,必须在加密方案落地前确定搜索降级策略(prefix-only / HMAC 盲索引精确匹配),而非事后补救。 - inventory crate 构建稳定性: 某些构建配置下不稳定,需 fallback 到 build.rs 代码生成;JSON Schema 校验增加每事件 ~0.1ms 开销,需采样率控制。
- 确定性 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-4(PP-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 可校验契约。