feat(health): 护理计划实体与服务 — Phase 1 关怀引擎 MVP 第一步
新增护理计划(Care Plan)完整 CRUD:3 张表(care_plans / care_plan_items / care_plan_outcomes)、3 个 SeaORM Entity、15 个 API 端点、4 个事件常量、 2 个权限码。支持透析/慢性/预防/康复计划类型,条目分干预/监测/目标/教育四类, 预后测量含基线/目标/当前值追踪。
This commit is contained in:
207
crates/erp-health/src/dto/care_plan_dto.rs
Normal file
207
crates/erp-health/src/dto/care_plan_dto.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
use chrono::{NaiveDate, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
use erp_core::sanitize::{sanitize_option, sanitize_string};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CarePlan
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CarePlanResp {
|
||||
pub id: Uuid,
|
||||
pub patient_id: Uuid,
|
||||
pub plan_type: String,
|
||||
pub status: String,
|
||||
pub title: String,
|
||||
pub goals: serde_json::Value,
|
||||
pub start_date: Option<NaiveDate>,
|
||||
pub end_date: Option<NaiveDate>,
|
||||
pub notes: Option<String>,
|
||||
pub created_at: chrono::DateTime<Utc>,
|
||||
pub updated_at: chrono::DateTime<Utc>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct CreateCarePlanReq {
|
||||
pub patient_id: Uuid,
|
||||
pub plan_type: String,
|
||||
pub title: String,
|
||||
pub goals: Option<serde_json::Value>,
|
||||
pub start_date: Option<NaiveDate>,
|
||||
pub end_date: Option<NaiveDate>,
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
impl CreateCarePlanReq {
|
||||
pub fn sanitize(&mut self) {
|
||||
self.title = sanitize_string(&self.title);
|
||||
self.notes = sanitize_option(self.notes.take());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateCarePlanReq {
|
||||
pub plan_type: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub goals: Option<serde_json::Value>,
|
||||
pub start_date: Option<NaiveDate>,
|
||||
pub end_date: Option<NaiveDate>,
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
impl UpdateCarePlanReq {
|
||||
pub fn sanitize(&mut self) {
|
||||
if let Some(ref mut v) = self.title {
|
||||
*v = sanitize_string(v);
|
||||
}
|
||||
if let Some(ref mut v) = self.notes {
|
||||
*v = sanitize_option(Some(std::mem::take(v))).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateCarePlanWithVersion {
|
||||
#[serde(flatten)]
|
||||
pub data: UpdateCarePlanReq,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct ListCarePlansParams {
|
||||
pub patient_id: Option<Uuid>,
|
||||
pub plan_type: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CarePlanItem
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CarePlanItemResp {
|
||||
pub id: Uuid,
|
||||
pub plan_id: Uuid,
|
||||
pub item_type: String,
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
pub status: String,
|
||||
pub schedule: Option<String>,
|
||||
pub sort_order: Option<i32>,
|
||||
pub created_at: chrono::DateTime<Utc>,
|
||||
pub updated_at: chrono::DateTime<Utc>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct CreateCarePlanItemReq {
|
||||
pub item_type: String,
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
pub schedule: Option<String>,
|
||||
pub sort_order: Option<i32>,
|
||||
}
|
||||
|
||||
impl CreateCarePlanItemReq {
|
||||
pub fn sanitize(&mut self) {
|
||||
self.title = sanitize_string(&self.title);
|
||||
self.description = sanitize_option(self.description.take());
|
||||
self.schedule = sanitize_option(self.schedule.take());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateCarePlanItemReq {
|
||||
pub item_type: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub schedule: Option<String>,
|
||||
pub sort_order: Option<i32>,
|
||||
}
|
||||
|
||||
impl UpdateCarePlanItemReq {
|
||||
pub fn sanitize(&mut self) {
|
||||
if let Some(ref mut v) = self.title {
|
||||
*v = sanitize_string(v);
|
||||
}
|
||||
if let Some(ref mut v) = self.description {
|
||||
*v = sanitize_option(Some(std::mem::take(v))).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateCarePlanItemWithVersion {
|
||||
#[serde(flatten)]
|
||||
pub data: UpdateCarePlanItemReq,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CarePlanOutcome
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CarePlanOutcomeResp {
|
||||
pub id: Uuid,
|
||||
pub plan_id: Uuid,
|
||||
pub item_id: Option<Uuid>,
|
||||
pub metric: String,
|
||||
pub baseline_value: String,
|
||||
pub target_value: String,
|
||||
pub current_value: Option<String>,
|
||||
pub measured_at: Option<chrono::DateTime<Utc>>,
|
||||
pub notes: Option<String>,
|
||||
pub created_at: chrono::DateTime<Utc>,
|
||||
pub updated_at: chrono::DateTime<Utc>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct CreateCarePlanOutcomeReq {
|
||||
pub item_id: Option<Uuid>,
|
||||
pub metric: String,
|
||||
pub baseline_value: String,
|
||||
pub target_value: String,
|
||||
pub current_value: Option<String>,
|
||||
pub measured_at: Option<chrono::DateTime<Utc>>,
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
impl CreateCarePlanOutcomeReq {
|
||||
pub fn sanitize(&mut self) {
|
||||
self.metric = sanitize_string(&self.metric);
|
||||
self.notes = sanitize_option(self.notes.take());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateCarePlanOutcomeReq {
|
||||
pub current_value: Option<String>,
|
||||
pub target_value: Option<String>,
|
||||
pub measured_at: Option<chrono::DateTime<Utc>>,
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
impl UpdateCarePlanOutcomeReq {
|
||||
pub fn sanitize(&mut self) {
|
||||
if let Some(ref mut v) = self.notes {
|
||||
*v = sanitize_option(Some(std::mem::take(v))).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateCarePlanOutcomeWithVersion {
|
||||
#[serde(flatten)]
|
||||
pub data: UpdateCarePlanOutcomeReq,
|
||||
pub version: i32,
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod appointment_dto;
|
||||
pub mod alert_dto;
|
||||
pub mod care_plan_dto;
|
||||
pub mod article_dto;
|
||||
pub mod consent_dto;
|
||||
pub mod consultation_dto;
|
||||
|
||||
64
crates/erp-health/src/entity/care_plan.rs
Normal file
64
crates/erp-health/src/entity/care_plan.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "care_plans")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub tenant_id: Uuid,
|
||||
pub patient_id: Uuid,
|
||||
pub plan_type: String,
|
||||
pub status: String,
|
||||
pub title: String,
|
||||
pub goals: serde_json::Value,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
pub start_date: Option<chrono::NaiveDate>,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
pub end_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,
|
||||
#[sea_orm(has_many = "super::care_plan_item::Entity")]
|
||||
Items,
|
||||
#[sea_orm(has_many = "super::care_plan_outcome::Entity")]
|
||||
Outcomes,
|
||||
}
|
||||
|
||||
impl Related<super::patient::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Patient.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::care_plan_item::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Items.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::care_plan_outcome::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Outcomes.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
47
crates/erp-health/src/entity/care_plan_item.rs
Normal file
47
crates/erp-health/src/entity/care_plan_item.rs
Normal 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 = "care_plan_items")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub tenant_id: Uuid,
|
||||
pub plan_id: Uuid,
|
||||
pub item_type: String,
|
||||
pub title: String,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub status: String,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
pub schedule: Option<String>,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
pub sort_order: Option<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::care_plan::Entity",
|
||||
from = "Column::PlanId",
|
||||
to = "super::care_plan::Column::Id"
|
||||
)]
|
||||
CarePlan,
|
||||
}
|
||||
|
||||
impl Related<super::care_plan::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::CarePlan.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
61
crates/erp-health/src/entity/care_plan_outcome.rs
Normal file
61
crates/erp-health/src/entity/care_plan_outcome.rs
Normal 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 = "care_plan_outcomes")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub tenant_id: Uuid,
|
||||
pub plan_id: Uuid,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
pub item_id: Option<Uuid>,
|
||||
pub metric: String,
|
||||
pub baseline_value: String,
|
||||
pub target_value: String,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
pub current_value: Option<String>,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
pub measured_at: Option<DateTimeUtc>,
|
||||
#[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::care_plan::Entity",
|
||||
from = "Column::PlanId",
|
||||
to = "super::care_plan::Column::Id"
|
||||
)]
|
||||
CarePlan,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::care_plan_item::Entity",
|
||||
from = "Column::ItemId",
|
||||
to = "super::care_plan_item::Column::Id"
|
||||
)]
|
||||
CarePlanItem,
|
||||
}
|
||||
|
||||
impl Related<super::care_plan::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::CarePlan.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::care_plan_item::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::CarePlanItem.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -43,5 +43,8 @@ pub mod offline_event_registration;
|
||||
pub mod medication_record;
|
||||
pub mod medication_reminder;
|
||||
pub mod vital_signs;
|
||||
pub mod care_plan;
|
||||
pub mod care_plan_item;
|
||||
pub mod care_plan_outcome;
|
||||
pub mod vital_signs_daily;
|
||||
pub mod vital_signs_hourly;
|
||||
|
||||
@@ -83,6 +83,15 @@ pub enum HealthError {
|
||||
#[error("药物提醒不存在")]
|
||||
MedicationReminderNotFound,
|
||||
|
||||
#[error("护理计划不存在")]
|
||||
CarePlanNotFound,
|
||||
|
||||
#[error("护理计划条目不存在")]
|
||||
CarePlanItemNotFound,
|
||||
|
||||
#[error("护理计划预后不存在")]
|
||||
CarePlanOutcomeNotFound,
|
||||
|
||||
#[error("状态转换无效: {0}")]
|
||||
InvalidStatusTransition(String),
|
||||
|
||||
@@ -121,7 +130,10 @@ impl From<HealthError> for AppError {
|
||||
| HealthError::AlertNotFound
|
||||
| HealthError::FollowUpTemplateNotFound
|
||||
| HealthError::CriticalAlertNotFound
|
||||
| HealthError::MedicationReminderNotFound => AppError::NotFound(err.to_string()),
|
||||
| HealthError::MedicationReminderNotFound
|
||||
| HealthError::CarePlanNotFound
|
||||
| HealthError::CarePlanItemNotFound
|
||||
| HealthError::CarePlanOutcomeNotFound => AppError::NotFound(err.to_string()),
|
||||
HealthError::ScheduleFull => AppError::Validation(err.to_string()),
|
||||
HealthError::InvalidStatusTransition(s) => AppError::Validation(s),
|
||||
HealthError::VersionMismatch => AppError::VersionMismatch,
|
||||
|
||||
@@ -57,6 +57,12 @@ pub const POINTS_EXPIRED: &str = "points.expired";
|
||||
pub const POINTS_EARNED: &str = "points.earned";
|
||||
pub const POINTS_EXCHANGED: &str = "points.exchanged";
|
||||
|
||||
// 护理计划
|
||||
pub const CARE_PLAN_CREATED: &str = "care_plan.created";
|
||||
pub const CARE_PLAN_UPDATED: &str = "care_plan.updated";
|
||||
pub const CARE_PLAN_ACTIVATED: &str = "care_plan.activated";
|
||||
pub const CARE_PLAN_COMPLETED: &str = "care_plan.completed";
|
||||
|
||||
/// 兼容旧签名 — 不做任何实际订阅(逻辑已迁移到 on_startup)
|
||||
pub fn register_handlers(_bus: &EventBus) {
|
||||
// 事件处理器已迁移到 on_startup → register_handlers_with_state
|
||||
|
||||
304
crates/erp-health/src/handler/care_plan_handler.rs
Normal file
304
crates/erp-health/src/handler/care_plan_handler.rs
Normal file
@@ -0,0 +1,304 @@
|
||||
use axum::extract::{FromRef, Json, Path, Query, State};
|
||||
use axum::Extension;
|
||||
use erp_core::error::AppError;
|
||||
use erp_core::rbac::require_permission;
|
||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||
|
||||
use crate::dto::care_plan_dto::*;
|
||||
use crate::service::care_plan_service;
|
||||
use crate::state::HealthState;
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct PaginationParams {
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CarePlan
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn list_care_plans<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Query(params): Query<ListCarePlansParams>,
|
||||
) -> Result<Json<ApiResponse<PaginatedResponse<CarePlanResp>>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.care-plan.list")?;
|
||||
let result = care_plan_service::list_care_plans(&state, ctx.tenant_id, ¶ms).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn get_care_plan<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(plan_id): Path<uuid::Uuid>,
|
||||
) -> Result<Json<ApiResponse<CarePlanResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.care-plan.list")?;
|
||||
let result = care_plan_service::get_care_plan(&state, ctx.tenant_id, plan_id).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn create_care_plan<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Json(mut req): Json<CreateCarePlanReq>,
|
||||
) -> Result<Json<ApiResponse<CarePlanResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.care-plan.manage")?;
|
||||
req.sanitize();
|
||||
let result =
|
||||
care_plan_service::create_care_plan(&state, ctx.tenant_id, Some(ctx.user_id), req).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn update_care_plan<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(plan_id): Path<uuid::Uuid>,
|
||||
Json(mut req): Json<UpdateCarePlanWithVersion>,
|
||||
) -> Result<Json<ApiResponse<CarePlanResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.care-plan.manage")?;
|
||||
req.data.sanitize();
|
||||
let result = care_plan_service::update_care_plan(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
plan_id,
|
||||
Some(ctx.user_id),
|
||||
req,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn delete_care_plan<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(plan_id): Path<uuid::Uuid>,
|
||||
Json(body): Json<crate::dto::DeleteWithVersion>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.care-plan.manage")?;
|
||||
care_plan_service::delete_care_plan(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
plan_id,
|
||||
Some(ctx.user_id),
|
||||
body.version,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CarePlanItem
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn list_care_plan_items<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(plan_id): Path<uuid::Uuid>,
|
||||
Query(params): Query<PaginationParams>,
|
||||
) -> Result<Json<ApiResponse<PaginatedResponse<CarePlanItemResp>>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.care-plan.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let result = care_plan_service::list_care_plan_items(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
plan_id,
|
||||
page,
|
||||
page_size,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn create_care_plan_item<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(plan_id): Path<uuid::Uuid>,
|
||||
Json(mut req): Json<CreateCarePlanItemReq>,
|
||||
) -> Result<Json<ApiResponse<CarePlanItemResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.care-plan.manage")?;
|
||||
req.sanitize();
|
||||
let result = care_plan_service::create_care_plan_item(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
plan_id,
|
||||
Some(ctx.user_id),
|
||||
req,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn update_care_plan_item<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path((plan_id, item_id)): Path<(uuid::Uuid, uuid::Uuid)>,
|
||||
Json(mut req): Json<UpdateCarePlanItemWithVersion>,
|
||||
) -> Result<Json<ApiResponse<CarePlanItemResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.care-plan.manage")?;
|
||||
req.data.sanitize();
|
||||
let result = care_plan_service::update_care_plan_item(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
plan_id,
|
||||
item_id,
|
||||
Some(ctx.user_id),
|
||||
req,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn delete_care_plan_item<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path((plan_id, item_id)): Path<(uuid::Uuid, uuid::Uuid)>,
|
||||
Json(body): Json<crate::dto::DeleteWithVersion>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.care-plan.manage")?;
|
||||
care_plan_service::delete_care_plan_item(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
plan_id,
|
||||
item_id,
|
||||
Some(ctx.user_id),
|
||||
body.version,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CarePlanOutcome
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn list_care_plan_outcomes<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(plan_id): Path<uuid::Uuid>,
|
||||
Query(params): Query<PaginationParams>,
|
||||
) -> Result<Json<ApiResponse<PaginatedResponse<CarePlanOutcomeResp>>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.care-plan.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let result = care_plan_service::list_care_plan_outcomes(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
plan_id,
|
||||
page,
|
||||
page_size,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn create_care_plan_outcome<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(plan_id): Path<uuid::Uuid>,
|
||||
Json(mut req): Json<CreateCarePlanOutcomeReq>,
|
||||
) -> Result<Json<ApiResponse<CarePlanOutcomeResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.care-plan.manage")?;
|
||||
req.sanitize();
|
||||
let result = care_plan_service::create_care_plan_outcome(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
plan_id,
|
||||
Some(ctx.user_id),
|
||||
req,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn update_care_plan_outcome<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path((plan_id, outcome_id)): Path<(uuid::Uuid, uuid::Uuid)>,
|
||||
Json(mut req): Json<UpdateCarePlanOutcomeWithVersion>,
|
||||
) -> Result<Json<ApiResponse<CarePlanOutcomeResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.care-plan.manage")?;
|
||||
req.data.sanitize();
|
||||
let result = care_plan_service::update_care_plan_outcome(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
plan_id,
|
||||
outcome_id,
|
||||
Some(ctx.user_id),
|
||||
req,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn delete_care_plan_outcome<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path((plan_id, outcome_id)): Path<(uuid::Uuid, uuid::Uuid)>,
|
||||
Json(body): Json<crate::dto::DeleteWithVersion>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.care-plan.manage")?;
|
||||
care_plan_service::delete_care_plan_outcome(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
plan_id,
|
||||
outcome_id,
|
||||
Some(ctx.user_id),
|
||||
body.version,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
@@ -5,6 +5,7 @@ pub mod appointment_handler;
|
||||
pub mod article_category_handler;
|
||||
pub mod article_handler;
|
||||
pub mod article_tag_handler;
|
||||
pub mod care_plan_handler;
|
||||
pub mod consultation_handler;
|
||||
pub mod consent_handler;
|
||||
pub mod critical_alert_handler;
|
||||
|
||||
@@ -8,7 +8,7 @@ use erp_core::module::{ErpModule, PermissionDescriptor};
|
||||
use crate::handler::{
|
||||
action_inbox_handler,
|
||||
alert_handler, alert_rule_handler,
|
||||
appointment_handler, article_category_handler, article_handler, article_tag_handler, consultation_handler, consent_handler, critical_alert_handler, critical_value_threshold_handler, daily_monitoring_handler, device_handler, device_reading_handler, diagnosis_handler, doctor_handler, follow_up_handler, follow_up_template_handler,
|
||||
appointment_handler, article_category_handler, article_handler, article_tag_handler, care_plan_handler, consultation_handler, consent_handler, critical_alert_handler, critical_value_threshold_handler, daily_monitoring_handler, device_handler, device_reading_handler, diagnosis_handler, doctor_handler, follow_up_handler, follow_up_template_handler,
|
||||
health_data_handler, medication_record_handler, medication_reminder_handler, patient_handler, points_handler, stats_handler,
|
||||
vital_signs_daily_handler,
|
||||
};
|
||||
@@ -820,6 +820,38 @@ impl HealthModule {
|
||||
"/health/oauth/clients/{id}/regenerate-secret",
|
||||
axum::routing::post(crate::oauth::handler::regenerate_secret),
|
||||
)
|
||||
// 护理计划
|
||||
.route(
|
||||
"/health/care-plans",
|
||||
axum::routing::get(care_plan_handler::list_care_plans)
|
||||
.post(care_plan_handler::create_care_plan),
|
||||
)
|
||||
.route(
|
||||
"/health/care-plans/{id}",
|
||||
axum::routing::get(care_plan_handler::get_care_plan)
|
||||
.put(care_plan_handler::update_care_plan)
|
||||
.delete(care_plan_handler::delete_care_plan),
|
||||
)
|
||||
.route(
|
||||
"/health/care-plans/{plan_id}/items",
|
||||
axum::routing::get(care_plan_handler::list_care_plan_items)
|
||||
.post(care_plan_handler::create_care_plan_item),
|
||||
)
|
||||
.route(
|
||||
"/health/care-plans/{plan_id}/items/{item_id}",
|
||||
axum::routing::put(care_plan_handler::update_care_plan_item)
|
||||
.delete(care_plan_handler::delete_care_plan_item),
|
||||
)
|
||||
.route(
|
||||
"/health/care-plans/{plan_id}/outcomes",
|
||||
axum::routing::get(care_plan_handler::list_care_plan_outcomes)
|
||||
.post(care_plan_handler::create_care_plan_outcome),
|
||||
)
|
||||
.route(
|
||||
"/health/care-plans/{plan_id}/outcomes/{outcome_id}",
|
||||
axum::routing::put(care_plan_handler::update_care_plan_outcome)
|
||||
.delete(care_plan_handler::delete_care_plan_outcome),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1240,6 +1272,19 @@ impl ErpModule for HealthModule {
|
||||
description: "创建/编辑/删除 FHIR API 合作方".into(),
|
||||
module: "health".into(),
|
||||
},
|
||||
// 护理计划
|
||||
PermissionDescriptor {
|
||||
code: "health.care-plan.list".into(),
|
||||
name: "查看护理计划".into(),
|
||||
description: "查看护理计划、条目和预后测量".into(),
|
||||
module: "health".into(),
|
||||
},
|
||||
PermissionDescriptor {
|
||||
code: "health.care-plan.manage".into(),
|
||||
name: "管理护理计划".into(),
|
||||
description: "创建/编辑/删除护理计划、条目和预后测量".into(),
|
||||
module: "health".into(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
647
crates/erp-health/src/service/care_plan_service.rs
Normal file
647
crates/erp-health/src/service/care_plan_service.rs
Normal file
@@ -0,0 +1,647 @@
|
||||
use chrono::Utc;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{ActiveValue::Set, QueryOrder, QuerySelect};
|
||||
use uuid::Uuid;
|
||||
|
||||
use erp_core::audit::AuditLog;
|
||||
use erp_core::audit_service;
|
||||
use erp_core::error::check_version;
|
||||
use erp_core::events::DomainEvent;
|
||||
use erp_core::types::PaginatedResponse;
|
||||
|
||||
use crate::dto::care_plan_dto::*;
|
||||
use crate::entity::{care_plan, care_plan_item, care_plan_outcome, patient};
|
||||
use crate::error::{HealthError, HealthResult};
|
||||
use crate::state::HealthState;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CarePlan CRUD
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn list_care_plans(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
params: &ListCarePlansParams,
|
||||
) -> HealthResult<PaginatedResponse<CarePlanResp>> {
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let limit = page_size.min(100);
|
||||
let offset = page.saturating_sub(1) * limit;
|
||||
|
||||
let mut query = care_plan::Entity::find()
|
||||
.filter(care_plan::Column::TenantId.eq(tenant_id))
|
||||
.filter(care_plan::Column::DeletedAt.is_null());
|
||||
|
||||
if let Some(pid) = params.patient_id {
|
||||
query = query.filter(care_plan::Column::PatientId.eq(pid));
|
||||
}
|
||||
if let Some(ref pt) = params.plan_type {
|
||||
query = query.filter(care_plan::Column::PlanType.eq(pt.as_str()));
|
||||
}
|
||||
if let Some(ref st) = params.status {
|
||||
query = query.filter(care_plan::Column::Status.eq(st.as_str()));
|
||||
}
|
||||
|
||||
let total: u64 = query.clone().count(&state.db).await?;
|
||||
let rows: Vec<care_plan::Model> = query
|
||||
.order_by_desc(care_plan::Column::CreatedAt)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.all(&state.db)
|
||||
.await?;
|
||||
|
||||
let total_pages = total.div_ceil(limit.max(1));
|
||||
let data = rows.into_iter().map(plan_to_resp).collect();
|
||||
|
||||
Ok(PaginatedResponse {
|
||||
data,
|
||||
total,
|
||||
page,
|
||||
page_size,
|
||||
total_pages,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_care_plan(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
plan_id: Uuid,
|
||||
) -> HealthResult<CarePlanResp> {
|
||||
let m = find_plan(state, tenant_id, plan_id).await?;
|
||||
Ok(plan_to_resp(m))
|
||||
}
|
||||
|
||||
pub async fn create_care_plan(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: CreateCarePlanReq,
|
||||
) -> HealthResult<CarePlanResp> {
|
||||
patient::Entity::find()
|
||||
.filter(patient::Column::Id.eq(req.patient_id))
|
||||
.filter(patient::Column::TenantId.eq(tenant_id))
|
||||
.filter(patient::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PatientNotFound)?;
|
||||
|
||||
validate_plan_type(&req.plan_type)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let active = care_plan::ActiveModel {
|
||||
id: Set(Uuid::now_v7()),
|
||||
tenant_id: Set(tenant_id),
|
||||
patient_id: Set(req.patient_id),
|
||||
plan_type: Set(req.plan_type),
|
||||
status: Set("draft".to_string()),
|
||||
title: Set(req.title),
|
||||
goals: Set(req.goals.unwrap_or(serde_json::json!([]))),
|
||||
start_date: Set(req.start_date),
|
||||
end_date: Set(req.end_date),
|
||||
notes: Set(req.notes),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(operator_id),
|
||||
updated_by: Set(operator_id),
|
||||
deleted_at: Set(None),
|
||||
version: Set(1),
|
||||
};
|
||||
let m = active.insert(&state.db).await?;
|
||||
|
||||
audit_service::record(
|
||||
AuditLog::new(tenant_id, operator_id, "care_plan.created", "care_plan")
|
||||
.with_resource_id(m.id),
|
||||
&state.db,
|
||||
)
|
||||
.await;
|
||||
|
||||
state
|
||||
.event_bus
|
||||
.publish(
|
||||
DomainEvent::new(
|
||||
crate::event::CARE_PLAN_CREATED,
|
||||
tenant_id,
|
||||
erp_core::events::build_event_payload(serde_json::json!({
|
||||
"plan_id": m.id, "patient_id": m.patient_id,
|
||||
"plan_type": m.plan_type, "title": m.title,
|
||||
})),
|
||||
),
|
||||
&state.db,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(plan_to_resp(m))
|
||||
}
|
||||
|
||||
pub async fn update_care_plan(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
plan_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: UpdateCarePlanWithVersion,
|
||||
) -> HealthResult<CarePlanResp> {
|
||||
let existing = find_plan(state, tenant_id, plan_id).await?;
|
||||
let _old_status = existing.status.clone(); // 用于后续事件类型判断
|
||||
let next_ver = check_version(req.version, existing.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
let old_status = existing.status.clone();
|
||||
let mut active: care_plan::ActiveModel = existing.into();
|
||||
let now = Utc::now();
|
||||
|
||||
if let Some(v) = req.data.plan_type {
|
||||
validate_plan_type(&v)?;
|
||||
active.plan_type = Set(v);
|
||||
}
|
||||
if let Some(v) = req.data.title {
|
||||
active.title = Set(v);
|
||||
}
|
||||
if let Some(v) = req.data.status {
|
||||
validate_plan_status(&v)?;
|
||||
active.status = Set(v);
|
||||
}
|
||||
if let Some(v) = req.data.goals {
|
||||
active.goals = Set(v);
|
||||
}
|
||||
if req.data.start_date.is_some() {
|
||||
active.start_date = Set(req.data.start_date);
|
||||
}
|
||||
if req.data.end_date.is_some() {
|
||||
active.end_date = Set(req.data.end_date);
|
||||
}
|
||||
if req.data.notes.is_some() {
|
||||
active.notes = Set(req.data.notes);
|
||||
}
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_ver);
|
||||
|
||||
let m = active.update(&state.db).await?;
|
||||
|
||||
audit_service::record(
|
||||
AuditLog::new(tenant_id, operator_id, "care_plan.updated", "care_plan")
|
||||
.with_resource_id(m.id),
|
||||
&state.db,
|
||||
)
|
||||
.await;
|
||||
|
||||
let event_type = match m.status.as_str() {
|
||||
"active" => crate::event::CARE_PLAN_ACTIVATED,
|
||||
"completed" => crate::event::CARE_PLAN_COMPLETED,
|
||||
_ => crate::event::CARE_PLAN_UPDATED,
|
||||
};
|
||||
state
|
||||
.event_bus
|
||||
.publish(
|
||||
DomainEvent::new(
|
||||
event_type,
|
||||
tenant_id,
|
||||
erp_core::events::build_event_payload(serde_json::json!({
|
||||
"plan_id": m.id, "patient_id": m.patient_id,
|
||||
"status": m.status,
|
||||
})),
|
||||
),
|
||||
&state.db,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(plan_to_resp(m))
|
||||
}
|
||||
|
||||
pub async fn delete_care_plan(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
plan_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
version: i32,
|
||||
) -> HealthResult<()> {
|
||||
let existing = find_plan(state, tenant_id, plan_id).await?;
|
||||
let next_ver = check_version(version, existing.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let mut active: care_plan::ActiveModel = existing.into();
|
||||
active.deleted_at = Set(Some(now));
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_ver);
|
||||
active.update(&state.db).await?;
|
||||
|
||||
audit_service::record(
|
||||
AuditLog::new(tenant_id, operator_id, "care_plan.deleted", "care_plan")
|
||||
.with_resource_id(plan_id),
|
||||
&state.db,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CarePlanItem CRUD
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn list_care_plan_items(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
plan_id: Uuid,
|
||||
page: u64,
|
||||
page_size: u64,
|
||||
) -> HealthResult<PaginatedResponse<CarePlanItemResp>> {
|
||||
let _plan = find_plan(state, tenant_id, plan_id).await?;
|
||||
let limit = page_size.min(100);
|
||||
let offset = page.saturating_sub(1) * limit;
|
||||
|
||||
let query = care_plan_item::Entity::find()
|
||||
.filter(care_plan_item::Column::TenantId.eq(tenant_id))
|
||||
.filter(care_plan_item::Column::PlanId.eq(plan_id))
|
||||
.filter(care_plan_item::Column::DeletedAt.is_null());
|
||||
|
||||
let total: u64 = query.clone().count(&state.db).await?;
|
||||
let rows: Vec<care_plan_item::Model> = query
|
||||
.order_by_asc(care_plan_item::Column::SortOrder)
|
||||
.order_by_desc(care_plan_item::Column::CreatedAt)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.all(&state.db)
|
||||
.await?;
|
||||
|
||||
let total_pages = total.div_ceil(limit.max(1));
|
||||
let data = rows.into_iter().map(item_to_resp).collect();
|
||||
|
||||
Ok(PaginatedResponse {
|
||||
data,
|
||||
total,
|
||||
page,
|
||||
page_size,
|
||||
total_pages,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_care_plan_item(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
plan_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: CreateCarePlanItemReq,
|
||||
) -> HealthResult<CarePlanItemResp> {
|
||||
let _plan = find_plan(state, tenant_id, plan_id).await?;
|
||||
validate_item_type(&req.item_type)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let active = care_plan_item::ActiveModel {
|
||||
id: Set(Uuid::now_v7()),
|
||||
tenant_id: Set(tenant_id),
|
||||
plan_id: Set(plan_id),
|
||||
item_type: Set(req.item_type),
|
||||
title: Set(req.title),
|
||||
description: Set(req.description),
|
||||
status: Set("pending".to_string()),
|
||||
schedule: Set(req.schedule),
|
||||
sort_order: Set(req.sort_order),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(operator_id),
|
||||
updated_by: Set(operator_id),
|
||||
deleted_at: Set(None),
|
||||
version: Set(1),
|
||||
};
|
||||
let m = active.insert(&state.db).await?;
|
||||
|
||||
Ok(item_to_resp(m))
|
||||
}
|
||||
|
||||
pub async fn update_care_plan_item(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
plan_id: Uuid,
|
||||
item_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: UpdateCarePlanItemWithVersion,
|
||||
) -> HealthResult<CarePlanItemResp> {
|
||||
let _plan = find_plan(state, tenant_id, plan_id).await?;
|
||||
let existing = care_plan_item::Entity::find_by_id(item_id)
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::CarePlanItemNotFound)?;
|
||||
|
||||
if existing.tenant_id != tenant_id || existing.plan_id != plan_id {
|
||||
return Err(HealthError::CarePlanItemNotFound);
|
||||
}
|
||||
|
||||
let next_ver = check_version(req.version, existing.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
let mut active: care_plan_item::ActiveModel = existing.into();
|
||||
let now = Utc::now();
|
||||
|
||||
if let Some(v) = req.data.item_type {
|
||||
validate_item_type(&v)?;
|
||||
active.item_type = Set(v);
|
||||
}
|
||||
if let Some(v) = req.data.title {
|
||||
active.title = Set(v);
|
||||
}
|
||||
if let Some(v) = req.data.status {
|
||||
active.status = Set(v);
|
||||
}
|
||||
if req.data.description.is_some() {
|
||||
active.description = Set(req.data.description);
|
||||
}
|
||||
if req.data.schedule.is_some() {
|
||||
active.schedule = Set(req.data.schedule);
|
||||
}
|
||||
if req.data.sort_order.is_some() {
|
||||
active.sort_order = Set(req.data.sort_order);
|
||||
}
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_ver);
|
||||
|
||||
let m = active.update(&state.db).await?;
|
||||
Ok(item_to_resp(m))
|
||||
}
|
||||
|
||||
pub async fn delete_care_plan_item(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
plan_id: Uuid,
|
||||
item_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
version: i32,
|
||||
) -> HealthResult<()> {
|
||||
let _plan = find_plan(state, tenant_id, plan_id).await?;
|
||||
let existing = care_plan_item::Entity::find_by_id(item_id)
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::CarePlanItemNotFound)?;
|
||||
|
||||
if existing.tenant_id != tenant_id || existing.plan_id != plan_id {
|
||||
return Err(HealthError::CarePlanItemNotFound);
|
||||
}
|
||||
|
||||
let next_ver = check_version(version, existing.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let mut active: care_plan_item::ActiveModel = existing.into();
|
||||
active.deleted_at = Set(Some(now));
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_ver);
|
||||
active.update(&state.db).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CarePlanOutcome CRUD
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn list_care_plan_outcomes(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
plan_id: Uuid,
|
||||
page: u64,
|
||||
page_size: u64,
|
||||
) -> HealthResult<PaginatedResponse<CarePlanOutcomeResp>> {
|
||||
let _plan = find_plan(state, tenant_id, plan_id).await?;
|
||||
let limit = page_size.min(100);
|
||||
let offset = page.saturating_sub(1) * limit;
|
||||
|
||||
let query = care_plan_outcome::Entity::find()
|
||||
.filter(care_plan_outcome::Column::TenantId.eq(tenant_id))
|
||||
.filter(care_plan_outcome::Column::PlanId.eq(plan_id))
|
||||
.filter(care_plan_outcome::Column::DeletedAt.is_null());
|
||||
|
||||
let total: u64 = query.clone().count(&state.db).await?;
|
||||
let rows: Vec<care_plan_outcome::Model> = query
|
||||
.order_by_desc(care_plan_outcome::Column::CreatedAt)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.all(&state.db)
|
||||
.await?;
|
||||
|
||||
let total_pages = total.div_ceil(limit.max(1));
|
||||
let data = rows.into_iter().map(outcome_to_resp).collect();
|
||||
|
||||
Ok(PaginatedResponse {
|
||||
data,
|
||||
total,
|
||||
page,
|
||||
page_size,
|
||||
total_pages,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_care_plan_outcome(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
plan_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: CreateCarePlanOutcomeReq,
|
||||
) -> HealthResult<CarePlanOutcomeResp> {
|
||||
let _plan = find_plan(state, tenant_id, plan_id).await?;
|
||||
|
||||
let now = Utc::now();
|
||||
let active = care_plan_outcome::ActiveModel {
|
||||
id: Set(Uuid::now_v7()),
|
||||
tenant_id: Set(tenant_id),
|
||||
plan_id: Set(plan_id),
|
||||
item_id: Set(req.item_id),
|
||||
metric: Set(req.metric),
|
||||
baseline_value: Set(req.baseline_value),
|
||||
target_value: Set(req.target_value),
|
||||
current_value: Set(req.current_value),
|
||||
measured_at: Set(req.measured_at),
|
||||
notes: Set(req.notes),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(operator_id),
|
||||
updated_by: Set(operator_id),
|
||||
deleted_at: Set(None),
|
||||
version: Set(1),
|
||||
};
|
||||
let m = active.insert(&state.db).await?;
|
||||
|
||||
Ok(outcome_to_resp(m))
|
||||
}
|
||||
|
||||
pub async fn update_care_plan_outcome(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
plan_id: Uuid,
|
||||
outcome_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: UpdateCarePlanOutcomeWithVersion,
|
||||
) -> HealthResult<CarePlanOutcomeResp> {
|
||||
let _plan = find_plan(state, tenant_id, plan_id).await?;
|
||||
let existing = care_plan_outcome::Entity::find_by_id(outcome_id)
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::CarePlanOutcomeNotFound)?;
|
||||
|
||||
if existing.tenant_id != tenant_id || existing.plan_id != plan_id {
|
||||
return Err(HealthError::CarePlanOutcomeNotFound);
|
||||
}
|
||||
|
||||
let next_ver = check_version(req.version, existing.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
let mut active: care_plan_outcome::ActiveModel = existing.into();
|
||||
let now = Utc::now();
|
||||
|
||||
if req.data.current_value.is_some() {
|
||||
active.current_value = Set(req.data.current_value);
|
||||
active.measured_at = Set(Some(now));
|
||||
}
|
||||
if let Some(v) = req.data.target_value {
|
||||
active.target_value = Set(v);
|
||||
}
|
||||
if req.data.measured_at.is_some() {
|
||||
active.measured_at = Set(req.data.measured_at);
|
||||
}
|
||||
if req.data.notes.is_some() {
|
||||
active.notes = Set(req.data.notes);
|
||||
}
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_ver);
|
||||
|
||||
let m = active.update(&state.db).await?;
|
||||
Ok(outcome_to_resp(m))
|
||||
}
|
||||
|
||||
pub async fn delete_care_plan_outcome(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
plan_id: Uuid,
|
||||
outcome_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
version: i32,
|
||||
) -> HealthResult<()> {
|
||||
let _plan = find_plan(state, tenant_id, plan_id).await?;
|
||||
let existing = care_plan_outcome::Entity::find_by_id(outcome_id)
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::CarePlanOutcomeNotFound)?;
|
||||
|
||||
if existing.tenant_id != tenant_id || existing.plan_id != plan_id {
|
||||
return Err(HealthError::CarePlanOutcomeNotFound);
|
||||
}
|
||||
|
||||
let next_ver = check_version(version, existing.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let mut active: care_plan_outcome::ActiveModel = existing.into();
|
||||
active.deleted_at = Set(Some(now));
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_ver);
|
||||
active.update(&state.db).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn find_plan(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
plan_id: Uuid,
|
||||
) -> HealthResult<care_plan::Model> {
|
||||
care_plan::Entity::find_by_id(plan_id)
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.filter(|m| m.tenant_id == tenant_id && m.deleted_at.is_none())
|
||||
.ok_or(HealthError::CarePlanNotFound)
|
||||
}
|
||||
|
||||
fn plan_to_resp(m: care_plan::Model) -> CarePlanResp {
|
||||
CarePlanResp {
|
||||
id: m.id,
|
||||
patient_id: m.patient_id,
|
||||
plan_type: m.plan_type,
|
||||
status: m.status,
|
||||
title: m.title,
|
||||
goals: m.goals,
|
||||
start_date: m.start_date,
|
||||
end_date: m.end_date,
|
||||
notes: m.notes,
|
||||
created_at: m.created_at,
|
||||
updated_at: m.updated_at,
|
||||
version: m.version,
|
||||
}
|
||||
}
|
||||
|
||||
fn item_to_resp(m: care_plan_item::Model) -> CarePlanItemResp {
|
||||
CarePlanItemResp {
|
||||
id: m.id,
|
||||
plan_id: m.plan_id,
|
||||
item_type: m.item_type,
|
||||
title: m.title,
|
||||
description: m.description,
|
||||
status: m.status,
|
||||
schedule: m.schedule,
|
||||
sort_order: m.sort_order,
|
||||
created_at: m.created_at,
|
||||
updated_at: m.updated_at,
|
||||
version: m.version,
|
||||
}
|
||||
}
|
||||
|
||||
fn outcome_to_resp(m: care_plan_outcome::Model) -> CarePlanOutcomeResp {
|
||||
CarePlanOutcomeResp {
|
||||
id: m.id,
|
||||
plan_id: m.plan_id,
|
||||
item_id: m.item_id,
|
||||
metric: m.metric,
|
||||
baseline_value: m.baseline_value,
|
||||
target_value: m.target_value,
|
||||
current_value: m.current_value,
|
||||
measured_at: m.measured_at,
|
||||
notes: m.notes,
|
||||
created_at: m.created_at,
|
||||
updated_at: m.updated_at,
|
||||
version: m.version,
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_plan_type(plan_type: &str) -> HealthResult<()> {
|
||||
let valid = ["dialysis", "chronic", "preventive", "rehabilitation"];
|
||||
if valid.contains(&plan_type) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(HealthError::Validation(format!(
|
||||
"plan_type 必须为以下之一: {}",
|
||||
valid.join(", ")
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_plan_status(status: &str) -> HealthResult<()> {
|
||||
let valid = ["draft", "active", "paused", "completed", "cancelled"];
|
||||
if valid.contains(&status) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(HealthError::Validation(format!(
|
||||
"status 必须为以下之一: {}",
|
||||
valid.join(", ")
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_item_type(item_type: &str) -> HealthResult<()> {
|
||||
let valid = ["intervention", "monitoring", "goal", "education"];
|
||||
if valid.contains(&item_type) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(HealthError::Validation(format!(
|
||||
"item_type 必须为以下之一: {}",
|
||||
valid.join(", ")
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ pub mod appointment_service;
|
||||
pub mod article_category_service;
|
||||
pub mod article_service;
|
||||
pub mod article_tag_service;
|
||||
pub mod care_plan_service;
|
||||
pub mod consultation_service;
|
||||
pub mod consent_service;
|
||||
pub mod critical_alert_service;
|
||||
|
||||
@@ -110,6 +110,7 @@ mod m20260504_000107_alter_article_article_tag_add_tenant_and_soft_delete;
|
||||
mod m20260504_000108_alter_vital_signs_hourly_add_soft_delete;
|
||||
mod m20260504_000109_add_missing_fk_constraints;
|
||||
mod m20260504_000110_alter_critical_alerts_version_i32;
|
||||
mod m20260505_000111_create_care_plan;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -227,6 +228,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260504_000108_alter_vital_signs_hourly_add_soft_delete::Migration),
|
||||
Box::new(m20260504_000109_add_missing_fk_constraints::Migration),
|
||||
Box::new(m20260504_000110_alter_critical_alerts_version_i32::Migration),
|
||||
Box::new(m20260505_000111_create_care_plan::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// care_plans — 护理计划主表
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Alias::new("care_plans"))
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("id"))
|
||||
.uuid()
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Alias::new("tenant_id")).uuid().not_null())
|
||||
.col(ColumnDef::new(Alias::new("patient_id")).uuid().not_null())
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("plan_type"))
|
||||
.string_len(50)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("status"))
|
||||
.string_len(32)
|
||||
.not_null()
|
||||
.default("draft"),
|
||||
)
|
||||
.col(ColumnDef::new(Alias::new("title")).string_len(200).not_null())
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("goals"))
|
||||
.json_binary()
|
||||
.not_null()
|
||||
.default("[]"),
|
||||
)
|
||||
.col(ColumnDef::new(Alias::new("start_date")).date())
|
||||
.col(ColumnDef::new(Alias::new("end_date")).date())
|
||||
.col(ColumnDef::new(Alias::new("notes")).text())
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("created_at"))
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("updated_at"))
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(ColumnDef::new(Alias::new("created_by")).uuid())
|
||||
.col(ColumnDef::new(Alias::new("updated_by")).uuid())
|
||||
.col(ColumnDef::new(Alias::new("deleted_at")).timestamp_with_time_zone())
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("version"))
|
||||
.integer()
|
||||
.not_null()
|
||||
.default(1),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_care_plans_tenant_patient")
|
||||
.table(Alias::new("care_plans"))
|
||||
.col(Alias::new("tenant_id"))
|
||||
.col(Alias::new("patient_id"))
|
||||
.col(Alias::new("deleted_at"))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_care_plans_tenant_status")
|
||||
.table(Alias::new("care_plans"))
|
||||
.col(Alias::new("tenant_id"))
|
||||
.col(Alias::new("status"))
|
||||
.col(Alias::new("deleted_at"))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// care_plan_items — 护理计划条目(干预/监测/目标)
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Alias::new("care_plan_items"))
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("id"))
|
||||
.uuid()
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Alias::new("tenant_id")).uuid().not_null())
|
||||
.col(ColumnDef::new(Alias::new("plan_id")).uuid().not_null())
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("item_type"))
|
||||
.string_len(32)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("title"))
|
||||
.string_len(200)
|
||||
.not_null(),
|
||||
)
|
||||
.col(ColumnDef::new(Alias::new("description")).text())
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("status"))
|
||||
.string_len(32)
|
||||
.not_null()
|
||||
.default("pending"),
|
||||
)
|
||||
.col(ColumnDef::new(Alias::new("schedule")).string_len(100))
|
||||
.col(ColumnDef::new(Alias::new("sort_order")).integer().default(0))
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("created_at"))
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("updated_at"))
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(ColumnDef::new(Alias::new("created_by")).uuid())
|
||||
.col(ColumnDef::new(Alias::new("updated_by")).uuid())
|
||||
.col(ColumnDef::new(Alias::new("deleted_at")).timestamp_with_time_zone())
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("version"))
|
||||
.integer()
|
||||
.not_null()
|
||||
.default(1),
|
||||
)
|
||||
.foreign_key(
|
||||
&mut ForeignKey::create()
|
||||
.name("fk_care_plan_items_plan_id")
|
||||
.from(Alias::new("care_plan_items"), Alias::new("plan_id"))
|
||||
.to(Alias::new("care_plans"), Alias::new("id"))
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.to_owned(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_care_plan_items_tenant_plan")
|
||||
.table(Alias::new("care_plan_items"))
|
||||
.col(Alias::new("tenant_id"))
|
||||
.col(Alias::new("plan_id"))
|
||||
.col(Alias::new("deleted_at"))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// care_plan_outcomes — 护理计划预后测量
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Alias::new("care_plan_outcomes"))
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("id"))
|
||||
.uuid()
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Alias::new("tenant_id")).uuid().not_null())
|
||||
.col(ColumnDef::new(Alias::new("plan_id")).uuid().not_null())
|
||||
.col(ColumnDef::new(Alias::new("item_id")).uuid())
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("metric"))
|
||||
.string_len(100)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("baseline_value"))
|
||||
.string_len(50)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("target_value"))
|
||||
.string_len(50)
|
||||
.not_null(),
|
||||
)
|
||||
.col(ColumnDef::new(Alias::new("current_value")).string_len(50))
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("measured_at"))
|
||||
.timestamp_with_time_zone(),
|
||||
)
|
||||
.col(ColumnDef::new(Alias::new("notes")).text())
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("created_at"))
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("updated_at"))
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(ColumnDef::new(Alias::new("created_by")).uuid())
|
||||
.col(ColumnDef::new(Alias::new("updated_by")).uuid())
|
||||
.col(ColumnDef::new(Alias::new("deleted_at")).timestamp_with_time_zone())
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("version"))
|
||||
.integer()
|
||||
.not_null()
|
||||
.default(1),
|
||||
)
|
||||
.foreign_key(
|
||||
&mut ForeignKey::create()
|
||||
.name("fk_care_plan_outcomes_plan_id")
|
||||
.from(Alias::new("care_plan_outcomes"), Alias::new("plan_id"))
|
||||
.to(Alias::new("care_plans"), Alias::new("id"))
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.to_owned(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_care_plan_outcomes_tenant_plan")
|
||||
.table(Alias::new("care_plan_outcomes"))
|
||||
.col(Alias::new("tenant_id"))
|
||||
.col(Alias::new("plan_id"))
|
||||
.col(Alias::new("deleted_at"))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Alias::new("care_plan_outcomes")).to_owned())
|
||||
.await?;
|
||||
manager
|
||||
.drop_table(Table::drop().table(Alias::new("care_plan_items")).to_owned())
|
||||
.await?;
|
||||
manager
|
||||
.drop_table(Table::drop().table(Alias::new("care_plans")).to_owned())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ async fn seed_hourly(
|
||||
sample_count: Set(1),
|
||||
created_at: Set(chrono::Utc::now()),
|
||||
updated_at: Set(chrono::Utc::now()),
|
||||
deleted_at: Set(None),
|
||||
version: Set(1),
|
||||
};
|
||||
model.insert(app.db()).await.expect("插入 hourly 应成功");
|
||||
|
||||
Reference in New Issue
Block a user