feat(health): 护理计划实体与服务 — Phase 1 关怀引擎 MVP 第一步
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

新增护理计划(Care Plan)完整 CRUD:3 张表(care_plans / care_plan_items /
care_plan_outcomes)、3 个 SeaORM Entity、15 个 API 端点、4 个事件常量、
2 个权限码。支持透析/慢性/预防/康复计划类型,条目分干预/监测/目标/教育四类,
预后测量含基线/目标/当前值追踪。
This commit is contained in:
iven
2026-05-04 18:40:22 +08:00
parent c35ea83799
commit ef422f354d
16 changed files with 1662 additions and 2 deletions

View 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,
}

View File

@@ -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;

View 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 {}

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 = "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 {}

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 = "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 {}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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

View 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, &params).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(())))
}

View File

@@ -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;

View File

@@ -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(),
},
]
}

View 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(", ")
)))
}
}

View File

@@ -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;

View File

@@ -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),
]
}
}

View File

@@ -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(())
}
}

View File

@@ -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 应成功");