- 新增 8 张数据库表: points_account/rule/transaction/product/order/checkin + offline_event/registration - SeaORM Entity: 8 个实体,含完整 Relation 定义 - DTO: 积分规则/商品/订单/签到/线下活动请求响应类型 - Service: FIFO 积分消费、每日打卡(连续奖励)、商品兑换(QR码核销)、线下活动报名 - Handler: 16 个 API 端点 (患者端10 + 管理端6) - 权限: health.points.list / health.points.manage - 12个月滚动过期机制 - 审计日志全量覆盖
260 lines
16 KiB
Rust
260 lines
16 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(())
|
||
}
|
||
}
|