Explorar el Código

fix(db): 回填 cancel_requested_flag 残留 NULL,收口为 NOT NULL | server 1.0.581

ado_module_dashboard_rebuild_job.cancel_requested_flag 最早由 CodeFirst 建成
可空无默认值;1.0.580.sql 的 @col_exists 判定为「已存在」而跳过了它本想建的
NOT NULL DEFAULT 0。实体属性是非空 bool,于是每次启动 CodeFirst 都执行
`alter ... change column ... tinyint(1) NOT NULL`,撞上 44506 行中的 89 行 NULL
抛 "Invalid use of NULL value";又因 ContinueInitTableOnEntityFailure=true
被降级成 warning 跳过,这张表的结构一直不完整。

1.0.581.sql 回填 NULL→0 并 MODIFY 为 NOT NULL DEFAULT 0(幂等);
1.0.581.verify.sql 断言列不可空且零 NULL。开发库已执行:命中 89 行,verify=1。

同时记录端口开放延迟的实测结论:229.1 秒中 CodeFirst InitTables 占 148s(65%)、
MdpSchemaAligner 仅占 38s(17%),故放弃方案 A1,包 R 按同步定稿。

版本从 1.0.571 跳到 1.0.581:572-580 已被 ETL 总控那批尚未提交的脚本占号。
YY968XX hace 1 día
padre
commit
469eb2ff22

+ 37 - 2
doc/plan/天缘对接/20260926启动期连接竞态根治执行任务书.md

@@ -1,7 +1,7 @@
 # 启动期数据库连接竞态根治执行任务书
 
 > **文档用途**:给**其他大模型 / 编码代理**照着执行。根因已于 2026-09-26 复现并三层取证完毕,**禁止再把它当成「MySQL 权限不足」或「SqlSugar 连接泄漏」去排查**。
-> **状态**:**全部完成(2026-09-26 23:49)**。火忘 `Task.Run` 已撤回为同步调用;5 次冷启动全部成功且竞态报错归零;`JobSchedule:Enabled` 已恢复 `true` 并复验一次仍干净(246.3s,竞态 0,未处理异常 0);守卫测试已补且验证过能拦住回归。后端版本 `1.0.571`(rebase 到远端 `1.0.570` 之上,故为 1.0.571 而非原计划的 1.0.581)。端口开放延迟实测约 160~200 秒(不含编译),超过建议阈值,是否改走方案 A1 仍待负责人定,但不阻塞本次收口。
+> **状态**:**全部完成(2026-09-26 23:49)**。火忘 `Task.Run` 已撤回为同步调用;5 次冷启动全部成功且竞态报错归零;`JobSchedule:Enabled` 已恢复 `true` 并复验一次仍干净(246.3s,竞态 0,未处理异常 0);守卫测试已补且验证过能拦住回归。后端版本 `1.0.571`(rebase 到远端 `1.0.570` 之上,故为 1.0.571 而非原计划的 1.0.581)。<br>**2026-09-27 补测并收口**:端口开放实测 229.1 秒,其中 CodeFirst `InitTables` 148s、`MdpSchemaAligner` 仅 38s;**方案 A1 已放弃**,包 R 按同步定稿(见 §4.1)。同次实测查出并修复了一个独立缺陷 `cancel_requested_flag` 残留 NULL(`1.0.581`,见 §4.2)。
 > **问题背景与完整证据**:[`20260926问题描述.md`](20260926问题描述.md),尤其第七节「复核结论与证据」。
 > **创建日期**:2026-09-26
 > **适用范围**:本机 `dotnet run` 连远程库 `123.60.180.165:3306/aidopdev`。本问题**只存在于当前工作区**,HEAD 主线不受影响(见 §1.2)。
@@ -13,7 +13,7 @@
 |---|---|---|
 | R 主修:撤回火忘任务 | **已完成(2026-09-26 22:24)** | `AiDOP/Startup.cs` 的火忘 `Task.Run` 已换回同步调用;三处必须保留的未提交改动(`AidopOutboundOptions`、两个 `AddHostedService`)未动 |
 | V 验证:连起 5 次 + 竞态归零 | **已完成(2026-09-26 22:46)** | 5/5 `listen=True`,`race=0`,`fatal=0`。耗时 232.4 / 242.2 / 206.1 / 204.6 / 201.0 秒。对齐结果与修复前一致:`mdp_source_table_registry` 53 行;`mdp_std_inv_trans` DB_SYNC 594230;`mdp_std_inventory` DB_SYNC 103600 / SEED 5;`mdp_std_item` DB_SYNC 49803 / PLATFORM_FORM 22091 |
-| M 实测:端口开放延迟 | **已完成,但口径要读批注** | 上面 5 个数字**含编译**(脚本按任务书原文每次都带编译,且重定向把输出缓冲到进程退出,无法从日志里拆出编译段)。编译本身约 40 秒(宿主进程在 `dotnet run` 之后约 40 秒才出现),故纯启动延迟约 **160~200 秒**,**超过 §4 建议的 60 秒阈值**。是否改走方案 A1 由负责人定 |
+| M 实测:端口开放延迟 | **已完成并已结论** | 2026-09-27 用端口探针重测(`--no-build`):**229.1 秒**,其中 CodeFirst `InitTables` 148s(65%)、`MdpSchemaAligner` 38s(17%)。**方案 A1 已放弃**——它只能省下那 17%,够不到 60 秒阈值,却把刚焊死的并发口子重新打开。分段数据与理由见 §4.1 |
 | S 守卫:补回归测试 | **已完成** | `Infrastructure/StartupConcurrencyGuardTests.cs` 通过;临时贴回 `Task.Run` 探针后该测试确实变红,探针已撤回 |
 | X 收口:App.json 提交风险 | **已完成(2026-09-26 23:49)** | `JobSchedule:Enabled` 恢复为 `true`。同文件的 `AiDOP:Jobs:Enabled=false`、`Outbound:Enabled=false`、`MdpRebuild` 各项属 165 治理既定决策,未动。恢复后复验一次:listen=True,246.3s,竞态 0,未处理异常 0 |
 | P 可选:ApprovalFlow 容错 | **不需要** | 包 V 已全绿且竞态归零,按 §7 第 1 条本包不做 |
@@ -216,6 +216,41 @@ SELECT TABLE_NAME, COLUMN_NAME FROM information_schema.COLUMNS
   - 延迟可接受(由负责人定阈值,建议 ≤ 60s)→ **包 R 定稿**,任务书收口;
   - 延迟不可接受 → 退回问题描述文档的**方案 A1**:把这段改为订阅 `IHostApplicationLifetime.ApplicationStarted` 后再执行。注意 A1 仍需过一遍包 V 的 5 次验证,因为它只是把窗口挪开,没有消除共享上下文。
 
+### 4.1 实测结果与结论(2026-09-27 00:36,**方案 A1 已放弃**)
+
+按端口探针重测一次(`--no-build`,编译 52 秒单独计时并排除):进程启动 `00:36:16.2` → 5005 可连 `00:40:05.4`,**229.1 秒**。分段:
+
+| 区间 | 耗时 | 内容 | 证据 |
+|---|---|---|---|
+| +0 → +5s | 5s | 运行时 / Host 引导 | 首条日志 `00:36:21.1` |
+| +5 → +153s | **148s(65%)** | SqlSugar CodeFirst `InitTables` 逐个对齐 285 个实体 | `初始化表结构 ... (1300000000001 - 001/285)` 数到 `285/285`,其间几乎无日志 |
+| +153 → +160s | 7s | `AutoVersionUpdate` + TaskQueue / Schedule 起服务 | `00:38:55.9 AutoVersionUpdate 中间件运行` |
+| +160 → +198s | **38s(17%)** | `MdpSchemaAligner` 来源登记 | 对齐日志 `00:38:56` → `00:39:34`,44 行 |
+| +198 → +229s | 31s | 其余 Configure(S8 规则供给、菜单同步、ApprovalFlow `InitTables` + 模板种子)+ Kestrel 绑定 | 端口探针 `00:40:05.4` |
+
+**结论:不采用 A1,包 R 按现状(同步)定稿。** 理由:
+
+1. **A1 解决不了被问的那个问题。** 挪走对齐只能把 229s 压到约 191s,§4 建议的 60 秒阈值照样够不着。延迟的主因是那 148 秒的 CodeFirst,不是对齐器,这个决策点本身是伪命题。
+2. **代价换来的是同一类风险。** `ApplicationStarted` 确实比 Configure 里 `Task.Run` 安全(那时 Configure 已跑完,不再撞 ApprovalFlow 的无容错 `InitTables`),但那一刻 `ModuleRebuildJobStore` 轮询、`EtlInstanceRegistrar` 心跳、`ScheduleService` 都已在后台运行,单例 `SqlSugarScope` 的 AsyncLocal 复用隐患仍在。为 17% 的收益重新打开刚焊死的门,不划算。
+3. **同步顺带保证一条不变量**:端口开放前 `source_system` / `written_by` 已对齐完。异步会出现一个窗口,期间监控页与证据页读到未对齐的数据 —— 那是正确性回退。
+
+**若确实要提速,该动的是那 148 秒**:本机开发环境把 `TableSettings:EnableInitTable` 置 `false`(`Database.json:43`,结构稳定后本地改、不提交),或本地指向本地 MySQL 而非云库(当前连的是远端主机,每实体一次元数据往返约 0.5 秒)。
+
+> **禁止**调大 `InitTableMaxDegreeOfParallelism`(现为 1,`Database.json:45`)。`SqlSugarSetup.cs:662` 的 `Parallel.ForEach` 捕获的是同一个 `dbProvider`,并行 DDL 就是本任务书刚修掉的那类连接复用,只是换了个入口。
+
+### 4.2 本次实测顺带查出的独立缺陷(已修,`1.0.581`)
+
+`ado_module_dashboard_rebuild_job.cancel_requested_flag` 的表结构初始化**每次启动都失败**:
+
+```
+[Sql]: alter table `ado_module_dashboard_rebuild_job` change column `cancel_requested_flag` `cancel_requested_flag` tinyint(1) NOT NULL
+SqlSugar.SqlSugarException: Invalid use of NULL value
+```
+
+成因:该列最早由 CodeFirst 建成可空无默认值,`1.0.580.sql` 的 `@col_exists` 判定为「已存在」而跳过了它本想建的 `NOT NULL DEFAULT 0`;实体属性是非空 `bool`,于是每次启动都尝试改 NOT NULL,撞上 44506 行中的 89 行 NULL。又因 `ContinueInitTableOnEntityFailure=true`(`Database.json:49`)被降级成 warning 跳过,这张表的结构一直是不完整的。
+
+已由 `UpdateScripts/1.0.581.sql` 回填 NULL→0 并把列收口为 `NOT NULL DEFAULT 0`;`1.0.581.verify.sql` 断言列不可空且零 NULL。开发库已执行:UPDATE 命中 89 行,verify 返回 1。
+
 ---
 
 ## 5. 包 S:补守卫测试(当前零防护)

+ 2 - 2
doc/plan/天缘对接/20260926问题描述.md

@@ -4,7 +4,7 @@
 
 > 记录日期:2026-09-26
 > 环境:本机 `dotnet run`(net10.0)连远程库 `123.60.180.165:3306/aidopdev`
-> 状态:**已修复并验证**(2026-09-26 22:46)。火忘 `Task.Run` 已撤回为同步调用,5 次冷启动全部成功、竞态报错归零,后端版本 `1.0.571`(rebase 到远端 `1.0.570` 之上,故为 1.0.571 而非原计划的 1.0.581)。端口开放延迟实测约 160~200 秒(不含编译),超过任务书建议阈值,是否改走方案 A1 待负责人定。详见 [`20260926启动期连接竞态根治执行任务书.md`](20260926启动期连接竞态根治执行任务书.md) 进度表)
+> 状态:**已修复并验证**(2026-09-26 22:46)。火忘 `Task.Run` 已撤回为同步调用,5 次冷启动全部成功、竞态报错归零,后端版本 `1.0.571`(rebase 到远端 `1.0.570` 之上,故为 1.0.571 而非原计划的 1.0.581)。**2026-09-27 补测收口**:端口开放实测 229.1 秒,分段后 CodeFirst `InitTables` 占 148s(65%)、`MdpSchemaAligner` 仅占 38s(17%),**方案 A1 已放弃**(省不下 17% 以外的任何东西,却会把并发口子重新打开),同步版本定稿。同次实测查出并修复了一个独立缺陷:`ado_module_dashboard_rebuild_job.cancel_requested_flag` 残留 89 行 NULL 导致每次启动该表结构对齐失败被静默跳过,已由 `1.0.581.sql` 收口。详见 [`20260926启动期连接竞态根治执行任务书.md`](20260926启动期连接竞态根治执行任务书.md) 进度表)
 
 ## 一、现象
 
@@ -110,7 +110,7 @@ if (App.GetConfig<bool?>("JobSchedule:Enabled", true) ?? true)
 
 > **2026-09-26 复核批注(先读这段再选方案)**
 >
-> - **A1 应替换为「revert」**:既然火忘 `Task.Run` 是未提交回归(见 2.5 更正),把它改成「等 `ApplicationStarted`」等于在一处回归上再叠一层设计。更小且更稳的做法是 `git checkout HEAD --` 那一个 hunk,直接回到已被验证过的同步版本。代价只有原作者注释里那句「端口要等它结束才开」,**须实测该延迟**后再定是否接受。
+> - **A1 应替换为「revert」**:既然火忘 `Task.Run` 是未提交回归(见 2.5 更正),把它改成「等 `ApplicationStarted`」等于在一处回归上再叠一层设计。更小且更稳的做法是 `git checkout HEAD --` 那一个 hunk,直接回到已被验证过的同步版本。代价只有原作者注释里那句「端口要等它结束才开」,**须实测该延迟**后再定是否接受。<br>**2026-09-27 已实测并定案:A1 放弃。** 对齐只占 229.1 秒启动时长中的 38 秒(17%),挪走它够不到 60 秒阈值;且 `ApplicationStarted` 触发时后台轮询与心跳已在跑,共享上下文的隐患没有消除。分段数据见任务书 §4.1。
 > - **A2 撞了仓库硬规则**:`.cursor/rules/approval-flow-integration.mdc` 明令「不得在未登记为后期待办的前提下修改 `Admin.NET.Plugin.ApprovalFlow` 插件自身代码」,要动须先在 `doc/plan/审批流-综合优化方案.md` 登记 P4-XX。A2 本身合理(把崩溃降级为稍后成功),但**不能直接落地**。
 > - **④⑤ 只是推断,没有实证**:日志里被证实互撞的只有 ①↔②(逐条 SQL 见第七节)。消掉 ① 之后是否还撞,须按第七节口径连起多次验证,不要预先假定还需要 A2。
 >

+ 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.571</AssemblyVersion>
-    <FileVersion>1.0.571</FileVersion>
-    <Version>1.0.571</Version>
+    <AssemblyVersion>1.0.581</AssemblyVersion>
+    <FileVersion>1.0.581</FileVersion>
+    <Version>1.0.581</Version>
   </PropertyGroup>
 
   <ItemGroup>
@@ -934,6 +934,12 @@
     <None Update="UpdateScripts\1.0.565.verify.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
+    <None Update="UpdateScripts\1.0.581.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.581.verify.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
     <None Update="UpdateScripts\UAT-PLACEHOLDER-MENU-HIDE.ops.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>

+ 19 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.581.sql

@@ -0,0 +1,19 @@
+-- 1.0.581 —— 回填 cancel_requested_flag 的历史 NULL,并把列收口为 NOT NULL。
+-- 背景:该列最早由 CodeFirst 建成可空且无默认值;1.0.580 的 @col_exists 判定为「已存在」,
+--       于是跳过了它本想建的 NOT NULL DEFAULT 0,列至今仍可空并残留历史 NULL 行。
+--       实体 AdoModuleDashboardRebuildJob.CancelRequestedFlag 是非空 bool,因此每次启动
+--       CodeFirst 都会执行 `alter ... change column cancel_requested_flag ... NOT NULL`,
+--       撞上 NULL 行抛 "Invalid use of NULL value";又因 Database.json 里
+--       TableSettings:ContinueInitTableOnEntityFailure=true,异常被降级成 warning 跳过,
+--       这张表的结构每次启动都是不完整的(见 doc/plan/天缘对接/20260926问题描述.md §七)。
+--       历史行语义上等同「未请求取消」,回填 0 不改变任何业务判断。
+--
+-- 幂等:UPDATE 命中 0 行即无副作用;MODIFY 到目标定义可重复执行。
+
+UPDATE ado_module_dashboard_rebuild_job
+   SET cancel_requested_flag = 0
+ WHERE cancel_requested_flag IS NULL;
+
+ALTER TABLE ado_module_dashboard_rebuild_job
+  MODIFY COLUMN cancel_requested_flag TINYINT(1) NOT NULL DEFAULT 0
+  COMMENT '管理员请求协作式取消,阶段边界生效';

+ 13 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.581.verify.sql

@@ -0,0 +1,13 @@
+-- 表尚未建成时放行。否则要求:列不可空,且无残留 NULL(不可空后必然为 0 行,双保险)。
+SELECT
+  (SELECT COUNT(*) FROM information_schema.TABLES
+    WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ado_module_dashboard_rebuild_job') = 0
+  OR (
+    (SELECT IS_NULLABLE FROM information_schema.COLUMNS
+      WHERE TABLE_SCHEMA=DATABASE()
+        AND TABLE_NAME='ado_module_dashboard_rebuild_job'
+        AND COLUMN_NAME='cancel_requested_flag') = 'NO'
+    AND
+    (SELECT COUNT(*) FROM ado_module_dashboard_rebuild_job
+      WHERE cancel_requested_flag IS NULL) = 0
+  );