feat(health): 随访模板系统 — follow_up_template + template_field 全栈
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

新增随访模板和模板字段两张表及完整 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:
iven
2026-04-27 14:40:28 +08:00
parent ca96310a84
commit dc5879228e
13 changed files with 869 additions and 2 deletions

View File

@@ -82,6 +82,7 @@ mod m20260427_000079_add_vital_signs_fields;
mod m20260427_000080_create_medication_record;
mod m20260427_000081_create_dialysis_prescription;
mod m20260427_000082_seed_ai_prompts;
mod m20260427_000083_create_follow_up_template;
pub struct Migrator;
@@ -171,6 +172,7 @@ impl MigratorTrait for Migrator {
Box::new(m20260427_000080_create_medication_record::Migration),
Box::new(m20260427_000081_create_dialysis_prescription::Migration),
Box::new(m20260427_000082_seed_ai_prompts::Migration),
Box::new(m20260427_000083_create_follow_up_template::Migration),
]
}
}

View File

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