- Chunk 1: 测试基础设施 + SQL 审计 + SSE + 清缓存 - Chunk 2: Crypto 版本标识 + Blind Index + RLS Policy - Chunk 3: 危急值告警全链路 + EventBus Dead-Letter - Chunk 4: 积分拆 erp-points + 安全测试 + 前端优化
29 KiB
技术债清理实施计划
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 或直接用命令行参数:
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-message 的 protected_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
预期:所有测试 PASS(7 个旧测试 + 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.triggered(alert_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.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 骨架
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— 实现ErpModuletrait(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.rspoints_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 crate(6 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 < 500KB(gzip 后)。
- 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-前端优化) 独立,可随时执行