Files
hms/docs/superpowers/specs/2026-04-28-technical-debt-cleanup-design.md
iven 755d95480e docs(spec): 技术债清理设计规格 — 安全/事件/测试三批次策略
发散式技术债讨论结论,涵盖:
- 批次 A:安全合规(SQL 审计、PII 后端加解密+Blind Index、RLS 兜底)
- 批次 B:事件架构(vital.critical 消费者优先、积分拆 erp-points crate)
- 批次 C:测试质量(事务回滚模式、安全测试驱动)
2026-04-28 10:03:03 +08:00

557 lines
22 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-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+A2SQL 注入风险排查与修复
**现状:** 审计报告标记 `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 A3SSE 连接有效性验证
**现状:** `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+A9PII 后端加解密 + 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 A7PostgreSQL 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 B2EventBus 可靠性增强
**现状:** 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 C3API 集成测试骨架
为关键 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 splittingReact.lazy + Suspense
4. 目标:首屏 JS < 500KBgzip 后)
## 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 提交