Files
hms/docs/superpowers/specs/2026-04-26-event-driven-architecture-design.md
iven d1ab8074a3
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
docs: 多专家组头脑风暴产出 — 5 份设计规格
基于全景审计分析,产出 5 份跨领域设计规格:

1. 性能优化 — 后端批量INSERT/合并COUNT/告警预加载 + 前端N+1内联name
2. 安全纵深防御 — PostgreSQL RLS/行级数据范围/session_key Redis/审计哈希链
3. 事件驱动架构增强 — 6个业务域11个缺失事件补发 + Outbox LISTEN/NOTIFY
4. 前端工程化 — 14个大组件拆分 + 3个重复模式统一 + Bundle优化
5. 可观测性与运维 — 深度健康检查/Prometheus/OpenTelemetry/生产Docker
2026-04-27 07:46:36 +08:00

250 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 事件驱动架构增强设计规格
> 日期: 2026-04-26 | 状态: draft | 主题: 缺失事件补发 + Outbox relay 优化 + 事件 schema 版本化
## 1. 背景
HMS 已有完整的事件总线基础设施:
- **EventBus** (`erp-core/src/events.rs`): 两阶段发布(先持久化 pending → 广播 → 更新 published
- **Outbox relay** (`erp-server/src/outbox.rs`): 5 秒轮询 domain_events 表,重发 pending 事件
- **domain_events 表**: id, tenant_id, event_type, payload, status, attempts, created_at, published_at
已有事件发布的模块: patient, appointment, follow_up, consultation, health_data, alert_engine, device_reading, doctor。
## 2. 问题分析
### 2.1 缺失事件清单
以下 6 个业务域的 service 文件中无任何 `event_bus.publish` 调用:
| 业务域 | Service 文件 | 缺失事件 |
|--------|-------------|----------|
| 透析记录 | `dialysis_service.rs` | `dialysis_record.created/reviewed` |
| 诊断 | `diagnosis_service.rs` | `diagnosis.created/updated` |
| 知情同意 | `consent_service.rs` | `consent.granted/revoked` |
| 日常监测 | `daily_monitoring_service.rs` | `daily_monitoring.created` |
| 积分 | `points_service.rs` | `points.earned/exchanged` |
| 资讯文章 | `article_service.rs` | `article.published/rejected` |
### 2.2 基础设施改进项
| 编号 | 问题 | 影响 |
|------|------|------|
| I-1 | Outbox relay 5 秒轮询延迟高 | 事件从产生到广播最长 5 秒延迟 |
| I-2 | 事件 payload 无 schema 版本 | 消费者无法安全演进,字段增删破坏兼容性 |
| I-3 | 无事件幂等性保证 | 消费者重复消费可能导致业务异常 |
| I-4 | domain_events 表无清理策略 | 表无限增长影响查询性能 |
## 3. 解决方案
### 3.1 缺失事件补发
#### 3.1.1 事件优先级排序
| 优先级 | 事件 | 理由 |
|--------|------|------|
| P0 | `dialysis_record.created/reviewed` | 透析是核心医疗流程,需触发统计更新和告警检查 |
| P0 | `diagnosis.created/updated` | 诊断关联后续治疗方案,影响预约/随访 |
| P1 | `consent.granted/revoked` | 合规要求,知情同意变更需通知医护 |
| P1 | `article.published/rejected` | 内容审核流程依赖事件驱动 |
| P2 | `daily_monitoring.created` | 日常监测触发趋势分析 |
| P2 | `points.earned/exchanged` | 积分变动通知用户 |
#### 3.1.2 统一事件信封格式
所有事件 payload 遵循统一信封:
```json
{
"schema_version": "v1",
"entity_id": "uuid",
"entity_type": "dialysis_record",
"action": "created",
"tenant_id": "uuid",
"operator_id": "uuid | null",
"timestamp": "ISO 8601",
"data": { /* */ },
"metadata": { "source": "erp-health", "trace_id": "uuid" }
}
```
#### 3.1.3 各事件 data 字段设计
| 事件 | data 关键字段 | 说明 |
|------|--------------|------|
| `dialysis_record.created` | patient_id, dialysis_type, status, dialysis_date, duration, ultrafiltration_volume | 新建透析记录 |
| `dialysis_record.reviewed` | patient_id, reviewer_id, dialysis_type, complication_notes | 医生审核完成 |
| `diagnosis.created` | patient_id, icd_code, diagnosis_name, diagnosis_type, severity, diagnosed_at | 新诊断录入 |
| `diagnosis.updated` | patient_id, changed_fields[], old_values{}, new_values{} | 诊断信息变更(含 diff |
| `consent.granted` | patient_id, consent_type, consent_scope, granted_by, expires_at | 知情同意签署 |
| `consent.revoked` | patient_id, consent_type, revoked_by, reason | 知情同意撤销 |
| `article.published` | title, author_id, category_id, tags[] | 文章审核通过发布 |
| `article.rejected` | title, reviewer_id, reason | 文章审核驳回 |
| `daily_monitoring.created` | patient_id, monitoring_date, monitoring_type, values{} | 日常监测数据录入 |
| `points.earned` | patient_id, points, source_type, source_id, balance_after | 积分获得 |
| `points.exchanged` | patient_id, points, product_name, order_id, balance_after | 积分兑换 |
### 3.2 Outbox relay 优化
#### 3.2.1 PostgreSQL LISTEN/NOTIFY 替代轮询
**当前**: 5 秒轮询 `domain_events` 表(`outbox.rs` 第 26-32 行)
**优化方案**:
1.`EventBus::publish()` 持久化事件后执行 `NOTIFY`:
```rust
// erp-core/src/events.rs publish() 末尾添加
let notify_sql = format!("NOTIFY outbox_channel, '{}'", event.id);
sqlx::query(&notify_sql).execute(db).await.ok();
```
2. Outbox relay 使用 `LISTEN` + 30 秒兜底轮询:
```rust
let mut listener = PgListener::connect_with(&db).await?;
listener.listen("outbox_channel").await?;
loop {
tokio::select! {
_ = listener.recv() => { process_pending_events(&db, &event_bus).await.ok(); }
_ = tokio::time::sleep(Duration::from_secs(30)) => {
process_pending_events(&db, &event_bus).await.ok();
}
}
}
```
**收益**: 事件延迟 0-5s → <100msDB 轮询压力降低 6x。**复杂度**: 低。
#### 3.2.2 domain_events 表清理
**方案**: 按月分区 + 90 天归档
```sql
CREATE TABLE domain_events_new (LIKE domain_events INCLUDING ALL)
PARTITION BY RANGE (created_at);
-- 按月创建分区
CREATE TABLE domain_events_2026_04 PARTITION OF domain_events_new
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
```
已 published 且 > 90 天的事件迁移到 `domain_events_archive` 表。
### 3.3 事件 schema 版本化
在 payload 中嵌入 `schema_version` 字段,消费者按 `event_type` + `schema_version` 路由:
```rust
fn handle_event(event: &DomainEvent) {
let version = event.payload["schema_version"].as_str().unwrap_or("v1");
match (event.event_type.as_str(), version) {
("dialysis_record.created", "v1") => handle_v1(event),
("dialysis_record.created", "v2") => handle_v2(event),
_ => tracing::warn!("Unknown event version"),
}
}
```
**演进规则**: 新增字段兼容(不升版),删除/重命名字段不兼容(升版)。
### 3.4 事件幂等性保证
消费者维护 `processed_events` 去重表:
```sql
CREATE TABLE processed_events (
event_id UUID NOT NULL,
consumer_id VARCHAR(64) NOT NULL,
processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (event_id, consumer_id)
);
```
流程: 收到事件 → 查已处理 → 已存在则跳过 → 否则执行业务 + 插入记录。7 天 TTL 定期清理。
## 4. 实施步骤
### Phase 1: P0 事件补发(预估 2 天)
| 步骤 | 任务 | 修改文件 |
|------|------|----------|
| 1.1 | dialysis_service 添加 created/reviewed 事件 | `dialysis_service.rs` |
| 1.2 | diagnosis_service 添加 created/updated 事件 | `diagnosis_service.rs` |
| 1.3 | 验证: 事件发布 + payload 格式正确 | - |
### Phase 2: P1 事件补发(预估 1-2 天)
| 步骤 | 任务 | 修改文件 |
|------|------|----------|
| 2.1 | consent_service 添加 granted/revoked 事件 | `consent_service.rs` |
| 2.2 | article_service 添加 published/rejected 事件 | `article_service.rs` |
| 2.3 | 验证: 事件发布正确触发 | - |
### Phase 3: P2 事件补发(预估 1 天)
| 步骤 | 任务 | 修改文件 |
|------|------|----------|
| 3.1 | daily_monitoring_service 添加 created 事件 | `daily_monitoring_service.rs` |
| 3.2 | points_service 添加 earned/exchanged 事件 | `points_service.rs` |
| 3.3 | 验证: 积分变动事件触发 | - |
### Phase 4: 基础设施优化(预估 2-3 天)
| 步骤 | 任务 | 修改文件 |
|------|------|----------|
| 4.1 | Outbox relay 改用 LISTEN/NOTIFY | `outbox.rs`, `events.rs` |
| 4.2 | 添加事件 schema_version 字段 | 所有事件发布处 |
| 4.3 | 创建 processed_events 去重表 | migration |
| 4.4 | domain_events 按月分区 + 清理策略 | migration + 后台任务 |
| 4.5 | 验证: 事件延迟 < 100ms + 去重测试 | - |
## 5. 风险与缓解
### 5.1 LISTEN/NOTIFY 连接管理
**风险**: PostgreSQL LISTEN 使用独立连接,连接断开需重建。
**缓解**: `sqlx::PgListener` 自动重连 + 30 秒兜底轮询确保不遗漏。
### 5.2 事件发布失败
**风险**: `event_bus.publish()` 失败但业务操作已提交。
**缓解**: 两阶段发布已处理 — 事件写入 pendingoutbox relay 重发。publish 失败仅 warn 日志,不阻塞业务。
### 5.3 去重表增长
**风险**: `processed_events` 表快速增长。
**缓解**: 7 天 TTL 定期清理,或使用 Redis SET NX + TTL 替代。
### 5.4 Schema 演进兼容性
**风险**: 新版本消费者无法处理老版本事件。
**缓解**: 消费者必须支持 N-1 版本 schema。升版前确保所有消费者已升级。
## 6. 已有事件 vs 缺失事件汇总
### 已发布事件8 个模块)
| 模块 | 事件类型 | 触发位置 |
|------|----------|----------|
| patient | `patient.created`, `patient.updated` | `patient_service.rs` |
| appointment | `appointment.created`, `appointment.status_changed` | `appointment_service.rs` |
| follow_up | `follow_up_task.created`, `follow_up_task.status_changed`, `follow_up_record.completed` | `follow_up_service.rs` |
| consultation | `consultation_session.created`, `consultation_session.status_changed` | `consultation_service.rs` |
| health_data | `vital_signs.created`, `lab_report.uploaded` | `health_data_service.rs` |
| alert | `alert.triggered` | `alert_engine.rs` |
| device | `device.readings.synced` | `device_reading_service.rs` |
| doctor | `doctor.schedule.updated` | `doctor_service.rs` |
### 待补发事件6 个模块11 个事件)
| 模块 | 事件类型 | 优先级 |
|------|----------|--------|
| dialysis | `dialysis_record.created`, `dialysis_record.reviewed` | P0 |
| diagnosis | `diagnosis.created`, `diagnosis.updated` | P0 |
| consent | `consent.granted`, `consent.revoked` | P1 |
| article | `article.published`, `article.rejected` | P1 |
| daily_monitoring | `daily_monitoring.created` | P2 |
| points | `points.earned`, `points.exchanged` | P2 |