From d7fb5da873e098d88dba60b618675fdf5b94174a Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 26 May 2026 01:09:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(health):=20=E7=A7=AF=E5=88=86=E8=A7=84?= =?UTF-8?q?=E5=88=99=E6=9F=A5=E9=87=8D=20=E2=80=94=20=E5=90=8C=E7=A7=9F?= =?UTF-8?q?=E6=88=B7=E5=90=8C=E4=BA=8B=E4=BB=B6=E7=B1=BB=E5=9E=8B=E4=B8=8D?= =?UTF-8?q?=E5=8F=AF=E9=87=8D=E5=A4=8D=E5=88=9B=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增迁移 m20260526_000163:points_rule (tenant_id, event_type) 部分唯一索引(排除软删除行) - 后端 create_rule 添加 event_type 查重,重复时返回 400 Validation 错误 - 前端 PointsRuleList 提取后端错误消息展示给用户 --- apps/web/src/pages/health/PointsRuleList.tsx | 5 ++- .../src/service/points_service/event.rs | 14 +++++++ crates/erp-server/migration/src/lib.rs | 2 + ...26_000163_points_rule_unique_event_type.rs | 41 +++++++++++++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 crates/erp-server/migration/src/m20260526_000163_points_rule_unique_event_type.rs 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(()) + } +}