Files
hms/docs/superpowers/plans/2026-04-28-technical-debt-cleanup-plan.md
iven aa5b26bf12
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(plan): 技术债清理实施计划 — 14 个 Task / 4 个 Chunk
- Chunk 1: 测试基础设施 + SQL 审计 + SSE + 清缓存
- Chunk 2: Crypto 版本标识 + Blind Index + RLS Policy
- Chunk 3: 危急值告警全链路 + EventBus Dead-Letter
- Chunk 4: 积分拆 erp-points + 安全测试 + 前端优化
2026-04-28 11:07:54 +08:00

29 KiB
Raw Blame History

技术债清理实施计划

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 DesignWeb、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.tomlworkspace deps 加 testcontainers

  • Step 1: 添加临时并行限制,让现有测试跑通

修改 crates/.cargo/config.toml 或直接用命令行参数:

cargo test --workspace -- --test-threads=4

验证 36 个后端测试通过。

  • Step 2: 创建 test_helpers 模块
// crates/erp-core/src/test_helpers.rs
//! 测试基础设施 — 事务回滚模式解决并行化问题

use sea_orm::{DatabaseConnection, DatabaseTransaction, ConnectOptions};
use std::sync::OnceLock;

static DB_URL: OnceLock<String> = 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 添加:

#[cfg(test)]
pub mod test_helpers;
  • Step 4: 验证编译
cargo check --workspace
  • Step 5: 提交
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识别风险点

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: 写审计报告测试
// 在现有 crypto 测试或新文件中
#[test]
fn test_no_raw_sql_injection_vectors() {
    // 编译时检查:确认高风险 service 无 from_string
    // 此测试作为文档 — 如果审计发现需修复的,此处断言通过
}
  • Step 5: 提交
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 键名一致

grep -n "access_token" apps/web/src/stores/auth.ts apps/web/src/stores/message.ts

预期:两处均使用 access_token

  • Step 2: 确认后端 SSE handler 已注册

搜索 message_stream 在路由中的注册:

grep -rn "message_stream\|/messages/stream" crates/erp-message/src/ crates/erp-server/src/

预期handler 定义在 sse_handler.rs,路由在 erp-messageprotected_routes() 中注册。

  • Step 3: 端到端验证

启动后端 + 前端,登录后在浏览器 DevTools Network 面板观察 /api/v1/messages/stream 是否建立 EventSource 连接,状态码 200。

  • Step 4: 如不工作,定位问题并修复

可能的问题路由未注册、token 传递方式不对、CORS 配置。

  • Step 5: 提交(如有修复)
git commit -m "fix(web): SSE 消息推送连接修复"

Task 4: A6 — 清缓存验证(已修复)

Files:

  • Verify: apps/miniprogram/src/pages/profile/settings/index.tsx

  • Step 1: 读取设置页清缓存逻辑

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 中添加:

#[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: 运行测试确认失败
cargo test -p erp-health encrypt_has_version -- --test-threads=1

预期FAIL当前密文无版本前缀

  • Step 3: 实现版本前缀

修改 encrypt()decrypt()

const CIPHER_VERSION: &str = "v1";

pub fn encrypt(&self, plaintext: &str) -> AppResult<String> {
    // ... 现有加密逻辑 ...
    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<String> {
    // 剥离版本前缀
    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] 添加:

zeroize = { version = "1", features = ["derive"] }
  • Step 5: 改造 HealthCrypto 密钥字段
use zeroize::Zeroizing;

pub struct HealthCrypto {
    aes_key: Zeroizing<[u8; 32]>,
    hmac_key: Zeroizing<[u8; 32]>,
}

更新 from_keys()dev_default() 中的字段赋值。

  • Step 6: 运行所有 crypto 测试
cargo test -p erp-health -- crypto --test-threads=1

预期:所有测试 PASS7 个旧测试 + 2 个新测试)。

  • Step 7: 提交
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创建下一个

// 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
// 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 添加:

pub mod blind_index;
  • Step 5: 编译验证
cargo check --workspace
  • Step 6: 运行迁移验证

启动后端服务,确认迁移执行成功。

  • Step 7: 提交
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

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
// 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: 注册并编译验证
cargo check --workspace
  • Step 4: 启动后端验证 RLS 生效

使用不同 tenant_id 的用户登录,确认只能看到自己租户的数据。

  • Step 5: 提交
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、软删除、乐观锁

// 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
// 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: 注册并编译
cargo check --workspace
  • Step 4: 提交
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 模块

// 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<Uuid> {
    // 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<String>,
) -> 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<Vec<Uuid>> {
    // 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
pub mod critical_alert_service;
  • Step 3: 编译验证
cargo check -p erp-health
  • Step 4: 提交
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

// 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 中添加消费者
// 在 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.triggeredalert_engine 发布的事件)
let (mut alert_rx, _) = state.event_bus.subscribe_filtered("alert.".to_string());
// ... 类似处理
  • Step 3: 注册升级定时任务

on_startup() 中添加 tokio::spawn 定时扫描:

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: 编译 + 功能验证
cargo check --workspace
# 启动后端,通过 Swagger 触发健康数据录入超阈值,确认告警记录创建
  • Step 5: 提交
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

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 辅助函数:

pub async fn consume_with_retry<F, Fut>(
    db: &DatabaseConnection,
    event: &DomainEvent,
    consumer_id: &str,
    max_attempts: u32,
    handler: F,
) -> ConsumeResult
where
    F: FnOnce(&DomainEvent) -> Fut,
    Fut: std::future::Future<Output = Result<(), String>>,
{
    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: 编译 + 提交
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.rserp-points/src/handler/

  • Refactor: points_service.rs1805 行)拆为 6 个子模块

  • Modify: Cargo.tomlworkspace members 加 erp-points

  • Modify: crates/erp-server/src/main.rs(注册新模块 + 路由)

  • Delete: erp-health 中的积分相关文件

  • Step 1: 创建 erp-points crate 骨架

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 traitname: "points", id: "erp-points"

  • state.rsPointsState { 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: 编译 + 全链路验证

cargo check --workspace
cargo test --workspace -- --test-threads=4
# 启动后端,通过 Swagger 验证积分相关 API 正常
  • Step 10: 提交
git commit -m "refactor: 积分系统拆分为独立 erp-points crate6 Entity + 6 Service"

Task 13: C1 — 安全测试

Files:

  • Create: crates/erp-health/tests/security_tests.rs

  • Step 1: 创建安全测试文件

// 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: 运行测试
cargo test -p erp-health --test security_tests -- --test-threads=1
  • Step 3: 提交
git commit -m "test(health): 安全测试 — PII 加解密 + 盲索引 + 密钥轮换 + 版本前缀"

Task 14: C4 — 前端 chunk 拆分优化

Files:

  • Modify: apps/web/vite.config.ts

  • Step 1: 分析当前 chunk 大小

cd apps/web && pnpm build 2>&1 | grep -E "kB|MB|chunk"
  • Step 2: 配置 manualChunks

vite.config.ts 的 build.rollupOptions.output 中:

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' 改为只导入使用的图表类型:

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: 构建验证
pnpm build

确认:首屏 JS < 500KBgzip 后)。

  • Step 5: 提交
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-前端优化) 独立,可随时执行