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

feat(s5): migrate material launch fulfillment KPI to MDP

S5_L1_002 物料上线满足率 从 legacy 直连 t8_v5 迁为读本地标准层 mdp_std_t8_*:
- T8BaseInbound 新增 2 实体贴源(Cj_Bg_Head_Rep→cj_bg_head_rep 取min kgdate / kc_dd_list_cllist→分母行),biz_key=Id
- mdp_std_t8_kc_tz_head 补 lynoid 列(EnsureColumnAsync 幂等) + BuildTzHeadTransform 投影
- BuildS5L1002 两段 SQL 改读 mdp_std_t8_*、QueryT8Async→_db.Ado,口径逐项不变(分子/分母/JOIN/去重/shtime<=kgdate/精度)
- 1.0.277.sql 把 2 VIEW 的 mdp_entity 翻 FULL;LegacyKpiCodes 移除 S5_L1_002(状态 LEGACY_SUCCESS→SUCCESS)
- 不改 S5_L1_004(仍 legacy TVF);对账新旧逐项一致(分子0/分母21,3,3,2/KPI0.0000);无 fallback

chore: bump version server 1.0.277
YY968XX 2 недель назад
Родитель
Сommit
f39fe51cba

+ 6 - 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.276</AssemblyVersion>
-    <FileVersion>1.0.276</FileVersion>
-    <Version>1.0.276</Version>
+    <AssemblyVersion>1.0.277</AssemblyVersion>
+    <FileVersion>1.0.277</FileVersion>
+    <Version>1.0.277</Version>
   </PropertyGroup>
 
   <ItemGroup>
@@ -244,6 +244,9 @@
     <None Update="UpdateScripts\1.0.276.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
+    <None Update="UpdateScripts\1.0.277.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
   </ItemGroup>
 
   <ItemGroup>

+ 23 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.277.sql

@@ -0,0 +1,23 @@
+-- 1.0.277:S5_L1_002 物料上线满足率 中台化 —— 启用 2 个报表/明细 VIEW 的 FULL 贴源。
+-- 背景:S5_L1_002 原直连 t8_v5(Cj_Bg_Head_Rep 按 noid 取 min(kgdate) 开工时间 / kc_dd_list_cllist 分母行数)。
+--   本批把这 2 张 VIEW 从"占位登记 sync_mode=NONE、target=NULL"启用为 FULL 贴源,指向 mdp_stg_t8_*
+--   (stg/std 建表 + mdp_std_t8_kc_tz_head 补 lynoid 列,由 T8BaseInboundMdpSyncService.EnsureTablesAsync 幂等处理)。
+-- biz_key:行级 Id(实测 count(*)==count(distinct Id):Cj_Bg_Head_Rep 5/5、kc_dd_list_cllist 29/29 唯一)。
+--   注意不沿用同源 S6_REPORT 的 biz_key_expr='noid,kgdate'(实测 noid、(noid,kgdate) 均会撞行:Id=4/5 同 noid 同 kgdate)。
+-- 幂等:可重复执行;按 entity_code 过滤,不触碰同源的 S6_REPORT / S6_REPORT_API。
+
+UPDATE mdp_entity e
+JOIN mdp_source s ON s.id = e.source_id AND s.source_code = 'T8_V5_SQLSERVER'
+SET
+  e.target_table_name = CASE e.entity_code
+    WHEN 'T8_CJ_BG_HEAD_REP' THEN 'mdp_stg_t8_cj_bg_head_rep'
+    WHEN 'T8_KC_DD_LIST_CLLIST' THEN 'mdp_stg_t8_kc_dd_list_cllist'
+  END,
+  e.sync_mode = 'FULL',
+  e.incr_column = NULL,
+  e.batch_size = 1000,
+  e.biz_key_expr = 'Id',
+  e.status = 1,
+  e.remark = CONCAT(IFNULL(e.remark,''), ' | S5_L1_002-MDP: FULL 贴源启用,biz_key=Id 行级'),
+  e.update_time = NOW()
+WHERE e.entity_code IN ('T8_CJ_BG_HEAD_REP','T8_KC_DD_LIST_CLLIST');

+ 2 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/DataPlatform/AidopT8KpiManualRefreshService.cs

@@ -27,8 +27,8 @@ public sealed class AidopT8KpiManualRefreshService : ITransient
     /// 取值须大于加固后最坏运行时长(T8 查询各 ≤60s、主库命令 ≤30s,全程数分钟内),10 分钟为安全上限。</summary>
     public const int RefreshLockTtlSeconds = 600;
 
-    // legacy 直连 T8 的 KPI(本轮保留),逐指标状态标 LEGACY_SUCCESS 以示区分。
-    private static readonly HashSet<string> LegacyKpiCodes = new(StringComparer.OrdinalIgnoreCase) { "S5_L1_002", "S5_L1_004" };
+    // legacy 直连 T8 的 KPI(仅剩 S5_L1_004 TVF;S5_L1_002 已中台化),逐指标状态标 LEGACY_SUCCESS 以示区分。
+    private static readonly HashSet<string> LegacyKpiCodes = new(StringComparer.OrdinalIgnoreCase) { "S5_L1_004" };
 
     private readonly T8BaseInboundMdpSyncService _inbound;
     private readonly S5MdpSyncTransformService _s5;

+ 60 - 6
server/Plugins/Admin.NET.Plugin.AiDOP/DataPlatform/T8BaseInboundMdpSyncService.cs

@@ -11,8 +11,9 @@ namespace Admin.NET.Plugin.AiDOP.DataPlatform;
 ///       (种子把 target_table_name 指向对应 mdp_stg_t8_*、sync_mode=FULL、biz_key_expr=Id)。
 /// 只读源、只写 mdp;不写 T8;FULL 全量(源表极小、re-upsert 刷新可带回字段/作废标志更新)。
 /// 下游 S5/S6/S7 KPI 已改读 mdp_std_t8_*;故 KPI 跑批前须先跑一次本入站。
-/// 说明:S5_L1_002(Cj_Bg_Head_Rep)/ S5_L1_004(Rep_总账_存货_V3 TVF)仍走 legacy 直连 T8,
-///       其对应实体 T8_KC_DD_LIST_CLLIST / T8_CJ_BG_HEAD_REP / T8_REP_ZONGZHANG_CUNHUO_V3 本批不贴源。
+/// 说明:S5_L1_002 已中台化——其依赖的 Cj_Bg_Head_Rep(报工头,取min kgdate) 与 kc_dd_list_cllist(订单物料明细)
+///       随本服务贴源到 mdp_std_t8_cj_bg_head_rep / mdp_std_t8_kc_dd_list_cllist(biz_key=Id,实测唯一)。
+///       仅 S5_L1_004(Rep_总账_存货_V3 TVF,报表聚合结果无逐行主键)仍走 legacy 直连 T8,其实体 T8_REP_ZONGZHANG_CUNHUO_V3 不贴源。
 /// </summary>
 public sealed class T8BaseInboundMdpSyncService : ITransient
 {
@@ -28,6 +29,9 @@ public sealed class T8BaseInboundMdpSyncService : ITransient
         ("T8_KC_DD_LIST", "mdp_stg_t8_kc_dd_list", "mdp_std_t8_kc_dd_list"),
         ("T8_KC_ZJ_LIST", "mdp_stg_t8_kc_zj_list", "mdp_std_t8_kc_zj_list"),
         ("T8_SYS_PELIST", "mdp_stg_t8_sys_pelist", "mdp_std_t8_sys_pelist"),
+        // S5_L1_002 中台化新增:报工头(取 min kgdate 开工时间) 与 订单物料明细(分母行数)。
+        ("T8_CJ_BG_HEAD_REP", "mdp_stg_t8_cj_bg_head_rep", "mdp_std_t8_cj_bg_head_rep"),
+        ("T8_KC_DD_LIST_CLLIST", "mdp_stg_t8_kc_dd_list_cllist", "mdp_std_t8_kc_dd_list_cllist"),
     };
 
     public T8BaseInboundMdpSyncService(ISqlSugarClient db, MdpSourcePullDispatcher pullDispatcher)
@@ -89,6 +93,8 @@ public sealed class T8BaseInboundMdpSyncService : ITransient
             "T8_KC_DD_LIST" => BuildDdListTransform(),
             "T8_KC_ZJ_LIST" => BuildZjListTransform(),
             "T8_SYS_PELIST" => BuildPelistTransform(),
+            "T8_CJ_BG_HEAD_REP" => BuildCjBgHeadRepTransform(),
+            "T8_KC_DD_LIST_CLLIST" => BuildDdListCllistTransform(),
             _ => throw new InvalidOperationException($"未支持的 T8 入站实体:{entityCode}")
         };
 
@@ -117,15 +123,15 @@ WHERE s.process_status='PENDING' AND s.source_biz_key IS NOT NULL AND s.source_b
 
     private static string BuildTzHeadTransform() => $@"
 INSERT INTO mdp_std_t8_kc_tz_head
-  (tenant_id, source_system, src_id, ztid, lbs, hzyn, zfyn, shyn, shtime, date0,
+  (tenant_id, source_system, src_id, ztid, lbs, lynoid, hzyn, zfyn, shyn, shtime, date0,
    source_row_id, source_biz_key, sync_batch_id, sync_time)
 SELECT IFNULL(s.tenant_id,@tid), IFNULL(NULLIF(s.source_system,''),'T8'),
-  {SrcId}, {JStr("ztid")}, {JStr("lbs")}, {JInt("hzyn")}, {JInt("zfyn")}, {JInt("shyn")},
+  {SrcId}, {JStr("ztid")}, {JStr("lbs")}, {JStr("lynoid")}, {JInt("hzyn")}, {JInt("zfyn")}, {JInt("shyn")},
   {JDate("shtime")}, {JDate("date0")},
   IFNULL(NULLIF(s.source_row_id,''),s.source_biz_key), s.source_biz_key, @batch, @now
 {StgHead("mdp_stg_t8_kc_tz_head")}
 ON DUPLICATE KEY UPDATE
-  src_id=VALUES(src_id), ztid=VALUES(ztid), lbs=VALUES(lbs),
+  src_id=VALUES(src_id), ztid=VALUES(ztid), lbs=VALUES(lbs), lynoid=VALUES(lynoid),
   hzyn=VALUES(hzyn), zfyn=VALUES(zfyn), shyn=VALUES(shyn),
   shtime=VALUES(shtime), date0=VALUES(date0),
   source_row_id=VALUES(source_row_id), sync_batch_id=VALUES(sync_batch_id),
@@ -203,7 +209,35 @@ ON DUPLICATE KEY UPDATE
   source_row_id=VALUES(source_row_id), sync_batch_id=VALUES(sync_batch_id),
   sync_time=VALUES(sync_time), update_time=CURRENT_TIMESTAMP";
 
-    /// <summary>建 6 stg(通用信封契约)+ 6 std(KPI 实际引用的 typed 列)。幂等 IF NOT EXISTS。</summary>
+    // S5_L1_002 分子:报工头,按 noid 取 min(kgdate)=开工时间(下游 KPI 侧 GROUP BY noid 重算 MIN,保留逐行 kgdate)。
+    private static string BuildCjBgHeadRepTransform() => $@"
+INSERT INTO mdp_std_t8_cj_bg_head_rep
+  (tenant_id, source_system, src_id, noid, kgdate, ztid,
+   source_row_id, source_biz_key, sync_batch_id, sync_time)
+SELECT IFNULL(s.tenant_id,@tid), IFNULL(NULLIF(s.source_system,''),'T8'),
+  {SrcId}, {JStr("noid")}, {JDate("kgdate")}, {JStr("ztid")},
+  IFNULL(NULLIF(s.source_row_id,''),s.source_biz_key), s.source_biz_key, @batch, @now
+{StgHead("mdp_stg_t8_cj_bg_head_rep")}
+ON DUPLICATE KEY UPDATE
+  src_id=VALUES(src_id), noid=VALUES(noid), kgdate=VALUES(kgdate), ztid=VALUES(ztid),
+  source_row_id=VALUES(source_row_id), sync_batch_id=VALUES(sync_batch_id),
+  sync_time=VALUES(sync_time), update_time=CURRENT_TIMESTAMP";
+
+    // S5_L1_002 分母:订单物料明细,count 行数 per 工单(关联 kc_dd_head.src_id = cllist.idid)。
+    private static string BuildDdListCllistTransform() => $@"
+INSERT INTO mdp_std_t8_kc_dd_list_cllist
+  (tenant_id, source_system, src_id, idid,
+   source_row_id, source_biz_key, sync_batch_id, sync_time)
+SELECT IFNULL(s.tenant_id,@tid), IFNULL(NULLIF(s.source_system,''),'T8'),
+  {SrcId}, {JInt("idid")},
+  IFNULL(NULLIF(s.source_row_id,''),s.source_biz_key), s.source_biz_key, @batch, @now
+{StgHead("mdp_stg_t8_kc_dd_list_cllist")}
+ON DUPLICATE KEY UPDATE
+  src_id=VALUES(src_id), idid=VALUES(idid),
+  source_row_id=VALUES(source_row_id), sync_batch_id=VALUES(sync_batch_id),
+  sync_time=VALUES(sync_time), update_time=CURRENT_TIMESTAMP";
+
+    /// <summary>建 8 stg(通用信封契约)+ 8 std(KPI 实际引用的 typed 列)。幂等 IF NOT EXISTS。</summary>
     private async Task EnsureTablesAsync()
     {
         foreach (var (_, stgTable, _) in Entities)
@@ -211,6 +245,7 @@ ON DUPLICATE KEY UPDATE
 
         await _db.Ado.ExecuteCommandAsync(StdDdl("mdp_std_t8_kc_tz_head",
             "src_id bigint DEFAULT NULL, ztid varchar(100) DEFAULT NULL, lbs varchar(100) DEFAULT NULL, " +
+            "lynoid varchar(400) DEFAULT NULL, " +
             "hzyn int DEFAULT NULL, zfyn int DEFAULT NULL, shyn int DEFAULT NULL, " +
             "shtime datetime DEFAULT NULL, date0 date DEFAULT NULL"));
         await _db.Ado.ExecuteCommandAsync(StdDdl("mdp_std_t8_kc_tz_list",
@@ -231,6 +266,25 @@ ON DUPLICATE KEY UPDATE
         await _db.Ado.ExecuteCommandAsync(StdDdl("mdp_std_t8_sys_pelist",
             "src_id bigint DEFAULT NULL, ztid varchar(100) DEFAULT NULL, zzzt varchar(100) DEFAULT NULL, " +
             "gw varchar(400) DEFAULT NULL"));
+        // S5_L1_002 中台化新增 2 张 std(stg 由上方 Entities 循环建)。
+        await _db.Ado.ExecuteCommandAsync(StdDdl("mdp_std_t8_cj_bg_head_rep",
+            "src_id bigint DEFAULT NULL, noid varchar(400) DEFAULT NULL, kgdate date DEFAULT NULL, " +
+            "ztid varchar(100) DEFAULT NULL"));
+        await _db.Ado.ExecuteCommandAsync(StdDdl("mdp_std_t8_kc_dd_list_cllist",
+            "src_id bigint DEFAULT NULL, idid bigint DEFAULT NULL"));
+        // 既有 mdp_std_t8_kc_tz_head(CREATE IF NOT EXISTS 不会补列)幂等补 lynoid(S5_L1_002 分子按工单号分组用)。
+        await EnsureColumnAsync("mdp_std_t8_kc_tz_head", "lynoid", "varchar(400) DEFAULT NULL");
+    }
+
+    /// <summary>幂等补列:目标表缺该列时 ADD COLUMN;已有则跳过。用于向既有 std 表安全追加字段。</summary>
+    private async Task EnsureColumnAsync(string table, string column, string columnDef)
+    {
+        var exists = await _db.Ado.GetIntAsync(
+            "SELECT COUNT(*) FROM information_schema.COLUMNS " +
+            "WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=@t AND COLUMN_NAME=@c",
+            new SugarParameter("@t", table), new SugarParameter("@c", column));
+        if (exists == 0)
+            await _db.Ado.ExecuteCommandAsync($"ALTER TABLE {table} ADD COLUMN {column} {columnDef}");
     }
 
     private static string StgDdl(string table) => $@"

+ 24 - 22
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/S5MdpSyncTransformService.cs

@@ -6,11 +6,11 @@ using System.Text.Json;
 namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
 
 /// <summary>
-/// S5 物料仓储 — KPI 计算与刷新转换服务。边界:7 迁 2 留。
+/// S5 物料仓储 — KPI 计算与刷新转换服务。边界:8 迁 1 留。
 /// 已中台化(读本地标准层 mdp_std_t8_*,由 T8BaseInboundMdpSyncService 从 T8 贴源→标准):
-///   S5_L1_001 物料上线周期 / S5_L1_003 物料仓储人效。
+///   S5_L1_001 物料上线周期 / S5_L1_002 物料上线满足率(依赖 Cj_Bg_Head_Rep + kc_dd_list_cllist,已贴源)/ S5_L1_003 物料仓储人效。
 /// 仍 legacy 直连 T8(QueryT8Async,ConfigId=t8_v5):
-///   S5_L1_002 物料上线满足率(依赖 T8 报表 Cj_Bg_Head_Rep)/ S5_L1_004 品类物料库存周转(依赖 T8 TVF Rep_总账_存货_V3)。
+///   S5_L1_004 品类物料库存周转(依赖 T8 TVF Rep_总账_存货_V3,报表聚合结果无逐行主键,暂不中台化)。
 /// 结果统一落 dwd_t8_* 与 ado_s9_kpi_value_l1_day。计算口径沿用方老师 v5.4 KPI J 列。
 /// </summary>
 public class S5MdpSyncTransformService : ITransient
@@ -21,9 +21,9 @@ public class S5MdpSyncTransformService : ITransient
     private const string JobCode = "S5_MDP_SYNC_TRANSFORM";
     private const string JobName = "S5 物料仓储 MDP 同步与转换";
     private const string T8ConfigId = "t8_v5";
-    // 加固:legacy 直连 T8 查询的命令超时上限(秒)。防止 T8 报表视图(Cj_Bg_Head_Rep)/TVF(Rep_总账_存货_V3)
-    // 慢或挂起时查询无限期阻塞、进而长时间持有刷新锁(见 AidopT8KpiManualRefreshService)。
-    // 仅加超时护栏,不改 S5_L1_002 / S5_L1_004 的 SQL / 参数 / 计算口径。
+    // 加固:legacy 直连 T8 查询的命令超时上限(秒)。防止 T8 TVF(Rep_总账_存货_V3) 慢或挂起时
+    // 查询无限期阻塞、进而长时间持有刷新锁(见 AidopT8KpiManualRefreshService)。
+    // 仅加超时护栏,不改 S5_L1_004 的 SQL / 参数 / 计算口径。(S5_L1_002 已中台化,不再走 QueryT8Async。)
     private const int T8CommandTimeoutSeconds = 60;
     private const string ModuleCode = "S5";
     // FAILURE-NOTIFICATION-1:超级管理员 superAdmin.NET(AccountType=999)
@@ -185,31 +185,33 @@ ON DUPLICATE KEY UPDATE
     {
         var sub = new KpiBuildSubResult();
 
+        // 中台化:读本地标准层 mdp_std_t8_*(kc_tz_head.Id→src_id、Cj_Bg_Head_Rep/kc_dd_list_cllist 已贴源),
+        // 口径与原直发 T8 SQL 逐项等价(分子/分母/JOIN/去重/shtime<=kgdate/count 均不变),不再直连 t8_v5。
         const string sqlNumer = @"
 select lynoid as lynoid, count(*) as codenum
  from (
-   select a.lynoid as lynoid, b.code as code
-    from kc_tz_head a with(nolock)
-    inner join kc_tz_list b with(nolock) on a.Id=b.idid
+   select h.lynoid as lynoid, l.code as code
+    from mdp_std_t8_kc_tz_head h
+    inner join mdp_std_t8_kc_tz_list l on h.src_id=l.idid
     left join (
       select noid as noid, min(kgdate) as kgdate
-       from Cj_Bg_Head_Rep with(nolock)
+       from mdp_std_t8_cj_bg_head_rep
        where ztid=@ztid group by noid
-    ) c on a.lynoid=c.noid
-    where a.ztid=@ztid and a.lbs='生产领料' and a.hzyn=0 and a.zfyn=0 and a.shyn=1 and a.shtime<=c.kgdate
-    group by a.lynoid, b.code
+    ) c on h.lynoid=c.noid
+    where h.ztid=@ztid and h.lbs='生产领料' and h.hzyn=0 and h.zfyn=0 and h.shyn=1 and h.shtime<=c.kgdate
+    group by h.lynoid, l.code
  ) n
  group by lynoid";
         const string sqlDenom = @"
-select a.noid as noid, count(b.id) as listnum
- from kc_dd_head a with(nolock)
- left join kc_dd_list_cllist b with(nolock) on a.Id=b.idid
- where a.ztid=@ztid and a.lbs='生产任务' and a.zf=0 and a.shyn=1
- group by a.noid";
+select h.noid as noid, count(l.src_id) as listnum
+ from mdp_std_t8_kc_dd_head h
+ left join mdp_std_t8_kc_dd_list_cllist l on h.src_id=l.idid
+ where h.ztid=@ztid and h.lbs='生产任务' and h.zf=0 and h.shyn=1
+ group by h.noid";
 
         var p = new[] { new SugarParameter("@ztid", option.SourceZtid) };
-        var numerRows = await QueryT8Async<S5FulfillmentNumerRow>(sqlNumer, p);
-        var denomRows = await QueryT8Async<S5FulfillmentDenomRow>(sqlDenom, p);
+        var numerRows = await _db.Ado.SqlQueryAsync<S5FulfillmentNumerRow>(sqlNumer, p);
+        var denomRows = await _db.Ado.SqlQueryAsync<S5FulfillmentDenomRow>(sqlDenom, p);
         sub.T8Rows = numerRows.Count + denomRows.Count;
 
         var numerByOrder = numerRows.Where(r => !string.IsNullOrEmpty(r.lynoid))
@@ -426,8 +428,8 @@ ON DUPLICATE KEY UPDATE
     // 跨库 / 写入 / 日志 封装
     // ─────────────────────────────────────────────────────────────────────────
 
-    // legacy 直连 T8:仅 S5_L1_002(Cj_Bg_Head_Rep 报表)/ S5_L1_004(Rep_总账_存货_V3 TVF)仍用;
-    // 这两类源非干净基表,暂不纳入双模式贴源,待底表摸清后单独立批。已中台化的 001/003 不再走此方法。
+    // legacy 直连 T8:仅 S5_L1_004(Rep_总账_存货_V3 TVF,报表聚合结果无逐行主键)仍用;
+    // 已中台化的 S5_L1_001/002/003 均读 mdp_std_t8_*,不再走此方法。
     private async Task<List<T>> QueryT8Async<T>(string sql, SugarParameter[] parameters)
     {
         var t8 = _db.AsTenant().GetConnectionScope(T8ConfigId);