diff --git a/docs/superpowers/specs/2026-04-28-technical-debt-cleanup-design.md b/docs/superpowers/specs/2026-04-28-technical-debt-cleanup-design.md new file mode 100644 index 0000000..deb7c3e --- /dev/null +++ b/docs/superpowers/specs/2026-04-28-technical-debt-cleanup-design.md @@ -0,0 +1,556 @@ +# 技术债清理设计规格 + +> 日期: 2026-04-28 | 状态: DRAFT | 来源: 发散式技术债讨论 + +## 1. 概述 + +### 1.1 背景 + +HMS 健康管理平台经过 17 天密集开发,功能层面已建成完整的医疗 SaaS 骨架: + +| 维度 | 数据 | +|------|------| +| Rust crate | 15 个 | +| 数据库表 | 67 张(30 基础 + 34 健康 + 3 AI) | +| 后端代码 | ~63k 行 Rust | +| Web 前端 | 133 个源文件(77 TSX + 56 TS) | +| 微信小程序 | 40 个页面,Taro 4.2 + React 18 | +| Git 提交 | 301+ 次 | + +但安全合规、测试质量、事件消费闭环三个维度存在系统性欠账,是当前最大的技术风险。 + +### 1.2 问题全景 + +**安全(6 个 Critical):** +- SQL 注入风险(需排查动态 SQL 使用场景) +- SSE 连接有效性需验证(键名一致但端点未确认可用) +- AES 加密密钥硬编码到小程序 bundle(`secure-storage.ts` 使用环境变量,但 dev 默认密钥不安全) +- PII 数据明文传输/展示 +- 密钥轮换端点未持久化、DEK 内存未 zeroize + +**事件(13 个事件仅 3 个被消费):** +- 危急体征告警(`health_data.critical_alert`)无人响应 +- 患者创建、预约确认/取消、随访逾期等业务事件无消费者 +- 缺少 dead-letter 和重试机制 + +**测试(覆盖率 < 5%):** +- 后端 36 个测试因并行化问题全部失败 +- 前端仅 3 个单元测试 +- 无安全测试、无多租户隔离测试 + +**代码质量:** +- `points_service.rs` 膨胀至 1805 行(超 800 行上限 2 倍+) +- 积分系统(6 实体)与 health 模块耦合过重 +- 密文缺版本标识,未来算法迁移困难 +- 前端 chunk 过大(antd 1.5MB) + +### 1.3 还债策略 + +**按类型分批,批内 Critical 优先:** + +``` +批次 A(安全与合规)→ 批次 B(事件与架构)→ 批次 C(测试与质量) +``` + +每个批次的修复都伴随对应测试(批次 C 随批次 A/B 同步推进)。 + +### 1.4 预估总工作量 + +| 批次 | 项目数 | 预估 | +|------|--------|------| +| A 安全与合规 | 9 | 3-4 天 | +| B 事件与架构 | 5 | 3-5 天 | +| C 测试与质量 | 5 | 2-3 天(持续) | +| **合计** | **19** | **8-12 天** | + +## 2. 批次 A:安全与合规 + +### 2.1 A1+A2:SQL 注入风险排查与修复 + +**现状:** 审计报告标记 `tenant_rls.rs` 和 `follow_up_service.rs` 存在 SQL 拼接。 + +**实际代码验证:** +- `follow_up_service.rs:73-84`:使用 `$N` 占位符 + `Statement::from_sql_and_values`,为参数化查询,**非注入风险** +- `tenant_rls.rs`:文件不存在于当前代码库,可能已被修复或移除 +- 代码库中存在 29 处 `Statement::from_string` 使用,需逐个审计 + +**需审计的高风险文件(非 migration):** +- `crates/erp-plugin/src/` — dynamic_table 相关动态 SQL +- `crates/erp-health/src/service/` — 各 service 中的原生 SQL +- `crates/erp-auth/src/` — 用户查询相关 + +**修复方案:** +1. 逐文件审计 29 处 `Statement::from_string`,区分 migration(安全)和业务代码(需参数化) +2. 如发现裸拼接,统一改为 `Statement::from_sql_and_values` + `$N` 占位符 +3. 引入 Clippy lint 或 CI 检查禁止 `Statement::from_string` 用于含用户输入的场景 + +### 2.2 A3:SSE 连接有效性验证 + +**现状:** `apps/web/src/stores/message.ts:75` 使用 `localStorage.getItem('access_token')` 获取 token,`apps/web/src/stores/auth.ts:6` 同样使用 `access_token` 键名。**键名一致**,但 SSE 端点 `/api/v1/messages/stream` 是否实际工作尚未验证。 + +**修复方案:** +1. 验证 SSE 端点是否正常响应(后端 handler 是否正确注册) +2. 统一使用 auth store 导出的获取方法而非直接 `localStorage.getItem` +3. 添加 token 有效性检查,避免无效连接 + +**涉及文件:** +- `apps/web/src/stores/message.ts` +- `apps/web/src/stores/auth.ts`(确认 token 键名) + +### 2.3 A4+A5+A8+A9:PII 后端加解密 + Blind Index + +这是批次 A 中工作量最大、架构影响最深的修复项。四个 Critical 问题合并为一个实施任务。 + +#### 2.3.1 架构决策:方案 D — 后端加解密 + Blind Index + +**核心原则:前端(含小程序)不接触加密密钥。** + +| 层 | 职责 | +|----|------| +| 后端 Service 层 | PII 字段透明加解密(写入前加密,读取后解密) | +| 后端 API 层 | 按角色/场景返回不同脱敏级别 | +| Blind Index 层 | HMAC-SHA256 盲索引支持精确搜索 | +| 前端 | 只展示后端返回的数据,不参与加密 | + +#### 2.3.2 现有加密基础设施 + +当前已实现(`crates/erp-health/src/crypto.rs`): +- `HealthCrypto`:AES-256-GCM 加密 + HMAC-SHA256 盲索引 +- `encrypt()`:UUID v7 nonce + 密文 → Base64 编码 +- `decrypt()`:Base64 解码 → nonce 分离 → 解密 +- `hmac_hash()`:确定性 HMAC 哈希(盲索引) +- `dev_default()`:开发环境硬编码密钥(**需移除**) +- 已有 7 个单元测试验证加解密正确性 + +#### 2.3.3 Blind Index 方案 + +**新增数据库表:** + +```sql +CREATE TABLE blind_indexes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + entity_type VARCHAR(64) NOT NULL, -- 如 "patient", "doctor" + entity_id UUID NOT NULL, + field_name VARCHAR(64) NOT NULL, -- 如 "id_card", "phone" + blind_hash VARCHAR(64) NOT NULL, -- HMAC-SHA256 hex + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(tenant_id, entity_type, field_name, blind_hash) +); +CREATE INDEX idx_blind_hashes ON blind_indexes (tenant_id, entity_type, field_name, blind_hash); +``` + +**搜索流程:** +1. 前端传入搜索条件(如手机号 `13812345678`) +2. 后端计算 HMAC 哈希 +3. 在 `blind_indexes` 表精确匹配 +4. 返回匹配的 entity_id 列表 +5. 模糊搜索(如姓张的所有患者):后端解密后内存过滤 + +**密钥轮换流程:** +1. 生成新 DEK +2. 批量重新加密所有 PII 字段 +3. 重算所有 blind_hash +4. 更新 KEK 加密的新 DEK +5. 旧 DEK 标记过期,保留 7 天过渡期 + +#### 2.3.4 API 脱敏级别 + +| 场景 | 返回策略 | +|------|---------| +| 列表页 | 脱敏展示(`张**`、`138****5678`) | +| 详情页(有权限) | 完整明文 | +| 导出 | 按权限级别,默认脱敏 | +| API 内部调用 | 明文(服务间信任) | + +**新增脱敏工具函数:** +- `mask_name("张三丰")` → `"张**"` +- `mask_phone("13812345678")` → `"138****5678"` +- `mask_id_card("110101199001011234")` → `"1101**********1234"` + +#### 2.3.5 小程序侧改造 + +- **审计** `apps/miniprogram/src/utils/secure-storage.ts` — 当前使用环境变量 `TARO_APP_ENCRYPTION_KEY` + CryptoJS.AES 做本地存储加密 +- **区分用途**: + - PII 端到端加密 → 移除(后端全面接管) + - 本地 token 安全存储 → 保留(非 PII 用途) +- API 返回的 PII 数据由后端控制脱敏级别 +- 列表页展示脱敏数据,详情页请求完整数据(需权限) + +#### 2.3.6 DEK 内存 zeroize + +- 引入 `zeroize` crate +- `HealthCrypto` 的 `aes_key` 和 `hmac_key` 字段使用 `Zeroizing<[u8; 32]>` +- Drop 时自动覆写内存 + +#### 2.3.7 密文版本标识 + +- 加密输出格式:`v1|Base64(nonce + ciphertext)` +- 未来算法升级时新增 `v2|...` +- 解密时根据版本前缀选择对应算法 + +**涉及文件:** +- `crates/erp-health/src/crypto.rs` — 主要修改 +- `crates/erp-health/src/state.rs` — HealthState.crypto 使用 +- `crates/erp-health/src/service/` — 所有涉及 PII 的 service +- `crates/erp-health/entity/` — 新增 blind_indexes entity +- `apps/miniprogram/src/utils/secure-storage.ts` — 审计并移除 PII 加密逻辑(保留本地存储加密) + +### 2.4 A6:清缓存破坏认证状态 + +**现状:** 小程序设置页已实现 preserve-and-restore 模式(`apps/miniprogram/src/pages/profile/settings/index.tsx:16-27`):清除缓存前保存 `access_token`、`refresh_token` 等 8 个关键 key,清除后重新写入。 + +**结论:** 此问题 **已修复**,改为验证任务 — 确认 preserve-and-restore 逻辑覆盖所有必要 key 且工作正常。 + +**涉及文件:** +- `apps/miniprogram/src/pages/profile/settings/index.tsx` + +### 2.5 A7:PostgreSQL RLS Policy 兜底 + +**现状:** 多租户隔离完全依赖应用层中间件注入 `tenant_id` 过滤。应用层 bug 可直接导致跨租户数据泄露。 + +**修复方案:** +1. 为所有含 `tenant_id` 的表创建 RLS policy +2. 使用 PostgreSQL session variable(`SET app.current_tenant_id`)传递当前租户 +3. 中间件在每次请求开始时设置 session variable +4. RLS policy 作为应用层过滤的兜底,而非替代 + +```sql +-- 示例 +ALTER TABLE patients ENABLE ROW LEVEL SECURITY; +CREATE POLICY tenant_isolation ON patients + USING (tenant_id::text = current_setting('app.current_tenant_id', true)); +``` + +**注意:** RLS policy 不影响超级用户(SUPERUSER),仅对应用数据库用户生效。 + +**涉及文件:** +- `crates/erp-core/src/middleware/` — 注入 session variable +- 新增 SeaORM migration 文件(RLS policy 通过迁移实施,遵循 CLAUDE.md 规范) + +## 3. 批次 B:事件与架构 + +### 3.1 B1:危急值告警消费者(最高优先) + +**现状:** `health_data.critical_alert` 事件已定义(`crates/erp-health/src/event.rs:43`),`alert_engine` 已实现评估逻辑,但 **无消费者响应此事件**。这意味着当患者体征超过危急值阈值时,系统仅发布事件但不通知任何医生。 + +**这是真实的医疗安全风险。** + +#### 3.1.1 消费链路设计 + +``` +健康数据录入 → critical_value_threshold 对比 → 超阈值 + ↓ +EventBus.publish("health_data.critical_alert", payload) + ↓ +┌──────────────────────────┐ +│ 危急值消费者 (consumer) │ +├──────────────────────────┤ +│ 1. 创建 critical_alert │ ← 新表,记录告警实例 +│ 2. 通知主治医生 │ ← 站内消息 +│ 3. 微信订阅消息推送 │ ← 患者端/医护端通知 +│ 4. 启动响应计时 │ ← 30min 未确认升级 +│ 5. 记录事件已处理 │ ← 幂等去重 +└──────────────────────────┘ +``` + +#### 3.1.2 告警升级策略 + +| 级别 | 触发条件 | 动作 | +|------|---------|------| +| Level 1 | 告警产生 | 通知主治医生(站内 + 微信) | +| Level 2 | 30min 未确认 | 通知科室主任 | +| Level 3 | 60min 未确认 | 通知院领导 + 标记为严重事件 | + +升级检查使用 `tokio::spawn` 定时任务,每分钟扫描未确认的告警。 + +#### 3.1.3 新增数据库表 + +```sql +CREATE TABLE critical_alerts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + patient_id UUID NOT NULL, + alert_type VARCHAR(64) NOT NULL, -- vital_sign / lab_result + metric_name VARCHAR(64) NOT NULL, -- blood_pressure / heart_rate ... + metric_value TEXT NOT NULL, + threshold_value TEXT NOT NULL, + severity VARCHAR(16) NOT NULL DEFAULT 'critical', + status VARCHAR(16) NOT NULL DEFAULT 'pending', -- pending/acknowledged/resolved/escalated + acknowledged_by UUID, + acknowledged_at TIMESTAMPTZ, + escalation_level SMALLINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID, + updated_by UUID, + deleted_at TIMESTAMPTZ, + version BIGINT NOT NULL DEFAULT 1 +); + +CREATE TABLE critical_alert_responses ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + alert_id UUID NOT NULL REFERENCES critical_alerts(id), + responder_id UUID NOT NULL, -- 确认人 + response_type VARCHAR(16) NOT NULL, -- acknowledge/escalate/resolve + notes TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID, + updated_by UUID, + deleted_at TIMESTAMPTZ, + version BIGINT NOT NULL DEFAULT 1 +); + +CREATE INDEX idx_critical_alerts_pending ON critical_alerts (tenant_id, status, created_at) + WHERE status IN ('pending', 'escalated'); +``` + +#### 3.1.4 幂等处理 + +利用 EventBus 已有的幂等基础设施: +- `is_event_processed(db, event_id, "critical_alert_consumer")` 检查 +- `mark_event_processed(db, event_id, "critical_alert_consumer")` 标记 +- `processed_events` 表的 event_id + consumer_id 唯一约束兜底 + +#### 3.1.5 实现位置 + +- 消费者注册:`crates/erp-health/src/event.rs` → `register_handlers_with_state()` +- 告警 service:`crates/erp-health/src/service/critical_alert_service.rs`(新增) +- 告警 handler:`crates/erp-health/src/handler/critical_alert_handler.rs`(新增) +- Entity:`crates/erp-health/src/entity/critical_alert.rs`(新增) + +### 3.2 B2:EventBus 可靠性增强 + +**现状:** EventBus(`crates/erp-core/src/events.rs`)已实现: +- 事件持久化到 `domain_events` 表(pending → published 状态机) +- 内存 broadcast channel +- PostgreSQL NOTIFY outbox relay +- 过滤订阅(`subscribe_filtered`) +- 幂等去重(`is_event_processed` / `mark_event_processed`) + +**缺少:** +- 消费失败的重试机制 +- dead-letter 存储 +- 消费者健康监控 + +**修复方案:** +1. `domain_events` 表增加 `attempts` 计数器(已有)和 `last_error` 字段(已有) +2. 新增 `dead_letter_events` 表:超过最大重试次数的事件转入 +3. 消费者返回 `Result` 时,失败则 `attempts++`,达到阈值转入 dead-letter +4. 提供 `GET /api/v1/admin/dead-letter-events` 端点查看失败事件 +5. 提供 `POST /api/v1/admin/dead-letter-events/{id}/retry` 手动重试 + +### 3.3 B3:积分系统拆分为独立 erp-points crate + +**现状:** 积分商城 6 个实体全在 `erp-health` 内(`points_account`、`points_checkin`、`points_order`、`points_product`、`points_rule`、`points_transaction`),`points_service.rs` 膨胀至 1805 行。 + +#### 3.3.1 新 crate 结构 + +``` +crates/erp-points/ +├── Cargo.toml +├── src/ +│ ├── lib.rs +│ ├── module.rs — ErpModule trait 实现 +│ ├── state.rs — PointsState { db, event_bus } +│ ├── event.rs — 事件常量 + 消费者注册 +│ ├── entity/ +│ │ ├── mod.rs +│ │ ├── points_account.rs +│ │ ├── points_transaction.rs +│ │ ├── points_product.rs +│ │ ├── points_order.rs +│ │ ├── points_checkin.rs +│ │ └── points_rule.rs +│ ├── service/ +│ │ ├── mod.rs +│ │ ├── account_service.rs +│ │ ├── product_service.rs +│ │ ├── order_service.rs +│ │ ├── check_in_service.rs +│ │ ├── exchange_service.rs +│ │ └── rule_engine.rs +│ ├── handler/ +│ │ ├── mod.rs +│ │ └── points_handler.rs +│ ├── dto/ +│ │ ├── mod.rs +│ │ └── points_dto.rs +│ └── error.rs +``` + +#### 3.3.2 事件契约 + +**erp-points 订阅的事件:** +| 事件 | 来源 | 动作 | +|------|------|------| +| `lab_report.uploaded` | erp-health | 化验上传奖励(已有事件常量) | +| `patient.verified` | erp-health | 实名认证奖励(已有事件常量) | +| `daily_monitoring.created` | erp-health | 日常监测奖励(已有事件常量) | + +> **注意:** 原讨论中设想的 `health.checkup.completed` 和 `health.appointment.completed` 事件在当前代码库中不存在,需在 erp-health 中新增定义并发布后才能订阅。建议作为后续迭代,Phase 1 先订阅已有事件。 + +**erp-points 发布的事件:** +| 事件 | 动作 | +|------|------| +| `points.earned` | 通知消息模块 | +| `points.exchanged` | 通知消息模块 | +| `points.expired` | 积分过期通知(已有事件常量) | +| `points.balance.changed` | 余额变动,供前端 SSE 推送(**新增事件常量**) | + +#### 3.3.3 迁移策略 + +1. 创建 `erp-points` crate,搭建标准基础设施(module/state/error) +2. 从 `erp-health` 迁移 6 个 Entity + 对应 Migration 文件 +3. 迁移 service 逻辑,`points_service.rs` 拆分为 6 个子模块 +4. 迁移 handler +5. `erp-server` 注册新模块 +6. 前端路由不变(API 路径保持 `/api/v1/points/*`) +7. 删除 `erp-health` 中的积分相关代码 + +### 3.4 B4+B5:代码质量修复 + +**B4 — points_service.rs 拆分:** +- 随 B3 拆 crate 时一并处理 +- 按 domain 拆为 account/product/order/check_in/exchange/rule_engine 6 个子模块 +- 每个模块 < 400 行 + +**B5 — 密文版本标识:** +- 已在 §2.3.7 描述,加密输出格式改为 `v1|Base64(nonce + ciphertext)` +- 解密时根据版本前缀路由到对应算法 + +## 4. 批次 C:测试与质量 + +### 4.1 C0:测试并行化基础设施修复 + +**现状:** `cargo test --workspace` 因 PostgreSQL 连接数不足导致 101 个测试全部失败(非代码 bug)。 + +**修复方案:事务回滚模式** + +1. 每个测试开始时开事务 +2. 测试内的所有数据库操作在此事务中执行 +3. 测试结束(无论成功失败)回滚事务 +4. 多个测试共享同一个数据库连接池,无连接竞争 + +```rust +#[cfg(test)] +async fn test_db() -> DatabaseTransaction { + let db = DatabaseConnection::connect(&database_url).await.unwrap(); + db.begin().await.unwrap() +} +// 测试结束自动 drop DatabaseTransaction → 回滚 +``` + +**替代方案:** `--test-threads=4` 限制并行度(临时方案,不推荐长期使用)。 + +### 4.2 C1:安全测试(与批次 A 联动) + +每个批次 A 的修复对应一个安全测试: + +| 测试 | 验证目标 | 实现方式 | +|------|---------|---------| +| `test_sql_injection_prevention` | 动态 SQL 均参数化 | 注入 `'; DROP TABLE --` 断言不执行 | +| `test_cross_tenant_isolation` | 租户 A 看不到租户 B 数据 | 不同 tenant_id 查询断言为空 | +| `test_pii_round_trip_encryption` | 加密→解密一致 | HealthCrypto encrypt→decrypt→比对 | +| `test_blind_index_exact_match` | 盲索引精确搜索 | 插入→HMAC 搜索→断言找到 | +| `test_blind_index_no_false_positive` | 盲索引无误匹配 | 搜索不存在的值→断言为空 | +| `test_key_rotation_re_encrypt` | 密钥轮换后数据可解密 | 轮换→重加密→新密钥解密→比对 | +| `test_sse_token_key_correct` | SSE 用正确 token 键名 | 对比 auth store 和 message store 的 key | +| `test_rls_policy_enforcement` | RLS policy 兜底 | 设置不同 tenant_id session variable 断言隔离 | +| `test_cache_clear_preserves_auth` | 清缓存不破坏认证 | 模拟清缓存→断言 token 存在 | + +**测试实现原则:** +- 安全测试独立于业务测试,放在 `tests/security/` 目录 +- 每个测试可独立运行,不依赖其他测试的执行顺序 +- 使用事务回滚,测试间互不干扰 + +### 4.3 C2:事件消费者测试 + +| 测试 | 验证目标 | +|------|---------| +| `test_vital_critical_alert_created` | 危急值事件→创建 alert 记录 | +| `test_vital_critical_acknowledge` | 医生确认→状态更新 | +| `test_vital_critical_escalation` | 30min 未确认→升级 | +| `test_event_idempotency` | 同一事件消费两次→只创建一条 alert | +| `test_dead_letter_on_failure` | 消费失败→转入 dead-letter | + +### 4.4 C3:API 集成测试骨架 + +为关键 API 端点建立集成测试模式: + +```rust +// tests/integration/api_test.rs +#[tokio::test] +async fn test_patient_crud() { + let app = TestApp::new().await; + let auth = app.login_as_doctor().await; + + // Create + let resp = app.post("/api/v1/health/patients") + .set_auth(&auth) + .json(json!({ "name": "测试患者", ... })) + .send().await; + assert_eq!(resp.status(), 201); + + // Read + let patient = app.get(&format!("/api/v1/health/patients/{}", id)) + .set_auth(&auth) + .send().await; + assert_eq!(patient.status(), 200); +} +``` + +优先覆盖的 API: +- 患者管理 CRUD +- 预约创建/取消 +- 健康数据录入 +- 积分账户操作 + +### 4.5 C4:前端 chunk 拆分优化 + +**现状:** Vite 构建产生超大 chunk — antd 1.5MB、charts 1.4MB。 + +**修复方案:** +1. 配置 `manualChunks` 将 antd/antd-icons 拆为独立 vendor chunk +2. echarts 按需引入(只导入使用的图表类型) +3. 路由级 code splitting(React.lazy + Suspense) +4. 目标:首屏 JS < 500KB(gzip 后) + +## 5. 核心决策记录 + +| 编号 | 决策 | 理由 | 影响 | +|------|------|------|------| +| D1 | PII 密钥管理 → 方案 D(后端加解密 + Blind Index) | 前端不接触密钥,安全性最高;Blind Index 支持搜索 | crypto.rs 重构、新增 blind_indexes 表、小程序移除加解密 | +| D2 | 积分系统 → 独立 erp-points crate | 清晰模块边界,积分可跨业务复用 | 新 crate + 6 实体迁移 + 事件契约 | +| D3 | 事件消费者 → vital.critical 最先 | 医疗安全底线,30min 未确认分级升级 | 新增 critical_alerts/responses 表 + 升级定时任务 | +| D4 | 测试入口 → 安全测试驱动 | 与批次 A 联动,修一个写一个 | 事务回滚模式 + 9 个安全测试 | +| D5 | 还债策略 → 按类型分批(安全→事件→测试) | 避免上下文切换,每批有清晰完成标准 | 3 批次串行执行 | + +## 6. 实施顺序与依赖关系 + +``` +批次 A(安全) 批次 B(事件) 批次 C(测试) +───────────────────── ───────────────── ──────────────── +A1+A2 SQL 注入排查 ──测试──→ B1 vital.critical 消费者 C0 事务回滚模式 +A3 SSE Token 修复 ──测试──→ B2 EventBus dead-letter C1 安全测试 +A6 清缓存修复 ──测试──→ B3 积分拆 erp-points C2 事件测试 +A4+A5+A8+A9 PII 方案 D ──测试──→ B4+B5 代码质量 C3 集成测试 +A7 RLS policy 兜底 ──测试──→ C4 chunk 优化 +``` + +**关键依赖:** +- C0(测试基础设施)**必须在批次 B 开始前完成**,作为批次 A 实施的前置条件 +- B1 依赖 C0(测试基础设施就绪) +- B3 可与 A4 并行(无交叉文件) +- C1 随 A1-A7 逐步推进(修一个写一个) +- C4 独立于所有其他任务,可随时执行 + +**每个任务的产出:** +- 代码修改 + 对应测试 +- 编译通过(`cargo check`) +- 测试通过(`cargo test --test-threads=4`) +- Git 提交 +