浏览代码

refactor(s6): restore L1=order/L2=work-order hierarchy, relocate work-order KPIs to config SQL

S6 口径核定(方案 B)Phase 1——恢复原始分层、工单归位、清理别名(订单级 L1 待 Phase 2 业务契约确认后实现):
- S6MdpSyncTransformService:工单制造满足率/人效从 S6_L1_001/002 归位到既有 L2 码 S6_L2_002/003,写 ado_s9_kpi_value_l2_day;注入 KpiCalcDispatcher,经 CONFIG_SQL/LEGACY_CODE 计算;数据准备(dwd)不变;不再写 S6_L1_001/002
- SmartOpsKpiAtomicBuildService:删除把 S2 排程别名写成 S6_L1_001/002/003 的三处,S6 L1 不再被 S2 排程污染(修复带筛选看板 Atomic 覆盖 L1 正式值)
- 1.0.280.sql:S6_L2_002/003 迁 CONFIG_SQL(v1 LEGACY+v2 CONFIG,读 dwd_t8_work_order_mfg_*);修正租户0 S6_L1_001/002 元数据 工单→订单;清 atomic_day 误写 S6 别名;隔离 l1_day S6_L1_001/002 历史误落位工单值(is_deleted=1)
- 冒烟:工单归位 L2(run-log EngineType,67.5)、CONFIG_SQL==legacy(preview 67.5 分子4/NO_HEADCOUNT保留)、双向无部署切换、L1订单看板 NO_DATA+元数据订单口径、atomic 覆盖消失、S5/S7 回归全 CONFIG_SQL
- Phase 2 只读调查:订单周期/满足率数据可行(限订单=生产任务口径),7 项业务口径歧义列硬门待确认,本批不实现

chore: bump version server 1.0.280
YY968XX 2 周之前
父节点
当前提交
091590bc8f

+ 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.279</AssemblyVersion>
-    <FileVersion>1.0.279</FileVersion>
-    <Version>1.0.279</Version>
+    <AssemblyVersion>1.0.280</AssemblyVersion>
+    <FileVersion>1.0.280</FileVersion>
+    <Version>1.0.280</Version>
   </PropertyGroup>
 
   <ItemGroup>
@@ -253,6 +253,9 @@
     <None Update="UpdateScripts\1.0.279.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
+    <None Update="UpdateScripts\1.0.280.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
   </ItemGroup>
 
   <ItemGroup>

+ 66 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.280.sql

@@ -0,0 +1,66 @@
+-- AIDOP-S6-L1-KPI-CONTRACT-RESOLUTION-1 Phase 1(方案 B:恢复 L1=订单 / L2=工单 分层)。
+-- 提交时 rename 为正式 1.0.<next>.sql 并注册 csproj Copy(不预占版本号)。
+-- 表列:ado_smart_ops_kpi_calc_config 为 PascalCase(EnableUnderLine=false);kpi_master 亦 PascalCase;
+--       ado_s9_kpi_value_l1_day / ado_smart_ops_kpi_atomic_day 为 snake(显式 SQL 建)。
+-- 幂等:配置 INSERT ... WHERE NOT EXISTS;数据修正为条件 UPDATE/DELETE(可重复执行)。
+-- 租户:工单 L2 KPI 计算落 AIDOP 运行租户 797403760988229。
+
+-- ═════════ 1) 工单 L2 KPI 迁 CONFIG_SQL(S6_L2_002 满足率 / S6_L2_003 人效)═════════
+-- 读已准备好的 dwd(数据准备由 S6MdpSyncTransformService 保留),与 legacy 逐项等价。
+
+-- S6_L2_002 工单制造满足率(日;dwd 存 0~1 逐单 rate → ×100)
+INSERT INTO ado_smart_ops_kpi_calc_config
+  (`TenantId`,`MetricCode`,`ModuleCode`,`VersionNo`,`CalcEngineType`,`DataSourceCode`,`SqlScript`,`TimeoutSeconds`,`PublishStatus`,`IsCurrent`,`Remark`,`CreatedBy`,`CreatedAt`)
+SELECT 797403760988229,'S6_L2_002','S6',1,'LEGACY_CODE',NULL,NULL,30,'PUBLISHED',0,'legacy 回滚基线:均值-of-逐单(报工/计划)×100','system',NOW()
+FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM ado_smart_ops_kpi_calc_config WHERE `MetricCode`='S6_L2_002' AND `TenantId`=797403760988229 AND `VersionNo`=1);
+
+INSERT INTO ado_smart_ops_kpi_calc_config
+  (`TenantId`,`MetricCode`,`ModuleCode`,`VersionNo`,`CalcEngineType`,`DataSourceCode`,`SqlScript`,`TimeoutSeconds`,`PublishStatus`,`IsCurrent`,`Remark`,`CreatedBy`,`CreatedAt`)
+SELECT 797403760988229,'S6_L2_002','S6',2,'CONFIG_SQL','MDP_LOCAL',
+'SELECT ROUND(AVG(fulfillment_rate) * 100, 4) AS metric_value, COUNT(*) AS numerator_value
+FROM dwd_t8_work_order_mfg_fulfillment
+WHERE tenant_id = @tenant_id AND factory_id = @factory_id AND biz_date = @biz_date AND source_ztid = @ztid
+  AND fulfillment_rate IS NOT NULL',
+30,'PUBLISHED',1,'CONFIG_SQL:读 dwd 逐单 fulfillment_rate(0~1) 均值×100,1:1 复刻 legacy','system',NOW()
+FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM ado_smart_ops_kpi_calc_config WHERE `MetricCode`='S6_L2_002' AND `TenantId`=797403760988229 AND `VersionNo`=2);
+
+-- S6_L2_003 工单制造人效(月;完成工单数/生产人数,headcount 空/0→NO_DATA 保留 NO_HEADCOUNT)
+INSERT INTO ado_smart_ops_kpi_calc_config
+  (`TenantId`,`MetricCode`,`ModuleCode`,`VersionNo`,`CalcEngineType`,`DataSourceCode`,`SqlScript`,`TimeoutSeconds`,`PublishStatus`,`IsCurrent`,`Remark`,`CreatedBy`,`CreatedAt`)
+SELECT 797403760988229,'S6_L2_003','S6',1,'LEGACY_CODE',NULL,NULL,30,'PUBLISHED',0,'legacy 回滚基线:完成工单数/生产人数','system',NOW()
+FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM ado_smart_ops_kpi_calc_config WHERE `MetricCode`='S6_L2_003' AND `TenantId`=797403760988229 AND `VersionNo`=1);
+
+INSERT INTO ado_smart_ops_kpi_calc_config
+  (`TenantId`,`MetricCode`,`ModuleCode`,`VersionNo`,`CalcEngineType`,`DataSourceCode`,`SqlScript`,`TimeoutSeconds`,`PublishStatus`,`IsCurrent`,`Remark`,`CreatedBy`,`CreatedAt`)
+SELECT 797403760988229,'S6_L2_003','S6',2,'CONFIG_SQL','MDP_LOCAL',
+'SELECT ROUND(done_count / NULLIF(production_headcount, 0), 4) AS metric_value,
+       done_count AS numerator_value, production_headcount AS denominator_value,
+       denominator_status AS result_message
+FROM dwd_t8_work_order_mfg_efficiency
+WHERE tenant_id = @tenant_id AND factory_id = @factory_id
+  AND biz_month = DATE_FORMAT(@biz_date, ''%Y-%m'') AND source_ztid = @ztid',
+30,'PUBLISHED',1,'CONFIG_SQL:读 dwd 月行 done_count/生产人数,1:1 复刻 legacy','system',NOW()
+FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM ado_smart_ops_kpi_calc_config WHERE `MetricCode`='S6_L2_003' AND `TenantId`=797403760988229 AND `VersionNo`=2);
+
+-- ═════════ 2) 修正租户 0 的 S6_L1_001/002 元数据:工单(1.0.178所写)→订单 ═════════
+-- 从既有订单口径租户 1300000000001 同码行拷贝,保证全租户 L1 统一订单口径。
+UPDATE ado_smart_ops_kpi_master t0
+JOIN ado_smart_ops_kpi_master src
+  ON src.`MetricCode` = t0.`MetricCode` AND src.`TenantId` = 1300000000001
+SET t0.`MetricName` = src.`MetricName`, t0.`Unit` = src.`Unit`, t0.`Direction` = src.`Direction`,
+    t0.`Formula` = src.`Formula`, t0.`CalcRule` = src.`CalcRule`, t0.`Description` = src.`Description`,
+    t0.`UpdatedAt` = NOW()
+WHERE t0.`TenantId` = 0 AND t0.`MetricCode` IN ('S6_L1_001','S6_L1_002')
+  AND t0.`MetricName` NOT LIKE '订单%';
+
+-- ═════════ 3) 清理 atomic_day 中被误写的 S6 排程别名(配合代码删别名,#5/#6)═════════
+-- 含 S6_L1_003(订单制造人效同属被 S2 排程别名污染,同一 bug)。
+DELETE FROM ado_smart_ops_kpi_atomic_day
+WHERE metric_code IN ('S6_L1_001','S6_L1_002','S6_L1_003');
+
+-- ═════════ 4) 隔离 l1_day 的 S6_L1_001/002 历史工单值(误落位,#7/#8)═════════
+-- is_deleted=1 隔离(保留行可追溯),看板读 is_deleted=0 → L1 呈现 NO_DATA;
+-- 工单历史不搬(按决策 L2 从下次刷新起累积)。
+UPDATE ado_s9_kpi_value_l1_day
+SET is_deleted = 1, update_time = NOW()
+WHERE module_code = 'S6' AND metric_code IN ('S6_L1_001','S6_L1_002') AND is_deleted = 0;

+ 50 - 26
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/S6MdpSyncTransformService.cs

@@ -9,13 +9,18 @@ namespace Admin.NET.Plugin.AiDOP.Manufacturing;
 /// S6 生产执行 — KPI 计算与刷新转换服务。
 /// 双模式:读本地标准层 mdp_std_t8_kc_*(由 T8BaseInboundMdpSyncService 从 T8 贴源→标准),不再直连 T8。
 /// 计算逻辑沿用方老师 v5.4 KPI J 列口径(等价改写为 MySQL 读 std)。
-/// 包含 KPI:S6_L1_001 工单制造满足率 / S6_L1_002 工单制造人效。
+/// 口径归位(S6-L1-KPI-CONTRACT-RESOLUTION-1,方案 B):L1=订单级(待 Phase 2 实现),
+///   L2=工单级——本服务产出 **工单** 指标写 L2:
+///   工单制造满足率→S6_L2_002(l2_day),工单制造人效→S6_L2_003(l2_day);不再写 S6_L1_001/002。
+/// 最终聚合经 KpiCalcDispatcher(LEGACY_CODE/CONFIG_SQL),数据准备(dwd)不变。
 /// </summary>
 public class S6MdpSyncTransformService : ITransient
 {
     private readonly ISqlSugarClient _db;
     private readonly SysNoticeService _sysNoticeService;
     private readonly ILogger<S6MdpSyncTransformService> _logger;
+    private readonly SmartOps.KpiCalcDispatcher _kpiCalcDispatcher;
+    private const string L2ValueTable = "ado_s9_kpi_value_l2_day";
     private const string JobCode = "S6_MDP_SYNC_TRANSFORM";
     private const string JobName = "S6 生产执行 MDP 同步与转换";
     private const string ModuleCode = "S6";
@@ -26,11 +31,13 @@ public class S6MdpSyncTransformService : ITransient
     public S6MdpSyncTransformService(
         ISqlSugarClient db,
         SysNoticeService sysNoticeService,
-        ILogger<S6MdpSyncTransformService> logger)
+        ILogger<S6MdpSyncTransformService> logger,
+        SmartOps.KpiCalcDispatcher kpiCalcDispatcher)
     {
         _db = db;
         _sysNoticeService = sysNoticeService;
         _logger = logger;
+        _kpiCalcDispatcher = kpiCalcDispatcher;
     }
 
     public async Task<S6MdpSyncTransformResult> RunFullAsync(
@@ -66,11 +73,12 @@ public class S6MdpSyncTransformService : ITransient
             result.StageRows = 0;
             result.StandardRows = 0;
 
-            var sub22 = await BuildS6L1001WorkOrderMfgFulfillmentAsync(batchId, now, option, cancellationToken);
-            result.MergeSub("S6_L1_001", sub22);
+            // 口径归位:工单指标写 L2(S6_L2_002 满足率 / S6_L2_003 人效);L1 订单级待 Phase 2。
+            var sub22 = await BuildS6L2002WorkOrderMfgFulfillmentAsync(batchId, now, option, normalizedTrigger, cancellationToken);
+            result.MergeSub("S6_L2_002", sub22);
 
-            var sub23 = await BuildS6L1002WorkOrderMfgEfficiencyAsync(batchId, now, option, cancellationToken);
-            result.MergeSub("S6_L1_002", sub23);
+            var sub23 = await BuildS6L2003WorkOrderMfgEfficiencyAsync(batchId, now, option, normalizedTrigger, cancellationToken);
+            result.MergeSub("S6_L2_003", sub23);
 
             await MarkTransformRunSuccessAsync(runLogId, now, result);
             return result;
@@ -84,9 +92,9 @@ public class S6MdpSyncTransformService : ITransient
 
     // ─────────────────────────────────────────────────────────────────────────
 
-    /// <summary>S6_L1_001 工单制造满足率 = 计划完工时间内累计报工 / 工单计划生产数量。</summary>
-    private async Task<KpiBuildSubResult> BuildS6L1001WorkOrderMfgFulfillmentAsync(
-        string batchId, DateTime now, S6MdpRefreshOption option, CancellationToken ct)
+    /// <summary>工单制造满足率 = 计划完工时间内累计报工 / 工单计划生产数量。归位 L2:写 S6_L2_002。</summary>
+    private async Task<KpiBuildSubResult> BuildS6L2002WorkOrderMfgFulfillmentAsync(
+        string batchId, DateTime now, S6MdpRefreshOption option, string triggerType, CancellationToken ct)
     {
         var sub = new KpiBuildSubResult();
 
@@ -145,17 +153,25 @@ ON DUPLICATE KEY UPDATE
         }
         sub.DwdRows = dwdAffected;
 
-        decimal? metricValue = rateList.Count > 0
+        // 数据准备(dwd)已完成。最终聚合交分发器(日 KPI:period 复用 BizDate,SQL 按 biz_date 圈选)。
+        decimal? legacyValue = rateList.Count > 0
             ? Math.Round(rateList.Average() * 100m, 4) // 百分号
             : null;
-        sub.KpiRows = await UpsertKpiValueAsync("S6_L1_001", option.BizDate, metricValue, now, option);
-        sub.DenominatorStatus = rateList.Count > 0 ? "OK" : "NO_VALID_WORK_ORDER";
+        var legacyDenom = rateList.Count > 0 ? "OK" : "NO_VALID_WORK_ORDER";
+        var dispatch = await _kpiCalcDispatcher.DispatchAsync(
+            "S6_L2_002", ModuleCode, option.TargetTenantId, option.TargetFactoryId,
+            option.BizDate, option.BizDate, option.BizDate, option.SourceZtid,
+            batchId, triggerType, legacyValue, legacyDenom, ct);
+        sub.KpiRows = dispatch.ShouldUpsert
+            ? await UpsertKpiValueAsync("S6_L2_002", option.BizDate, dispatch.MetricValue, now, option, L2ValueTable)
+            : 0;
+        sub.DenominatorStatus = dispatch.DenominatorStatus;
         return sub;
     }
 
-    /// <summary>S6_L1_002 工单制造人效 = 完成制造工单数(lbs=生产入库 AND (slzx>=sl OR gdyn=1)) / count(gw=生产)。</summary>
-    private async Task<KpiBuildSubResult> BuildS6L1002WorkOrderMfgEfficiencyAsync(
-        string batchId, DateTime now, S6MdpRefreshOption option, CancellationToken ct)
+    /// <summary>工单制造人效 = 完成制造工单数(lbs=生产入库 AND (slzx>=sl OR gdyn=1)) / count(gw=生产)。归位 L2:写 S6_L2_003。</summary>
+    private async Task<KpiBuildSubResult> BuildS6L2003WorkOrderMfgEfficiencyAsync(
+        string batchId, DateTime now, S6MdpRefreshOption option, string triggerType, CancellationToken ct)
     {
         var sub = new KpiBuildSubResult();
 
@@ -225,22 +241,30 @@ ON DUPLICATE KEY UPDATE
             new SugarParameter("@now", now));
         sub.DwdRows = dwdAffected;
 
-        sub.KpiRows = await UpsertKpiValueAsync("S6_L1_002", option.MonthlyPeriodEnd, efficiency, now, option);
+        // 月度 KPI 最终聚合交分发器;bizDate=月末、period=当月窗口;legacyValue=efficiency、legacyDenom 保留 NO_HEADCOUNT/NO_NUMERATOR。
+        var dispatch = await _kpiCalcDispatcher.DispatchAsync(
+            "S6_L2_003", ModuleCode, option.TargetTenantId, option.TargetFactoryId,
+            option.MonthlyPeriodEnd, option.MonthlyPeriodStart, option.MonthlyPeriodEnd, option.SourceZtid,
+            batchId, triggerType, efficiency, denomStatus, ct);
+        sub.KpiRows = dispatch.ShouldUpsert
+            ? await UpsertKpiValueAsync("S6_L2_003", option.MonthlyPeriodEnd, dispatch.MetricValue, now, option, L2ValueTable)
+            : 0;
+        sub.DenominatorStatus = dispatch.DenominatorStatus;
         return sub;
     }
 
     // ─────────────────────────────────────────────────────────────────────────
 
-    private async Task<int> UpsertKpiValueAsync(string metricCode, DateTime bizDate, decimal? metricValue, DateTime now, S6MdpRefreshOption option)
+    private async Task<int> UpsertKpiValueAsync(string metricCode, DateTime bizDate, decimal? metricValue, DateTime now, S6MdpRefreshOption option, string valueTable = "ado_s9_kpi_value_l1_day")
     {
         // 沿用 S3 UpsertS3KpiValueAsync 范式:先查现存行 → UPDATE;不存在 → SELECT MAX(id)+1 显式生成 id 后 INSERT。
-        // ado_s9_kpi_value_l1_day.id 为手工分配主键(无 AUTO_INCREMENT),必须显式 set;
-        // metric_value 允许 NULL(分母缺失不得伪装真实 0)
-        // FIX-2:截断时分秒(月度 KPI 入参可能为 YYYY-MM-DD 23:59:59),保证 SELECT WHERE biz_date=@BizDate 与 DB date 列匹配,避免重复 INSERT
-        // FIX-1:tenant_id/factory_id 取自 option,默认仍为 1300000000001/1,不破坏 Demo
+        // 值表 id 为手工分配主键(无 AUTO_INCREMENT),必须显式 set;metric_value 允许 NULL(分母缺失不得伪装真实 0)。
+        // valueTable:L1 KPI 写 ado_s9_kpi_value_l1_day;L2(本服务工单指标 S6_L2_002/003)写 ado_s9_kpi_value_l2_day
+        //   表名为受控常量(非用户输入),可安全内插;各表 id 序列独立,MAX(id) 取目标表
+        // FIX-2:截断时分秒(月度 KPI 入参可能为 YYYY-MM-DD 23:59:59),保证 SELECT WHERE biz_date=@BizDate 与 DB date 列匹配
         bizDate = bizDate.Date;
         var existingId = await _db.Ado.GetLongAsync(
-            "SELECT IFNULL((SELECT id FROM ado_s9_kpi_value_l1_day WHERE tenant_id=@TenantId AND factory_id=@FactoryId " +
+            $"SELECT IFNULL((SELECT id FROM {valueTable} WHERE tenant_id=@TenantId AND factory_id=@FactoryId " +
             "AND module_code=@ModuleCode AND metric_code=@MetricCode AND biz_date=@BizDate AND is_deleted=0 " +
             "ORDER BY id LIMIT 1), 0)",
             new List<SugarParameter>
@@ -255,7 +279,7 @@ ON DUPLICATE KEY UPDATE
         if (existingId > 0)
         {
             return await _db.Ado.ExecuteCommandAsync(
-                "UPDATE ado_s9_kpi_value_l1_day SET metric_value=@MetricValue, calc_time=@Now, " +
+                $"UPDATE {valueTable} SET metric_value=@MetricValue, calc_time=@Now, " +
                 "update_time=@Now, is_deleted=0, is_active=1 WHERE id=@Id",
                 new SugarParameter("@MetricValue", metricValue),
                 new SugarParameter("@Now", now),
@@ -263,9 +287,9 @@ ON DUPLICATE KEY UPDATE
         }
 
         var nextId = await _db.Ado.GetLongAsync(
-            "SELECT COALESCE(MAX(id), 0) + 1 FROM ado_s9_kpi_value_l1_day");
-        return await _db.Ado.ExecuteCommandAsync(@"
-INSERT INTO ado_s9_kpi_value_l1_day
+            $"SELECT COALESCE(MAX(id), 0) + 1 FROM {valueTable}");
+        return await _db.Ado.ExecuteCommandAsync($@"
+INSERT INTO {valueTable}
   (id, tenant_id, org_id, company_id, factory_id, status, biz_date,
    create_time, update_time, is_deleted, is_active,
    module_code, metric_code, metric_value, calc_time)

+ 7 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/SmartOps/SmartOpsKpiAtomicBuildService.cs

@@ -362,7 +362,9 @@ public class SmartOpsKpiAtomicBuildService : ITransient
             """,
             new { Domain = domain, StatDate = date });
 
-        foreach (var metricCode in new[] { "S2_L1_001", "S6_L1_001" })
+        // 口径归位(S6-L1-KPI-CONTRACT-RESOLUTION-1):S2 排程周期只属 S2_L1_001,
+        //   不再别名写 S6_L1_001(订单制造周期≠排程周期,S6 L1 由 Phase 2 订单口径实现)。
+        foreach (var metricCode in new[] { "S2_L1_001" })
         {
             affected += await _db.Ado.ExecuteCommandAsync(
                 """
@@ -381,7 +383,8 @@ public class SmartOpsKpiAtomicBuildService : ITransient
                 new { Domain = domain, MetricCode = metricCode, StatDate = date, BatchId = batchId, Now = now });
         }
 
-        foreach (var metricCode in new[] { "S2_L1_002", "S6_L1_002" })
+        // 口径归位:S2 排程满足率只属 S2_L1_002,不再别名写 S6_L1_002。
+        foreach (var metricCode in new[] { "S2_L1_002" })
         {
             affected += await _db.Ado.ExecuteCommandAsync(
                 """
@@ -400,7 +403,8 @@ public class SmartOpsKpiAtomicBuildService : ITransient
                 new { Domain = domain, MetricCode = metricCode, StatDate = date, BatchId = batchId, Now = now });
         }
 
-        foreach (var metricCode in new[] { "S2_L1_003", "S6_L1_003", "S9_L1_004" })
+        // 口径归位:S2 排程人效只属 S2_L1_003,不再别名写 S6_L1_003(订单制造人效由 Phase 2 订单口径实现)。
+        foreach (var metricCode in new[] { "S2_L1_003", "S9_L1_004" })
         {
             affected += await _db.Ado.ExecuteCommandAsync(
                 """