Files
hms/docs/archive/audits-v1/06-error-handling.md
iven 18fa6ce6d4 docs: 全局文档梳理归档 — 删除过期文件 + 归档 V1/早期设计 + wiki 数据校正 + CLAUDE.md 规则优化
**根目录清理:**
- 删除 CLAUDE-1.md(ZCLAW 旧项目配置,HMS 已完全脱离)
- 移动 DESIGN.md → docs/archive/(ERP 旧设计系统)
- 删除 plans/ 98 个临时会话计划文件

**归档重组:**
- V1 审计(12 文件)→ docs/archive/audits-v1/
- 早期 CRM/插件迭代设计(13 文件)→ docs/archive/superpowers-early/
- 已完成/已取代设计(28 文件)→ docs/archive/superpowers-completed/
- 早期讨论/测试报告 → docs/archive/discussions-early/ + test-reports-early/
- QA 重复文件清理(3 个旧版 result 文件)

**wiki 数据校正:**
- 迁移数 137→145,源文件 599→649,提交数 720→800+
- 小程序文件 124→163,Web 前端 297→332
- 后端测试 999→943(实际统计),权限码 75+→128
- 文档索引新增归档目录说明

**CLAUDE.md 规则优化:**
- §2.5 闭环工作法:提交+文档+推送三合一 + wiki 更新触发条件
- §2.6 Feature DoD:新增文档一致性检查项
- §6 反模式:新增 wiki 更新滞后/推送不及时警告
2026-05-15 09:29:04 +08:00

9.6 KiB
Raw Blame History

HMS 功能审计 — Phase 6: 错误处理与降级审计

日期: 2026-04-30 | 审计范围: 错误处理、降级策略、日志完整性

总览

指标
AppError 变体 8 个
领域错误变体 HealthError 26 + AiError 11
Handler 错误映射机制 统一 IntoResponse trait
生产 unwrap() 18 处13 低风险 + 2 中风险 + 3 无害)
审计日志 140 处33 文件)+ SHA256 哈希链
运行时 tracing 日志 11 处health service 层)

1. 错误处理覆盖率

1.1 AppError 枚举8 变体)

变体 HTTP 状态码 触发场景
NotFound(String) 404 资源不存在
Validation(String) 400 输入验证失败
Unauthorized 401 未认证
Forbidden(String) 403 无权限
Conflict(String) 409 唯一约束冲突
VersionMismatch 409 乐观锁版本不匹配
TooManyRequests 429 速率限制
Internal(String) 500 内部错误(消息对外隐藏)

1.2 领域错误映射

HealthError → AppError26 变体):

映射规则 变体示例 目标 AppError
资源不存在 PatientNotFound, DoctorNotFound, ScheduleNotFound NotFound
状态转换无效 InvalidStatusTransition, AppointmentAlreadyCancelled Validation
容量限制 ScheduleFull Validation
乐观锁 VersionMismatch VersionMismatch (409)
数据库错误 DbError Internal
加密错误 EncryptionError, DecryptionError Internal

AiError → AppError11 变体):

映射规则 变体示例 目标 AppError
资源不存在 AnalysisNotFound, PromptNotFound NotFound
验证失败 Validation, SanitizationError, TemplateError Validation
提供商不可用 ProviderUnavailable Internal
提供商错误 ProviderError Internal
速率限制 RateLimitExceeded TooManyRequests (429)

1.3 Handler 错误传播模式

所有 handler 函数统一使用 ? 运算符自动传播错误:

Handler → require_permission()? → service.do_work()? → Ok(Json(ApiResponse::ok(result)))

无需手动 match/mapFrom<DomainError> for AppError 自动转换。SeaORM 的 DbErr 也有智能映射RecordNotFound → 404, duplicate key → 409

1.4 PII 加密错误处理

所有加密/解密失败通过 AppError::Internal 返回。关键设计:

  • 错误细节仅通过 tracing::error! 记录到日志
  • HTTP 响应体统一替换为 "内部错误",不泄露加密实现细节
  • 防止通过错误消息推断加密方案

1.5 SSE 端点错误处理

预流阶段(连接建立前):

  • 权限/验证/数据获取全部通过 ? 传播
  • AI Provider 不可用时返回标准 JSON 错误响应500不会挂起连接

流中阶段(连接建立后):

  • Provider 断连:发送 SSE error 事件 → 标记分析记录为 failed → 发布 ai.analysis.failed 事件 → 优雅终止流
  • 序列化错误:使用 unwrap_or_default() 避免 panic

结论SSE 端点不会挂起。 Provider 不可用时有完整的错误传播和清理机制。

1.6 生产代码 unwrap() 风险

类别 数量 风险 建议
active.version.unwrap() + 1 13 LOW 改用 expect("version from DB must be set")
PluginHost::db.unwrap() 1 MEDIUM 改用 ok_or(AppError::Internal(...))
信号量 acquire().unwrap() 1 MEDIUM 改用 map_err 处理关闭场景
Response builder .unwrap() 3 LOW 可接受,硬编码值不会失败
unwrap_or/unwrap_or_default ~20 NONE 安全模式

2. 降级策略评估

2.1 Redis 不可用

场景 降级策略 评估
IP 限流 Fail-OpenRedis 不可达时限流被旁路,请求正常放行 ✓ 安全(宁可放过不可误杀)
账户锁定 可配置ERP__RATE_LIMIT__FAIL_CLOSE 环境变量控制,默认 Fail-Open生产可切 Fail-Close ✓ 灵活
微信会话存储 优雅降级AuthState.redisOption<redis::Client>,缺失时降级 ✓ 安全
断路器 30 秒冷却期,避免重复连接失败阻塞请求 ✓ 成熟

风险点AppState.redisredis::Client(非 Option),仅 AuthStateOption 包裹。但没有全局 Redis 健康检查端点。

2.2 AI Provider (Claude) 不可用

阶段 行为 评估
预流(连接前) 返回 500 JSON 错误,不挂起
流中(连接后) 发送 SSE error 事件 → 标记失败 → 发布事件 → 终止流
分析记录 状态更新为 failed,保留错误信息

结论AI Provider 不可用不会导致连接挂起。 有完整的错误传播和清理机制。

2.3 EventBus 满载

层级 机制 评估
内存层 broadcast channel 容量 1024慢消费者收到 Lagged 错误
持久化层 domain_events 表 outbox 模式,事件不丢失
兜底轮询 30 秒间隔扫描 pending 事件
最大重试 5 次,超限进入 dead_letter_events

结论:事件不会丢失。 broadcast channel 满载时实时性受影响(最多 30 秒延迟),但 outbox relay 保证最终交付。

2.4 前端 SSE 重连

维度 实现 评估
重连机制 浏览器原生 EventSource 自动重连 基本可用
指数退避 无(浏览器默认 ~3 秒固定间隔) ⚠️ 缺失
最大重连次数 无(原生无限重试) ⚠️ 缺失
连接状态 UI useAlertSSE 暴露 connected 状态但未在界面显示 ⚠️ 缺失
Keep-alive 后端 Sse::new().keep_alive(KeepAlive::default())

3. 日志完整性

3.1 运行时 tracing 日志密度

erp-health/src/service/ 目录(核心业务层):

日志级别 调用次数 分布文件
tracing::info! 4 points_service(1), seed(3)
tracing::warn! 6 appointment(1), device_reading(1), health_data(3), points(1), seed(1)
tracing::error! 0
tracing::debug! 1 points(1)
总计 11 5/26 文件

关键缺口

文件 行数 tracing 调用 问题
patient_service.rs 949 0 患者创建/更新/删除无运行时日志
appointment_service.rs 590 1 仅取消预约边缘场景有日志
consultation_service.rs ~400 0 咨询会话/消息操作无运行时日志
follow_up_service.rs ~500 0 随访任务操作无运行时日志

对比:基础设施层日志丰富:

  • erp-core/events.rsEventBus 有完整的生命周期日志
  • erp-server/rate_limit.rs:限流有 warn/error 日志
  • erp-server/outbox.rsOutbox relay 有完整的处理日志

3.2 审计日志覆盖

审计日志通过 audit_service::record 实现,全局 140 处调用分布在 33 个文件中。

Health 模块审计覆盖

文件 审计调用数 覆盖操作
patient_service.rs 12 创建/更新/删除患者、标签管理、家庭成员 CRUD、医生分配
points_service.rs 12 积分发放/兑换/过期
health_data_service.rs 10 体征/化验/体检记录 CRUD + 审核
article_service.rs 7 文章 CRUD
follow_up_service.rs 7 随访任务 CRUD
appointment_service.rs 4 预约创建/状态变更/排班管理
其余 7 个文件 各 2-3 咨询/诊断/同意/医生/透析/设备/告警

审计日志特性

特性 实现
结构化字段 tenant_id, user_id, action, resource_type, resource_id, ip_address, user_agent
变更追踪 更新操作记录 old_value/new_value 快照
哈希链 SHA256 链式签名,可验证完整性
请求来源 task_local 自动注入 IP + User-Agent
写入模式 Fire-and-forget失败仅 warn不影响业务

审计日志风险点

  • Fire-and-forget 意味着写入失败是静默的,无重试或告警
  • 哈希链在并发写入时可能出现短暂链断裂(查询和写入非原子操作)

3.3 日志分层评估

层级 tracing 日志 审计日志 评估
Handler 层 依赖 service 层
Service 层health 11 处 70+ 处 运行时日志不足,审计日志完善
Service 层infrastructure 丰富 日志密度高
EventBus 完整 发布/消费全链路可追踪
加密层 仅错误时 tracing::error

4. 评分

检查项 评分 说明
错误变体覆盖 95% 8 个 AppError + 37 个领域错误,覆盖全面
Handler 错误传播 100% 统一 ? + IntoResponse无手动 match
PII 错误安全 100% Internal 错误消息对外隐藏,仅日志记录
SSE 挂起风险 100% Provider 不可用不会挂起,有完整清理
unwrap() 安全性 95% 2 处中等风险PluginHost::db + 信号量)
Redis 降级 90% Fail-Open 断路器 + 可配置,缺健康检查端点
EventBus 降级 95% Outbox 兜底 + 死信队列,事件不丢失
前端重连 60% 依赖原生 EventSource缺指数退避和 UI 反馈
运行时日志 30% health service 仅 11 处 tracing运维盲区大
审计日志 95% 140 处覆盖所有写操作 + 哈希链验证
综合评分 76% 错误处理架构优秀,日志和前端重连有差距