feat(health): 随访模板系统 — follow_up_template + template_field 全栈
新增随访模板和模板字段两张表及完整 CRUD: - 迁移 083: follow_up_template + follow_up_template_field - Entity: 模板(名称/类型/适用范围/状态) + 字段(标签/键名/类型/选项/校验) - DTO: 创建时内嵌字段列表、更新支持全量替换字段 - Service: 随访类型+字段类型校验、级联软删除 - Handler: 5 端点 + RBAC 权限 - 路由: /api/v1/health/follow-up-templates
This commit is contained in:
117
crates/erp-health/src/dto/follow_up_template_dto.rs
Normal file
117
crates/erp-health/src/dto/follow_up_template_dto.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::{IntoParams, ToSchema};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use erp_core::sanitize::sanitize_option;
|
||||||
|
|
||||||
|
// ── 模板字段 DTO ──
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct TemplateFieldReq {
|
||||||
|
pub label: String,
|
||||||
|
pub field_key: String,
|
||||||
|
/// text/number/date/select/checkbox/textarea/scale
|
||||||
|
pub field_type: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub required: bool,
|
||||||
|
pub options: Option<String>,
|
||||||
|
pub placeholder: Option<String>,
|
||||||
|
pub validation: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub sort_order: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct TemplateFieldResp {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub template_id: Uuid,
|
||||||
|
pub label: String,
|
||||||
|
pub field_key: String,
|
||||||
|
pub field_type: String,
|
||||||
|
pub required: bool,
|
||||||
|
pub options: Option<String>,
|
||||||
|
pub placeholder: Option<String>,
|
||||||
|
pub validation: Option<String>,
|
||||||
|
pub sort_order: i32,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
pub version: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 模板 DTO ──
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, IntoParams)]
|
||||||
|
pub struct FollowUpTemplateListQuery {
|
||||||
|
pub page: Option<u64>,
|
||||||
|
pub page_size: Option<u64>,
|
||||||
|
pub follow_up_type: Option<String>,
|
||||||
|
pub status: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct CreateFollowUpTemplateReq {
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
/// phone/outpatient/home_visit/online/wechat
|
||||||
|
pub follow_up_type: String,
|
||||||
|
pub applicable_scope: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub fields: Vec<TemplateFieldReq>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CreateFollowUpTemplateReq {
|
||||||
|
pub fn sanitize(&mut self) {
|
||||||
|
self.name = self.name.trim().to_string();
|
||||||
|
self.description = sanitize_option(self.description.take());
|
||||||
|
self.applicable_scope = sanitize_option(self.applicable_scope.take());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct UpdateFollowUpTemplateReq {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub follow_up_type: Option<String>,
|
||||||
|
pub applicable_scope: Option<String>,
|
||||||
|
pub status: Option<String>,
|
||||||
|
/// 全量替换字段列表
|
||||||
|
pub fields: Option<Vec<TemplateFieldReq>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateFollowUpTemplateReq {
|
||||||
|
pub fn sanitize(&mut self) {
|
||||||
|
if let Some(ref mut n) = self.name {
|
||||||
|
*n = n.trim().to_string();
|
||||||
|
}
|
||||||
|
self.description = sanitize_option(self.description.take());
|
||||||
|
self.applicable_scope = sanitize_option(self.applicable_scope.take());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct FollowUpTemplateResp {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub follow_up_type: String,
|
||||||
|
pub applicable_scope: Option<String>,
|
||||||
|
pub status: String,
|
||||||
|
pub fields: Vec<TemplateFieldResp>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
pub version: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct FollowUpTemplateListItemResp {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub follow_up_type: String,
|
||||||
|
pub status: String,
|
||||||
|
pub field_count: i64,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
pub version: i32,
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ pub mod dialysis_dto;
|
|||||||
pub mod dialysis_prescription_dto;
|
pub mod dialysis_prescription_dto;
|
||||||
pub mod doctor_dto;
|
pub mod doctor_dto;
|
||||||
pub mod follow_up_dto;
|
pub mod follow_up_dto;
|
||||||
|
pub mod follow_up_template_dto;
|
||||||
pub mod health_data_dto;
|
pub mod health_data_dto;
|
||||||
pub mod patient_dto;
|
pub mod patient_dto;
|
||||||
pub mod points_dto;
|
pub mod points_dto;
|
||||||
|
|||||||
45
crates/erp-health/src/entity/follow_up_template.rs
Normal file
45
crates/erp-health/src/entity/follow_up_template.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "follow_up_template")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub id: Uuid,
|
||||||
|
pub tenant_id: Uuid,
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub name: String,
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<String>,
|
||||||
|
// phone/outpatient/home_visit/online/wechat
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub follow_up_type: String,
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub applicable_scope: Option<String>,
|
||||||
|
// active/disabled
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub status: String,
|
||||||
|
pub created_at: DateTimeUtc,
|
||||||
|
pub updated_at: DateTimeUtc,
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_by: Option<Uuid>,
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub updated_by: Option<Uuid>,
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub deleted_at: Option<DateTimeUtc>,
|
||||||
|
pub version: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(has_many = "super::follow_up_template_field::Entity")]
|
||||||
|
Fields,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::follow_up_template_field::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Fields.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
54
crates/erp-health/src/entity/follow_up_template_field.rs
Normal file
54
crates/erp-health/src/entity/follow_up_template_field.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "follow_up_template_field")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub id: Uuid,
|
||||||
|
pub tenant_id: Uuid,
|
||||||
|
pub template_id: Uuid,
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub label: String,
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub field_key: String,
|
||||||
|
// text/number/date/select/checkbox/textarea/scale
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub field_type: String,
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub required: bool,
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub options: Option<String>,
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub placeholder: Option<String>,
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub validation: Option<String>,
|
||||||
|
pub sort_order: 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::follow_up_template::Entity",
|
||||||
|
from = "Column::TemplateId",
|
||||||
|
to = "super::follow_up_template::Column::Id"
|
||||||
|
)]
|
||||||
|
Template,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::follow_up_template::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Template.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
@@ -19,6 +19,8 @@ pub mod doctor_profile;
|
|||||||
pub mod doctor_schedule;
|
pub mod doctor_schedule;
|
||||||
pub mod follow_up_record;
|
pub mod follow_up_record;
|
||||||
pub mod follow_up_task;
|
pub mod follow_up_task;
|
||||||
|
pub mod follow_up_template;
|
||||||
|
pub mod follow_up_template_field;
|
||||||
pub mod health_record;
|
pub mod health_record;
|
||||||
pub mod health_trend;
|
pub mod health_trend;
|
||||||
pub mod lab_report;
|
pub mod lab_report;
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ pub enum HealthError {
|
|||||||
#[error("透析方案不存在")]
|
#[error("透析方案不存在")]
|
||||||
DialysisPrescriptionNotFound,
|
DialysisPrescriptionNotFound,
|
||||||
|
|
||||||
|
#[error("随访模板不存在")]
|
||||||
|
FollowUpTemplateNotFound,
|
||||||
|
|
||||||
#[error("状态转换无效: {0}")]
|
#[error("状态转换无效: {0}")]
|
||||||
InvalidStatusTransition(String),
|
InvalidStatusTransition(String),
|
||||||
|
|
||||||
@@ -113,7 +116,8 @@ impl From<HealthError> for AppError {
|
|||||||
| HealthError::ConsentNotFound
|
| HealthError::ConsentNotFound
|
||||||
| HealthError::AlertRuleNotFound
|
| HealthError::AlertRuleNotFound
|
||||||
| HealthError::AlertNotFound
|
| HealthError::AlertNotFound
|
||||||
| HealthError::DialysisPrescriptionNotFound => AppError::NotFound(err.to_string()),
|
| HealthError::DialysisPrescriptionNotFound
|
||||||
|
| HealthError::FollowUpTemplateNotFound => AppError::NotFound(err.to_string()),
|
||||||
HealthError::ScheduleFull => AppError::Validation(err.to_string()),
|
HealthError::ScheduleFull => AppError::Validation(err.to_string()),
|
||||||
HealthError::InvalidStatusTransition(s) => AppError::Validation(s),
|
HealthError::InvalidStatusTransition(s) => AppError::Validation(s),
|
||||||
HealthError::VersionMismatch => AppError::VersionMismatch,
|
HealthError::VersionMismatch => AppError::VersionMismatch,
|
||||||
|
|||||||
119
crates/erp-health/src/handler/follow_up_template_handler.rs
Normal file
119
crates/erp-health/src/handler/follow_up_template_handler.rs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
use axum::Extension;
|
||||||
|
use axum::extract::{FromRef, Json, Path, Query, State};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use erp_core::error::AppError;
|
||||||
|
use erp_core::rbac::require_permission;
|
||||||
|
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||||
|
|
||||||
|
use crate::dto::follow_up_template_dto::*;
|
||||||
|
use crate::dto::DeleteWithVersion;
|
||||||
|
use crate::service::follow_up_template_service;
|
||||||
|
use crate::state::HealthState;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, IntoParams)]
|
||||||
|
pub struct FollowUpTemplateListParams {
|
||||||
|
pub page: Option<u64>,
|
||||||
|
pub page_size: Option<u64>,
|
||||||
|
pub follow_up_type: Option<String>,
|
||||||
|
pub status: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct UpdateFollowUpTemplateWithVersion {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub data: UpdateFollowUpTemplateReq,
|
||||||
|
pub version: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_templates<S>(
|
||||||
|
State(state): State<HealthState>,
|
||||||
|
Extension(ctx): Extension<TenantContext>,
|
||||||
|
Query(params): Query<FollowUpTemplateListParams>,
|
||||||
|
) -> Result<Json<ApiResponse<PaginatedResponse<FollowUpTemplateListItemResp>>>, AppError>
|
||||||
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
require_permission(&ctx, "health.follow-up-template.list")?;
|
||||||
|
let page = params.page.unwrap_or(1);
|
||||||
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
|
let result = follow_up_template_service::list_templates(
|
||||||
|
&state, ctx.tenant_id, page, page_size, params.follow_up_type, params.status,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_template<S>(
|
||||||
|
State(state): State<HealthState>,
|
||||||
|
Extension(ctx): Extension<TenantContext>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
) -> Result<Json<ApiResponse<FollowUpTemplateResp>>, AppError>
|
||||||
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
require_permission(&ctx, "health.follow-up-template.list")?;
|
||||||
|
let result = follow_up_template_service::get_template(&state, ctx.tenant_id, id).await?;
|
||||||
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_template<S>(
|
||||||
|
State(state): State<HealthState>,
|
||||||
|
Extension(ctx): Extension<TenantContext>,
|
||||||
|
Json(req): Json<CreateFollowUpTemplateReq>,
|
||||||
|
) -> Result<Json<ApiResponse<FollowUpTemplateResp>>, AppError>
|
||||||
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
require_permission(&ctx, "health.follow-up-template.manage")?;
|
||||||
|
let mut req = req;
|
||||||
|
req.sanitize();
|
||||||
|
let result = follow_up_template_service::create_template(
|
||||||
|
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_template<S>(
|
||||||
|
State(state): State<HealthState>,
|
||||||
|
Extension(ctx): Extension<TenantContext>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
Json(req): Json<UpdateFollowUpTemplateWithVersion>,
|
||||||
|
) -> Result<Json<ApiResponse<FollowUpTemplateResp>>, AppError>
|
||||||
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
require_permission(&ctx, "health.follow-up-template.manage")?;
|
||||||
|
let mut data = req.data;
|
||||||
|
data.sanitize();
|
||||||
|
let result = follow_up_template_service::update_template(
|
||||||
|
&state, ctx.tenant_id, id, Some(ctx.user_id), data, req.version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_template<S>(
|
||||||
|
State(state): State<HealthState>,
|
||||||
|
Extension(ctx): Extension<TenantContext>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
Json(req): Json<DeleteWithVersion>,
|
||||||
|
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||||
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
require_permission(&ctx, "health.follow-up-template.manage")?;
|
||||||
|
follow_up_template_service::delete_template(
|
||||||
|
&state, ctx.tenant_id, id, Some(ctx.user_id), req.version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(ApiResponse::ok(())))
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ pub mod dialysis_handler;
|
|||||||
pub mod dialysis_prescription_handler;
|
pub mod dialysis_prescription_handler;
|
||||||
pub mod doctor_handler;
|
pub mod doctor_handler;
|
||||||
pub mod follow_up_handler;
|
pub mod follow_up_handler;
|
||||||
|
pub mod follow_up_template_handler;
|
||||||
pub mod health_data_handler;
|
pub mod health_data_handler;
|
||||||
pub mod patient_handler;
|
pub mod patient_handler;
|
||||||
pub mod points_handler;
|
pub mod points_handler;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use erp_core::module::{ErpModule, PermissionDescriptor};
|
|||||||
|
|
||||||
use crate::handler::{
|
use crate::handler::{
|
||||||
alert_handler, alert_rule_handler,
|
alert_handler, alert_rule_handler,
|
||||||
appointment_handler, article_category_handler, article_handler, article_tag_handler, consultation_handler, consent_handler, critical_value_threshold_handler, daily_monitoring_handler, device_reading_handler, diagnosis_handler, dialysis_handler, dialysis_prescription_handler, doctor_handler, follow_up_handler,
|
appointment_handler, article_category_handler, article_handler, article_tag_handler, consultation_handler, consent_handler, critical_value_threshold_handler, daily_monitoring_handler, device_reading_handler, diagnosis_handler, dialysis_handler, dialysis_prescription_handler, doctor_handler, follow_up_handler, follow_up_template_handler,
|
||||||
health_data_handler, medication_record_handler, patient_handler, points_handler, stats_handler,
|
health_data_handler, medication_record_handler, patient_handler, points_handler, stats_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -234,6 +234,18 @@ impl HealthModule {
|
|||||||
.put(dialysis_prescription_handler::update_prescription)
|
.put(dialysis_prescription_handler::update_prescription)
|
||||||
.delete(dialysis_prescription_handler::delete_prescription),
|
.delete(dialysis_prescription_handler::delete_prescription),
|
||||||
)
|
)
|
||||||
|
// 随访模板
|
||||||
|
.route(
|
||||||
|
"/health/follow-up-templates",
|
||||||
|
axum::routing::get(follow_up_template_handler::list_templates)
|
||||||
|
.post(follow_up_template_handler::create_template),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/health/follow-up-templates/{id}",
|
||||||
|
axum::routing::get(follow_up_template_handler::get_template)
|
||||||
|
.put(follow_up_template_handler::update_template)
|
||||||
|
.delete(follow_up_template_handler::delete_template),
|
||||||
|
)
|
||||||
// 日常监测
|
// 日常监测
|
||||||
.route(
|
.route(
|
||||||
"/health/patients/{id}/daily-monitoring",
|
"/health/patients/{id}/daily-monitoring",
|
||||||
|
|||||||
346
crates/erp-health/src/service/follow_up_template_service.rs
Normal file
346
crates/erp-health/src/service/follow_up_template_service.rs
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
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::types::PaginatedResponse;
|
||||||
|
|
||||||
|
use crate::dto::follow_up_template_dto::*;
|
||||||
|
use crate::entity::{follow_up_template, follow_up_template_field};
|
||||||
|
use crate::error::{HealthError, HealthResult};
|
||||||
|
use crate::state::HealthState;
|
||||||
|
|
||||||
|
const VALID_FOLLOW_UP_TYPES: &[&str] = &["phone", "outpatient", "home_visit", "online", "wechat"];
|
||||||
|
const VALID_FIELD_TYPES: &[&str] = &["text", "number", "date", "select", "checkbox", "textarea", "scale"];
|
||||||
|
|
||||||
|
pub async fn list_templates(
|
||||||
|
state: &HealthState,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
page: u64,
|
||||||
|
page_size: u64,
|
||||||
|
follow_up_type: Option<String>,
|
||||||
|
status: Option<String>,
|
||||||
|
) -> HealthResult<PaginatedResponse<FollowUpTemplateListItemResp>> {
|
||||||
|
let limit = page_size.min(100);
|
||||||
|
let offset = page.saturating_sub(1) * limit;
|
||||||
|
|
||||||
|
let mut query = follow_up_template::Entity::find()
|
||||||
|
.filter(follow_up_template::Column::TenantId.eq(tenant_id))
|
||||||
|
.filter(follow_up_template::Column::DeletedAt.is_null());
|
||||||
|
|
||||||
|
if let Some(ref t) = follow_up_type {
|
||||||
|
query = query.filter(follow_up_template::Column::FollowUpType.eq(t));
|
||||||
|
}
|
||||||
|
if let Some(ref s) = status {
|
||||||
|
query = query.filter(follow_up_template::Column::Status.eq(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
let total = query.clone().count(&state.db).await?;
|
||||||
|
let models = query
|
||||||
|
.order_by_desc(follow_up_template::Column::CreatedAt)
|
||||||
|
.offset(offset)
|
||||||
|
.limit(limit)
|
||||||
|
.all(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut data = Vec::with_capacity(models.len());
|
||||||
|
for m in models {
|
||||||
|
let field_count = follow_up_template_field::Entity::find()
|
||||||
|
.filter(follow_up_template_field::Column::TemplateId.eq(m.id))
|
||||||
|
.filter(follow_up_template_field::Column::DeletedAt.is_null())
|
||||||
|
.count(&state.db)
|
||||||
|
.await?;
|
||||||
|
data.push(FollowUpTemplateListItemResp {
|
||||||
|
id: m.id,
|
||||||
|
name: m.name,
|
||||||
|
description: m.description,
|
||||||
|
follow_up_type: m.follow_up_type,
|
||||||
|
status: m.status,
|
||||||
|
field_count: field_count as i64,
|
||||||
|
created_at: m.created_at,
|
||||||
|
updated_at: m.updated_at,
|
||||||
|
version: m.version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_pages = total.div_ceil(limit.max(1));
|
||||||
|
Ok(PaginatedResponse { data, total, page, page_size: limit, total_pages })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_template(
|
||||||
|
state: &HealthState,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
id: Uuid,
|
||||||
|
) -> HealthResult<FollowUpTemplateResp> {
|
||||||
|
let m = follow_up_template::Entity::find()
|
||||||
|
.filter(follow_up_template::Column::Id.eq(id))
|
||||||
|
.filter(follow_up_template::Column::TenantId.eq(tenant_id))
|
||||||
|
.filter(follow_up_template::Column::DeletedAt.is_null())
|
||||||
|
.one(&state.db)
|
||||||
|
.await?
|
||||||
|
.ok_or(HealthError::FollowUpTemplateNotFound)?;
|
||||||
|
|
||||||
|
let fields = load_fields(&state, id).await?;
|
||||||
|
Ok(template_to_resp(m, fields))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_template(
|
||||||
|
state: &HealthState,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
operator_id: Option<Uuid>,
|
||||||
|
req: CreateFollowUpTemplateReq,
|
||||||
|
) -> HealthResult<FollowUpTemplateResp> {
|
||||||
|
validate_follow_up_type(&req.follow_up_type)?;
|
||||||
|
for f in &req.fields {
|
||||||
|
validate_field_type(&f.field_type)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
|
let template_id = Uuid::now_v7();
|
||||||
|
let active = follow_up_template::ActiveModel {
|
||||||
|
id: Set(template_id),
|
||||||
|
tenant_id: Set(tenant_id),
|
||||||
|
name: Set(req.name),
|
||||||
|
description: Set(req.description),
|
||||||
|
follow_up_type: Set(req.follow_up_type),
|
||||||
|
applicable_scope: Set(req.applicable_scope),
|
||||||
|
status: Set("active".to_string()),
|
||||||
|
created_at: Set(now),
|
||||||
|
updated_at: Set(now),
|
||||||
|
created_by: Set(operator_id),
|
||||||
|
updated_by: Set(operator_id),
|
||||||
|
deleted_at: Set(None),
|
||||||
|
version: Set(1),
|
||||||
|
};
|
||||||
|
let m = active.insert(&state.db).await?;
|
||||||
|
|
||||||
|
let mut field_resps = Vec::with_capacity(req.fields.len());
|
||||||
|
for (i, f) in req.fields.into_iter().enumerate() {
|
||||||
|
let field = insert_field(&state, tenant_id, template_id, operator_id, f, i as i32).await?;
|
||||||
|
field_resps.push(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
audit_service::record(
|
||||||
|
AuditLog::new(tenant_id, operator_id, "follow_up_template.created", "follow_up_template")
|
||||||
|
.with_resource_id(m.id),
|
||||||
|
&state.db,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
Ok(template_to_resp(m, field_resps))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_template(
|
||||||
|
state: &HealthState,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
id: Uuid,
|
||||||
|
operator_id: Option<Uuid>,
|
||||||
|
req: UpdateFollowUpTemplateReq,
|
||||||
|
expected_version: i32,
|
||||||
|
) -> HealthResult<FollowUpTemplateResp> {
|
||||||
|
let model = follow_up_template::Entity::find()
|
||||||
|
.filter(follow_up_template::Column::Id.eq(id))
|
||||||
|
.filter(follow_up_template::Column::TenantId.eq(tenant_id))
|
||||||
|
.filter(follow_up_template::Column::DeletedAt.is_null())
|
||||||
|
.one(&state.db)
|
||||||
|
.await?
|
||||||
|
.ok_or(HealthError::FollowUpTemplateNotFound)?;
|
||||||
|
|
||||||
|
let next_ver = check_version(expected_version, model.version)
|
||||||
|
.map_err(|_| HealthError::VersionMismatch)?;
|
||||||
|
|
||||||
|
if let Some(ref t) = req.follow_up_type { validate_follow_up_type(t)?; }
|
||||||
|
if let Some(ref fields) = req.fields {
|
||||||
|
for f in fields { validate_field_type(&f.field_type)?; }
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut active: follow_up_template::ActiveModel = model.into();
|
||||||
|
if let Some(v) = req.name { active.name = Set(v); }
|
||||||
|
if let Some(v) = req.description { active.description = Set(Some(v)); }
|
||||||
|
if let Some(v) = req.follow_up_type { active.follow_up_type = Set(v); }
|
||||||
|
if let Some(v) = req.applicable_scope { active.applicable_scope = Set(Some(v)); }
|
||||||
|
if let Some(v) = req.status { active.status = Set(v); }
|
||||||
|
active.updated_at = Set(Utc::now());
|
||||||
|
active.updated_by = Set(operator_id);
|
||||||
|
active.version = Set(next_ver);
|
||||||
|
|
||||||
|
let m = active.update(&state.db).await?;
|
||||||
|
|
||||||
|
// 全量替换字段
|
||||||
|
let field_resps = if let Some(fields) = req.fields {
|
||||||
|
// 软删除旧字段
|
||||||
|
let old_fields = follow_up_template_field::Entity::find()
|
||||||
|
.filter(follow_up_template_field::Column::TemplateId.eq(id))
|
||||||
|
.filter(follow_up_template_field::Column::DeletedAt.is_null())
|
||||||
|
.all(&state.db)
|
||||||
|
.await?;
|
||||||
|
for old in old_fields {
|
||||||
|
let mut af: follow_up_template_field::ActiveModel = old.into();
|
||||||
|
af.deleted_at = Set(Some(Utc::now()));
|
||||||
|
af.update(&state.db).await?;
|
||||||
|
}
|
||||||
|
// 插入新字段
|
||||||
|
let mut resps = Vec::with_capacity(fields.len());
|
||||||
|
for (i, f) in fields.into_iter().enumerate() {
|
||||||
|
let resp = insert_field(&state, tenant_id, id, operator_id, f, i as i32).await?;
|
||||||
|
resps.push(resp);
|
||||||
|
}
|
||||||
|
resps
|
||||||
|
} else {
|
||||||
|
load_fields(&state, id).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
audit_service::record(
|
||||||
|
AuditLog::new(tenant_id, operator_id, "follow_up_template.updated", "follow_up_template")
|
||||||
|
.with_resource_id(m.id),
|
||||||
|
&state.db,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
Ok(template_to_resp(m, field_resps))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_template(
|
||||||
|
state: &HealthState,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
id: Uuid,
|
||||||
|
operator_id: Option<Uuid>,
|
||||||
|
expected_version: i32,
|
||||||
|
) -> HealthResult<()> {
|
||||||
|
let model = follow_up_template::Entity::find()
|
||||||
|
.filter(follow_up_template::Column::Id.eq(id))
|
||||||
|
.filter(follow_up_template::Column::TenantId.eq(tenant_id))
|
||||||
|
.filter(follow_up_template::Column::DeletedAt.is_null())
|
||||||
|
.one(&state.db)
|
||||||
|
.await?
|
||||||
|
.ok_or(HealthError::FollowUpTemplateNotFound)?;
|
||||||
|
|
||||||
|
let next_ver = check_version(expected_version, model.version)
|
||||||
|
.map_err(|_| HealthError::VersionMismatch)?;
|
||||||
|
|
||||||
|
let mut active: follow_up_template::ActiveModel = model.into();
|
||||||
|
active.deleted_at = Set(Some(Utc::now()));
|
||||||
|
active.updated_at = Set(Utc::now());
|
||||||
|
active.updated_by = Set(operator_id);
|
||||||
|
active.version = Set(next_ver);
|
||||||
|
active.update(&state.db).await?;
|
||||||
|
|
||||||
|
// 同时软删除所有字段
|
||||||
|
let fields = follow_up_template_field::Entity::find()
|
||||||
|
.filter(follow_up_template_field::Column::TemplateId.eq(id))
|
||||||
|
.filter(follow_up_template_field::Column::DeletedAt.is_null())
|
||||||
|
.all(&state.db)
|
||||||
|
.await?;
|
||||||
|
for f in fields {
|
||||||
|
let mut af: follow_up_template_field::ActiveModel = f.into();
|
||||||
|
af.deleted_at = Set(Some(Utc::now()));
|
||||||
|
af.update(&state.db).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
audit_service::record(
|
||||||
|
AuditLog::new(tenant_id, operator_id, "follow_up_template.deleted", "follow_up_template")
|
||||||
|
.with_resource_id(id),
|
||||||
|
&state.db,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 内部辅助 ──
|
||||||
|
|
||||||
|
async fn load_fields(
|
||||||
|
state: &HealthState,
|
||||||
|
template_id: Uuid,
|
||||||
|
) -> HealthResult<Vec<TemplateFieldResp>> {
|
||||||
|
let fields = follow_up_template_field::Entity::find()
|
||||||
|
.filter(follow_up_template_field::Column::TemplateId.eq(template_id))
|
||||||
|
.filter(follow_up_template_field::Column::DeletedAt.is_null())
|
||||||
|
.order_by_asc(follow_up_template_field::Column::SortOrder)
|
||||||
|
.all(&state.db)
|
||||||
|
.await?;
|
||||||
|
Ok(fields.into_iter().map(field_to_resp).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert_field(
|
||||||
|
state: &HealthState,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
template_id: Uuid,
|
||||||
|
operator_id: Option<Uuid>,
|
||||||
|
req: TemplateFieldReq,
|
||||||
|
sort_order: i32,
|
||||||
|
) -> HealthResult<TemplateFieldResp> {
|
||||||
|
let now = Utc::now();
|
||||||
|
let active = follow_up_template_field::ActiveModel {
|
||||||
|
id: Set(Uuid::now_v7()),
|
||||||
|
tenant_id: Set(tenant_id),
|
||||||
|
template_id: Set(template_id),
|
||||||
|
label: Set(req.label),
|
||||||
|
field_key: Set(req.field_key),
|
||||||
|
field_type: Set(req.field_type),
|
||||||
|
required: Set(req.required),
|
||||||
|
options: Set(req.options),
|
||||||
|
placeholder: Set(req.placeholder),
|
||||||
|
validation: Set(req.validation),
|
||||||
|
sort_order: Set(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(field_to_resp(m))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn template_to_resp(m: follow_up_template::Model, fields: Vec<TemplateFieldResp>) -> FollowUpTemplateResp {
|
||||||
|
FollowUpTemplateResp {
|
||||||
|
id: m.id,
|
||||||
|
name: m.name,
|
||||||
|
description: m.description,
|
||||||
|
follow_up_type: m.follow_up_type,
|
||||||
|
applicable_scope: m.applicable_scope,
|
||||||
|
status: m.status,
|
||||||
|
fields,
|
||||||
|
created_at: m.created_at,
|
||||||
|
updated_at: m.updated_at,
|
||||||
|
version: m.version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_to_resp(m: follow_up_template_field::Model) -> TemplateFieldResp {
|
||||||
|
TemplateFieldResp {
|
||||||
|
id: m.id,
|
||||||
|
template_id: m.template_id,
|
||||||
|
label: m.label,
|
||||||
|
field_key: m.field_key,
|
||||||
|
field_type: m.field_type,
|
||||||
|
required: m.required,
|
||||||
|
options: m.options,
|
||||||
|
placeholder: m.placeholder,
|
||||||
|
validation: m.validation,
|
||||||
|
sort_order: m.sort_order,
|
||||||
|
created_at: m.created_at,
|
||||||
|
updated_at: m.updated_at,
|
||||||
|
version: m.version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_follow_up_type(val: &str) -> HealthResult<()> {
|
||||||
|
if !VALID_FOLLOW_UP_TYPES.contains(&val) {
|
||||||
|
return Err(HealthError::Validation(format!(
|
||||||
|
"follow_up_type 必须为: {}", VALID_FOLLOW_UP_TYPES.join(", ")
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_field_type(val: &str) -> HealthResult<()> {
|
||||||
|
if !VALID_FIELD_TYPES.contains(&val) {
|
||||||
|
return Err(HealthError::Validation(format!(
|
||||||
|
"field_type 必须为: {}", VALID_FIELD_TYPES.join(", ")
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ pub mod dialysis_prescription_service;
|
|||||||
pub mod dialysis_service;
|
pub mod dialysis_service;
|
||||||
pub mod doctor_service;
|
pub mod doctor_service;
|
||||||
pub mod follow_up_service;
|
pub mod follow_up_service;
|
||||||
|
pub mod follow_up_template_service;
|
||||||
pub mod health_data_service;
|
pub mod health_data_service;
|
||||||
pub mod masking;
|
pub mod masking;
|
||||||
pub mod patient_service;
|
pub mod patient_service;
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ mod m20260427_000079_add_vital_signs_fields;
|
|||||||
mod m20260427_000080_create_medication_record;
|
mod m20260427_000080_create_medication_record;
|
||||||
mod m20260427_000081_create_dialysis_prescription;
|
mod m20260427_000081_create_dialysis_prescription;
|
||||||
mod m20260427_000082_seed_ai_prompts;
|
mod m20260427_000082_seed_ai_prompts;
|
||||||
|
mod m20260427_000083_create_follow_up_template;
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
@@ -171,6 +172,7 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20260427_000080_create_medication_record::Migration),
|
Box::new(m20260427_000080_create_medication_record::Migration),
|
||||||
Box::new(m20260427_000081_create_dialysis_prescription::Migration),
|
Box::new(m20260427_000081_create_dialysis_prescription::Migration),
|
||||||
Box::new(m20260427_000082_seed_ai_prompts::Migration),
|
Box::new(m20260427_000082_seed_ai_prompts::Migration),
|
||||||
|
Box::new(m20260427_000083_create_follow_up_template::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
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> {
|
||||||
|
// 随访模板主表
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Alias::new("follow_up_template"))
|
||||||
|
.if_not_exists()
|
||||||
|
.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("name")).string_len(200).not_null())
|
||||||
|
// 模板描述
|
||||||
|
.col(ColumnDef::new(Alias::new("description")).text().null())
|
||||||
|
// 随访类型: phone/outpatient/home_visit/online/wechat
|
||||||
|
.col(ColumnDef::new(Alias::new("follow_up_type")).string_len(20).not_null())
|
||||||
|
// 适用疾病/科室(JSON 数组)
|
||||||
|
.col(ColumnDef::new(Alias::new("applicable_scope")).text().null())
|
||||||
|
// 状态: active/disabled
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("status"))
|
||||||
|
.string_len(20)
|
||||||
|
.not_null()
|
||||||
|
.default("active"),
|
||||||
|
)
|
||||||
|
// 标准审计字段
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("created_at"))
|
||||||
|
.timestamp_with_time_zone()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp()),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("updated_at"))
|
||||||
|
.timestamp_with_time_zone()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp()),
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(Alias::new("created_by")).uuid().null())
|
||||||
|
.col(ColumnDef::new(Alias::new("updated_by")).uuid().null())
|
||||||
|
.col(ColumnDef::new(Alias::new("deleted_at")).timestamp_with_time_zone().null())
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("version"))
|
||||||
|
.integer()
|
||||||
|
.not_null()
|
||||||
|
.default(1),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.create_index(
|
||||||
|
Index::create()
|
||||||
|
.if_not_exists()
|
||||||
|
.name("idx_follow_up_template_tenant_id")
|
||||||
|
.table(Alias::new("follow_up_template"))
|
||||||
|
.col(Alias::new("tenant_id"))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 模板字段表
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Alias::new("follow_up_template_field"))
|
||||||
|
.if_not_exists()
|
||||||
|
.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("template_id")).uuid().not_null())
|
||||||
|
// 字段标签
|
||||||
|
.col(ColumnDef::new(Alias::new("label")).string_len(200).not_null())
|
||||||
|
// 字段键名(用于程序引用)
|
||||||
|
.col(ColumnDef::new(Alias::new("field_key")).string_len(100).not_null())
|
||||||
|
// 字段类型: text/number/date/select/checkbox/textarea/scale
|
||||||
|
.col(ColumnDef::new(Alias::new("field_type")).string_len(20).not_null())
|
||||||
|
// 是否必填
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("required"))
|
||||||
|
.boolean()
|
||||||
|
.not_null()
|
||||||
|
.default(false),
|
||||||
|
)
|
||||||
|
// 选项(JSON 数组,select/checkbox 时使用)
|
||||||
|
.col(ColumnDef::new(Alias::new("options")).text().null())
|
||||||
|
// 占位提示
|
||||||
|
.col(ColumnDef::new(Alias::new("placeholder")).string_len(200).null())
|
||||||
|
// 校验规则(JSON)
|
||||||
|
.col(ColumnDef::new(Alias::new("validation")).text().null())
|
||||||
|
// 排序序号
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("sort_order"))
|
||||||
|
.integer()
|
||||||
|
.not_null()
|
||||||
|
.default(0),
|
||||||
|
)
|
||||||
|
// 标准审计字段
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("created_at"))
|
||||||
|
.timestamp_with_time_zone()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp()),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("updated_at"))
|
||||||
|
.timestamp_with_time_zone()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp()),
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(Alias::new("created_by")).uuid().null())
|
||||||
|
.col(ColumnDef::new(Alias::new("updated_by")).uuid().null())
|
||||||
|
.col(ColumnDef::new(Alias::new("deleted_at")).timestamp_with_time_zone().null())
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("version"))
|
||||||
|
.integer()
|
||||||
|
.not_null()
|
||||||
|
.default(1),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.create_index(
|
||||||
|
Index::create()
|
||||||
|
.if_not_exists()
|
||||||
|
.name("idx_follow_up_template_field_template_id")
|
||||||
|
.table(Alias::new("follow_up_template_field"))
|
||||||
|
.col(Alias::new("template_id"))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.create_index(
|
||||||
|
Index::create()
|
||||||
|
.if_not_exists()
|
||||||
|
.name("idx_follow_up_template_field_tenant_id")
|
||||||
|
.table(Alias::new("follow_up_template_field"))
|
||||||
|
.col(Alias::new("tenant_id"))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(Alias::new("follow_up_template_field")).to_owned())
|
||||||
|
.await?;
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(Alias::new("follow_up_template")).to_owned())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user