feat(health): 添加 erp-health 健康管理模块骨架
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

新建 erp-health 原生 Rust crate,覆盖设计规格中定义的 5 大业务域:

- 16 个 SeaORM Entity(患者/家属/标签/医生/健康档案/体征/化验单/预约/排班/随访/咨询等)
- 16 表数据库迁移(含索引、外键、默认值、可回滚)
- 40+ API 路由骨架(患者管理/健康数据/预约排班/随访/咨询/医生管理)
- 12 个权限声明(health.patient/health-data/appointment/follow-up/consultation/doctor 各 .list/.manage)
- DTO / Service / Handler / Event 四层架构,Service 使用 todo!() 占位
- erp-server 集成:模块注册 + AppState FromRef 桥接 + 路由挂载

同步更新 CLAUDE.md 项目进度、wiki 知识库、设计规格文档。
This commit is contained in:
iven
2026-04-23 19:59:22 +08:00
parent 5ac8e18d74
commit ca50d32f6e
61 changed files with 6853 additions and 1208 deletions

View File

@@ -0,0 +1,19 @@
[package]
name = "erp-health"
version.workspace = true
edition.workspace = true
[dependencies]
erp-core.workspace = true
tokio.workspace = true
serde.workspace = true
serde_json.workspace = true
uuid.workspace = true
chrono.workspace = true
axum.workspace = true
sea-orm.workspace = true
tracing.workspace = true
thiserror.workspace = true
validator.workspace = true
utoipa.workspace = true
async-trait.workspace = true

View File

@@ -0,0 +1,95 @@
use chrono::{NaiveDate, NaiveTime};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct CreateAppointmentReq {
pub patient_id: Uuid,
pub doctor_id: Option<Uuid>,
pub appointment_type: Option<String>,
pub appointment_date: NaiveDate,
pub start_time: NaiveTime,
pub end_time: NaiveTime,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct UpdateAppointmentStatusReq {
pub status: String,
pub cancel_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct AppointmentResp {
pub id: Uuid,
pub patient_id: Uuid,
pub doctor_id: Option<Uuid>,
pub appointment_type: String,
pub appointment_date: NaiveDate,
pub start_time: NaiveTime,
pub end_time: NaiveTime,
pub status: String,
pub cancel_reason: Option<String>,
pub notes: Option<String>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub version: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct CreateScheduleReq {
pub doctor_id: Uuid,
pub schedule_date: NaiveDate,
pub period_type: Option<String>,
pub start_time: NaiveTime,
pub end_time: NaiveTime,
pub max_appointments: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct UpdateScheduleReq {
pub start_time: Option<NaiveTime>,
pub end_time: Option<NaiveTime>,
pub max_appointments: Option<i32>,
pub status: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ScheduleResp {
pub id: Uuid,
pub doctor_id: Uuid,
pub schedule_date: NaiveDate,
pub period_type: String,
pub start_time: NaiveTime,
pub end_time: NaiveTime,
pub max_appointments: i32,
pub current_appointments: i32,
pub status: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub version: i32,
}
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
pub struct CalendarQuery {
pub start_date: Option<NaiveDate>,
pub end_date: Option<NaiveDate>,
pub doctor_id: Option<Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct CalendarDayResp {
pub date: NaiveDate,
pub schedules: Vec<ScheduleResp>,
}
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
pub struct AppointmentListQuery {
pub page: Option<u64>,
pub page_size: Option<u64>,
pub status: Option<String>,
pub patient_id: Option<Uuid>,
pub doctor_id: Option<Uuid>,
pub date: Option<NaiveDate>,
}

View File

@@ -0,0 +1,46 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct SessionResp {
pub id: Uuid,
pub patient_id: Uuid,
pub doctor_id: Option<Uuid>,
pub consultation_type: String,
pub status: String,
pub last_message_at: Option<chrono::DateTime<chrono::Utc>>,
pub unread_count_patient: i32,
pub unread_count_doctor: i32,
pub created_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct MessageResp {
pub id: Uuid,
pub session_id: Uuid,
pub sender_id: Uuid,
pub sender_role: String,
pub content_type: String,
pub content: String,
pub is_read: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct CreateMessageReq {
pub session_id: Uuid,
pub sender_id: Uuid,
pub sender_role: String,
pub content_type: Option<String>,
pub content: String,
}
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
pub struct SessionQuery {
pub status: Option<String>,
pub patient_id: Option<Uuid>,
pub doctor_id: Option<Uuid>,
pub page: Option<u64>,
pub page_size: Option<u64>,
}

View File

@@ -0,0 +1,123 @@
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;
/// 用 f64 替代 Decimal 以满足 utoipa ToSchema
type Decimal = f64;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct CreateVitalSignsReq {
pub record_date: NaiveDate,
pub systolic_bp_morning: Option<i32>,
pub diastolic_bp_morning: Option<i32>,
pub systolic_bp_evening: Option<i32>,
pub diastolic_bp_evening: Option<i32>,
pub heart_rate: Option<i32>,
pub weight: Option<Decimal>,
pub blood_sugar: Option<Decimal>,
pub water_intake_ml: Option<i32>,
pub urine_output_ml: Option<i32>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct UpdateVitalSignsReq {
pub record_date: Option<NaiveDate>,
pub systolic_bp_morning: Option<i32>,
pub diastolic_bp_morning: Option<i32>,
pub systolic_bp_evening: Option<i32>,
pub diastolic_bp_evening: Option<i32>,
pub heart_rate: Option<i32>,
pub weight: Option<Decimal>,
pub blood_sugar: Option<Decimal>,
pub water_intake_ml: Option<i32>,
pub urine_output_ml: Option<i32>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct VitalSignsResp {
pub id: Uuid,
pub patient_id: Uuid,
pub record_date: NaiveDate,
pub systolic_bp_morning: Option<i32>,
pub diastolic_bp_morning: Option<i32>,
pub systolic_bp_evening: Option<i32>,
pub diastolic_bp_evening: Option<i32>,
pub heart_rate: Option<i32>,
pub weight: Option<Decimal>,
pub blood_sugar: Option<Decimal>,
pub water_intake_ml: Option<i32>,
pub urine_output_ml: Option<i32>,
pub notes: Option<String>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub version: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct CreateLabReportReq {
pub report_date: NaiveDate,
pub report_type: String,
pub indicators: Option<serde_json::Value>,
pub image_urls: Option<serde_json::Value>,
pub doctor_interpretation: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct LabReportResp {
pub id: Uuid,
pub patient_id: Uuid,
pub report_date: NaiveDate,
pub report_type: String,
pub indicators: Option<serde_json::Value>,
pub image_urls: Option<serde_json::Value>,
pub doctor_interpretation: Option<String>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub version: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct CreateHealthRecordReq {
pub record_type: Option<String>,
pub record_date: NaiveDate,
pub source: Option<String>,
pub overall_assessment: Option<String>,
pub report_file_url: Option<String>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct HealthRecordResp {
pub id: Uuid,
pub patient_id: Uuid,
pub record_type: String,
pub record_date: NaiveDate,
pub source: Option<String>,
pub overall_assessment: Option<String>,
pub report_file_url: Option<String>,
pub notes: Option<String>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub version: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct TrendResp {
pub id: Uuid,
pub patient_id: Uuid,
pub period_start: NaiveDate,
pub period_end: NaiveDate,
pub indicator_summary: Option<serde_json::Value>,
pub abnormal_items: Option<serde_json::Value>,
pub generation_type: String,
pub report_file_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct IndicatorTimeseriesResp {
pub indicator: String,
pub data: Vec<(NaiveDate, f64)>,
}

View File

@@ -0,0 +1,4 @@
pub mod appointment_dto;
pub mod consultation_dto;
pub mod health_data_dto;
pub mod patient_dto;

View File

@@ -0,0 +1,93 @@
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct CreatePatientReq {
pub name: String,
pub gender: Option<String>,
pub birth_date: Option<NaiveDate>,
pub blood_type: Option<String>,
pub id_number: Option<String>,
pub allergy_history: Option<String>,
pub medical_history_summary: Option<String>,
pub emergency_contact_name: Option<String>,
pub emergency_contact_phone: Option<String>,
pub source: Option<String>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct UpdatePatientReq {
pub name: Option<String>,
pub gender: Option<String>,
pub birth_date: Option<NaiveDate>,
pub blood_type: Option<String>,
pub id_number: Option<String>,
pub allergy_history: Option<String>,
pub medical_history_summary: Option<String>,
pub emergency_contact_name: Option<String>,
pub emergency_contact_phone: Option<String>,
pub source: Option<String>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct PatientResp {
pub id: Uuid,
pub user_id: Option<Uuid>,
pub name: String,
pub gender: Option<String>,
pub birth_date: Option<NaiveDate>,
pub blood_type: Option<String>,
pub id_number: Option<String>,
pub allergy_history: Option<String>,
pub medical_history_summary: Option<String>,
pub emergency_contact_name: Option<String>,
pub emergency_contact_phone: Option<String>,
pub status: String,
pub verification_status: String,
pub source: Option<String>,
pub notes: Option<String>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub version: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct FamilyMemberReq {
pub name: String,
pub relationship: String,
pub phone: Option<String>,
pub birth_date: Option<NaiveDate>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct FamilyMemberResp {
pub id: Uuid,
pub patient_id: Uuid,
pub name: String,
pub relationship: String,
pub phone: Option<String>,
pub birth_date: Option<NaiveDate>,
pub notes: Option<String>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub version: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ManageTagsReq {
pub tag_ids: Vec<Uuid>,
}
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
pub struct PatientListQuery {
pub page: Option<u64>,
pub page_size: Option<u64>,
pub search: Option<String>,
pub tag_id: Option<Uuid>,
pub status: Option<String>,
}

View File

@@ -0,0 +1,55 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "appointment")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub tenant_id: Uuid,
pub patient_id: Uuid,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub doctor_id: Option<Uuid>,
pub appointment_type: String,
pub appointment_date: chrono::NaiveDate,
pub start_time: chrono::NaiveTime,
pub end_time: chrono::NaiveTime,
pub status: String,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub cancel_reason: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub created_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub updated_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<DateTimeUtc>,
pub version: i32,
}
#[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,
#[sea_orm(
belongs_to = "super::doctor_profile::Entity",
from = "Column::DoctorId",
to = "super::doctor_profile::Column::Id"
)]
Doctor,
}
impl Related<super::patient::Entity> for Entity {
fn to() -> RelationDef {
Relation::Patient.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,43 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "consultation_message")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub tenant_id: Uuid,
pub session_id: Uuid,
pub sender_id: Uuid,
pub sender_role: String,
pub content_type: String,
pub content: String,
pub is_read: bool,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub created_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub updated_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<DateTimeUtc>,
pub version: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::consultation_session::Entity",
from = "Column::SessionId",
to = "super::consultation_session::Column::Id"
)]
Session,
}
impl Related<super::consultation_session::Entity> for Entity {
fn to() -> RelationDef {
Relation::Session.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,61 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "consultation_session")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub tenant_id: Uuid,
pub patient_id: Uuid,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub doctor_id: Option<Uuid>,
#[sea_orm(rename = "type")]
pub consultation_type: String,
pub status: String,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub last_message_at: Option<DateTimeUtc>,
pub unread_count_patient: i32,
pub unread_count_doctor: i32,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub created_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub updated_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<DateTimeUtc>,
pub version: i32,
}
#[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,
#[sea_orm(
belongs_to = "super::doctor_profile::Entity",
from = "Column::DoctorId",
to = "super::doctor_profile::Column::Id"
)]
Doctor,
#[sea_orm(has_many = "super::consultation_message::Entity")]
Message,
}
impl Related<super::patient::Entity> for Entity {
fn to() -> RelationDef {
Relation::Patient.def()
}
}
impl Related<super::consultation_message::Entity> for Entity {
fn to() -> RelationDef {
Relation::Message.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,54 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "doctor_profile")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub tenant_id: Uuid,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub user_id: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub department: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub specialty: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub license_number: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub bio: Option<String>,
pub online_status: String,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub created_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub updated_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<DateTimeUtc>,
pub version: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::patient_doctor_relation::Entity")]
PatientRelation,
#[sea_orm(has_many = "super::doctor_schedule::Entity")]
Schedule,
}
impl Related<super::patient_doctor_relation::Entity> for Entity {
fn to() -> RelationDef {
Relation::PatientRelation.def()
}
}
impl Related<super::doctor_schedule::Entity> for Entity {
fn to() -> RelationDef {
Relation::Schedule.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,45 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "doctor_schedule")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub tenant_id: Uuid,
pub doctor_id: Uuid,
pub schedule_date: chrono::NaiveDate,
pub period_type: String,
pub start_time: chrono::NaiveTime,
pub end_time: chrono::NaiveTime,
pub max_appointments: i32,
pub current_appointments: i32,
pub status: String,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub created_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub updated_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<DateTimeUtc>,
pub version: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::doctor_profile::Entity",
from = "Column::DoctorId",
to = "super::doctor_profile::Column::Id"
)]
Doctor,
}
impl Related<super::doctor_profile::Entity> for Entity {
fn to() -> RelationDef {
Relation::Doctor.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,48 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "follow_up_record")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub tenant_id: Uuid,
pub task_id: Uuid,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub executed_by: Option<Uuid>,
pub executed_date: chrono::NaiveDate,
pub result: String,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub patient_condition: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub medical_advice: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub next_follow_up_date: Option<chrono::NaiveDate>,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub created_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub updated_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<DateTimeUtc>,
pub version: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::follow_up_task::Entity",
from = "Column::TaskId",
to = "super::follow_up_task::Column::Id"
)]
Task,
}
impl Related<super::follow_up_task::Entity> for Entity {
fn to() -> RelationDef {
Relation::Task.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,55 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "follow_up_task")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub tenant_id: Uuid,
pub patient_id: Uuid,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub assigned_to: Option<Uuid>,
pub follow_up_type: String,
pub planned_date: chrono::NaiveDate,
pub status: String,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub content_template: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub related_appointment_id: Option<Uuid>,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub created_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub updated_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<DateTimeUtc>,
pub version: i32,
}
#[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,
#[sea_orm(has_many = "super::follow_up_record::Entity")]
Record,
}
impl Related<super::patient::Entity> for Entity {
fn to() -> RelationDef {
Relation::Patient.def()
}
}
impl Related<super::follow_up_record::Entity> for Entity {
fn to() -> RelationDef {
Relation::Record.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,48 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "health_record")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub tenant_id: Uuid,
pub patient_id: Uuid,
pub record_type: String,
pub record_date: chrono::NaiveDate,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub overall_assessment: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub report_file_url: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub created_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub updated_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<DateTimeUtc>,
pub version: i32,
}
#[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()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,47 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "health_trend")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub tenant_id: Uuid,
pub patient_id: Uuid,
pub period_start: chrono::NaiveDate,
pub period_end: chrono::NaiveDate,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub indicator_summary: Option<serde_json::Value>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub abnormal_items: Option<serde_json::Value>,
pub generation_type: String,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub report_file_url: Option<String>,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub created_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub updated_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<DateTimeUtc>,
pub version: i32,
}
#[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()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,46 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "lab_report")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub tenant_id: Uuid,
pub patient_id: Uuid,
pub report_date: chrono::NaiveDate,
pub report_type: String,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub indicators: Option<serde_json::Value>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub image_urls: Option<serde_json::Value>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub doctor_interpretation: Option<String>,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub created_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub updated_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<DateTimeUtc>,
pub version: i32,
}
#[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()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,16 @@
pub mod appointment;
pub mod consultation_message;
pub mod consultation_session;
pub mod doctor_profile;
pub mod doctor_schedule;
pub mod follow_up_record;
pub mod follow_up_task;
pub mod health_record;
pub mod health_trend;
pub mod lab_report;
pub mod patient;
pub mod patient_doctor_relation;
pub mod patient_family_member;
pub mod patient_tag;
pub mod patient_tag_relation;
pub mod vital_signs;

View File

@@ -0,0 +1,74 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "patient")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
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 gender: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub birth_date: Option<chrono::NaiveDate>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub blood_type: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub id_number: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub allergy_history: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub medical_history_summary: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub emergency_contact_name: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub emergency_contact_phone: Option<String>,
pub status: String,
pub verification_status: String,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub created_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub updated_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<DateTimeUtc>,
pub version: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::patient_family_member::Entity")]
FamilyMember,
#[sea_orm(has_many = "super::patient_tag_relation::Entity")]
TagRelation,
#[sea_orm(has_many = "super::patient_doctor_relation::Entity")]
DoctorRelation,
#[sea_orm(has_many = "super::health_record::Entity")]
HealthRecord,
#[sea_orm(has_many = "super::vital_signs::Entity")]
VitalSigns,
#[sea_orm(has_many = "super::lab_report::Entity")]
LabReport,
#[sea_orm(has_many = "super::appointment::Entity")]
Appointment,
#[sea_orm(has_many = "super::follow_up_task::Entity")]
FollowUpTask,
#[sea_orm(has_many = "super::consultation_session::Entity")]
ConsultationSession,
}
impl Related<super::patient_family_member::Entity> for Entity {
fn to() -> RelationDef {
Relation::FamilyMember.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,51 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "patient_doctor_relation")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub tenant_id: Uuid,
pub patient_id: Uuid,
pub doctor_id: Uuid,
pub relationship_type: String,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub created_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub updated_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<DateTimeUtc>,
}
#[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,
#[sea_orm(
belongs_to = "super::doctor_profile::Entity",
from = "Column::DoctorId",
to = "super::doctor_profile::Column::Id"
)]
Doctor,
}
impl Related<super::patient::Entity> for Entity {
fn to() -> RelationDef {
Relation::Patient.def()
}
}
impl Related<super::doctor_profile::Entity> for Entity {
fn to() -> RelationDef {
Relation::Doctor.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,46 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "patient_family_member")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub tenant_id: Uuid,
pub patient_id: Uuid,
pub name: String,
pub relationship: String,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub phone: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub birth_date: Option<chrono::NaiveDate>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub created_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub updated_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<DateTimeUtc>,
pub version: i32,
}
#[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()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,39 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "patient_tag")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub tenant_id: Uuid,
pub name: String,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub is_system: bool,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub created_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub updated_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<DateTimeUtc>,
pub version: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::patient_tag_relation::Entity")]
TagRelation,
}
impl Related<super::patient_tag_relation::Entity> for Entity {
fn to() -> RelationDef {
Relation::TagRelation.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,50 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "patient_tag_relation")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub tenant_id: Uuid,
pub patient_id: Uuid,
pub tag_id: Uuid,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub created_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub updated_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<DateTimeUtc>,
}
#[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,
#[sea_orm(
belongs_to = "super::patient_tag::Entity",
from = "Column::TagId",
to = "super::patient_tag::Column::Id"
)]
Tag,
}
impl Related<super::patient::Entity> for Entity {
fn to() -> RelationDef {
Relation::Patient.def()
}
}
impl Related<super::patient_tag::Entity> for Entity {
fn to() -> RelationDef {
Relation::Tag.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,59 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "vital_signs")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub tenant_id: Uuid,
pub patient_id: Uuid,
pub record_date: chrono::NaiveDate,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub systolic_bp_morning: Option<i32>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub diastolic_bp_morning: Option<i32>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub systolic_bp_evening: Option<i32>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub diastolic_bp_evening: Option<i32>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub heart_rate: Option<i32>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub weight: Option<Decimal>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub blood_sugar: Option<Decimal>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub water_intake_ml: Option<i32>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub urine_output_ml: Option<i32>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub created_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub updated_by: Option<Uuid>,
#[sea_orm(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<DateTimeUtc>,
pub version: i32,
}
#[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()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,63 @@
use erp_core::error::AppError;
#[derive(Debug, thiserror::Error)]
pub enum HealthError {
#[error("{0}")]
Validation(String),
#[error("患者不存在")]
PatientNotFound,
#[error("医护档案不存在")]
DoctorNotFound,
#[error("预约不存在")]
AppointmentNotFound,
#[error("排班不存在")]
ScheduleNotFound,
#[error("排班已满,无法预约")]
ScheduleFull,
#[error("随访任务不存在")]
FollowUpTaskNotFound,
#[error("会话不存在")]
ConsultationNotFound,
#[error("状态转换无效: {0}")]
InvalidStatusTransition(String),
#[error("版本冲突: 数据已被其他操作修改,请刷新后重试")]
VersionMismatch,
#[error("数据库操作失败: {0}")]
DbError(String),
}
impl From<HealthError> for AppError {
fn from(err: HealthError) -> Self {
match err {
HealthError::Validation(s) => AppError::Validation(s),
HealthError::PatientNotFound
| HealthError::DoctorNotFound
| HealthError::AppointmentNotFound
| HealthError::ScheduleNotFound
| HealthError::FollowUpTaskNotFound
| HealthError::ConsultationNotFound => AppError::NotFound(err.to_string()),
HealthError::ScheduleFull => AppError::Validation(err.to_string()),
HealthError::InvalidStatusTransition(s) => AppError::Validation(s),
HealthError::VersionMismatch => AppError::VersionMismatch,
HealthError::DbError(_) => AppError::Internal(err.to_string()),
}
}
}
impl From<sea_orm::DbErr> for HealthError {
fn from(err: sea_orm::DbErr) -> Self {
HealthError::DbError(err.to_string())
}
}
pub type HealthResult<T> = Result<T, HealthError>;

View File

@@ -0,0 +1,7 @@
use erp_core::events::EventBus;
pub fn register_handlers(_bus: &EventBus) {
// Health 模块订阅的事件处理器
// - workflow.task.completed → 更新随访任务状态
// - message.sent → 联动咨询会话 last_message_at
}

View File

@@ -0,0 +1,226 @@
use axum::Extension;
use axum::extract::{FromRef, Json, Path, Query, State};
use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;
use erp_core::error::AppError;
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
use crate::state::HealthState;
// ---------------------------------------------------------------------------
// DTO — 预约排班
// ---------------------------------------------------------------------------
/// 预约列表查询参数
#[derive(Debug, Deserialize, IntoParams)]
pub struct AppointmentListParams {
pub page: Option<u64>,
pub page_size: Option<u64>,
pub patient_id: Option<Uuid>,
pub doctor_id: Option<Uuid>,
pub status: Option<String>,
pub date: Option<String>,
}
/// 创建预约请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct CreateAppointmentReq {
pub patient_id: Uuid,
pub doctor_id: Uuid,
pub schedule_id: Uuid,
pub appointment_date: String,
pub start_time: String,
pub end_time: String,
pub reason: Option<String>,
}
/// 更新预约状态请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateAppointmentStatusReq {
pub status: String,
pub cancel_reason: Option<String>,
}
/// 预约响应
#[derive(Debug, Serialize, ToSchema)]
pub struct AppointmentResp {
pub id: Uuid,
pub patient_id: Uuid,
pub doctor_id: Uuid,
pub schedule_id: Uuid,
pub appointment_date: String,
pub start_time: String,
pub end_time: String,
pub status: String,
pub reason: Option<String>,
pub cancel_reason: Option<String>,
pub created_at: String,
pub updated_at: String,
}
/// 排班列表查询参数
#[derive(Debug, Deserialize, IntoParams)]
pub struct ScheduleListParams {
pub page: Option<u64>,
pub page_size: Option<u64>,
pub doctor_id: Option<Uuid>,
pub start_date: Option<String>,
pub end_date: Option<String>,
}
/// 创建排班请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct CreateScheduleReq {
pub doctor_id: Uuid,
pub schedule_date: String,
pub start_time: String,
pub end_time: String,
pub max_appointments: i32,
pub slot_duration_minutes: Option<i32>,
}
/// 更新排班请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateScheduleReq {
pub start_time: Option<String>,
pub end_time: Option<String>,
pub max_appointments: Option<i32>,
pub slot_duration_minutes: Option<i32>,
pub version: i32,
}
/// 排班响应
#[derive(Debug, Serialize, ToSchema)]
pub struct ScheduleResp {
pub id: Uuid,
pub doctor_id: Uuid,
pub schedule_date: String,
pub start_time: String,
pub end_time: String,
pub max_appointments: i32,
pub current_appointments: i32,
pub slot_duration_minutes: Option<i32>,
pub created_at: String,
pub updated_at: String,
}
/// 日历视图查询参数
#[derive(Debug, Deserialize, IntoParams)]
pub struct CalendarViewParams {
pub doctor_id: Option<Uuid>,
pub start_date: String,
pub end_date: String,
}
/// 日历视图单个日期条目
#[derive(Debug, Serialize, ToSchema)]
pub struct CalendarDayEntry {
pub date: String,
pub schedules: Vec<ScheduleResp>,
pub appointments: Vec<AppointmentResp>,
}
/// 日历视图响应
#[derive(Debug, Serialize, ToSchema)]
pub struct CalendarViewResp {
pub days: Vec<CalendarDayEntry>,
}
// ---------------------------------------------------------------------------
// Handler — 预约排班 (7 个端点)
// ---------------------------------------------------------------------------
/// GET /api/v1/health/appointments — 预约列表
pub async fn list_appointments<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Query(_params): Query<AppointmentListParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<AppointmentResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// POST /api/v1/health/appointments — 创建预约
pub async fn create_appointment<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Json(_req): Json<CreateAppointmentReq>,
) -> Result<Json<ApiResponse<AppointmentResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// PUT /api/v1/health/appointments/{id}/status — 更新预约状态
pub async fn update_appointment_status<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
Json(_req): Json<UpdateAppointmentStatusReq>,
) -> Result<Json<ApiResponse<AppointmentResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// GET /api/v1/health/schedules — 排班列表
pub async fn list_schedules<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Query(_params): Query<ScheduleListParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<ScheduleResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// POST /api/v1/health/schedules — 创建排班
pub async fn create_schedule<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Json(_req): Json<CreateScheduleReq>,
) -> Result<Json<ApiResponse<ScheduleResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// PUT /api/v1/health/schedules/{id} — 更新排班
pub async fn update_schedule<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
Json(_req): Json<UpdateScheduleReq>,
) -> Result<Json<ApiResponse<ScheduleResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// GET /api/v1/health/calendar — 日历视图
pub async fn calendar_view<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Query(_params): Query<CalendarViewParams>,
) -> Result<Json<ApiResponse<CalendarViewResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}

View File

@@ -0,0 +1,142 @@
use axum::Extension;
use axum::extract::{FromRef, Json, Path, Query, State};
use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;
use erp_core::error::AppError;
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
use crate::state::HealthState;
// ---------------------------------------------------------------------------
// DTO — 咨询管理
// ---------------------------------------------------------------------------
/// 会话列表查询参数
#[derive(Debug, Deserialize, IntoParams)]
pub struct SessionListParams {
pub page: Option<u64>,
pub page_size: Option<u64>,
pub patient_id: Option<Uuid>,
pub doctor_id: Option<Uuid>,
pub status: Option<String>,
}
/// 会话响应
#[derive(Debug, Serialize, ToSchema)]
pub struct ConsultationSessionResp {
pub id: Uuid,
pub patient_id: Uuid,
pub doctor_id: Uuid,
pub subject: String,
pub status: String,
pub created_at: String,
pub updated_at: String,
}
/// 消息列表查询参数
#[derive(Debug, Deserialize, IntoParams)]
pub struct MessageListParams {
pub page: Option<u64>,
pub page_size: Option<u64>,
}
/// 创建消息请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct CreateConsultationMessageReq {
pub content: String,
pub message_type: Option<String>,
}
/// 消息响应
#[derive(Debug, Serialize, ToSchema)]
pub struct ConsultationMessageResp {
pub id: Uuid,
pub session_id: Uuid,
pub sender_id: Uuid,
pub sender_type: String,
pub content: String,
pub message_type: String,
pub created_at: String,
}
/// 导出会话请求
#[derive(Debug, Deserialize, IntoParams)]
pub struct ExportSessionsParams {
pub patient_id: Option<Uuid>,
pub doctor_id: Option<Uuid>,
pub start_date: Option<String>,
pub end_date: Option<String>,
}
// ---------------------------------------------------------------------------
// Handler — 咨询管理 (5 个端点)
// ---------------------------------------------------------------------------
/// GET /api/v1/health/consultations/sessions — 会话列表
pub async fn list_sessions<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Query(_params): Query<SessionListParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<ConsultationSessionResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// GET /api/v1/health/consultations/sessions/{id}/messages — 消息列表
pub async fn list_messages<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_session_id): Path<Uuid>,
Query(_params): Query<MessageListParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<ConsultationMessageResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// PUT /api/v1/health/consultations/sessions/{id}/close — 关闭会话
pub async fn close_session<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
) -> Result<Json<ApiResponse<ConsultationSessionResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// POST /api/v1/health/consultations/sessions/{id}/messages — 创建消息
pub async fn create_message<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_session_id): Path<Uuid>,
Json(_req): Json<CreateConsultationMessageReq>,
) -> Result<Json<ApiResponse<ConsultationMessageResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// GET /api/v1/health/consultations/export — 导出会话
pub async fn export_sessions<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Query(_params): Query<ExportSessionsParams>,
) -> Result<Json<ApiResponse<Vec<ConsultationSessionResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}

View File

@@ -0,0 +1,136 @@
use axum::Extension;
use axum::extract::{FromRef, Json, Path, Query, State};
use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;
use erp_core::error::AppError;
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
use crate::state::HealthState;
// ---------------------------------------------------------------------------
// DTO — 医护管理
// ---------------------------------------------------------------------------
/// 医护列表查询参数
#[derive(Debug, Deserialize, IntoParams)]
pub struct DoctorListParams {
pub page: Option<u64>,
pub page_size: Option<u64>,
/// 按姓名模糊搜索
pub search: Option<String>,
/// 按科室筛选
pub department: Option<String>,
/// 按职称筛选
pub title: Option<String>,
}
/// 创建医护档案请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct CreateDoctorReq {
pub user_id: Uuid,
pub name: String,
pub department: Option<String>,
pub title: Option<String>,
pub specialty: Option<String>,
pub license_number: Option<String>,
pub bio: Option<String>,
}
/// 更新医护档案请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateDoctorReq {
pub name: Option<String>,
pub department: Option<String>,
pub title: Option<String>,
pub specialty: Option<String>,
pub license_number: Option<String>,
pub bio: Option<String>,
pub version: i32,
}
/// 医护档案响应
#[derive(Debug, Serialize, ToSchema)]
pub struct DoctorResp {
pub id: Uuid,
pub user_id: Uuid,
pub name: String,
pub department: Option<String>,
pub title: Option<String>,
pub specialty: Option<String>,
pub license_number: Option<String>,
pub bio: Option<String>,
pub created_at: String,
pub updated_at: String,
}
// ---------------------------------------------------------------------------
// Handler — 医护管理 (5 个端点)
// ---------------------------------------------------------------------------
/// GET /api/v1/health/doctors — 医护档案列表
pub async fn list_doctors<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Query(_params): Query<DoctorListParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<DoctorResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// POST /api/v1/health/doctors — 创建医护档案
pub async fn create_doctor<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Json(_req): Json<CreateDoctorReq>,
) -> Result<Json<ApiResponse<DoctorResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// GET /api/v1/health/doctors/{id} — 获取医护档案详情
pub async fn get_doctor<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
) -> Result<Json<ApiResponse<DoctorResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// PUT /api/v1/health/doctors/{id} — 更新医护档案
pub async fn update_doctor<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
Json(_req): Json<UpdateDoctorReq>,
) -> Result<Json<ApiResponse<DoctorResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// DELETE /api/v1/health/doctors/{id} — 删除医护档案
pub async fn delete_doctor<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}

View File

@@ -0,0 +1,178 @@
use axum::Extension;
use axum::extract::{FromRef, Json, Path, Query, State};
use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;
use erp_core::error::AppError;
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
use crate::state::HealthState;
// ---------------------------------------------------------------------------
// DTO — 随访管理
// ---------------------------------------------------------------------------
/// 随访任务列表查询参数
#[derive(Debug, Deserialize, IntoParams)]
pub struct FollowUpTaskListParams {
pub page: Option<u64>,
pub page_size: Option<u64>,
pub patient_id: Option<Uuid>,
pub assigned_to: Option<Uuid>,
pub status: Option<String>,
}
/// 创建随访任务请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct CreateFollowUpTaskReq {
pub patient_id: Uuid,
pub task_type: String,
pub title: String,
pub description: Option<String>,
pub due_date: String,
pub assigned_to: Option<Uuid>,
}
/// 更新随访任务请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateFollowUpTaskReq {
pub task_type: Option<String>,
pub title: Option<String>,
pub description: Option<String>,
pub due_date: Option<String>,
pub assigned_to: Option<Uuid>,
pub status: Option<String>,
pub version: i32,
}
/// 随访任务响应
#[derive(Debug, Serialize, ToSchema)]
pub struct FollowUpTaskResp {
pub id: Uuid,
pub patient_id: Uuid,
pub task_type: String,
pub title: String,
pub description: Option<String>,
pub due_date: String,
pub assigned_to: Option<Uuid>,
pub status: String,
pub created_at: String,
pub updated_at: String,
}
/// 创建随访记录请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct CreateFollowUpRecordReq {
pub task_id: Uuid,
pub contact_method: String,
pub content: String,
pub outcome: Option<String>,
pub next_follow_up_date: Option<String>,
}
/// 随访记录列表查询参数
#[derive(Debug, Deserialize, IntoParams)]
pub struct FollowUpRecordListParams {
pub page: Option<u64>,
pub page_size: Option<u64>,
pub task_id: Option<Uuid>,
pub patient_id: Option<Uuid>,
}
/// 随访记录响应
#[derive(Debug, Serialize, ToSchema)]
pub struct FollowUpRecordResp {
pub id: Uuid,
pub task_id: Uuid,
pub patient_id: Uuid,
pub contact_method: String,
pub content: String,
pub outcome: Option<String>,
pub next_follow_up_date: Option<String>,
pub created_by: Uuid,
pub created_at: String,
}
// ---------------------------------------------------------------------------
// Handler — 随访管理 (6 个端点)
// ---------------------------------------------------------------------------
/// GET /api/v1/health/follow-up/tasks — 随访任务列表
pub async fn list_tasks<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Query(_params): Query<FollowUpTaskListParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<FollowUpTaskResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// POST /api/v1/health/follow-up/tasks — 创建随访任务
pub async fn create_task<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Json(_req): Json<CreateFollowUpTaskReq>,
) -> Result<Json<ApiResponse<FollowUpTaskResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// PUT /api/v1/health/follow-up/tasks/{id} — 更新随访任务
pub async fn update_task<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
Json(_req): Json<UpdateFollowUpTaskReq>,
) -> Result<Json<ApiResponse<FollowUpTaskResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// DELETE /api/v1/health/follow-up/tasks/{id} — 删除随访任务
pub async fn delete_task<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// POST /api/v1/health/follow-up/records — 创建随访记录
pub async fn create_record<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Json(_req): Json<CreateFollowUpRecordReq>,
) -> Result<Json<ApiResponse<FollowUpRecordResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// GET /api/v1/health/follow-up/records — 随访记录列表
pub async fn list_records<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Query(_params): Query<FollowUpRecordListParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<FollowUpRecordResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}

View File

@@ -0,0 +1,408 @@
use axum::Extension;
use axum::extract::{FromRef, Json, Path, Query, State};
use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;
use erp_core::error::AppError;
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
use crate::state::HealthState;
// ---------------------------------------------------------------------------
// DTO — 健康数据
// ---------------------------------------------------------------------------
/// 生命体征列表查询参数
#[derive(Debug, Deserialize, IntoParams)]
pub struct VitalSignsListParams {
pub page: Option<u64>,
pub page_size: Option<u64>,
pub patient_id: Uuid,
}
/// 创建生命体征请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct CreateVitalSignsReq {
pub patient_id: Uuid,
pub blood_pressure_systolic: Option<i32>,
pub blood_pressure_diastolic: Option<i32>,
pub heart_rate: Option<i32>,
pub temperature: Option<f64>,
pub blood_oxygen: Option<i32>,
pub weight: Option<f64>,
pub height: Option<f64>,
pub measured_at: Option<String>,
pub notes: Option<String>,
}
/// 更新生命体征请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateVitalSignsReq {
pub blood_pressure_systolic: Option<i32>,
pub blood_pressure_diastolic: Option<i32>,
pub heart_rate: Option<i32>,
pub temperature: Option<f64>,
pub blood_oxygen: Option<i32>,
pub weight: Option<f64>,
pub height: Option<f64>,
pub measured_at: Option<String>,
pub notes: Option<String>,
pub version: i32,
}
/// 生命体征响应
#[derive(Debug, Serialize, ToSchema)]
pub struct VitalSignsResp {
pub id: Uuid,
pub patient_id: Uuid,
pub blood_pressure_systolic: Option<i32>,
pub blood_pressure_diastolic: Option<i32>,
pub heart_rate: Option<i32>,
pub temperature: Option<f64>,
pub blood_oxygen: Option<i32>,
pub weight: Option<f64>,
pub height: Option<f64>,
pub measured_at: Option<String>,
pub notes: Option<String>,
pub created_at: String,
pub updated_at: String,
}
/// 化验报告列表查询参数
#[derive(Debug, Deserialize, IntoParams)]
pub struct LabReportListParams {
pub page: Option<u64>,
pub page_size: Option<u64>,
pub patient_id: Option<Uuid>,
}
/// 创建化验报告请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct CreateLabReportReq {
pub patient_id: Uuid,
pub report_type: String,
pub report_date: String,
pub indicators: serde_json::Value,
pub file_url: Option<String>,
pub notes: Option<String>,
}
/// 更新化验报告请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateLabReportReq {
pub report_type: Option<String>,
pub report_date: Option<String>,
pub indicators: Option<serde_json::Value>,
pub file_url: Option<String>,
pub notes: Option<String>,
pub version: i32,
}
/// 化验报告响应
#[derive(Debug, Serialize, ToSchema)]
pub struct LabReportResp {
pub id: Uuid,
pub patient_id: Uuid,
pub report_type: String,
pub report_date: String,
pub indicators: serde_json::Value,
pub file_url: Option<String>,
pub notes: Option<String>,
pub created_at: String,
pub updated_at: String,
}
/// 健康档案列表查询参数
#[derive(Debug, Deserialize, IntoParams)]
pub struct HealthRecordListParams {
pub page: Option<u64>,
pub page_size: Option<u64>,
pub patient_id: Option<Uuid>,
}
/// 创建健康档案请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct CreateHealthRecordReq {
pub patient_id: Uuid,
pub record_type: String,
pub title: String,
pub content: serde_json::Value,
pub record_date: String,
}
/// 更新健康档案请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateHealthRecordReq {
pub record_type: Option<String>,
pub title: Option<String>,
pub content: Option<serde_json::Value>,
pub record_date: Option<String>,
pub version: i32,
}
/// 健康档案响应
#[derive(Debug, Serialize, ToSchema)]
pub struct HealthRecordResp {
pub id: Uuid,
pub patient_id: Uuid,
pub record_type: String,
pub title: String,
pub content: serde_json::Value,
pub record_date: String,
pub created_at: String,
pub updated_at: String,
}
/// 趋势分析列表查询参数
#[derive(Debug, Deserialize, IntoParams)]
pub struct TrendListParams {
pub patient_id: Option<Uuid>,
}
/// 生成趋势请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct GenerateTrendReq {
pub patient_id: Uuid,
pub indicator_name: String,
pub start_date: String,
pub end_date: String,
}
/// 趋势分析响应
#[derive(Debug, Serialize, ToSchema)]
pub struct TrendResp {
pub id: Uuid,
pub patient_id: Uuid,
pub indicator_name: String,
pub trend_data: serde_json::Value,
pub analysis_summary: Option<String>,
pub generated_at: String,
}
/// 指标时间序列查询参数
#[derive(Debug, Deserialize, IntoParams)]
pub struct IndicatorTimeseriesParams {
pub patient_id: Uuid,
pub indicator_name: String,
pub start_date: Option<String>,
pub end_date: Option<String>,
}
/// 指标时间序列数据点
#[derive(Debug, Serialize, ToSchema)]
pub struct TimeseriesDataPoint {
pub date: String,
pub value: f64,
pub unit: Option<String>,
}
/// 指标时间序列响应
#[derive(Debug, Serialize, ToSchema)]
pub struct IndicatorTimeseriesResp {
pub indicator_name: String,
pub patient_id: Uuid,
pub data_points: Vec<TimeseriesDataPoint>,
}
// ---------------------------------------------------------------------------
// Handler — 健康数据 (15 个端点)
// ---------------------------------------------------------------------------
/// GET /api/v1/health/vital-signs — 生命体征列表
pub async fn list_vital_signs<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Query(_params): Query<VitalSignsListParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<VitalSignsResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// POST /api/v1/health/vital-signs — 创建生命体征记录
pub async fn create_vital_signs<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Json(_req): Json<CreateVitalSignsReq>,
) -> Result<Json<ApiResponse<VitalSignsResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// PUT /api/v1/health/vital-signs/{id} — 更新生命体征记录
pub async fn update_vital_signs<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
Json(_req): Json<UpdateVitalSignsReq>,
) -> Result<Json<ApiResponse<VitalSignsResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// DELETE /api/v1/health/vital-signs/{id} — 删除生命体征记录
pub async fn delete_vital_signs<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// GET /api/v1/health/lab-reports — 化验报告列表
pub async fn list_lab_reports<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Query(_params): Query<LabReportListParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<LabReportResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// POST /api/v1/health/lab-reports — 创建化验报告
pub async fn create_lab_report<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Json(_req): Json<CreateLabReportReq>,
) -> Result<Json<ApiResponse<LabReportResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// PUT /api/v1/health/lab-reports/{id} — 更新化验报告
pub async fn update_lab_report<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
Json(_req): Json<UpdateLabReportReq>,
) -> Result<Json<ApiResponse<LabReportResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// DELETE /api/v1/health/lab-reports/{id} — 删除化验报告
pub async fn delete_lab_report<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// GET /api/v1/health/records — 健康档案列表
pub async fn list_health_records<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Query(_params): Query<HealthRecordListParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<HealthRecordResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// POST /api/v1/health/records — 创建健康档案
pub async fn create_health_record<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Json(_req): Json<CreateHealthRecordReq>,
) -> Result<Json<ApiResponse<HealthRecordResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// PUT /api/v1/health/records/{id} — 更新健康档案
pub async fn update_health_record<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
Json(_req): Json<UpdateHealthRecordReq>,
) -> Result<Json<ApiResponse<HealthRecordResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// DELETE /api/v1/health/records/{id} — 删除健康档案
pub async fn delete_health_record<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// GET /api/v1/health/trends — 趋势分析列表
pub async fn list_trends<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Query(_params): Query<TrendListParams>,
) -> Result<Json<ApiResponse<Vec<TrendResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// POST /api/v1/health/trends/generate — 生成趋势分析
pub async fn generate_trend<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Json(_req): Json<GenerateTrendReq>,
) -> Result<Json<ApiResponse<TrendResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// GET /api/v1/health/trends/timeseries — 获取指标时间序列
pub async fn get_indicator_timeseries<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Query(_params): Query<IndicatorTimeseriesParams>,
) -> Result<Json<ApiResponse<IndicatorTimeseriesResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}

View File

@@ -0,0 +1,6 @@
pub mod appointment_handler;
pub mod consultation_handler;
pub mod doctor_handler;
pub mod follow_up_handler;
pub mod health_data_handler;
pub mod patient_handler;

View File

@@ -0,0 +1,311 @@
use axum::Extension;
use axum::extract::{FromRef, Json, Path, Query, State};
use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;
use erp_core::error::AppError;
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
use crate::state::HealthState;
// ---------------------------------------------------------------------------
// DTO — 患者管理
// ---------------------------------------------------------------------------
/// 患者列表查询参数
#[derive(Debug, Deserialize, IntoParams)]
pub struct PatientListParams {
pub page: Option<u64>,
pub page_size: Option<u64>,
/// 按姓名/身份证号模糊搜索
pub search: Option<String>,
/// 按标签筛选
pub tag_id: Option<Uuid>,
/// 按负责医生筛选
pub doctor_id: Option<Uuid>,
}
/// 创建患者请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct CreatePatientReq {
pub name: String,
pub id_card: Option<String>,
pub phone: Option<String>,
pub gender: Option<String>,
pub birth_date: Option<String>,
pub address: Option<String>,
pub emergency_contact: Option<String>,
pub emergency_phone: Option<String>,
pub medical_notes: Option<String>,
}
/// 更新患者请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdatePatientReq {
pub name: Option<String>,
pub id_card: Option<String>,
pub phone: Option<String>,
pub gender: Option<String>,
pub birth_date: Option<String>,
pub address: Option<String>,
pub emergency_contact: Option<String>,
pub emergency_phone: Option<String>,
pub medical_notes: Option<String>,
pub version: i32,
}
/// 患者标签管理请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct ManagePatientTagsReq {
pub tag_ids: Vec<Uuid>,
}
/// 患者响应
#[derive(Debug, Serialize, ToSchema)]
pub struct PatientResp {
pub id: Uuid,
pub name: String,
pub id_card: Option<String>,
pub phone: Option<String>,
pub gender: Option<String>,
pub birth_date: Option<String>,
pub address: Option<String>,
pub emergency_contact: Option<String>,
pub emergency_phone: Option<String>,
pub medical_notes: Option<String>,
pub tags: Vec<PatientTagResp>,
pub created_at: String,
pub updated_at: String,
}
/// 患者标签响应
#[derive(Debug, Serialize, ToSchema)]
pub struct PatientTagResp {
pub id: Uuid,
pub name: String,
pub color: Option<String>,
}
/// 健康摘要响应
#[derive(Debug, Serialize, ToSchema)]
pub struct HealthSummaryResp {
pub patient_id: Uuid,
pub latest_vital_signs: Option<serde_json::Value>,
pub latest_lab_report: Option<serde_json::Value>,
pub upcoming_appointments: u64,
pub pending_follow_ups: u64,
}
/// 家庭成员请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct CreateFamilyMemberReq {
pub name: String,
pub relationship: String,
pub phone: Option<String>,
pub id_card: Option<String>,
}
/// 更新家庭成员请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateFamilyMemberReq {
pub name: Option<String>,
pub relationship: Option<String>,
pub phone: Option<String>,
pub id_card: Option<String>,
}
/// 家庭成员响应
#[derive(Debug, Serialize, ToSchema)]
pub struct FamilyMemberResp {
pub id: Uuid,
pub patient_id: Uuid,
pub name: String,
pub relationship: String,
pub phone: Option<String>,
pub id_card: Option<String>,
}
/// 分配医生请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct AssignDoctorReq {
pub doctor_id: Uuid,
}
// ---------------------------------------------------------------------------
// Handler — 患者管理 (13 个端点)
// ---------------------------------------------------------------------------
/// GET /api/v1/health/patients — 患者列表
pub async fn list_patients<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Query(_params): Query<PatientListParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<PatientResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// POST /api/v1/health/patients — 创建患者
pub async fn create_patient<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Json(_req): Json<CreatePatientReq>,
) -> Result<Json<ApiResponse<PatientResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// GET /api/v1/health/patients/{id} — 获取患者详情
pub async fn get_patient<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
) -> Result<Json<ApiResponse<PatientResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// PUT /api/v1/health/patients/{id} — 更新患者
pub async fn update_patient<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
Json(_req): Json<UpdatePatientReq>,
) -> Result<Json<ApiResponse<PatientResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// DELETE /api/v1/health/patients/{id} — 删除患者(软删除)
pub async fn delete_patient<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// POST /api/v1/health/patients/{id}/tags — 管理患者标签
pub async fn manage_patient_tags<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
Json(_req): Json<ManagePatientTagsReq>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// GET /api/v1/health/patients/{id}/health-summary — 获取患者健康摘要
pub async fn get_health_summary<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
) -> Result<Json<ApiResponse<HealthSummaryResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// GET /api/v1/health/patients/{id}/family-members — 家庭成员列表
pub async fn list_family_members<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
) -> Result<Json<ApiResponse<Vec<FamilyMemberResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// POST /api/v1/health/patients/{id}/family-members — 创建家庭成员
pub async fn create_family_member<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
Json(_req): Json<CreateFamilyMemberReq>,
) -> Result<Json<ApiResponse<FamilyMemberResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// PUT /api/v1/health/patients/{patient_id}/family-members/{member_id} — 更新家庭成员
pub async fn update_family_member<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path((_patient_id, _member_id)): Path<(Uuid, Uuid)>,
Json(_req): Json<UpdateFamilyMemberReq>,
) -> Result<Json<ApiResponse<FamilyMemberResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// DELETE /api/v1/health/patients/{patient_id}/family-members/{member_id} — 删除家庭成员
pub async fn delete_family_member<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path((_patient_id, _member_id)): Path<(Uuid, Uuid)>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// POST /api/v1/health/patients/{id}/doctors — 分配负责医生
pub async fn assign_doctor<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path(_id): Path<Uuid>,
Json(_req): Json<AssignDoctorReq>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}
/// DELETE /api/v1/health/patients/{patient_id}/doctors/{doctor_id} — 移除负责医生
pub async fn remove_doctor<S>(
State(_state): State<HealthState>,
Extension(_ctx): Extension<TenantContext>,
Path((_patient_id, _doctor_id)): Path<(Uuid, Uuid)>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Err(AppError::Internal("Not implemented yet".into()))
}

View 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::HealthModule;
pub use state::HealthState;

View File

@@ -0,0 +1,322 @@
use axum::Router;
use uuid::Uuid;
use erp_core::error::AppResult;
use erp_core::events::EventBus;
use erp_core::module::{ErpModule, PermissionDescriptor};
use crate::handler::{
appointment_handler, consultation_handler, doctor_handler, follow_up_handler,
health_data_handler, patient_handler,
};
pub struct HealthModule;
impl HealthModule {
pub fn new() -> Self {
Self
}
pub fn public_routes<S>() -> Router<S>
where
crate::state::HealthState: axum::extract::FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Router::new()
}
pub fn protected_routes<S>() -> Router<S>
where
crate::state::HealthState: axum::extract::FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Router::new()
// 患者管理
.route(
"/health/patients",
axum::routing::get(patient_handler::list_patients)
.post(patient_handler::create_patient),
)
.route(
"/health/patients/{id}",
axum::routing::get(patient_handler::get_patient)
.put(patient_handler::update_patient)
.delete(patient_handler::delete_patient),
)
.route(
"/health/patients/{id}/tags",
axum::routing::post(patient_handler::manage_patient_tags),
)
.route(
"/health/patients/{id}/health-summary",
axum::routing::get(patient_handler::get_health_summary),
)
.route(
"/health/patients/{id}/family-members",
axum::routing::get(patient_handler::list_family_members)
.post(patient_handler::create_family_member),
)
.route(
"/health/patients/{id}/family-members/{fid}",
axum::routing::put(patient_handler::update_family_member)
.delete(patient_handler::delete_family_member),
)
.route(
"/health/patients/{id}/doctors",
axum::routing::post(patient_handler::assign_doctor),
)
.route(
"/health/patients/{id}/doctors/{did}",
axum::routing::delete(patient_handler::remove_doctor),
)
// 健康数据
.route(
"/health/patients/{id}/vital-signs",
axum::routing::get(health_data_handler::list_vital_signs)
.post(health_data_handler::create_vital_signs),
)
.route(
"/health/patients/{id}/vital-signs/{vid}",
axum::routing::put(health_data_handler::update_vital_signs)
.delete(health_data_handler::delete_vital_signs),
)
.route(
"/health/patients/{id}/lab-reports",
axum::routing::get(health_data_handler::list_lab_reports)
.post(health_data_handler::create_lab_report),
)
.route(
"/health/patients/{id}/lab-reports/{rid}",
axum::routing::put(health_data_handler::update_lab_report)
.delete(health_data_handler::delete_lab_report),
)
.route(
"/health/patients/{id}/health-records",
axum::routing::get(health_data_handler::list_health_records)
.post(health_data_handler::create_health_record),
)
.route(
"/health/patients/{id}/health-records/{rid}",
axum::routing::put(health_data_handler::update_health_record)
.delete(health_data_handler::delete_health_record),
)
.route(
"/health/patients/{id}/trends",
axum::routing::get(health_data_handler::list_trends),
)
.route(
"/health/patients/{id}/trends/generate",
axum::routing::post(health_data_handler::generate_trend),
)
.route(
"/health/patients/{id}/trends/{indicator}",
axum::routing::get(health_data_handler::get_indicator_timeseries),
)
// 预约排班
.route(
"/health/appointments",
axum::routing::get(appointment_handler::list_appointments)
.post(appointment_handler::create_appointment),
)
.route(
"/health/appointments/{id}/status",
axum::routing::put(appointment_handler::update_appointment_status),
)
.route(
"/health/doctor-schedules",
axum::routing::get(appointment_handler::list_schedules)
.post(appointment_handler::create_schedule),
)
.route(
"/health/doctor-schedules/{id}",
axum::routing::put(appointment_handler::update_schedule),
)
.route(
"/health/doctor-schedules/calendar",
axum::routing::get(appointment_handler::calendar_view),
)
// 随访管理
.route(
"/health/follow-up-tasks",
axum::routing::get(follow_up_handler::list_tasks)
.post(follow_up_handler::create_task),
)
.route(
"/health/follow-up-tasks/{id}",
axum::routing::put(follow_up_handler::update_task)
.delete(follow_up_handler::delete_task),
)
.route(
"/health/follow-up-tasks/{id}/records",
axum::routing::post(follow_up_handler::create_record),
)
.route(
"/health/follow-up-records",
axum::routing::get(follow_up_handler::list_records),
)
// 咨询管理
.route(
"/health/consultation-sessions",
axum::routing::get(consultation_handler::list_sessions),
)
.route(
"/health/consultation-sessions/{id}/messages",
axum::routing::get(consultation_handler::list_messages),
)
.route(
"/health/consultation-sessions/{id}/close",
axum::routing::put(consultation_handler::close_session),
)
.route(
"/health/consultation-messages",
axum::routing::post(consultation_handler::create_message),
)
.route(
"/health/consultation-sessions/export",
axum::routing::get(consultation_handler::export_sessions),
)
// 医护管理
.route(
"/health/doctors",
axum::routing::get(doctor_handler::list_doctors)
.post(doctor_handler::create_doctor),
)
.route(
"/health/doctors/{id}",
axum::routing::get(doctor_handler::get_doctor)
.put(doctor_handler::update_doctor)
.delete(doctor_handler::delete_doctor),
)
}
}
impl Default for HealthModule {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl ErpModule for HealthModule {
fn name(&self) -> &str {
"health"
}
fn version(&self) -> &str {
env!("CARGO_PKG_VERSION")
}
fn dependencies(&self) -> Vec<&str> {
vec!["auth"]
}
fn register_event_handlers(&self, bus: &EventBus) {
crate::event::register_handlers(bus);
}
async fn on_tenant_created(
&self,
tenant_id: Uuid,
db: &sea_orm::DatabaseConnection,
_event_bus: &EventBus,
) -> AppResult<()> {
crate::service::seed::seed_tenant_health(db, tenant_id)
.await
.map_err(|e| erp_core::error::AppError::Internal(e.to_string()))?;
tracing::info!(tenant_id = %tenant_id, "Health module tenant initialized");
Ok(())
}
async fn on_tenant_deleted(
&self,
tenant_id: Uuid,
db: &sea_orm::DatabaseConnection,
) -> AppResult<()> {
crate::service::seed::soft_delete_tenant_data(db, tenant_id)
.await
.map_err(|e| erp_core::error::AppError::Internal(e.to_string()))?;
tracing::info!(tenant_id = %tenant_id, "Health module tenant data soft-deleted");
Ok(())
}
fn permissions(&self) -> Vec<PermissionDescriptor> {
vec![
PermissionDescriptor {
code: "health.patient.list".into(),
name: "查看患者列表".into(),
description: "查看和搜索患者列表、详情".into(),
module: "health".into(),
},
PermissionDescriptor {
code: "health.patient.manage".into(),
name: "管理患者".into(),
description: "创建、编辑、删除患者".into(),
module: "health".into(),
},
PermissionDescriptor {
code: "health.health-data.list".into(),
name: "查看健康数据".into(),
description: "查看体检记录、监测数据、化验报告".into(),
module: "health".into(),
},
PermissionDescriptor {
code: "health.health-data.manage".into(),
name: "管理健康数据".into(),
description: "录入、编辑、删除健康数据".into(),
module: "health".into(),
},
PermissionDescriptor {
code: "health.appointment.list".into(),
name: "查看预约".into(),
description: "查看预约列表和排班".into(),
module: "health".into(),
},
PermissionDescriptor {
code: "health.appointment.manage".into(),
name: "管理预约".into(),
description: "创建、确认、取消预约".into(),
module: "health".into(),
},
PermissionDescriptor {
code: "health.follow-up.list".into(),
name: "查看随访".into(),
description: "查看随访任务和记录".into(),
module: "health".into(),
},
PermissionDescriptor {
code: "health.follow-up.manage".into(),
name: "管理随访".into(),
description: "创建、分配、完成随访任务".into(),
module: "health".into(),
},
PermissionDescriptor {
code: "health.consultation.list".into(),
name: "查看咨询".into(),
description: "查看咨询会话和消息记录".into(),
module: "health".into(),
},
PermissionDescriptor {
code: "health.consultation.manage".into(),
name: "管理咨询".into(),
description: "关闭会话、导出记录".into(),
module: "health".into(),
},
PermissionDescriptor {
code: "health.doctor.list".into(),
name: "查看医护".into(),
description: "查看医护列表和详情".into(),
module: "health".into(),
},
PermissionDescriptor {
code: "health.doctor.manage".into(),
name: "管理医护".into(),
description: "创建、编辑医护档案、排班".into(),
module: "health".into(),
},
]
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}

View File

@@ -0,0 +1,120 @@
//! 预约排班 Service — 预约CRUD、排班管理、日历视图、原子CAS预约
use chrono::NaiveDate;
use uuid::Uuid;
use erp_core::types::{PaginatedResponse, Pagination};
use crate::dto::appointment_dto::{
AppointmentResp, CalendarDayResp, CreateAppointmentReq, CreateScheduleReq,
ScheduleResp, UpdateAppointmentStatusReq, UpdateScheduleReq,
};
use crate::error::HealthResult;
use crate::state::HealthState;
// ---------------------------------------------------------------------------
// 预约管理 (Appointments)
// ---------------------------------------------------------------------------
/// 预约列表(分页 + 多条件筛选)
pub async fn list_appointments(
state: &HealthState,
tenant_id: Uuid,
pagination: Pagination,
status: Option<String>,
patient_id: Option<Uuid>,
doctor_id: Option<Uuid>,
date: Option<NaiveDate>,
) -> HealthResult<PaginatedResponse<AppointmentResp>> {
let _ = (state, tenant_id, pagination, status, patient_id, doctor_id, date);
todo!()
}
/// 创建预约(原子 CAS 占位,防止超额预约)
pub async fn create_appointment(
state: &HealthState,
tenant_id: Uuid,
req: CreateAppointmentReq,
user_id: Option<Uuid>,
) -> HealthResult<AppointmentResp> {
let _ = (state, tenant_id, req, user_id);
// 实现时需要:
// 1. 查找对应排班档位
// 2. 原子 CAS: UPDATE doctor_schedule SET current_appointments = current_appointments + 1
// WHERE id = ? AND current_appointments < max_appointments
// 3. CAS 失败返回 ScheduleFull 错误
// 4. 创建预约记录
// 5. 发布 appointment.created 事件
todo!()
}
/// 更新预约状态(确认/取消/完成/未到)
pub async fn update_appointment_status(
state: &HealthState,
tenant_id: Uuid,
appointment_id: Uuid,
req: UpdateAppointmentStatusReq,
version: i32,
) -> HealthResult<AppointmentResp> {
let _ = (state, tenant_id, appointment_id, req, version);
// 实现时需要:
// 1. 状态机校验pending -> confirmed/cancelled, confirmed -> completed/no_show/cancelled
// 2. 取消时释放排班名额(原子减 1
// 3. 发布 appointment.confirmed / appointment.cancelled 事件
todo!()
}
// ---------------------------------------------------------------------------
// 排班管理 (Doctor Schedules)
// ---------------------------------------------------------------------------
/// 排班列表
pub async fn list_schedules(
state: &HealthState,
tenant_id: Uuid,
pagination: Pagination,
doctor_id: Option<Uuid>,
date: Option<NaiveDate>,
) -> HealthResult<PaginatedResponse<ScheduleResp>> {
let _ = (state, tenant_id, pagination, doctor_id, date);
todo!()
}
/// 创建排班
pub async fn create_schedule(
state: &HealthState,
tenant_id: Uuid,
req: CreateScheduleReq,
user_id: Option<Uuid>,
) -> HealthResult<ScheduleResp> {
let _ = (state, tenant_id, req, user_id);
todo!()
}
/// 更新排班(乐观锁)
pub async fn update_schedule(
state: &HealthState,
tenant_id: Uuid,
schedule_id: Uuid,
req: UpdateScheduleReq,
version: i32,
) -> HealthResult<ScheduleResp> {
let _ = (state, tenant_id, schedule_id, req, version);
todo!()
}
// ---------------------------------------------------------------------------
// 日历视图
// ---------------------------------------------------------------------------
/// 日历视图(按日期范围返回每天的排班汇总)
pub async fn calendar_view(
state: &HealthState,
tenant_id: Uuid,
start_date: NaiveDate,
end_date: NaiveDate,
doctor_id: Option<Uuid>,
) -> HealthResult<Vec<CalendarDayResp>> {
let _ = (state, tenant_id, start_date, end_date, doctor_id);
todo!()
}

View File

@@ -0,0 +1,83 @@
//! 咨询管理 Service — 会话管理、消息收发、会话关闭、导出
use uuid::Uuid;
use erp_core::types::{PaginatedResponse, Pagination};
use crate::dto::consultation_dto::{
CreateMessageReq, MessageResp, SessionQuery, SessionResp,
};
use crate::error::HealthResult;
use crate::state::HealthState;
// ---------------------------------------------------------------------------
// 咨询会话 (Consultation Sessions)
// ---------------------------------------------------------------------------
/// 咨询会话列表(分页 + 多条件筛选)
pub async fn list_sessions(
state: &HealthState,
tenant_id: Uuid,
query: SessionQuery,
) -> HealthResult<PaginatedResponse<SessionResp>> {
let _ = (state, tenant_id, query);
todo!()
}
/// 关闭咨询会话
pub async fn close_session(
state: &HealthState,
tenant_id: Uuid,
session_id: Uuid,
version: i32,
) -> HealthResult<SessionResp> {
let _ = (state, tenant_id, session_id, version);
// 实现时需要:
// 1. 校验会话存在且状态为 active
// 2. 更新状态为 closed
// 3. 发布 consultation.closed 事件
todo!()
}
/// 导出咨询会话(按条件筛选后返回汇总数据)
pub async fn export_sessions(
state: &HealthState,
tenant_id: Uuid,
status: Option<String>,
patient_id: Option<Uuid>,
doctor_id: Option<Uuid>,
) -> HealthResult<Vec<SessionResp>> {
let _ = (state, tenant_id, status, patient_id, doctor_id);
todo!()
}
// ---------------------------------------------------------------------------
// 咨询消息 (Consultation Messages)
// ---------------------------------------------------------------------------
/// 消息列表(按会话 ID 查询,分页)
pub async fn list_messages(
state: &HealthState,
tenant_id: Uuid,
session_id: Uuid,
pagination: Pagination,
) -> HealthResult<PaginatedResponse<MessageResp>> {
let _ = (state, tenant_id, session_id, pagination);
todo!()
}
/// 发送消息
pub async fn create_message(
state: &HealthState,
tenant_id: Uuid,
req: CreateMessageReq,
) -> HealthResult<MessageResp> {
let _ = (state, tenant_id, req);
// 实现时需要:
// 1. 校验会话存在且状态为 active
// 2. 创建消息记录
// 3. 更新会话的 last_message_at
// 4. 根据发送者角色更新对方的 unread_count
// 5. 发布 consultation.message.created 事件
todo!()
}

View File

@@ -0,0 +1,161 @@
//! 随访管理 Service — 随访任务CRUD、随访记录、状态流转
use chrono::NaiveDate;
use uuid::Uuid;
use erp_core::types::{PaginatedResponse, Pagination};
use crate::error::{HealthError, HealthResult};
use crate::state::HealthState;
// ---------------------------------------------------------------------------
// 随访任务 DTO内部使用follow_up_dto 尚未创建独立文件)
// ---------------------------------------------------------------------------
/// 创建随访任务请求
#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)]
pub struct CreateFollowUpTaskReq {
pub patient_id: Uuid,
pub assigned_to: Option<Uuid>,
pub follow_up_type: String,
pub planned_date: NaiveDate,
pub content_template: Option<String>,
pub related_appointment_id: Option<Uuid>,
}
/// 更新随访任务请求
#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)]
pub struct UpdateFollowUpTaskReq {
pub assigned_to: Option<Uuid>,
pub follow_up_type: Option<String>,
pub planned_date: Option<NaiveDate>,
pub content_template: Option<String>,
pub status: Option<String>,
}
/// 随访任务响应
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
pub struct FollowUpTaskResp {
pub id: Uuid,
pub patient_id: Uuid,
pub assigned_to: Option<Uuid>,
pub follow_up_type: String,
pub planned_date: NaiveDate,
pub status: String,
pub content_template: Option<String>,
pub related_appointment_id: Option<Uuid>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub version: i32,
}
/// 创建随访记录请求
#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)]
pub struct CreateFollowUpRecordReq {
pub task_id: Uuid,
pub executed_by: Option<Uuid>,
pub executed_date: NaiveDate,
pub result: String,
pub patient_condition: Option<String>,
pub medical_advice: Option<String>,
pub next_follow_up_date: Option<NaiveDate>,
}
/// 随访记录响应
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
pub struct FollowUpRecordResp {
pub id: Uuid,
pub task_id: Uuid,
pub executed_by: Option<Uuid>,
pub executed_date: NaiveDate,
pub result: String,
pub patient_condition: Option<String>,
pub medical_advice: Option<String>,
pub next_follow_up_date: Option<NaiveDate>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub version: i32,
}
// ---------------------------------------------------------------------------
// 随访任务
// ---------------------------------------------------------------------------
/// 随访任务列表(分页 + 多条件筛选)
pub async fn list_tasks(
state: &HealthState,
tenant_id: Uuid,
pagination: Pagination,
patient_id: Option<Uuid>,
assigned_to: Option<Uuid>,
status: Option<String>,
) -> HealthResult<PaginatedResponse<FollowUpTaskResp>> {
let _ = (state, tenant_id, pagination, patient_id, assigned_to, status);
todo!()
}
/// 创建随访任务
pub async fn create_task(
state: &HealthState,
tenant_id: Uuid,
req: CreateFollowUpTaskReq,
user_id: Option<Uuid>,
) -> HealthResult<FollowUpTaskResp> {
let _ = (state, tenant_id, req, user_id);
todo!()
}
/// 更新随访任务(乐观锁)
pub async fn update_task(
state: &HealthState,
tenant_id: Uuid,
task_id: Uuid,
req: UpdateFollowUpTaskReq,
version: i32,
) -> HealthResult<FollowUpTaskResp> {
let _ = (state, tenant_id, task_id, req, version);
todo!()
}
/// 删除随访任务(软删除)
pub async fn delete_task(
state: &HealthState,
tenant_id: Uuid,
task_id: Uuid,
) -> HealthResult<()> {
let _ = (state, tenant_id, task_id);
todo!()
}
// ---------------------------------------------------------------------------
// 随访记录
// ---------------------------------------------------------------------------
/// 创建随访执行记录(同时将任务状态推进为 completed
pub async fn create_record(
state: &HealthState,
tenant_id: Uuid,
req: CreateFollowUpRecordReq,
user_id: Option<Uuid>,
) -> HealthResult<FollowUpRecordResp> {
let _ = (state, tenant_id, req, user_id);
// 实现时需要:
// 1. 校验任务存在且状态为 in_progress / pending
// 2. 创建随访记录
// 3. 更新任务状态为 completed
// 4. 如果设置了 next_follow_up_date自动创建下一个随访任务
// 5. 发布 follow_up.completed 事件
todo!()
}
/// 随访记录列表(分页)
pub async fn list_records(
state: &HealthState,
tenant_id: Uuid,
pagination: Pagination,
task_id: Option<Uuid>,
patient_id: Option<Uuid>,
) -> HealthResult<PaginatedResponse<FollowUpRecordResp>> {
let _ = (state, tenant_id, pagination, task_id, patient_id);
todo!()
}

View File

@@ -0,0 +1,207 @@
//! 健康数据 Service — 体征记录、化验报告、体检记录、趋势分析
use chrono::NaiveDate;
use uuid::Uuid;
use erp_core::types::{PaginatedResponse, Pagination};
use crate::dto::health_data_dto::{
CreateHealthRecordReq, CreateLabReportReq, CreateVitalSignsReq, HealthRecordResp,
IndicatorTimeseriesResp, LabReportResp, TrendResp, UpdateVitalSignsReq,
};
use crate::error::HealthResult;
use crate::state::HealthState;
// ---------------------------------------------------------------------------
// 体征记录 (Vital Signs)
// ---------------------------------------------------------------------------
/// 体征记录列表
pub async fn list_vital_signs(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
pagination: Pagination,
) -> HealthResult<PaginatedResponse<crate::dto::health_data_dto::VitalSignsResp>> {
let _ = (state, tenant_id, patient_id, pagination);
todo!()
}
/// 创建体征记录
pub async fn create_vital_signs(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
req: CreateVitalSignsReq,
user_id: Option<Uuid>,
) -> HealthResult<crate::dto::health_data_dto::VitalSignsResp> {
let _ = (state, tenant_id, patient_id, req, user_id);
todo!()
}
/// 更新体征记录(乐观锁)
pub async fn update_vital_signs(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
vital_signs_id: Uuid,
req: UpdateVitalSignsReq,
version: i32,
) -> HealthResult<crate::dto::health_data_dto::VitalSignsResp> {
let _ = (state, tenant_id, patient_id, vital_signs_id, req, version);
todo!()
}
/// 删除体征记录
pub async fn delete_vital_signs(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
vital_signs_id: Uuid,
) -> HealthResult<()> {
let _ = (state, tenant_id, patient_id, vital_signs_id);
todo!()
}
// ---------------------------------------------------------------------------
// 化验报告 (Lab Reports)
// ---------------------------------------------------------------------------
/// 化验报告列表
pub async fn list_lab_reports(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
pagination: Pagination,
) -> HealthResult<PaginatedResponse<LabReportResp>> {
let _ = (state, tenant_id, patient_id, pagination);
todo!()
}
/// 创建化验报告
pub async fn create_lab_report(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
req: CreateLabReportReq,
user_id: Option<Uuid>,
) -> HealthResult<LabReportResp> {
let _ = (state, tenant_id, patient_id, req, user_id);
todo!()
}
/// 更新化验报告(乐观锁)
pub async fn update_lab_report(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
report_id: Uuid,
req: CreateLabReportReq,
version: i32,
) -> HealthResult<LabReportResp> {
let _ = (state, tenant_id, patient_id, report_id, req, version);
todo!()
}
/// 删除化验报告
pub async fn delete_lab_report(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
report_id: Uuid,
) -> HealthResult<()> {
let _ = (state, tenant_id, patient_id, report_id);
todo!()
}
// ---------------------------------------------------------------------------
// 体检记录 (Health Records)
// ---------------------------------------------------------------------------
/// 体检记录列表
pub async fn list_health_records(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
pagination: Pagination,
) -> HealthResult<PaginatedResponse<HealthRecordResp>> {
let _ = (state, tenant_id, patient_id, pagination);
todo!()
}
/// 创建体检记录
pub async fn create_health_record(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
req: CreateHealthRecordReq,
user_id: Option<Uuid>,
) -> HealthResult<HealthRecordResp> {
let _ = (state, tenant_id, patient_id, req, user_id);
todo!()
}
/// 更新体检记录(乐观锁)
pub async fn update_health_record(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
record_id: Uuid,
req: CreateHealthRecordReq,
version: i32,
) -> HealthResult<HealthRecordResp> {
let _ = (state, tenant_id, patient_id, record_id, req, version);
todo!()
}
/// 删除体检记录
pub async fn delete_health_record(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
record_id: Uuid,
) -> HealthResult<()> {
let _ = (state, tenant_id, patient_id, record_id);
todo!()
}
// ---------------------------------------------------------------------------
// 趋势分析 (Trends)
// ---------------------------------------------------------------------------
/// 趋势列表
pub async fn list_trends(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
pagination: Pagination,
) -> HealthResult<PaginatedResponse<TrendResp>> {
let _ = (state, tenant_id, patient_id, pagination);
todo!()
}
/// 生成趋势分析报告(基于历史体征 + 化验数据聚合)
pub async fn generate_trend(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
period_start: NaiveDate,
period_end: NaiveDate,
user_id: Option<Uuid>,
) -> HealthResult<TrendResp> {
let _ = (state, tenant_id, patient_id, period_start, period_end, user_id);
todo!()
}
/// 获取单个指标的时间序列数据
pub async fn get_indicator_timeseries(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
indicator: String,
start_date: Option<NaiveDate>,
end_date: Option<NaiveDate>,
) -> HealthResult<IndicatorTimeseriesResp> {
let _ = (state, tenant_id, patient_id, indicator, start_date, end_date);
todo!()
}

View File

@@ -0,0 +1,6 @@
pub mod appointment_service;
pub mod consultation_service;
pub mod follow_up_service;
pub mod health_data_service;
pub mod patient_service;
pub mod seed;

View File

@@ -0,0 +1,179 @@
//! 患者管理 Service — 患者CRUD、家庭成员、标签、医生关联、健康摘要
use uuid::Uuid;
use erp_core::types::{PaginatedResponse, Pagination};
use crate::dto::patient_dto::{
CreatePatientReq, FamilyMemberReq, FamilyMemberResp, ManageTagsReq, PatientResp,
UpdatePatientReq,
};
use crate::error::HealthResult;
use crate::state::HealthState;
// ---------------------------------------------------------------------------
// 患者 CRUD
// ---------------------------------------------------------------------------
/// 患者列表(分页 + 搜索 + 标签筛选)
pub async fn list_patients(
state: &HealthState,
tenant_id: Uuid,
pagination: Pagination,
search: Option<String>,
tag_id: Option<Uuid>,
) -> HealthResult<PaginatedResponse<PatientResp>> {
let _ = (state, tenant_id, pagination, search, tag_id);
todo!()
}
/// 创建患者
pub async fn create_patient(
state: &HealthState,
tenant_id: Uuid,
user_id: Option<Uuid>,
req: CreatePatientReq,
) -> HealthResult<PatientResp> {
let _ = (state, tenant_id, user_id, req);
todo!()
}
/// 获取患者详情
pub async fn get_patient(
state: &HealthState,
tenant_id: Uuid,
id: Uuid,
) -> HealthResult<PatientResp> {
let _ = (state, tenant_id, id);
todo!()
}
/// 更新患者信息(乐观锁)
pub async fn update_patient(
state: &HealthState,
tenant_id: Uuid,
id: Uuid,
req: UpdatePatientReq,
version: i32,
) -> HealthResult<PatientResp> {
let _ = (state, tenant_id, id, req, version);
todo!()
}
/// 软删除患者
pub async fn delete_patient(
state: &HealthState,
tenant_id: Uuid,
id: Uuid,
) -> HealthResult<()> {
let _ = (state, tenant_id, id);
todo!()
}
// ---------------------------------------------------------------------------
// 标签管理
// ---------------------------------------------------------------------------
/// 管理患者标签(覆盖式:传入的 tag_ids 替换当前关联)
pub async fn manage_patient_tags(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
req: ManageTagsReq,
user_id: Option<Uuid>,
) -> HealthResult<()> {
let _ = (state, tenant_id, patient_id, req, user_id);
todo!()
}
// ---------------------------------------------------------------------------
// 健康摘要
// ---------------------------------------------------------------------------
/// 获取患者健康摘要(最新体征 + 最新化验 + 待处理预约 + 待办随访)
pub async fn get_health_summary(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
) -> HealthResult<serde_json::Value> {
let _ = (state, tenant_id, patient_id);
todo!()
}
// ---------------------------------------------------------------------------
// 家庭成员
// ---------------------------------------------------------------------------
/// 家庭成员列表
pub async fn list_family_members(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
) -> HealthResult<Vec<FamilyMemberResp>> {
let _ = (state, tenant_id, patient_id);
todo!()
}
/// 创建家庭成员
pub async fn create_family_member(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
req: FamilyMemberReq,
user_id: Option<Uuid>,
) -> HealthResult<FamilyMemberResp> {
let _ = (state, tenant_id, patient_id, req, user_id);
todo!()
}
/// 更新家庭成员(乐观锁)
pub async fn update_family_member(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
family_member_id: Uuid,
req: FamilyMemberReq,
version: i32,
) -> HealthResult<FamilyMemberResp> {
let _ = (state, tenant_id, patient_id, family_member_id, req, version);
todo!()
}
/// 删除家庭成员
pub async fn delete_family_member(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
family_member_id: Uuid,
) -> HealthResult<()> {
let _ = (state, tenant_id, patient_id, family_member_id);
todo!()
}
// ---------------------------------------------------------------------------
// 患者-医生关联
// ---------------------------------------------------------------------------
/// 分配负责医生
pub async fn assign_doctor(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
doctor_id: Uuid,
relationship_type: String,
user_id: Option<Uuid>,
) -> HealthResult<()> {
let _ = (state, tenant_id, patient_id, doctor_id, relationship_type, user_id);
todo!()
}
/// 移除负责医生
pub async fn remove_doctor(
state: &HealthState,
tenant_id: Uuid,
patient_id: Uuid,
doctor_id: Uuid,
) -> HealthResult<()> {
let _ = (state, tenant_id, patient_id, doctor_id);
todo!()
}

View File

@@ -0,0 +1,22 @@
//! 租户初始化种子数据 — 创建默认标签、默认排班模板等
use sea_orm::DatabaseConnection;
use uuid::Uuid;
/// 初始化租户健康模块默认数据
pub async fn seed_tenant_health(
_db: &DatabaseConnection,
tenant_id: Uuid,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
tracing::info!(tenant_id = %tenant_id, "Seeding health module default data");
Ok(())
}
/// 软删除该租户下所有健康模块数据
pub async fn soft_delete_tenant_data(
_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");
Ok(())
}

View File

@@ -0,0 +1,8 @@
use erp_core::events::EventBus;
use sea_orm::DatabaseConnection;
#[derive(Clone)]
pub struct HealthState {
pub db: DatabaseConnection,
pub event_bus: EventBus,
}

View File

@@ -27,6 +27,7 @@ erp-config.workspace = true
erp-workflow.workspace = true
erp-message.workspace = true
erp-plugin.workspace = true
erp-health.workspace = true
anyhow.workspace = true
uuid.workspace = true
chrono.workspace = true

View File

@@ -41,6 +41,7 @@ mod m20260419_000038_fix_crm_permission_codes;
mod m20260419_000039_entity_registry_columns;
mod m20260419_000040_plugin_market;
mod m20260419_000041_plugin_user_views;
mod m20260423_000042_create_health_tables;
pub struct Migrator;
@@ -89,6 +90,7 @@ impl MigratorTrait for Migrator {
Box::new(m20260419_000039_entity_registry_columns::Migration),
Box::new(m20260419_000040_plugin_market::Migration),
Box::new(m20260419_000041_plugin_user_views::Migration),
Box::new(m20260423_000042_create_health_tables::Migration),
]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -319,12 +319,21 @@ async fn main() -> anyhow::Result<()> {
"Message module initialized"
);
// Initialize health module
let health_module = erp_health::HealthModule::new();
tracing::info!(
module = health_module.name(),
version = health_module.version(),
"Health module initialized"
);
// Initialize module registry and register modules
let registry = ModuleRegistry::new()
.register(auth_module)
.register(config_module)
.register(workflow_module)
.register(message_module);
.register(message_module)
.register(health_module);
tracing::info!(
module_count = registry.modules().len(),
"Modules registered"
@@ -431,6 +440,7 @@ async fn main() -> anyhow::Result<()> {
.merge(erp_workflow::WorkflowModule::protected_routes())
.merge(erp_message::MessageModule::protected_routes())
.merge(erp_plugin::module::PluginModule::protected_routes())
.merge(erp_health::HealthModule::protected_routes())
.merge(handlers::audit_log::audit_log_router())
.layer(axum::middleware::from_fn_with_state(
state.clone(),

View File

@@ -96,3 +96,13 @@ impl FromRef<AppState> for erp_plugin::state::PluginState {
}
}
}
/// Allow erp-health handlers to extract their required state.
impl FromRef<AppState> for erp_health::HealthState {
fn from_ref(state: &AppState) -> Self {
Self {
db: state.db.clone(),
event_bus: state.event_bus.clone(),
}
}
}