Pārlūkot izejas kodu

fix(mdp): enforce inbound grant uniqueness | server 1.0.553

YY968XX 3 dienas atpakaļ
vecāks
revīzija
aa4fb187c1

+ 9 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.552</AssemblyVersion>
-    <FileVersion>1.0.552</FileVersion>
-    <Version>1.0.552</Version>
+    <AssemblyVersion>1.0.553</AssemblyVersion>
+    <FileVersion>1.0.553</FileVersion>
+    <Version>1.0.553</Version>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
@@ -892,6 +892,12 @@
     <None Update="UpdateScripts\1.0.551.verify.sql">
     <None Update="UpdateScripts\1.0.551.verify.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
     </None>
+    <None Update="UpdateScripts\1.0.553.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.553.verify.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
     <None Update="UpdateScripts\UAT-PLACEHOLDER-MENU-HIDE.ops.sql">
     <None Update="UpdateScripts\UAT-PLACEHOLDER-MENU-HIDE.ops.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
     </None>

+ 75 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.553.sql

@@ -0,0 +1,75 @@
+-- ============================================================================
+-- 1.0.553 · 补 mdp_inbound_grant 缺失的唯一键 uk_inbound_grant(access_key, entity_code)
+--
+-- 背景:1.0.536 里这张表是 CREATE TABLE IF NOT EXISTS。在 1.0.536 正式执行之前,
+--   该表已被某个实例的 SqlSugar CodeFirst 抢先建出(CodeFirst 在容器初始化阶段跑,
+--   早于 UseAutoVersionUpdate 中间件),于是 1.0.536 的建表语句整条 no-op,
+--   脚本里声明的 UNIQUE KEY 从未落地。实测证据:该表无任何 COLUMN COMMENT、
+--   无 DEFAULT,status 是 int 而非脚本声明的 tinyint —— 全是 CodeFirst 产物特征。
+--
+-- 为什么必须补:mdp_inbound_grant 是第三方推数入站的授权表。
+--   (access_key, entity_code) 是它的业务身份:同一凭证对同一实体只应有一条授权。
+--   缺了这个唯一键,重复授权行可以写进去,而入站鉴权按该组合查授权,
+--   出现两条就意味着限流配额、IP 白名单、batch 上限到底以哪条为准是不确定的。
+--
+-- 本脚本只做这一件事。刻意不碰 status 的类型与 DEFAULT、不碰其余四张 inbound 表 ——
+--   那些是等价差异,不影响业务语义,混在一起改会让这次热修的回滚面无谓扩大。
+--
+-- 幂等与安全:
+--   · 索引不存在        → 创建
+--   · 已存在且定义正确  → no-op
+--   · 同名但定义不对    → SIGNAL 中止,绝不 DROP 重建(可能是别人有意改过)
+--   · 存在重复数据      → SIGNAL 中止(加唯一键会失败,提前给出可读原因)
+--
+-- AutoVersionUpdate 按分号拆句、无事务,故全部用 PREPARE/EXECUTE 自包含,
+-- 且注释内不出现会被 parser 误切的分号模式。
+-- ============================================================================
+
+-- ── 前置门禁 1:不能有重复数据,否则加唯一键必然失败 ────────────────────────
+SET @dup_groups := (
+  SELECT COUNT(*) FROM (
+    SELECT 1 FROM `mdp_inbound_grant` GROUP BY `access_key`, `entity_code` HAVING COUNT(*) > 1
+  ) d
+);
+SET @sql := IF(@dup_groups <> 0,
+  "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '1.0.553-G1: mdp_inbound_grant 存在重复 (access_key, entity_code),无法建唯一键 — 先人工裁决保留哪条'",
+  'DO 0');
+PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
+
+-- ── 前置门禁 2:同名索引若已存在,定义必须完全正确,否则中止 ────────────────
+SET @idx_cols := (
+  SELECT GROUP_CONCAT(`COLUMN_NAME` ORDER BY `SEQ_IN_INDEX` SEPARATOR ',')
+    FROM information_schema.`STATISTICS`
+   WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_inbound_grant'
+     AND `INDEX_NAME` = 'uk_inbound_grant'
+);
+SET @idx_unique := (
+  SELECT MIN(`NON_UNIQUE`) FROM information_schema.`STATISTICS`
+   WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_inbound_grant'
+     AND `INDEX_NAME` = 'uk_inbound_grant'
+);
+SET @sql := IF(@idx_cols IS NOT NULL
+               AND (@idx_cols <> 'access_key,entity_code' OR @idx_unique <> 0),
+  "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '1.0.553-G2: uk_inbound_grant 已存在但定义不符预期 — 中止,不自动 DROP 重建'",
+  'DO 0');
+PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
+
+-- ── 主体:不存在才创建 ──────────────────────────────────────────────────────
+SET @sql := IF(@idx_cols IS NULL,
+  'ALTER TABLE `mdp_inbound_grant` ADD UNIQUE KEY `uk_inbound_grant` (`access_key`, `entity_code`)',
+  'DO 0');
+PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
+
+-- ── 结果断言:执行完必须存在且定义正确 ──────────────────────────────────────
+SET @after_cols := (
+  SELECT GROUP_CONCAT(`COLUMN_NAME` ORDER BY `SEQ_IN_INDEX` SEPARATOR ',')
+    FROM information_schema.`STATISTICS`
+   WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_inbound_grant'
+     AND `INDEX_NAME` = 'uk_inbound_grant' AND `NON_UNIQUE` = 0
+);
+SET @sql := IF(IFNULL(@after_cols, '') <> 'access_key,entity_code',
+  "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '1.0.553-A1: uk_inbound_grant 建后校验失败'",
+  'DO 0');
+PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
+
+SELECT @after_cols AS uk_inbound_grant_columns;

+ 24 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.553.verify.sql

@@ -0,0 +1,24 @@
+-- 1.0.553.verify.sql — 每条 SELECT 都必须返回 1
+
+-- ① 唯一键存在,且恰好 2 列
+SELECT (SELECT COUNT(*) FROM information_schema.`STATISTICS`
+         WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_inbound_grant'
+           AND `INDEX_NAME` = 'uk_inbound_grant' AND `NON_UNIQUE` = 0) = 2;
+
+-- ② 列序完全正确(access_key 在前,entity_code 在后)
+SELECT (SELECT GROUP_CONCAT(`COLUMN_NAME` ORDER BY `SEQ_IN_INDEX` SEPARATOR ',')
+          FROM information_schema.`STATISTICS`
+         WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_inbound_grant'
+           AND `INDEX_NAME` = 'uk_inbound_grant') = 'access_key,entity_code';
+
+-- ③ 数据层无重复(唯一键生效的必要条件,也防止有人事后禁用校验塞重复)
+SELECT (SELECT COUNT(*) FROM (
+          SELECT 1 FROM `mdp_inbound_grant` GROUP BY `access_key`, `entity_code` HAVING COUNT(*) > 1
+        ) d) = 0;
+
+-- ④ 授权行未被本次热修误删(只加索引,不动数据)
+SELECT (SELECT COUNT(*) FROM `mdp_inbound_grant`) > 0;
+
+-- ⑤ 其余四张 inbound 表未被波及(本批只改 grant 一张)
+SELECT (SELECT COUNT(*) FROM information_schema.`TABLES`
+         WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` LIKE 'mdp\_inbound\_%') = 5;

+ 9 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/DataPlatform/MdpInboundEntities.cs

@@ -1,7 +1,16 @@
 namespace Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
 namespace Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
 
 
 /// <summary>第三方推数入站授权。不走 CodeFirst,以 UpdateScripts 为唯一建表口径。</summary>
 /// <summary>第三方推数入站授权。不走 CodeFirst,以 UpdateScripts 为唯一建表口径。</summary>
+/// <remarks>
+/// <para>下面的 <see cref="SugarIndexAttribute"/> 是<b>兜底</b>,不是建表口径的改变。</para>
+/// <para>起因:1.0.536 用 CREATE TABLE IF NOT EXISTS 建表,而本表曾被某实例的 CodeFirst
+/// 抢先建出(CodeFirst 在容器初始化阶段跑,早于 UseAutoVersionUpdate 中间件),
+/// 导致脚本整条 no-op、唯一键从未落地,直到 1.0.553 才补上。
+/// 声明本索引后,即便将来又出现 CodeFirst 抢建的时序,
+/// <c>(access_key, entity_code)</c> 的唯一性也不会再丢。</para>
+/// </remarks>
 [SugarTable("mdp_inbound_grant", "第三方推数入站授权")]
 [SugarTable("mdp_inbound_grant", "第三方推数入站授权")]
+[SugarIndex("uk_inbound_grant", nameof(AccessKey), OrderByType.Asc, nameof(EntityCode), OrderByType.Asc, true)]
 public class MdpInboundGrant
 public class MdpInboundGrant
 {
 {
     [SugarColumn(ColumnName = "id", IsPrimaryKey = true, IsIdentity = true)]
     [SugarColumn(ColumnName = "id", IsPrimaryKey = true, IsIdentity = true)]