diff --git a/docs/design/miniprogram/design-system.html b/docs/design/miniprogram/design-system.html new file mode 100644 index 0000000..6055acc --- /dev/null +++ b/docs/design/miniprogram/design-system.html @@ -0,0 +1,317 @@ + + + + + +HMS 设计系统 · 温润东方风 + + + + + + + + + +
+ + + diff --git a/docs/design/miniprogram/preview.html b/docs/design/miniprogram/preview.html new file mode 100644 index 0000000..5178054 --- /dev/null +++ b/docs/design/miniprogram/preview.html @@ -0,0 +1,346 @@ + + + + +HMS 小程序设计预览 · 温润东方风 + + + + + + + +
+ + + diff --git a/docs/design/web/preview.html b/docs/design/web/preview.html new file mode 100644 index 0000000..18bf128 --- /dev/null +++ b/docs/design/web/preview.html @@ -0,0 +1,319 @@ + + + + +HMS Web 端重设计 · 温润东方风 + + + + + + + +
+ + + diff --git a/docs/designs/hms-miniprogram-mockup.html b/docs/designs/hms-miniprogram-mockup.html new file mode 100644 index 0000000..b243861 --- /dev/null +++ b/docs/designs/hms-miniprogram-mockup.html @@ -0,0 +1,732 @@ + + + + + +HMS 健康管理小程序原型 + + + +
+ + +
+
+ + + + +
+
+9:41 +
+ +
+ +
+
+
+ +
早上好,张明
2026年4月23日 星期四
+
+ +
+
+ +
+
今日健康
+
+
血压
128/82
mmHg · 偏高
+
心率
72
bpm · 正常
+
血糖
5.6
mmol/L · 正常
+
体重
68.5
kg · 稳定
+
+
+ +
+
快捷服务
+
+
录数据
+
预约
+
报告
+
咨询
+
+
+ +
+
明日值班
+
+ +
王建国 主任医师
肾内科 · 明天 09:00
+已确认 +
+
+ +
+
待办随访
+
+
+
+
血压监测记录
今日到期
+
+今日 +
+
+
+
+
健康问卷调查
3天后到期
+
+3天后 +
+
+
+ +
+
首页
+
数据
+
预约
+
我的
+
+
+ + +
+
9:41
+ +
+ +
+
血压
+
血糖
+
体重
+
+ +
+
7日趋势
+ + + + +14512510585 + + + +17日18日19日20日21日22日23日 + + + + + + + +128 +82 + +
收缩压舒张压正常范围
+
+ +
+
记录数据
+
+
+
+
+
+
+ +
+ +
+
历史记录
+
+
128/82 mmHg · 72 bpm
今天 08:30
+
125/78 mmHg · 70 bpm
昨天 07:45
+
132/85 mmHg · 75 bpm
4/21 08:15
+
118/76 mmHg · 68 bpm
4/20 07:50
+
130/84 mmHg · 74 bpm
4/19 08:00
+
+
+
+
+
首页
+
数据
+
预约
+
我的
+
+
+ + +
+
9:41
+ +
+ +
+ +
+全部 +肾内科 +心内科 +内分泌 +全科 +
+ +
+
+ +
+
王建国 主任医师
+
肾内科
+
慢性肾病透析管理
+
★ 4.9 · 接诊 1,286 人
+
+
+
+
+
+ +
+
李芳 副主任医师
+
心内科
+
高血压冠心病
+
★ 4.8 · 接诊 986 人
+
+
+
+
+
+ +
+
赵明辉 主治医师
+
内分泌科
+
糖尿病甲状腺
+
★ 4.7 · 接诊 756 人
+
+
+
+ +
+
选择预约时间 · 王建国
+
+
周四
24
+
周五
25
+
周六
26
+
周日
27
+
周一
28
+
+
选择时段
+
+
上午 09:00
+
下午 14:00
+
+ +
+
+
+
首页
+
数据
+
预约
+
我的
+
+
+ + +
+
9:41
+ +
+
+
化验报告
+
体检记录
+
+
+
+
肾功能检查
2026-04-20 · 市第一医院
2项异常
+
正常 8 项,异常 2 项
+
肌酐 156 μmol/L (参考 44-133)
尿素氮 9.8 mmol/L (参考 2.5-7.1)
+
+
+
血常规
2026-04-15 · 仁爱体检中心
全部正常
+
正常 12 项,异常 0 项
+
+
+
尿常规
2026-04-10 · 市第一医院
1项异常
+
正常 9 项,异常 1 项
+
+ +
+
肾功能检查详情已出
+
2026-04-20 · 市第一医院检验科
+
+
指标
结果
单位
参考值
+
肌酐
156
μmol/L
44-133
+
尿素氮
9.8
mmol/L
2.5-7.1
+
尿酸
386
μmol/L
150-420
+
eGFR
72
ml/min
>60
+
+
+
医生解读
+
肌酐和尿素氮轻度升高,建议控制蛋白质摄入,两周后复查肾功能。eGFR 72 处于轻度下降阶段,需持续关注。
+
—— 王建国 主任医师
+
+
+
+
+
首页
+
数据
+
预约
+
我的
+
+
+ + +
+
9:41
+ +
+ +
+
+
+
+
+
王建国 肾内科
09:30
+
您好,我最近血压波动比较大,想咨询一下...
+
+
3
+
+
+
+
+
+
+
李芳 心内科
昨天
+
好的,我明白了。建议您按时服药并...
+
+
+
+ +
+
以下为与王建国医生的对话预览
+ +
+ +
您好张先生,请问最近血压控制得怎么样?有按时服药吗?
09:30
+
+ +
+
王医生好,最近血压波动比较大,早上量了一次128/82,有时会到140/90
09:32
+
+ +
+ +
了解,您的血压确实有些波动。建议:1. 每天固定时间测血压并记录 2. 低盐饮食 3. 我帮您调整一下用药方案,请明天来门诊
09:35
+
+ +
系统提示:已为您预约明天 09:00 王建国医生门诊
+ +
+ + +
+
+
+
+
+
首页
+
数据
+
预约
+
我的
+
+
+ + + + +
+
9:41
+ +
+ +
+
+
+ +
王建国
肾内科 · 主任医师
+
+
在线
+
+
+ +
+
+
12
待随访
+
8
今日预约
+
5
待回复咨询
+
3
逾期任务
+
+
+ +
今日任务
+
+
+
+
张明 · 电话随访
血压监测随访 · 10:00
+待处理 +
+
+
+
+
+
刘芳 · 门诊预约
肾功能复查 · 11:00
+已确认 +
+
+
+
+
+
李强 · 在线咨询
用药咨询 · 14:00
+待回复 +
+
+
+
+
+
赵婷 · 面访随访
术后随访 · 16:00
+已完成 +
+
+
+
+
+
工作台
+
患者
+
排班
+
我的
+
+
+ + +
+
9:41
+ +
+
+
+全部 +高血压 +糖尿病 +慢性肾病 +术后随访 +
+ +
+
+
+
+
张明52岁
+
高血压慢性肾病
+
主治:王建国 · 上次随访 4/18
+
+ +
+
+
+
+
+
+
刘芳45岁
+
糖尿病术后随访
+
主治:王建国 · 上次随访 4/20
+
+ +
+
+
+
+
+
+
李强68岁
+
高血压冠心病
+
主治:李芳 · 上次随访 4/15
+
+ +
+
+
+
+
+
+
赵婷38岁
+
术后随访
+
主治:王建国 · 上次随访 4/22
+
+ +
+
+
+
+
+
+
陈华55岁
+
慢性肾病高血压
+
主治:王建国 · 已停用
+
+ +
+
+
+ +
+
+
+
+
工作台
+
患者
+
排班
+
我的
+
+
+ + +
+
9:41
+ +
+ +
+
+
+
张明男 · 52岁
+
高血压慢性肾病
+
+
+ +
+
任务信息
+
+
随访类型电话随访
+
计划日期2026-04-23
+
状态进行中
+
关联预约4/24 王建国门诊
+
+
+ +
+
随访记录
+ +
+
随访结果
+
+ + + + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
附件
+
+ +
点击添加附件
+
+
+
+ +
+ + +
+
+
+ + +
+
9:41
+ +
+
+ +
张明
+
ID: HMS20260001 · 已实名
+
+
+
健康档案
+
家庭成员
+
隐私设置
+
关于我们
+
+
+
+
首页
+
数据
+
预约
+
我的
+
+
+ + +
+
9:41
+ +
+
排班管理页面
功能开发中
+
+
+
工作台
+
患者
+
排班
+
我的
+
+
+ +
+
9:41
+ +
+
+ +
王建国
+
肾内科 · 主任医师
+
+
+
个人信息
+
设置
+
退出登录
+
+
+
+
工作台
+
患者
+
排班
+
我的
+
+
+ +
+ + + diff --git a/docs/discussions/2026-04-26-platform-retrospective-and-evolution.md b/docs/discussions/2026-04-26-platform-retrospective-and-evolution.md new file mode 100644 index 0000000..86fbccf --- /dev/null +++ b/docs/discussions/2026-04-26-platform-retrospective-and-evolution.md @@ -0,0 +1,152 @@ +# HMS 平台基座回顾与演进 — 多专家评审讨论 + +> 日期: 2026-04-26 | 参与者: 用户 + AI (三专家视角评审) + +## 背景 + +HMS 健康管理平台经过 17 天密集开发(4/10-4/26),从 ERP 底座演进到包含 16 个 Rust crate、62 个前端页面、27 个小程序页面的综合医疗 SaaS 平台。用户希望对项目的迭代开发过程进行回顾总结,验证基座设计是否合理,并讨论后续演进方向。 + +## 讨论要点 + +### 1. 项目演进脉络 + +- **Phase 1-6(4/10-4/16)**:ERP 底座搭建 — core/auth/config/workflow/message 全部原生 Rust 模块 +- **WASM 插件实验(4/13-4/18)**:设计并实现插件系统,落地 CRM/Inventory/Freelance/ITOps 4 个插件 +- **HMS 分叉(4/23-4/26)**:健康模块因 5 个硬限制(强类型/加密/文件/后台任务/外部 API)选择原生开发 +- **快速迭代**:18+ 健康实体、AI 模块、微信小程序、按钮级权限控制 + +### 2. 基座设计验证 + +**验证通过的设计:** +- 星形依赖拓扑(零循环依赖) +- ErpModule trait 统一接口(生命周期/权限/事件/健康检查) +- 事件总线基础设施(broadcast + outbox 持久化 + 前缀过滤) +- JWT → TenantContext 多租户全链路贯通 +- ModuleRegistry 拓扑排序(Kahn 算法 + 循环检测) + +**存在问题的设计:** +- 事件消费侧不完整(13 个事件只有 3 个被消费) +- 路由手动合并(不在 trait 中,每个新模块需手动接线) +- erp-message 全量订阅(性能隐患) +- 事件注册双路径(`register_event_handlers` vs `on_startup`) + +### 3. 四个关键张力 + +#### 张力 1:双轨并行 — 插件 vs 原生 + +两条截然不同的模块开发范式并存,没有明确的判断框架。 + +**插件路径**:plugin.toml → Guest trait → WASM → 自动路由/动态表/沙盒隔离 +**原生路径**:Rust crate → ErpModule trait → 手动路由/强类型/完全能力 + +结论:HMS 核心业务(健康/AI/透析/积分)全部需要原生,插件只适用于 CRUD 密集型通用 ERP 模块。 + +#### 张力 2:事件消费缺口 + +erp-health 发布 13 种事件,消费侧严重不足: +- `health_data.critical_alert` 无消费者(危急体征无人响应) +- `follow_up.overdue` 无消费者(逾期随访无催办) +- `patient.created/updated`、`lab_report.uploaded`、`consultation.opened/closed` 等全部空转 + +#### 张力 3:租户隔离最后一公里 + +应用层隔离完整但缺兜底: +- 无 PostgreSQL RLS policy +- 无强制 tenant_id 过滤机制 +- 微信登录硬编码 default_tenant_id + +#### 张力 4:健康模块复杂性 + +erp-health 已成为子平台:加密子系统、脱敏管道、积分体系、AI 数据提供者、后台调度、小程序 API。积分系统(8 实体/12+ 路由)不属于健康模块。 + +### 4. 三专家评审 + +#### 专家 1:高级系统架构师 + +- 诊断准确度 7/10,优先级有偏差 +- 发现 EventBus 无重放机制(服务重启丢事件)、overdue 事件无幂等保护 +- RLS 不是 P0,多租户集成测试才是 +- 积分系统不应在 health 内 +- 核心原则:先补测试再重构,先修事件再上功能,先验证再加固 + +#### 专家 2:医疗信息化专家 + +发现比原始诊断更深层的风险: +- 危急值阈值全部硬编码(不可配置,无法适应不同科室) +- `daily_monitoring` 表体征数据不经过危急值检测(合并遗留问题) +- 过敏史更新直接覆盖,无变更历史 +- 知情同意完全缺失(搜索 consent/同意/授权/隐私 零结果,违反 PIPL 第29条) +- 只有身份证号加密,姓名/过敏史/诊断/咨询内容明文 +- 审计日志不完整(只有预约状态变更记录前后值) +- ip_address 和 user_agent 从未被填充 +- 读操作完全没有审计记录 + +#### 专家 3:产品策略专家 + +- 17 天 237 提交不可持续但不必恐慌,fix 提交占 21.6% +- 41% Rust 代码在插件系统,对核心业务贡献接近零(最大 ROI 失衡) +- 单人+AI 的"速度幻觉":68 提交/天 = 审查不足 +- 测试正确水位:关键路径 50-80 用例,3-4 天投入 +- V2 血透路线图:技术储备够,但缺市场验证,建议先做 3-5 家客户调研 + +### 5. 三专家共识 + +| 共识 | 说明 | +|------|------| +| 危急值告警闭环是 P0 | 三方一致,错误的血糖阈值可致患者昏迷 | +| 知情同意缺失是法律红线 | PIPL 违规可罚 5000 万 | +| 积分系统不属于 health | 三方独立得出相同结论 | +| 测试覆盖是所有后续工作的前提 | 先有测试才能放心重构 | +| 插件系统应冻结 | 保留代码,不再投入 | +| EventBus 可靠性需增强 | 无重放 + 无幂等 | + +### 6. 重新排序的优先级 + +**P0(2-3 周,上线前必修):** +1. 危急值告警消费者(1天) +2. 危急值阈值可配置化(2天) +3. daily_monitoring 合并后告警验证(1天) +4. 随访逾期通知 + 幂等保护(1天) +5. 知情同意记录(3天) +6. 审计日志补全(3天) +7. EventBus 持久化增强(2天) + +**P1(2-4 周,治理):** +8. 积分系统剥离(5天) +9. 关键路径测试 50-80 用例(4天) +10. 插件系统冻结声明(0.5天) +11. erp-message 改用 subscribe_filtered(1天) +12. 统一事件消费模式(2天) +13. 过敏史变更历史(1天) + +**P2(后续迭代,扩展):** +14. PostgreSQL RLS +15. 血透专科(先客户调研) +16. OCR / IM(血透验证后) +17. health 模块按子域重组 +18. 动态菜单系统 + +## 结论 / 待定 + +### 达成的共识 + +1. **基座设计方向正确** — 星形依赖、trait 抽象、事件总线经受住了实践检验 +2. **插件系统从核心战略降级为实验性功能** — 保留但冻结 +3. **临床安全和合规是最高优先级** — 危急值闭环和知情同意必须先于功能扩展 +4. **积分系统应从 health 模块拆出** — 降低合规复杂度 +5. **单人+AI 开发需要节奏控制** — 每日提交上限、ADR 强制化、医疗安全代码外部 review + +### 遗留问题 + +1. **知情同意的具体实现方案** — 需要单独讨论:同意类型(数据收集/共享/研究使用)、获取时机(建档时/首次使用时)、存储结构 +2. **积分系统拆分的接口设计** — 事件总线通信还是共享 trait? +3. **血透专科的市场验证** — 需要用户确认是否已做客户调研 +4. **PostgreSQL RLS 的实施策略** — 全量 RLS 还是只覆盖敏感表? +5. **合规审计的准备** — 是否有外部合规审计计划? + +### 关联文档 + +- 设计规格:`docs/superpowers/specs/2026-04-26-platform-retrospective-and-evolution-design.md` +- 插件系统设计:`docs/superpowers/specs/2026-04-13-wasm-plugin-system-design.md` +- 健康模块设计:`docs/superpowers/specs/2026-04-23-health-management-module-design.md` +- 插件平台讨论:`docs/discussions/2026-04-18-plugin-platform-brainstorm.md` diff --git a/docs/discussions/2026-04-27-miniprogram-audit-report.md b/docs/discussions/2026-04-27-miniprogram-audit-report.md new file mode 100644 index 0000000..0dbb518 --- /dev/null +++ b/docs/discussions/2026-04-27-miniprogram-audit-report.md @@ -0,0 +1,330 @@ +# HMS 小程序端审计报告 + +> 日期: 2026-04-27 | 审计范围: 代码审查 + API 链路实测 | 审计人: Claude + +## 执行摘要 + +对 HMS 健康管理平台微信小程序端进行了全面审计,覆盖 **40 个页面、10+ 服务模块、2 个 Store**。通过代码静态分析 + 后端 API 实测,发现 **6 个严重问题、8 个中等问题、5 个低级问题**,以及 **3 个功能链路断链**。 + +### 关键数字 + +| 指标 | 值 | +|------|-----| +| 审计页面 | 40 个(含 8 个医护端) | +| 审计服务 | 12 个 API 服务文件 | +| 审计组件 | 6 个 | +| 严重问题 (HIGH) | 6 个 | +| 中等问题 (MEDIUM) | 8 个 + 3 个功能断链 | +| 低级问题 (LOW) | 5 个 | +| 正面发现 | 8 项 | + +--- + +## 一、功能链路断链(实测发现) + +### F1. [HIGH] 今日体征数据未反映刚录入的数据 + +**实测过程:** +1. `POST /health/patients/{id}/vital-signs` 成功创建记录(`heart_rate: 72`) +2. `GET /health/vital-signs/today` 仍返回所有字段 `null` + +**根因分析:** +- `today` 端点依赖 JWT 中的 `user_id` 反查 `patient` 表的 `user_id` 字段 +- 测试患者(张三)的 `user_id` 为 `null`(API 返回 `"user_id":null`),即该患者未关联登录账号 +- 今日体征接口无法定位到正确的患者 + +**前端影响:** +- 用户在健康录入页提交数据后,返回首页或健康 Tab,"今日体征概览"区域仍然为空 +- 用户体验严重断裂:**数据明明录入了,但看不到** + +**修复建议:** +1. 前端:`today` 接口改用 `X-Patient-Id` header 传递当前选中患者的 ID(代码已实现 header 注入,但后端 `today` 端点未读取) +2. 后端:`vital_signs_today` handler 应优先使用 `X-Patient-Id` header 中的 patient_id + +### F2. [HIGH] 积分/签到功能对未关联患者的用户完全不可用 + +**实测过程:** +- `GET /health/points/account` → 404 `"当前用户未关联患者档案"` +- `GET /health/points/checkin/status` → 404 + +**根因:** 积分、签到、兑换等患者端功能都依赖 `user_id → patient` 的关联查询。管理员账号没有对应的患者档案。 + +**前端影响:** +- 积分商城 Tab 页(5 个 Tab 之一)对未关联患者的用户显示为空白或报错 +- **没有友好的降级提示**(如"请先完善个人档案") + +**修复建议:** +1. 前端:积分商城/签到页面需增加"未关联患者档案"的降级 UI +2. Auth store `restore()` 时检查是否有 `currentPatient`,无则引导用户建档 + +### F3. [MEDIUM] 文章列表返回草稿状态的文章 + +**实测过程:** +- `GET /health/articles` 返回 4 篇文章,其中 `status: "draft"` 的文章也被返回 + +**前端影响:** 患者端文章列表可能显示未发布的草稿文章。 + +**修复建议:** +1. 前端:`article.ts` 服务请求时添加 `status=published` 过滤参数 +2. 后端:患者端文章列表 API 应默认只返回 `published` 状态的文章 + +--- + +## 二、安全审计发现 + +### 2.1 严重 (HIGH) + +| # | 问题 | 文件 | 影响 | +|---|------|------|------| +| H1 | **Token 刷新竞态条件** | `services/request.ts:57-65` | 多个 API 同时 401 时,各自独立调用 `tryRefreshToken()`,可能导致 refresh token 被消耗多次,锁死用户 | +| H2 | **静态加密密钥无密钥派生** | `utils/secure-storage.ts:4` | 所有用户共享同一编译时烘焙的密钥,反编译小程序包即可解密全部本地存储数据 | +| H3 | **非生产环境明文回退** | `utils/secure-storage.ts:11-33` | 开发模式下 token/PII 明文存储在 localStorage | +| H4 | **Token 冗余暴露在 React State** | `stores/auth.ts:31-32` | Zustand store 持有 `token`/`refreshToken` 副本,与 secure storage 冗余,增加攻击面 | +| H5 | **登录无防重复点击保护** | `stores/auth.ts:53-75` | `login()` 函数无 `if (loading) return` 守卫,快速双击可触发两次登录请求 | +| H6 | **Analytics 绕过请求层** | `services/analytics.ts:67-73` | 直接调用 `Taro.request()`,无 Authorization header,无 token 刷新,BASE_URL 硬编码重复 | + +### 2.2 中等 (MEDIUM) + +| # | 问题 | 文件 | 影响 | +|---|------|------|------| +| M1 | **Logout 清理不完整** | `stores/auth.ts:120-129` | 残留 `wechat_openid`、`tenant_id`、`analytics_queue`、`edit_patient` 在 storage 中 | +| M2 | **PII 未加密存储** | `stores/auth.ts:62-64` | `user` 对象(含 id/phone)通过 `Taro.setStorageSync` 明文存储 | +| M3 | **开发日志含敏感数据** | `services/request.ts:50` | `console.log` 输出完整请求 body(可能含血压、血糖等健康数据) | +| M4 | **解密失败静默返回空串** | `utils/secure-storage.ts:31-33` | 无法区分"未登录"和"数据被篡改"两种情况 | +| M5 | **聊天轮询闭包过时** | `consultation/detail` 两个页面 | `pollNewMessages` 捕获的 `session` 状态可能过时,session 关闭后轮询可能继续 | +| M6 | **daily-monitoring 缺输入验证** | `health/daily-monitoring/index.tsx` | 无 Zod 验证,`parseFloat('999999')` 会被接受(对比 health/input 有完整 Zod) | +| M7 | **OpenID 明文存储** | `stores/auth.ts:68` | WeChat OpenID 敏感标识符通过明文 storage 存储 | +| M8 | **身份证号明文传递** | `profile/family/index.tsx:46` | 编辑患者时 `edit_patient`(含 `id_number`)通过明文 storage 传递给编辑页 | + +### 2.3 低 (LOW) + +| # | 问题 | 说明 | +|---|------|------| +| L1 | 18 处 `any` 类型 | auth 流程最集中(5 处 `as any`),绕过类型检查 | +| L2 | 聊天无指数退避 | 8s 固定间隔轮询,网络异常时产生大量无效请求 | +| L3 | 无客户端消息排序 | 假设服务端返回有序,无 `created_at` 排序 | +| L4 | devLogin 残留在产物中 | `services/auth.ts:47` 的开发登录函数未从生产构建中移除 | +| L5 | 大量静默 catch | 多处 `catch { }` 隐藏错误,影响生产问题排查 | + +### 2.4 正面安全发现 + +- **XSS 防护完善**:零 `dangerouslySetInnerHTML`、零 `innerHTML`、零 `eval()`,所有用户内容通过 Taro `` 渲染 +- **无硬编码密钥**:所有敏感配置通过环境变量注入 +- **后端多租户隔离一致**:所有查询均含 `tenant_id` + `deleted_at IS NULL` 过滤 +- **后端 CAS 并发控制**:预约、积分余额、库存、未读计数等关键操作使用乐观锁 +- **后端 PII 加密存储**:身份证号、手机号、咨询消息等使用 AES + HMAC 可搜索加密 +- **表单防重复提交**:所有表单使用 `submitting`/`loading` state + 按钮禁用 +- **健康录入 Zod 验证**:`health/input` 页面使用完整的 Zod schema + 阈值警告 +- **URL 参数编码**:`buildQuery` 正确过滤 undefined 并 `encodeURIComponent` + +--- + +## 三、后端 API 数据结构实测 + +### 3.1 分页响应结构 + +后端统一返回结构(所有分页列表 API): + +```json +{ + "success": true, + "data": { + "data": [...], // 实际数据数组 + "total": 6, + "page": 1, + "page_size": 10, + "total_pages": 1 + } +} +``` + +前端 `request.ts` 提取 `body.data` 后得到 `{ data: [...], total, page, ... }`。 +前端各 service 的泛型声明基本匹配此结构(如 `{ data: Patient[], total: number }`)。 + +**实测结论:前端 service 层的字段映射基本正确。** 但需注意: +- 部分页面可能直接用 `resp.items`(错误)而非 `resp.data`(正确)来访问列表数据 +- 需要逐页验证页面层的消费代码 + +### 3.2 关键实体字段映射 + +| 实体 | 后端字段 | 前端期望 | 匹配状态 | +|------|----------|----------|----------| +| 商品积分价 | `points_cost` | `points_cost` | ✅ 匹配 | +| 咨询主题 | **无 `subject`/`title` 字段** | `subject` | ❌ 前端引用了不存在的字段 | +| 文章分类 | `category` (string) | `category` | ✅ 匹配 | +| 医生姓名 | `name` | `name` | ✅ 匹配 | +| 体征日期 | `record_date` | `record_date` | ✅ 匹配 | + +### 3.3 咨询会话无 subject 字段 + +**实测发现:** 咨询会话实体后端字段为: +`id, patient_id, doctor_id, consultation_type, status, last_message_at, unread_count_patient, unread_count_doctor, created_at, updated_at, version` + +**没有 `subject` 字段!** 前端会话列表页如果显示 `session.subject`,将渲染为 `undefined`。 + +--- + +## 四、代码质量与优化 + +### 4.1 包体积问题 + +**实测发现:** `pages/health/trend/index.js` 体积 **455 KiB**,原因是全量引入 ECharts。 + +**优化建议:** +1. 使用 `echarts/charts` 按需引入(仅 LineChart) +2. 或考虑小程序原生图表库(如 wx-charts) +3. 预计可减少 ~80% 的趋势页体积 + +### 4.2 架构优化建议 + +| 优先级 | 建议 | 说明 | +|--------|------|------| +| P0 | Token 刷新加锁 | 实现单例 Promise 模式避免并发刷新 | +| P0 | 积分商城降级 UI | 未关联患者时显示引导而非空白 | +| P1 | daily-monitoring 加 Zod | 与 health/input 对齐验证标准 | +| P1 | Analytics 复用 request.ts | 消除独立的 `Taro.request` 调用 | +| P1 | 文章列表过滤草稿 | 患者端只展示 published 文章 | +| P2 | 聊天轮询 → WebSocket | 后端 SSE 基础设施已规划 | +| P2 | ECharts 按需引入 | 趋势页 455KiB → ~90KiB | +| P2 | 类型安全强化 | auth store 消除 `as any` | +| P3 | 统一错误处理 | 静默 catch → console.warn + 上报 | + +--- + +## 五、缺失功能 / TODO 清单 + +| 功能 | 状态 | 说明 | +|------|------|------| +| 模板消息 | TODO | `wechat-templates.ts` 模板 ID 全部为空 | +| 用药提醒 | 仅本地 | 无后端同步,换设备即丢失 | +| 文章 Tab | 注册但未使用 | `pages/article/index` 在 pages 列表但不在 tabBar | +| 医生排班日历 | API 存在 | 后端 `doctor-schedules/calendar` 已实现,前端调用需验证 | +| AI 报告 | 页面存在 | 需验证 erp-ai 模块集成后的实际数据渲染 | + +--- + +## 六、修复优先级建议 + +### 立即修复(P0 — 影响用户体验/安全) + +1. **F1: 今日体征数据不刷新** — 核心健康功能链路断裂 +2. **H1: Token 刷新竞态** — 可能导致用户被锁死 +3. **F2: 积分商城降级 UI** — Tab 页空白影响用户信任 + +### 短期修复(P1 — 1-2 周内) + +4. **F3: 文章列表过滤草稿** +5. **H5: 登录防重复点击** +6. **H6: Analytics 复用请求层** +7. **M6: daily-monitoring 加 Zod 验证** +8. **咨询会话 subject 字段缺失处理** + +### 中期优化(P2 — 迭代规划) + +9. H2/H3: 存储加密加强 +10. M1/M2/M7/M8: 存储清理 + PII 加密统一 +11. 包体积优化(ECharts 按需引入) +12. 类型安全强化(消除 `any`) + +--- + +## 七、附录:API 实测数据 + +### 数据库当前状态 + +| 实体 | 记录数 | +|------|--------| +| 患者 | 6 | +| 医生 | 2 | +| 预约 | 1(已完成) | +| 咨询会话 | 2(1 active, 1 waiting) | +| 随访任务 | 5(3 pending, 2 completed) | +| 文章 | 4(1 draft, 3 published) | +| 文章分类 | 2+ | +| 商品 | 1(Health Kit, 50 积分, 库存 100) | +| 医生排班 | 1(2026-04-28 AM, max 10, current 1) | +| 化验报告 | 0 | +| 线下活动 | 0 | + +### 关键 ID(用于后续测试) + +- 患者张三: `019dca49-1a88-7280-b44d-3ee5162b61ee` (user_id: null) +- 活跃咨询会话: `019dc2ac-235f-7550-a64c-3d66edfbf1ae` +- 已完成预约: doctor `019dc29b...`, date 2026-04-28 + +--- + +## 八、MCP 自动化页面渲染审计(实测补充) + +> 使用 miniprogram-automator 通过 MCP 协议连接微信开发者工具,逐页导航验证渲染。 + +### 8.1 TabBar 页面(5/5 通过) + +| 页面路径 | 状态 | +|----------|------| +| pages/index/index | OK | +| pages/health/index | OK | +| pages/consultation/index | OK | +| pages/mall/index | OK | +| pages/profile/index | OK | + +### 8.2 患者端 + 医护端子页面(24/24 通过) + +全部使用 `reLaunch` 逐页导航,无崩溃、无重定向到登录页: + +| 页面路径 | 状态 | +|----------|------| +| pages/health/input/index | OK | +| pages/health/trend/index | OK | +| pages/health/daily-monitoring/index | OK | +| pages/appointment/index | OK | +| pages/appointment/create/index | OK | +| pages/article/index | OK | +| pages/ai-report/list/index | OK | +| pages/followup/detail/index | OK | +| pages/consultation/detail/index | OK | +| pages/mall/orders/index | OK | +| pages/profile/family/index | OK | +| pages/profile/reports/index | OK | +| pages/profile/followups/index | OK | +| pages/profile/medication/index | OK | +| pages/profile/settings/index | OK | +| pages/legal/user-agreement | OK | +| pages/legal/privacy-policy | OK | +| pages/doctor/index | OK | +| pages/doctor/patients/index | OK | +| pages/doctor/consultation/index | OK | +| pages/doctor/followup/index | OK | +| pages/doctor/report/index | OK | +| pages/events/index | OK | +| pages/device-sync/index | OK | + +### 8.3 详情页(假 ID 优雅降级,11/11 通过) + +使用 UUID `00000000-0000-0000-0000-000000000000` 测试,所有页面不崩溃、显示空状态或加载中: + +| 页面路径 | 状态 | +|----------|------| +| pages/appointment/detail/index?id=... | OK | +| pages/article/detail/index?id=... | OK | +| pages/report/detail/index?id=... | OK | +| pages/ai-report/detail/index?id=... | OK | +| pages/mall/detail/index?id=... | OK | +| pages/mall/exchange/index?id=... | OK | +| pages/profile/family-add/index | OK | +| pages/doctor/patients/detail/index?id=... | OK | +| pages/doctor/consultation/detail/index?id=... | OK | +| pages/doctor/followup/detail/index?id=... | OK | +| pages/doctor/report/detail/index?id=... | OK | + +### 8.4 MCP 审计结论 + +**40/40 页面全部正常渲染**,无白屏、无崩溃、无意外重定向。详情页对无效 ID 参数均能优雅降级。 + +### 8.5 关于积分 API 404 的补充说明 + +上一轮审计中 `GET /health/points/account` 等 4 个端点返回 404,经查: +- 路由已正确注册在 `crates/erp-health/src/module.rs:454-484` +- 404 原因是 `resolve_patient_id()` 查找 admin 用户的关联患者档案失败 +- 这是应用层 404("当前用户未关联患者档案"),不是路由缺失 +- 属于预期行为:积分端点设计为患者端使用,管理员账号无患者档案 diff --git a/docs/sales/2026-04-27-hms-health-management-sales-pitch.md b/docs/sales/2026-04-27-hms-health-management-sales-pitch.md new file mode 100644 index 0000000..1543ce7 --- /dev/null +++ b/docs/sales/2026-04-27-hms-health-management-sales-pitch.md @@ -0,0 +1,431 @@ +# HMS 健康管理平台 — 让健康人群成为你的患者 + +> **从「管理已就诊患者」到「主动获取未来患者」的思维转变** + +--- + +## 1. 你的诊所只覆盖了患者生命的 0.02% + +一个患者每年在你诊所停留的时间大约是 **2 小时**。 + +而他一年有 **8,760 小时** 在诊所之外。 + +你的 HIS(医院信息系统)完美覆盖了那 2 小时——挂号、诊疗、开药、收费。但剩下的 8,760 小时里,你几乎一无所知: + +- 他回家后按时吃药了吗? +- 血压血糖控制得怎么样? +- 上次说"下周来复查",来了吗? +- 那些从来没挂过号的「健康人」,你怎么让他们在需要的时候想到你? + +**这不是 HIS 的问题——HIS 天生就是为院内流程设计的。** + +你需要的是另一套系统:一套 **从健康人群出发,构建完整健康管理闭环的平台**。 + +### 你在漏掉什么? + +| 你在漏掉的 | 这意味着什么 | +|-----------|-------------| +| **健康人群** | 从未就诊的人完全没有渠道接触你,你只能等他们生病上门 | +| **院外数据** | 患者在家测的血压、血糖、体重你完全不知道,直到下次就诊才发现异常 | +| **随访机会** | 出院时说"记得复查",3 个月后双方都忘了,复诊率靠运气 | +| **早期干预** | 慢病早期指标变化缓慢,等患者感觉不适才来就诊,往往已经加重 | +| **持续关系** | 治疗结束 = 关系结束,没有持续的触点维持医患连接 | + +### 我们的方法:把「漏斗」翻过来 + +传统模式下,诊所是**被动等待**患者上门。 + +HMS 健康管理平台提供一种全新的路径——**主动经营健康人群,在问题出现时自然转化为就诊**: + +``` +健康人群 ──→ 建立粘性 ──→ 早期预警 ──→ 主动干预 ──→ 就诊转化 + ↑ │ + └──────────── 康复管理 · 持续随访 · 长期粘性 ─────────────┘ +``` + +**核心逻辑:健康管理系统不是「售后工具」,而是「患者获取引擎」。** + +--- + +## 2. 六阶段用户旅程:从陌生人到长期患者 + +HMS 平台围绕用户的完整健康生命周期设计,每一个阶段都有对应的产品能力支撑。 + +### 第一阶段:吸引 — 让健康人群认识你 + +在用户还是「健康人」的时候就开始建立连接。 + +**积分签到体系** + +- 每日打开小程序签到获得积分 +- 连续打卡 7/14/30 天获得阶梯奖励 +- 让用户养成每天打开的习惯——**日活就是一切增长的起点** + +**健康内容推送** + +- 科普文章、饮食建议、运动指导、疾病预防知识 +- 按用户标签精准推送(如:50 岁以上推送心血管内容) +- **内容即广告**——每一次阅读都是品牌心智的建立 + +**线下活动运营** + +- 健康讲座、义诊、社区筛查活动 +- 活动报名、现场扫码签到、自动发放积分奖励 +- 线下触达 → 线上留存,O2O 闭环 + +### 第二阶段:感知 — 让用户看见自己的健康数据 + +当用户开始关注自己的身体,系统提供专业级的数据采集能力。 + +**每日体征上报(小程序端)** + +用户在家即可记录: +- 血压(收缩压/舒张压,晨起/晚间) +- 心率 +- 体重 +- 血糖(支持空腹/餐后类型标记) +- 体温 +- 血氧饱和度(SpO₂) +- 入水量 / 出尿量 + +**趋势可视化** + +- 7 天 / 30 天 / 90 天趋势图表 +- 单一指标时间趋势 + 正常范围参考线 +- 异常数据点高亮标注——**从数字到图形,异常一目了然** + +**日常监测** + +- 结构化的早晚血压记录 +- 体重变化追踪 +- 出入量管理(肾病/心衰患者关键指标) + +### 第三阶段:预警 — 数据驱动,主动干预 + +系统替你盯着每一个用户的数据,异常时自动通知医护团队。 + +**智能告警引擎** + +- 按指标、科室、年龄段灵活配置告警阈值 +- 支持多种条件类型(超限、趋势异常、连续偏高) +- 按严重级别分级处理:低级记录、中级通知、高级紧急 + +**危急值通知** + +- 血压严重偏离、血糖危险值、血氧过低等即时触发 +- 自动通知对应科室的负责医生 +- **从"等患者来看"变成"主动找患者"** + +**趋势分析报告** + +- 按时间区间生成指标摘要 +- 自动识别异常项和变化趋势 +- 为医生提供数据驱动的干预建议,不是拍脑袋 + +### 第四阶段:转化 — 从健康用户到就诊患者 + +当预警触发或用户主动寻求帮助时,系统提供无缝的转化通道。 + +**在线咨询** + +- 用户有疑问 → 发消息 → 医生回复 +- 支持文本、图片、处方建议 +- 降低就医门槛——**一次线上咨询可能就是一次到诊** + +**预约排班** + +- 日历视图展示医生排班 +- 患者一键预约,名额原子级 CAS 保护(不超额) +- 预约状态全流程追踪:待确认 → 已确认 → 已完成/已取消 + +**医生主页** + +- 科室、职称、专长、简介展示 +- 在线状态实时显示 +- 建立医患信任——选医生不是盲选 + +### 第五阶段:粘性 — 就诊后不流失 + +患者完成就诊后,系统确保关系不断裂。 + +**智能随访管理** + +- 就诊/出院后自动创建随访任务 +- 5 种随访方式:电话、门诊、上门、线上、微信 +- 逾期自动标记并通知医护 +- 批量创建、批量指派,高效执行 + +**用药管理** + +- 当前/历史用药完整记录 +- 用药频率(每日/BID/TID/QID/按需)和给药途径 +- 跨科室用药安全审查的基础 + +**检验报告管理** + +- 上传化验单 → 结构化提取 → 医生审阅 +- 支持拍照识别和手工录入 +- 异常项自动标注 +- 报告类型:肾功能、血常规、电解质、肝功能等 + +**全生命周期健康档案** + +- 体检记录 + 体征数据 + 化验报告 + 诊断记录 + 用药记录 +- 360° 患者画像,时间线展示 +- 每次就诊都有完整历史上下文——**医患信任因数据而加深** + +**专科深度能力** + +| 专科领域 | 系统专项能力 | +|---------|-------------| +| 肾内科 | 透析记录(HD/HDF/HF)、干体重、出入量、超滤量、并发症追踪 | +| 糖尿病 | 血糖类型标记(空腹/餐后)、用药频率管理、长期趋势监控 | +| 心血管 | 早晚血压分离、心率/血氧综合监测、心血管风险评估 | +| 体检中心 | 报告结构化入库、异常项追踪、自动生成随访任务 | + +### 第六阶段:运营 — 数据驱动持续增长 + +全面的数据看板,让你对经营状况了如指掌。 + +**运营统计看板** + +- 患者统计:总数、新增、活跃、留存 +- 咨询统计:会话量、响应时间、满意度 +- 随访统计:完成率、逾期率 +- 体征统计:上报率、7 天趋势 +- 透析/化验:数量、类型分布、待审核 + +**积分商城** + +- 积分兑换服务(免费复查、健康评估) +- 积分兑换实物(健康设备、保健品) +- 核销扫码——积分有出口,用户有动力 + +**隐私合规** + +- 知情同意管理:患者授权/撤销有据可查 +- PII 数据加密存储:身份证、手机号、过敏史等敏感信息 +- 完整审计日志——合规无忧 + +--- + +## 3. 为什么选择 HMS — 三个差异化优势 + +市面上有随访工具、健康小程序、积分平台。但 HMS 不是工具拼凑,是 **一体化健康管理平台**。 + +### 差异化一:一体化 vs 拼凑 + +**别家的方案:** + +> 随访用 A 系统 + 问诊用 B 小程序 + 健康数据用 C App + 积分用 D 平台 +> → 数据不互通,患者要注册 4 个账号,医护要开 4 个后台 + +**HMS 的方案:** + +> 一套平台,一套数据,一个后台,一个小程序 +> → 患者的体征、化验、随访、咨询、积分全在一条时间线上 + +**这意味着什么?** +- 患者数据之间产生联动:体征异常 → 触发告警 → 自动创建随访 → 医生发起咨询 +- 运营数据统一看板:不需要在 4 个系统之间切换 +- 患者只装一个小程序,使用门槛极低 + +### 差异化二:专科级深度 vs 通用工具 + +通用健康工具只能记个血压血糖。HMS 为专科场景提供深度支持: + +| 能力维度 | 通用健康工具 | HMS | +|---------|-------------|-----| +| 体征采集 | 血压、心率 | 血压(晨/晚)、心率、体重、血糖(空腹/餐后)、体温、血氧、入水量、出尿量 | +| 透析管理 | 无 | HD/HDF/HF 三种透析类型、干体重、超滤量、并发症追踪、审阅流程 | +| 化验报告 | 上传 PDF | 结构化提取 + 异常标注 + 医生审阅 + 趋势对比 | +| 告警规则 | 固定阈值 | 按指标/科室/年龄段/严重级别灵活配置 | +| 随访管理 | 提醒功能 | 任务创建 → 执行 → 记录 → 后续任务自动流转,支持 5 种随访方式 | +| 诊断编码 | 无 | 支持 ICD 编码,主诊断/次诊断/合并症分类 | + +**通用工具只能做「记录」,HMS 能做「管理」。** + +### 差异化三:企业级安全 vs 小作坊 + +医疗数据的安全不是可选项,是底线。 + +| 安全维度 | HMS 的做法 | +|---------|-----------| +| **数据加密** | PII 敏感信息(身份证、手机号、过敏史、病史、咨询内容)AES-256 加密存储 + HMAC 索引 | +| **数据隔离** | 多租户架构,每家机构的数据完全独立,SQL 级别隔离 | +| **并发安全** | 乐观锁 + CAS 原子操作——积分不超发、预约不超额、库存不超卖 | +| **审计追溯** | 所有写操作记录审计日志(谁、何时、改了什么) | +| **隐私合规** | 知情同意管理(授权/撤销/有效期/见证人) | +| **部署灵活** | 支持 SaaS 和私有化部署——数据可以不出机构 | + +--- + +## 4. 从专科诊所到健康管理中心的升级路径 + +**你不需要一步到位。HMS 支持你按需扩展,同一套平台,随时升级。** + +### 三步走战略 + +**第一步:专科诊所(今天)** + +选择你最擅长的专科领域,先用起来: +- 肾内科 → 透析记录 + 出入量 + 化验追踪 +- 糖尿病 → 血糖监测 + 用药管理 + 并发症预警 +- 心血管 → 血压监测 + 心率/血氧 + 趋势分析 + +核心能力:患者管理 + 体征监测 + 随访 + 在线咨询 + +**第二步:多科室健康管理中心(明天)** + +在一个平台上开通多个科室,数据互通: +- 患者在不同科室的就诊记录统一归档 +- 跨科室用药安全审查 +- 统一运营看板,全院经营数据一目了然 + +新增能力:告警引擎 + 积分运营 + 内容管理 + 统计报表 + +**第三步:区域健康管理平台(后天)** + +从治疗走向预防,从院内走向社区: +- 体检中心对接:团检报告结构化入库 + 异常追踪 +- 企业健康管理:员工健康档案 + 年度体检对比 +- 社区健康站:日常监测 + 居家养老 + 紧急告警 +- IoT 设备对接:血压计、血糖仪、血氧仪数据自动上传 + +新增能力:私有化部署 + IoT 设备对接 + 定制开发 + +### 关键承诺 + +> **你今天投入的每一分钱,都不会因为规模扩大而浪费。** +> 同一套平台,从 1 个科室到 100 个机构,数据架构和安全体系完全一致。 +> 不需要换系统,不需要迁移数据,只需要开通新的模块。 + +--- + +## 5. 一个真实场景:王先生的 45 天健康管理旅程 + +> 王先生,52 岁,既往体健,从未到过我们的诊所。 + +**Day 1 — 相遇** + +王先生在社区看到一场「心血管健康讲座」的海报,扫码报名(线下活动)。讲座当天签到获得 50 积分,关注了诊所的小程序。 + +**Day 1–7 — 建立习惯** + +每天打开小程序签到领积分(积分签到)。连续 7 天打卡,获得阶梯奖励。期间看了 3 篇心血管科普文章,学会了在家测血压的重要性(健康内容)。 + +**Day 8 — 开始记录** + +王先生翻出家里的血压计,开始每天在小程序记录血压和心率(体征上报)。系统展示 7 天趋势图,一切正常。 + +**Day 15 — 系统发出预警** + +连续 3 天晨起收缩压超过 140mmHg,系统触发告警(智能告警引擎)。医护团队在后台看到预警信息。 + +**Day 16 — 主动触达** + +系统自动推送消息:「王先生您好,您近 3 天的血压偏高,建议咨询医生。」王先生点击消息,直接进入在线咨询页面,向心内科医生描述了自己的情况(在线咨询)。 + +**Day 16 — 医生建议就诊** + +医生查看王先生的历史体征数据,建议来院做进一步检查。王先生当场完成预约(预约排班),挂号心内科。 + +**Day 18 — 确诊就诊** + +王先生来院检查,确诊轻度高血压。医生开具处方,建立健康档案(诊断管理 + 用药管理)。出院时,系统自动创建随访任务:2 周后电话随访(智能随访)。 + +**Day 18–45 — 持续管理** + +- 每日血压继续上报,系统自动监测趋势 +- 第 4 周电话随访执行,血压控制良好 +- 用药记录持续更新 +- 积分兑换了一次免费复查(积分商城) + +**Day 45 — 数据说话** + +血压控制稳定,王先生在平台上已经活跃了 45 天。他不仅是一名患者,更是一个长期健康管理的忠实用户。 + +**回顾这条路径:** + +| 阶段 | 平台能力 | 业务价值 | +|------|---------|---------| +| 吸引 | 线下活动 + 积分 | 获客:一个「路人」变成「关注者」 | +| 感知 | 体征上报 + 趋势 | 数据积累:开始看见自己的健康 | +| 预警 | 智能告警 | 主动干预:在患者感觉不适前介入 | +| 转化 | 在线咨询 + 预约 | 获诊:从「想看病」到「挂上号」一步到位 | +| 粘性 | 随访 + 用药 + 档案 | 留存:就诊后不流失 | +| 运营 | 积分商城 | 复购:积分驱动复查复诊 | + +> **一个从未踏入过诊所的「健康人」,在 45 天内被转化为一名长期管理的患者。** +> **全程无断裂,数据驱动每一个决策。** + +--- + +## 6. 合作方案 + +我们提供三种合作方式,按需选择,按需升级。 + +### 标准版 — 适合单科室/单诊所 + +| 能力 | 说明 | +|------|------| +| 患者管理 | 患者档案、家庭成员、标签分组、医生关联 | +| 体征监测 | 每日体征上报、趋势图表、日常监测 | +| 随访管理 | 智能任务创建、5 种随访方式、逾期提醒 | +| 在线咨询 | 患-医实时消息、会话管理 | +| 预约排班 | 医生排班、患者预约、状态追踪 | +| 小程序 | 患者端微信小程序(数据上报 + 咨询 + 预约) | +| Web 管理后台 | 医护管理端,浏览器即用 | + +### 专业版 — 适合多科室机构 + +在标准版基础上增加: + +| 能力 | 说明 | +|------|------| +| 智能告警引擎 | 可配置阈值、分级通知、危急值预警 | +| 积分运营体系 | 签到积分、连续打卡、积分商城、核销扫码 | +| 内容管理系统 | 科普文章、分类标签、审核发布、阅读统计 | +| 线下活动 | 活动发布、报名管理、签到积分 | +| 运营统计看板 | 患者增长、咨询量、随访完成率、体征上报率 | +| 专科深度模块 | 透析管理 / 糖尿病管理 / 心血管管理(按需开通) | +| 检验报告管理 | 结构化入库、异常标注、医生审阅 | + +### 旗舰版 — 适合体检中心/区域平台 + +在专业版基础上增加: + +| 能力 | 说明 | +|------|------| +| 私有化部署 | 数据不出机构,完全自主可控 | +| IoT 设备对接 | 血压计、血糖仪、血氧仪数据自动上传 | +| 企业团检 | 企业员工健康管理、年度体检对比 | +| 定制开发 | 按业务需求定制功能模块 | +| 多机构管理 | 统一平台管理多个科室/分院/合作点 | +| 专属客户成功经理 | 1 对 1 服务,确保平台成功上线运营 | + +--- + +## 7. 立即开始 + +**你的患者正在院外度过 8,760 小时。** + +他们中有多少人正在默默发展成慢病患者? +有多少人会在下次体检时才被发现? +有多少人本来可以更早干预? + +HMS 健康管理平台帮你: + +- **触达** 那些从未走进你诊所的健康人群 +- **看见** 患者在院外的真实健康数据 +- **干预** 在问题变严重之前主动介入 +- **留存** 就诊后持续管理,不复失 +- **增长** 数据驱动的运营,持续获客 + +**从今天开始,让健康管理成为你的增长引擎。** + +--- + +> HMS 健康管理平台 — 从健康到医疗的完整闭环 +> +> 联系我们获取演示账号或预约产品演示 diff --git a/docs/superpowers/specs/2026-04-26-pii-encryption-expansion-design.md b/docs/superpowers/specs/2026-04-26-pii-encryption-expansion-design.md new file mode 100644 index 0000000..a360c8d --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-pii-encryption-expansion-design.md @@ -0,0 +1,591 @@ +# PII 分级加密扩展设计 + +> 日期: 2026-04-26 | 状态: 草案 | 作者: iven + Claude + +## 目录 + +1. [背景与动机](#1-背景与动机) +2. [设计目标](#2-设计目标) +3. [当前状态分析](#3-当前状态分析) +4. [分级加密策略](#4-分级加密策略) +5. [加密基础设施提升 (erp-core)](#5-加密基础设施提升) +6. [每租户独立密钥管理](#6-每租户独立密钥管理) +7. [Entity 层改造](#7-entity-层改造) +8. [Service 层集成](#8-service-层集成) +9. [数据迁移](#9-数据迁移) +10. [边界场景处理](#10-边界场景处理) +11. [分步实施计划](#11-分步实施计划) +12. [测试策略](#12-测试策略) +13. [风险与缓解](#13-风险与缓解) + +--- + +## 1. 背景与动机 + +HMS 健康管理平台当前仅加密了 `patient.id_number`(身份证号)一个字段,使用 AES-256-GCM + HMAC-SHA256 搜索索引。其余 30+ 个 PII 字段(患者姓名、电话、过敏史、病史摘要、咨询内容等)均为明文存储。 + +**驱动力:** +- 体检中心/医疗机构在采购前会进行安全审计,明文 PII 是 dealbreaker +- 《个人信息保护法》对医疗健康信息有特殊保护要求 +- 多租户 SaaS 架构下,数据隔离需要从访问控制扩展到存储加密 +- 三专家评审一致指出 PII 加密范围不足是高优合规风险 + +**约束:** +- 系统前期不涉及实际医疗行为,PII 加密是合规门槛而非实时临床安全 +- 保持可搜索性:患者姓名是高频搜索字段,不能因加密丧失搜索能力 +- 性能可接受:加密/解密开销不能显著影响 API 响应时间 + +--- + +## 2. 设计目标 + +| 目标 | 指标 | +|------|------| +| PII 字段加密覆盖 | 从 1 个字段扩展到 15+ 个高风险字段 | +| 多租户密钥隔离 | 每租户独立 DEK,泄漏不影响其他租户 | +| 搜索能力保持 | HMAC 精确搜索 + 姓名明文模糊搜索 | +| 性能影响 | 单条解密 < 0.1ms,列表页不触发解密 | +| 零停机迁移 | 现有明文数据逐步加密,不锁表 | + +--- + +## 3. 当前状态分析 + +### 已有加密能力 + +| 组件 | 位置 | 状态 | +|------|------|------| +| `HealthCrypto` | `crates/erp-health/src/crypto.rs` | AES-256-GCM + HMAC-SHA256 | +| 身份证号加密 | `patient_service.rs` | 已上线 | +| 脱敏管道 | `crates/erp-health/src/service/masking.rs` | `mask_id_number`, `mask_phone` | +| AI 脱敏层 | `crates/erp-ai/src/sanitization/mod.rs` | DTO 去标识化 | +| 密钥配置 | `crates/erp-server/config/default.toml` | 环境变量注入 | + +### 明文高风险字段清单 + +**患者 (patient) — 8 个未加密 PII 字段:** +- `name` — 姓名(决定不加密,保持搜索性) +- `emergency_contact_phone` — 紧急联系人电话 +- `allergy_history` — 过敏史 +- `medical_history_summary` — 病史摘要 +- `emergency_contact_name` — 紧急联系人姓名 +- `birth_date` — 出生日期 +- `blood_type` — 血型 +- `notes` — 备注 + +**家庭成员 (patient_family_member) — 2 个:** +- `phone` — 电话 +- `name` — 姓名 + +**咨询消息 (consultation_message) — 1 个:** +- `content` — 咨询/问诊内容 + +**随访记录 (follow_up_record) — 3 个:** +- `result` — 随访结果 +- `patient_condition` — 患者状况 +- `medical_advice` — 医嘱 + +**诊断 (diagnosis) — 1 个:** +- `notes` — 诊断备注 + +**医生档案 (doctor_profile) — 1 个:** +- `license_number` — 执业证号 + +**透析记录 (dialysis_record) — 2 个:** +- `symptoms` — 症状 JSON +- `complication_notes` — 并发症记录 + +**化验报告 (lab_report) — 1 个:** +- `doctor_notes` — 医生备注 + +--- + +## 4. 分级加密策略 + +### Tier 定义 + +| 级别 | 存储方式 | 搜索方式 | 展示方式 | 适用字段 | +|------|---------|---------|---------|---------| +| **Tier 1** | AES-256-GCM 加密 | HMAC-SHA256 精确匹配 | 解密后展示/脱敏 | 高敏感、可枚举的字段 | +| **Tier 2** | 明文 | 直接 SQL LIKE/模糊 | API 脱敏展示 | 中敏感、高频搜索的字段 | +| **Tier 3** | 明文 | 直接 SQL | 原样展示 | 非敏感或非 PII 字段 | + +### Tier 1 — 加密存储字段 + +| Entity | 字段 | HMAC 搜索 | +|--------|------|----------| +| patient | `emergency_contact_phone` | `emergency_contact_phone_hash` | +| patient | `allergy_history` | — | +| patient | `medical_history_summary` | — | +| patient_family_member | `phone` | `phone_hash` | +| consultation_message | `content` | — | + +**咨询内容搜索说明:** `consultation_message.content` 加密后无法做全文搜索。替代方案:按咨询会话(consultation)维度搜索和过滤(通过状态、时间、医生等非加密字段),进入详情后查看具体消息内容。如未来需要全文搜索,可引入加密索引(如 Order-Preserving Encryption 或专用密文搜索引擎),但 V1 不实施。 +| follow_up_record | `result` | — | +| follow_up_record | `patient_condition` | — | +| follow_up_record | `medical_advice` | — | +| doctor_profile | `license_number` | `license_number_hash` | +| diagnosis | `notes` | — | +| dialysis_record | `complication_notes` | — | +| dialysis_record | `symptoms` (JSON) | — | +| lab_report | `doctor_notes` | — | + +**JSON 字段加密说明:** `dialysis_record.symptoms` 类型为 `serde_json::Value`,加密时先序列化为 JSON 字符串再加密,解密后反序列化回 `Value`。 + +**注:** `patient.id_number` 和 `id_number_hash` 已存在,无需改动。 + +### Tier 2 — 明文 + API 脱敏 + +| Entity | 字段 | 脱敏规则 | +|--------|------|---------| +| patient | `emergency_contact_name` | 保留首字 + `**`(如 `张**`),两字姓名也适用(`张*`) | +| patient | `birth_date` | 仅显示年份 | +| patient_family_member | `name` | 保留首字 + `**`,同上 | + +### Tier 3 — 明文 + +`name`, `gender`, `blood_type`, 数值型体征数据, 日期, 状态字段, `icd_code`, `diagnosis_name`, `notes`(patient 和 health_record 的备注字段,内容不可预测且非临床核心)等。 + +**分类澄清:** `notes` 字段在不同 Entity 中分级不同。`patient.notes` 归入 Tier 3(一般备注),`diagnosis.notes` 归入 Tier 1(临床诊断备注),`follow_up_record` 的 `result`/`patient_condition`/`medical_advice` 归入 Tier 1。具体分级以上面 Tier 1/2/3 表格为准。 + +**设计决策:患者姓名不加密。** 原因:医疗场景下姓名不是真正意义上的隐私(前台叫号、医生喊名),保持明文可搜索对运营效率至关重要。加密姓名会强制改变搜索交互模式(从"先搜索"变成"先选择"),代价过大。 + +--- + +## 5. 加密基础设施提升 + +### 5.1 从 HealthCrypto 到 PiiCrypto + +将 `crates/erp-health/src/crypto.rs` 中的 `HealthCrypto` 提升到 `crates/erp-core/src/crypto/`: + +``` +crates/erp-core/src/crypto/ +├── mod.rs # PiiCrypto 公开接口 +├── engine.rs # AES-256-GCM 加密/解密引擎 +├── hmac.rs # HMAC-SHA256 搜索索引 +├── key_manager.rs # 每租户 DEK 管理 +└── masking.rs # 脱敏管道(从 health/service/masking.rs 提升) +``` + +### 5.2 PiiCrypto 接口设计 + +```rust +pub struct PiiCrypto { + kek: [u8; 32], // Master KEK + dek_cache: DashMap, // 租户 DEK 缓存 +} + +struct CachedDek { + dek: [u8; 32], + version: u32, + loaded_at: Instant, +} + +impl PiiCrypto { + /// 加密单个字段,返回 Base64(nonce + ciphertext + tag) + pub fn encrypt(&self, dek: &[u8; 32], plaintext: &str) -> Result; + + /// 解密单个字段 + pub fn decrypt(&self, dek: &[u8; 32], ciphertext: &str) -> Result; + + /// 生成 HMAC-SHA256 搜索索引 + pub fn hmac_hash(&self, dek: &[u8; 32], value: &str) -> String; + + /// 批量解密(避免重复 DEK 加载) + pub fn decrypt_batch(&self, dek: &[u8; 32], ciphertexts: &[String]) -> Result>; + + /// 获取/创建指定租户的 DEK + pub async fn get_dek(&self, tenant_id: Uuid, db: &DatabaseConnection) -> Result; + + /// 轮换指定租户的 DEK + pub async fn rotate_dek(&self, tenant_id: Uuid, db: &DatabaseConnection) -> Result<()>; +} +``` + +### 5.3 依赖关系变化 + +``` +Before: erp-health → (内部 crypto.rs) +After: erp-core (crypto/) ← erp-health (调用 PiiCrypto) + ← 未来其他模块 +``` + +### 5.4 Cargo.toml 依赖变更 + +`erp-core/Cargo.toml` 需要新增: +```toml +[dependencies] +aes-gcm = "0.10" +hmac = "0.12" +sha2 = "0.10" +base64 = "0.22" +hex = "0.4" +dashmap = "6" +``` + +`erp-health/Cargo.toml` 可移除 `aes-gcm`, `hmac`, `sha2`, `base64`, `hex`(已提升到 erp-core),改为依赖 `erp-core` 的 crypto 模块。 + +### 5.5 erp-ai 依赖关系澄清 + +当前 erp-ai 通过 `HealthDataProvider` trait 获取**已脱敏**的 DTO(不含 name/phone/id_number),**不需要**直接访问加密数据。PiiCrypto 提升到 erp-core 后,erp-ai 不需要调用 PiiCrypto 解密。AI 脱敏层继续作为独立防线运作。 + +如果未来 AI 需要原始 PII 数据(如精确匹配患者),可通过新增 `HealthDataProviderWithPii` trait 提供,此时才需要 erp-ai 调用 PiiCrypto。 + +--- + +## 6. 每租户独立密钥管理 + +### 6.1 密钥层级 + +``` +Master KEK (Key Encryption Key) + 来源: 环境变量 ERP__CRYPTO__KEK + 用途: 加密保护所有租户的 DEK + 格式: 32 字节 hex-encoded + + └── Per-Tenant DEK (Data Encryption Key) + 来源: 首次使用时随机生成,用 KEK 加密后存入 DB + 用途: 加密该租户的 PII 数据 + 格式: 32 字节随机 +``` + +### 6.2 数据库表 + +```sql +CREATE TABLE tenant_crypto_keys ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id), + encrypted_dek VARCHAR(128) NOT NULL, -- AES-256-GCM 加密的 DEK + key_version INTEGER NOT NULL DEFAULT 1, + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + created_by UUID, + updated_by UUID, + deleted_at TIMESTAMPTZ, + version INTEGER NOT NULL DEFAULT 1, + UNIQUE(tenant_id, key_version) +); +-- 项目规范要求所有表包含: id, tenant_id, created_at, updated_at, created_by, updated_by, deleted_at, version +``` + +### 6.3 DEK 缓存策略 + +| 参数 | 值 | 原因 | +|------|-----|------| +| 最大缓存数 | 100 个租户 | 典型 SaaS 实例租户数 < 100 | +| TTL | 5 分钟 | 平衡安全性和性能 | +| 淘汰策略 | LRU | 简单有效 | +| 强制刷新 | 密钥轮换后 | 确保新密钥立即生效 | + +使用 `DashMap`(并发安全 HashMap)实现缓存,避免每次请求查询数据库。 + +### 6.4 密钥轮换流程 + +1. 管理员调用 `POST /api/v1/admin/tenants/{id}/rotate-key` +2. 生成新 DEK,用 KEK 加密,写入 `tenant_crypto_keys` 表(version +1) +3. 后台任务启动: + - 读取该租户所有加密数据 + - 用旧 DEK 解密 → 用新 DEK 加密 + - 更新 `key_version` 字段 + - 分批处理(每批 100 条),每批提交一次 +4. 轮换期间,读取侧根据每条记录的 `key_version` 字段精确选择对应 DEK,不做"尝试解密 + 回退"(避免掩盖数据篡改或密钥管理错误) +5. 全部完成后,标记旧 DEK `is_active = false` + +--- + +## 7. Entity 层改造 + +### 7.1 新增伴生字段 + +每个需要 HMAC 搜索的字段,在 SeaORM Entity 中新增 `_hash` 列: + +| Entity | 新增字段 | 迁移操作 | +|--------|---------|---------| +| patient | `emergency_contact_phone_hash: Option` | ALTER TABLE ADD COLUMN | +| patient_family_member | `phone_hash: Option` | ALTER TABLE ADD COLUMN | +| doctor_profile | `license_number_hash: Option` | ALTER TABLE ADD COLUMN | + +**HMAC hash 列索引:** 每个 `_hash` 列创建普通 B-tree 索引(`CREATE INDEX idx_{table}_{field}_hash ON {table}({field}_hash) WHERE {field}_hash IS NOT NULL`),确保 HMAC 搜索不会退化为全表扫描。 + +### 7.2 Entity Model 变化示例 + +```rust +// crates/erp-health/src/entity/patient.rs +// 改造前: +pub struct Model { + pub id_number: Option, // 已加密 + pub id_number_hash: Option, // 已有 + pub emergency_contact_phone: Option, // 明文 + pub allergy_history: Option, // 明文 + // ... +} + +// 改造后: +pub struct Model { + pub id_number: Option, // 已加密 (不变) + pub id_number_hash: Option, // 已有 (不变) + pub emergency_contact_phone: Option, // 现在存储加密值 + pub emergency_contact_phone_hash: Option, // 新增: HMAC 搜索索引 + pub allergy_history: Option, // 现在存储加密值 + // ... +} +``` + +**重要:** Entity 字段类型不变(仍为 `Option`),只是存储内容从明文变为密文。这确保 SeaORM 查询层无需改动。 + +--- + +## 8. Service 层集成 + +### 8.1 加密/解密注入点 + +Service 层在每个 CRUD 操作中透明处理加密: + +| 操作 | 加密处理 | 解密处理 | +|------|---------|---------| +| Create | 加密 Tier 1 字段 + 生成 HMAC | — | +| Update | 重新加密变更的 Tier 1 字段 + 更新 HMAC | — | +| Get by ID | — | 解密 Tier 1 字段 | +| List | — | 列表不展示加密字段(返回 None) | +| Search | HMAC hash 替代明文搜索条件 | — | + +### 8.2 PiiCrypto 注入方式 + +```rust +// erp-server/src/state.rs +pub struct HealthState { + pub pii_crypto: PiiCrypto, // 替换原来的 HealthCrypto + // ... +} + +// patient_service.rs +pub async fn create_patient( + db: &DatabaseConnection, + crypto: &PiiCrypto, + tenant_id: Uuid, + input: CreatePatientRequest, +) -> Result { + let dek = crypto.get_dek(tenant_id, db).await?; + + // 加密 Tier 1 字段 + let encrypted_phone = input.emergency_contact_phone + .map(|p| crypto.encrypt(&dek.key, &p)) + .transpose()?; + let phone_hash = input.emergency_contact_phone + .map(|p| crypto.hmac_hash(&dek.key, &p)); + + // ... 插入数据库 +} +``` + +--- + +## 9. 数据迁移 + +### 9.1 迁移策略 + +采用**渐进式迁移**,不锁表,不中断服务: + +1. **部署新代码** — 新代码根据 `key_version` 字段判断:`NULL` = 明文(直接返回),有值 = 密文(解密后返回)。不依赖 Base64 格式检测(自由文本字段本身可能是合法 Base64)。 +2. **后台迁移任务** — 逐条加密现有明文数据 +3. **完成切换** — 所有数据加密后,移除明文兼容路径 + +### 9.2 迁移任务设计 + +```rust +pub async fn migrate_pii_encryption( + db: &DatabaseConnection, + crypto: &PiiCrypto, + tenant_id: Uuid, +) -> Result { + let dek = crypto.get_dek(tenant_id, db).await?; + let batch_size = 100; + + loop { + // 读取一批未迁移的记录(key_version < current 或 key_version IS NULL) + let batch = find_unencrypted_patients(db, tenant_id, batch_size).await?; + if batch.is_empty() { break; } + + for patient in &batch { + // 加密明文字段 + let encrypted_phone = patient.emergency_contact_phone.as_ref() + .map(|p| crypto.encrypt(&dek.key, p)) + .transpose()?; + let phone_hash = patient.emergency_contact_phone.as_ref() + .map(|p| crypto.hmac_hash(&dek.key, p)); + + // 更新记录 + update_patient_encrypted(db, patient.id, encrypted_phone, phone_hash, dek.version).await?; + } + + sleep(Duration::from_millis(50)).await; // 避免压垮数据库 + } +} +``` + +### 9.3 Entity 新增 key_version 字段 + +所有包含 Tier 1 字段的 Entity 新增 `key_version: Option` 列,用于: +- 跟踪每条记录使用的 DEK 版本 +- 支持渐进式迁移和轮换 +- NULL 表示未加密(明文) + +### 9.4 回滚计划 + +部署后如果需要回滚到旧代码: +- **新创建的加密数据:** 旧代码无法读取(`key_version` 非空的记录)。回滚前需要运行"解密迁移"脚本,将加密数据还原为明文。 +- **未迁移的明文数据:** 旧代码可正常读取。 +- **回滚脚本:** 提供与加密迁移对称的解密迁移脚本,从最新 `key_version` 的 DEK 解密所有字段,清除 `key_version`(设为 NULL)。 +- **回滚窗口:** 建议在 Phase A 部署后 24 小时内保持回滚能力,超过 24 小时确认无问题后可移除明文兼容路径。 + +--- + +## 10. 边界场景处理 + +### 10.1 列表页性能 + +**策略:** 列表页不返回 Tier 1 加密字段(设为 `None`),仅展示 Tier 2/3 字段。 + +这与当前 `model_to_resp` 的设计一致(列表中 `id_number` 已经返回 `None`),无需改变交互模式。 + +**前端影响评估:** 当前 `model_to_resp` 已经将 `emergency_contact_phone` 做脱敏处理(`mask_phone`)后返回。加密后,列表页的 `model_to_resp` 需要将该字段设为 `None`(因为不解密),详情页的 `model_to_resp_decrypted` 需要解密后再脱敏。前端列表页如果展示了 `emergency_contact_phone`,需要适配为"详情中查看"。`patient_family_member` 和 `doctor_profile` 同理。 + +### 10.2 AI 分析管道 + +erp-ai **不直接调用 PiiCrypto**。它通过 `HealthDataProvider` trait(在 erp-core 中)获取已脱敏的 DTO,不含 name/phone/id_number 等原始 PII。PiiCrypto 对 erp-ai 完全透明——erp-health 在提供数据给 erp-ai 之前已经在 Service 层完成了解密+脱敏。AI 脱敏层(`SanitizationService`)作为二次防护,确保发送给 LLM 的数据不包含原始 PII。 + +### 10.3 数据导出 + +导出操作需要独立的权限码(如 `patient.export`),Service 层解密后输出。审计日志记录所有导出操作。 + +### 10.4 小程序端 + +小程序不直接处理加密数据。所有加密/解密在服务端完成,API 响应中的 Tier 2 字段做脱敏处理。 + +### 10.5 跨租户数据泄漏 + +即使数据库层面发生泄漏(如 SQL 注入),攻击者没有 KEK 就无法解密 DEK,没有 DEK 就无法解密 PII 数据。HMAC hash 字段不可逆,不泄漏原始值。 + +--- + +## 11. 分步实施计划 + +### Phase A — 基础设施 + Patient 实体(2-3 天) + +**目标:** 加密基础设施落地,patient 实体全面加密。 + +| 步骤 | 内容 | 预估 | +|------|------|------| +| A1 | 提升加密基础设施到 erp-core/src/crypto/ | 0.5 天 | +| A2 | 实现 `tenant_crypto_keys` 表 + DEK 管理 | 0.5 天 | +| A3 | 修改 patient Entity(新增 hash 字段 + key_version) | 0.5 天 | +| A4 | 修改 patient_service 集成 PiiCrypto | 0.5 天 | +| A5 | 数据迁移脚本(patient 表现有数据) | 0.5 天 | +| A6 | 单元测试 + 集成测试 | 0.5 天 | + +**验证标准:** +- `cargo check` + `cargo test` 全部通过 +- 创建新患者 → 数据库中 phone/allergy 为密文,hash 正确 +- 查询患者详情 → API 返回解密后的明文 +- 搜索电话号码 → 通过 HMAC hash 匹配 + +### Phase B — 全 Entity 扩展 + 密钥轮换(2-3 天) + +**目标:** 所有 Tier 1 字段加密,管理端支持密钥轮换。 + +| 步骤 | 内容 | 预估 | +|------|------|------| +| B1 | consultation_message, follow_up_record 加密 | 0.5 天 | +| B2 | patient_family_member, doctor_profile 加密 | 0.5 天 | +| B3 | dialysis_record, lab_report 加密 | 0.5 天 | +| B4 | 密钥轮换管理端点 + 后台任务 | 0.5 天 | +| B5 | DEK 缓存(DashMap LRU) | 0.5 天 | +| B6 | 全量集成测试 + 性能基准 | 0.5 天 | + +**验证标准:** +- 所有 Tier 1 字段在数据库中为密文 +- 所有现有 API 功能正常(透明加密/解密) +- 密钥轮换后数据可正常读写 +- 批量解密 50 条记录 < 10ms + +--- + +## 12. 测试策略 + +### 12.1 单元测试 + +| 测试类别 | 覆盖内容 | 数量 | +|----------|---------|------| +| 加密引擎 | encrypt/decrypt 正确性、随机 nonce 不可预测 | 5 | +| HMAC 索引 | 相同输入相同输出、不同输入不同输出 | 3 | +| 密钥管理 | DEK 生成/缓存/轮换 | 5 | +| 脱敏管道 | 各脱敏规则正确性 | 4 | + +### 12.2 集成测试 + +| 测试类别 | 覆盖内容 | 数量 | +|----------|---------|------| +| CRUD 加密流 | 创建→加密存储→查询→解密返回 | 8 | +| HMAC 搜索 | 精确匹配搜索正确性 | 3 | +| 多租户隔离 | 租户 A 的 DEK 无法解密租户 B 的数据 | 2 | +| 密钥轮换 | 轮换前后数据可正常访问 | 2 | + +### 12.3 性能测试 + +| 指标 | 基准 | 目标 | +|------|------|------| +| 单字段加密 | — | < 0.05ms | +| 单字段解密 | — | < 0.05ms | +| 50 条批量解密 | — | < 10ms | +| HMAC 生成 | — | < 0.01ms | + +--- + +## 13. 风险与缓解 + +| 风险 | 概率 | 影响 | 缓解措施 | +|------|------|------|---------| +| 加密后搜索性能下降 | 低 | 中 | HMAC 索引 + B-tree 索引 | +| 迁移过程中服务中断 | 低 | 高 | 渐进式迁移 + 兼容读取 | +| DEK 缓存一致性 | 中 | 中 | TTL + 轮换后强制刷新 | +| 密钥泄漏 | 低 | 极高 | KEK 不存数据库 + 环境变量注入 + 泄漏恢复流程(见下) | +| 加密字段误漏 | 中 | 中 | 代码审查 + 单元测试覆盖 | +| KEK 泄漏灾难恢复 | 极低 | 极高 | 见 §14 灾难恢复 | + +--- + +## 14. KEK 泄漏灾难恢复 + +**场景:** Master KEK 泄露(如环境变量配置文件意外提交到 Git)。 + +**恢复流程:** + +1. **立即轮换 KEK** — 生成新 KEK,更新环境变量 +2. **重新包装所有 DEK** — 用新 KEK 解密并重新加密每个租户的 DEK + ```sql + -- 不需要重新加密 PII 数据,只需重新加密 DEK + UPDATE tenant_crypto_keys SET encrypted_dek = new_kek_encrypt(dek) WHERE is_active = true; + ``` +3. **清除 DEK 缓存** — 强制所有实例从数据库重新加载 +4. **轮换泄漏渠道** — 轮换所有可能暴露 KEK 的凭据(Git tokens、CI secrets、部署脚本) +5. **审计追溯** — 检查访问日志,确认是否有未授权的数据访问 + +**关键优势:** KEK 泄漏只需要重新包装 DEK(N 次轻量操作),不需要重新加密所有 PII 数据(可能数百万条记录)。这是 KEK/DEK 两层架构的核心价值。 + +**预防措施:** +- KEK 通过 secrets manager 注入,不写入文件系统 +- CI/CD 管道中扫描 KEK 值,防止意外提交 +- `.gitignore` 排除 `.env` 文件 + +--- + +## 附录:决策记录 + +| 决策 | 选项 | 选择 | 原因 | +|------|------|------|------| +| 加密范围 | 全面/分级/TDE | 分级 | 成本与收益平衡 | +| 姓名是否加密 | 加密/不加密 | 不加密 | 医疗场景下姓名不属隐私,搜索性更重要 | +| 基础设施位置 | health/core/独立 crate | erp-core | 通用性 + 简洁性平衡 | +| 密钥策略 | 全局/per-tenant/渐进 | 每租户独立 | 安全隔离,泄漏影响面最小 | +| 实施节奏 | 一次完成/分步/最小可用 | 分步 (A+B) | 风险分散,每步可验证 | diff --git a/docs/superpowers/specs/2026-04-26-platform-retrospective-and-evolution-design.md b/docs/superpowers/specs/2026-04-26-platform-retrospective-and-evolution-design.md new file mode 100644 index 0000000..87c44b1 --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-platform-retrospective-and-evolution-design.md @@ -0,0 +1,406 @@ +# HMS 平台基座回顾与演进设计 + +> 日期: 2026-04-26 | 状态: Draft | 方法: 三专家多视角评审 + +--- + +## 1. 概述 + +### 1.1 回顾目的 + +HMS 健康管理平台经过 17 天密集开发(2026-04-10 ~ 2026-04-26),从 ERP 底座演进到包含 16 个 Rust crate、62 个前端页面、27 个小程序页面的综合医疗 SaaS 平台。本次回顾旨在: + +- **验证基座设计** — 星形依赖拓扑、ErpModule trait、事件总线、多租户策略是否经得起实践检验 +- **评估演进路径** — 从插件开发模式到原生模块开发的决策是否正确 +- **识别缺口与风险** — 通过多专家视角发现盲点 +- **制定演进路线** — 基于 P0/P1/P2 优先级指导后续迭代 + +### 1.2 评审方法 + +采用三专家独立评审,每个专家从不同视角分析相同的诊断和建议: + +| 专家 | 视角 | 关注点 | +|------|------|--------| +| 高级系统架构师 | 架构可持续性 | 模块边界、事件可靠性、技术债 | +| 医疗信息化专家 | 临床安全与合规 | 患者安全、PIPL 合规、领域模型 | +| 产品策略专家 | ROI 与开发节奏 | 优先级、技术债量化、路线图现实性 | + +### 1.3 核心结论 + +**基座设计方向正确,但深度不足。** 星形依赖、trait 抽象、事件总线等基础架构经受住了实践检验。但在临床安全(危急值告警未闭环)、合规(知情同意缺失)、事件可靠性(无重放机制)方面存在需立即修复的缺口。插件系统已验证可行性但对 HMS 核心业务贡献有限,建议有条件冻结。 + +--- + +## 2. 基座设计验证 + +### 2.1 评分总览 + +| 维度 | 评分 | 说明 | +|------|------|------| +| 模块边界 | ★★★★ | 星形拓扑零循环依赖,trait 契约清晰 | +| ErpModule trait | ★★★★ | 生命周期/权限/事件/健康检查统一接口 | +| 事件总线 | ★★★☆ | 基础设施扎实(broadcast+outbox),但无重放机制,消费侧不完整 | +| 多租户 | ★★★☆ | JWT→TenantContext 全链路贯通,但缺 RLS 兜底和集成测试 | +| 权限体系 | ★★★★ | RBAC + 行级数据权限 + 按钮级控制 | +| 插件系统 | ★★★☆ | CRUD 场景验证通过,医疗场景天花板明显 | +| API 一致性 | ★★★★ | 统一 envelope、分页、OpenAPI 自动文档 | +| 数据库迁移 | ★★★★ | 59 个迁移,幂等、可回滚、fixup 模式健康 | +| 测试覆盖 | ★☆☆☆ | 36 后端 + 3 前端,覆盖率 < 5% | +| 合规性 | ★☆☆☆ | 知情同意缺失,审计不完整,PIE 加密范围不足 | + +### 2.2 星形依赖拓扑 + +``` + erp-core (L1) + / | \ \ \ \ + erp-auth workflow message config erp-health erp-plugin erp-ai + \ | / / / / / + erp-server (L3, 组装入口) +``` + +- `erp-core`:零业务依赖,纯净基础层 +- 7 个业务 crate:各只依赖 `erp-core`,兄弟间无横向依赖 +- `erp-server`:唯一组装点,负责路由合并和模块初始化 +- **无循环依赖** — 架构师验证通过 + +### 2.3 ErpModule trait + +当前 trait 提供统一的模块接口: + +- **身份**:`name()` / `id()` / `version()` +- **依赖声明**:`dependencies()` — 用于拓扑排序启动顺序 +- **生命周期**:`on_startup()` / `on_shutdown()` / `health_check()` +- **多租户**:`on_tenant_created()` / `on_tenant_deleted()` +- **权限自描述**:`permissions()` — 模块声明自己需要的权限码 +- **事件订阅**:`register_event_handlers()` / `as_any()` + +**已知张力**:路由注册不在 trait 中,而是通过各模块的 inherent method (`public_routes()` / `protected_routes()`) 手动在 `main.rs` 中合并。原因是 Axum 的 `Router` 泛型约束不适合 trait object。这是务实的妥协,但在添加新模块时有 boilerplate 成本。 + +### 2.4 事件总线 + +**实现机制**:`tokio::sync::broadcast` (容量 1024) + `domain_events` 表持久化(best-effort)+ Outbox relay (5秒轮询,3次重试) + +**发布侧**(已识别的事件类型): + +| 模块 | 事件类型数 | 示例 | +|------|-----------|------| +| erp-auth | 10 | `user.login`, `user.created`, `role.created` | +| erp-workflow | 4 | `process_instance.started`, `task.completed` | +| erp-message | 1 | `message.sent` | +| erp-health | 13 | `patient.created`, `health_data.critical_alert`, `follow_up.overdue` | +| erp-plugin | 2+ | `plugin.config.updated`, `plugin.trigger.*` | + +**消费侧**(已识别的订阅者): + +| 订阅者 | 订阅方式 | 处理的事件 | +|--------|---------|-----------| +| erp-message | `subscribe()` 全量 | `appointment.*`, `process_instance.*`, `task.*` | +| erp-health | `register_handlers_with_state` | `workflow.task.completed` | +| erp-plugin 通知 | `subscribe_filtered("plugin.trigger.*")` | 插件触发通知 | +| outbox relay | 轮询 DB | 重发 pending 事件 | + +**已识别缺陷**: +1. **无重放机制** — 内存 broadcast,服务重启后未消费的事件丢失 +2. **无幂等保护** — `follow_up.overdue` 每 6 小时检查会重复发布同一条逾期事件 +3. **全量订阅** — erp-message 使用 `subscribe()` 而非 `subscribe_filtered()`,所有事件都经过消息模块 + +### 2.5 多租户 + +**已实现**: +- JWT claims 提取 `tenant_id` → `TenantContext` 注入请求扩展 +- 所有 Entity 含 `tenant_id` 字段,BaseFields 统一 +- 所有 DomainEvent 携带 `tenant_id` +- `on_tenant_created()` / `on_tenant_deleted()` 钩子(auth 和 health 已实现) +- 部门级数据范围(`department_ids` 在 TenantContext 中) + +**缺失**: +- 无 PostgreSQL RLS policy 作为兜底层 +- 无强制 tenant_id 过滤的查询层机制 — 依赖每个 service 手动 `.filter()` +- 当前实际只有 default_tenant,微信登录硬编码使用 `default_tenant_id` +- 无多租户管理 API(创建/配置/迁移) + +--- + +## 3. 演进路径回顾 + +### 3.1 时间线 + +``` +4/10-4/16 基座搭建 (Phase 1-6) + → core → auth → config → workflow → message + → 全部原生 Rust 模块,30+ 数据库表 + +4/13-4/18 WASM 插件实验 + → 插件系统设计与实现 (Wasmtime + WIT bindgen) + → CRM (5实体) → Inventory (6实体) → Freelance → ITOps + → 证明:CRUD 密集型领域可行,沙盒隔离有效 + → 跨插件数据引用未解决 + +4/23-4/26 HMS 分叉 — 健康模块原生开发 + → 18+ 强类型实体 (患者/家属/医生/预约/排班/随访/咨询/体征/化验/透析/诊断/积分...) + → PII 加密 (AES-256-GCM)、脱敏管道 + → AI 模块 (4 SSE 流式端点 + 3 REST 端点) + → 微信小程序 (27 页面) + → 按钮级权限控制 +``` + +### 3.2 从插件到原生的决策链 + +**原始插件愿景**(设计规格 2026-04-13): + +- 平台模块原生,行业模块 WASM 插件 +- 插件通过 9 个 Host API 函数通信(db_insert/query/update/delete、event_publish、config_get 等) +- 数据存 JSONB 动态表,路由自动生成 +- UI 配置驱动,通用 PluginCRUDPage 组件 + +**健康模块原生的 5 个硬限制**(设计规格 2026-04-23 §1.3): + +| 限制 | 影响 | 不可妥协原因 | +|------|------|-------------| +| 20 实体上限 | 健康平台轻松超过 | 18+ 实体已是最低合理粒度 | +| JSONB 存储 | 无强类型、无外键约束 | 医疗数据需要引用完整性和精确索引 | +| 无自定义 API | 只有自动 CRUD | 趋势分析/统计报表/日历视图无法实现 | +| 无文件上传 | 沙盒阻止文件系统访问 | 化验单/体检报告需要文件存储 | +| WASM 沙盒限制 | 无 native crypto/外部 API/后台任务 | PII 加密、微信集成、定时任务全部需要 | + +### 3.3 得失评估 + +**得 — 正确的决策:** + +| 决策 | 收益 | +|------|------| +| 星形依赖拓扑 | 模块独立性强,可独立测试和替换 | +| ErpModule 统一接口 | 新模块注册流程标准化 | +| 事件总线 | 跨模块解耦通信的基础设施已就绪 | +| JWT→TenantContext | 多租户全链路贯通 | +| 健康模块原生 | 不受沙盒限制,加密/文件/后台任务全部可用 | +| 插件实验 | 验证了平台灵活性,CRM/库存可正常使用 | + +**失 — 需要修正的问题:** + +| 决策 | 代价 | +|------|------| +| 插件系统投入过大 | 22,000 行代码(41% Rust 总量),对 HMS 核心业务贡献接近零 | +| 积分系统混入 health | 8 实体/12+ 路由,增加合规复杂度和数据泄露面 | +| 事件消费侧忽视 | 13 个事件只有 3 个被消费,危急告警和逾期通知空转 | +| 测试覆盖极薄 | 36 后端 + 3 前端测试,覆盖率 < 5% | +| 合规意识不足 | 知情同意缺失、审计不完整、PIE 加密范围不足 | + +--- + +## 4. 三专家评审摘要 + +### 4.1 高级系统架构师 + +**诊断准确度:7/10** — 四个张力都真实存在,但优先级和细节有偏差。 + +关键补充: + +| 发现 | 严重程度 | +|------|---------| +| WIT 接口是同步调用(阻塞),WASM 运行时嵌入主进程(故障隔离不足) | 架构隐患 | +| EventBus 内存 broadcast 无重放机制,服务重启丢事件 | P1 | +| `follow_up.overdue` 无幂等保护,每 6h 检查重复发布 | P0 | +| erp-message 用 `subscribe()` 全量订阅,性能隐患 | P1 | +| RLS 不是 P0,多租户集成测试才是 | 观点 | +| 积分系统(8 独立实体、12+ 路由)不应在 erp-health 内 | 共识 | +| 缺监控/可观测性、数据备份策略、API 版本升级路线图 | 盲点 | + +核心原则:**先补测试再重构,先修事件再上功能,先验证再加固。** + +### 4.2 医疗信息化专家 + +**发现了比原始诊断更深层的临床安全风险。** + +| 新发现 | 严重程度 | +|--------|---------| +| 危急值阈值全部硬编码(收缩压 180/80、心率 150/40),不可配置 | P0 | +| `daily_monitoring` 表体征数据不经过危急值检测(合并前遗留) | P0 | +| 过敏史更新直接覆盖,无变更历史 | P0 | +| 知情同意完全缺失(搜索 consent/同意/授权/隐私 零结果) | P0 — PIPL 违规 | +| 只有身份证号存储加密,姓名/过敏史/诊断/咨询内容明文 | P1 | +| 审计日志不完整 — 只有预约状态变更记录前后值 | P1 | +| `ip_address` 和 `user_agent` 从未被填充 | P1 | +| 读操作(查看患者详情/化验报告)完全没有审计记录 | P1 | +| 诊断记录 `icd_code` 只做字符串约束,无格式校验,无同行审核 | P1 | + +合规评估:PIPL 第 29 条要求处理敏感个人信息须取得单独同意。医疗数据属于敏感个人信息。知情同意缺失是法律红线。 + +领域模型建议:积分系统(6 实体 + 2 线下活动实体)应拆分为独立 `erp-points` 或 `erp-engagement` 模块,与健康数据分离以降低合规复杂度。 + +### 4.3 产品策略专家 + +**开发节奏不可持续但不必恐慌。** + +| 分析 | 结论 | +|------|------| +| 峰值 68 提交/天,fix 提交占 21.6% | 短期冲刺可以,长期人会耗竭 | +| 41% Rust 代码在插件系统,对核心业务贡献接近零 | 最大 ROI 失衡 | +| 单人 + AI 的"速度幻觉" | 68 提交/天 = 审查不足,积分混入 health 就是例证 | +| 测试覆盖 < 5% | 正确水位不是 80%,而是关键路径不回退(目标 50-80 用例,3-4 天) | + +关键风险缓解建议: +- ADR(架构决策记录)强制化 +- 医疗安全代码双人外部 review +- 每日提交上限 15 次 +- 每月需求裁剪 + +V2 血透路线图评估:技术储备已够(`dialysis_service` 286 行骨架在),但缺市场验证。建议先做 3-5 家目标客户调研,确认需求后再做 2 周 MVP 试运行。 + +--- + +## 5. 共识优先级 + +### 5.1 三专家加权共识矩阵 + +| 议题 | 架构师 | 医疗专家 | 产品策略 | 共识等级 | +|------|--------|---------|---------|---------| +| 危急值告警闭环 | P0 | P0 + 硬编码 | P0 | 三方一致 | +| 知情同意 (PIPL) | 未涉及 | P0 | P0 | 两方一致 | +| 审计日志补全 | 未涉及 | P1 | P0 | P0-P1 | +| EventBus 可靠性 | P1 | 未涉及 | P0 | P0-P1 | +| 随访逾期通知 | P0 | P0 | P0 | 三方一致 | +| 积分系统拆分 | 应拆 | 应拆(合规) | 占 19.5% | 三方一致 | +| RLS | 不是 P0 | P1 | P0 | 有分歧 | +| 插件系统 | 有条件冻结 | 未涉及 | 冻结 | 两方一致 | +| 测试覆盖 | 先补测试 | 上线前必修 | 50-80 用例 | 三方一致 | +| V2 血透 | 未涉及 | 缺标准流程 | 先调研 | 两方一致 | + +### 5.2 P0 — 上线前必修(估计 2-3 周) + +| 序号 | 项 | 工作量 | 负责 crate | 说明 | +|------|---|--------|-----------|------| +| 1 | 危急值告警消费者 | 1 天 | erp-health + erp-message | `health_data.critical_alert` → 推送通知给责任医护 | +| 2 | 危急值阈值可配置化 | 2 天 | erp-health | 硬编码阈值改为数据库配置,支持科室/年龄差异化 | +| 3 | daily_monitoring 合并后告警验证 | 1 天 | erp-health | 确认合并到 vital_signs 后所有体征数据都经过告警检测 | +| 4 | 随访逾期通知 | 1 天 | erp-health + erp-message | `follow_up.overdue` → 催办通知 + 幂等保护 | +| 5 | 知情同意记录 | 3 天 | erp-health | 患者数据处理同意获取和记录机制 | +| 6 | 审计日志补全 | 3 天 | erp-core + erp-health | 临床数据变更记录前后值、读操作审计、IP/UA 填充 | +| 7 | EventBus 持久化增强 | 2 天 | erp-core | 服务重启不丢事件 + overdue 事件幂等 | + +### 5.3 P1 — 治理(2-4 周) + +| 序号 | 项 | 工作量 | 说明 | +|------|---|--------|------| +| 8 | 积分系统剥离 | 5 天 | 从 erp-health 拆分为独立 erp-engagement crate | +| 9 | 关键路径测试 | 4 天 | 多租户隔离、患者安全路径、预约并发(50-80 用例) | +| 10 | 插件系统冻结声明 | 0.5 天 | 保留代码,README 声明实验性,不再投入 | +| 11 | erp-message 改用 `subscribe_filtered` | 1 天 | 减少无效事件传递 | +| 12 | 统一事件消费模式 | 2 天 | 消除 `register_event_handlers` vs `on_startup` 双路径 | +| 13 | 过敏史变更历史 | 1 天 | 更新时记录旧值 | + +### 5.4 P2 — 扩展(后续迭代) + +| 序号 | 项 | 前置条件 | +|------|---|---------| +| 14 | PostgreSQL RLS | P1 测试覆盖完成 | +| 15 | 血透专科 | 3-5 家客户调研完成 | +| 16 | OCR 化验单提取 | 血透验证后 | +| 17 | IM 咨询 | 血透验证后 | +| 18 | health 模块按子域重组目录 | P1 测试覆盖完成 | +| 19 | 前端测试覆盖提升 | P1 后端测试完成 | +| 20 | 动态菜单系统 | 现有计划可用 | + +--- + +## 6. 风险与缓解 + +### 6.1 开发模式风险 + +| 风险 | 影响 | 缓解措施 | +|------|------|---------| +| 单人认知单点 | 一人理解 16 个 crate,bus factor = 1 | ADR 强制化,关键决策留文档 | +| AI 生成"编译对但逻辑错" | 危急值阈值硬编码、积分混入 health 就是例证 | 医疗安全代码双人外部 review | +| 速度幻觉 | 68 提交/天 = 审查不足 | 每日提交上限 15 次 | +| AI 回音壁 | AI 不质疑需求合理性 | 每月需求裁剪,引入真实用户反馈 | + +### 6.2 临床安全风险 + +| 风险 | 影响 | 缓解措施 | +|------|------|---------| +| 危急值告警未闭环 | 危急体征值无人响应,可致患者安全事故 | P0-1:实现消费者 + 阈值可配置 | +| 逾期随访无催办 | 患者失访,影响医疗质量指标 | P0-4:实现通知 + 幂等保护 | +| 过敏史无变更记录 | 无法追溯过敏史变更,用药风险 | P1-13:添加变更历史 | +| 告警阈值硬编码 | 无法适应儿科/老年科/血透科不同范围 | P0-2:数据库配置 | + +### 6.3 合规风险 + +| 风险 | 法规依据 | 缓解措施 | +|------|---------|---------| +| 知情同意缺失 | PIPL 第 29 条 | P0-5:实现同意记录机制 | +| 审计不完整 | 医疗机构信息化建设要求 | P0-6:补全审计日志 | +| PIE 加密范围不足 | PIPL 第 51 条 | P1:扩展加密到姓名/过敏史/诊断 | +| 数据删除权缺失 | PIPL 第 47 条 | P2:实现患者数据导出/删除 | + +### 6.4 架构风险 + +| 风险 | 影响 | 缓解措施 | +|------|------|---------| +| EventBus 无重放 | 服务重启丢事件 | P0-7:增强持久化 | +| 全量订阅 | 性能隐患,所有事件经消息模块 | P1-11:改用过滤订阅 | +| 路由手动合并 | 新模块 boilerplate 成本 | 长期:ErpModule trait v2 | +| erp-health 过大 | 18+ 实体,维护复杂度上升 | P2-18:按子域重组 | + +--- + +## 7. 附录 + +### 7.1 关键文件索引 + +| 文件 | 说明 | +|------|------| +| `crates/erp-core/src/module.rs` | ErpModule trait + ModuleRegistry (拓扑排序) | +| `crates/erp-core/src/events.rs` | EventBus 实现 (broadcast + outbox) | +| `crates/erp-core/src/types.rs` | TenantContext, BaseFields, Pagination | +| `crates/erp-core/src/rbac.rs` | 权限/角色检查 | +| `crates/erp-server/src/main.rs` | 服务组装和手动路由合并 | +| `crates/erp-server/src/state.rs` | AppState + FromRef 桥接 | +| `crates/erp-server/src/outbox.rs` | Outbox relay (5s 轮询, 3 次重试) | +| `crates/erp-auth/src/middleware/jwt_auth.rs` | JWT 认证 + TenantContext 注入 | +| `crates/erp-health/src/module.rs` | HealthModule (ErpModule 实现 + 后台任务) | +| `crates/erp-health/src/event.rs` | 健康模块事件订阅 | +| `crates/erp-health/src/crypto.rs` | AES-256-GCM 加密 | +| `crates/erp-health/src/service/masking.rs` | PII 脱敏管道 | +| `crates/erp-plugin/src/engine.rs` | WASM 插件引擎 | +| `docs/superpowers/specs/2026-04-13-wasm-plugin-system-design.md` | 插件系统设计规格 | +| `docs/superpowers/specs/2026-04-23-health-management-module-design.md` | 健康模块设计规格 | +| `docs/discussions/2026-04-18-plugin-platform-brainstorm.md` | 插件平台演进讨论 | + +### 7.2 迁移历史时间线 + +| 日期 | 迁移范围 | 说明 | +|------|---------|------| +| 4/10-11 | 核心平台 | 租户、用户、凭证、角色、权限、组织、部门、岗位 | +| 4/12 | 配置 + 工作流 | 字典、菜单、设置、编号规则 + 流程定义/实例/令牌/任务 | +| 4/13 | 消息 + 审计 | 模板、消息、订阅 + 审计日志 | +| 4/14 | 修复 | 唯一索引与软删除冲突、标准字段补全 | +| 4/16 | 领域事件 | domain_events 表 | +| 4/17 | 插件系统 | 插件表、动态表 | +| 4/18 | 搜索 + 权限 | pg_trgm、实体注册表、数据范围 | +| 4/19 | 关联修复 | 用户部门、CRM 修复、插件市场 | +| 4/23 | 健康表 | 患者、微信用户、文章 | +| 4/24 | 索引修复 | 3 个 fixup 迁移 | +| 4/25 | 健康扩展 | 患者ID哈希、医生名、透析/化验增强、AI 表、积分 | +| 4/26 | 业务改进 | 诊断、列重命名、daily_monitoring 合并、菜单种子 | + +**总计:59 个迁移,17 天内。** fixup 迁移模式健康(不编辑旧迁移,单独修复)。 + +### 7.3 项目统计快照 (2026-04-26) + +| 指标 | 值 | +|------|-----| +| Rust crate 数 | 16 | +| Rust 代码行 | ~57,000 | +| 前端文件数 | 174 (TSX/TS) | +| 前端页面 | 62 | +| 小程序页面 | 27 | +| 数据库迁移 | 59 | +| 数据库表 | 30 基础 + 18 健康 + 3 AI | +| 后端测试 | 36 | +| 前端单元测试 | 3 | +| Git 提交 | 237 | +| 开发周期 | 17 天 | + +--- + +*本文档由三专家多视角评审生成,作为 HMS 平台基座演进的参考基准。后续实施计划将基于本文档的优先级排序展开。* diff --git a/docs/wiki-methodology.md b/docs/wiki-methodology.md new file mode 100644 index 0000000..4d5a096 --- /dev/null +++ b/docs/wiki-methodology.md @@ -0,0 +1,360 @@ +# 项目 Wiki 知识库编制方法论 + +> 基于 ZCLAW 项目实战经验(10 crates + React 前端,~155KB wiki 重构)提炼。 +> 适用于任何有 AI 辅助开发参与的中大型项目。 +> **一句话总结**:Wiki 只记录代码无法告诉你的东西。 + +--- + +## 一、设计原则 + +### 原则 1:Wiki 记录"代码不能告诉你的" + +| 记录在 Wiki ✅ | 不记录在 Wiki ❌ | +|---------------|-----------------| +| 为什么这样设计(WHY) | 字段列表、函数签名 | +| 跨模块数据流走向 | 单文件内的代码逻辑 | +| 历史踩坑和教训 | 可用 `grep` 直接查到的信息 | +| 必须始终成立的约束(不变量) | CRUD 操作、getter/setter | +| 模块间调用接口(集成契约) | 具体的行号、变量名 | + +**判断标准**:如果 `git log` 或 `grep` 能在 30 秒内回答这个问题,就不需要写在 wiki 里。 + +### 原则 2:每个模块页统一 5 节结构 + +按**阅读优先级**排列(先给最重要的信息): + +``` +1. 设计决策 (WHY) — 为什么这样设计、历史背景、权衡取舍 +2. 关键文件 + 数据流 — 3-7 个核心文件 + 跨模块接口 +3. 代码逻辑 — 数据流走向 + 不变量 + 非显而易见的算法 +4. 活跃问题 + 陷阱 — 当前未解决 + 历史教训 +5. 变更记录 — 最近 5 条,超出的归入全局日志 +``` + +**为什么是这个顺序**:新的 AI 会话(或开发者)首先需要知道"这个模块为什么存在"和"文件在哪",然后才是"怎么工作的",最后是"有什么问题"。 + +### 原则 3:页面大小必须有预算 + +| 页面类型 | 行数预算 | 原因 | +|---------|---------|------| +| 首页/索引 | ≤ 120 行 | 需要快速扫描,AI 一次加载 | +| 模块页 | 100-200 行 | AI 一次加载 2-3 个模块不爆 context | +| 全局日志 | ≤ 50 条活跃 | 防止无限膨胀,旧条目归档 | + +**超过预算怎么办**:把详细内容归档到 `archive/` 目录,模块页只保留摘要 + 链接。 + +### 原则 4:单一真相源 + +同一信息只出现在一个页面。其他需要该信息的地方只放引用。 + +``` +错误:安全认证流程同时写在 saas.md、security.md、middleware.md +正确:security.md 拥有完整描述,saas.md 只写"详见 [[security]]" +``` + +**检查方法**:`grep` 关键内容,如果出现在 ≥ 3 个页面,就需要去重。 + +### 原则 5:Append-only 内容必须封顶 + +日志、问题列表等只增不减的内容,必须设置上限并定期归档。 + +``` +活跃日志 ≤ 50 条 → 旧条目归入 archive/log-{YYYY-MM}.md +活跃问题 ≤ 5 条/模块 → 修复后立即移除 +变更记录 ≤ 5 条/模块 → 旧记录在全局 log.md +``` + +### 原则 6:用症状导航补充模块导航 + +模块导航解决"这个模块是什么"的问题。但实际开发中,人们更多是在解决"出了问题该看哪里"。 + +**症状导航表**格式: + +| 症状 | 先查 | 再查 | 常见根因 | +|------|------|------|----------| +| 流式响应卡住 | routing | chat → middleware | 连接断开 / 超时 | +| 数据没持久化 | data-model | 对应模块 | 表结构 / 迁移缺失 | + +放在首页/索引页,让新来的人(或 AI 会话)0 跳就能定位排查方向。 + +--- + +## 二、结构模板 + +### 2.1 三级层级 + +``` +项目 Wiki +├── Level 1: index.md — 纯导航 + 症状索引(≤ 120 行) +├── Level 2: {module}.md — 每个功能模块一个页面(100-200 行) +├── Level 3: archive/ — 历史内容归档 +└── (可选) known-issues.md — 活跃问题全局索引 +``` + +### 2.2 首页模板 (index.md) + +```markdown +# {项目名} 知识库 + +> 一句话定位。使用方式说明。 + +## 关键数字 +| 指标 | 值 | 验证方式 | +|------|-----|---------| + +## 系统数据流 +{ASCII 全景图} + +## 模块导航 +- [[module-a]] — 一句话说明 +- [[module-b]] — 一句话说明 + +## 症状导航 +| 症状 | 先查 | 再查 | 常见根因 | +|------|------|------|----------| +``` + +### 2.3 模块页模板 ({module}.md) + +```markdown +--- +title: {模块名} +updated: {YYYY-MM-DD} +status: active | stable | developing +tags: [{tags}] +--- + +# {模块名} + +> 从 [[index]] 导航。关联: [[related-1]] [[related-2]] + +## 1. 设计决策 + +{为什么这样设计、历史背景、权衡取舍} +{用 Q&A 格式记录关键架构决策} + +## 2. 关键文件 + 数据流 + +### 核心文件 +| 文件 | 职责 | +|------|------| +| `path/to/file` | 一句话说明 | + +### 数据流 +{ASCII 流程图} + +### 集成契约 +| 方向 | 模块 | 接口 | 触发时机 | +|------|------|------|---------| +| 调用 → | {module} | `{function/API}` | {when} | +| 被调用 ← | {module} | `{function/API}` | {when} | + +## 3. 代码逻辑 + +### 关键数据流 +{跨函数/跨文件的完整路径,附意图说明} + +### 不变量 +⚡ {不变量 1}: {必须始终成立的约束} +⚡ {不变量 2}: {描述} + +### 非显而易见的算法 +{读代码难以理解的逻辑} + +## 4. 活跃问题 + 陷阱 + +### 活跃问题 +| 问题 | 级别 | 状态 | 说明 | +|------|------|------|------| +{0-5 条,修复后移除} + +### 历史教训 +- {教训}: {一句话描述} + +### 注意事项 +⚠️ {易出错的地方} + +## 5. 变更记录 +| 日期 | 变更 | +|------|------| +{最近 5 条} +``` + +--- + +## 三、关键机制详解 + +### 3.1 集成契约 + +**问题**:跨模块边界的信息(谁调谁、接口形状)是最难从代码中获取的知识,也是 wiki 最大的结构性缺口。 + +**做法**:每个模块页的"关键文件"节下增加一个"集成契约"小表,回答四个问题: + +| 问题 | 对应列 | +|------|--------| +| 这个模块调用了谁? | 调用 → | +| 这个模块被谁调用? | 被调用 ← | +| 通过什么接口? | 接口(函数名/API路径) | +| 什么时候触发? | 触发时机 | + +**示例**(中间件模块): + +| 方向 | 模块 | 接口 | 触发时机 | +|------|------|------|---------| +| 被调用 ← | kernel | `create_middleware_chain()` | 内核启动 | +| 调用 → | runtime | `run_before_completion()` | 每次聊天请求 | +| 提供 → | 所有模块 | `AgentMiddleware` trait | 14 个实现 | + +### 3.2 不变量标记 + +**问题**:系统中有一些"必须始终成立的约束",它们不像代码那样显式存在,但一旦被违反就会产生隐蔽的 bug。 + +**做法**:用 ⚡ 标记不变量,放在"代码逻辑"节下。 + +``` +⚡ Priority 是升序排列:0-999,数值越小越先执行 +⚡ memories.db 和 data.db 是独立数据库,跨库查询需确认目标库 +⚡ 记忆注入在中间件@150,在管家路由@80之后,技能索引@200之前 +``` + +**判断什么是好的不变量**: +- 它描述的是一种**关系或顺序**,不是单个组件的行为 +- 如果有人不知道这个约束,修改代码时很可能无意中违反它 +- 违反的后果不会立即显现,而是演化几轮后变成隐性 bug + +### 3.3 去重规则 + +| 重复类型 | 处理方式 | +|---------|---------| +| 完整描述出现在 A 和 B | 选择一个为真相源,另一个只引用 | +| 相同信息出现在 ≥ 3 页 | 必须去重,指定唯一归属 | +| 概述 vs 详情 | 概述页保留一句话 + 链接,详情页拥有完整描述 | + +**去重检查命令**: +```bash +grep -l '关键内容' wiki/*.md | wc -l +# 结果 ≥ 3 → 需要去重 +``` + +### 3.4 症状导航 + +**为什么需要**:模块导航是"模块→功能"方向,但排查问题时需要的是"症状→模块"方向。 + +**编制方法**: +1. 收集团队/AI 会话中反复出现的调试场景(8-12 个) +2. 每个场景记录:症状、先查哪个页面、再查哪个、最常见根因 +3. 放在首页,新会话/新人 0 跳可达 + +**示例**: + +| 症状 | 先查 | 再查 | 常见根因 | +|------|------|------|----------| +| API 返回 502 | saas | routing | Token 耗尽 / 服务超时 | +| 数据不持久 | data-model | 对应模块 | 表缺失 / 字段不匹配 | +| 流式中断 | chat | middleware | 连接断开 / 超时守护 | + +--- + +## 四、维护工作流 + +### 4.1 什么时候更新 Wiki + +| 触发事件 | 更新什么 | +|---------|---------| +| 修复 bug | 对应模块页"活跃问题" + 全局 known-issues 索引 | +| 架构变更 | 对应模块页"设计决策" + 集成契约 | +| 文件结构变化 | 对应模块页"核心文件"表 | +| 跨模块接口变化 | 涉及双方的"集成契约"表 | +| 发现新不变量 | 对应模块页"代码逻辑"节的 ⚡ 项 | +| 每次更新 | 模块页"变更记录"(保持5条) + 全局 log.md | + +### 4.2 防止 drift 的策略 + +| 策略 | 做法 | +|------|------| +| 页面大小预算 | 超过 200 行强制裁剪,移入 archive/ | +| 活跃问题生命周期 | 修复后立即移除,不保留已修复项 | +| 变更记录滑动窗口 | 只保留最近 5 条,旧的自然滚入全局日志 | +| 数字验证 | 关键数字标注验证命令,定期执行确认 | +| "最后验证"日期 | 在 frontmatter 的 `updated` 字段记录,超过 30 天需要复查 | + +### 4.3 重构 Wiki 的执行顺序 + +如果需要对已有 wiki 进行重构,按依赖关系分阶段: + +``` +Phase 1: 归档/封顶 — 压缩日志、归档旧内容(无依赖) +Phase 2: 确立真相源 — 最被其他页面引用的模块优先重构 +Phase 3: 依赖页面 — 引用 Phase 2 模块的页面去重 +Phase 4: 剩余模块 — 独立页面逐一重构 +Phase 5: 首页/索引 — 最后改(依赖所有模块页完成) +``` + +**关键约束**:每个模块页独立提交,可安全 `git revert` 回滚单个页面。 + +--- + +## 五、AI 辅助开发的特殊考量 + +### 5.1 Wiki 的主要读者可能是 AI + +在 AI 辅助开发中,wiki 的主要读者是每次新会话的 AI 实例(context 从零开始)。这改变了 wiki 的设计优先级: + +| 传统 wiki | AI 辅助 wiki | +|-----------|-------------| +| 详细、全面 | 精炼、可快速加载 | +| 按主题组织 | 按任务场景导航 | +| 历史记录丰富 | 只保留活跃信息 | +| 人工索引 | 症状→页面直接映射 | + +### 5.2 Context 预算思维 + +AI 的 context window 是有限资源。wiki 的每个字节都在消耗这个预算。 + +**优化策略**: +- 首页只放导航,不放内容(让 AI 按需读取模块页) +- 模块页控制在 100-200 行(一次加载 2-3 个不爆 context) +- 代码逻辑只写流向和不变量,不写可从代码读取的细节 +- 使用 `archive/` 存放低频需要的历史内容 + +### 5.3 Wiki 作为新会话的启动燃料 + +设计 wiki 时要问:**一个全新的 AI 会话,读完首页后能定位问题吗?读完 2 个模块页后能开始工作吗?** + +如果答案是"不能",说明 wiki 的导航层不够好(首页缺症状导航)或模块页的结构不对(信息不在前两节)。 + +--- + +## 六、检查清单 + +### 创建 Wiki 时 + +- [ ] 首页 ≤ 120 行,包含:项目一句话定位、关键数字、模块导航、症状导航 +- [ ] 每个模块页统一 5 节结构 +- [ ] 每个模块页有集成契约表 +- [ ] 每个模块页有 ⚡ 不变量 +- [ ] 每个模块页 100-200 行 +- [ ] 无内容重复出现在 ≥ 3 个页面 +- [ ] 全局日志封顶 50 条,有归档机制 + +### 维护 Wiki 时 + +- [ ] 修复 bug 后更新对应模块"活跃问题" +- [ ] 架构变更后更新对应模块"设计决策"+ 集成契约 +- [ ] 每次更新追加全局 log.md 条目 +- [ ] 每次更新模块页变更记录(保持 5 条) +- [ ] 定期检查页面是否超过大小预算 + +--- + +## 附录:ZCLAW 重构效果 + +| 指标 | 重构前 | 重构后 | 变化 | +|------|--------|--------|------| +| 模块页总行数 | ~2,800 | ~1,547 | -45% | +| 重复内容 | 安全×3, 进化×3 | 各×1 | 消除 | +| 集成契约覆盖 | 0/10 页 | 10/10 页 | 全覆盖 | +| 症状导航 | 无 | 8 条路径 | 新增 | +| 首页 | 144 行 | 101 行 | +症状导航 | +| 最大单页 | 424 行 | 199 行 | 控住 | diff --git a/docs/健康管理/客户需求整理.md b/docs/健康管理/客户需求整理.md new file mode 100644 index 0000000..40a83a0 --- /dev/null +++ b/docs/健康管理/客户需求整理.md @@ -0,0 +1,353 @@ +# 健康管理系统 — 客户初步设想整理 + +> 资料来源:客户提供的 Excel 功能文档(`管理系统功能文档(1).xlsx`)及 3 张微信图片(系统功能思维导图)。 +> 整理日期:2026-04-26 +> 备注:以下为客户原始需求,尚未经过技术评审和优先级排序。 + +--- + +## 一、系统概述 + +系统面向**血透/肾病专科**体检中心或医疗机构,包含三个端: + +| 端 | 形式 | 用户 | +|----|------|------| +| 患者端 | 微信小程序 | 透析患者、慢病患者及家属 | +| 医护端 | 微信小程序 | 医生、护士 | +| 管理后台 | PC Web | 管理员、运营人员 | + +--- + +## 二、患者端(微信小程序) + +### 2.1 首页 + +- 医院/中心公告轮播展示,点击可查看详情 +- 功能入口快捷方式:数据上报、我的医生、在线咨询、血透预约、耗材商城(支持自定义快捷方式) +- 健康数据概览卡片(今日血压/体重/最近一次透析记录),可视化图表展示 +- 每日健康打卡提醒 +- 待办提醒:提示即将到期的检查或治疗 + +### 2.2 数据上报(核心功能) + +**透析记录** +- 记录透析治疗的详细信息:透析日期、时间、干体重、透析前/后血压、心率、不适症状 +- 包括透析类型、频率等 +- 数据同步至医生端 + +**化验报告** +- 化验指标手动录入 + 拍照上传化验单 +- 上传肌酐、尿酸、血钾、血磷等化验单照片 +- 自动提取指标 + +**日常监测** +- 血压(早/晚)、体重、血糖、饮水量、尿量 +- 如血糖、血压等慢性病相关数据 +- 提供日常监测建议 + +**AI 分析** +- 自动生成趋势图 +- 异常指标标红 +- 生成《健康报告》 +- 基于数据分析预测潜在健康风险,提供预防措施建议 +- 定期生成健康周期报告,分析健康趋势,提供改善建议 + +### 2.3 预约与随访 + +- **透析预约**:支持透析治疗预约,提供预约时间选择,自动推荐预约时间 +- **复查/抽血预约**:提供预约流程指导 +- **护士随访任务接收**:护士定期随访服务,提供随访任务提醒,支持随访结果记录 +- **智能提醒**:多场景消息提醒 + - 透析提醒 + - 服药时间提醒(含药物相互作用提示) + - 测血压/体重提醒 + - 复查提醒 + - 支持自定义提醒时间、提醒事项 + +### 2.4 在线咨询 + +**客服咨询** +- 24 小时在线客服 +- 解答非医疗问题(订单、物流、使用问题) +- 提供预约协助 + +**医生咨询** +- 选择医生 +- 图文/语音沟通,支持语音消息 +- 上传报告 +- 适用于非紧急医疗咨询 +- 留言功能:医生离线时留存问题 +- 历史对话归档:保存历史咨询记录,支持搜索历史信息 + +**报告解读** +- 提供详细报告分析 +- 解读专业术语 + +**饮食/护理指导** +- 提供日常护理指导 +- 基于健康状况定制个性化饮食方案 + +### 2.5 医疗耗材商城(改为积分商城) + +**商品浏览** +- 商品分类:血透护理/瘘管保护/肾病食品/慢病器械 +- 提供详细分类筛选 +- 支持多商品同时结算,提供结算清单 + +**购物与支付** +- 购物车/结算 +- 微信支付(安全便捷) +- 支持修改商品数量和选页 + +**配送** +- 快递送货 +- 到院自提 +- 明确配送时间和费用 + +**订单管理** +- 订单/物流/售后管理 +- 支持售后服务申请 +- 电子发票下载 + +### 2.6 我的中心 + +- **个人信息 & 实名认证**:确保个人信息安全,提高服务质量和安全性 +- **多就诊人管理**:方便管理家庭成员健康(家属代绑) +- **健康档案**:自动收集并整理健康数据,形成个人健康档案,支持档案导出和分享,数据同步至健康档案 +- **我的医生**:管理个人医生信息 +- **我的订单/报告**:查看和管理订单记录,查看和下载健康报告 +- **系统设置**:包括通知设置、隐私设置 +- **健康打卡**:打卡功能记录个人健康数据,数据同步至健康档案 + +--- + +## 三、医护端(微信小程序) + +### 3.1 数据概览 + +- 按时间顺序显示管理的患者咨询 +- 每日待回复消息统计(回复消息数量) +- 异常指标预警 + - 按疾病类型分类 + - 按严重程度分级 +- 患者数据统计/病情分布 + - 统计患者数量 + - 绘制健康数据图表 + - 分析患者健康的趋势 + +### 3.2 患者管理 + +**患者列表** +- 按透析/慢病/高危筛选 +- 按疾病类型筛选 +- 按治疗阶段筛选 +- 多维度筛选:根据用户需求设置筛选项 +- 自定义筛选条件:保存常用筛选组合 + +**患者标签管理** +- 根据病情添加标签(高钾/高磷/体重超标/糖肾) +- 根据治疗反应添加标签 +- 标签分类管理:现有标签、编辑、删除不再适用的标签 + +**健康档案 & 数据趋势图** +- 详细记录病历信息 +- 查看报告和化验单 +- 绘制生命体征变化图 +- 展示治疗效果对比图 +- 患者在线状态显示 +- 最近一次咨询时间 + +### 3.3 咨询回复 + +- **未读消息列表**:新消息提醒 +- **回复方式**: + - 图文/语音回复 + - 语音输入快速回复 + - 加文字消息 +- **预设回复模板**:保存常用回复内容,分类管理回复模板 +- **科普内容发布**: + - 按疾病类型发布 + - 按季节性健康问题发布 + - 统计科普阅读量 + - 收集患者反馈信息 + +### 3.4 随访管理 + +**随访任务列表** +- 指定随访内容和方式 +- 修改随访计划 +- 管理随访任务和记录 +- 删除已完成的随访任务 + +**随访记录填写** +- 详细信息记录 +- 医生建议记录 +- 随访完成情况分析 +- 分析随访效果 + +**随访台账导出** +- 选择导出时间范围 +- 选择导出数据类型 +- 导出为 Excel 或 PDF 格式 +- 提供数据统计图表导出 + +### 3.5 报告查看与解读 + +- 化验单批量查看(按检查日期排序) +- 高亮显示异常指标 +- 快速查看化验结果 +- 提供历史化验结果对比 +- 报告标注备注(标注重要发现,添加医生个人备注) +- 编辑或删除已有备注 +- 管理报告备注信息 +- 搜索特定备注内容 +- 随访状态标记 + +--- + +## 四、PC 管理后台 + +### 4.1 系统管理 + +**账号权限配置** +- 管理员/医生/护士/客服角色 +- 创建角色、定义权限 +- 审核权限变更请求 +- 调整角色权限设置 +- 根据职责分配角色 +- 更新用户角色信息 +- 审核角色权限分配 +- 确认权限分配合理性 + +**操作日志** +- 记录用户操作 +- 审查操作日志 +- 分析异常操作模式 + +### 4.2 患者管理 + +- **患者列表**:患者基本信息展示 +- **档案查看/编辑**: + - 查看患者信息 + - 患者状态追踪 + - 纠正错误信息 + - 查看患者健康记录 + - 编辑患者健康信息 + - 设置档案访问权限 + - 审核档案访问请求 +- **数据导出 Excel**: + - 选择导出字段 + - 设置导出格式 + - 分析导出数据 + - 生成报告 + +### 4.3 健康数据中心 + +- **透析数据统计**: + - 计算透析次数 + - 分析透析完成率 + - 评估透析效果 +- **异常指标排行**: + - 异常健康指标统计 + - 指标预警排行 + - 排序异常指标 + - 展示排行结果 +- **上报率统计**: + - 统计数据上报完成度 + - 计算上报率 + +### 4.4 咨询管理 + +- **医生排班**: + - 设置医生工作时间 + - 创建排班计划 + - 安排医生轮班 + - 更新排班信息 +- **对话记录查看/导出**: + - 导出对话内容 + - 保存对话记录 + +### 4.5 商城管理 + +**商品/分类/资质管理** +- 添加新商品、更新商品信息 +- 创建商品分类 +- 审核商家资质 +- 确认资质有效性 + +**订单/发货/退款处理** +- 确认订单信息 +- 安排发货信息 +- 审核退款原因 +- 处理退款 + +**库存管理** +- 处理库存不足情况 +- 调整库存预警 +- 库存设置 + +**营收统计** +- 计算销售总额 +- 分析收入趋势 +- 计算商品成本 +- 分析成本结构 + +### 4.6 内容管理 + +**公告管理** +- 编写公告内容 +- 审核公告信息 +- 发布/下架公告 +- 管理公告展示 + +**科普文章** +- 编写科普内容 +- 审核科普文章 +- 发布到指定栏目 + +**首页轮播配置** +- 设计轮播图内容 +- 设置轮播时间间隔 + +### 4.7 统计报表 + +- **患者增长**:计算新增患者数、患者数量,分析患者流失率,制作增长趋势图 +- **咨询量**:计算咨询总次数,分析咨询高峰时段,分析咨询主题,提炼常见问题 +- **随访完成率**:记录随访任务完成情况,计算随访完成率,提出改进建议 +- **商城销售数据**:计算各商品销售量,分析销售额和趋势,分析利润变化原因 + +### 4.8 系统设置 + +- **消息推送配置**:编辑推送消息内容,设置推送时间和方式 +- **数据备份**:设定备份周期,确认备份完整性 +- **合规声明配置**:编写合规声明内容,更新合规声明版本,在系统中展示声明,确保用户阅读并同意声明 + +--- + +## 五、与现有 HMS 系统的差异点(初步分析) + +> 以下为客户需求与当前已实现 HMS 健康模块的对比,供后续需求评审参考。 + +| 客户需求 | 现有 HMS | 差异 | +|----------|----------|------| +| 肾病/血透专科方向 | 通用健康管理 | 需增加血透相关实体(透析记录、干体重等) | +| 医疗耗材商城 | 无 | 全新模块 | +| 微信支付集成 | 无 | 需对接微信支付 | +| 患者端微信小程序 | Taro 小程序(通用健康) | 需适配专科场景 | +| 医护端微信小程序 | 无(仅有 PC 管理后台) | 全新端 | +| AI 趋势分析/预警 | erp-ai 模块(基础) | 需增强专科 AI 分析能力 | +| 多就诊人管理(家属代绑) | 无 | 需新增 | +| 快递/到院自提配送 | 无 | 需新增 | +| 科普文章/内容管理 | 无 | 需新增 CMS 功能 | +| 合规声明 | 无 | 需新增 | + +--- + +## 六、原始资料清单 + +| 文件 | 类型 | 内容说明 | +|------|------|----------| +| `管理系统功能文档(1).xlsx` | Excel | 三端功能清单(系统模块/一级功能/二级功能描述) | +| `微信图片_20260424100721_255_611.png` | 图片 | 医护端功能思维导图(数据概览/患者管理/咨询回复/随访管理/报告解读) | +| `微信图片_20260424100722_256_611.png` | 图片 | 患者端功能思维导图(首页/数据上报/预约随访/商城/咨询/我的中心) | +| `微信图片_20260424100723_257_611.png` | 图片 | PC管理后台功能思维导图(系统管理/患者管理/数据中心/咨询管理/商城管理/内容管理/统计报表/系统设置) | diff --git a/docs/健康管理/微信图片_20260424100721_255_611.png b/docs/健康管理/微信图片_20260424100721_255_611.png new file mode 100644 index 0000000..ea3d6e8 Binary files /dev/null and b/docs/健康管理/微信图片_20260424100721_255_611.png differ diff --git a/docs/健康管理/微信图片_20260424100722_256_611.png b/docs/健康管理/微信图片_20260424100722_256_611.png new file mode 100644 index 0000000..1fd0e0e Binary files /dev/null and b/docs/健康管理/微信图片_20260424100722_256_611.png differ diff --git a/docs/健康管理/微信图片_20260424100723_257_611.png b/docs/健康管理/微信图片_20260424100723_257_611.png new file mode 100644 index 0000000..b721d15 Binary files /dev/null and b/docs/健康管理/微信图片_20260424100723_257_611.png differ diff --git a/docs/健康管理/管理系统功能文档(1).xlsx b/docs/健康管理/管理系统功能文档(1).xlsx new file mode 100644 index 0000000..718cecd Binary files /dev/null and b/docs/健康管理/管理系统功能文档(1).xlsx differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..596c98d --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "erp", + "private": true, + "scripts": { + "start:dev": "powershell -ExecutionPolicy Bypass -File ./dev.ps1", + "start:stop": "powershell -ExecutionPolicy Bypass -File ./dev.ps1 -Stop", + "start:restart": "powershell -ExecutionPolicy Bypass -File ./dev.ps1 -Restart", + "start:status": "powershell -ExecutionPolicy Bypass -File ./dev.ps1 -Status" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..9b60ae1 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,9 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} diff --git a/scripts/api_test_mp.py b/scripts/api_test_mp.py new file mode 100644 index 0000000..b727c72 --- /dev/null +++ b/scripts/api_test_mp.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +"""Miniprogram API comprehensive test - 6 endpoints.""" +import json, urllib.request, urllib.error, sys + +BASE = 'http://localhost:3000/api/v1' +# Read fresh token from file +with open('g:/hms/.test_token_fresh.txt') as f: + TOKEN = f.read().strip() +PATIENT_ID = '019dcd34-bc4d-72c1-8c19-77ce1f4839d6' +TENANT_ID = '019d80da-7a2c-7820-b0a3-3d5266a3a324' + +headers = { + 'Authorization': f'Bearer {TOKEN}', + 'X-Tenant-Id': TENANT_ID, + 'X-Patient-Id': PATIENT_ID, + 'Content-Type': 'application/json' +} + + +def api_call(method, path, data=None): + url = f'{BASE}{path}' + if data: + req = urllib.request.Request(url, data=json.dumps(data).encode(), headers=headers, method=method) + else: + req = urllib.request.Request(url, headers=headers, method=method) + try: + with urllib.request.urlopen(req, timeout=10) as resp: + body = json.loads(resp.read().decode()) + return resp.status, body + except urllib.error.HTTPError as e: + body = e.read().decode() + try: + return e.code, json.loads(body) + except Exception: + return e.code, body + except Exception as e: + return 0, str(e) + + +def get_keys(obj, prefix=''): + """Recursively extract field names from a dict.""" + fields = [] + if isinstance(obj, dict): + for k, v in obj.items(): + full = f'{prefix}{k}' if not prefix else f'{prefix}.{k}' + if isinstance(v, dict): + fields.extend(get_keys(v, full)) + else: + fields.append(full) + return fields + + +def print_header(title, api_path): + print() + print('=' * 70) + print(f'{title}') + print(f'Endpoint: {api_path}') + print('=' * 70) + + +def analyze_fields(name, data, expected_fields): + """Compare actual vs expected fields.""" + actual_keys = set(data.keys()) if isinstance(data, dict) else set() + expected_set = set(expected_fields) + matched = actual_keys & expected_set + missing = expected_set - actual_keys + extra = actual_keys - expected_set + + match_status = "MATCH" if not missing else "MISMATCH" + print(f'[{name}] status=200 | {match_status}') + print(f' Actual fields: {sorted(actual_keys)}') + print(f' Expected fields: {sorted(expected_set)}') + if matched: + print(f' Matched: {sorted(matched)}') + if missing: + print(f' ** MISSING: {sorted(missing)} **') + if extra: + print(f' Extra: {sorted(extra)}') + return missing + + +# ===================================================== +# API 1: GET /health/vital-signs/today +# ===================================================== +print_header('API 1: Today Summary', f'GET /health/vital-signs/today?patient_id={PATIENT_ID}') +status, body = api_call('GET', f'/health/vital-signs/today?patient_id={PATIENT_ID}') +print(f'status={status}') +print(f'Response: {json.dumps(body, indent=2, ensure_ascii=False)}') + +if status == 200: + data = body.get('data', body) + expected = ['blood_pressure', 'heart_rate', 'blood_sugar', 'weight'] + missing = analyze_fields('Today Summary', data, expected) + # Check nested structure + for key in ['blood_pressure', 'heart_rate', 'blood_sugar', 'weight']: + if key in data and isinstance(data[key], dict): + print(f' {key} sub-fields: {sorted(data[key].keys())}') +else: + print(f' FAILED: {status}') + # Try without patient_id + print(' Retrying without patient_id param...') + status2, body2 = api_call('GET', '/health/vital-signs/today') + print(f' status={status2}') + print(f' Response: {json.dumps(body2, indent=2, ensure_ascii=False)}') + +# ===================================================== +# API 2: POST /health/patients/{id}/vital-signs +# ===================================================== +print_header('API 2: Create Vital Signs', f'POST /health/patients/{PATIENT_ID}/vital-signs') +create_data = { + 'record_date': '2026-04-27', + 'systolic_bp_morning': 130, + 'diastolic_bp_morning': 85, + 'heart_rate': 75, + 'weight': 69.0, + 'blood_sugar': 5.5, +} +status, body = api_call('POST', f'/health/patients/{PATIENT_ID}/vital-signs', create_data) +print(f'status={status}') +print(f'Response: {json.dumps(body, indent=2, ensure_ascii=False)}') + +if status in (200, 201): + data = body.get('data', body) + actual_keys = set(data.keys()) if isinstance(data, dict) else set() + expected_keys = {'id', 'patient_id', 'record_date', 'systolic_bp_morning', 'diastolic_bp_morning', + 'heart_rate', 'weight', 'blood_sugar', 'created_at', 'updated_at', 'version', 'tenant_id'} + analyze_fields('Create Vital Signs', data, expected_keys) + +# ===================================================== +# API 3: GET /health/patients/{id}/vital-signs (paginated) +# ===================================================== +print_header('API 3: Vital Signs History', f'GET /health/patients/{PATIENT_ID}/vital-signs?page=1&page_size=5') +status, body = api_call('GET', f'/health/patients/{PATIENT_ID}/vital-signs?page=1&page_size=5') +print(f'status={status}') +print(f'Response: {json.dumps(body, indent=2, ensure_ascii=False)}') + +if status == 200: + data = body.get('data', body) + # Check pagination structure + if isinstance(body.get('data'), dict) and 'data' in body['data']: + # wrapped: {data: {data: [...], total: N}} + paginated = body['data'] + print(f' Pagination structure: data={type(paginated.get("data")).__name__}, total={paginated.get("total")}') + items = paginated.get('data', []) + elif isinstance(body.get('data'), list): + items = body['data'] + print(f' Response is direct array, len={len(items)}') + elif isinstance(body.get('data'), dict) and 'items' in body.get('data', {}): + paginated = body['data'] + items = paginated.get('items', []) + print(f' Pagination via items: total={paginated.get("total")}') + else: + items = [] + print(f' Unknown pagination structure') + + if items and isinstance(items[0], dict): + actual_keys = set(items[0].keys()) + expected_keys = {'id', 'record_date', 'systolic_bp_morning', 'diastolic_bp_morning', + 'heart_rate', 'weight', 'blood_sugar', 'created_at', 'updated_at', 'patient_id'} + analyze_fields('Vital Signs Item', items[0], expected_keys) + +# ===================================================== +# API 4: GET /health/vital-signs/trend (mini trend) +# ===================================================== +print_header('API 4: Vital Signs Trend', 'GET /health/vital-signs/trend?indicator=blood_pressure&range=7d') +status, body = api_call('GET', '/health/vital-signs/trend?indicator=blood_pressure&range=7d') +print(f'status={status}') +print(f'Response: {json.dumps(body, indent=2, ensure_ascii=False)}') + +# Also try the task-specified path +print() +print(' Also testing: GET /health/patients/{id}/vital-signs/trend') +status2, body2 = api_call('GET', f'/health/patients/{PATIENT_ID}/vital-signs/trend?start_date=2026-04-20&end_date=2026-04-27') +print(f' status={status2}') +print(f' Response: {json.dumps(body2, indent=2, ensure_ascii=False)}') + +if status == 200: + data = body.get('data', body) + print(f' Top-level type: {type(data).__name__}') + if isinstance(data, dict): + print(f' Keys: {sorted(data.keys())}') + elif isinstance(data, list): + print(f' Array length: {len(data)}') + if data: + print(f' First item keys: {sorted(data[0].keys()) if isinstance(data[0], dict) else data[0]}') + +# ===================================================== +# API 5: GET /health/appointments +# ===================================================== +print_header('API 5: Appointments List', f'GET /health/appointments?patient_id={PATIENT_ID}&page=1&page_size=5') +status, body = api_call('GET', f'/health/appointments?patient_id={PATIENT_ID}&page=1&page_size=5') +print(f'status={status}') +print(f'Response: {json.dumps(body, indent=2, ensure_ascii=False)}') + +if status == 200: + data = body.get('data', body) + if isinstance(data, dict) and 'data' in data: + items = data.get('data', []) + total = data.get('total', 'N/A') + print(f' Pagination: total={total}, items={len(items)}') + elif isinstance(data, list): + items = data + else: + items = [] + + if items and isinstance(items[0], dict): + actual_keys = set(items[0].keys()) + # From miniprogram appointment.ts Appointment interface + expected_keys = {'id', 'patient_name', 'doctor_name', 'department', + 'appointment_date', 'start_time', 'end_time', 'status', 'version'} + analyze_fields('Appointment', items[0], expected_keys) + +# ===================================================== +# API 6: GET /health/follow-up-tasks +# ===================================================== +print_header('API 6: Follow-Up Tasks', f'GET /health/follow-up-tasks?patient_id={PATIENT_ID}&status=pending&page=1&page_size=5') +status, body = api_call('GET', f'/health/follow-up-tasks?patient_id={PATIENT_ID}&status=pending&page=1&page_size=5') +print(f'status={status}') +print(f'Response: {json.dumps(body, indent=2, ensure_ascii=False)}') + +if status == 200: + data = body.get('data', body) + if isinstance(data, dict) and 'data' in data: + items = data.get('data', []) + total = data.get('total', 'N/A') + print(f' Pagination: total={total}, items={len(items)}') + elif isinstance(data, list): + items = data + else: + items = [] + + if items and isinstance(items[0], dict): + actual_keys = set(items[0].keys()) + # From miniprogram followup.ts FollowUpTask interface + expected_keys = {'id', 'patient_id', 'patient_name', 'follow_up_type', + 'content_template', 'status', 'planned_date', 'version'} + analyze_fields('FollowUpTask', items[0], expected_keys) + elif not items: + print(' No pending tasks. Retrying without status filter...') + status2, body2 = api_call('GET', f'/health/follow-up-tasks?patient_id={PATIENT_ID}&page=1&page_size=5') + print(f' status={status2}') + data2 = body2.get('data', body2) if status2 == 200 else {} + if isinstance(data2, dict) and 'data' in data2: + items2 = data2.get('data', []) + print(f' Items without status filter: {len(items2)}') + if items2 and isinstance(items2[0], dict): + actual_keys = set(items2[0].keys()) + expected_keys = {'id', 'patient_id', 'patient_name', 'follow_up_type', + 'content_template', 'status', 'planned_date', 'version'} + analyze_fields('FollowUpTask (all)', items2[0], expected_keys) + +print() +print('=' * 70) +print('TEST COMPLETE') +print('=' * 70) diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..1632278 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "skills": { + "huashu-design": { + "source": "C:\\Users\\szend\\AppData\\Local\\Temp\\huashu-design", + "sourceType": "local", + "computedHash": "aedfa8eedd540a46aaa74a8c113bef80c81c8ead52c6714fbc261b74c0aebf2e" + } + } +} diff --git a/snapshot-menu-config.txt b/snapshot-menu-config.txt new file mode 100644 index 0000000..b0c8f94 --- /dev/null +++ b/snapshot-menu-config.txt @@ -0,0 +1,97 @@ +uid=83_0 RootWebArea "ERP Platform" url="http://localhost:5174/#/settings" + uid=83_1 link "跳转到主要内容" url="http://localhost:5174/#root" + uid=83_2 StaticText "跳转到主要内容" + uid=83_3 complementary + uid=83_4 StaticText "E" + uid=83_5 StaticText "ERP Platform" + uid=83_6 StaticText "基础模块" + uid=83_7 image "home" + uid=83_8 StaticText "工作台" + uid=83_9 image "user" + uid=83_10 StaticText "用户管理" + uid=83_11 image "safety" + uid=83_12 StaticText "权限管理" + uid=83_13 image "apartment" + uid=83_14 StaticText "组织架构" + uid=83_15 StaticText "业务模块" + uid=83_16 image "partition" + uid=83_17 StaticText "工作流" + uid=83_18 image "message" + uid=83_19 StaticText "消息中心" + uid=83_20 StaticText "健康管理" + uid=83_21 image "dashboard" + uid=83_22 StaticText "统计报表" + uid=83_23 image "team" + uid=83_24 StaticText "患者管理" + uid=83_25 image "medicine-box" + uid=83_26 StaticText "医护管理" + uid=83_27 image "calendar" + uid=83_28 StaticText "预约排班" + uid=83_29 image "heart" + uid=83_30 StaticText "排班管理" + uid=83_31 image "phone" + uid=83_32 StaticText "随访管理" + uid=83_33 image "comment" + uid=83_34 StaticText "咨询管理" + uid=83_35 image "tags" + uid=83_36 StaticText "标签管理" + uid=83_37 image "trophy" + uid=83_38 StaticText "积分规则" + uid=83_39 image "shop" + uid=83_40 StaticText "商品管理" + uid=83_41 image "file-text" + uid=83_42 StaticText "订单管理" + uid=83_43 image "calendar" + uid=83_44 StaticText "线下活动" + uid=83_45 image "robot" + uid=83_46 StaticText "AI Prompt 管理" + uid=83_47 image "history" + uid=83_48 StaticText "AI 分析历史" + uid=83_49 image "bar-chart" + uid=83_50 StaticText "AI 用量统计" + uid=83_51 StaticText "系统" + uid=83_52 image "setting" + uid=83_53 StaticText "系统设置" + uid=83_54 image "appstore" + uid=83_55 StaticText "插件管理" + uid=83_56 banner + uid=83_57 image "menu-fold" + uid=83_58 StaticText "系统设置" + uid=83_59 image "search" + uid=83_60 image "bulb" + uid=83_61 image "bell" + uid=83_62 StaticText "4" + uid=83_63 StaticText "系" + uid=83_64 StaticText "系统管理员" + uid=83_65 main + uid=85_0 heading "系统设置" level="4" + uid=85_1 StaticText "管理系统参数、字典、菜单、主题等配置" + uid=85_2 tab "book 数据字典" selectable selected + uid=85_3 tab "global 语言管理" selectable + uid=85_4 tab "menu 菜单配置" selectable + uid=85_5 tab "number 编号规则" selectable + uid=85_6 tab "setting 系统参数" selectable + uid=85_7 tab "bg-colors 主题设置" selectable + uid=85_8 tab "audit 审计日志" selectable + uid=85_9 tab "lock 修改密码" selectable + uid=85_10 tabpanel "book 数据字典" + uid=85_11 heading "数据字典管理" level="5" + uid=85_12 button "plus 新建字典" + uid=85_13 generic live="polite" relevant="additions text" + uid=85_14 StaticText "名称" + uid=85_15 StaticText "编码" + uid=85_16 StaticText "说明" + uid=85_17 StaticText "操作" + uid=85_18 button expandable + uid=85_19 StaticText "测试字典-已修改" + uid=85_20 StaticText "test_dict" + uid=85_21 StaticText "验证用" + uid=85_22 button "添加项" + uid=85_23 button "编 辑" + uid=85_24 button "删 除" + uid=85_25 button "left" disableable disabled + uid=85_26 listitem level="1" + uid=85_27 StaticText "1" + uid=85_28 button "right" disableable disabled + uid=83_141 contentinfo + uid=83_142 StaticText "HMS 健康管理平台"