Parcourir la source

fix(mdp): 修复销售订单关闭状态标准层更新语义

mdp_std_so 的 ON DUPLICATE KEY UPDATE 列表缺 closed,该列因此在首次
INSERT 之后永不刷新。而 order_status 在列表内——两列派生自同一个源字段
h.raw_data.$.closed(分别在 :1057 与 :1063),于是订单在源端关闭后,
同一行会分叉成 order_status='CLOSED' 而 closed=0。

后果由一个常规业务动作触发:mdp_std_so.closed 的全仓唯一读取方是
S8OrderDeliveryDataProvider 的 IFNULL(s.closed,0)=0,它靠这一列把已关闭
订单排除出 Rule02 数据集。closed 冻结意味着已关单订单继续留在结果集——
既会对已关闭订单建出新预警(假建单),也会让既有预警无法因关单而清除
(漏恢复,因为 Rule02 的恢复判据是「dedup_key 不在本轮 hits 里」)。

这是 Rule02 由直读 crm_seorder 切换到中台标准层时引入的回归:切换前
Provider 是 JOIN crm_seorder o ... AND o.closed = 0,实时读源,不受
标准层刷新语义影响。

修法是把 closed 补进 ODKU,与 order_status 同步刷新;不改 Provider 去读
order_status——closed 是 tinyint 直投列且只有 Rule02 一个读取方,
order_status 是 varchar 派生列且另有 dwd_ship_trans 下游消费者,
让 S8 判据绑到后者会扩大耦合面。

不需要 migration:实测源侧 crm_seorder 81 行 closed 全为 0,
「源已关闭但标准层仍视作未关闭」的 stale 行数 = 0,无历史数据需要回填;
修复后下一轮全量转换即自愈。
(部署当天仍应复跑一次 stale 判据,若 > 0 需人工决定是否一次性修复。)

本地 sandbox 实测(真实跑批链路,非等价 SQL 推演):
  源 closed 0→1 → 一轮同步后 std.closed=1、order_status=CLOSED,
                  Provider 可见集 40→39、命中 9→8,该单退出数据集
  源 closed 1→0 → 一轮同步后 std.closed=0,可见集回到 40、命中回到 9
  全库 776 行 closed 与 order_status 分叉数 = 0,deleted_flag 不受影响

新增契约测试按 mdp_std_so 的 ODKU 区间断言(对整份源文件做 Contains
会被别的表的 ODKU 蒙混过关),并反向验证过:撤掉修复该测试即失败。

server 1.0.521 → 1.0.522
YY968XX il y a 1 jour
Parent
commit
e31b5f1e86

+ 3 - 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.521</AssemblyVersion>
-    <FileVersion>1.0.521</FileVersion>
-    <Version>1.0.521</Version>
+    <AssemblyVersion>1.0.522</AssemblyVersion>
+    <FileVersion>1.0.522</FileVersion>
+    <Version>1.0.522</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 46 - 0
server/Plugins/Admin.NET.Plugin.AiDOP.Tests/DataPlatform/MdpStandardUpsertContractTests.cs

@@ -18,6 +18,34 @@ public class MdpStandardUpsertContractTests
         Assert.DoesNotContain("SELECT tenant_id AS TenantId, 1 AS FactoryId,\n                   'S1_L2_015'", source);
     }
 
+    /// <summary>
+    /// mdp_std_so 的 closed 必须与 order_status 一起刷新。
+    ///
+    /// <para>两列派生自同一个源字段 <c>h.raw_data.$.closed</c>(分别在 :1057 / :1063)。
+    /// 若只把 order_status 放进 ODKU,订单在源端关闭后同一行会分叉成
+    /// <c>order_status='CLOSED'</c> 而 <c>closed=0</c> —— 而 <c>mdp_std_so.closed</c> 的
+    /// 全仓唯一读取方 <c>S8OrderDeliveryDataProvider</c>(<c>IFNULL(s.closed,0)=0</c>)正是靠它
+    /// 把已关闭订单排除出 Rule02 数据集。分叉的后果是已关单订单继续命中(假建单),
+    /// 且已建异常无法因关单退出数据集(漏恢复)。</para>
+    ///
+    /// <para>断言限定在 <c>mdp_std_so</c> 这一条 INSERT 的 ODKU 区间内 —— 对整份源文件做
+    /// Contains 会被别的表的 ODKU 蒙混过关。</para>
+    /// </summary>
+    [Fact]
+    public void S1SoStandardUpsert_RefreshesClosedAlongsideOrderStatus()
+    {
+        var source = File.ReadAllText(FindFile(
+            "server", "Plugins", "Admin.NET.Plugin.AiDOP", "Order", "S1MdpSyncTransformService.cs"));
+
+        var odku = ExtractOnDuplicateKeyUpdate(source, "INSERT INTO mdp_std_so");
+
+        Assert.Contains("closed=VALUES(closed)", odku);
+        Assert.Contains("order_status=VALUES(order_status)", odku);
+
+        // 补 closed 不得挤掉既有刷新列(deleted_flag 与 closed 成对被 Provider 消费)。
+        Assert.Contains("deleted_flag=VALUES(deleted_flag)", odku);
+    }
+
     [Fact]
     public void S3DeliveryScheduleUpsert_RefreshesLifecycleDates()
     {
@@ -60,6 +88,24 @@ public class MdpStandardUpsertContractTests
             source);
     }
 
+    /// <summary>
+    /// 取出 <paramref name="insertMarker"/> 之后那一条 INSERT 的 ODKU 区间(到 raw string 结束符为止)。
+    /// 用它把断言限定在目标表上——同一份源文件里有多条 INSERT,全文 Contains 判不出列属于哪张表。
+    /// </summary>
+    private static string ExtractOnDuplicateKeyUpdate(string source, string insertMarker)
+    {
+        var insertAt = source.IndexOf(insertMarker, StringComparison.Ordinal);
+        Assert.True(insertAt >= 0, $"未找到 {insertMarker}");
+
+        var odkuAt = source.IndexOf("ON DUPLICATE KEY UPDATE", insertAt, StringComparison.Ordinal);
+        Assert.True(odkuAt > insertAt, $"{insertMarker} 之后未找到 ON DUPLICATE KEY UPDATE");
+
+        var endAt = source.IndexOf("\"\"\"", odkuAt, StringComparison.Ordinal);
+        Assert.True(endAt > odkuAt, $"{insertMarker} 的 ODKU 未正常收尾");
+
+        return source[odkuAt..endAt];
+    }
+
     private static int Count(string source, string token)
     {
         var count = 0;

+ 1 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Order/S1MdpSyncTransformService.cs

@@ -1111,7 +1111,7 @@ public class S1MdpSyncTransformService : ITransient
                 plan_delivery_date=VALUES(plan_delivery_date), promised_delivery_date=VALUES(promised_delivery_date),
                 capacity_date=VALUES(capacity_date), material_ready_date=VALUES(material_ready_date),
                 order_status=VALUES(order_status), review_status=VALUES(review_status), review_stage=VALUES(review_stage),
-                flow_state=VALUES(flow_state), progress=VALUES(progress), deleted_flag=VALUES(deleted_flag),
+                flow_state=VALUES(flow_state), progress=VALUES(progress), closed=VALUES(closed), deleted_flag=VALUES(deleted_flag),
                 sync_batch_id=VALUES(sync_batch_id), sync_time=VALUES(sync_time), update_time=CURRENT_TIMESTAMP
             """, scope, batchId, now);