feat(dialysis): 激活 erp-dialysis 独立模块 — 注册到 erp-server
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

- workspace Cargo.toml 添加 erp-dialysis 依赖声明
- erp-server 注册 DialysisModule 并挂载透析路由
- 修复权限码:health.health-data.* → health.dialysis.list/manage
- 集成测试迁移:erp_health → erp_dialysis import + DialysisState
- TestApp 新增 dialysis_state() 方法
- cargo check 通过,erp-dialysis 10 个单元测试全部通过
This commit is contained in:
iven
2026-04-28 15:21:13 +08:00
parent 75cd305996
commit 5941a6b764
11 changed files with 81 additions and 47 deletions

1
Cargo.lock generated
View File

@@ -1656,6 +1656,7 @@ dependencies = [
"erp-auth",
"erp-config",
"erp-core",
"erp-dialysis",
"erp-health",
"erp-message",
"erp-plugin",

View File

@@ -107,6 +107,7 @@ erp-plugin = { path = "crates/erp-plugin" }
erp-health = { path = "crates/erp-health" }
erp-ai = { path = "crates/erp-ai" }
erp-points = { path = "crates/erp-points" }
erp-dialysis = { path = "crates/erp-dialysis" }
# Async streaming
futures = "0.3"

View File

@@ -41,7 +41,7 @@ where
DialysisState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.list")?;
require_permission(&ctx, "health.dialysis.list")?;
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20);
let result = dialysis_service::list_dialysis_records(
@@ -60,7 +60,7 @@ where
DialysisState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.list")?;
require_permission(&ctx, "health.dialysis.list")?;
let result = dialysis_service::get_dialysis_record(
&state, ctx.tenant_id, record_id,
)
@@ -77,7 +77,7 @@ where
DialysisState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.manage")?;
require_permission(&ctx, "health.dialysis.manage")?;
let mut req = req;
req.sanitize();
let result = dialysis_service::create_dialysis_record(
@@ -97,7 +97,7 @@ where
DialysisState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.manage")?;
require_permission(&ctx, "health.dialysis.manage")?;
let mut data = req.data;
data.sanitize();
let result = dialysis_service::update_dialysis_record(
@@ -117,7 +117,7 @@ where
DialysisState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.manage")?;
require_permission(&ctx, "health.dialysis.manage")?;
let result = dialysis_service::review_dialysis_record(
&state, ctx.tenant_id, record_id, ctx.user_id, req.version,
)
@@ -135,7 +135,7 @@ where
DialysisState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.manage")?;
require_permission(&ctx, "health.dialysis.manage")?;
dialysis_service::delete_dialysis_record(
&state, ctx.tenant_id, record_id, Some(ctx.user_id), req.version,
)

View File

@@ -78,13 +78,13 @@ impl ErpModule for DialysisModule {
fn permissions(&self) -> Vec<PermissionDescriptor> {
vec![
PermissionDescriptor {
code: "health.health-data.list".into(),
code: "health.dialysis.list".into(),
name: "查看透析记录".into(),
description: "查看透析记录列表和详情".into(),
module: "erp-dialysis".into(),
},
PermissionDescriptor {
code: "health.health-data.manage".into(),
code: "health.dialysis.manage".into(),
name: "管理透析记录".into(),
description: "创建、编辑、审阅、删除透析记录".into(),
module: "erp-dialysis".into(),

View File

@@ -30,6 +30,7 @@ erp-message.workspace = true
erp-plugin.workspace = true
erp-health.workspace = true
erp-ai.workspace = true
erp-dialysis.workspace = true
erp-points.workspace = true
anyhow.workspace = true
uuid.workspace = true
@@ -41,3 +42,4 @@ erp-auth = { workspace = true }
erp-plugin = { workspace = true }
erp-workflow = { workspace = true }
erp-core = { workspace = true }
erp-dialysis = { workspace = true }

View File

@@ -357,6 +357,14 @@ async fn main() -> anyhow::Result<()> {
"Points module initialized"
);
// Initialize dialysis module
let dialysis_module = erp_dialysis::DialysisModule;
tracing::info!(
module = dialysis_module.name(),
version = dialysis_module.version(),
"Dialysis module initialized"
);
// Initialize module registry and register modules
let registry = ModuleRegistry::new()
.register(auth_module)
@@ -365,7 +373,8 @@ async fn main() -> anyhow::Result<()> {
.register(message_module)
.register(health_module)
.register(ai_module)
.register(points_module);
.register(points_module)
.register(dialysis_module);
tracing::info!(
module_count = registry.modules().len(),
"Modules registered"
@@ -545,6 +554,7 @@ async fn main() -> anyhow::Result<()> {
.merge(erp_health::HealthModule::protected_routes())
.merge(erp_ai::AiModule::protected_routes())
.merge(erp_points::PointsModule::protected_routes())
.merge(erp_dialysis::DialysisModule::protected_routes())
.merge(handlers::audit_log::audit_log_router())
.route(
"/upload",

View File

@@ -132,3 +132,14 @@ impl FromRef<AppState> for erp_points::PointsState {
}
}
}
/// Allow erp-dialysis handlers to extract their required state.
impl FromRef<AppState> for erp_dialysis::DialysisState {
fn from_ref(state: &AppState) -> Self {
Self {
db: state.db.clone(),
event_bus: state.event_bus.clone(),
crypto: state.pii_crypto.clone(),
}
}
}

View File

@@ -3,7 +3,7 @@
//! 验证体征 CRUD、化验报告 CRUD + 审阅、租户隔离、乐观锁。
use erp_health::dto::health_data_dto::*;
use erp_health::dto::dialysis_dto::ReviewLabReportReq;
use erp_dialysis::dto::dialysis_dto::ReviewLabReportReq;
use erp_health::service::health_data_service;
use super::test_fixture::TestApp;

View File

@@ -1,9 +1,9 @@
//! erp-health 透析方案管理集成测试
//! erp-dialysis 透析方案管理集成测试
//!
//! 验证透析方案 CRUD、列表按患者过滤、租户隔离、乐观锁。
use erp_health::dto::dialysis_prescription_dto::*;
use erp_health::service::dialysis_prescription_service;
use erp_dialysis::dto::dialysis_prescription_dto::*;
use erp_dialysis::service::dialysis_prescription_service;
use super::test_fixture::TestApp;
@@ -33,7 +33,7 @@ fn default_create_req(patient_id: uuid::Uuid) -> CreateDialysisPrescriptionReq {
async fn seed_prescription(app: &TestApp, patient_id: uuid::Uuid) -> DialysisPrescriptionResp {
dialysis_prescription_service::create_prescription(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()),
default_create_req(patient_id),
)
.await
@@ -67,7 +67,7 @@ async fn test_dialysis_prescription_get() {
let rx = seed_prescription(&app, patient_id).await;
let fetched = dialysis_prescription_service::get_prescription(
app.health_state(), app.tenant_id(), rx.id,
app.dialysis_state(), app.tenant_id(), rx.id,
)
.await
.expect("查询应成功");
@@ -84,7 +84,7 @@ async fn test_dialysis_prescription_update() {
let rx = seed_prescription(&app, patient_id).await;
let updated = dialysis_prescription_service::update_prescription(
app.health_state(), app.tenant_id(), rx.id,
app.dialysis_state(), app.tenant_id(), rx.id,
Some(app.operator_id()),
UpdateDialysisPrescriptionReq {
blood_flow_rate: Some(350),
@@ -122,14 +122,14 @@ async fn test_dialysis_prescription_list_by_patient() {
seed_prescription(&app, patient_b).await;
let list_a = dialysis_prescription_service::list_prescriptions(
app.health_state(), app.tenant_id(), 1, 20, Some(patient_a), None,
app.dialysis_state(), app.tenant_id(), 1, 20, Some(patient_a), None,
)
.await
.unwrap();
assert_eq!(list_a.total, 1);
let list_b = dialysis_prescription_service::list_prescriptions(
app.health_state(), app.tenant_id(), 1, 20, Some(patient_b), None,
app.dialysis_state(), app.tenant_id(), 1, 20, Some(patient_b), None,
)
.await
.unwrap();
@@ -146,14 +146,14 @@ async fn test_dialysis_prescription_soft_delete() {
let rx = seed_prescription(&app, patient_id).await;
dialysis_prescription_service::delete_prescription(
app.health_state(), app.tenant_id(), rx.id,
app.dialysis_state(), app.tenant_id(), rx.id,
Some(app.operator_id()), rx.version,
)
.await
.expect("删除应成功");
let result = dialysis_prescription_service::get_prescription(
app.health_state(), app.tenant_id(), rx.id,
app.dialysis_state(), app.tenant_id(), rx.id,
)
.await;
assert!(result.is_err(), "软删除后查询应失败");
@@ -170,7 +170,7 @@ async fn test_dialysis_prescription_tenant_isolation() {
let other_tenant = uuid::Uuid::new_v4();
let list = dialysis_prescription_service::list_prescriptions(
app.health_state(), other_tenant, 1, 20, None, None,
app.dialysis_state(), other_tenant, 1, 20, None, None,
)
.await
.unwrap();
@@ -188,7 +188,7 @@ async fn test_dialysis_prescription_version_conflict() {
// 先更新一次
dialysis_prescription_service::update_prescription(
app.health_state(), app.tenant_id(), rx.id,
app.dialysis_state(), app.tenant_id(), rx.id,
Some(app.operator_id()),
UpdateDialysisPrescriptionReq {
blood_flow_rate: Some(350),
@@ -208,7 +208,7 @@ async fn test_dialysis_prescription_version_conflict() {
// 用旧 version 再更新应失败
let result = dialysis_prescription_service::update_prescription(
app.health_state(), app.tenant_id(), rx.id,
app.dialysis_state(), app.tenant_id(), rx.id,
Some(app.operator_id()),
UpdateDialysisPrescriptionReq {
blood_flow_rate: Some(400),

View File

@@ -1,10 +1,10 @@
//! erp-health 透析记录集成测试
//! erp-dialysis 透析记录集成测试
//!
//! 验证透析 CRUD、状态流转、PII 加密、乐观锁、租户隔离等核心行为。
use chrono::{NaiveDate, NaiveTime};
use erp_health::dto::dialysis_dto::*;
use erp_health::service::dialysis_service;
use erp_dialysis::dto::dialysis_dto::*;
use erp_dialysis::service::dialysis_service;
use super::test_fixture::TestApp;
@@ -42,7 +42,7 @@ async fn test_dialysis_create_basic() {
let req = default_create_req(patient_id);
let record = dialysis_service::create_dialysis_record(
app.health_state(), app.tenant_id(), Some(app.operator_id()), req,
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()), req,
)
.await
.expect("创建透析记录应成功");
@@ -54,7 +54,7 @@ async fn test_dialysis_create_basic() {
// 读取
let fetched = dialysis_service::get_dialysis_record(
app.health_state(), app.tenant_id(), record.id,
app.dialysis_state(), app.tenant_id(), record.id,
)
.await
.expect("查询应成功");
@@ -74,7 +74,7 @@ async fn test_dialysis_create_pii_encrypted() {
req.complication_notes = Some("低血压发作".to_string());
let record = dialysis_service::create_dialysis_record(
app.health_state(), app.tenant_id(), Some(app.operator_id()), req,
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()), req,
)
.await
.expect("创建应成功");
@@ -93,7 +93,7 @@ async fn test_dialysis_update_status_flow() {
let patient_id = app.create_patient("状态流转患者").await;
let record = dialysis_service::create_dialysis_record(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()),
default_create_req(patient_id),
)
.await
@@ -102,7 +102,7 @@ async fn test_dialysis_update_status_flow() {
// 先将状态推进到 completeddraft → completed → reviewed
use sea_orm::{EntityTrait, ColumnTrait, QueryFilter};
use erp_health::entity::dialysis_record;
use erp_dialysis::entity::dialysis_record;
let _ = dialysis_record::Entity::update_many()
.col_expr(dialysis_record::Column::Status, sea_orm::sea_query::Expr::val("completed").into())
.col_expr(dialysis_record::Column::Version, sea_orm::sea_query::Expr::val(record.version + 1).into())
@@ -113,7 +113,7 @@ async fn test_dialysis_update_status_flow() {
// 审核: completed → reviewed
let reviewed = dialysis_service::review_dialysis_record(
app.health_state(), app.tenant_id(), record.id, app.operator_id(), record.version + 1,
app.dialysis_state(), app.tenant_id(), record.id, app.operator_id(), record.version + 1,
)
.await
.expect("审核应成功");
@@ -131,35 +131,35 @@ async fn test_dialysis_list_by_patient() {
let patient_b = app.create_patient("列表患者B").await;
dialysis_service::create_dialysis_record(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()),
default_create_req(patient_a),
)
.await
.unwrap();
dialysis_service::create_dialysis_record(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()),
default_create_req(patient_a),
)
.await
.unwrap();
dialysis_service::create_dialysis_record(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()),
default_create_req(patient_b),
)
.await
.unwrap();
let list_a = dialysis_service::list_dialysis_records(
app.health_state(), app.tenant_id(), patient_a, 1, 20,
app.dialysis_state(), app.tenant_id(), patient_a, 1, 20,
)
.await
.unwrap();
assert_eq!(list_a.total, 2);
let list_b = dialysis_service::list_dialysis_records(
app.health_state(), app.tenant_id(), patient_b, 1, 20,
app.dialysis_state(), app.tenant_id(), patient_b, 1, 20,
)
.await
.unwrap();
@@ -175,7 +175,7 @@ async fn test_dialysis_tenant_isolation() {
let patient_id = app.create_patient("隔离患者").await;
let record = dialysis_service::create_dialysis_record(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()),
default_create_req(patient_id),
)
.await
@@ -184,7 +184,7 @@ async fn test_dialysis_tenant_isolation() {
// 用不同 tenant_id 查询应失败
let other_tenant = uuid::Uuid::new_v4();
let result = dialysis_service::get_dialysis_record(
app.health_state(), other_tenant, record.id,
app.dialysis_state(), other_tenant, record.id,
)
.await;
assert!(result.is_err(), "不同租户不应看到此记录");
@@ -199,7 +199,7 @@ async fn test_dialysis_version_conflict() {
let patient_id = app.create_patient("乐观锁患者").await;
let record = dialysis_service::create_dialysis_record(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()),
default_create_req(patient_id),
)
.await
@@ -207,7 +207,7 @@ async fn test_dialysis_version_conflict() {
// 用正确版本更新
let updated = dialysis_service::update_dialysis_record(
app.health_state(), app.tenant_id(), record.id, Some(app.operator_id()),
app.dialysis_state(), app.tenant_id(), record.id, Some(app.operator_id()),
UpdateDialysisRecordReq {
dialysis_date: None, start_time: None, end_time: None,
dry_weight: None, pre_weight: None, post_weight: None,
@@ -227,7 +227,7 @@ async fn test_dialysis_version_conflict() {
// 用旧版本更新应失败
let result = dialysis_service::update_dialysis_record(
app.health_state(), app.tenant_id(), record.id, Some(app.operator_id()),
app.dialysis_state(), app.tenant_id(), record.id, Some(app.operator_id()),
UpdateDialysisRecordReq {
dialysis_date: None, start_time: None, end_time: None,
dry_weight: None, pre_weight: None, post_weight: None,
@@ -254,7 +254,7 @@ async fn test_dialysis_soft_delete() {
let patient_id = app.create_patient("软删除患者").await;
let record = dialysis_service::create_dialysis_record(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()),
default_create_req(patient_id),
)
.await
@@ -262,21 +262,21 @@ async fn test_dialysis_soft_delete() {
// 删除
dialysis_service::delete_dialysis_record(
app.health_state(), app.tenant_id(), record.id, Some(app.operator_id()), record.version,
app.dialysis_state(), app.tenant_id(), record.id, Some(app.operator_id()), record.version,
)
.await
.expect("删除应成功");
// 查询应失败
let result = dialysis_service::get_dialysis_record(
app.health_state(), app.tenant_id(), record.id,
app.dialysis_state(), app.tenant_id(), record.id,
)
.await;
assert!(result.is_err(), "软删除后应不可见");
// 列表中不应出现
let list = dialysis_service::list_dialysis_records(
app.health_state(), app.tenant_id(), patient_id, 1, 20,
app.dialysis_state(), app.tenant_id(), patient_id, 1, 20,
)
.await
.unwrap();
@@ -292,7 +292,7 @@ async fn test_dialysis_create_without_patient_returns_error() {
let fake_patient = uuid::Uuid::new_v4();
let result = dialysis_service::create_dialysis_record(
app.health_state(), app.tenant_id(), Some(app.operator_id()),
app.dialysis_state(), app.tenant_id(), Some(app.operator_id()),
default_create_req(fake_patient),
)
.await;

View File

@@ -6,6 +6,7 @@ use erp_health::dto::doctor_dto::CreateDoctorReq;
use erp_health::dto::patient_dto::CreatePatientReq;
use erp_health::service::{appointment_service, doctor_service, patient_service};
use erp_health::state::HealthState;
use erp_dialysis::state::DialysisState;
use super::test_db::TestDb;
@@ -41,6 +42,14 @@ impl TestApp {
&self.health_state
}
pub fn dialysis_state(&self) -> DialysisState {
DialysisState {
db: self.test_db.db().clone(),
event_bus: self.health_state.event_bus.clone(),
crypto: self.health_state.crypto.clone(),
}
}
pub fn tenant_id(&self) -> uuid::Uuid {
self.tenant_id
}