功能修复: 1. 患者创建空名称验证:后端添加 name.trim().is_empty() 检查 2. 仪表盘统计容错:单个查询失败返回零值而非 500 3. FHIR 路由修复:从 /fhir 移到 /api/v1/fhir 保持一致 4. 冻结模块后端中间件:新增 frozen_module_middleware 拦截冻结路径 5. 积分端点权限码:health.health-data.list → health.points.list 6. 角色权限迁移:护士补充 devices.list,运营补充 points.list/manage 7. 测试结果文档:R01-R05 角色测试 + T00/T10 结果归档 Clippy 全 workspace 清零(14→0 errors): - erp-core: 修复 empty doc line、collapsible if、redundant closure 等 9 处 - erp-health: 修复 too_many_arguments、unused var、unnecessary parens 等 58 处 - erp-ai: 修复 dead_code、unused import 等 11 处 - erp-plugin: 修复 too_many_arguments、wildcard pattern 等 11 处 - erp-server-migration: 修复 enum_variant_names 5 处 - erp-auth/config/workflow/message: 各 1-3 处 工程改进: - lint-staged 配置迁移到 .lintstagedrc.js(函数式避免文件列表传给 clippy) - cargo fmt 统一格式化
620 lines
25 KiB
Rust
620 lines
25 KiB
Rust
use sea_orm_migration::prelude::*;
|
||
|
||
#[derive(DeriveMigrationName)]
|
||
pub struct Migration;
|
||
|
||
/// V2 积分商城: 8 张新表 — account/rule/transaction/product/order/checkin/event/registration
|
||
#[async_trait::async_trait]
|
||
impl MigrationTrait for Migration {
|
||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||
// 1. points_account — 积分账户(每患者一个)
|
||
manager
|
||
.create_table(
|
||
Table::create()
|
||
.table(Alias::new("points_account"))
|
||
.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("patient_id")).uuid().not_null())
|
||
.col(
|
||
ColumnDef::new(Alias::new("balance"))
|
||
.integer()
|
||
.not_null()
|
||
.default(0),
|
||
)
|
||
.col(
|
||
ColumnDef::new(Alias::new("total_earned"))
|
||
.integer()
|
||
.not_null()
|
||
.default(0),
|
||
)
|
||
.col(
|
||
ColumnDef::new(Alias::new("total_spent"))
|
||
.integer()
|
||
.not_null()
|
||
.default(0),
|
||
)
|
||
.col(
|
||
ColumnDef::new(Alias::new("total_expired"))
|
||
.integer()
|
||
.not_null()
|
||
.default(0),
|
||
)
|
||
.col(
|
||
ColumnDef::new(Alias::new("version"))
|
||
.integer()
|
||
.not_null()
|
||
.default(1),
|
||
)
|
||
.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())
|
||
.col(ColumnDef::new(Alias::new("updated_by")).uuid())
|
||
.col(ColumnDef::new(Alias::new("deleted_at")).timestamp_with_time_zone())
|
||
.to_owned(),
|
||
)
|
||
.await?;
|
||
manager
|
||
.create_index(
|
||
Index::create()
|
||
.if_not_exists()
|
||
.name("idx_points_account_patient")
|
||
.table(Alias::new("points_account"))
|
||
.col(Alias::new("patient_id"))
|
||
.unique()
|
||
.to_owned(),
|
||
)
|
||
.await?;
|
||
|
||
// 2. points_rule — 积分获取规则
|
||
manager
|
||
.create_table(
|
||
Table::create()
|
||
.table(Alias::new("points_rule"))
|
||
.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("event_type"))
|
||
.string_len(64)
|
||
.not_null(),
|
||
)
|
||
.col(
|
||
ColumnDef::new(Alias::new("name"))
|
||
.string_len(128)
|
||
.not_null(),
|
||
)
|
||
.col(ColumnDef::new(Alias::new("description")).text())
|
||
.col(
|
||
ColumnDef::new(Alias::new("points_value"))
|
||
.integer()
|
||
.not_null()
|
||
.default(0),
|
||
)
|
||
.col(
|
||
ColumnDef::new(Alias::new("daily_cap"))
|
||
.integer()
|
||
.not_null()
|
||
.default(0),
|
||
)
|
||
.col(
|
||
ColumnDef::new(Alias::new("streak_7d_bonus"))
|
||
.integer()
|
||
.not_null()
|
||
.default(0),
|
||
)
|
||
.col(
|
||
ColumnDef::new(Alias::new("streak_14d_bonus"))
|
||
.integer()
|
||
.not_null()
|
||
.default(0),
|
||
)
|
||
.col(
|
||
ColumnDef::new(Alias::new("streak_30d_bonus"))
|
||
.integer()
|
||
.not_null()
|
||
.default(0),
|
||
)
|
||
.col(
|
||
ColumnDef::new(Alias::new("is_active"))
|
||
.boolean()
|
||
.not_null()
|
||
.default(true),
|
||
)
|
||
.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())
|
||
.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()
|
||
.if_not_exists()
|
||
.name("idx_points_rule_event_type")
|
||
.table(Alias::new("points_rule"))
|
||
.col(Alias::new("event_type"))
|
||
.to_owned(),
|
||
)
|
||
.await?;
|
||
|
||
// 3. points_transaction — 积分流水(FIFO 桶模型)
|
||
manager
|
||
.create_table(
|
||
Table::create()
|
||
.table(Alias::new("points_transaction"))
|
||
.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("account_id")).uuid().not_null())
|
||
// earn / spend / expired / refund
|
||
.col(
|
||
ColumnDef::new(Alias::new("r#type"))
|
||
.string_len(16)
|
||
.not_null(),
|
||
)
|
||
.col(ColumnDef::new(Alias::new("amount")).integer().not_null())
|
||
.col(
|
||
ColumnDef::new(Alias::new("remaining_amount"))
|
||
.integer()
|
||
.not_null()
|
||
.default(0),
|
||
)
|
||
// active / expired / consumed
|
||
.col(
|
||
ColumnDef::new(Alias::new("status"))
|
||
.string_len(16)
|
||
.not_null()
|
||
.default("active"),
|
||
)
|
||
.col(ColumnDef::new(Alias::new("expires_at")).timestamp_with_time_zone())
|
||
.col(
|
||
ColumnDef::new(Alias::new("balance_after"))
|
||
.integer()
|
||
.not_null()
|
||
.default(0),
|
||
)
|
||
.col(ColumnDef::new(Alias::new("rule_id")).uuid())
|
||
.col(ColumnDef::new(Alias::new("order_id")).uuid())
|
||
.col(ColumnDef::new(Alias::new("description")).string_len(256))
|
||
.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())
|
||
.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()
|
||
.if_not_exists()
|
||
.name("idx_points_txn_account")
|
||
.table(Alias::new("points_transaction"))
|
||
.col(Alias::new("account_id"))
|
||
.col(Alias::new("status"))
|
||
.col(Alias::new("expires_at"))
|
||
.to_owned(),
|
||
)
|
||
.await?;
|
||
|
||
// 4. points_product — 兑换商品
|
||
manager
|
||
.create_table(
|
||
Table::create()
|
||
.table(Alias::new("points_product"))
|
||
.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(128)
|
||
.not_null(),
|
||
)
|
||
// physical / service / privilege
|
||
.col(
|
||
ColumnDef::new(Alias::new("product_type"))
|
||
.string_len(16)
|
||
.not_null()
|
||
.default("physical"),
|
||
)
|
||
.col(
|
||
ColumnDef::new(Alias::new("points_cost"))
|
||
.integer()
|
||
.not_null(),
|
||
)
|
||
.col(
|
||
ColumnDef::new(Alias::new("stock"))
|
||
.integer()
|
||
.not_null()
|
||
.default(-1),
|
||
)
|
||
.col(ColumnDef::new(Alias::new("image_url")).string_len(512))
|
||
.col(ColumnDef::new(Alias::new("description")).text())
|
||
.col(ColumnDef::new(Alias::new("service_config")).json())
|
||
.col(
|
||
ColumnDef::new(Alias::new("is_active"))
|
||
.boolean()
|
||
.not_null()
|
||
.default(true),
|
||
)
|
||
.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())
|
||
.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?;
|
||
|
||
// 5. points_order — 兑换订单
|
||
manager
|
||
.create_table(
|
||
Table::create()
|
||
.table(Alias::new("points_order"))
|
||
.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("patient_id")).uuid().not_null())
|
||
.col(ColumnDef::new(Alias::new("product_id")).uuid().not_null())
|
||
.col(
|
||
ColumnDef::new(Alias::new("points_cost"))
|
||
.integer()
|
||
.not_null(),
|
||
)
|
||
// pending / verified / cancelled / expired
|
||
.col(
|
||
ColumnDef::new(Alias::new("status"))
|
||
.string_len(16)
|
||
.not_null()
|
||
.default("pending"),
|
||
)
|
||
.col(ColumnDef::new(Alias::new("qr_code")).uuid())
|
||
.col(ColumnDef::new(Alias::new("verified_by")).uuid())
|
||
.col(ColumnDef::new(Alias::new("verified_at")).timestamp_with_time_zone())
|
||
.col(ColumnDef::new(Alias::new("expires_at")).timestamp_with_time_zone())
|
||
.col(ColumnDef::new(Alias::new("notes")).string_len(256))
|
||
.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())
|
||
.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()
|
||
.if_not_exists()
|
||
.name("idx_points_order_patient")
|
||
.table(Alias::new("points_order"))
|
||
.col(Alias::new("patient_id"))
|
||
.col(Alias::new("status"))
|
||
.to_owned(),
|
||
)
|
||
.await?;
|
||
manager
|
||
.create_index(
|
||
Index::create()
|
||
.if_not_exists()
|
||
.name("idx_points_order_qr")
|
||
.table(Alias::new("points_order"))
|
||
.col(Alias::new("qr_code"))
|
||
.to_owned(),
|
||
)
|
||
.await?;
|
||
|
||
// 6. points_checkin — 每日打卡
|
||
manager
|
||
.create_table(
|
||
Table::create()
|
||
.table(Alias::new("points_checkin"))
|
||
.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("patient_id")).uuid().not_null())
|
||
.col(ColumnDef::new(Alias::new("checkin_date")).date().not_null())
|
||
.col(
|
||
ColumnDef::new(Alias::new("consecutive_days"))
|
||
.integer()
|
||
.not_null()
|
||
.default(1),
|
||
)
|
||
.col(
|
||
ColumnDef::new(Alias::new("created_at"))
|
||
.timestamp_with_time_zone()
|
||
.not_null()
|
||
.default(Expr::current_timestamp()),
|
||
)
|
||
.to_owned(),
|
||
)
|
||
.await?;
|
||
manager
|
||
.create_index(
|
||
Index::create()
|
||
.if_not_exists()
|
||
.name("idx_points_checkin_unique")
|
||
.table(Alias::new("points_checkin"))
|
||
.col(Alias::new("patient_id"))
|
||
.col(Alias::new("checkin_date"))
|
||
.unique()
|
||
.to_owned(),
|
||
)
|
||
.await?;
|
||
|
||
// 7. offline_event — 线下活动
|
||
manager
|
||
.create_table(
|
||
Table::create()
|
||
.table(Alias::new("offline_event"))
|
||
.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("title"))
|
||
.string_len(256)
|
||
.not_null(),
|
||
)
|
||
.col(ColumnDef::new(Alias::new("description")).text())
|
||
.col(ColumnDef::new(Alias::new("event_date")).date().not_null())
|
||
.col(ColumnDef::new(Alias::new("start_time")).time())
|
||
.col(ColumnDef::new(Alias::new("end_time")).time())
|
||
.col(ColumnDef::new(Alias::new("location")).string_len(256))
|
||
.col(
|
||
ColumnDef::new(Alias::new("points_reward"))
|
||
.integer()
|
||
.not_null()
|
||
.default(0),
|
||
)
|
||
.col(
|
||
ColumnDef::new(Alias::new("max_participants"))
|
||
.integer()
|
||
.not_null()
|
||
.default(0),
|
||
)
|
||
.col(
|
||
ColumnDef::new(Alias::new("current_participants"))
|
||
.integer()
|
||
.not_null()
|
||
.default(0),
|
||
)
|
||
// draft / published / ongoing / completed / cancelled
|
||
.col(
|
||
ColumnDef::new(Alias::new("status"))
|
||
.string_len(16)
|
||
.not_null()
|
||
.default("draft"),
|
||
)
|
||
.col(ColumnDef::new(Alias::new("image_url")).string_len(512))
|
||
.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())
|
||
.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?;
|
||
|
||
// 8. offline_event_registration — 活动报名 + 签到
|
||
manager
|
||
.create_table(
|
||
Table::create()
|
||
.table(Alias::new("offline_event_registration"))
|
||
.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("event_id")).uuid().not_null())
|
||
.col(ColumnDef::new(Alias::new("patient_id")).uuid().not_null())
|
||
// registered / checked_in / cancelled
|
||
.col(
|
||
ColumnDef::new(Alias::new("status"))
|
||
.string_len(16)
|
||
.not_null()
|
||
.default("registered"),
|
||
)
|
||
.col(ColumnDef::new(Alias::new("checked_in_at")).timestamp_with_time_zone())
|
||
.col(ColumnDef::new(Alias::new("checked_in_by")).uuid())
|
||
.col(
|
||
ColumnDef::new(Alias::new("points_granted"))
|
||
.boolean()
|
||
.not_null()
|
||
.default(false),
|
||
)
|
||
.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())
|
||
.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()
|
||
.if_not_exists()
|
||
.name("idx_event_reg_unique")
|
||
.table(Alias::new("offline_event_registration"))
|
||
.col(Alias::new("event_id"))
|
||
.col(Alias::new("patient_id"))
|
||
.unique()
|
||
.to_owned(),
|
||
)
|
||
.await?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||
let tables = [
|
||
"offline_event_registration",
|
||
"offline_event",
|
||
"points_checkin",
|
||
"points_order",
|
||
"points_product",
|
||
"points_transaction",
|
||
"points_rule",
|
||
"points_account",
|
||
];
|
||
for t in tables {
|
||
manager
|
||
.drop_table(Table::drop().table(Alias::new(t)).if_exists().to_owned())
|
||
.await?;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|