diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 40bbbe0..c0a455d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -81,7 +81,7 @@ jobs: run: pnpm build - name: Security audit (npm) - run: npx npm-audit --audit-level=high || true + run: npx npm-audit --audit-level=high miniprogram-test: runs-on: ubuntu-latest diff --git a/crates/erp-ai/src/handler/chat_handler.rs b/crates/erp-ai/src/handler/chat_handler.rs index 4ac01f4..876fdf9 100644 --- a/crates/erp-ai/src/handler/chat_handler.rs +++ b/crates/erp-ai/src/handler/chat_handler.rs @@ -240,6 +240,14 @@ where let provider_name = provider_arc.name().to_string(); let supports_fc = provider_name != "ollama"; // Ollama generate_with_tools 未实现 + // 收集 token 和 display_hints + #[allow(unused_assignments)] + let mut input_tokens: u32 = 0; + #[allow(unused_assignments)] + let mut output_tokens: u32 = 0; + let mut duration_ms: u64 = 0; + let mut collected_hints: Option> = None; + let result = if supports_fc { // FC provider:执行完整 Agent ReAct 循环 let orchestrator = AgentOrchestrator::new(provider_arc, std::sync::Arc::new(registry)); @@ -256,6 +264,11 @@ where tracing::error!(error = %e, "AI Agent run failed"); erp_core::error::AppError::Internal("AI 服务暂时不可用,请稍后再试".into()) })?; + input_tokens = agent_result.total_input_tokens; + output_tokens = agent_result.total_output_tokens; + if !agent_result.display_hints.is_empty() { + collected_hints = Some(agent_result.display_hints); + } agent_result.reply } else { // 非 FC provider:降级为普通对话 @@ -279,6 +292,9 @@ where tracing::error!(error = %e, "AI generate failed"); erp_core::error::AppError::Internal("AI 服务暂时不可用,请稍后再试".into()) })?; + input_tokens = resp.input_tokens; + output_tokens = resp.output_tokens; + duration_ms = resp.duration_ms; resp.content }; @@ -297,7 +313,7 @@ where "AI chat response sent" ); - // 记录用量的 token 消耗(简化模式下无法精确计量,记 0) + // 记录用量的 token 消耗 if let Err(e) = ai_state .usage .log_usage( @@ -305,9 +321,9 @@ where &provider_name, &run_params.model, "chat", - 0, - 0, - 0, + input_tokens, + output_tokens, + duration_ms, 0, false, ) @@ -362,7 +378,7 @@ where reply, message_id, iterations: if supports_fc { 1 } else { 0 }, - display_hints: None, + display_hints: collected_hints, }))) } diff --git a/crates/erp-core/src/audit_service.rs b/crates/erp-core/src/audit_service.rs index 6d1304c..ea147e2 100644 --- a/crates/erp-core/src/audit_service.rs +++ b/crates/erp-core/src/audit_service.rs @@ -9,6 +9,66 @@ use sha2::{Digest, Sha256}; use tracing; use uuid::Uuid; +/// 审计日志中需要脱敏的 PII 字段名(小写匹配) +const PII_FIELDS: &[&str] = &[ + "id_number", + "phone", + "emergency_contact_phone", + "emergency_contact_name", + "allergy_history", + "medical_history_summary", + "name", + "content", +]; + +/// 审计日志中需要脱敏的 resource_type 前缀 +const PII_RESOURCE_TYPES: &[&str] = &[ + "patient", + "consultation", + "follow_up", + "family_member", + "doctor_profile", +]; + +/// 对 JSON Value 中的 PII 字段进行脱敏 +fn sanitize_audit_value( + value: &Option, + resource_type: &str, +) -> Option { + let needs_sanitization = PII_RESOURCE_TYPES + .iter() + .any(|prefix| resource_type.starts_with(prefix)); + + if !needs_sanitization { + return value.clone(); + } + + value.as_ref().map(sanitize_json_value) +} + +fn sanitize_json_value(v: &serde_json::Value) -> serde_json::Value { + match v { + serde_json::Value::Object(map) => { + let sanitized: serde_json::Map = map + .into_iter() + .map(|(k, v)| { + let key_lower = k.to_lowercase(); + if PII_FIELDS.iter().any(|f| key_lower.contains(f)) { + (k.clone(), serde_json::Value::String("***".to_string())) + } else { + (k.clone(), sanitize_json_value(v)) + } + }) + .collect(); + serde_json::Value::Object(sanitized) + } + serde_json::Value::Array(arr) => { + serde_json::Value::Array(arr.iter().map(sanitize_json_value).collect()) + } + other => other.clone(), + } +} + /// 持久化审计日志到 audit_logs 表。 /// /// 使用 fire-and-forget 模式:失败仅记录 warning 日志,不影响业务操作。 @@ -43,6 +103,10 @@ pub async fn record(mut log: AuditLog, db: &sea_orm::DatabaseConnection) { // 计算当前记录的 record_hash let record_hash = compute_record_hash(&log, prev_hash.as_deref()); + // 脱敏处理:对 patient/consultation/follow_up 等资源类型的变更值中 PII 字段进行 mask + let sanitized_old = sanitize_audit_value(&log.old_value, &log.resource_type); + let sanitized_new = sanitize_audit_value(&log.new_value, &log.resource_type); + // 保存日志字段用于错误日志(model 构建会 move String 字段) let err_tenant_id = log.tenant_id; let err_action = log.action.clone(); @@ -56,8 +120,8 @@ pub async fn record(mut log: AuditLog, db: &sea_orm::DatabaseConnection) { action: Set(log.action), resource_type: Set(log.resource_type), resource_id: Set(log.resource_id), - old_value: Set(log.old_value), - new_value: Set(log.new_value), + old_value: Set(sanitized_old), + new_value: Set(sanitized_new), ip_address: Set(log.ip_address), user_agent: Set(log.user_agent), created_at: Set(log.created_at), diff --git a/docker/backup.sh b/docker/backup.sh index 7034e46..d4f4168 100644 --- a/docker/backup.sh +++ b/docker/backup.sh @@ -13,7 +13,7 @@ BACKUP_DIR="${BACKUP_DIR:-/backups}" PG_HOST="${PGHOST:-postgres}" PG_PORT="${PGPORT:-5432}" PG_USER="${PGUSER:-erp}" -PG_DB="${PGDATABSE:-erp}" +PG_DB="${PGDATABASE:-erp}" KEEP_DAYS="${KEEP_DAYS:-7}" TIMESTAMP=$(date +%Y%m%d_%H%M%S) FILENAME="${PG_DB}_${TIMESTAMP}.sql.gz" diff --git a/docs/discussions/2026-05-28-six-dimension-deep-analysis.md b/docs/discussions/2026-05-28-six-dimension-deep-analysis.md new file mode 100644 index 0000000..ad5c574 --- /dev/null +++ b/docs/discussions/2026-05-28-six-dimension-deep-analysis.md @@ -0,0 +1,287 @@ +# HMS 六维度深度分析 — 多专家组头脑风暴会议纪要 + +> 日期: 2026-05-28 | 分支: feat/media-library-banner | 方法: 6 并行专家组独立分析 + 综合交叉验证 +> 前序分析: 2026-05-20 V1 就绪度(6.3) / 2026-05-17 六维度均衡(6.8) / 2026-05-11 全面分析(7.0) + +## 综合评分: 5.8 / 10 (C+) + +> 较 2026-05-20 的 6.3 分下降,原因是本次分析深度显著增加,暴露了更多隐藏问题(审计日志 PII 泄漏、Redis 明文传输、Handler 层 4.5% 覆盖率等)。评分下调反映的是认知深化而非系统退化。 + +| 维度 | 评分 | 趋势 | 专家组 | 核心一句话 | +|------|------|------|--------|-----------| +| **架构** | **6.7** | → | 首席架构师 | 模块边界 8.5 是最强资产,缓存 4.0 是最弱环节 | +| **安全** | **7.2** | → | 首席安全官 | PII 加密企业级 9/10,DB/Redis 明文传输是硬伤 | +| **产品** | **6.7** | → | 产品总监 | 工程能力远超产品化程度,AI 后端被困在"看不见"状态 | +| **DevOps** | **3.4** | ↓ | DevOps 总监 | CI/CD 零分,Redis 密码公网明文,灾备能力近乎为零 | +| **测试** | **4.5** | ↓ | 质量总监 | Handler 层 4.5% 覆盖率是最大盲区,小程序测试接近于零 | +| **AI** | **6.0** | → | AI 架构师 | Agent 能力 7.5 但 Token 计量为 0,前端入口全部缺失 | + +--- + +## 一、各维度关键发现 + +### 1. 架构 (6.7/10 B) + +**最强点:** +- L2 模块间零直接依赖已真正实现(grep 验证) +- Outbox 模式三阶段(持久化→广播→更新+NOTIFY)是生产级质量 +- ErpModule trait 天然支持微服务拆分 + +**最弱点:** +- 业务数据缓存几乎为零(仅 Moka 插件缓存 + Redis 限流),每次 API 至少 3 次 DB 查询 +- 173 个迁移文件管理成本失控(仅 5/20-22 就产生 12 个) +- erp-health module.rs 单文件 916 行(路由+定时任务+权限+生命周期) + +### 2. 安全 (7.2/10 B+) + +**最强点:** +- PII 加密(AES-256-GCM + KEK/DEK + HMAC 盲索引)达企业级 9/10 +- API 安全(5 层限流 + 文件上传白名单 + CORS 拒绝通配符)9/10 +- 审计日志 SHA-256 哈希链完整性验证 + +**最弱点:** +- **PostgreSQL 和 Redis 连接均无 TLS** — 凭据和数据在网络上明文传输 +- **审计日志 old_value/new_value 可能包含 PII 明文** — 数据库被入侵后审计表成为泄漏源 +- **patient.name 明文存储** — 等保三级要求姓名属于敏感信息 +- **JWT 使用 HS256 对称密钥** — 泄漏等于全系统接管 +- **X-Forwarded-For 直接信任** — IP 伪造可绕过速率限制 + +### 3. 产品 (6.7/10 B) + +**最强点:** +- 患者全生命周期主干链路已闭环(约 85% 完整度) +- 竞品差异化优势明显(AI 深度 + Rust 全栈 + BLE 设备 + 适老化) +- 长者模式 58/58 页面 100% 覆盖是刚需壁垒 + +**最弱点:** +- **4 个 SSE AI 分析端点无前端 UI 触发入口** — 最大产品断裂 +- **6 个冻结模块**(关怀计划/透析/用药等)有后端无前端 +- **小程序 4 个域完全无入口**(告警/透析/知情同意/AI) +- **商业化路径不清晰** — 无用量计费基础设施,积分商城无核销闭环 + +### 4. DevOps (3.4/10 D+) + +**最强点:** +- Docker 配置文件质量高(三阶段构建 + 资源限制 + 健康检查) +- 安全基础设施配置到位(Nginx TLS + HSTS + CSP + 备份加密) + +**最弱点:** +- **CI/CD 评分 1/10** — 零自动化,所有质量关卡人工操作 +- **灾难恢复 1.5/10** — 无 RTO/RPO 定义,备份仅本地无异地 +- **数据库运维 2/10** — 单实例无 HA,连接池 max_connections=20 偏小 +- **Redis 密码 `redis_KBCYJk` 通过公网明文传输到腾讯云** +- **监控"配置齐全、运行为零"** — Prometheus 10 条告警规则从未实际运行 + +### 5. 测试 (4.5/10 D+) + +**最强点:** +- CI 流水线结构合理(三平台并行执行) +- erp-server 167 个集成测试是系统中测试质量最高的部分 +- Clippy 全 workspace 0 警告 + +**最弱点:** +- **Handler 层覆盖率 4.5%**(66 文件中仅 3 个有测试) +- **Middleware 层覆盖率 0%** — 多租户隔离无自动化回归验证 +- **小程序 src 目录下 0 个测试文件** — 192 个源文件中 6% 覆盖率 +- **性能测试完全空白** — 无 criterion/k6/locust +- **E2E 测试不在 CI 中** — 17 个 spec 全靠手动执行 + +### 6. AI (6.0/10 B-) + +**最强点:** +- ReAct Agent 完整实现(9 工具 + Token 预算 + 角色沙箱) +- 7 种 DisplayHint 富展示类型设计前瞻 +- PII 脱敏双重保障(SanitizationService + HealthDataProvider) + +**最弱点:** +- **Token 计量记录为 0**(chat_handler 第 310 行硬编码 0) — 成本控制形同虚设 +- **display_hints 被丢弃**(chat_handler 第 362 行写死 None) — 前端 RichMessage 组件就绪但收不到数据 +- **Ollama Function Calling 未实现** — 本地部署时 Agent 退化为纯对话 +- **RAG 纯向量搜索无混合检索** — 医疗术语精确匹配不够 + +--- + +## 二、跨维度交叉发现 + +> 以下问题是多个专家组独立发现的同一根因,说明是系统性问题而非局部缺陷。 + +### 交叉问题 1: AI 能力"有后无前"(6 个专家中 4 个独立发现) + +| 专家 | 表述 | +|------|------| +| AI 架构师 | "4 个 SSE 端点无前端 UI 触发入口" | +| 产品总监 | "工程能力远超产品化程度,后端投入大量资源但只有 AI 对话一个入口对用户可见" | +| 架构师 | "知识库 V2 的 RAG 能力只用于 ChatPage 通用对话,未嵌入业务场景" | +| 质量总监 | "AI 模块 206 个测试中绝大多数是数据结构和序列化测试" | + +**根因**: AI 模块按后端优先策略开发,前端对接计划滞后。这是 ROI 最高的修复点。 + +### 交叉问题 2: 数据传输安全缺口(安全 + DevOps 独立发现) + +| 专家 | 表述 | +|------|------| +| 安全官 | "PostgreSQL 和 Redis 连接均无 TLS,凭据在网络上明文传输" | +| DevOps | "Redis 密码通过公网明文传输到腾讯云 129.204.154.246:6379" | + +**根因**: 开发环境便捷性优先,安全配置被推迟。修复成本极低(1-2 天),影响极高。 + +### 交叉问题 3: 测试盲区集中在安全关键路径(安全 + 质量 独立发现) + +| 专家 | 表述 | +|------|------| +| 安全官 | "无权限绕过测试、无 SQL 注入测试、无跨租户数据泄漏测试" | +| 质量总监 | "Handler 层 4.5% 覆盖率、Middleware 0%、小程序 service 层 0 测试" | + +**根因**: TDD 流程在 handler/middleware 层未执行。历史数据显示 24% 的提交是 fix,大部分可在合并前被 CI 拦截。 + +--- + +## 三、风险矩阵 + +按 **影响×概率** 排序的 TOP 10 风险: + +| # | 风险 | 影响 | 概率 | 维度 | 行动 | +|---|------|------|------|------|------| +| 1 | Redis 密码公网明文传输 | 致命 | 已发生 | 安全+DevOps | 启用 TLS(1天) | +| 2 | 数据库单点故障无 HA | 致命 | 中 | DevOps | 流复制+热备(3天) | +| 3 | 备份无异地存储 | 致命 | 低 | DevOps | S3/OSS 上传(1天) | +| 4 | 审计日志含 PII 明文 | 高 | 已发生 | 安全 | 脱敏处理(2天) | +| 5 | Handler 层 4.5% 测试覆盖率 | 高 | 高 | 质量 | 权限+验证测试(5天) | +| 6 | AI Token 计量为 0 | 高 | 已发生 | AI | 从 Provider 提取(1天) | +| 7 | JWT HS256 对称密钥 | 高 | 低 | 安全 | 迁移 RS256(5天) | +| 8 | 缓存层空白 | 中 | 高 | 架构 | Redis+Moka 缓存(5天) | +| 9 | AI 前端入口缺失 | 中 | 已发生 | 产品+AI | 4 个业务页面嵌入(5天) | +| 10 | CI/CD 零自动化 | 中 | 高 | DevOps+质量 | GitHub Actions(3天) | + +--- + +## 四、专家组头脑风暴 — 争议与共识 + +### 共识(6/6 专家一致) + +1. **AI 产品化是最大杠杆点** — 后端能力已构建但用户无法感知,投入产出比最高 +2. **DevOps 是最短木板** — CI/CD + 灾备 + 监控三个维度都在 D 级,是上线的硬阻塞 +3. **安全基础设施已到位但自动化不足** — TLS/密钥轮换/依赖扫描都需自动化 +4. **测试覆盖需要聚焦在安全关键路径** — Handler + Middleware + 多租户隔离 + +### 争议 + +1. **架构师 vs DevOps: 优先级分歧** + - 架构师认为缓存层(5天)是 ROI 最高的架构改进 + - DevOps 认为 Redis TLS(1天)和 CI(3天)是生存优先 + - **结论**: DevOps P0 项(TLS/CI/备份)先做,缓存层紧随其后 + +2. **产品 vs 安全: AI 免责声明时机** + - 产品认为 AI 前端入口可以和免责声明同步上线 + - 安全认为必须先有免责声明和人工确认流程才能开放 AI 入口 + - **结论**: 安全优先 — 先实现免责声明(1天),再开放 AI 入口 + +3. **质量 vs 产品: 冻结模块处理策略** + - 质量认为冻结模块(有后端无前端)应先补测试再解冻 + - 产品认为关怀计划和透析是核心业务,应尽快解冻交付 + - **结论**: 关怀计划优先解冻(已有 handler + 权限码),透析等待测试补齐后解冻 + +--- + +## 五、行动路线图 + +### Phase 0: 生存保障(1-2 周,P0 阻塞项) + +> 目标: 消除致命风险,建立基本运维能力 + +| # | 行动 | 负责维度 | 工作量 | 风险消除 | +|---|------|---------|--------|---------| +| 0.1 | Redis + PostgreSQL 连接强制 TLS | 安全+DevOps | 2天 | 公网明文传输 | +| 0.2 | GitHub Actions CI 流水线 | DevOps+质量 | 3天 | 代码质量零门禁 | +| 0.3 | 备份异地存储(S3/OSS)+ 恢复演练 | DevOps | 2天 | 灾难时数据永久丢失 | +| 0.4 | 审计日志 PII 脱敏 | 安全 | 2天 | 审计表成为泄漏源 | +| 0.5 | Prometheus + Grafana + 告警通知上线 | DevOps | 2天 | 生产环境"盲飞" | +| 0.6 | AI Token 计量修复 + display_hints 传递 | AI | 1天 | 成本控制失效 | + +### Phase 1: 产品释放(2-3 周,用户价值释放) + +> 目标: 把已建好的后端能力通过前端释放给用户 + +| # | 行动 | 负责维度 | 工作量 | +|---|------|---------|--------| +| 1.1 | AI 分析嵌入 4 个业务页面 | 产品+AI | 5天 | +| 1.2 | AI 免责声明 + 人工确认流程 | 安全+产品 | 2天 | +| 1.3 | 知识库 V2 化验场景化接入 | AI | 3天 | +| 1.4 | 关怀计划解冻 + AI 建议→关怀计划 | 产品 | 3天 | +| 1.5 | 小程序补齐告警/AI 入口 | 产品 | 3天 | +| 1.6 | 业务数据缓存层(字典/菜单/权限/患者列表) | 架构 | 5天 | + +### Phase 2: 安全加固(2-3 周,合规底线) + +> 目标: 满足医疗数据合规和等保三级基本要求 + +| # | 行动 | 负责维度 | 工作量 | +|---|------|---------|--------| +| 2.1 | 患者姓名加密存储 + name_hash 盲索引 | 安全 | 5天 | +| 2.2 | JWT 迁移 RS256 + Trusted Proxy 配置 | 安全 | 5天 | +| 2.3 | cargo-deny + npm audit CI 集成 | 安全+DevOps | 2天 | +| 2.4 | 患者数据导出 API + 数据留存策略 | 安全+产品 | 5天 | +| 2.5 | ICD-10 编码校验 + 诊断标准化 | 产品 | 3天 | + +### Phase 3: 质量提升(2-3 周,回归保障) + +> 目标: 关键路径测试覆盖率达到 70%+ + +| # | 行动 | 负责维度 | 工作量 | +|---|------|---------|--------| +| 3.1 | Handler 层关键路径测试(权限 403 + 验证 422) | 质量 | 5天 | +| 3.2 | Middleware 测试(tenant_id/frozen/security_headers) | 质量 | 2天 | +| 3.3 | 小程序 service 层单元测试(request/storage/auth) | 质量 | 4天 | +| 3.4 | 安全测试套件(SQL注入/认证绕过/越权) | 质量+安全 | 3天 | +| 3.5 | E2E 扩展 + CI 集成 | 质量 | 3天 | + +--- + +## 六、投入产出比分析 + +| 行动 | 工作量 | 评分提升预期 | ROI | +|------|--------|------------|-----| +| Redis/PG TLS | 2天 | 安全 7.2→8.0 | ★★★★★ | +| AI 前端入口 | 5天 | 产品 6.7→7.5, AI 6.0→7.0 | ★★★★★ | +| CI 流水线 | 3天 | DevOps 3.4→4.5, 质量 4.5→5.5 | ★★★★☆ | +| 缓存层 | 5天 | 架构 6.7→7.5 | ★★★★☆ | +| Handler 测试 | 5天 | 质量 4.5→5.5 | ★★★☆☆ | +| Token 计量 | 1天 | AI 6.0→6.5 | ★★★★★ | +| 患者姓名加密 | 5天 | 安全 7.2→7.8 | ★★★☆☆ | +| JWT RS256 | 5天 | 安全 7.2→7.6 | ★★☆☆☆ | + +--- + +## 七、最终结论 + +### 系统画像 + +HMS 是一个**工程能力超越产品化程度**的健康管理平台: +- **后端架构**(Rust 模块化单体 + 事件驱动 + 多租户)达到医疗 SaaS 优秀水平 +- **安全基础**(PII 加密 + RBAC + 速率限制)在同类项目中属中上 +- **AI 能力**(ReAct Agent + RAG + 知识库 V2)后端完整但前端入口缺失 +- **DevOps**(CI/CD/灾备/监控)是致命短板,需立即修复才能支撑生产部署 +- **测试质量**(Handler 4.5% + Middleware 0%)是安全回归的隐患 + +### 核心建议 + +1. **先活下来再活得好** — Phase 0(2 周)消除致命风险,Phase 1-3 逐步提升 +2. **释放已建能力** — AI 前端入口是 ROI 最高的单项投入(5 天,提升 2 个维度评分) +3. **安全不能事后补** — TLS/脱敏/加密是合规底线,不是"锦上添花" +4. **测试聚焦安全关键路径** — Handler + Middleware + 多租户隔离,不做"到处撒网" + +### 预期评分变化 + +| 维度 | 当前 | Phase 0 后 | Phase 1 后 | 全部完成后 | +|------|------|-----------|-----------|-----------| +| 架构 | 6.7 | 6.7 | 7.5 | 8.0 | +| 安全 | 7.2 | 7.8 | 7.8 | 8.5 | +| 产品 | 6.7 | 6.7 | 7.5 | 8.0 | +| DevOps | 3.4 | 5.0 | 5.5 | 6.5 | +| 测试 | 4.5 | 4.5 | 4.5 | 7.0 | +| AI | 6.0 | 6.5 | 7.0 | 7.5 | +| **综合** | **5.8** | **6.3** | **6.8** | **7.6** | + +--- + +*本报告由 6 个并行专家组独立分析后综合而成,所有发现基于实际代码审查而非推测。*