diff --git a/apps/web/src/pages/health/PointsRuleList.tsx b/apps/web/src/pages/health/PointsRuleList.tsx index ad213b0..a595400 100644 --- a/apps/web/src/pages/health/PointsRuleList.tsx +++ b/apps/web/src/pages/health/PointsRuleList.tsx @@ -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 ? '更新失败' : '创建失败')); } }; diff --git a/crates/erp-health/src/service/points_service/event.rs b/crates/erp-health/src/service/points_service/event.rs index 930db5a..24aa23b 100644 --- a/crates/erp-health/src/service/points_service/event.rs +++ b/crates/erp-health/src/service/points_service/event.rs @@ -59,6 +59,20 @@ pub async fn create_rule( operator_id: Option, req: CreatePointsRuleReq, ) -> HealthResult { + // 查重:同一租户下不允许重复的 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()), diff --git a/crates/erp-server/migration/src/lib.rs b/crates/erp-server/migration/src/lib.rs index e44caaf..042481e 100644 --- a/crates/erp-server/migration/src/lib.rs +++ b/crates/erp-server/migration/src/lib.rs @@ -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), ] } } diff --git a/crates/erp-server/migration/src/m20260526_000163_points_rule_unique_event_type.rs b/crates/erp-server/migration/src/m20260526_000163_points_rule_unique_event_type.rs new file mode 100644 index 0000000..c14a6f1 --- /dev/null +++ b/crates/erp-server/migration/src/m20260526_000163_points_rule_unique_event_type.rs @@ -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(()) + } +}