refactor(dialysis): 透析模块拆分为独立 erp-dialysis crate
- 创建 erp-dialysis crate(DialysisState + DialysisError + DialysisModule) - 迁移 2 Entity + 2 Service + 2 Handler + 2 DTO 共 8 个文件 - Entity 移除跨 crate patient Relation(FK 列保留) - Service 内联 validation 逻辑,移除 patient 存在性检查(FK 约束保证) - erp-health 的 stats/consultation 中 dialysis 查询改为 raw SQL - ReviewLabReportReq 从 dialysis_dto 移至 health_data_dto(正确归属) - workspace 全量编译通过
This commit is contained in:
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -1465,6 +1465,26 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "erp-dialysis"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
"chrono",
|
||||
"erp-core",
|
||||
"num-traits",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"utoipa",
|
||||
"uuid",
|
||||
"validator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "erp-health"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -18,6 +18,7 @@ members = [
|
||||
"crates/erp-health",
|
||||
"crates/erp-ai",
|
||||
"crates/erp-plugin-assessment",
|
||||
"crates/erp-dialysis",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
||||
20
crates/erp-dialysis/Cargo.toml
Normal file
20
crates/erp-dialysis/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "erp-dialysis"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
erp-core.workspace = true
|
||||
sea-orm.workspace = true
|
||||
tokio.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
uuid.workspace = true
|
||||
chrono.workspace = true
|
||||
axum.workspace = true
|
||||
utoipa.workspace = true
|
||||
validator.workspace = true
|
||||
async-trait.workspace = true
|
||||
tracing.workspace = true
|
||||
thiserror.workspace = true
|
||||
num-traits = "0.2"
|
||||
7
crates/erp-dialysis/src/dto/mod.rs
Normal file
7
crates/erp-dialysis/src/dto/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod dialysis_dto;
|
||||
pub mod dialysis_prescription_dto;
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct DeleteWithVersion {
|
||||
pub version: i32,
|
||||
}
|
||||
@@ -59,19 +59,6 @@ pub struct Model {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::patient::Entity",
|
||||
from = "Column::PatientId",
|
||||
to = "super::patient::Column::Id"
|
||||
)]
|
||||
Patient,
|
||||
}
|
||||
|
||||
impl Related<super::patient::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Patient.def()
|
||||
}
|
||||
}
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -63,19 +63,6 @@ pub struct Model {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::patient::Entity",
|
||||
from = "Column::PatientId",
|
||||
to = "super::patient::Column::Id"
|
||||
)]
|
||||
Patient,
|
||||
}
|
||||
|
||||
impl Related<super::patient::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Patient.def()
|
||||
}
|
||||
}
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
2
crates/erp-dialysis/src/entity/mod.rs
Normal file
2
crates/erp-dialysis/src/entity/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod dialysis_prescription;
|
||||
pub mod dialysis_record;
|
||||
60
crates/erp-dialysis/src/error.rs
Normal file
60
crates/erp-dialysis/src/error.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use erp_core::error::AppError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DialysisError {
|
||||
#[error("{0}")]
|
||||
Validation(String),
|
||||
|
||||
#[error("患者不存在")]
|
||||
PatientNotFound,
|
||||
|
||||
#[error("透析记录不存在")]
|
||||
DialysisRecordNotFound,
|
||||
|
||||
#[error("透析方案不存在")]
|
||||
DialysisPrescriptionNotFound,
|
||||
|
||||
#[error("状态转换无效: {0}")]
|
||||
InvalidStatusTransition(String),
|
||||
|
||||
#[error("版本冲突: 数据已被其他操作修改,请刷新后重试")]
|
||||
VersionMismatch,
|
||||
|
||||
#[error("数据库操作失败: {0}")]
|
||||
DbError(String),
|
||||
}
|
||||
|
||||
impl From<DialysisError> for AppError {
|
||||
fn from(err: DialysisError) -> Self {
|
||||
match err {
|
||||
DialysisError::Validation(s) => AppError::Validation(s),
|
||||
DialysisError::PatientNotFound
|
||||
| DialysisError::DialysisRecordNotFound
|
||||
| DialysisError::DialysisPrescriptionNotFound => AppError::NotFound(err.to_string()),
|
||||
DialysisError::InvalidStatusTransition(s) => AppError::Validation(s),
|
||||
DialysisError::VersionMismatch => AppError::VersionMismatch,
|
||||
DialysisError::DbError(_) => AppError::Internal(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sea_orm::DbErr> for DialysisError {
|
||||
fn from(err: sea_orm::DbErr) -> Self {
|
||||
DialysisError::DbError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AppError> for DialysisError {
|
||||
fn from(err: AppError) -> Self {
|
||||
DialysisError::Validation(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for DialysisError {
|
||||
fn from(err: String) -> Self {
|
||||
DialysisError::Validation(err)
|
||||
}
|
||||
}
|
||||
|
||||
pub type DialysisResult<T> = Result<T, DialysisError>;
|
||||
6
crates/erp-dialysis/src/event.rs
Normal file
6
crates/erp-dialysis/src/event.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use erp_core::events::EventBus;
|
||||
|
||||
/// 预留事件处理器注册
|
||||
pub fn register_handlers_with_state(_state: crate::state::DialysisState) {
|
||||
// 透析模块事件消费者待后续迭代
|
||||
}
|
||||
@@ -11,7 +11,7 @@ use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||
use crate::dto::dialysis_dto::*;
|
||||
use crate::dto::DeleteWithVersion;
|
||||
use crate::service::dialysis_service;
|
||||
use crate::state::HealthState;
|
||||
use crate::state::DialysisState;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct PaginationParams {
|
||||
@@ -32,13 +32,13 @@ pub struct ReviewDialysisWithVersion {
|
||||
}
|
||||
|
||||
pub async fn list_dialysis_records<S>(
|
||||
State(state): State<HealthState>,
|
||||
State(state): State<DialysisState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(patient_id): Path<Uuid>,
|
||||
Query(params): Query<PaginationParams>,
|
||||
) -> Result<Json<ApiResponse<PaginatedResponse<DialysisRecordResp>>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
DialysisState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.list")?;
|
||||
@@ -52,12 +52,12 @@ where
|
||||
}
|
||||
|
||||
pub async fn get_dialysis_record<S>(
|
||||
State(state): State<HealthState>,
|
||||
State(state): State<DialysisState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(record_id): Path<Uuid>,
|
||||
) -> Result<Json<ApiResponse<DialysisRecordResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
DialysisState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.list")?;
|
||||
@@ -69,12 +69,12 @@ where
|
||||
}
|
||||
|
||||
pub async fn create_dialysis_record<S>(
|
||||
State(state): State<HealthState>,
|
||||
State(state): State<DialysisState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Json(req): Json<CreateDialysisRecordReq>,
|
||||
) -> Result<Json<ApiResponse<DialysisRecordResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
DialysisState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
@@ -88,13 +88,13 @@ where
|
||||
}
|
||||
|
||||
pub async fn update_dialysis_record<S>(
|
||||
State(state): State<HealthState>,
|
||||
State(state): State<DialysisState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(record_id): Path<Uuid>,
|
||||
Json(req): Json<UpdateDialysisWithVersion>,
|
||||
) -> Result<Json<ApiResponse<DialysisRecordResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
DialysisState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
@@ -108,13 +108,13 @@ where
|
||||
}
|
||||
|
||||
pub async fn review_dialysis_record<S>(
|
||||
State(state): State<HealthState>,
|
||||
State(state): State<DialysisState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(record_id): Path<Uuid>,
|
||||
Json(req): Json<ReviewDialysisWithVersion>,
|
||||
) -> Result<Json<ApiResponse<DialysisRecordResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
DialysisState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
@@ -126,13 +126,13 @@ where
|
||||
}
|
||||
|
||||
pub async fn delete_dialysis_record<S>(
|
||||
State(state): State<HealthState>,
|
||||
State(state): State<DialysisState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(record_id): Path<Uuid>,
|
||||
Json(req): Json<DeleteWithVersion>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
DialysisState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
@@ -11,7 +11,7 @@ use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||
use crate::dto::dialysis_prescription_dto::*;
|
||||
use crate::dto::DeleteWithVersion;
|
||||
use crate::service::dialysis_prescription_service;
|
||||
use crate::state::HealthState;
|
||||
use crate::state::DialysisState;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct DialysisPrescriptionListParams {
|
||||
@@ -29,12 +29,12 @@ pub struct UpdateDialysisPrescriptionWithVersion {
|
||||
}
|
||||
|
||||
pub async fn list_prescriptions<S>(
|
||||
State(state): State<HealthState>,
|
||||
State(state): State<DialysisState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Query(params): Query<DialysisPrescriptionListParams>,
|
||||
) -> Result<Json<ApiResponse<PaginatedResponse<DialysisPrescriptionResp>>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
DialysisState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.dialysis-prescription.list")?;
|
||||
@@ -48,12 +48,12 @@ where
|
||||
}
|
||||
|
||||
pub async fn get_prescription<S>(
|
||||
State(state): State<HealthState>,
|
||||
State(state): State<DialysisState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<ApiResponse<DialysisPrescriptionResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
DialysisState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.dialysis-prescription.list")?;
|
||||
@@ -62,12 +62,12 @@ where
|
||||
}
|
||||
|
||||
pub async fn create_prescription<S>(
|
||||
State(state): State<HealthState>,
|
||||
State(state): State<DialysisState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Json(req): Json<CreateDialysisPrescriptionReq>,
|
||||
) -> Result<Json<ApiResponse<DialysisPrescriptionResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
DialysisState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.dialysis-prescription.manage")?;
|
||||
@@ -81,13 +81,13 @@ where
|
||||
}
|
||||
|
||||
pub async fn update_prescription<S>(
|
||||
State(state): State<HealthState>,
|
||||
State(state): State<DialysisState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(req): Json<UpdateDialysisPrescriptionWithVersion>,
|
||||
) -> Result<Json<ApiResponse<DialysisPrescriptionResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
DialysisState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.dialysis-prescription.manage")?;
|
||||
@@ -101,13 +101,13 @@ where
|
||||
}
|
||||
|
||||
pub async fn delete_prescription<S>(
|
||||
State(state): State<HealthState>,
|
||||
State(state): State<DialysisState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(req): Json<DeleteWithVersion>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
DialysisState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.dialysis-prescription.manage")?;
|
||||
2
crates/erp-dialysis/src/handler/mod.rs
Normal file
2
crates/erp-dialysis/src/handler/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod dialysis_handler;
|
||||
pub mod dialysis_prescription_handler;
|
||||
11
crates/erp-dialysis/src/lib.rs
Normal file
11
crates/erp-dialysis/src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
pub mod dto;
|
||||
pub mod entity;
|
||||
pub mod error;
|
||||
pub mod event;
|
||||
pub mod handler;
|
||||
pub mod module;
|
||||
pub mod service;
|
||||
pub mod state;
|
||||
|
||||
pub use module::DialysisModule;
|
||||
pub use state::DialysisState;
|
||||
114
crates/erp-dialysis/src/module.rs
Normal file
114
crates/erp-dialysis/src/module.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use async_trait::async_trait;
|
||||
use axum::Router;
|
||||
use erp_core::error::AppResult;
|
||||
use erp_core::module::{ErpModule, ModuleContext, ModuleType, PermissionDescriptor};
|
||||
|
||||
use crate::handler::{dialysis_handler, dialysis_prescription_handler};
|
||||
use crate::state::DialysisState;
|
||||
|
||||
pub struct DialysisModule;
|
||||
|
||||
impl DialysisModule {
|
||||
pub fn public_routes<S>() -> Router<S>
|
||||
where
|
||||
DialysisState: axum::extract::FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
Router::new()
|
||||
}
|
||||
|
||||
pub fn protected_routes<S>() -> Router<S>
|
||||
where
|
||||
DialysisState: axum::extract::FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
Router::new()
|
||||
// 透析记录
|
||||
.route(
|
||||
"/health/patients/{id}/dialysis-records",
|
||||
axum::routing::get(dialysis_handler::list_dialysis_records),
|
||||
)
|
||||
.route(
|
||||
"/health/dialysis-records",
|
||||
axum::routing::post(dialysis_handler::create_dialysis_record),
|
||||
)
|
||||
.route(
|
||||
"/health/dialysis-records/{id}",
|
||||
axum::routing::get(dialysis_handler::get_dialysis_record)
|
||||
.put(dialysis_handler::update_dialysis_record)
|
||||
.delete(dialysis_handler::delete_dialysis_record),
|
||||
)
|
||||
.route(
|
||||
"/health/dialysis-records/{id}/review",
|
||||
axum::routing::put(dialysis_handler::review_dialysis_record),
|
||||
)
|
||||
// 透析方案
|
||||
.route(
|
||||
"/health/dialysis-prescriptions",
|
||||
axum::routing::get(dialysis_prescription_handler::list_prescriptions)
|
||||
.post(dialysis_prescription_handler::create_prescription),
|
||||
)
|
||||
.route(
|
||||
"/health/dialysis-prescriptions/{id}",
|
||||
axum::routing::get(dialysis_prescription_handler::get_prescription)
|
||||
.put(dialysis_prescription_handler::update_prescription)
|
||||
.delete(dialysis_prescription_handler::delete_prescription),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ErpModule for DialysisModule {
|
||||
fn name(&self) -> &str {
|
||||
"透析管理"
|
||||
}
|
||||
|
||||
fn id(&self) -> &str {
|
||||
"erp-dialysis"
|
||||
}
|
||||
|
||||
fn version(&self) -> &str {
|
||||
"0.1.0"
|
||||
}
|
||||
|
||||
fn module_type(&self) -> ModuleType {
|
||||
ModuleType::Builtin
|
||||
}
|
||||
|
||||
fn permissions(&self) -> Vec<PermissionDescriptor> {
|
||||
vec![
|
||||
PermissionDescriptor {
|
||||
code: "health.health-data.list".into(),
|
||||
name: "查看透析记录".into(),
|
||||
description: "查看透析记录列表和详情".into(),
|
||||
module: "erp-dialysis".into(),
|
||||
},
|
||||
PermissionDescriptor {
|
||||
code: "health.health-data.manage".into(),
|
||||
name: "管理透析记录".into(),
|
||||
description: "创建、编辑、审阅、删除透析记录".into(),
|
||||
module: "erp-dialysis".into(),
|
||||
},
|
||||
PermissionDescriptor {
|
||||
code: "health.dialysis-prescription.list".into(),
|
||||
name: "查看透析处方".into(),
|
||||
description: "查看透析处方列表和详情".into(),
|
||||
module: "erp-dialysis".into(),
|
||||
},
|
||||
PermissionDescriptor {
|
||||
code: "health.dialysis-prescription.manage".into(),
|
||||
name: "管理透析处方".into(),
|
||||
description: "创建、编辑、删除透析处方".into(),
|
||||
module: "erp-dialysis".into(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
async fn on_startup(&self, _ctx: &ModuleContext) -> AppResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//! 透析方案 Service — 透析处方 CRUD
|
||||
|
||||
use chrono::Utc;
|
||||
use num_traits::ToPrimitive;
|
||||
use sea_orm::entity::prelude::*;
|
||||
@@ -10,18 +12,18 @@ use erp_core::error::check_version;
|
||||
use erp_core::types::PaginatedResponse;
|
||||
|
||||
use crate::dto::dialysis_prescription_dto::*;
|
||||
use crate::entity::{dialysis_prescription, patient};
|
||||
use crate::error::{HealthError, HealthResult};
|
||||
use crate::state::HealthState;
|
||||
use crate::entity::dialysis_prescription;
|
||||
use crate::error::{DialysisError, DialysisResult};
|
||||
use crate::state::DialysisState;
|
||||
|
||||
pub async fn list_prescriptions(
|
||||
state: &HealthState,
|
||||
state: &DialysisState,
|
||||
tenant_id: Uuid,
|
||||
page: u64,
|
||||
page_size: u64,
|
||||
patient_id: Option<Uuid>,
|
||||
status: Option<String>,
|
||||
) -> HealthResult<PaginatedResponse<DialysisPrescriptionResp>> {
|
||||
) -> DialysisResult<PaginatedResponse<DialysisPrescriptionResp>> {
|
||||
let limit = page_size.min(100);
|
||||
let offset = page.saturating_sub(1) * limit;
|
||||
|
||||
@@ -51,35 +53,28 @@ pub async fn list_prescriptions(
|
||||
}
|
||||
|
||||
pub async fn get_prescription(
|
||||
state: &HealthState,
|
||||
state: &DialysisState,
|
||||
tenant_id: Uuid,
|
||||
id: Uuid,
|
||||
) -> HealthResult<DialysisPrescriptionResp> {
|
||||
) -> DialysisResult<DialysisPrescriptionResp> {
|
||||
let m = dialysis_prescription::Entity::find()
|
||||
.filter(dialysis_prescription::Column::Id.eq(id))
|
||||
.filter(dialysis_prescription::Column::TenantId.eq(tenant_id))
|
||||
.filter(dialysis_prescription::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::DialysisPrescriptionNotFound)?;
|
||||
.ok_or(DialysisError::DialysisPrescriptionNotFound)?;
|
||||
|
||||
Ok(model_to_resp(m))
|
||||
}
|
||||
|
||||
pub async fn create_prescription(
|
||||
state: &HealthState,
|
||||
state: &DialysisState,
|
||||
tenant_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: CreateDialysisPrescriptionReq,
|
||||
) -> HealthResult<DialysisPrescriptionResp> {
|
||||
// 校验患者存在
|
||||
patient::Entity::find()
|
||||
.filter(patient::Column::Id.eq(req.patient_id))
|
||||
.filter(patient::Column::TenantId.eq(tenant_id))
|
||||
.filter(patient::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PatientNotFound)?;
|
||||
) -> DialysisResult<DialysisPrescriptionResp> {
|
||||
// 患者存在性由数据库 FK 约束保证,不再显式查询 patient 表
|
||||
|
||||
validate_anticoagulation_type(req.anticoagulation_type.as_deref())?;
|
||||
validate_vascular_access_type(req.vascular_access_type.as_deref())?;
|
||||
@@ -128,23 +123,23 @@ pub async fn create_prescription(
|
||||
}
|
||||
|
||||
pub async fn update_prescription(
|
||||
state: &HealthState,
|
||||
state: &DialysisState,
|
||||
tenant_id: Uuid,
|
||||
id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: UpdateDialysisPrescriptionReq,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<DialysisPrescriptionResp> {
|
||||
) -> DialysisResult<DialysisPrescriptionResp> {
|
||||
let model = dialysis_prescription::Entity::find()
|
||||
.filter(dialysis_prescription::Column::Id.eq(id))
|
||||
.filter(dialysis_prescription::Column::TenantId.eq(tenant_id))
|
||||
.filter(dialysis_prescription::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::DialysisPrescriptionNotFound)?;
|
||||
.ok_or(DialysisError::DialysisPrescriptionNotFound)?;
|
||||
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
.map_err(|_| DialysisError::VersionMismatch)?;
|
||||
|
||||
if let Some(ref t) = req.anticoagulation_type { validate_anticoagulation_type(Some(t))?; }
|
||||
if let Some(ref t) = req.vascular_access_type { validate_vascular_access_type(Some(t))?; }
|
||||
@@ -185,22 +180,22 @@ pub async fn update_prescription(
|
||||
}
|
||||
|
||||
pub async fn delete_prescription(
|
||||
state: &HealthState,
|
||||
state: &DialysisState,
|
||||
tenant_id: Uuid,
|
||||
id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<()> {
|
||||
) -> DialysisResult<()> {
|
||||
let model = dialysis_prescription::Entity::find()
|
||||
.filter(dialysis_prescription::Column::Id.eq(id))
|
||||
.filter(dialysis_prescription::Column::TenantId.eq(tenant_id))
|
||||
.filter(dialysis_prescription::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::DialysisPrescriptionNotFound)?;
|
||||
.ok_or(DialysisError::DialysisPrescriptionNotFound)?;
|
||||
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
.map_err(|_| DialysisError::VersionMismatch)?;
|
||||
|
||||
let mut active: dialysis_prescription::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(Utc::now()));
|
||||
@@ -218,6 +213,10 @@ pub async fn delete_prescription(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 私有辅助函数
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn model_to_resp(m: dialysis_prescription::Model) -> DialysisPrescriptionResp {
|
||||
DialysisPrescriptionResp {
|
||||
id: m.id,
|
||||
@@ -248,11 +247,11 @@ fn model_to_resp(m: dialysis_prescription::Model) -> DialysisPrescriptionResp {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_anticoagulation_type(val: Option<&str>) -> HealthResult<()> {
|
||||
fn validate_anticoagulation_type(val: Option<&str>) -> DialysisResult<()> {
|
||||
if let Some(t) = val {
|
||||
let valid = ["heparin", "lmwh", "heparin_free"];
|
||||
if !valid.contains(&t) {
|
||||
return Err(HealthError::Validation(format!(
|
||||
return Err(DialysisError::Validation(format!(
|
||||
"anticoagulation_type 必须为: {}", valid.join(", ")
|
||||
)));
|
||||
}
|
||||
@@ -260,11 +259,11 @@ fn validate_anticoagulation_type(val: Option<&str>) -> HealthResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_vascular_access_type(val: Option<&str>) -> HealthResult<()> {
|
||||
fn validate_vascular_access_type(val: Option<&str>) -> DialysisResult<()> {
|
||||
if let Some(t) = val {
|
||||
let valid = ["avf", "avg", "cvc"];
|
||||
if !valid.contains(&t) {
|
||||
return Err(HealthError::Validation(format!(
|
||||
return Err(DialysisError::Validation(format!(
|
||||
"vascular_access_type 必须为: {}", valid.join(", ")
|
||||
)));
|
||||
}
|
||||
@@ -13,18 +13,17 @@ use erp_core::error::check_version;
|
||||
use erp_core::types::PaginatedResponse;
|
||||
|
||||
use crate::dto::dialysis_dto::*;
|
||||
use crate::entity::{dialysis_record, patient};
|
||||
use crate::error::{HealthError, HealthResult};
|
||||
use crate::service::validation;
|
||||
use crate::state::HealthState;
|
||||
use crate::entity::dialysis_record;
|
||||
use crate::error::{DialysisError, DialysisResult};
|
||||
use crate::state::DialysisState;
|
||||
|
||||
pub async fn list_dialysis_records(
|
||||
state: &HealthState,
|
||||
state: &DialysisState,
|
||||
tenant_id: Uuid,
|
||||
patient_id: Uuid,
|
||||
page: u64,
|
||||
page_size: u64,
|
||||
) -> HealthResult<PaginatedResponse<DialysisRecordResp>> {
|
||||
) -> DialysisResult<PaginatedResponse<DialysisRecordResp>> {
|
||||
let limit = page_size.min(100);
|
||||
let offset = page.saturating_sub(1) * limit;
|
||||
|
||||
@@ -49,34 +48,28 @@ pub async fn list_dialysis_records(
|
||||
}
|
||||
|
||||
pub async fn get_dialysis_record(
|
||||
state: &HealthState,
|
||||
state: &DialysisState,
|
||||
tenant_id: Uuid,
|
||||
record_id: Uuid,
|
||||
) -> HealthResult<DialysisRecordResp> {
|
||||
) -> DialysisResult<DialysisRecordResp> {
|
||||
let m = dialysis_record::Entity::find()
|
||||
.filter(dialysis_record::Column::Id.eq(record_id))
|
||||
.filter(dialysis_record::Column::TenantId.eq(tenant_id))
|
||||
.filter(dialysis_record::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::DialysisRecordNotFound)?;
|
||||
.ok_or(DialysisError::DialysisRecordNotFound)?;
|
||||
|
||||
Ok(to_resp(&state.crypto, m))
|
||||
}
|
||||
|
||||
pub async fn create_dialysis_record(
|
||||
state: &HealthState,
|
||||
state: &DialysisState,
|
||||
tenant_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: CreateDialysisRecordReq,
|
||||
) -> HealthResult<DialysisRecordResp> {
|
||||
patient::Entity::find()
|
||||
.filter(patient::Column::Id.eq(req.patient_id))
|
||||
.filter(patient::Column::TenantId.eq(tenant_id))
|
||||
.filter(patient::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PatientNotFound)?;
|
||||
) -> DialysisResult<DialysisRecordResp> {
|
||||
// 患者存在性由数据库 FK 约束保证,不再显式查询 patient 表
|
||||
|
||||
validate_dialysis_type(&req.dialysis_type)?;
|
||||
|
||||
@@ -84,9 +77,9 @@ pub async fn create_dialysis_record(
|
||||
|
||||
// PII 加密
|
||||
let encrypted_symptoms = req.symptoms.as_ref()
|
||||
.map(|v| -> HealthResult<serde_json::Value> {
|
||||
.map(|v| -> DialysisResult<serde_json::Value> {
|
||||
let json_str = serde_json::to_string(v)
|
||||
.map_err(|e| HealthError::Validation(e.to_string()))?;
|
||||
.map_err(|e| DialysisError::Validation(e.to_string()))?;
|
||||
Ok(serde_json::Value::String(pii::encrypt(kek, &json_str)?))
|
||||
})
|
||||
.transpose()?;
|
||||
@@ -141,23 +134,23 @@ pub async fn create_dialysis_record(
|
||||
}
|
||||
|
||||
pub async fn update_dialysis_record(
|
||||
state: &HealthState,
|
||||
state: &DialysisState,
|
||||
tenant_id: Uuid,
|
||||
record_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: UpdateDialysisRecordReq,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<DialysisRecordResp> {
|
||||
) -> DialysisResult<DialysisRecordResp> {
|
||||
let model = dialysis_record::Entity::find()
|
||||
.filter(dialysis_record::Column::Id.eq(record_id))
|
||||
.filter(dialysis_record::Column::TenantId.eq(tenant_id))
|
||||
.filter(dialysis_record::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::DialysisRecordNotFound)?;
|
||||
.ok_or(DialysisError::DialysisRecordNotFound)?;
|
||||
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
.map_err(|_| DialysisError::VersionMismatch)?;
|
||||
|
||||
let mut active: dialysis_record::ActiveModel = model.into();
|
||||
if let Some(v) = req.dialysis_date { active.dialysis_date = Set(v); }
|
||||
@@ -205,24 +198,24 @@ pub async fn update_dialysis_record(
|
||||
}
|
||||
|
||||
pub async fn review_dialysis_record(
|
||||
state: &HealthState,
|
||||
state: &DialysisState,
|
||||
tenant_id: Uuid,
|
||||
record_id: Uuid,
|
||||
reviewer_id: Uuid,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<DialysisRecordResp> {
|
||||
) -> DialysisResult<DialysisRecordResp> {
|
||||
let model = dialysis_record::Entity::find()
|
||||
.filter(dialysis_record::Column::Id.eq(record_id))
|
||||
.filter(dialysis_record::Column::TenantId.eq(tenant_id))
|
||||
.filter(dialysis_record::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::DialysisRecordNotFound)?;
|
||||
.ok_or(DialysisError::DialysisRecordNotFound)?;
|
||||
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
.map_err(|_| DialysisError::VersionMismatch)?;
|
||||
|
||||
validation::validate_dialysis_status_transition(&model.status, "reviewed")?;
|
||||
validate_dialysis_status_transition(&model.status, "reviewed")?;
|
||||
|
||||
let mut active: dialysis_record::ActiveModel = model.into();
|
||||
active.status = Set("reviewed".to_string());
|
||||
@@ -244,22 +237,22 @@ pub async fn review_dialysis_record(
|
||||
}
|
||||
|
||||
pub async fn delete_dialysis_record(
|
||||
state: &HealthState,
|
||||
state: &DialysisState,
|
||||
tenant_id: Uuid,
|
||||
record_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<()> {
|
||||
) -> DialysisResult<()> {
|
||||
let model = dialysis_record::Entity::find()
|
||||
.filter(dialysis_record::Column::Id.eq(record_id))
|
||||
.filter(dialysis_record::Column::TenantId.eq(tenant_id))
|
||||
.filter(dialysis_record::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::DialysisRecordNotFound)?;
|
||||
.ok_or(DialysisError::DialysisRecordNotFound)?;
|
||||
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
.map_err(|_| DialysisError::VersionMismatch)?;
|
||||
|
||||
let mut active: dialysis_record::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(Utc::now()));
|
||||
@@ -277,15 +270,40 @@ pub async fn delete_dialysis_record(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_dialysis_type(dialysis_type: &str) -> HealthResult<()> {
|
||||
// ---------------------------------------------------------------------------
|
||||
// 私有辅助函数
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 校验透析类型枚举
|
||||
fn validate_dialysis_type(dialysis_type: &str) -> DialysisResult<()> {
|
||||
match dialysis_type {
|
||||
"HD" | "HDF" | "HF" => Ok(()),
|
||||
_ => Err(HealthError::Validation(format!(
|
||||
_ => Err(DialysisError::Validation(format!(
|
||||
"无效的透析类型: {},允许值: HD, HDF, HF", dialysis_type
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// 校验透析记录状态转换
|
||||
/// draft -> completed -> reviewed
|
||||
fn validate_dialysis_status_transition(current: &str, new: &str) -> DialysisResult<()> {
|
||||
if current == new {
|
||||
return Ok(());
|
||||
}
|
||||
let allowed = match current {
|
||||
"draft" => matches!(new, "completed"),
|
||||
"completed" => matches!(new, "reviewed"),
|
||||
_ => false,
|
||||
};
|
||||
if allowed {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(DialysisError::InvalidStatusTransition(format!(
|
||||
"dialysis_record.status: 不允许从 '{}' 转换到 '{}'", current, new
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn to_resp(crypto: &erp_core::crypto::PiiCrypto, m: dialysis_record::Model) -> DialysisRecordResp {
|
||||
let kek = crypto.kek();
|
||||
|
||||
@@ -330,3 +348,32 @@ fn to_resp(crypto: &erp_core::crypto::PiiCrypto, m: dialysis_record::Model) -> D
|
||||
version: m.version,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// --- validate_dialysis_type ---
|
||||
#[test]
|
||||
fn dialysis_type_hd() { assert!(validate_dialysis_type("HD").is_ok()); }
|
||||
#[test]
|
||||
fn dialysis_type_hdf() { assert!(validate_dialysis_type("HDF").is_ok()); }
|
||||
#[test]
|
||||
fn dialysis_type_hf() { assert!(validate_dialysis_type("HF").is_ok()); }
|
||||
#[test]
|
||||
fn dialysis_type_invalid() { assert!(validate_dialysis_type("PD").is_err()); }
|
||||
|
||||
// --- validate_dialysis_status_transition ---
|
||||
#[test]
|
||||
fn dial_draft_to_completed() { assert!(validate_dialysis_status_transition("draft", "completed").is_ok()); }
|
||||
#[test]
|
||||
fn dial_draft_to_reviewed_fails() { assert!(validate_dialysis_status_transition("draft", "reviewed").is_err()); }
|
||||
#[test]
|
||||
fn dial_completed_to_reviewed() { assert!(validate_dialysis_status_transition("completed", "reviewed").is_ok()); }
|
||||
#[test]
|
||||
fn dial_completed_to_draft_fails() { assert!(validate_dialysis_status_transition("completed", "draft").is_err()); }
|
||||
#[test]
|
||||
fn dial_reviewed_to_any_fails() { assert!(validate_dialysis_status_transition("reviewed", "draft").is_err()); }
|
||||
#[test]
|
||||
fn dial_same_status_ok() { assert!(validate_dialysis_status_transition("draft", "draft").is_ok()); }
|
||||
}
|
||||
2
crates/erp-dialysis/src/service/mod.rs
Normal file
2
crates/erp-dialysis/src/service/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod dialysis_service;
|
||||
pub mod dialysis_prescription_service;
|
||||
10
crates/erp-dialysis/src/state.rs
Normal file
10
crates/erp-dialysis/src/state.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use erp_core::crypto::PiiCrypto;
|
||||
use erp_core::events::EventBus;
|
||||
use sea_orm::DatabaseConnection;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DialysisState {
|
||||
pub db: DatabaseConnection,
|
||||
pub event_bus: EventBus,
|
||||
pub crypto: PiiCrypto,
|
||||
}
|
||||
@@ -263,3 +263,15 @@ pub struct MiniTodayResp {
|
||||
pub blood_sugar: Option<IndicatorSummary>,
|
||||
pub weight: Option<IndicatorSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize, ToSchema)]
|
||||
pub struct ReviewLabReportReq {
|
||||
pub doctor_notes: Option<String>,
|
||||
pub items: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl ReviewLabReportReq {
|
||||
pub fn sanitize(&mut self) {
|
||||
self.doctor_notes = sanitize_option(self.doctor_notes.take());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ pub mod consultation_dto;
|
||||
pub mod daily_monitoring_dto;
|
||||
pub mod diagnosis_dto;
|
||||
pub mod medication_record_dto;
|
||||
pub mod dialysis_dto;
|
||||
pub mod dialysis_prescription_dto;
|
||||
pub mod doctor_dto;
|
||||
pub mod follow_up_dto;
|
||||
pub mod follow_up_template_dto;
|
||||
|
||||
@@ -16,8 +16,6 @@ pub mod critical_alert_response;
|
||||
pub mod daily_monitoring;
|
||||
pub mod device_readings;
|
||||
pub mod diagnosis;
|
||||
pub mod dialysis_prescription;
|
||||
pub mod dialysis_record;
|
||||
pub mod doctor_profile;
|
||||
pub mod doctor_schedule;
|
||||
pub mod follow_up_record;
|
||||
|
||||
@@ -9,7 +9,6 @@ use erp_core::rbac::require_permission;
|
||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||
|
||||
use crate::dto::health_data_dto::*;
|
||||
use crate::dto::dialysis_dto::ReviewLabReportReq;
|
||||
use crate::dto::DeleteWithVersion;
|
||||
use crate::service::health_data_service;
|
||||
use crate::service::trend_service;
|
||||
|
||||
@@ -12,8 +12,6 @@ pub mod daily_monitoring_handler;
|
||||
pub mod device_reading_handler;
|
||||
pub mod diagnosis_handler;
|
||||
pub mod medication_record_handler;
|
||||
pub mod dialysis_handler;
|
||||
pub mod dialysis_prescription_handler;
|
||||
pub mod doctor_handler;
|
||||
pub mod follow_up_handler;
|
||||
pub mod follow_up_template_handler;
|
||||
|
||||
@@ -7,7 +7,7 @@ use erp_core::module::{ErpModule, PermissionDescriptor};
|
||||
|
||||
use crate::handler::{
|
||||
alert_handler, alert_rule_handler,
|
||||
appointment_handler, article_category_handler, article_handler, article_tag_handler, consultation_handler, consent_handler, critical_alert_handler, critical_value_threshold_handler, daily_monitoring_handler, device_reading_handler, diagnosis_handler, dialysis_handler, dialysis_prescription_handler, doctor_handler, follow_up_handler, follow_up_template_handler,
|
||||
appointment_handler, article_category_handler, article_handler, article_tag_handler, consultation_handler, consent_handler, critical_alert_handler, critical_value_threshold_handler, daily_monitoring_handler, device_reading_handler, diagnosis_handler, doctor_handler, follow_up_handler, follow_up_template_handler,
|
||||
health_data_handler, medication_record_handler, patient_handler, points_handler, stats_handler,
|
||||
};
|
||||
|
||||
@@ -225,37 +225,6 @@ impl HealthModule {
|
||||
"/health/vital-signs/today",
|
||||
axum::routing::get(health_data_handler::get_mini_today),
|
||||
)
|
||||
// 透析记录(血透专科)
|
||||
.route(
|
||||
"/health/patients/{id}/dialysis-records",
|
||||
axum::routing::get(dialysis_handler::list_dialysis_records),
|
||||
)
|
||||
.route(
|
||||
"/health/dialysis-records",
|
||||
axum::routing::post(dialysis_handler::create_dialysis_record),
|
||||
)
|
||||
.route(
|
||||
"/health/dialysis-records/{id}",
|
||||
axum::routing::get(dialysis_handler::get_dialysis_record)
|
||||
.put(dialysis_handler::update_dialysis_record)
|
||||
.delete(dialysis_handler::delete_dialysis_record),
|
||||
)
|
||||
.route(
|
||||
"/health/dialysis-records/{id}/review",
|
||||
axum::routing::put(dialysis_handler::review_dialysis_record),
|
||||
)
|
||||
// 透析方案
|
||||
.route(
|
||||
"/health/dialysis-prescriptions",
|
||||
axum::routing::get(dialysis_prescription_handler::list_prescriptions)
|
||||
.post(dialysis_prescription_handler::create_prescription),
|
||||
)
|
||||
.route(
|
||||
"/health/dialysis-prescriptions/{id}",
|
||||
axum::routing::get(dialysis_prescription_handler::get_prescription)
|
||||
.put(dialysis_prescription_handler::update_prescription)
|
||||
.delete(dialysis_prescription_handler::delete_prescription),
|
||||
)
|
||||
// 随访模板
|
||||
.route(
|
||||
"/health/follow-up-templates",
|
||||
|
||||
@@ -581,16 +581,18 @@ pub async fn enrich_doctor_dashboard_health(
|
||||
doctor_user_id: Uuid,
|
||||
dashboard: &mut DoctorDashboard,
|
||||
) -> HealthResult<()> {
|
||||
use crate::entity::{dialysis_record, lab_report, appointment};
|
||||
use crate::entity::{lab_report, appointment};
|
||||
use sea_orm::{FromQueryResult, Statement, DatabaseBackend};
|
||||
|
||||
// 待审核透析记录(doctor_id 通过患者关联,这里取全租户待审核)
|
||||
let pending_dialysis = dialysis_record::Entity::find()
|
||||
.filter(dialysis_record::Column::TenantId.eq(tenant_id))
|
||||
.filter(dialysis_record::Column::DeletedAt.is_null())
|
||||
.filter(dialysis_record::Column::Status.eq("draft"))
|
||||
.count(&state.db)
|
||||
.await?;
|
||||
dashboard.pending_dialysis_review = pending_dialysis as i64;
|
||||
// 待审核透析记录(raw SQL — entity 已拆分到 erp-dialysis crate)
|
||||
#[derive(FromQueryResult)]
|
||||
struct DialysisCount { count: i64 }
|
||||
let pending_dialysis = DialysisCount::find_by_statement(Statement::from_sql_and_values(
|
||||
DatabaseBackend::Postgres,
|
||||
"SELECT COUNT(*)::int8 AS count FROM dialysis_record WHERE tenant_id = $1 AND deleted_at IS NULL AND status = 'draft'",
|
||||
[tenant_id.into()],
|
||||
)).one(&state.db).await?.map(|r| r.count).unwrap_or(0);
|
||||
dashboard.pending_dialysis_review = pending_dialysis;
|
||||
|
||||
// 待审核化验报告
|
||||
let pending_lab = lab_report::Entity::find()
|
||||
|
||||
@@ -563,7 +563,7 @@ pub async fn review_lab_report(
|
||||
patient_id: Uuid,
|
||||
report_id: Uuid,
|
||||
reviewer_id: Uuid,
|
||||
req: crate::dto::dialysis_dto::ReviewLabReportReq,
|
||||
req: crate::dto::health_data_dto::ReviewLabReportReq,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<LabReportResp> {
|
||||
let model = lab_report::Entity::find()
|
||||
|
||||
@@ -13,8 +13,6 @@ pub mod daily_monitoring_service;
|
||||
pub mod device_reading_service;
|
||||
pub mod diagnosis_service;
|
||||
pub mod medication_record_service;
|
||||
pub mod dialysis_prescription_service;
|
||||
pub mod dialysis_service;
|
||||
pub mod doctor_service;
|
||||
pub mod follow_up_service;
|
||||
pub mod follow_up_template_service;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, sea_query::Expr, FromQueryResult};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, sea_query::Expr, FromQueryResult, Statement, DatabaseBackend};
|
||||
|
||||
use erp_core::error::AppResult;
|
||||
|
||||
use crate::dto::stats_dto::*;
|
||||
use crate::entity::{
|
||||
patient, consultation_session, follow_up_task,
|
||||
points_transaction, dialysis_record, lab_report,
|
||||
points_transaction, lab_report,
|
||||
appointment, vital_signs, patient_doctor_relation, doctor_profile,
|
||||
};
|
||||
use crate::state::HealthState;
|
||||
@@ -190,25 +190,27 @@ pub async fn get_dialysis_statistics(
|
||||
) -> AppResult<DialysisStatisticsResp> {
|
||||
let db = &state.db;
|
||||
|
||||
let total_records = dialysis_record::Entity::find()
|
||||
.filter(dialysis_record::Column::TenantId.eq(tenant_id))
|
||||
.filter(dialysis_record::Column::DeletedAt.is_null())
|
||||
.count(db)
|
||||
.await?;
|
||||
// 使用 raw SQL 替代 dialysis_record entity(已拆分到 erp-dialysis crate)
|
||||
#[derive(FromQueryResult)]
|
||||
struct CountRow { count: i64 }
|
||||
|
||||
let this_month = dialysis_record::Entity::find()
|
||||
.filter(dialysis_record::Column::TenantId.eq(tenant_id))
|
||||
.filter(dialysis_record::Column::DeletedAt.is_null())
|
||||
.filter(Expr::col(dialysis_record::Column::CreatedAt).gte(Expr::cust("date_trunc('month', NOW())")))
|
||||
.count(db)
|
||||
.await?;
|
||||
let total_records = CountRow::find_by_statement(Statement::from_sql_and_values(
|
||||
DatabaseBackend::Postgres,
|
||||
"SELECT COUNT(*)::int8 AS count FROM dialysis_record WHERE tenant_id = $1 AND deleted_at IS NULL",
|
||||
[tenant_id.into()],
|
||||
)).one(db).await?.map(|r| r.count).unwrap_or(0);
|
||||
|
||||
let pending_review = dialysis_record::Entity::find()
|
||||
.filter(dialysis_record::Column::TenantId.eq(tenant_id))
|
||||
.filter(dialysis_record::Column::DeletedAt.is_null())
|
||||
.filter(dialysis_record::Column::Status.eq("draft"))
|
||||
.count(db)
|
||||
.await?;
|
||||
let this_month = CountRow::find_by_statement(Statement::from_sql_and_values(
|
||||
DatabaseBackend::Postgres,
|
||||
"SELECT COUNT(*)::int8 AS count FROM dialysis_record WHERE tenant_id = $1 AND deleted_at IS NULL AND created_at >= date_trunc('month', NOW())",
|
||||
[tenant_id.into()],
|
||||
)).one(db).await?.map(|r| r.count).unwrap_or(0);
|
||||
|
||||
let pending_review = CountRow::find_by_statement(Statement::from_sql_and_values(
|
||||
DatabaseBackend::Postgres,
|
||||
"SELECT COUNT(*)::int8 AS count FROM dialysis_record WHERE tenant_id = $1 AND deleted_at IS NULL AND status = 'draft'",
|
||||
[tenant_id.into()],
|
||||
)).one(db).await?.map(|r| r.count).unwrap_or(0);
|
||||
|
||||
let type_distribution = count_by_field(
|
||||
db, tenant_id,
|
||||
@@ -223,13 +225,13 @@ pub async fn get_dialysis_statistics(
|
||||
let avg_duration = compute_avg_field(db, tenant_id, "dialysis_duration").await?;
|
||||
|
||||
Ok(DialysisStatisticsResp {
|
||||
total_records: total_records as i64,
|
||||
this_month: this_month as i64,
|
||||
total_records,
|
||||
this_month,
|
||||
type_distribution,
|
||||
complication_rate,
|
||||
avg_ultrafiltration,
|
||||
avg_duration,
|
||||
pending_review: pending_review as i64,
|
||||
pending_review,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user