feat(health): 积分规则查重 — 同租户同事件类型不可重复创建

- 新增迁移 m20260526_000163:points_rule (tenant_id, event_type) 部分唯一索引(排除软删除行)
- 后端 create_rule 添加 event_type 查重,重复时返回 400 Validation 错误
- 前端 PointsRuleList 提取后端错误消息展示给用户
This commit is contained in:
iven
2026-05-26 01:09:21 +08:00
parent 8027cdd1d9
commit d7fb5da873
4 changed files with 60 additions and 2 deletions

View File

@@ -142,8 +142,9 @@ export default function PointsRuleList() {
setModalOpen(false);
form.resetFields();
fetchData();
} catch {
message.error(editing ? '更新失败' : '创建失败');
} catch (err: unknown) {
const apiMsg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
message.error(apiMsg || (editing ? '更新失败' : '创建失败'));
}
};

View File

@@ -59,6 +59,20 @@ pub async fn create_rule(
operator_id: Option<Uuid>,
req: CreatePointsRuleReq,
) -> HealthResult<PointsRuleResp> {
// 查重:同一租户下不允许重复的 event_type
let existing = points_rule::Entity::find()
.filter(points_rule::Column::TenantId.eq(tenant_id))
.filter(points_rule::Column::EventType.eq(&req.event_type))
.filter(points_rule::Column::DeletedAt.is_null())
.one(&state.db)
.await?;
if existing.is_some() {
return Err(HealthError::Validation(format!(
"事件类型 '{}' 已存在规则,不可重复创建",
req.event_type
)));
}
let now = Utc::now();
let active = points_rule::ActiveModel {
id: Set(Uuid::now_v7()),

View File

@@ -169,6 +169,7 @@ mod m20260521_000164_reorganize_menus_scheme_b;
mod m20260522_000160_article_add_is_public;
mod m20260522_000161_patient_points_manage_perm;
mod m20260522_000162_seed_patient_miniprogram_permissions;
mod m20260526_000163_points_rule_unique_event_type;
pub struct Migrator;
@@ -345,6 +346,7 @@ impl MigratorTrait for Migrator {
Box::new(m20260522_000160_article_add_is_public::Migration),
Box::new(m20260522_000161_patient_points_manage_perm::Migration),
Box::new(m20260522_000162_seed_patient_miniprogram_permissions::Migration),
Box::new(m20260526_000163_points_rule_unique_event_type::Migration),
]
}
}

View File

@@ -0,0 +1,41 @@
use sea_orm_migration::prelude::*;
/// 为 points_rule 添加 (tenant_id, event_type) 唯一索引(排除软删除行)
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
// 删除旧的非唯一索引
db.execute_unprepared("DROP INDEX IF EXISTS idx_points_rule_event_type")
.await?;
// 创建部分唯一索引(仅对未软删除的行生效)
db.execute_unprepared(
r#"
CREATE UNIQUE INDEX IF NOT EXISTS uq_points_rule_tenant_event
ON points_rule (tenant_id, event_type)
WHERE deleted_at IS NULL
"#,
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared("DROP INDEX IF EXISTS uq_points_rule_tenant_event")
.await?;
db.execute_unprepared(
"CREATE INDEX IF NOT EXISTS idx_points_rule_event_type ON points_rule (event_type)",
)
.await?;
Ok(())
}
}