fix(health): 修复审计发现的 10 个 CRITICAL 问题
权限与安全: - 为全部 51 个 handler 端点添加 require_permission 权限检查 - 修复 CAS 预约操作中 doctor_id 为 None 时使用 Uuid::nil() 的问题 状态机修复: - 预约初始状态从 "scheduled" 改为 "pending"(匹配设计规格) - 排班状态从 "active" 改为 "enabled" - 咨询会话添加 waiting→active 自动触发(首条消息时) - 新增 create_session 端点和 DTO 数据完整性: - doctor_profile 表添加 name 列(entity + migration + service) - lab_report/health_trend 的 json 列改为 json_binary(支持 GIN 索引) - 添加关键索引:patient.id_number UNIQUE、patient_tag UNIQUE、 doctor_schedule 唯一排班槽位、health_trend、doctor_profile.name - 随访记录完成后自动检查 next_follow_up_date 创建后续任务 事件总线: - 实现 10 种核心事件发布(patient/appointment/follow_up/consultation/lab_report) - 实现 workflow.task.completed 和 message.sent 事件订阅框架 种子数据: - 实现 seed_tenant_health(8 个默认患者标签) - 实现 soft_delete_tenant_data(16 张表级联软删除)
This commit is contained in:
@@ -36,6 +36,13 @@ pub struct CreateMessageReq {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CreateSessionReq {
|
||||
pub patient_id: Uuid,
|
||||
pub doctor_id: Option<Uuid>,
|
||||
pub consultation_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
|
||||
pub struct SessionQuery {
|
||||
pub status: Option<String>,
|
||||
|
||||
@@ -9,6 +9,7 @@ pub struct Model {
|
||||
pub tenant_id: Uuid,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
pub user_id: Option<Uuid>,
|
||||
pub name: String,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
pub department: Option<String>,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -1,7 +1,39 @@
|
||||
use erp_core::events::EventBus;
|
||||
|
||||
pub fn register_handlers(_bus: &EventBus) {
|
||||
// Health 模块订阅的事件处理器
|
||||
// - workflow.task.completed → 更新随访任务状态
|
||||
// - message.sent → 联动咨询会话 last_message_at
|
||||
pub fn register_handlers(bus: &EventBus) {
|
||||
// workflow.task.completed → 更新随访任务状态
|
||||
let (mut workflow_rx, _wf_handle) = bus.subscribe_filtered("workflow.task.".to_string());
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match workflow_rx.recv().await {
|
||||
Some(event) if event.event_type == "workflow.task.completed" => {
|
||||
tracing::info!(
|
||||
event_id = %event.id,
|
||||
"健康模块收到工作流任务完成事件"
|
||||
);
|
||||
// 后续可通过 db 连接更新 follow_up_task 状态
|
||||
}
|
||||
Some(_) => {}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// message.sent → 联动咨询会话 last_message_at
|
||||
let (mut msg_rx, _msg_handle) = bus.subscribe_filtered("message.".to_string());
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match msg_rx.recv().await {
|
||||
Some(event) if event.event_type == "message.sent" => {
|
||||
tracing::info!(
|
||||
event_id = %event.id,
|
||||
"健康模块收到消息发送事件"
|
||||
);
|
||||
// 后续可通过 db 连接更新 consultation_session.last_message_at
|
||||
}
|
||||
Some(_) => {}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use utoipa::IntoParams;
|
||||
use uuid::Uuid;
|
||||
|
||||
use erp_core::error::AppError;
|
||||
use erp_core::rbac::require_permission;
|
||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||
|
||||
use crate::dto::appointment_dto::*;
|
||||
@@ -59,6 +60,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.appointment.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let result = appointment_service::list_appointments(
|
||||
@@ -78,6 +80,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.appointment.manage")?;
|
||||
let result = appointment_service::create_appointment(
|
||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
||||
)
|
||||
@@ -95,6 +98,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.appointment.manage")?;
|
||||
let update_req = UpdateAppointmentStatusReq {
|
||||
status: req.status,
|
||||
cancel_reason: req.cancel_reason,
|
||||
@@ -115,6 +119,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.appointment.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let result = appointment_service::list_schedules(
|
||||
@@ -133,6 +138,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.appointment.manage")?;
|
||||
let result = appointment_service::create_schedule(
|
||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
||||
)
|
||||
@@ -150,6 +156,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.appointment.manage")?;
|
||||
let result = appointment_service::update_schedule(
|
||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.data, req.version,
|
||||
)
|
||||
@@ -166,6 +173,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.appointment.list")?;
|
||||
let result = appointment_service::calendar_view(
|
||||
&state, ctx.tenant_id, params.start_date, params.end_date, params.doctor_id,
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ use utoipa::IntoParams;
|
||||
use uuid::Uuid;
|
||||
|
||||
use erp_core::error::AppError;
|
||||
use erp_core::rbac::require_permission;
|
||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||
|
||||
use crate::dto::consultation_dto::*;
|
||||
@@ -47,6 +48,23 @@ pub struct ExportSessionsParams {
|
||||
pub doctor_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
pub async fn create_session<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Json(req): Json<CreateSessionReq>,
|
||||
) -> Result<Json<ApiResponse<SessionResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.consultation.manage")?;
|
||||
let result = consultation_service::create_session(
|
||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn list_sessions<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
@@ -56,6 +74,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.consultation.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let result = consultation_service::list_sessions(
|
||||
@@ -76,6 +95,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.consultation.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let result = consultation_service::list_messages(
|
||||
@@ -95,6 +115,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.consultation.manage")?;
|
||||
let result = consultation_service::close_session(
|
||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.version,
|
||||
)
|
||||
@@ -111,6 +132,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.consultation.manage")?;
|
||||
let msg_req = CreateMessageReq {
|
||||
session_id: req.session_id,
|
||||
sender_id: req.sender_id,
|
||||
@@ -134,6 +156,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.consultation.list")?;
|
||||
let result = consultation_service::export_sessions(
|
||||
&state, ctx.tenant_id, params.status, params.patient_id, params.doctor_id,
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ use utoipa::IntoParams;
|
||||
use uuid::Uuid;
|
||||
|
||||
use erp_core::error::AppError;
|
||||
use erp_core::rbac::require_permission;
|
||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||
|
||||
use crate::dto::doctor_dto::*;
|
||||
@@ -36,6 +37,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.doctor.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let result = doctor_service::list_doctors(
|
||||
@@ -54,6 +56,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.doctor.manage")?;
|
||||
let result = doctor_service::create_doctor(
|
||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
||||
)
|
||||
@@ -70,6 +73,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.doctor.list")?;
|
||||
let result = doctor_service::get_doctor(&state, ctx.tenant_id, id).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
@@ -84,6 +88,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.doctor.manage")?;
|
||||
let result = doctor_service::update_doctor(
|
||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.data, req.version,
|
||||
)
|
||||
@@ -100,6 +105,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.doctor.manage")?;
|
||||
doctor_service::delete_doctor(&state, ctx.tenant_id, id, Some(ctx.user_id)).await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use utoipa::IntoParams;
|
||||
use uuid::Uuid;
|
||||
|
||||
use erp_core::error::AppError;
|
||||
use erp_core::rbac::require_permission;
|
||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||
|
||||
use crate::dto::follow_up_dto::*;
|
||||
@@ -44,6 +45,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.follow-up.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let result = follow_up_service::list_tasks(
|
||||
@@ -63,6 +65,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.follow-up.manage")?;
|
||||
let result = follow_up_service::create_task(
|
||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
||||
)
|
||||
@@ -80,6 +83,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.follow-up.manage")?;
|
||||
let result = follow_up_service::update_task(
|
||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.data, req.version,
|
||||
)
|
||||
@@ -96,6 +100,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.follow-up.manage")?;
|
||||
follow_up_service::delete_task(&state, ctx.tenant_id, id, Some(ctx.user_id)).await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
@@ -109,6 +114,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.follow-up.manage")?;
|
||||
let result = follow_up_service::create_record(
|
||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
||||
)
|
||||
@@ -125,6 +131,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.follow-up.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let result = follow_up_service::list_records(
|
||||
|
||||
@@ -5,6 +5,7 @@ use utoipa::IntoParams;
|
||||
use uuid::Uuid;
|
||||
|
||||
use erp_core::error::AppError;
|
||||
use erp_core::rbac::require_permission;
|
||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||
|
||||
use crate::dto::health_data_dto::*;
|
||||
@@ -53,6 +54,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let result = health_data_service::list_vital_signs(
|
||||
@@ -72,6 +74,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
let result = health_data_service::create_vital_signs(
|
||||
&state, ctx.tenant_id, patient_id, Some(ctx.user_id), req,
|
||||
)
|
||||
@@ -89,6 +92,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
let result = health_data_service::update_vital_signs(
|
||||
&state, ctx.tenant_id, patient_id, vid, Some(ctx.user_id), req.data, req.version,
|
||||
)
|
||||
@@ -105,6 +109,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
health_data_service::delete_vital_signs(&state, ctx.tenant_id, vid, Some(ctx.user_id)).await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
@@ -123,6 +128,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let result = health_data_service::list_lab_reports(
|
||||
@@ -142,6 +148,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
let result = health_data_service::create_lab_report(
|
||||
&state, ctx.tenant_id, patient_id, Some(ctx.user_id), req,
|
||||
)
|
||||
@@ -159,6 +166,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
let result = health_data_service::update_lab_report(
|
||||
&state, ctx.tenant_id, rid, Some(ctx.user_id), req.data, req.version,
|
||||
)
|
||||
@@ -175,6 +183,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
health_data_service::delete_lab_report(&state, ctx.tenant_id, rid, Some(ctx.user_id)).await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
@@ -193,6 +202,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let result = health_data_service::list_health_records(
|
||||
@@ -212,6 +222,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
let result = health_data_service::create_health_record(
|
||||
&state, ctx.tenant_id, patient_id, Some(ctx.user_id), req,
|
||||
)
|
||||
@@ -229,6 +240,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
let result = health_data_service::update_health_record(
|
||||
&state, ctx.tenant_id, rid, Some(ctx.user_id), req.data, req.version,
|
||||
)
|
||||
@@ -245,6 +257,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
health_data_service::delete_health_record(&state, ctx.tenant_id, rid, Some(ctx.user_id)).await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
@@ -263,6 +276,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let result = health_data_service::list_trends(
|
||||
@@ -282,6 +296,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
let result = health_data_service::generate_trend(
|
||||
&state, ctx.tenant_id, patient_id, Some(ctx.user_id), req.period_start, req.period_end,
|
||||
)
|
||||
@@ -299,6 +314,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.list")?;
|
||||
let result = health_data_service::get_indicator_timeseries(
|
||||
&state, ctx.tenant_id, patient_id, indicator, params.start_date, params.end_date,
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ use utoipa::IntoParams;
|
||||
use uuid::Uuid;
|
||||
|
||||
use erp_core::error::AppError;
|
||||
use erp_core::rbac::require_permission;
|
||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||
|
||||
use crate::dto::patient_dto::{
|
||||
@@ -38,6 +39,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.patient.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let result = patient_service::list_patients(
|
||||
@@ -56,6 +58,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.patient.manage")?;
|
||||
let result = patient_service::create_patient(
|
||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
||||
)
|
||||
@@ -72,6 +75,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.patient.list")?;
|
||||
let result = patient_service::get_patient(&state, ctx.tenant_id, id).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
@@ -86,6 +90,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.patient.manage")?;
|
||||
let version = req.version;
|
||||
let update = UpdatePatientReq {
|
||||
name: req.name,
|
||||
@@ -116,6 +121,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.patient.manage")?;
|
||||
patient_service::delete_patient(&state, ctx.tenant_id, id, Some(ctx.user_id)).await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
@@ -130,6 +136,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.patient.manage")?;
|
||||
patient_service::manage_patient_tags(&state, ctx.tenant_id, id, req, Some(ctx.user_id)).await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
@@ -143,6 +150,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.patient.list")?;
|
||||
let result = patient_service::get_health_summary(&state, ctx.tenant_id, id).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
@@ -156,6 +164,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.patient.list")?;
|
||||
let result = patient_service::list_family_members(&state, ctx.tenant_id, id).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
@@ -170,6 +179,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.patient.manage")?;
|
||||
let result = patient_service::create_family_member(
|
||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req,
|
||||
)
|
||||
@@ -187,6 +197,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.patient.manage")?;
|
||||
let version = req.version;
|
||||
let update = FamilyMemberReq {
|
||||
name: req.name,
|
||||
@@ -211,6 +222,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.patient.manage")?;
|
||||
patient_service::delete_family_member(
|
||||
&state, ctx.tenant_id, patient_id, member_id, Some(ctx.user_id),
|
||||
)
|
||||
@@ -228,6 +240,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.patient.manage")?;
|
||||
patient_service::assign_doctor(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
@@ -249,6 +262,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.patient.manage")?;
|
||||
patient_service::remove_doctor(&state, ctx.tenant_id, patient_id, doctor_id).await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
|
||||
@@ -157,7 +157,8 @@ impl HealthModule {
|
||||
// 咨询管理
|
||||
.route(
|
||||
"/health/consultation-sessions",
|
||||
axum::routing::get(consultation_handler::list_sessions),
|
||||
axum::routing::get(consultation_handler::list_sessions)
|
||||
.post(consultation_handler::create_session),
|
||||
)
|
||||
.route(
|
||||
"/health/consultation-sessions/{id}/messages",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! 预约排班 Service — 预约CRUD、排班管理、日历视图、原子CAS预约
|
||||
|
||||
use chrono::Utc;
|
||||
use erp_core::events::DomainEvent;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{ActiveValue::Set, Condition, QueryOrder, QuerySelect};
|
||||
use uuid::Uuid;
|
||||
@@ -74,7 +75,7 @@ pub async fn create_appointment(
|
||||
)
|
||||
.col_expr(doctor_schedule::Column::UpdatedAt, Expr::value(Utc::now()))
|
||||
.filter(doctor_schedule::Column::TenantId.eq(tenant_id))
|
||||
.filter(doctor_schedule::Column::DoctorId.eq(req.doctor_id.unwrap_or_default()))
|
||||
.filter(doctor_schedule::Column::DoctorId.eq(req.doctor_id.ok_or(HealthError::Validation("doctor_id is required".to_string()))?))
|
||||
.filter(doctor_schedule::Column::ScheduleDate.eq(req.appointment_date))
|
||||
.filter(doctor_schedule::Column::StartTime.eq(req.start_time))
|
||||
.filter(
|
||||
@@ -102,7 +103,7 @@ pub async fn create_appointment(
|
||||
appointment_date: Set(req.appointment_date),
|
||||
start_time: Set(req.start_time),
|
||||
end_time: Set(req.end_time),
|
||||
status: Set("scheduled".to_string()),
|
||||
status: Set("pending".to_string()),
|
||||
cancel_reason: Set(None),
|
||||
notes: Set(req.notes),
|
||||
created_at: Set(now),
|
||||
@@ -113,6 +114,14 @@ pub async fn create_appointment(
|
||||
version: Set(1),
|
||||
};
|
||||
let m = active.insert(&state.db).await?;
|
||||
|
||||
let event = DomainEvent::new(
|
||||
"appointment.created",
|
||||
tenant_id,
|
||||
serde_json::json!({ "appointment_id": m.id, "patient_id": m.patient_id, "status": m.status }),
|
||||
);
|
||||
state.event_bus.publish(event, &state.db).await;
|
||||
|
||||
Ok(AppointmentResp {
|
||||
id: m.id, patient_id: m.patient_id, doctor_id: m.doctor_id,
|
||||
appointment_type: m.appointment_type, appointment_date: m.appointment_date,
|
||||
@@ -140,7 +149,7 @@ pub async fn update_appointment_status(
|
||||
|
||||
// 状态机校验
|
||||
let valid = match (model.status.as_str(), req.status.as_str()) {
|
||||
("scheduled", "confirmed" | "cancelled") => true,
|
||||
("pending", "confirmed" | "cancelled") => true,
|
||||
("confirmed", "completed" | "no_show" | "cancelled") => true,
|
||||
_ => false,
|
||||
};
|
||||
@@ -179,6 +188,15 @@ pub async fn update_appointment_status(
|
||||
active.version = Set(next_ver);
|
||||
|
||||
let m = active.update(&state.db).await?;
|
||||
|
||||
let event_type = format!("appointment.{}", m.status);
|
||||
let event = DomainEvent::new(
|
||||
event_type,
|
||||
tenant_id,
|
||||
serde_json::json!({ "appointment_id": m.id, "patient_id": m.patient_id, "status": m.status }),
|
||||
);
|
||||
state.event_bus.publish(event, &state.db).await;
|
||||
|
||||
Ok(AppointmentResp {
|
||||
id: m.id, patient_id: m.patient_id, doctor_id: m.doctor_id,
|
||||
appointment_type: m.appointment_type, appointment_date: m.appointment_date,
|
||||
@@ -246,7 +264,7 @@ pub async fn create_schedule(
|
||||
end_time: Set(req.end_time),
|
||||
max_appointments: Set(req.max_appointments),
|
||||
current_appointments: Set(0),
|
||||
status: Set("active".to_string()),
|
||||
status: Set("enabled".to_string()),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(operator_id),
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//! 咨询管理 Service — 会话管理、消息收发、会话关闭、导出
|
||||
|
||||
use chrono::Utc;
|
||||
use erp_core::events::DomainEvent;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{ActiveValue::Set, QueryOrder, QuerySelect};
|
||||
use sea_orm::{ActiveValue::Set, Condition, QueryOrder, QuerySelect};
|
||||
use uuid::Uuid;
|
||||
|
||||
use erp_core::error::check_version;
|
||||
@@ -17,6 +18,49 @@ use crate::state::HealthState;
|
||||
// 咨询会话
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn create_session(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: CreateSessionReq,
|
||||
) -> HealthResult<SessionResp> {
|
||||
let now = Utc::now();
|
||||
let active = consultation_session::ActiveModel {
|
||||
id: Set(Uuid::now_v7()),
|
||||
tenant_id: Set(tenant_id),
|
||||
patient_id: Set(req.patient_id),
|
||||
doctor_id: Set(req.doctor_id),
|
||||
consultation_type: Set(req.consultation_type.unwrap_or_else(|| "text".to_string())),
|
||||
status: Set("waiting".to_string()),
|
||||
last_message_at: Set(None),
|
||||
unread_count_patient: Set(0),
|
||||
unread_count_doctor: Set(0),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(operator_id),
|
||||
updated_by: Set(operator_id),
|
||||
deleted_at: Set(None),
|
||||
version: Set(1),
|
||||
};
|
||||
let m = active.insert(&state.db).await?;
|
||||
|
||||
let event = DomainEvent::new(
|
||||
"consultation.opened",
|
||||
tenant_id,
|
||||
serde_json::json!({ "session_id": m.id, "patient_id": m.patient_id }),
|
||||
);
|
||||
state.event_bus.publish(event, &state.db).await;
|
||||
|
||||
Ok(SessionResp {
|
||||
id: m.id, patient_id: m.patient_id, doctor_id: m.doctor_id,
|
||||
consultation_type: m.consultation_type, status: m.status,
|
||||
last_message_at: m.last_message_at,
|
||||
unread_count_patient: m.unread_count_patient,
|
||||
unread_count_doctor: m.unread_count_doctor,
|
||||
created_at: m.created_at,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn list_sessions(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
@@ -84,6 +128,14 @@ pub async fn close_session(
|
||||
active.version = Set(next_ver);
|
||||
|
||||
let m = active.update(&state.db).await?;
|
||||
|
||||
let event = DomainEvent::new(
|
||||
"consultation.closed",
|
||||
tenant_id,
|
||||
serde_json::json!({ "session_id": m.id, "patient_id": m.patient_id }),
|
||||
);
|
||||
state.event_bus.publish(event, &state.db).await;
|
||||
|
||||
Ok(SessionResp {
|
||||
id: m.id, patient_id: m.patient_id, doctor_id: m.doctor_id,
|
||||
consultation_type: m.consultation_type, status: m.status,
|
||||
@@ -138,7 +190,7 @@ pub async fn list_messages(
|
||||
let limit = page_size.min(100);
|
||||
let offset = page.saturating_sub(1) * limit;
|
||||
|
||||
let mut query = consultation_message::Entity::find()
|
||||
let query = consultation_message::Entity::find()
|
||||
.filter(consultation_message::Column::TenantId.eq(tenant_id))
|
||||
.filter(consultation_message::Column::SessionId.eq(session_id))
|
||||
.filter(consultation_message::Column::DeletedAt.is_null());
|
||||
@@ -167,18 +219,23 @@ pub async fn create_message(
|
||||
operator_id: Option<Uuid>,
|
||||
req: CreateMessageReq,
|
||||
) -> HealthResult<MessageResp> {
|
||||
// 校验会话存在且状态为 active
|
||||
// 校验会话存在且状态为 active 或 waiting
|
||||
let session = consultation_session::Entity::find()
|
||||
.filter(consultation_session::Column::Id.eq(req.session_id))
|
||||
.filter(consultation_session::Column::TenantId.eq(tenant_id))
|
||||
.filter(consultation_session::Column::DeletedAt.is_null())
|
||||
.filter(consultation_session::Column::Status.eq("active"))
|
||||
.filter(
|
||||
Condition::any()
|
||||
.add(consultation_session::Column::Status.eq("active"))
|
||||
.add(consultation_session::Column::Status.eq("waiting")),
|
||||
)
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::ConsultationNotFound)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let is_patient = req.sender_role == "patient";
|
||||
let should_activate = session.status == "waiting";
|
||||
|
||||
// 创建消息
|
||||
let active = consultation_message::ActiveModel {
|
||||
@@ -199,9 +256,12 @@ pub async fn create_message(
|
||||
};
|
||||
let m = active.insert(&state.db).await?;
|
||||
|
||||
// 更新会话的 last_message_at 和未读计数
|
||||
// 更新会话的 last_message_at 和未读计数,waiting→active 自动触发
|
||||
let mut session_active: consultation_session::ActiveModel = session.into();
|
||||
session_active.last_message_at = Set(Some(now));
|
||||
if should_activate {
|
||||
session_active.status = Set("active".to_string());
|
||||
}
|
||||
// 根据发送者角色更新对方的 unread_count
|
||||
if is_patient {
|
||||
session_active.unread_count_doctor = Set(session_active.unread_count_doctor.unwrap() + 1);
|
||||
|
||||
@@ -30,10 +30,10 @@ pub async fn list_doctors(
|
||||
.filter(doctor_profile::Column::DeletedAt.is_null());
|
||||
|
||||
if let Some(ref s) = search {
|
||||
// doctor_profile 没有 name 字段,按 license_number/department 模糊搜索
|
||||
let pattern = format!("%{}%", s);
|
||||
query = query.filter(
|
||||
Condition::any()
|
||||
.add(doctor_profile::Column::Name.contains(&pattern))
|
||||
.add(doctor_profile::Column::LicenseNumber.contains(&pattern))
|
||||
.add(doctor_profile::Column::Department.contains(&pattern))
|
||||
.add(doctor_profile::Column::Specialty.contains(&pattern)),
|
||||
@@ -79,6 +79,7 @@ pub async fn create_doctor(
|
||||
id: Set(id),
|
||||
tenant_id: Set(tenant_id),
|
||||
user_id: Set(req.user_id),
|
||||
name: Set(req.name),
|
||||
department: Set(req.department),
|
||||
title: Set(req.title),
|
||||
specialty: Set(req.specialty),
|
||||
@@ -119,8 +120,7 @@ pub async fn update_doctor(
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
let mut active: doctor_profile::ActiveModel = model.into();
|
||||
// doctor_profile 没有 name 字段,但 handler DTO 有 name
|
||||
// name 需要通过 user_id 关联到 erp-auth 的 users 表,此处暂不处理
|
||||
if let Some(v) = req.name { active.name = Set(v); }
|
||||
if let Some(v) = req.department { active.department = Set(Some(v)); }
|
||||
if let Some(v) = req.title { active.title = Set(Some(v)); }
|
||||
if let Some(v) = req.specialty { active.specialty = Set(Some(v)); }
|
||||
@@ -170,7 +170,7 @@ fn model_to_resp(m: doctor_profile::Model) -> DoctorResp {
|
||||
DoctorResp {
|
||||
id: m.id,
|
||||
user_id: m.user_id,
|
||||
name: m.user_id.map(|_| "".to_string()).unwrap_or_default(),
|
||||
name: m.name,
|
||||
department: m.department,
|
||||
title: m.title,
|
||||
specialty: m.specialty,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! 随访管理 Service — 随访任务CRUD、随访记录、状态流转
|
||||
|
||||
use chrono::Utc;
|
||||
use erp_core::events::DomainEvent;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{ActiveValue::Set, QueryOrder, QuerySelect};
|
||||
use uuid::Uuid;
|
||||
@@ -82,6 +83,14 @@ pub async fn create_task(
|
||||
version: Set(1),
|
||||
};
|
||||
let m = active.insert(&state.db).await?;
|
||||
|
||||
let event = DomainEvent::new(
|
||||
"follow_up.created",
|
||||
tenant_id,
|
||||
serde_json::json!({ "task_id": m.id, "patient_id": m.patient_id }),
|
||||
);
|
||||
state.event_bus.publish(event, &state.db).await;
|
||||
|
||||
Ok(FollowUpTaskResp {
|
||||
id: m.id, patient_id: m.patient_id, assigned_to: m.assigned_to,
|
||||
follow_up_type: m.follow_up_type, planned_date: m.planned_date,
|
||||
@@ -197,12 +206,44 @@ pub async fn create_record(
|
||||
let record = record_active.insert(&state.db).await?;
|
||||
|
||||
let mut task_active: follow_up_task::ActiveModel = task.into();
|
||||
let task_patient_id = task_active.patient_id.clone().unwrap();
|
||||
let task_assigned_to = task_active.assigned_to.clone().unwrap();
|
||||
let task_follow_up_type = task_active.follow_up_type.clone().unwrap();
|
||||
task_active.status = Set("completed".to_string());
|
||||
task_active.updated_at = Set(now);
|
||||
task_active.updated_by = Set(operator_id);
|
||||
task_active.version = Set(task_active.version.unwrap() + 1);
|
||||
task_active.update(&state.db).await?;
|
||||
|
||||
// 当 next_follow_up_date 不为空时,自动创建后续随访任务
|
||||
if let Some(next_date) = req.next_follow_up_date {
|
||||
let new_task = follow_up_task::ActiveModel {
|
||||
id: Set(Uuid::now_v7()),
|
||||
tenant_id: Set(tenant_id),
|
||||
patient_id: Set(task_patient_id),
|
||||
assigned_to: Set(task_assigned_to),
|
||||
follow_up_type: Set(task_follow_up_type),
|
||||
planned_date: Set(next_date),
|
||||
status: Set("pending".to_string()),
|
||||
content_template: Set(None),
|
||||
related_appointment_id: Set(None),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(operator_id),
|
||||
updated_by: Set(operator_id),
|
||||
deleted_at: Set(None),
|
||||
version: Set(1),
|
||||
};
|
||||
new_task.insert(&state.db).await?;
|
||||
}
|
||||
|
||||
let event = DomainEvent::new(
|
||||
"follow_up.completed",
|
||||
tenant_id,
|
||||
serde_json::json!({ "task_id": record.task_id, "patient_id": task_patient_id }),
|
||||
);
|
||||
state.event_bus.publish(event, &state.db).await;
|
||||
|
||||
Ok(FollowUpRecordResp {
|
||||
id: record.id, task_id: record.task_id, executed_by: record.executed_by,
|
||||
executed_date: record.executed_date, result: record.result,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! 健康数据 Service — 体征记录、化验报告、体检记录、趋势分析
|
||||
|
||||
use chrono::Utc;
|
||||
use erp_core::events::DomainEvent;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{ActiveValue::Set, QueryOrder, QuerySelect};
|
||||
use uuid::Uuid;
|
||||
@@ -27,7 +28,7 @@ pub async fn list_vital_signs(
|
||||
let limit = page_size.min(100);
|
||||
let offset = page.saturating_sub(1) * limit;
|
||||
|
||||
let mut query = vital_signs::Entity::find()
|
||||
let query = vital_signs::Entity::find()
|
||||
.filter(vital_signs::Column::TenantId.eq(tenant_id))
|
||||
.filter(vital_signs::Column::PatientId.eq(patient_id))
|
||||
.filter(vital_signs::Column::DeletedAt.is_null());
|
||||
@@ -191,7 +192,7 @@ pub async fn list_lab_reports(
|
||||
let limit = page_size.min(100);
|
||||
let offset = page.saturating_sub(1) * limit;
|
||||
|
||||
let mut query = lab_report::Entity::find()
|
||||
let query = lab_report::Entity::find()
|
||||
.filter(lab_report::Column::TenantId.eq(tenant_id))
|
||||
.filter(lab_report::Column::PatientId.eq(patient_id))
|
||||
.filter(lab_report::Column::DeletedAt.is_null());
|
||||
@@ -240,6 +241,14 @@ pub async fn create_lab_report(
|
||||
version: Set(1),
|
||||
};
|
||||
let m = active.insert(&state.db).await?;
|
||||
|
||||
let event = DomainEvent::new(
|
||||
"lab_report.uploaded",
|
||||
tenant_id,
|
||||
serde_json::json!({ "report_id": m.id, "patient_id": m.patient_id, "report_type": m.report_type }),
|
||||
);
|
||||
state.event_bus.publish(event, &state.db).await;
|
||||
|
||||
Ok(LabReportResp {
|
||||
id: m.id, patient_id: m.patient_id, report_date: m.report_date,
|
||||
report_type: m.report_type, indicators: m.indicators,
|
||||
@@ -322,7 +331,7 @@ pub async fn list_health_records(
|
||||
let limit = page_size.min(100);
|
||||
let offset = page.saturating_sub(1) * limit;
|
||||
|
||||
let mut query = health_record::Entity::find()
|
||||
let query = health_record::Entity::find()
|
||||
.filter(health_record::Column::TenantId.eq(tenant_id))
|
||||
.filter(health_record::Column::PatientId.eq(patient_id))
|
||||
.filter(health_record::Column::DeletedAt.is_null());
|
||||
@@ -455,7 +464,7 @@ pub async fn list_trends(
|
||||
let limit = page_size.min(100);
|
||||
let offset = page.saturating_sub(1) * limit;
|
||||
|
||||
let mut query = health_trend::Entity::find()
|
||||
let query = health_trend::Entity::find()
|
||||
.filter(health_trend::Column::TenantId.eq(tenant_id))
|
||||
.filter(health_trend::Column::PatientId.eq(patient_id))
|
||||
.filter(health_trend::Column::DeletedAt.is_null());
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! 患者管理 Service — CRUD、家庭成员、标签、医生关联、健康摘要
|
||||
|
||||
use chrono::Utc;
|
||||
use erp_core::events::DomainEvent;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{ActiveValue::Set, Condition, QueryOrder, QuerySelect};
|
||||
use uuid::Uuid;
|
||||
@@ -122,6 +123,14 @@ pub async fn create_patient(
|
||||
};
|
||||
|
||||
let model = active.insert(&state.db).await?;
|
||||
|
||||
let event = DomainEvent::new(
|
||||
"patient.created",
|
||||
tenant_id,
|
||||
serde_json::json!({ "patient_id": model.id, "name": model.name }),
|
||||
);
|
||||
state.event_bus.publish(event, &state.db).await;
|
||||
|
||||
Ok(model_to_resp(model))
|
||||
}
|
||||
|
||||
@@ -167,6 +176,14 @@ pub async fn update_patient(
|
||||
active.version = Set(next_ver);
|
||||
|
||||
let updated = active.update(&state.db).await?;
|
||||
|
||||
let event = DomainEvent::new(
|
||||
"patient.updated",
|
||||
tenant_id,
|
||||
serde_json::json!({ "patient_id": updated.id }),
|
||||
);
|
||||
state.event_bus.publish(event, &state.db).await;
|
||||
|
||||
Ok(model_to_resp(updated))
|
||||
}
|
||||
|
||||
@@ -277,7 +294,7 @@ pub async fn get_health_summary(
|
||||
let upcoming = appointment::Entity::find()
|
||||
.filter(appointment::Column::TenantId.eq(tenant_id))
|
||||
.filter(appointment::Column::PatientId.eq(patient_id))
|
||||
.filter(appointment::Column::Status.eq("scheduled"))
|
||||
.filter(appointment::Column::Status.eq("pending"))
|
||||
.filter(appointment::Column::DeletedAt.is_null())
|
||||
.count(&state.db)
|
||||
.await?;
|
||||
|
||||
@@ -1,22 +1,95 @@
|
||||
//! 租户初始化种子数据 — 创建默认标签、默认排班模板等
|
||||
//! 租户初始化种子数据 — 创建默认标签
|
||||
|
||||
use sea_orm::DatabaseConnection;
|
||||
use chrono::Utc;
|
||||
use sea_orm::{ActiveModelTrait, ActiveValue::Set, ConnectionTrait, DatabaseConnection};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::entity::patient_tag;
|
||||
|
||||
const DEFAULT_TAGS: &[(&str, &str, &str)] = &[
|
||||
("高血压", "#E74C3C", "高血压患者标签"),
|
||||
("糖尿病", "#3498DB", "糖尿病患者标签"),
|
||||
("心脏病", "#9B59B6", "心脏病患者标签"),
|
||||
("过敏体质", "#F39C12", "过敏体质患者标签"),
|
||||
("老年患者", "#1ABC9C", "65岁以上老年患者"),
|
||||
("孕产妇", "#E91E63", "孕产期健康管理"),
|
||||
("慢性病", "#607D8B", "慢性病长期随访"),
|
||||
("术后随访", "#795548", "手术后随访管理"),
|
||||
];
|
||||
|
||||
/// 初始化租户健康模块默认数据
|
||||
pub async fn seed_tenant_health(
|
||||
_db: &DatabaseConnection,
|
||||
db: &DatabaseConnection,
|
||||
tenant_id: Uuid,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
tracing::info!(tenant_id = %tenant_id, "Seeding health module default data");
|
||||
|
||||
let now = Utc::now();
|
||||
for (name, color, description) in DEFAULT_TAGS {
|
||||
let active = patient_tag::ActiveModel {
|
||||
id: Set(Uuid::now_v7()),
|
||||
tenant_id: Set(tenant_id),
|
||||
name: Set(name.to_string()),
|
||||
color: Set(Some(color.to_string())),
|
||||
description: Set(Some(description.to_string())),
|
||||
is_system: Set(true),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(None),
|
||||
updated_by: Set(None),
|
||||
deleted_at: Set(None),
|
||||
version: Set(1),
|
||||
};
|
||||
active.insert(db).await?;
|
||||
}
|
||||
|
||||
tracing::info!(tenant_id = %tenant_id, "Health module default data seeded successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 软删除该租户下所有健康模块数据
|
||||
pub async fn soft_delete_tenant_data(
|
||||
_db: &DatabaseConnection,
|
||||
db: &DatabaseConnection,
|
||||
tenant_id: Uuid,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
tracing::info!(tenant_id = %tenant_id, "Soft-deleting health module data for tenant");
|
||||
|
||||
let now = Utc::now();
|
||||
|
||||
let tables_to_soft_delete: Vec<&str> = vec![
|
||||
"consultation_message",
|
||||
"consultation_session",
|
||||
"follow_up_record",
|
||||
"follow_up_task",
|
||||
"appointment",
|
||||
"doctor_schedule",
|
||||
"health_trend",
|
||||
"lab_report",
|
||||
"health_record",
|
||||
"vital_signs",
|
||||
"patient_doctor_relation",
|
||||
"patient_tag_relation",
|
||||
"patient_family_member",
|
||||
"patient",
|
||||
"patient_tag",
|
||||
"doctor_profile",
|
||||
];
|
||||
|
||||
for table in tables_to_soft_delete {
|
||||
let sql = format!(
|
||||
"UPDATE {} SET deleted_at = $1, updated_at = $1 WHERE tenant_id = $2 AND deleted_at IS NULL",
|
||||
table
|
||||
);
|
||||
let stmt = sea_orm::Statement::from_sql_and_values(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
&sql,
|
||||
[now.into(), tenant_id.into()],
|
||||
);
|
||||
if let Err(e) = db.execute(stmt).await {
|
||||
tracing::warn!(table = %table, error = %e, "Failed to soft-delete table");
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(tenant_id = %tenant_id, "Health module data soft-deleted");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ impl MigrationTrait for Migration {
|
||||
.table(Patient::Table)
|
||||
.col(Patient::TenantId)
|
||||
.col(Patient::IdNumber)
|
||||
.unique()
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
@@ -266,6 +267,20 @@ impl MigrationTrait for Migration {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// patient_tag 唯一名称索引
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.if_not_exists()
|
||||
.name("idx_patient_tag_tenant_name_unique")
|
||||
.table(PatientTag::Table)
|
||||
.col(PatientTag::TenantId)
|
||||
.col(PatientTag::Name)
|
||||
.unique()
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 5. doctor_profile — 医护档案
|
||||
manager
|
||||
.create_table(
|
||||
@@ -275,6 +290,7 @@ impl MigrationTrait for Migration {
|
||||
.col(ColumnDef::new(DoctorProfile::Id).uuid().not_null().primary_key())
|
||||
.col(ColumnDef::new(DoctorProfile::TenantId).uuid().not_null())
|
||||
.col(ColumnDef::new(DoctorProfile::UserId).uuid().null())
|
||||
.col(ColumnDef::new(DoctorProfile::Name).string_len(100).not_null())
|
||||
.col(ColumnDef::new(DoctorProfile::Department).string_len(100).null())
|
||||
.col(ColumnDef::new(DoctorProfile::Title).string_len(50).null())
|
||||
.col(ColumnDef::new(DoctorProfile::Specialty).string_len(200).null())
|
||||
@@ -311,6 +327,19 @@ impl MigrationTrait for Migration {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// doctor_profile 名称搜索索引
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.if_not_exists()
|
||||
.name("idx_doctor_profile_tenant_name")
|
||||
.table(DoctorProfile::Table)
|
||||
.col(DoctorProfile::TenantId)
|
||||
.col(DoctorProfile::Name)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 6. patient_doctor_relation — 医患关系
|
||||
manager
|
||||
.create_table(
|
||||
@@ -529,8 +558,8 @@ impl MigrationTrait for Migration {
|
||||
.col(ColumnDef::new(LabReport::PatientId).uuid().not_null())
|
||||
.col(ColumnDef::new(LabReport::ReportDate).date().not_null())
|
||||
.col(ColumnDef::new(LabReport::ReportType).string_len(50).not_null())
|
||||
.col(ColumnDef::new(LabReport::Indicators).json().null())
|
||||
.col(ColumnDef::new(LabReport::ImageUrls).json().null())
|
||||
.col(ColumnDef::new(LabReport::Indicators).json_binary().null())
|
||||
.col(ColumnDef::new(LabReport::ImageUrls).json_binary().null())
|
||||
.col(ColumnDef::new(LabReport::DoctorInterpretation).text().null())
|
||||
.col(
|
||||
ColumnDef::new(LabReport::CreatedAt)
|
||||
@@ -587,8 +616,8 @@ impl MigrationTrait for Migration {
|
||||
.col(ColumnDef::new(HealthTrend::PatientId).uuid().not_null())
|
||||
.col(ColumnDef::new(HealthTrend::PeriodStart).date().not_null())
|
||||
.col(ColumnDef::new(HealthTrend::PeriodEnd).date().not_null())
|
||||
.col(ColumnDef::new(HealthTrend::IndicatorSummary).json().null())
|
||||
.col(ColumnDef::new(HealthTrend::AbnormalItems).json().null())
|
||||
.col(ColumnDef::new(HealthTrend::IndicatorSummary).json_binary().null())
|
||||
.col(ColumnDef::new(HealthTrend::AbnormalItems).json_binary().null())
|
||||
.col(
|
||||
ColumnDef::new(HealthTrend::GenerationType)
|
||||
.string_len(20)
|
||||
@@ -795,6 +824,22 @@ impl MigrationTrait for Migration {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// doctor_schedule 唯一约束:同一医生同一天同一时段不能重复排班
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.if_not_exists()
|
||||
.name("idx_doctor_schedule_unique_slot")
|
||||
.table(DoctorSchedule::Table)
|
||||
.col(DoctorSchedule::TenantId)
|
||||
.col(DoctorSchedule::DoctorId)
|
||||
.col(DoctorSchedule::ScheduleDate)
|
||||
.col(DoctorSchedule::StartTime)
|
||||
.unique()
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 13. follow_up_task — 随访任务
|
||||
manager
|
||||
.create_table(
|
||||
@@ -1130,6 +1175,19 @@ impl MigrationTrait for Migration {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// health_trend 索引
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.if_not_exists()
|
||||
.name("idx_health_trend_tenant_patient")
|
||||
.table(HealthTrend::Table)
|
||||
.col(HealthTrend::TenantId)
|
||||
.col(HealthTrend::PatientId)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1237,6 +1295,7 @@ enum DoctorProfile {
|
||||
Id,
|
||||
TenantId,
|
||||
UserId,
|
||||
Name,
|
||||
Department,
|
||||
Title,
|
||||
Specialty,
|
||||
|
||||
Reference in New Issue
Block a user