# 技术债清理实施计划 > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 消除 HMS 平台的 6 个 Critical 安全问题、补全事件消费闭环、建立测试基础设施。 **Architecture:** 三批次串行执行 — 安全修复 → 事件架构 → 测试质量。每个修复伴随对应测试。所有数据库变更通过 `crates/erp-server/migration/src/` 统一管理。 **Tech Stack:** Rust/SeaORM/Axum(后端)、React/Ant Design(Web)、Taro(小程序) **Spec:** `docs/superpowers/specs/2026-04-28-technical-debt-cleanup-design.md` --- ## Chunk 1: 前置 + 批次 A 前半 ### Task 1: C0 — 测试并行化修复(前置条件) **Files:** - Create: `crates/erp-core/src/test_helpers.rs` - Modify: `crates/erp-core/src/lib.rs` - Modify: `Cargo.toml`(workspace deps 加 testcontainers) - [ ] **Step 1: 添加临时并行限制,让现有测试跑通** 修改 `crates/.cargo/config.toml` 或直接用命令行参数: ```bash cargo test --workspace -- --test-threads=4 ``` 验证 36 个后端测试通过。 - [ ] **Step 2: 创建 test_helpers 模块** ```rust // crates/erp-core/src/test_helpers.rs //! 测试基础设施 — 事务回滚模式解决并行化问题 use sea_orm::{DatabaseConnection, DatabaseTransaction, ConnectOptions}; use std::sync::OnceLock; static DB_URL: OnceLock = OnceLock::new(); fn db_url() -> String { DB_URL.get_or_init(|| { std::env::var("TEST_DATABASE_URL") .unwrap_or_else(|_| "postgres://erp:erp@localhost:5432/erp_test".into()) }).clone() } /// 创建测试用事务。测试结束自动回滚,无数据残留。 pub async fn test_txn() -> DatabaseTransaction { let opt = ConnectOptions::new(db_url()); let db = DatabaseConnection::connect(opt).await .expect("测试数据库连接失败"); db.begin().await.expect("测试事务创建失败") } ``` - [ ] **Step 3: 注册模块** 在 `crates/erp-core/src/lib.rs` 添加: ```rust #[cfg(test)] pub mod test_helpers; ``` - [ ] **Step 4: 验证编译** ```bash cargo check --workspace ``` - [ ] **Step 5: 提交** ```bash git add crates/erp-core/src/test_helpers.rs crates/erp-core/src/lib.rs git commit -m "test(core): 添加事务回滚测试基础设施" ``` --- ### Task 2: A1+A2 — SQL 注入审计 **Files:** - Modify: `crates/erp-server/src/middleware/tenant_rls.rs`(如需参数化) - Audit: `crates/erp-plugin/src/` dynamic_table 相关 - Audit: `crates/erp-health/src/service/` 原生 SQL - [ ] **Step 1: 全局搜索 Statement::from_string,识别风险点** ```bash cd g:/hms && grep -rn "from_string" crates/*/src/ --include="*.rs" | grep -v migration | grep -v target ``` 记录每个命中的文件、行号、是否含用户输入。 - [ ] **Step 2: 逐文件审计,区分安全(migration/静态SQL)和风险(含用户输入)** **已知安全(参数化):** - `follow_up_service.rs:78` — 使用 `Statement::from_sql_and_values` + `$N` **需审计:** - `crates/erp-server/src/middleware/tenant_rls.rs` — SET 命令,tenant_id 来自 JWT(可信源) - `crates/erp-plugin/src/` — dynamic_table SQL,需检查 identifier 是否经 `sanitize_identifier` - [ ] **Step 3: 修复发现的问题(如有)** 对含用户输入的 `from_string` 改为 `from_sql_and_values`。 - [ ] **Step 4: 写审计报告测试** ```rust // 在现有 crypto 测试或新文件中 #[test] fn test_no_raw_sql_injection_vectors() { // 编译时检查:确认高风险 service 无 from_string // 此测试作为文档 — 如果审计发现需修复的,此处断言通过 } ``` - [ ] **Step 5: 提交** ```bash git commit -m "fix(security): SQL 注入审计 — 确认参数化查询覆盖" ``` --- ### Task 3: A3 — SSE 连接验证 **Files:** - Verify: `apps/web/src/stores/message.ts` - Verify: `apps/web/src/stores/auth.ts` - Verify: `crates/erp-message/src/handler/sse_handler.rs` - [ ] **Step 1: 确认 token 键名一致** ```bash grep -n "access_token" apps/web/src/stores/auth.ts apps/web/src/stores/message.ts ``` 预期:两处均使用 `access_token`。 - [ ] **Step 2: 确认后端 SSE handler 已注册** 搜索 `message_stream` 在路由中的注册: ```bash grep -rn "message_stream\|/messages/stream" crates/erp-message/src/ crates/erp-server/src/ ``` 预期:handler 定义在 `sse_handler.rs`,路由在 `erp-message` 的 `protected_routes()` 中注册。 - [ ] **Step 3: 端到端验证** 启动后端 + 前端,登录后在浏览器 DevTools Network 面板观察 `/api/v1/messages/stream` 是否建立 EventSource 连接,状态码 200。 - [ ] **Step 4: 如不工作,定位问题并修复** 可能的问题:路由未注册、token 传递方式不对、CORS 配置。 - [ ] **Step 5: 提交(如有修复)** ```bash git commit -m "fix(web): SSE 消息推送连接修复" ``` --- ### Task 4: A6 — 清缓存验证(已修复) **Files:** - Verify: `apps/miniprogram/src/pages/profile/settings/index.tsx` - [ ] **Step 1: 读取设置页清缓存逻辑** ```bash grep -n -A 10 "clearStorage\|clearCache\|preserveKeys" apps/miniprogram/src/pages/profile/settings/index.tsx ``` - [ ] **Step 2: 确认 preserve-and-restore 覆盖关键 key** 验证列表包含:`access_token`, `refresh_token`, `userInfo`, `patientId` 等。 - [ ] **Step 3: MCP 自动化验证(可选)** 通过小程序 MCP 连接 → 设置页 → 点击清除缓存 → 返回首页 → 确认未跳转登录页。 - [ ] **Step 4: 如无问题,标记为已验证,无需提交** 如发现遗漏 key,补充后提交。 --- ## Chunk 2: 批次 A 后半 — PII 加解密 + RLS ### Task 5: A4 — crypto.rs 版本标识 + zeroize **Files:** - Modify: `crates/erp-health/src/crypto.rs` - Modify: `crates/erp-health/Cargo.toml`(加 zeroize 依赖) - [ ] **Step 1: 写失败测试 — 版本前缀加解密** 在 `crates/erp-health/src/crypto.rs` 的 `#[cfg(test)] mod tests` 中添加: ```rust #[test] fn encrypt_has_version_prefix() { let crypto = test_crypto(); let encrypted = crypto.encrypt("test").unwrap(); assert!(encrypted.starts_with("v1|"), "密文应以 v1| 开头"); } #[test] fn decrypt_versioned_ciphertext() { let crypto = test_crypto(); let encrypted = crypto.encrypt("test123").unwrap(); let decrypted = crypto.decrypt(&encrypted).unwrap(); assert_eq!("test123", decrypted); } ``` - [ ] **Step 2: 运行测试确认失败** ```bash cargo test -p erp-health encrypt_has_version -- --test-threads=1 ``` 预期:FAIL(当前密文无版本前缀)。 - [ ] **Step 3: 实现版本前缀** 修改 `encrypt()` 和 `decrypt()`: ```rust const CIPHER_VERSION: &str = "v1"; pub fn encrypt(&self, plaintext: &str) -> AppResult { // ... 现有加密逻辑 ... let mut combined = nonce_bytes.as_bytes()[..12].to_vec(); combined.extend_from_slice(&ciphertext); Ok(format!("{}|{}", CIPHER_VERSION, BASE64.encode(&combined))) } pub fn decrypt(&self, encoded: &str) -> AppResult { // 剥离版本前缀 let b64 = if let Some(pos) = encoded.find('|') { &encoded[pos + 1..] } else { // 兼容旧格式(无版本前缀) encoded }; let combined = BASE64.decode(b64)?; // ... 现有解密逻辑 ... } ``` - [ ] **Step 4: 添加 zeroize 依赖** 在 `crates/erp-health/Cargo.toml` 的 `[dependencies]` 添加: ```toml zeroize = { version = "1", features = ["derive"] } ``` - [ ] **Step 5: 改造 HealthCrypto 密钥字段** ```rust use zeroize::Zeroizing; pub struct HealthCrypto { aes_key: Zeroizing<[u8; 32]>, hmac_key: Zeroizing<[u8; 32]>, } ``` 更新 `from_keys()` 和 `dev_default()` 中的字段赋值。 - [ ] **Step 6: 运行所有 crypto 测试** ```bash cargo test -p erp-health -- crypto --test-threads=1 ``` 预期:所有测试 PASS(7 个旧测试 + 2 个新测试)。 - [ ] **Step 7: 提交** ```bash git add crates/erp-health/src/crypto.rs crates/erp-health/Cargo.toml git commit -m "fix(health): 密文版本标识 v1 前缀 + DEK zeroize" ``` --- ### Task 6: A5 — Blind Index Entity + Migration **Files:** - Create: `crates/erp-health/src/entity/blind_index.rs` - Modify: `crates/erp-health/src/entity/mod.rs` - Create: `crates/erp-server/migration/src/m20260429_000089_blind_indexes.rs`(新迁移序号需确认) - Modify: `crates/erp-server/migration/src/lib.rs` - [ ] **Step 1: 创建 Migration 文件** 确认当前最大迁移序号(88),创建下一个: ```rust // crates/erp-server/migration/src/m20260429_000089_blind_indexes.rs #[derive(DeriveMigrationName)] pub struct Migration; #[async_trait::async_trait] impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager.create_table( Table::create() .table(BlindIndex::Table) .col(ColumnDef::new(BlindIndex::Id).uuid().not_null().primary_key().default(PgFunc::gen_random_uuid())) .col(ColumnDef::new(BlindIndex::TenantId).uuid().not_null()) .col(ColumnDef::new(BlindIndex::EntityType).string_len(64).not_null()) .col(ColumnDef::new(BlindIndex::EntityId).uuid().not_null()) .col(ColumnDef::new(BlindIndex::FieldName).string_len(64).not_null()) .col(ColumnDef::new(BlindIndex::BlindHash).string_len(64).not_null()) .col(ColumnDef::new(BlindIndex::CreatedAt).timestamp_with_time_zone().not_null().default(PgFunc::current_timestamp())) .col(ColumnDef::new(BlindIndex::UpdatedAt).timestamp_with_time_zone().not_null().default(PgFunc::current_timestamp())) .index(Index::create().col(BlindIndex::TenantId).col(BlindIndex::EntityType).col(BlindIndex::FieldName).col(BlindIndex::BlindHash).unique()) .to_owned(), ).await?; manager.create_index( Index::create() .name("idx_blind_hashes") .table(BlindIndex::Table) .col(BlindIndex::TenantId) .col(BlindIndex::EntityType) .col(BlindIndex::FieldName) .col(BlindIndex::BlindHash) .to_owned(), ).await } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager.drop_table(Table::drop().table(BlindIndex::Table).to_owned()).await } } #[derive(Iden)] enum BlindIndex { Table, Id, TenantId, EntityType, EntityId, FieldName, BlindHash, CreatedAt, UpdatedAt, } ``` - [ ] **Step 2: 注册 Migration** 在 `crates/erp-server/migration/src/lib.rs` 中添加 mod 声明和 Box::new 注册。 - [ ] **Step 3: 创建 SeaORM Entity** ```rust // crates/erp-health/src/entity/blind_index.rs //! Blind Index — HMAC-SHA256 盲索引支持 PII 精确搜索 use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] #[sea_orm(table_name = "blind_indexes")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: Uuid, pub tenant_id: Uuid, pub entity_type: String, pub entity_id: Uuid, pub field_name: String, pub blind_hash: String, pub created_at: DateTimeUtc, pub updated_at: DateTimeUtc, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation {} impl ActiveModelBehavior for ActiveModel {} ``` - [ ] **Step 4: 注册 Entity** 在 `crates/erp-health/src/entity/mod.rs` 添加: ```rust pub mod blind_index; ``` - [ ] **Step 5: 编译验证** ```bash cargo check --workspace ``` - [ ] **Step 6: 运行迁移验证** 启动后端服务,确认迁移执行成功。 - [ ] **Step 7: 提交** ```bash git add crates/erp-health/src/entity/blind_index.rs crates/erp-health/src/entity/mod.rs crates/erp-server/migration/src/m20260429_000089_blind_indexes.rs crates/erp-server/migration/src/lib.rs git commit -m "feat(health): 新增 blind_indexes 表 + Entity 支持 PII 盲索引搜索" ``` --- ### Task 7: A7 — PostgreSQL RLS Policy 兜底 **Files:** - Verify: `crates/erp-server/src/middleware/tenant_rls.rs`(已存在 SET 语句) - Create: `crates/erp-server/migration/src/m20260429_000090_rls_policies.rs` - Modify: `crates/erp-server/migration/src/lib.rs` - [ ] **Step 1: 确认 tenant_rls 中间件已设置 session variable** ```bash grep -n "app.current_tenant_id" crates/erp-server/src/middleware/tenant_rls.rs ``` 预期:已有 `SET app.current_tenant_id = '{tenant_id}'`。 - [ ] **Step 2: 创建 RLS policy Migration** ```rust // crates/erp-server/migration/src/m20260429_000090_rls_policies.rs #[derive(DeriveMigrationName)] pub struct Migration; const TENANT_TABLES: &[&str] = &[ "users", "roles", "departments", "dict_items", "menus", "patients", "doctors", "appointments", "health_data", "follow_up_tasks", "follow_up_records", "consultations", "points_accounts", "points_products", "points_orders", // ... 其余含 tenant_id 的表 ]; #[async_trait::async_trait] impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { for table in TENANT_TABLES { let sql = format!( "ALTER TABLE {} ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON {} USING (tenant_id::text = current_setting('app.current_tenant_id', true));", table, table ); manager.get_connection().execute_unprepared(&sql).await?; } Ok(()) } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { for table in TENANT_TABLES { let sql = format!( "DROP POLICY IF EXISTS tenant_isolation ON {}; ALTER TABLE {} DISABLE ROW LEVEL SECURITY;", table, table ); manager.get_connection().execute_unprepared(&sql).await?; } Ok(()) } } ``` - [ ] **Step 3: 注册并编译验证** ```bash cargo check --workspace ``` - [ ] **Step 4: 启动后端验证 RLS 生效** 使用不同 tenant_id 的用户登录,确认只能看到自己租户的数据。 - [ ] **Step 5: 提交** ```bash git commit -m "feat(db): 为所有 tenant 表启用 RLS policy 兜底" ``` --- ## Chunk 3: 批次 B — 事件架构 ### Task 8: B1 — 危急值告警 Entity + Migration **Files:** - Create: `crates/erp-health/src/entity/critical_alert.rs` - Create: `crates/erp-server/migration/src/m20260429_000091_critical_alerts.rs` - Modify: `crates/erp-health/src/entity/mod.rs` - Modify: `crates/erp-server/migration/src/lib.rs` - [ ] **Step 1: 创建 Migration — critical_alerts + critical_alert_responses 两张表** 遵循现有 Entity 模式(UUID v7 主键、tenant_id、软删除、乐观锁)。 ```rust // crates/erp-server/migration/src/m20260429_000091_critical_alerts.rs // up(): 创建 critical_alerts 表和 critical_alert_responses 表 // down(): DROP TABLE critical_alert_responses; DROP TABLE critical_alerts; // 详见设计规格 §3.1.3 的 SQL ``` - [ ] **Step 2: 创建 SeaORM Entity** ```rust // crates/erp-health/src/entity/critical_alert.rs // Model 字段: id, tenant_id, patient_id, alert_type, metric_name, // metric_value, threshold_value, severity, status, acknowledged_by, // acknowledged_at, escalation_level, created_at, updated_at, // created_by, updated_by, deleted_at, version // Relation: patient (belongs_to) ``` - [ ] **Step 3: 注册并编译** ```bash cargo check --workspace ``` - [ ] **Step 4: 提交** ```bash git commit -m "feat(health): 新增 critical_alerts + responses 表 + Entity" ``` --- ### Task 9: B1 — 危急值告警 Service **Files:** - Create: `crates/erp-health/src/service/critical_alert_service.rs` - Modify: `crates/erp-health/src/service/mod.rs` - [ ] **Step 1: 创建 service 模块** ```rust // crates/erp-health/src/service/critical_alert_service.rs //! 危急值告警 Service — 创建告警、确认、升级 /// 消费 health_data.critical_alert 事件,创建告警记录 pub async fn handle_critical_alert_event( db: &DatabaseConnection, tenant_id: Uuid, patient_id: Uuid, metric_name: &str, metric_value: &str, threshold_value: &str, ) -> HealthResult { // 1. 创建 critical_alert 记录(status = pending) // 2. 返回 alert_id } /// 医生确认告警 pub async fn acknowledge_alert( db: &DatabaseConnection, tenant_id: Uuid, alert_id: Uuid, responder_id: Uuid, notes: Option, ) -> HealthResult<()> { // 1. 更新 status = acknowledged, acknowledged_by, acknowledged_at // 2. 创建 critical_alert_response 记录 // 3. 检查乐观锁 version } /// 升级扫描 — 由定时任务每分钟调用 pub async fn scan_escalation( db: &DatabaseConnection, tenant_id: Uuid, ) -> HealthResult> { // 1. 查询 status = pending 且 created_at < 30min 的 → escalation_level = 1 // 2. 查询 status = pending 且 created_at < 60min 的 → escalation_level = 2 // 3. 返回升级的 alert_id 列表 } ``` - [ ] **Step 2: 注册到 service/mod.rs** ```rust pub mod critical_alert_service; ``` - [ ] **Step 3: 编译验证** ```bash cargo check -p erp-health ``` - [ ] **Step 4: 提交** ```bash git commit -m "feat(health): 危急值告警 service — 创建/确认/升级" ``` --- ### Task 10: B1 — 危急值告警 Handler + 事件注册 **Files:** - Create: `crates/erp-health/src/handler/critical_alert_handler.rs` - Modify: `crates/erp-health/src/handler/mod.rs` - Modify: `crates/erp-health/src/event.rs` - [ ] **Step 1: 创建 handler** ```rust // crates/erp-health/src/handler/critical_alert_handler.rs // 端点: // GET /health/critical-alerts — 列表(待确认 + 已升级) // GET /health/critical-alerts/:id — 详情 // POST /health/critical-alerts/:id/acknowledge — 确认 // 权限: health.critical-alert.list / health.critical-alert.manage ``` - [ ] **Step 2: 在 event.rs 的 register_handlers_with_state 中添加消费者** ```rust // 在 register_handlers_with_state() 中新增: let (mut critical_rx, _critical_handle) = state.event_bus .subscribe_filtered("health_data.".to_string()); let critical_db = state.db.clone(); tokio::spawn(async move { loop { match critical_rx.recv().await { Some(event) if event.event_type == HEALTH_DATA_CRITICAL_ALERT => { if erp_core::events::is_event_processed(&critical_db, event.id, "critical_alert_consumer").await.unwrap_or(false) { continue; // 幂等:已处理 } // 从 payload 提取参数,调用 critical_alert_service::handle_critical_alert_event // 成功后 mark_event_processed } Some(_) => {} None => break, } } }); // 同时监听 alert.triggered(alert_engine 发布的事件) let (mut alert_rx, _) = state.event_bus.subscribe_filtered("alert.".to_string()); // ... 类似处理 ``` - [ ] **Step 3: 注册升级定时任务** 在 `on_startup()` 中添加 tokio::spawn 定时扫描: ```rust tokio::spawn(async move { let mut interval = tokio::time::interval(Duration::from_secs(60)); loop { interval.tick().await; if let Err(e) = critical_alert_service::scan_escalation(&db, tenant_id).await { tracing::error!(error = %e, "危急值升级扫描失败"); } } }); ``` - [ ] **Step 4: 编译 + 功能验证** ```bash cargo check --workspace # 启动后端,通过 Swagger 触发健康数据录入超阈值,确认告警记录创建 ``` - [ ] **Step 5: 提交** ```bash git commit -m "feat(health): 危急值告警消费者 — 30min 分级升级 + 幂等处理" ``` --- ### Task 11: B2 — EventBus Dead-Letter **Files:** - Create: `crates/erp-server/migration/src/m20260429_000092_dead_letter_events.rs` - Create: `crates/erp-core/src/entity/dead_letter_event.rs` - Modify: `crates/erp-core/src/events.rs` - Create: `crates/erp-server/src/handler/dead_letter_handler.rs` - [ ] **Step 1: 创建 dead_letter_events Migration + Entity** ```sql CREATE TABLE dead_letter_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID, original_event_id UUID NOT NULL, event_type VARCHAR(128) NOT NULL, payload JSONB, consumer_id VARCHAR(128) NOT NULL, attempts INT NOT NULL DEFAULT 0, last_error TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), resolved_at TIMESTAMPTZ ); ``` - [ ] **Step 2: 在 EventBus 中添加失败处理逻辑** 在 `events.rs` 中新增 `consume_with_retry` 辅助函数: ```rust pub async fn consume_with_retry( db: &DatabaseConnection, event: &DomainEvent, consumer_id: &str, max_attempts: u32, handler: F, ) -> ConsumeResult where F: FnOnce(&DomainEvent) -> Fut, Fut: std::future::Future>, { if is_event_processed(db, event.id, consumer_id).await? { return ConsumeResult::AlreadyProcessed; } match handler(event).await { Ok(()) => { mark_event_processed(db, event.id, consumer_id).await?; ConsumeResult::Success } Err(err) => { // 转入 dead_letter_events insert_dead_letter(db, event, consumer_id, &err).await?; ConsumeResult::DeadLettered(err) } } } ``` - [ ] **Step 3: 创建 admin API 端点** ``` GET /api/v1/admin/dead-letter-events — 列表 POST /api/v1/admin/dead-letter-events/:id/retry — 重试 ``` - [ ] **Step 4: 编译 + 提交** ```bash cargo check --workspace git commit -m "feat(core): EventBus dead-letter + consume_with_retry 辅助函数" ``` --- ## Chunk 4: 批次 B 后半 + 批次 C ### Task 12: B3 — 积分系统拆分为 erp-points crate > 这是工作量最大的任务(~1 天),建议作为独立 subagent 任务执行。 **Files:** - Create: `crates/erp-points/` 整个 crate 目录 - Create: `crates/erp-points/Cargo.toml` - Create: `crates/erp-points/src/{lib,module,state,event,error}.rs` - Move: 6 个 entity 从 `erp-health/src/entity/points_*` → `erp-points/src/entity/` - Move: handler 从 `erp-health/src/handler/points_handler.rs` → `erp-points/src/handler/` - Refactor: `points_service.rs`(1805 行)拆为 6 个子模块 - Modify: `Cargo.toml`(workspace members 加 erp-points) - Modify: `crates/erp-server/src/main.rs`(注册新模块 + 路由) - Delete: `erp-health` 中的积分相关文件 - [ ] **Step 1: 创建 erp-points crate 骨架** ```bash mkdir -p crates/erp-points/src/{entity,service,handler,dto} ``` `Cargo.toml` 参照 `erp-health/Cargo.toml` 模式,依赖 `erp-core.workspace = true`。 - [ ] **Step 2: 实现标准模块** - `lib.rs` — 导出模块 - `module.rs` — 实现 `ErpModule` trait(name: "points", id: "erp-points") - `state.rs` — `PointsState { db, event_bus }` - `error.rs` — 错误类型 - [ ] **Step 3: 迁移 6 个 Entity 文件** 从 `crates/erp-health/src/entity/` 复制: - `points_account.rs`, `points_checkin.rs`, `points_order.rs` - `points_product.rs`, `points_rule.rs`, `points_transaction.rs` 更新 `mod.rs` 中的内部引用(`super::` → 指向 erp-points 的 entity)。 - [ ] **Step 4: 迁移并拆分 points_service.rs** 将 1805 行拆为: - `account_service.rs` — 积分账户 CRUD - `product_service.rs` — 商品管理 - `order_service.rs` — 订单 + 兑换 - `check_in_service.rs` — 签到 - `rule_engine.rs` — 积分规则引擎 - `exchange_service.rs` — 线下活动兑换 - [ ] **Step 5: 迁移 handler** - [ ] **Step 6: 注册到 workspace + erp-server** 在根 `Cargo.toml` 添加 `erp-points` 到 workspace members。 在 `erp-server/src/main.rs` 中: - 添加 `use erp_points::PointsModule;` - 在 `registry.register()` 链中添加 `.register(points_module)` - 在路由 merge 中添加 `.merge(erp_points::PointsModule::protected_routes())` - [ ] **Step 7: 删除 erp-health 中的积分代码** - 删除 6 个 entity 文件 - 删除 points_service.rs(或清空为 re-export) - 删除 points_handler.rs - 清理 entity/mod.rs 和 handler/mod.rs 中的 mod 声明 - [ ] **Step 8: 事件契约** 在 `erp-points/src/event.rs` 中: - 订阅 `lab_report.uploaded`, `patient.verified`, `daily_monitoring.created` - 发布 `points.earned`, `points.exchanged`, `points.expired`, `points.balance.changed` - [ ] **Step 9: 编译 + 全链路验证** ```bash cargo check --workspace cargo test --workspace -- --test-threads=4 # 启动后端,通过 Swagger 验证积分相关 API 正常 ``` - [ ] **Step 10: 提交** ```bash git commit -m "refactor: 积分系统拆分为独立 erp-points crate(6 Entity + 6 Service)" ``` --- ### Task 13: C1 — 安全测试 **Files:** - Create: `crates/erp-health/tests/security_tests.rs` - [ ] **Step 1: 创建安全测试文件** ```rust // crates/erp-health/tests/security_tests.rs #[cfg(test)] mod tests { use erp_health::HealthCrypto; #[test] fn test_pii_encrypt_decrypt_roundtrip() { let crypto = HealthCrypto::dev_default(); let plaintext = "110101199001011234"; let encrypted = crypto.encrypt(plaintext).unwrap(); let decrypted = crypto.decrypt(&encrypted).unwrap(); assert_eq!(plaintext, decrypted); } #[test] fn test_blind_index_deterministic() { let crypto = HealthCrypto::dev_default(); let h1 = crypto.hmac_hash("13812345678"); let h2 = crypto.hmac_hash("13812345678"); assert_eq!(h1, h2); } #[test] fn test_blind_index_different_inputs() { let crypto = HealthCrypto::dev_default(); let h1 = crypto.hmac_hash("13812345678"); let h2 = crypto.hmac_hash("13898765432"); assert_ne!(h1, h2); } #[test] fn test_ciphertext_has_version_prefix() { let crypto = HealthCrypto::dev_default(); let encrypted = crypto.encrypt("test").unwrap(); assert!(encrypted.starts_with("v1|")); } #[test] fn test_decrypt_legacy_no_prefix() { // 确认旧格式(无版本前缀)仍可解密 let crypto = HealthCrypto::dev_default(); // 手动构造旧格式密文... } #[test] fn test_key_rotation() { let old_crypto = HealthCrypto::dev_default(); let hex_key = "ab".repeat(32); let new_crypto = HealthCrypto::from_keys(&hex_key, &hex_key).unwrap(); let plaintext = "敏感数据"; let encrypted = old_crypto.encrypt(plaintext).unwrap(); // 用旧密钥解密 let decrypted = old_crypto.decrypt(&encrypted).unwrap(); assert_eq!(plaintext, decrypted); // 用新密钥解密应失败 assert!(new_crypto.decrypt(&encrypted).is_err()); } } ``` - [ ] **Step 2: 运行测试** ```bash cargo test -p erp-health --test security_tests -- --test-threads=1 ``` - [ ] **Step 3: 提交** ```bash git commit -m "test(health): 安全测试 — PII 加解密 + 盲索引 + 密钥轮换 + 版本前缀" ``` --- ### Task 14: C4 — 前端 chunk 拆分优化 **Files:** - Modify: `apps/web/vite.config.ts` - [ ] **Step 1: 分析当前 chunk 大小** ```bash cd apps/web && pnpm build 2>&1 | grep -E "kB|MB|chunk" ``` - [ ] **Step 2: 配置 manualChunks** 在 `vite.config.ts` 的 build.rollupOptions.output 中: ```typescript manualChunks: { 'vendor-antd': ['antd', '@ant-design/icons'], 'vendor-charts': ['echarts'], 'vendor-react': ['react', 'react-dom', 'react-router-dom'], }, ``` - [ ] **Step 3: echarts 按需引入** 将 `import * as echarts from 'echarts'` 改为只导入使用的图表类型: ```typescript import * as echarts from 'echarts/core'; import { LineChart, BarChart } from 'echarts/charts'; import { GridComponent, TooltipComponent } from 'echarts/components'; import { CanvasRenderer } from 'echarts/renderers'; echarts.use([LineChart, BarChart, GridComponent, TooltipComponent, CanvasRenderer]); ``` - [ ] **Step 4: 构建验证** ```bash pnpm build ``` 确认:首屏 JS < 500KB(gzip 后)。 - [ ] **Step 5: 提交** ```bash git commit -m "perf(web): 前端 chunk 拆分 — antd/charts vendor 分离 + echarts 按需引入" ``` --- ## 执行摘要 | Chunk | Tasks | 内容 | |-------|-------|------| | 1 | T1-T4 | 测试基础设施 + SQL 审计 + SSE 验证 + 清缓存验证 | | 2 | T5-T7 | Crypto 版本标识 + Blind Index Entity + RLS Policy | | 3 | T8-T11 | 危急值告警全链路 + EventBus Dead-Letter | | 4 | T12-T14 | 积分拆 erp-points + 安全测试 + 前端优化 | **总计 14 个 Task,预估 8-12 天。** **关键依赖链:** ``` T1(C0) → T5(A4) → T6(A5) T1(C0) → T8(B1-Entity) → T9(B1-Service) → T10(B1-Handler) T1(C0) → T13(C1-安全测试) T12(B3-积分拆分) 独立,可与 T5-T7 并行 T14(C4-前端优化) 独立,可随时执行 ```