Просмотр исходного кода

chore(s8): 退役旧通知分层数据并补齐 server 版本号

P0-C2A 已把 Runtime 对 ado_s8_notification_layer 的读取整体切断、Seed 停止供数,
但库里那 16 行还在。留着的问题不是它会生效——已经生效不了——而是下一个
不了解背景的人看到 16 行"通知配置",仍会以为它在起作用。

1.0.505.sql 只有一句 DELETE,不动结构。表、Entity、CRUD、Controller、
Seed 空壳、旧页面与 route 全部保留,物理清理留给 P1-C3。

verify 除了断言残留为 0,还顺带断言"表还在、别的表也还在"。这不是凑数:
一条 DELETE 写错表名的代价是把当前唯一在用的通知配置或规则责任池清空,
而那种损失没有回滚点。同样的理由,S8LegacyNotifyDataRetirementTests 逐一点名
受保护的表,将来有人往这个脚本里追加语句时会立刻被指出碰了哪张不该碰的。

版本号取 1.0.505:上一批因 1.0.504 被并行实例占用而 BLOCKED,现在对方
(1c24b933a)已正式提交推送,槽位释放,按 origin/master 的 1.0.504 顺延取号。

注意:本批只把脚本纳入代码,未对任何数据库执行。共享库那 16 行仍在,
将在下次部署时由 AutoVersionUpdate 清理。
YY968XX 2 дней назад
Родитель
Сommit
2e8a7765b5

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

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

+ 18 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.505.sql

@@ -0,0 +1,18 @@
+-- =====================================================================================
+-- 1.0.505  S8-LEGACY-NOTIFY-DECOMMISSION-1 · 退役旧通知分层数据
+--
+-- 背景:通知收件人的 Current Authority 已是 ado_s8_notification_recipient
+--       (Rule + Event + RecipientType → 责任池 / 当前处理人 / 当前复核人 → SysUser)。
+--       上一批(8f8116cd9)已把 Runtime 对 ado_s8_notification_layer 的读取整体切断,
+--       并让 S8NotificationLayerSeedData 停止供数。本脚本清掉库里最后的遗留行。
+--
+-- 为什么不迁移:旧表按 (scene, severity, level) 选人,新模型按 (rule, event, responsibility),
+--       是两套互不相干的维度,不存在把前者映射到后者的正确做法。真库取证也支持退役 ——
+--       16 行里 14 行是从没人改过的平台默认种子(tenant_id=0),
+--       2 行是租户侧 orphan(scene 不在场景表 / 目标角色在本租户无成员)。
+--
+-- 边界:只删数据,不动结构。表、Entity、CRUD Service、Controller、Seed 空壳、旧页面与 route
+--       全部保留,物理清理属 backend dead code 范畴,留给 P1-C3 单独一批。
+-- =====================================================================================
+
+DELETE FROM `ado_s8_notification_layer`;

+ 22 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.505.verify.sql

@@ -0,0 +1,22 @@
+-- 1.0.505.verify.sql
+-- 契约(由 AutoVersionUpdate.RunVerifyScriptIfExists 逐条执行取首行首列判真):
+--   ① 禁止 SET @var(返回 null 即判失败)→ 一律内联子查询,每条语句自包含
+--   ② 每条返回「通过为真」的标量(用 = 期望值 得 1/0),不得返回错误条数
+--   ③ 一条语句一个断言,不用 UNION 汇总
+--   ④ 文件以最后一条 SQL 的分号结束,末尾不得残留 comment-only 块
+--
+-- 本文件只断言「旧数据已清空」与「没有顺手动别的表」。
+-- 断言 2~4 是刻意的越界防护:本迁移只有一条 DELETE,但把「不该被改的表仍有数据」
+-- 写成断言,能在将来有人往这个脚本里加东西时立刻拦住 —— 一条 DELETE 写错表名的代价,
+-- 是把当前唯一在用的通知配置或规则责任池删空,而那种损失没有回滚点。
+
+SELECT (SELECT COUNT(*) FROM `ado_s8_notification_layer`) = 0;
+
+SELECT (SELECT COUNT(*) FROM information_schema.TABLES
+        WHERE TABLE_SCHEMA = DATABASE()
+          AND TABLE_NAME = 'ado_s8_notification_layer') = 1;
+
+SELECT (SELECT COUNT(*) FROM information_schema.TABLES
+        WHERE TABLE_SCHEMA = DATABASE()
+          AND TABLE_NAME IN ('ado_s8_notification_recipient','ado_s8_watch_rule',
+                             'ado_s8_rule_handler_user','ado_s8_rule_responsibility_user')) = 4;

+ 156 - 0
server/Plugins/Admin.NET.Plugin.AiDOP.Tests/S8/S8LegacyNotifyDataRetirementTests.cs

@@ -0,0 +1,156 @@
+using Xunit;
+
+namespace Admin.NET.Plugin.AiDOP.Tests.S8;
+
+/// <summary>
+/// S8-LEGACY-NOTIFY-DECOMMISSION-1 · 收尾:旧通知分层<b>数据</b>的退役脚本契约。
+///
+/// <para><b>为什么用静态契约测试而不是跑真库</b>:这条迁移只有一句 DELETE,
+/// 而它的风险恰恰不在「能不能删掉」,在「会不会顺手删错表」。
+/// 一条 DELETE 写错表名的代价是把当前唯一在用的通知配置或规则责任池清空 ——
+/// 那种损失没有回滚点。因此这里守的是<b>脚本边界</b>:只准碰一张表、只准 DELETE、
+/// 不准出现任何 DDL。</para>
+///
+/// <para>真库执行由 <c>AutoVersionUpdate</c> 在部署时完成,verify 脚本会当场断言残留为 0。
+/// 本批<b>没有</b>、也不应该为了"证明删得掉"去动 shared DB。</para>
+/// </summary>
+public class S8LegacyNotifyDataRetirementTests
+{
+    private const string Version = "1.0.505";
+
+    private static string ScriptsDir()
+    {
+        var dir = new DirectoryInfo(AppContext.BaseDirectory);
+        while (dir != null && !Directory.Exists(Path.Combine(dir.FullName, "Admin.NET.Web.Entry")))
+            dir = dir.Parent;
+        Assert.NotNull(dir);
+        return Path.Combine(dir!.FullName, "Admin.NET.Web.Entry", "UpdateScripts");
+    }
+
+    private static string Read(string fileName)
+    {
+        var path = Path.Combine(ScriptsDir(), fileName);
+        Assert.True(File.Exists(path), $"升级脚本不存在:{path}");
+        return File.ReadAllText(path);
+    }
+
+    /// <summary>去掉 `--` 注释行 —— 注释里写了某个词不等于脚本会执行它。</summary>
+    private static string SqlOnly(string fileName) =>
+        string.Join('\n', Read(fileName).Split('\n')
+            .Where(l => !l.TrimStart().StartsWith("--", StringComparison.Ordinal)));
+
+    /// <summary>迁移与 verify 两个文件都必须存在(项目既有规范)。</summary>
+    [Fact]
+    public void MigrationAndVerifyScriptsExist()
+    {
+        Assert.False(string.IsNullOrWhiteSpace(Read($"{Version}.sql")));
+        Assert.False(string.IsNullOrWhiteSpace(Read($"{Version}.verify.sql")));
+    }
+
+    /// <summary>
+    /// 迁移<b>只删一张表</b>,且是纯数据操作。
+    /// <para>DDL 一律禁止:本批的边界是「清数据不删表」,表 / Entity / CRUD 留给 P1-C3。</para>
+    /// </summary>
+    [Fact]
+    public void Migration_OnlyDeletesLegacyNotificationLayer()
+    {
+        var sql = SqlOnly($"{Version}.sql");
+
+        Assert.Contains("DELETE FROM `ado_s8_notification_layer`;", sql);
+
+        // 只有这一条语句
+        Assert.Equal(1, sql.Split(';').Count(seg => !string.IsNullOrWhiteSpace(seg)));
+
+        // 任何 DDL 或结构变更都不允许
+        foreach (var banned in new[] { "DROP TABLE", "DROP ", "ALTER TABLE", "CREATE TABLE", "TRUNCATE", "RENAME", "INSERT INTO", "UPDATE " })
+            Assert.DoesNotContain(banned, sql, StringComparison.OrdinalIgnoreCase);
+
+        // AutoVersionUpdate 会整份拒绝含 DELIMITER 的脚本
+        Assert.DoesNotContain("DELIMITER", Read($"{Version}.sql"));
+    }
+
+    /// <summary>
+    /// <b>不得触碰任何其它表。</b>
+    /// <para>逐一点名而不是只看语句数:将来若有人在这里追加一条,
+    /// 这份名单会立刻指出他碰了哪张不该碰的表。</para>
+    /// </summary>
+    [Fact]
+    public void Migration_TouchesNoOtherTable()
+    {
+        var sql = SqlOnly($"{Version}.sql");
+        foreach (var protectedTable in new[]
+                 {
+                     "ado_s8_notification_recipient", "ado_s8_watch_rule",
+                     "ado_s8_rule_handler_user", "ado_s8_rule_responsibility_user",
+                     "ado_s8_exception", "ado_s8_exception_type", "ado_s8_exception_action_role",
+                     "ado_s8_scene_config", "SysRole", "SysUser", "SysUserRole",
+                     "mdp_", "dim_",
+                 })
+            Assert.DoesNotContain(protectedTable, sql, StringComparison.OrdinalIgnoreCase);
+    }
+
+    /// <summary>
+    /// 幂等:重跑就是再 DELETE 一次空表,影响 0 行、不报错。
+    /// <para>判据是「语句本身天然幂等」——不依赖任何 IF EXISTS 包装,
+    /// 也不依赖执行框架的去重(框架按 version + hash 记录,本就不会重复执行成功过的版本)。</para>
+    /// </summary>
+    [Fact]
+    public void Migration_IsNaturallyIdempotent()
+    {
+        var sql = SqlOnly($"{Version}.sql").Trim();
+        Assert.StartsWith("DELETE FROM", sql, StringComparison.OrdinalIgnoreCase);
+        Assert.DoesNotContain("WHERE", sql, StringComparison.OrdinalIgnoreCase);
+    }
+
+    /// <summary>
+    /// verify 断言残留为 0,并顺带守住「表还在、别的表也还在」。
+    /// <para>遵循仓库既有 verify 契约:每条语句自包含、返回布尔标量、禁止 <c>SET @var</c>。</para>
+    /// </summary>
+    [Fact]
+    public void Verify_AssertsZeroResidueAndNoCollateralDamage()
+    {
+        var sql = SqlOnly($"{Version}.verify.sql");
+
+        Assert.Contains("(SELECT COUNT(*) FROM `ado_s8_notification_layer`) = 0", sql);
+
+        // 表本身不得被删(本批只清数据)
+        Assert.Contains("TABLE_NAME = 'ado_s8_notification_layer') = 1", sql);
+
+        // 不该被动的表仍在
+        Assert.Contains("ado_s8_notification_recipient", sql);
+        Assert.Contains("ado_s8_rule_responsibility_user", sql);
+
+        // 契约:不得使用 SET @var(返回 null 会被判失败)。
+        // 只看去注释后的 SQL —— 契约说明本身就写着「禁止 SET @var」,查原文会自匹配。
+        Assert.DoesNotContain("SET @", sql, StringComparison.OrdinalIgnoreCase);
+
+        // verify 只读,不得改数据
+        foreach (var banned in new[] { "DELETE", "INSERT", "UPDATE", "DROP", "ALTER", "CREATE" })
+            Assert.DoesNotContain(banned, sql, StringComparison.OrdinalIgnoreCase);
+    }
+
+    /// <summary>
+    /// csproj 必须同时满足:版本号 ≥ 本脚本编号,且两个脚本都注册了 Copy 条目。
+    /// <para>少了 Copy 条目脚本不会被复制到输出目录,<c>AutoVersionUpdate</c> 扫不到它 ——
+    /// 那是一种「文件在仓库里、但永远不会执行」的静默失效。</para>
+    /// </summary>
+    [Fact]
+    public void Csproj_RegistersScriptsAndVersionIsAtLeastScriptVersion()
+    {
+        var csprojPath = Path.Combine(
+            new DirectoryInfo(ScriptsDir()).Parent!.FullName, "Admin.NET.Web.Entry.csproj");
+        var csproj = File.ReadAllText(csprojPath);
+
+        Assert.Contains($@"<None Update=""UpdateScripts\{Version}.sql"">", csproj);
+        Assert.Contains($@"<None Update=""UpdateScripts\{Version}.verify.sql"">", csproj);
+
+        var m = System.Text.RegularExpressions.Regex.Match(csproj, @"<Version>([\d.]+)</Version>");
+        Assert.True(m.Success, "csproj 未找到 <Version>");
+        Assert.True(System.Version.Parse(m.Groups[1].Value) >= System.Version.Parse(Version),
+            $"csproj 版本 {m.Groups[1].Value} 低于脚本编号 {Version},AutoVersionUpdate 不会应用它");
+
+        // 三处版本号必须同号
+        foreach (var tag in new[] { "AssemblyVersion", "FileVersion", "Version" })
+            Assert.Contains($"<{tag}>{m.Groups[1].Value}</{tag}>", csproj);
+    }
+}