|
|
@@ -0,0 +1,659 @@
|
|
|
+# 九宫格 S5–S9 无数据根因修复执行任务书
|
|
|
+
|
|
|
+> 适用范围:智慧运营九宫格看板(FUNC-S9-005,`/aidop/smart-ops/grid`)取数链路、MDP 贴源入站、T8 外部数据源连接
|
|
|
+> 目标读者:后端开发、前端开发、大语言模型执行代理、测试负责人
|
|
|
+> 编写日期:2026-09-28(同日自查修订一次,见 §0 的四条推翻记录)
|
|
|
+> 任务类型:根因修复 / 数据可见性治理 / 死代码清理
|
|
|
+> 基线提交:`85acd840f`(server 1.0.590 / Web 2.4.412)
|
|
|
+> 运行库:MySQL `123.60.180.165:3306/aidopdev`,UAT 租户 `838257186181189`,factory_id `1`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 0. 执行者须知(先读这一节)
|
|
|
+
|
|
|
+本任务书的调查过程中**产生过四个错误结论,均已被实测推翻**。写在最前面,是为了防止执行者从别处(历史会话、旧文档、口头转述、本文档的早期版本)把它们再捡回来:
|
|
|
+
|
|
|
+| 曾经的错误结论 | 实际情况 | 推翻它的证据 |
|
|
|
+|---|---|---|
|
|
|
+| ❌「九宫格空白是因为 `GetHomeL1` 用了全局 `MAX(biz_date)`,把 S4–S9 筛掉了」 | `GetHomeL1` **不是**九宫格的数据源。九宫格走 `GetHomeGridGeneric`,该端点早已是布局驱动 + **每指标各自**取最新业务日,写法正确 | `HomeModuleKpiCard.vue:23` → `fetchHomeGrid` → `kanbanData.ts:1275` → `GET /api/AidopKanban/home-grid/{moduleCode}` → `AidopKanbanController.Generic.cs:64-141`;`fetchHomeL1` 全仓零调用点 |
|
|
|
+| ❌「九个模块都有 L1 数据,是查询把它们筛掉了」 | 九个模块都有 **行**,但 S5/S6/S7 的绝大多数行是 `result_status='NO_DATA'` 且 `metric_value IS NULL`。卡片一直在渲染,只是没有值 | 见 §1.3 的实测结果 |
|
|
|
+| ❌「S5 死在 `InventoryMdpSyncService`,S7 死在 `ShipTransNeutralProjection`,各一处」 | **S5 与 S7 死在同一处**(`InventoryMdpSyncService.cs:891`)。`ShipTransNeutralProjection` 位于其下游 `:876`,从未被执行到 | 见 §3.B.2 的完整调用栈 |
|
|
|
+| ❌「`db_extra_params` 的非法值应在源配置保存时校验拦截」 | 该字段**根本无法通过保存接口设置**,脏值来自已提交的迁移脚本 `1.0.178.sql:210`。"保存时校验"无处施力 | 全仓检索 `DbExtraParams` 仅两处命中(实体 + 消费点);`MdpSourceConfigService.Save:84` 的入参 DTO 无此字段 |
|
|
|
+
|
|
|
+**教训有两条**:
|
|
|
+
|
|
|
+1. **数了行数不等于有数据。** 本任务书里所有"有/没有"的断言都带 `result_status` 与 `metric_value` 的取值,执行者验收时必须照同样标准,不得只 `COUNT(*)`。
|
|
|
+2. **按语义猜调用关系必错。** 上表第三行就是"S5 是库存所以走库存服务、S7 是发运所以走发运投影"这种想当然的产物。**一律以实际堆栈/实际检索为准**,本任务书凡涉及调用关系处均已附堆栈或检索结果。
|
|
|
+
|
|
|
+另:本任务书遵循仓库既有约定,执行时必须一并满足:
|
|
|
+
|
|
|
+- 动手前先按 §7 列出改动清单并取得确认(`.cursor/rules/collaboration-scope.mdc`)
|
|
|
+- 交付前必须写「反向影响推演」小节(`.cursor/rules/impact-backtrace.mdc`),§6 给出了本轮的基线版本,执行者若扩大范围须自行补充
|
|
|
+- 能力断言必须分代码层/配置层/数据层三层取证(`.cursor/rules/capability-claims-evidence.mdc`)
|
|
|
+- 版本号按本次提交实际纳入的端递增(`.cursor/rules/version-bump-on-commit.mdc`),详见 §8
|
|
|
+- **新增的 `UpdateScripts/*.sql` 必须在 `Admin.NET.Web.Entry.csproj` 里登记**,否则脚本永远不会执行,详见 §8.2
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. 真实根因
|
|
|
+
|
|
|
+### 1.1 结论一句话
|
|
|
+
|
|
|
+九宫格 S5–S9 看起来没数据,是**三件独立的事**叠在一起:
|
|
|
+
|
|
|
+1. **S5 唯一算出真值的指标没有被登记**,因此不显示 —— 显示侧缺陷,本任务 A。
|
|
|
+2. **S5/S7 的重算跑不完**,被一条诊断日志语句中途杀死 —— 计算侧缺陷,本任务 B。
|
|
|
+3. **T8 外部源连不上**,源数据根本没进来,所以绝大多数指标只能写 `NO_DATA` —— 数据侧缺陷,本任务 C。
|
|
|
+
|
|
|
+B 和 C 才是格子真正空着的原因;A 是一个被冤枉的指标。
|
|
|
+
|
|
|
+另有两项伴生治理:
|
|
|
+
|
|
|
+4. **卡片值与展示日期口径不一致**,会把 8 月的数值标成 9 月的 —— 展示侧缺陷,本任务 E。
|
|
|
+5. **一条已死但会误导后人的取数链路**,内含编造的演示数字 —— 清理,本任务 D。
|
|
|
+
|
|
|
+### 1.2 取数链路(已核实,不要再改这条路)
|
|
|
+
|
|
|
+```
|
|
|
+Web/src/views/dashboard/home.vue
|
|
|
+ └─ HomeModuleKpiCard.vue:23 → fetchHomeGrid()
|
|
|
+ └─ kanbanData.ts:1275 → GET /api/AidopKanban/home-grid/{moduleCode}
|
|
|
+ └─ AidopKanbanController.Generic.cs:64 GetHomeGridGeneric()
|
|
|
+ ├─ 卡片集:ado_smart_ops_layout_item(MetricLevel=1, IsEnabled=1,按 SortNo)
|
|
|
+ ├─ 指标元数据:ado_smart_ops_kpi_master
|
|
|
+ └─ 指标值:ado_s9_kpi_value_l1_day,每个 metric_code 各自取 MAX(biz_date)
|
|
|
+```
|
|
|
+
|
|
|
+值查询的相关子查询在 `AidopKanbanController.Generic.cs:100-107`,它按 `metric_code` 逐个取各自最新业务日,**不是**按模块、更不是全局。这个写法是正确的,本任务书**不修改**它。
|
|
|
+
|
|
|
+### 1.3 数据层实测基线(可重跑)
|
|
|
+
|
|
|
+```sql
|
|
|
+-- 九宫格 S5/S6/S7/S9 实际会拿到的值(与 Generic.cs:96-108 的 valSql 等价)
|
|
|
+SELECT v.module_code, v.metric_code, v.metric_value, v.result_status, v.biz_date
|
|
|
+FROM ado_s9_kpi_value_l1_day v
|
|
|
+WHERE v.tenant_id=838257186181189 AND v.factory_id=1
|
|
|
+ AND v.module_code IN ('S5','S6','S7','S9') AND v.is_deleted=0
|
|
|
+ AND v.biz_date=(SELECT MAX(v2.biz_date) FROM ado_s9_kpi_value_l1_day v2
|
|
|
+ WHERE v2.tenant_id=v.tenant_id AND v2.factory_id=v.factory_id
|
|
|
+ AND v2.module_code=v.module_code AND v2.metric_code=v.metric_code
|
|
|
+ AND v2.is_deleted=0)
|
|
|
+ORDER BY v.metric_code;
|
|
|
+```
|
|
|
+
|
|
|
+2026-09-28 22:5x 实测结果:
|
|
|
+
|
|
|
+| 指标 | 值 | 状态 | biz_date | 九宫格上是否可见 |
|
|
|
+|---|---|---|---|---|
|
|
|
+| S5_L1_001 | NULL | NO_DATA | 2026-09-25 | 可见(显示无业务数据) |
|
|
|
+| S5_L1_002 | NULL | NO_DATA | 2026-09-25 | 可见(显示无业务数据) |
|
|
|
+| S5_L1_003 | NULL | NO_DATA | 2026-08-30 | 可见(显示无业务数据) |
|
|
|
+| **S5_L1_004** | **31.6653** | **OK** | 2026-08-30 | **不可见 ← 任务 A** |
|
|
|
+| S6_L1_001 / 002 / 003 | NULL | NO_DATA | 09-26 / 09-26 / 08-30 | 可见(显示无业务数据) |
|
|
|
+| S7_L1_001 | NULL | NO_DATA | 2026-09-26 | 可见(显示无业务数据) |
|
|
|
+| S7_L1_002 | 0.0000 | OK | 2026-09-26 | 可见 |
|
|
|
+| S7_L1_003 | NULL | NO_DATA | 2026-08-30 | 可见(显示无业务数据) |
|
|
|
+| S9_L1_001 | NULL | NO_DATA | 2026-08-30 | 可见(显示无业务数据) |
|
|
|
+| S9_L1_002 | 193.7930 | OK | 2026-09-23 | 可见 |
|
|
|
+| S9_L1_003 | 0.0000 | OK | 2026-09-22 | 可见 |
|
|
|
+| S9_L1_004 | 100.0000 | OK | 2026-08-17 | 可见 |
|
|
|
+| S9_L1_005 | 513.3946 | OK | 2026-09-24 | 可见 |
|
|
|
+
|
|
|
+**S9 其实有 4 个真值在显示**,与"S5–S9 全空"的直觉不符,执行者验收前请先自行重跑上表确认现状。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. 任务 A:补齐 S5_L1_004 的租户登记
|
|
|
+
|
|
|
+### A.1 问题
|
|
|
+
|
|
|
+`S5_L1_004`「品类物料库存周转」是一个**完整的一等指标**,代码层全部就绪,唯独在 UAT 租户的**指标字典**与**布局表**里没有行,因此计算出来的真值(31.6653)在九宫格上不可见。
|
|
|
+
|
|
|
+三层取证:
|
|
|
+
|
|
|
+- **代码层:有。** 算法 `S5MdpSyncTransformService.cs:521 BuildS5L1004MaterialInventoryTurnoverAsync`,调用点 `:123-124`;语义依赖 `KpiSemanticDependency.cs:34`;智慧诊断证据 `SmartDiagnosisEvidenceRegistry.cs:1337-1339`;筛选白名单 `AidopKanbanController.SmartOpsFilter.cs:17`;守卫测试 `SmartDiagnosisEvidenceTests.cs:64`、`KpiResultStatusResolverTests.cs:21`。
|
|
|
+- **配置层:缺。** `SELECT TenantId, COUNT(*) FROM ado_smart_ops_kpi_master WHERE MetricCode='S5_L1_004' GROUP BY TenantId` → 只有 `0` 与 `1300000000001` 两行,**没有** `838257186181189`。布局表同样没有(见下方校验 SQL)。
|
|
|
+- **数据层:通。** 值已在 `ado_s9_kpi_value_l1_day` 落库,`metric_value=31.6653`、`result_status='OK'`。
|
|
|
+
|
|
|
+### A.2 根因
|
|
|
+
|
|
|
+`AidopKpiMasterSeed.cs` 的 S5 段(`:144-158`)只定义了 `S5_L1_001/002/003`,**从来没有 `S5_L1_004`**。历史上它是靠一次性迁移脚本手工补进两个租户的(`1.0.187.sql` 补 `1300000000001`),种子本身没修,所以此后每个新建租户都会缺这一项。
|
|
|
+
|
|
|
+布局表是由指标字典派生的(`AidopKpiMasterSeed.cs:1006-1040`),且 `:1008` 有 `if (layoutExists) continue;` —— 已有布局的租户不会被二次补齐。所以**改种子只能救新租户,存量租户必须靠迁移脚本**。
|
|
|
+
|
|
|
+### A.3 改动清单
|
|
|
+
|
|
|
+**A.3.1 修种子(治新租户)**
|
|
|
+
|
|
|
+文件:`server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/AidopKpiMasterSeed.cs`
|
|
|
+
|
|
|
+在 `l1_s5_3`(`:156`)之后增加一条 `Ins(...)`,参数取自 `TenantId=0` 的模板行:
|
|
|
+
|
|
|
+| 字段 | 值 |
|
|
|
+|---|---|
|
|
|
+| MetricCode | `S5_L1_004` |
|
|
|
+| ModuleCode | `S5` |
|
|
|
+| MetricLevel | `1` |
|
|
|
+| MetricName | `品类物料库存周转` |
|
|
|
+| Unit | `天` |
|
|
|
+| Direction | `lower_is_better` |
|
|
|
+| YellowThreshold | `110` |
|
|
|
+| RedThreshold | `120` |
|
|
|
+| Formula | `D1 / D2 × 30` |
|
|
|
+
|
|
|
+SortNo:取一个**大于 S5_L1_003(当前 19)**的值即可。布局是按 `moduleCode` 分组生成的(`:1010` 的 `module.OrderBy(...)`),跨模块重号无影响,只需保证模块内 S5_L1_004 排在 003 之后。先读取邻近取值再定,不要盲填。
|
|
|
+
|
|
|
+> 注意:`TenantId=0` 模板行的 `SortNo` 是 `540`,与种子里 17/18/19 这套编号不是同一套体系,**不要直接抄 540**。
|
|
|
+
|
|
|
+**A.3.2 写迁移(治存量租户)**
|
|
|
+
|
|
|
+新建 `server/Admin.NET.Web.Entry/UpdateScripts/<新版本>.sql`,要求**幂等**,按以下顺序:
|
|
|
+
|
|
|
+1. 对每个"**已有 `S5_L1_004` 值行、但 `ado_smart_ops_kpi_master` 里没有该指标**"的租户,以 `TenantId=0` 的行为模板 `INSERT ... SELECT` 补齐字典行。用 `NOT EXISTS` 守卫,`TenantId`/`Id` 之外的业务字段原样复制。可参照既有写法 `UpdateScripts/1.0.187.sql`。
|
|
|
+2. 对每个"**字典已有、布局缺行**"的 (tenant, factory, module) 组合,向 `ado_smart_ops_layout_item` 插入一行:
|
|
|
+ - `RowId = 'S5-L1-S5_L1_004'`(与 `AidopKpiMasterSeed.cs:1016` 的 `$"{moduleCode}-L{kpi.MetricLevel}-{kpi.MetricCode}"` 完全一致,**不要自创格式**)
|
|
|
+ - `MetricLevel=1`、`MetricCode='S5_L1_004'`、`DisplayName='品类物料库存周转'`、`IsEnabled=1`、`ParentRowId=NULL`
|
|
|
+ - `PanelZone` 按 `ResolveDefaultPanelZone`(`AidopKpiMasterSeed.cs:1044`)对 S5/L1 的返回值填写,执行前先读该方法确认
|
|
|
+ - `SortNo` 与 A.3.1 保持一致
|
|
|
+3. 迁移**不得**限定 `tenant_id=838257186181189`。按"有值却无登记"的条件驱动,这样其它租户的同类缺口一并收口。
|
|
|
+
|
|
|
+> **排序规则陷阱**:`ado_smart_ops_layout_item.MetricCode` 与 `ado_s9_kpi_value_l1_day.metric_code` 的 collation 不一致,两表直接 JOIN 会报 `Illegal mix of collations`。控制器里是显式加 `COLLATE utf8mb4_general_ci` 绕过的(见 `AidopKanbanController.cs:101`),迁移脚本与验收 SQL 里凡跨这两表比较 `metric_code` 的地方都必须照做。`ado_smart_ops_kpi_master.MetricCode` 同理。
|
|
|
+
|
|
|
+**A.3.3 配套 verify 脚本**
|
|
|
+
|
|
|
+新建同名 `.verify.sql`,返回单个布尔:对每个有 `S5_L1_004` 值的租户,字典行与布局行都存在。
|
|
|
+
|
|
|
+### A.4 验收
|
|
|
+
|
|
|
+```sql
|
|
|
+-- ① 值有而布局无 的指标应为 0 行
|
|
|
+SELECT DISTINCT v.module_code, v.metric_code
|
|
|
+FROM ado_s9_kpi_value_l1_day v
|
|
|
+LEFT JOIN ado_smart_ops_layout_item l
|
|
|
+ ON l.TenantId=v.tenant_id AND l.FactoryId=v.factory_id
|
|
|
+ AND l.MetricCode COLLATE utf8mb4_general_ci = v.metric_code
|
|
|
+ AND l.MetricLevel=1 AND l.IsEnabled=1
|
|
|
+WHERE v.tenant_id=838257186181189 AND v.factory_id=1 AND v.is_deleted=0
|
|
|
+ AND l.Id IS NULL;
|
|
|
+```
|
|
|
+
|
|
|
+修复前返回 `S5 / S5_L1_004` 一行,修复后必须返回 0 行。
|
|
|
+
|
|
|
+页面侧:以 UATAdminA 登录,`/aidop/smart-ops/grid` 的 S5 卡片应从 3 项变为 4 项,新增「品类物料库存周转 31.6653 天」。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. 任务 B:诊断日志语句杀掉 S5/S7 重算
|
|
|
+
|
|
|
+### B.1 问题
|
|
|
+
|
|
|
+S5 与 S7 的重算在**入站成功之后、KPI 转换阶段**抛异常中止,作业落 `FAILED`,`error_message = "KPI 计算失败(源数据已入站,看板保留原有数据)"`。
|
|
|
+
|
|
|
+实测异常(`AidopT8KpiManualRefreshService`,2026-09-28 18:15:32 与 18:20:50):
|
|
|
+
|
|
|
+```
|
|
|
+MySqlConnector.MySqlException (0x80004005): Row 103 was cut by GROUP_CONCAT()
|
|
|
+ at SqlSugar.MySqlProvider.ExecuteCommandAsync(String sql, SugarParameter[] parameters)
|
|
|
+```
|
|
|
+
|
|
|
+数据层佐证:`SELECT @@group_concat_max_len, @@sql_mode` → `1024`,`sql_mode` 含 `STRICT_TRANS_TABLES`。严格模式把 GROUP_CONCAT 的截断**警告**升级成**错误**。
|
|
|
+
|
|
|
+### B.2 根因(两个缺陷叠加,必须都修)
|
|
|
+
|
|
|
+**缺陷一:`LEFT(GROUP_CONCAT(...), 500)` 是无效防御。**
|
|
|
+作者的本意是"这个采样字段截到 500 字符就够",但 `GROUP_CONCAT` 先在内部按 1024 字节截断并抛警告,`LEFT` 根本没有机会介入。仓库里有两处同构写法:
|
|
|
+
|
|
|
+| # | 位置 | 现状 |
|
|
|
+|---|---|---|
|
|
|
+| 一 | `MaterialWarehouse/InventoryMdpSyncService.cs:896`(方法 `LogWorkOrderNotFoundAsync:885`) | **已引爆**,S5 与 S7 都死在这里 |
|
|
|
+| 二 | `DataPlatform/Wms/ShipTransNeutralProjection.cs:127` | **未引爆的同类地雷**,见下 |
|
|
|
+
|
|
|
+**S5 与 S7 是死在同一个点上**,不是各死一处。完整调用栈(2026-09-28 18:15:32 与 18:20:50 实测):
|
|
|
+
|
|
|
+```
|
|
|
+InventoryMdpSyncService.LogWorkOrderNotFoundAsync :891
|
|
|
+ ← InventoryMdpSyncService.MaterializeInvTransStdAsync :871
|
|
|
+ ← InventoryMdpSyncService.TransformTransStdFromStgAsync :299
|
|
|
+ ← S5MdpSyncTransformService.RunFullAsync :110 ← S5 走这条
|
|
|
+ ← S7MdpSyncTransformService.RunFullAsync :100 ← S7 走这条
|
|
|
+ ← AidopT8KpiManualRefreshService.RunModuleRefreshAsync :155 / :173
|
|
|
+```
|
|
|
+
|
|
|
+**第二处为什么至今没炸,以及为什么必须一起修**:`MaterializeInvTransStdAsync` 里 `LogWorkOrderNotFoundAsync` 在 `:871` 调用,而 `_ship.ProjectAsync`(即第二处所在)在 `:876`。`:871` 先抛异常,`:876` 从未被执行到。一旦修好第一处,执行流会**第一次**走到第二处,同类崩溃极可能立即出现。
|
|
|
+
|
|
|
+> 因此 B.4 的验收**必须包含"修完第一处后重跑并确认第二处不炸"**,不能修完一处就收工。
|
|
|
+
|
|
|
+本仓库其它地方**已经踩过这个坑并留了字条**,改动时请与它们保持一致的处理范式:
|
|
|
+
|
|
|
+- `Supply/S3MdpSyncTransformService.cs:880-881`:「去重用 ROW_NUMBER 而不是 GROUP_CONCAT+SUBSTRING_INDEX:…… `group_concat_max_len` 实测仅 1024,拼接必然截断」
|
|
|
+- `DataPlatform/S0Dim/S0DimSqlBuilder.cs:250`:「用 SUM 而非 GROUP_CONCAT/BIT_XOR ……不受 `group_concat_max_len` 截断」
|
|
|
+
|
|
|
+**缺陷二:诊断日志写在致命路径上。**
|
|
|
+上述两条语句写的都是 `mdp_source_gate_log`(`sample_keys` 列),是**纯诊断**记录。一条 gate log 写失败,把整条 S5/S7 业务管线干掉了。即使缺陷一修好,下次诊断写入因别的原因失败仍会再杀一次。
|
|
|
+
|
|
|
+同一个仓库里已有正确范式:`DataPlatform/Executors/MdpDbPullExecutor.cs:231-234` 的作用域探针明写「探针纯诊断用途,任何失败都不得影响主流程,按"无异常"处理」。
|
|
|
+
|
|
|
+### B.3 改动清单
|
|
|
+
|
|
|
+**B.3.1 采样在进聚合之前限条**
|
|
|
+
|
|
|
+两处 SQL 改为:先用子查询按 `ROW_NUMBER()` 取前 N 条去重键,再对这 N 条做 `GROUP_CONCAT`。要求:
|
|
|
+
|
|
|
+- N 的取值必须保证 `N × 单元素最大长度 < 1024`,使结果**不依赖任何服务器变量**。`work_ord` 一类编码按 32 字节估算,N 取 20 是安全的;`ShipTransNeutralProjection` 的元素是 `OrdNbr:OrdLine` 拼接,需按实际字段长度重估后再定 N,不要照抄 20。
|
|
|
+- 保留外层 `LEFT(..., 500)` 作为列宽兜底(`sample_keys` 的列定义约束),但注释要写明它**不是**防截断手段。
|
|
|
+- `row_count` 仍统计**全量**条数,不受采样限条影响 —— 采样是样例,计数是事实,两者不能一起被截。
|
|
|
+
|
|
|
+**明确否决的方案**:`SET SESSION group_concat_max_len = <更大值>`。理由:① 只是把悬崖往后挪,元素再多一样炸;② SqlSugar 走连接池,需要保证每条连接都设过,是脆弱的会话态依赖;③ 完全不解决缺陷二。
|
|
|
+
|
|
|
+**B.3.2 gate log 写入移出致命路径**
|
|
|
+
|
|
|
+把这两处 `mdp_source_gate_log` 的 `INSERT` 包进 try/catch,失败只记 `LogWarning`,不向上抛。注释须说明:gate log 是数据质量诊断记录,它的失败不得中断业务管线;参照 `MdpDbPullExecutor.cs:231-234`。
|
|
|
+
|
|
|
+> 边界:**只放行 gate log 的写入失败**。同一方法里其它语句(贴源写入、投影 upsert)的异常仍必须向上抛,不得顺手一起吞掉。
|
|
|
+
|
|
|
+**B.3.3 守卫测试**
|
|
|
+
|
|
|
+在 `server/Plugins/Admin.NET.Plugin.AiDOP.Tests/` 下新增测试,至少覆盖:
|
|
|
+
|
|
|
+1. 两处源码中不再出现「`GROUP_CONCAT` 直接作用于未限条集合」的形态(可用源码文本断言,锚定到方法体而非接口声明 —— 本仓库有过因锚点匹配到接口声明而误判的先例)。
|
|
|
+2. gate log 的 `INSERT` 处于 try/catch 内。
|
|
|
+3. 采样限条的 N 值存在且小于安全上界。
|
|
|
+
|
|
|
+### B.4 验收
|
|
|
+
|
|
|
+代码层:上述守卫测试通过。
|
|
|
+
|
|
|
+数据层:以 UATAdminA 在 `/aidop/smart-ops/s5` 与 `/s7` 点「重算数据」,然后
|
|
|
+
|
|
|
+```sql
|
|
|
+SELECT id, module_code, trigger_type, status, error_message
|
|
|
+FROM ado_module_dashboard_rebuild_job
|
|
|
+WHERE requested_by IS NOT NULL AND submitted_at > <本次重算时刻>
|
|
|
+ORDER BY submitted_at DESC;
|
|
|
+```
|
|
|
+
|
|
|
+S5 与 S7 的 `status` 必须不再是 `FAILED`(允许 `SUCCESS`;若因任务 C 未完成而入站失败,可以是 `FAILED` 但 `error_message` 必须是入站类而**不是**「KPI 计算失败」)。
|
|
|
+
|
|
|
+后端日志中 `Row 103 was cut by GROUP_CONCAT()` 不得再出现 —— **注意要连着检查两遍**:第一遍确认 `InventoryMdpSyncService.cs:891` 不再抛;由于 `ShipTransNeutralProjection` 此前从未被执行到(见 B.2),第一遍跑通后必须**再跑一次并确认栈里没有 `ShipTransNeutralProjection.ProjectAsync`**,否则只是把崩溃点往后挪了 5 行。
|
|
|
+
|
|
|
+`mdp_source_gate_log` 应出现新行,`sample_keys` 非空且长度受控,`row_count` 与实际不匹配行数一致(不受采样限条影响)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. 任务 C:T8 外部源连接串被一条配置写坏
|
|
|
+
|
|
|
+### C.1 问题
|
|
|
+
|
|
|
+22:00 那批 `AUTO` 重算的入站阶段失败,`error_message = "源数据入站失败,看板保留原有数据"`。实测异常:
|
|
|
+
|
|
|
+```
|
|
|
+SqlSugar.SqlSugarException: Connection open error . 不支持的关键字:"configid"。
|
|
|
+ at SqlSugar.SqlServerProvider.get_Connection()
|
|
|
+ at Admin.NET.Plugin.AiDOP.DataPlatform.Executors.MdpDbPullExecutor.PullAsync(...) :line 101
|
|
|
+ at Admin.NET.Plugin.AiDOP.DataPlatform.T8BaseInboundMdpSyncService.RunInboundAsync(...) :line 91
|
|
|
+```
|
|
|
+
|
|
|
+### C.2 根因
|
|
|
+
|
|
|
+配置层:
|
|
|
+
|
|
|
+```sql
|
|
|
+SELECT source_code, db_extra_params, (db_password_enc IS NULL OR db_password_enc='') AS pwd_empty,
|
|
|
+ health_status, LEFT(health_msg,150) AS msg
|
|
|
+FROM mdp_source WHERE source_code IN ('T8_V5_SQLSERVER','DOPDEMORQ_SQLSERVER');
|
|
|
+```
|
|
|
+
|
|
|
+| source_code | db_extra_params | 口令为空 | 健康 |
|
|
|
+|---|---|---|---|
|
|
|
+| `T8_V5_SQLSERVER` | `ConfigId=t8_v5;TrustServerCertificate=true;Encrypt=false` | **是** | 失败 |
|
|
|
+| `DOPDEMORQ_SQLSERVER` | `NULL` | 否 | OK |
|
|
|
+
|
|
|
+`ConfigId` 是 **SqlSugar** 的概念,不是 SQL Server 连接串关键字。`MdpSourceScopeFactory.BuildConnectionString`(`:117-139`)把 `db_extra_params` **原样拼接**到连接串尾部(`:125` 组装 `extra`,`:129-130` 为 SqlServer 分支),于是 SqlClient 报「不支持的关键字」。
|
|
|
+
|
|
|
+附带问题:该 `extra` 里的 `TrustServerCertificate` 与 `Encrypt` 与 `:130` 已硬编码的同名参数**重复**。
|
|
|
+
|
|
|
+**脏值的来源是仓库里一个已提交的迁移脚本,不是有人手工改库**:
|
|
|
+
|
|
|
+```205:221:server/Admin.NET.Web.Entry/UpdateScripts/1.0.178.sql
|
|
|
+INSERT INTO `mdp_source` (
|
|
|
+ ... `db_password_enc`,`db_extra_params`, `remark`
|
|
|
+) VALUES (
|
|
|
+ ... NULL,'ConfigId=t8_v5;TrustServerCertificate=true;Encrypt=false',
|
|
|
+ 'S5/S6/S7 KPI 数据贴源同步用;只读 SELECT;凭据见 CLAUDE.md「T8 源库连接信息」与 Database.json'
|
|
|
+)
|
|
|
+ON DUPLICATE KEY UPDATE
|
|
|
+ ... `db_extra_params`=VALUES(`db_extra_params`), ...
|
|
|
+```
|
|
|
+
|
|
|
+注意 `db_password_enc` 在这里是**显式写 NULL 的**,remark 里说凭据另见他处——但 `BuildConnectionString:122` 读的就是 `DbPasswordEnc`,没有任何其它来源。所以这个源从登记那天起就没有可用口令。
|
|
|
+
|
|
|
+**代码层缺陷(此处修正一个想当然的判断)**:`db_extra_params` **无法通过页面/接口设置**。源配置保存入口是 `MdpSourceConfigService.Save:84`(`[HttpPost("sources")]`),其入参 DTO 里没有这个字段——全仓检索 `DbExtraParams` 只有两处命中:实体定义 `Entity/DataPlatform/MdpSource.cs:54` 与消费点 `MdpSourceScopeFactory.cs:125`。
|
|
|
+
|
|
|
+因此"在保存时校验"这个方向**在本字段上无处施力**,不能作为根治手段。真正的缺陷是:这个只能由迁移脚本写入的字段,**在被拼进连接串时没有任何校验**,非法值要等到某次重算深处真正拉数时才炸,而且炸出来的用户可见信息(「源数据入站失败」)完全指不到「你那条源配置里有个非法关键字」。
|
|
|
+
|
|
|
+### C.3 改动清单
|
|
|
+
|
|
|
+**C.3.1 连接串组装处校验(代码侧,主修复)**
|
|
|
+
|
|
|
+在 `MdpSourceScopeFactory.BuildConnectionString`(`:117-139`)拼接 `extra` 之前,用 `System.Data.Common.DbConnectionStringBuilder` 解析 `db_extra_params`,逐 key 判定并**剔除**非法项:
|
|
|
+
|
|
|
+- SqlSugar 自有概念(`ConfigId` 等)—— 它们不属于连接串
|
|
|
+- 与本分支已硬编码的键重复的项(SqlServer 分支 `:130` 已写死 `Server`/`Database`/`User Id`/`Password`/`TrustServerCertificate`/`Encrypt`)
|
|
|
+
|
|
|
+剔除时**必须 `LogWarning`**,信息要能定位到源与键,形如「源 `T8_V5_SQLSERVER` 的 db_extra_params 含非法关键字 `ConfigId`,已忽略」。不得静默丢弃。
|
|
|
+
|
|
|
+本仓库已有 `DbConnectionStringBuilder` 的使用先例可参照:`MdpSourceConnectionImportService.cs:33`。
|
|
|
+
|
|
|
+> 这里选择"剔除并告警"而非"抛异常":抛异常会让一条历史脏配置直接阻断整条入站链路,而剔除后连接串是合法的、能继续工作,告警足以驱动人工收口。
|
|
|
+
|
|
|
+**C.3.2 健康检查前移暴露(代码侧)**
|
|
|
+
|
|
|
+`MdpSourceHealthCheckJob`(`Job/MdpSourceHealthCheckJob.cs:35`)在巡检时对同样的非法 key 给出**明确的 `health_msg`**,使这类问题在源管理页就能看见,而不必等某次重算深处报「源数据入站失败」。
|
|
|
+
|
|
|
+**C.3.3 清理存量配置(数据侧)**
|
|
|
+
|
|
|
+新迁移脚本把 `T8_V5_SQLSERVER` 的 `db_extra_params` 清为 `NULL`。幂等:仅当当前值包含 `ConfigId` 时才更新。
|
|
|
+
|
|
|
+**C.3.4 修正脏值的源头(否则新环境会重新中招)**
|
|
|
+
|
|
|
+脏值来自 `UpdateScripts/1.0.178.sql:205-221`,该脚本在存量环境已执行且 SHA256 已记账。执行者必须在两种做法里选一种,**并在交付说明里写明选了哪种及理由**:
|
|
|
+
|
|
|
+- **(推荐)** 保持 `1.0.178.sql` 原样不动,在其 `db_extra_params` 那行上方加注释指向本次的新迁移,说明该值已被后续脚本收口。理由:已执行脚本的内容变更会与 `sys_db_migration_log` 里记录的 SHA256 不一致,可能触发校验告警。
|
|
|
+- 直接改 `1.0.178.sql` 的 `VALUES` 为 `NULL`。仅当已确认 `AutoVersionUpdate` 不会因 SHA 不一致而报错或重跑时才可选此项——**必须先取证**,不得想当然。
|
|
|
+
|
|
|
+**C.3.5 口令(须由人工提供,不得写进代码或脚本)**
|
|
|
+
|
|
|
+`T8_V5_SQLSERVER.db_password_enc` 为空(由 `1.0.178.sql:210` 显式写入 NULL),必须由负责人通过**页面**补录。保存入口 `MdpSourceConfigService.Save:84` 的 `DbPassword` 字段是可写的(`:128`),所以页面补录这条路是通的。
|
|
|
+
|
|
|
+> **安全红线**:口令、`AccessSecret`、数据库密码一律不得写入代码、迁移脚本、任务书或提交。本条只能以"待人工操作"形式留在验收清单里。
|
|
|
+
|
|
|
+**C.3.6 另一条独立的凭据问题(不在本任务范围)**
|
|
|
+
|
|
|
+`UAT_SRC_DB`(MySQL)的健康信息是 `Access denied for user 'root'@'49.65.129.7' (using password: YES)`,这是**真的凭据错误**,与 C.1 的连接串缺陷无关,需要单独处置。记录在此避免被合并误判。
|
|
|
+
|
|
|
+### C.4 验收
|
|
|
+
|
|
|
+```sql
|
|
|
+SELECT source_code, health_status, LEFT(health_msg,150) AS msg, last_health_check
|
|
|
+FROM mdp_source WHERE source_code='T8_V5_SQLSERVER';
|
|
|
+```
|
|
|
+
|
|
|
+`health_status` 为成功态、`health_msg='OK'`。
|
|
|
+
|
|
|
+代码层:构造一个 `db_extra_params` 含 `ConfigId=x` 的源,走一次拉数,确认连接成功建立且日志中有指名该源与该键的 `LogWarning`。
|
|
|
+
|
|
|
+> 由于 `db_extra_params` 无法通过页面设置(见 C.2),本项验收只能用单元测试直接调用组装逻辑,或临时用 SQL 造一条测试源。**不要**去页面上找这个输入框,它不存在。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. 任务 D:删除已死的 `home-l1` 取数链路(内含编造数据)
|
|
|
+
|
|
|
+### D.1 问题
|
|
|
+
|
|
|
+`GetHomeL1` 端点与其前端消费方**已全部失去调用点**,但代码仍在,且内含大量编造的演示数字。留着的风险不是它现在做错了什么,而是**后人会把它当成有效实现接回去**。
|
|
|
+
|
|
|
+死代码取证(全仓检索,`Web/src` 范围内零调用点):
|
|
|
+
|
|
|
+| 对象 | 位置 | 调用点 |
|
|
|
+|---|---|---|
|
|
|
+| `GetHomeL1` | `Controllers/AidopKanbanController.cs:91-121` | 仅下列已死前端 |
|
|
|
+| `fetchHomeL1` / `HomeL1Row` | `Web/src/views/aidop/api/kanbanData.ts:4,703-710` | **0** |
|
|
|
+| `loadHomeModuleMetrics` 等 | `Web/src/views/dashboard/data/homeModulesSync.ts` | **0** |
|
|
|
+| `loadHomeModuleMetrics` 等 | `Web/src/views/aidop/kanban/data/homeModulesSync.ts` | **0** |
|
|
|
+| `home-l1` 调用 | `Web/src/views/dashboard/data/s2Kpis.ts:93` | 该文件本身 **0** 调用点 |
|
|
|
+
|
|
|
+> `Web/src/views/aidop/kanban/data/operationModelSchema.ts` 里出现的 `'homeS5.matchOnTimeActualPct'` 一类**是字符串字面量**(`dataRef` 元数据),不是 ES import,不构成依赖。
|
|
|
+
|
|
|
+### D.2 为什么必须删而不是留着
|
|
|
+
|
|
|
+`homeModulesSync.ts` 里同时存在四种会直接违反「不得编造数据」要求的写法:
|
|
|
+
|
|
|
+1. **硬编码演示数字作为默认值**:`manufacturingEfficiency: 2.05`、`woOnTimeDeliveryPct: 75` 等,配合 `?? homeS1.reviewSatisfactionPct` 的兜底,接口无数据时页面显示的是假数。
|
|
|
+2. **编造趋势序列**:`homeS1.trendSatisfaction = [71.5, 72, 72.5, 73, 73.5, 74, 74.5, <真值>]`。
|
|
|
+3. **一个指标填进多个不同指标的槽位**:`homeS1.constraintMaterialKitPct = homeS1.reviewSatisfactionPct`、`homeS6.woOnTimeDeliveryPct = homeS6.firstPassYieldPct`。
|
|
|
+4. **按 moduleCode 收敛导致同模块多指标互相覆盖**:`Object.fromEntries(list.map((x) => [x.moduleCode, x]))`,一个模块只剩最后一条。
|
|
|
+
|
|
|
+### D.3 改动清单
|
|
|
+
|
|
|
+删除:
|
|
|
+
|
|
|
+- `Web/src/views/dashboard/data/homeModulesSync.ts`
|
|
|
+- `Web/src/views/aidop/kanban/data/homeModulesSync.ts`
|
|
|
+- `Web/src/views/dashboard/data/s2Kpis.ts`
|
|
|
+- `kanbanData.ts` 中的 `fetchHomeL1` 与 `HomeL1Row`
|
|
|
+
|
|
|
+`GetHomeL1`(后端端点)**先不删,改为标注弃用**并在方法注释里写明:前端零调用点、九宫格实际走 `home-grid/{moduleCode}`、该端点的全局 `MAX(biz_date)` 写法存在跨模块互相筛除的缺陷因而不可复用。
|
|
|
+
|
|
|
+> 理由:后端端点属于对外可见接口,删除前需确认没有外部消费者(第三方集成、ChatBI、导出、运维脚本)。执行者若能取证确认无外部调用,可在同一轮删除;取证不足则只标弃用,另开一项。
|
|
|
+
|
|
|
+删除前必须先跑一次全仓检索确认调用点仍为 0(本任务书的取证时间是 2026-09-28,期间可能有人新增引用)。
|
|
|
+
|
|
|
+### D.4 验收
|
|
|
+
|
|
|
+`npm run build` 通过;全仓检索 `home-l1`、`fetchHomeL1`、`homeModulesSync` 在 `Web/src` 下零匹配(除保留的后端注释外)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5.5 任务 E:卡片按各自业务日标注「数据截至」
|
|
|
+
|
|
|
+> 文档里排在 D 之后,**执行时排在 A 之后、B 之前**,原因见 §7。
|
|
|
+
|
|
|
+### E.1 问题
|
|
|
+
|
|
|
+九宫格的值是**每个指标各自**取最新业务日的(`AidopKanbanController.Generic.cs:100-107`),但返回给前端的 `bizDate` 只有**模块级**一个(`:166`,来自 `ResolveMaxBizDateGenericAsync` 的模块内 MAX)。两者口径不一致。
|
|
|
+
|
|
|
+实测后果(数据来自 §1.3):S5 的模块级 `bizDate` 是 `2026-09-25`,而 S5_L1_004 的值实际来自 `2026-08-30`;S9 的模块级是 `2026-09-24`,而 S9_L1_004 的值来自 `2026-08-17`。**把 8 月 17 日的数值标成"数据截至 9 月 24 日",比不标日期更有害。**
|
|
|
+
|
|
|
+这直接触碰本项目的硬要求:真实数值可以显示,但不得让人误以为是当前值。
|
|
|
+
|
|
|
+### E.2 改动清单
|
|
|
+
|
|
|
+**E.2.1 后端逐卡返回业务日**
|
|
|
+
|
|
|
+`AidopKanbanController.Generic.cs`:
|
|
|
+
|
|
|
+1. 值查询 `valSql`(`:96-108`)的 SELECT 增加 `v.biz_date AS BizDate`。
|
|
|
+2. 承载 DTO(`S4ValRow`)增加可空 `BizDate`。**注意该 DTO 与 `GetS4HomeGrid`(`AidopKanbanController.S4.cs:244-249`)共用**,S4 的 valSql 不选这一列,须确认留空不会影响 S4 现有行为,或为 S4 一并补上。
|
|
|
+3. `items` 的匿名对象(`:144-159`)增加 `bizDate`,取该指标自己的值;无值时为 `null`。
|
|
|
+4. 模块级 `bizDate`(`:166`)**保留**,语义改为"本模块最新的那一天",不再被当作每张卡的日期。
|
|
|
+
|
|
|
+> `filteredBundle`(`:119-138`,带业务维度筛选时的旁路取值)也要给出对应的业务日,否则一加筛选条件日期就失真。若该旁路暂无法给出日期,须显式返回 `null` 而不是回落到模块级日期。
|
|
|
+
|
|
|
+**E.2.2 前端按卡展示**
|
|
|
+
|
|
|
+`Web/src/views/dashboard/components/HomeModuleKpiCard.vue`:每张卡在数值下方标注「数据截至 {bizDate}」。超过阈值(建议 7 天,需确认)的卡片做弱化处理(变灰/降低对比度),并在悬浮提示里说明数据陈旧。
|
|
|
+
|
|
|
+`resultStatus='NO_DATA'` 的卡片继续显示「无业务数据」,**不显示**日期标注 —— 没有值就没有"截至"可言。
|
|
|
+
|
|
|
+### E.3 验收
|
|
|
+
|
|
|
+`GET /api/AidopKanban/home-grid/S9` 返回的 `items` 中,`S9_L1_004` 的 `bizDate` 为 `2026-08-17`,`S9_L1_005` 为 `2026-09-24`,两者不同。页面上两张卡显示各自的日期,`S9_L1_004` 呈弱化态。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. 反向影响推演(基线)
|
|
|
+
|
|
|
+执行者若扩大改动范围,必须在此基础上补充。
|
|
|
+
|
|
|
+### 任务 A(补 S5_L1_004 登记)
|
|
|
+
|
|
|
+| 面 | 结论 |
|
|
|
+|---|---|
|
|
|
+| 代码调用面 | **无影响**。只增数据行,不改 `GetHomeGridGeneric` / `GetOperationL1Generic` 逻辑。`AidopKpiMasterSeed` 的新增 `Ins` 仅在新租户初始化时执行(`:1008` 的 `layoutExists` 守卫保证不改存量) |
|
|
|
+| 数据契约面 | **需同步改**。`ado_smart_ops_kpi_master` 与 `ado_smart_ops_layout_item` 各增一行。`ado_s9_kpi_value_l1_day` 不动 |
|
|
|
+| 多数据源面 | **无影响**。S5_L1_004 的计算链路(T8 TVF `Rep_总账_存货_V3` → `dwd_t8_material_inventory_turnover` → CONFIG_SQL)不变 |
|
|
|
+| 多租户 / Domain 面 | **需注意**。迁移必须按"有值却无登记"驱动,**不得**写死 `tenant_id=838257186181189`;`TenantId=0` 是模板行,不得修改或删除 |
|
|
|
+| 运行面 | **需同步改**。新增迁移脚本 + verify,且必须在 `.csproj` 登记(§8.2) |
|
|
|
+| 验证面 | **需补**。建议加守卫:`AidopKpiMasterSeed` 中 S5 的 L1 指标数与 `ado_smart_ops_kpi_master` 的模块契约一致 |
|
|
|
+
|
|
|
+### 任务 B(GROUP_CONCAT + gate log)
|
|
|
+
|
|
|
+| 面 | 结论 |
|
|
|
+|---|---|
|
|
|
+| 代码调用面 | **需同步改**。`InventoryMdpSyncService.cs:891-908`(已引爆,S5/S7 共用)与 `ShipTransNeutralProjection.cs:121-139`(未引爆,位于前者下游 `:876`)两处。改前须检索是否还有第三处同构写法。`LogWorkOrderNotFoundAsync` 由 `MaterializeInvTransStdAsync:871` 唯一调用,后者经 `TransformTransStdFromStgAsync:299` 被 S5 与 S7 两条重算链共用 —— 改这一处会同时影响两个模块 |
|
|
|
+| 数据契约面 | **需注意**。`mdp_source_gate_log.sample_keys` 的语义由"截断的全量拼接"变为"限条采样";`row_count` 仍为全量。若有下游读 `sample_keys` 并假设它是完整集合,须一并核查 |
|
|
|
+| 多数据源面 | **需核查**。`ShipTransNeutralProjection` 的 gate log 写死 `source_system='DOPDEMORQ_SQLSERVER'`;`InventoryMdpSyncService.LogWorkOrderNotFoundAsync`(`:885`,调用点 `:871`)开头即按 `DOPDEMORQ_SQLSERVER` 提前返回。T8 与 165 两条来源的可得性差异须确认 |
|
|
|
+| 多租户 / Domain 面 | **无影响**。两处语句均已带 `tenant_id=@tid` |
|
|
|
+| 运行面 | **无影响**。纯代码改动,无 DDL、无种子、无定时任务变更 |
|
|
|
+| 验证面 | **需补**。见 B.3.3 |
|
|
|
+
|
|
|
+### 任务 C(源连接串)
|
|
|
+
|
|
|
+| 面 | 结论 |
|
|
|
+|---|---|
|
|
|
+| 代码调用面 | **需同步改**。`MdpSourceScopeFactory.BuildConnectionString` 被所有 EXTERNAL 源共用(入口 `GetScopeAsync:21`,被 `MdpDbPullExecutor`、`S0DimSameDbStagingLoader`、`MdpExcelImportService`、`MdpHotWatchService` 等多处使用),加过滤会影响 MySQL / PostgreSQL / Oracle 分支,须确认过滤规则不会误杀这些库的合法参数。**过滤必须按 `MapDbType` 分支各自的硬编码键集合来定,不能用一份全局黑名单** |
|
|
|
+| 数据契约面 | **无影响**。只改 `mdp_source.db_extra_params` 的取值,不改表结构。但须注意脏值源头是已提交脚本 `1.0.178.sql`,见 C.3.4 |
|
|
|
+| 多数据源面 | **需核查**。全表扫一遍 `db_extra_params`,确认除 `T8_V5_SQLSERVER` 外没有其它源也藏着非法关键字 |
|
|
|
+| 多租户 / Domain 面 | **需注意**。`T8_V5_SQLSERVER` 的 `tenant_id=0`(全局源),`UAT_SRC_DB` 的 `tenant_id=838257186181189`(租户源)。清理脚本按 `source_code` 定位,不要按租户 |
|
|
|
+| 运行面 | **需同步改**。迁移脚本 + 健康检查会在下次巡检时重跑 |
|
|
|
+| 验证面 | **需补**。建议加守卫:`db_extra_params` 不得含 `ConfigId` |
|
|
|
+
|
|
|
+### 任务 E(逐卡业务日)
|
|
|
+
|
|
|
+| 面 | 结论 |
|
|
|
+|---|---|
|
|
|
+| 代码调用面 | **需同步改**。`S4ValRow` 被 `GetHomeGridGeneric` 与 `GetS4HomeGrid` 共用,加字段须确认 S4 路径不受影响;`HomeModuleKpiCard.vue` 被九宫格全部 8 个非 S8 模块复用,改一处即改全部 |
|
|
|
+| 数据契约面 | **需同步改**。`home-grid/{moduleCode}` 的 `items` 元素新增 `bizDate` 字段(增量、向后兼容)。模块级 `bizDate` 语义收窄,须在代码注释中写明 |
|
|
|
+| 多数据源面 | **无影响**。不触碰取数来源 |
|
|
|
+| 多租户 / Domain 面 | **无影响**。沿用既有 `tenant_id` / `factory_id` 谓词 |
|
|
|
+| 运行面 | **无影响**。无 DDL、无种子、无定时任务变更 |
|
|
|
+| 验证面 | **需补**。建议加守卫:`items` 的 `bizDate` 不得回落到模块级 `bizDate`(防止后人"图省事"把两者写成同一个值) |
|
|
|
+
|
|
|
+### 任务 D(删死代码)
|
|
|
+
|
|
|
+| 面 | 结论 |
|
|
|
+|---|---|
|
|
|
+| 代码调用面 | **需先取证**。删除前重跑全仓检索;后端 `GetHomeL1` 的外部消费者取证不足时只标弃用 |
|
|
|
+| 数据契约面 | **无影响** |
|
|
|
+| 多数据源面 | **无影响** |
|
|
|
+| 多租户 / Domain 面 | **无影响** |
|
|
|
+| 运行面 | **无影响** |
|
|
|
+| 验证面 | **无需新增**,`npm run build` 与既有测试即可 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. 执行顺序与范围边界
|
|
|
+
|
|
|
+建议顺序:**A → E → B → C → D**。A 立刻产生可见效果且风险最低;E 紧随其后,因为 A 会让一个 8 月 30 日的值出现在九宫格上,没有日期标注就构成误导;B 让 S5/S7 的重算能跑完;C 需要人工补口令,会卡住;D 是清理,放最后避免与前四项的检索互相干扰。
|
|
|
+
|
|
|
+> A 与 E 存在**顺序依赖**:单独做 A 会短暂引入一张无日期标注的陈旧卡片。若不能连续完成,应先做 E 再做 A。
|
|
|
+
|
|
|
+**本轮明确不做的事**(越界前须另行确认):
|
|
|
+
|
|
|
+- 不改 `GetHomeGridGeneric` / `GetOperationL1Generic` 的取数逻辑 —— 它们是对的。
|
|
|
+- 不改 `ado_s9_kpi_value_l1_day` 的任何已有数据。
|
|
|
+- 不改 KPI 的业务公式、口径、维度配置。
|
|
|
+- 不动 `ResolveMaxBizDateGenericAsync`(`AidopKanbanController.Generic.cs:277-293`)里可疑的工厂过滤 `factory_id=0 OR factory_id=1 OR factory_id=@f` 与 `factory_id<>@t`。**这是一个已知的遗留疑点**(工厂号与租户号在比较),与本任务书的 `factoryId=1` vs 雪花 `factory_id` 是同一件事,应另开任务处置。
|
|
|
+- 不注册 `UpdateScripts/` 下 11 个历史未登记脚本(`1.0.208`、`1.0.212`、`1.0.257`–`1.0.264`、`1.0.463`)。它们从未执行过,注册即等于对生产库执行 11 个未经审阅的迁移,其中含一个 `BEFORE UPDATE` 触发器。基线与守卫见 `Admin.NET.Plugin.AiDOP.Tests/DataPlatform/MigrationScriptRegistrationTests.cs`。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 8. 版本号、迁移登记与提交
|
|
|
+
|
|
|
+### 8.1 版本号
|
|
|
+
|
|
|
+按本次提交**实际纳入的文件**决定,不按会话历史:
|
|
|
+
|
|
|
+- 含 `server/` 下代码或配置 → `server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj` 的 `<Version>` / `<AssemblyVersion>` / `<FileVersion>` **三处同号** patch +1(当前基线 `1.0.590`)。
|
|
|
+- 含 `Web/` 下代码或配置 → `Web/package.json` 的 `version` patch +1(当前基线 `2.4.412`)。
|
|
|
+- 前后端都改 → 两边都递增。
|
|
|
+- 纯文档 → 都不递增。
|
|
|
+
|
|
|
+任务 A/B/C 是后端,任务 D/E 是前后端都碰。
|
|
|
+
|
|
|
+> **易踩的坑**:用文本替换升版本号时,`.csproj` 里的 `<None Update="UpdateScripts\1.0.XXX.sql">` 会被一起替换掉,导致脚本登记指向不存在的文件。升号后务必回读该文件确认这两类出现位置。
|
|
|
+
|
|
|
+### 8.2 迁移脚本必须登记(否则永远不执行)
|
|
|
+
|
|
|
+`AutoVersionUpdate.LoadMigrationScripts()` 扫描的是**输出目录** `AppContext.BaseDirectory/UpdateScripts`,因此每个新脚本都必须在 `server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj` 里单独登记:
|
|
|
+
|
|
|
+```xml
|
|
|
+<None Update="UpdateScripts\1.0.XXX.sql">
|
|
|
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
|
+</None>
|
|
|
+<None Update="UpdateScripts\1.0.XXX.verify.sql">
|
|
|
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
|
+</None>
|
|
|
+```
|
|
|
+
|
|
|
+**漏了这一步的表现极具迷惑性**:表和列可能仍然存在(SqlSugar CodeFirst 会按实体建),看起来像迁移成功了,但脚本里的数据变更一条都没执行。判别方法是查 `sys_db_migration_log` 里有没有该版本号。`MigrationScriptRegistrationTests` 会卡住这个疏漏。
|
|
|
+
|
|
|
+### 8.3 构建与测试命令(实测可用,Windows / PowerShell)
|
|
|
+
|
|
|
+```powershell
|
|
|
+# 后端构建(改 server/ 后必跑)
|
|
|
+dotnet build server\Admin.NET.Web.Entry\Admin.NET.Web.Entry.csproj -v q --nologo
|
|
|
+
|
|
|
+# 测试工程构建 + 全量测试(用 trx 取失败用例名;控制台中文输出会乱码,不要靠肉眼读)
|
|
|
+dotnet build server\Plugins\Admin.NET.Plugin.AiDOP.Tests\Admin.NET.Plugin.AiDOP.Tests.csproj -v q --nologo
|
|
|
+dotnet test server\Plugins\Admin.NET.Plugin.AiDOP.Tests\Admin.NET.Plugin.AiDOP.Tests.csproj `
|
|
|
+ --no-build --nologo --logger "trx;LogFileName=r.trx"
|
|
|
+$x=[xml](Get-Content server\Plugins\Admin.NET.Plugin.AiDOP.Tests\TestResults\r.trx)
|
|
|
+"TOTAL=$($x.TestRun.ResultSummary.Counters.total) FAILED=$($x.TestRun.ResultSummary.Counters.failed)"
|
|
|
+$x.TestRun.Results.UnitTestResult | Where-Object { $_.outcome -eq 'Failed' } | ForEach-Object { $_.testName }
|
|
|
+
|
|
|
+# 前端构建(改 Web/ 后必跑)
|
|
|
+cd Web; npm run build
|
|
|
+```
|
|
|
+
|
|
|
+已知坑:
|
|
|
+
|
|
|
+- 构建可能因输出 DLL 被正在运行的 `Admin.NET.Web.Entry` 进程占用而报 MSB3027/MSB3021。这**不是编译错误**,先 `Stop-Process` 该进程再重试。
|
|
|
+- 测试完成后删除 `TestResults` 目录,不要提交。
|
|
|
+- 源码文本类守卫测试的锚点要落在**实现**上(如 `public async Task<int> Xxx`),不能只用签名文本——接口声明与实现签名相同,会误匹配到空方法体。本仓库已因此误判过一次。
|
|
|
+
|
|
|
+### 8.4 提交
|
|
|
+
|
|
|
+本仓库「提交」= **commit + push** 到跟踪远端。收尾必须汇报远端可达的 commit 哈希。
|
|
|
+
|
|
|
+不得纳入提交:口令与任何凭据、构建产物(`bin`/`obj`)、临时日志(`_tmp_*.log`、`frontend-dev.log`)、Word 锁文件(`~$*.docx`)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 9. 总验收清单
|
|
|
+
|
|
|
+执行完成后逐条打勾,每条都要附**可重跑的证据**(SQL 及其返回值,或 `文件:行`):
|
|
|
+
|
|
|
+- [ ] **A-1** `AidopKpiMasterSeed.cs` 的 S5 段含 `S5_L1_004`
|
|
|
+- [ ] **A-2** §2.4 的"值有而布局无"查询返回 0 行
|
|
|
+- [ ] **A-3** 迁移脚本与 verify 已创建,且已在 `.csproj` 登记
|
|
|
+- [ ] **A-4** `sys_db_migration_log` 中该版本号 `status='Success'`
|
|
|
+- [ ] **A-5** 九宫格 S5 卡片为 4 项,含「品类物料库存周转 31.6653 天」
|
|
|
+- [ ] **B-1** 两处 `GROUP_CONCAT` 已改为进聚合前限条,注释说明 `LEFT()` 不是防截断手段
|
|
|
+- [ ] **B-2** 两处 gate log `INSERT` 已在 try/catch 内,失败只记 warning
|
|
|
+- [ ] **B-3** 新增守卫测试通过
|
|
|
+- [ ] **B-4** S5/S7 手工重算不再以「KPI 计算失败」结束
|
|
|
+- [ ] **B-5** 日志中不再出现 `Row 103 was cut by GROUP_CONCAT()`
|
|
|
+- [ ] **C-1** `BuildConnectionString` 剔除非法 key + `LogWarning`(指名源与键),且不误杀 MySQL/PG/Oracle 合法参数
|
|
|
+- [ ] **C-2** `MdpSourceHealthCheckJob` 对非法 key 给出可定位的 `health_msg`
|
|
|
+- [ ] **C-3** `T8_V5_SQLSERVER.db_extra_params` 已清理,且 `1.0.178.sql` 的源头问题已按 C.3.4 选定方案处置(交付说明写明选了哪种及理由)
|
|
|
+- [ ] **C-4** (**人工**)`T8_V5_SQLSERVER` 口令已通过页面补录 —— 不得写进任何文件
|
|
|
+- [ ] **C-5** `mdp_source` 全表复查:无其它源藏有非法关键字
|
|
|
+- [ ] **C-6** `T8_V5_SQLSERVER` 健康检查 `OK`
|
|
|
+- [ ] **E-1** `items` 每个元素带各自的 `bizDate`,与模块级 `bizDate` 不相等的情形能被复现(S9_L1_004=2026-08-17 vs 模块级 2026-09-24)
|
|
|
+- [ ] **E-2** 卡片显示「数据截至 X」,陈旧卡片弱化;`NO_DATA` 卡片不显示日期
|
|
|
+- [ ] **E-3** 带业务维度筛选时日期不失真(或显式为 null)
|
|
|
+- [ ] **D-1** 四个死文件/导出已删除,`npm run build` 通过
|
|
|
+- [ ] **D-2** `GetHomeL1` 已标弃用(或取证后删除)
|
|
|
+- [ ] **D-3** `Web/src` 下 `home-l1` / `fetchHomeL1` / `homeModulesSync` 零匹配
|
|
|
+- [ ] **全局-1** 后端与测试工程 0 编译错误
|
|
|
+- [ ] **全局-2** 全量测试失败数 **等于**改动前基线 **7**,且失败用例名逐一对齐。总数会因本任务新增守卫而大于基线 3213,这是预期的;**只比对失败集合,不比对总数**。基线 7 个失败为:
|
|
|
+ - `S0DimContractTests.ProductDesign_bom_and_routing_sql_must_carry_tenant_predicate_everywhere`
|
|
|
+ - `S1RequirementExamineDwdContractTests` × 5(`Table_HasNoS8SpecificColumns`、`CurrentSnapshotIndex_ExistsInBothInlineDdlAndMigration`、`BooleanColumn_FollowsRepoFlagConvention_NotBit`、`NewColumns_ExistInBothInlineDdlAndMigration`、`QuantityColumns_DocumentedAsStandardNames`)
|
|
|
+ - `S6ProductionInstructionContractTests.Std_Schedule_HasNewNullableColumns`
|
|
|
+- [ ] **全局-3** 版本号按 §8.1 递增,且 `.csproj` 的脚本登记未被文本替换误伤
|
|
|
+- [ ] **全局-4** 已 push,并汇报远端可达的 commit 哈希
|
|
|
+- [ ] **全局-5** 交付说明中包含「反向影响推演」小节,结论只用「无影响 / 需同步改(列文件)/ 需你决策(列选项)」三种表述
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 10. 附:本任务书未覆盖但已知的待决事项
|
|
|
+
|
|
|
+以下问题在调查中浮现,**不属于本任务范围**,各自需要单独决策,记录于此以免遗失:
|
|
|
+
|
|
|
+1. `ResolveMaxBizDateGenericAsync` 的工厂过滤疑点(§7)。
|
|
|
+2. `UAT_SRC_DB` 的 `Access denied` 凭据问题(§4.C.3.5)。
|
|
|
+3. `UpdateScripts/` 下 11 个从未执行的历史脚本(§7)。
|
|
|
+4. `dwd_supplier_delivery` 缺 `shipment_no` 列:2026-09-28 18:14:50 日志中有 `Unknown column 'shipment_no' in 'field list'`,当次未致命,属契约与实表不一致。
|
|
|
+5. S6 三个 L1 指标全部 `NO_DATA`,且 `S6_L1_003` 的最新业务日停在 2026-08-30。任务 B/C 完成后若仍为 `NO_DATA`,需单独排查 S6 的源数据可得性。
|
|
|
+6. S8 的两个 L1 指标最新业务日停在 2026-08-31,需单独排查。
|