Ver código fonte

feat(s5-s7): S5/S6/S7 MDP KPI 租户去硬编码

- 新增 AidopSourceTenantMap:T8 账套 ztid→AiDOP 租户映射(pbxfxp→797403760988229,可经 appsettings 覆盖;未映射即抛错,不回退固定租户)
- S5/S6/S7 MdpSyncTransformService:18 处写死 tenant_id=1300000000001 全部改为按 ztid 映射解析的 @TenantId(DWD/值 INSERT、去重查询、l1_day KPI、run_log)
- Option 增加 TenantId,RunFullAsync 内经映射解析;factory_id 本轮保持不变
- 冒烟已验证:手动 refresh 后 run_log/DWD/kpi_value 三层新行均落 797,不再落 1300000000001
- bump server 1.0.254 -> 1.0.255
YY968XX 2 semanas atrás
pai
commit
ed6e503ef4

+ 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.254</AssemblyVersion>
-    <FileVersion>1.0.254</FileVersion>
-    <Version>1.0.254</Version>
+    <AssemblyVersion>1.0.255</AssemblyVersion>
+    <FileVersion>1.0.255</FileVersion>
+    <Version>1.0.255</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 22 - 12
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/S7MdpSyncTransformService.cs

@@ -39,11 +39,13 @@ public class S7MdpSyncTransformService : ITransient
     {
         cancellationToken.ThrowIfCancellationRequested();
         option ??= S7MdpRefreshOption.Default();
+        // 目标租户由 T8 账套(ztid)映射决定,替代此前写死的 1300000000001(禁止固定默认/兜底)
+        option.TenantId = AidopSourceTenantMap.ResolveTenantId(option.SourceZtid, option.TenantId);
 
         var now = DateTime.Now;
         var batchId = $"S7_MDP_FULL_{now:yyyyMMddHHmmss}";
         var normalizedTrigger = NormalizeTriggerType(triggerType);
-        var runLogId = await InsertTransformRunLogAsync(batchId, now, normalizedTrigger);
+        var runLogId = await InsertTransformRunLogAsync(batchId, now, normalizedTrigger, option.TenantId);
 
         var result = new S7MdpSyncTransformResult
         {
@@ -128,10 +130,11 @@ select noid as noid,
 INSERT INTO dwd_t8_order_shipment_cycle
   (tenant_id, factory_id, biz_date, source_ztid, order_no, cycle_days, batch_id, create_time)
 VALUES
-  (1300000000001, 1, @bizDate, @ztid, @orderNo, @cycleDays, @batchId, @now)
+  (@TenantId, 1, @bizDate, @ztid, @orderNo, @cycleDays, @batchId, @now)
 ON DUPLICATE KEY UPDATE
   cycle_days=VALUES(cycle_days),
   batch_id=VALUES(batch_id), update_time=@now",
+                new SugarParameter("@TenantId", option.TenantId),
                 new SugarParameter("@bizDate", option.BizDate),
                 new SugarParameter("@ztid", option.SourceZtid),
                 new SugarParameter("@orderNo", r.noid),
@@ -144,7 +147,7 @@ ON DUPLICATE KEY UPDATE
         decimal? metricValue = cycleList.Count > 0
             ? Math.Round((decimal)cycleList.Average(), 4)
             : null;
-        sub.KpiRows = await UpsertKpiValueAsync("S7_L1_001", option.BizDate, metricValue, now);
+        sub.KpiRows = await UpsertKpiValueAsync("S7_L1_001", option.TenantId, option.BizDate, metricValue, now);
         sub.DenominatorStatus = cycleList.Count > 0 ? "OK" : "NO_COMPLETED_ORDER";
         return sub;
     }
@@ -195,12 +198,13 @@ INSERT INTO dwd_t8_order_shipment_fulfillment
   (tenant_id, factory_id, biz_date, source_ztid, order_no,
    total_rows, in_window_rows, fulfillment_rate, batch_id, create_time)
 VALUES
-  (1300000000001, 1, @bizDate, @ztid, @orderNo,
+  (@TenantId, 1, @bizDate, @ztid, @orderNo,
    @total, @inWindow, @rate, @batchId, @now)
 ON DUPLICATE KEY UPDATE
   total_rows=VALUES(total_rows), in_window_rows=VALUES(in_window_rows),
   fulfillment_rate=VALUES(fulfillment_rate),
   batch_id=VALUES(batch_id), update_time=@now",
+                new SugarParameter("@TenantId", option.TenantId),
                 new SugarParameter("@bizDate", option.BizDate),
                 new SugarParameter("@ztid", option.SourceZtid),
                 new SugarParameter("@orderNo", r.noid),
@@ -215,7 +219,7 @@ ON DUPLICATE KEY UPDATE
         decimal? metricValue = rateList.Count > 0
             ? Math.Round(rateList.Average(), 4)
             : null;
-        sub.KpiRows = await UpsertKpiValueAsync("S7_L1_002", option.BizDate, metricValue, now);
+        sub.KpiRows = await UpsertKpiValueAsync("S7_L1_002", option.TenantId, option.BizDate, metricValue, now);
         sub.DenominatorStatus = rateList.Count > 0 ? "OK" : "NO_VALID_ORDER";
         return sub;
     }
@@ -273,13 +277,14 @@ INSERT INTO dwd_t8_finished_warehouse_efficiency
   (tenant_id, factory_id, biz_month, source_ztid, period_start, period_end,
    shipment_qty, warehouse_headcount, efficiency, denominator_status, batch_id, create_time)
 VALUES
-  (1300000000001, 1, @bizMonth, @ztid, @periodStart, @periodEnd,
+  (@TenantId, 1, @bizMonth, @ztid, @periodStart, @periodEnd,
    @shipmentQty, @headcount, @efficiency, @denomStatus, @batchId, @now)
 ON DUPLICATE KEY UPDATE
   period_start=VALUES(period_start), period_end=VALUES(period_end),
   shipment_qty=VALUES(shipment_qty), warehouse_headcount=VALUES(warehouse_headcount),
   efficiency=VALUES(efficiency), denominator_status=VALUES(denominator_status),
   batch_id=VALUES(batch_id), update_time=@now",
+            new SugarParameter("@TenantId", option.TenantId),
             new SugarParameter("@bizMonth", option.BizMonth),
             new SugarParameter("@ztid", option.SourceZtid),
             new SugarParameter("@periodStart", option.MonthlyPeriodStart),
@@ -292,7 +297,7 @@ ON DUPLICATE KEY UPDATE
             new SugarParameter("@now", now));
         sub.DwdRows = dwdAffected;
 
-        sub.KpiRows = await UpsertKpiValueAsync("S7_L1_003", option.MonthlyPeriodEnd, efficiency, now);
+        sub.KpiRows = await UpsertKpiValueAsync("S7_L1_003", option.TenantId, option.MonthlyPeriodEnd, efficiency, now);
         return sub;
     }
 
@@ -304,7 +309,7 @@ ON DUPLICATE KEY UPDATE
         return await t8.Ado.SqlQueryAsync<T>(sql, parameters);
     }
 
-    private async Task<int> UpsertKpiValueAsync(string metricCode, DateTime bizDate, decimal? metricValue, DateTime now)
+    private async Task<int> UpsertKpiValueAsync(string metricCode, long tenantId, DateTime bizDate, decimal? metricValue, DateTime now)
     {
         // 沿用 S3 UpsertS3KpiValueAsync 范式:先查现存行 → UPDATE;不存在 → SELECT MAX(id)+1 显式生成 id 后 INSERT。
         // ado_s9_kpi_value_l1_day.id 为手工分配主键(无 AUTO_INCREMENT),必须显式 set;
@@ -312,11 +317,12 @@ ON DUPLICATE KEY UPDATE
         // FIX-2:截断时分秒(月度 KPI 入参可能为 YYYY-MM-DD 23:59:59),保证 SELECT WHERE biz_date=@BizDate 与 DB date 列匹配,避免重复 INSERT。
         bizDate = bizDate.Date;
         var existingId = await _db.Ado.GetLongAsync(
-            "SELECT IFNULL((SELECT id FROM ado_s9_kpi_value_l1_day WHERE tenant_id=1300000000001 AND factory_id=1 " +
+            "SELECT IFNULL((SELECT id FROM ado_s9_kpi_value_l1_day WHERE tenant_id=@TenantId AND factory_id=1 " +
             "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>
             {
+                new("@TenantId", tenantId),
                 new("@ModuleCode", ModuleCode),
                 new("@MetricCode", metricCode),
                 new("@BizDate", bizDate)
@@ -340,10 +346,11 @@ INSERT INTO ado_s9_kpi_value_l1_day
    create_time, update_time, is_deleted, is_active,
    module_code, metric_code, metric_value, calc_time)
 VALUES
-  (@Id, 1300000000001, NULL, NULL, 1, NULL, @BizDate,
+  (@Id, @TenantId, NULL, NULL, 1, NULL, @BizDate,
    @Now, @Now, 0, 1,
    @ModuleCode, @MetricCode, @MetricValue, @Now)",
             new SugarParameter("@Id", nextId),
+            new SugarParameter("@TenantId", tenantId),
             new SugarParameter("@BizDate", bizDate),
             new SugarParameter("@Now", now),
             new SugarParameter("@ModuleCode", ModuleCode),
@@ -351,13 +358,14 @@ VALUES
             new SugarParameter("@MetricValue", metricValue));
     }
 
-    private async Task<long> InsertTransformRunLogAsync(string batchId, DateTime startedAt, string triggerType)
+    private async Task<long> InsertTransformRunLogAsync(string batchId, DateTime startedAt, string triggerType, long tenantId)
     {
         await _db.Ado.ExecuteCommandAsync(@"
 INSERT INTO mdp_transform_run_log
   (tenant_id, job_code, job_name, trigger_type, batch_id, status, start_time, stage_rows, standard_rows, dwd_rows, create_time, update_time)
 VALUES
-  (1300000000001, @JobCode, @JobName, @TriggerType, @BatchId, 'RUNNING', @StartTime, 0, 0, 0, @StartTime, @StartTime)",
+  (@TenantId, @JobCode, @JobName, @TriggerType, @BatchId, 'RUNNING', @StartTime, 0, 0, 0, @StartTime, @StartTime)",
+            new SugarParameter("@TenantId", tenantId),
             new SugarParameter("@JobCode", JobCode),
             new SugarParameter("@JobName", JobName),
             new SugarParameter("@TriggerType", triggerType),
@@ -452,6 +460,8 @@ WHERE id=@Id",
 public sealed class S7MdpRefreshOption
 {
     public string SourceZtid { get; set; } = "pbxfxp";
+    /// <summary>KPI 落库目标租户;0=由 SourceZtid 经 AidopSourceTenantMap 解析(禁止写死固定租户)。</summary>
+    public long TenantId { get; set; }
     public DateTime BizDate { get; set; }
     public string BizMonth { get; set; } = "";
     public DateTime MonthlyPeriodStart { get; set; }

+ 60 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/AidopSourceTenantMap.cs

@@ -0,0 +1,60 @@
+using Furion;
+using Microsoft.Extensions.Configuration;
+
+namespace Admin.NET.Plugin.AiDOP.Infrastructure;
+
+/// <summary>
+/// T8 源账套(ztid)→ AiDOP 目标租户(TenantId)映射解析器。
+///
+/// 背景:S5/S6/S7 的 KPI 由 T8 源库按账套 <c>ztid</c> 聚合产生,T8 行本身不含 DOP TenantId,
+/// 因此 KPI 落库租户不能像 S1–S4 那样"从源行 tenant_id 推导",只能由"一账套 → 一租户"的显式映射注入。
+/// 本类即该唯一映射来源,替代此前写死的 <c>tenant_id = 1300000000001</c>(禁止再作默认/兜底)。
+///
+/// 映射值来源【证据,非臆造默认】:T8 当前唯一账套 <c>pbxfxp</c> 对应的公司数据经 S1–S4 贴源落地后,
+/// 全部业务主数据(crm_seorder / PurOrdRctMaster / NbrMaster / WorkOrdMaster …)均在 TenantId=797403760988229
+/// (SysOrg.Name='AIDOP')名下,故 pbxfxp ↔ 797403760988229 与既有管线一致。
+///
+/// 可覆盖:appsettings 段 <c>AidopMdp:ZtidTenantMap</c>(形如 { "pbxfxp": "797403760988229" })优先于内置映射。
+/// 未映射的 ztid 一律抛错,绝不回退到任何固定租户。
+/// </summary>
+public static class AidopSourceTenantMap
+{
+    /// <summary>内置映射(部署可用 appsettings AidopMdp:ZtidTenantMap 覆盖)。</summary>
+    private static readonly IReadOnlyDictionary<string, long> BuiltInMap = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase)
+    {
+        // pbxfxp = T8 当前唯一账套;对应 DOP 业务数据租户 797403760988229(SysOrg.Name='AIDOP')
+        ["pbxfxp"] = 797403760988229L,
+    };
+
+    /// <summary>
+    /// 解析一次刷新的目标租户。
+    /// 优先用显式 <paramref name="explicitTenantId"/>(&gt;0);否则按 <paramref name="sourceZtid"/> 查映射;
+    /// 查不到即抛 <see cref="InvalidOperationException"/>,不回退固定租户。
+    /// </summary>
+    public static long ResolveTenantId(string sourceZtid, long explicitTenantId = 0)
+    {
+        if (explicitTenantId > 0)
+            return explicitTenantId;
+
+        if (string.IsNullOrWhiteSpace(sourceZtid))
+            throw new InvalidOperationException("MDP 刷新缺少 SourceZtid,无法确定目标租户;不允许回退固定租户。");
+
+        // appsettings 覆盖优先
+        var overridden = App.GetConfig<Dictionary<string, string>>("AidopMdp:ZtidTenantMap", true);
+        if (overridden != null)
+        {
+            foreach (var kv in overridden)
+            {
+                if (string.Equals(kv.Key, sourceZtid, StringComparison.OrdinalIgnoreCase)
+                    && long.TryParse(kv.Value, out var cfgTid) && cfgTid > 0)
+                    return cfgTid;
+            }
+        }
+
+        if (BuiltInMap.TryGetValue(sourceZtid, out var tid))
+            return tid;
+
+        throw new InvalidOperationException(
+            $"未找到 T8 账套 ztid='{sourceZtid}' 对应的 AiDOP 租户映射;请在 appsettings 段 AidopMdp:ZtidTenantMap 配置后再刷新(禁止回退固定租户)。");
+    }
+}

+ 19 - 10
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/S6MdpSyncTransformService.cs

@@ -39,11 +39,13 @@ public class S6MdpSyncTransformService : ITransient
     {
         cancellationToken.ThrowIfCancellationRequested();
         option ??= S6MdpRefreshOption.Default();
+        // 目标租户由 T8 账套(ztid)映射决定,替代此前写死的 1300000000001(禁止固定默认/兜底)
+        option.TenantId = AidopSourceTenantMap.ResolveTenantId(option.SourceZtid, option.TenantId);
 
         var now = DateTime.Now;
         var batchId = $"S6_MDP_FULL_{now:yyyyMMddHHmmss}";
         var normalizedTrigger = NormalizeTriggerType(triggerType);
-        var runLogId = await InsertTransformRunLogAsync(batchId, now, normalizedTrigger);
+        var runLogId = await InsertTransformRunLogAsync(batchId, now, normalizedTrigger, option.TenantId);
 
         var result = new S6MdpSyncTransformResult
         {
@@ -119,12 +121,13 @@ INSERT INTO dwd_t8_work_order_mfg_fulfillment
   (tenant_id, factory_id, biz_date, source_ztid, order_no, task_no, item_code,
    plan_qty, done_qty_in_window, fulfillment_rate, batch_id, create_time)
 VALUES
-  (1300000000001, 1, @bizDate, @ztid, @orderNo, @taskNo, @itemCode,
+  (@TenantId, 1, @bizDate, @ztid, @orderNo, @taskNo, @itemCode,
    @planQty, @doneQty, @rate, @batchId, @now)
 ON DUPLICATE KEY UPDATE
   plan_qty=VALUES(plan_qty), done_qty_in_window=VALUES(done_qty_in_window),
   fulfillment_rate=VALUES(fulfillment_rate),
   batch_id=VALUES(batch_id), update_time=@now",
+                new SugarParameter("@TenantId", option.TenantId),
                 new SugarParameter("@bizDate", option.BizDate),
                 new SugarParameter("@ztid", option.SourceZtid),
                 new SugarParameter("@orderNo", r.noid),
@@ -141,7 +144,7 @@ ON DUPLICATE KEY UPDATE
         decimal? metricValue = rateList.Count > 0
             ? Math.Round(rateList.Average() * 100m, 4) // 百分号
             : null;
-        sub.KpiRows = await UpsertKpiValueAsync("S6_L1_001", option.BizDate, metricValue, now);
+        sub.KpiRows = await UpsertKpiValueAsync("S6_L1_001", option.TenantId, option.BizDate, metricValue, now);
         sub.DenominatorStatus = rateList.Count > 0 ? "OK" : "NO_VALID_WORK_ORDER";
         return sub;
     }
@@ -197,13 +200,14 @@ INSERT INTO dwd_t8_work_order_mfg_efficiency
   (tenant_id, factory_id, biz_month, source_ztid, period_start, period_end,
    done_count, production_headcount, efficiency, denominator_status, batch_id, create_time)
 VALUES
-  (1300000000001, 1, @bizMonth, @ztid, @periodStart, @periodEnd,
+  (@TenantId, 1, @bizMonth, @ztid, @periodStart, @periodEnd,
    @doneCount, @headcount, @efficiency, @denomStatus, @batchId, @now)
 ON DUPLICATE KEY UPDATE
   period_start=VALUES(period_start), period_end=VALUES(period_end),
   done_count=VALUES(done_count), production_headcount=VALUES(production_headcount),
   efficiency=VALUES(efficiency), denominator_status=VALUES(denominator_status),
   batch_id=VALUES(batch_id), update_time=@now",
+            new SugarParameter("@TenantId", option.TenantId),
             new SugarParameter("@bizMonth", option.BizMonth),
             new SugarParameter("@ztid", option.SourceZtid),
             new SugarParameter("@periodStart", option.MonthlyPeriodStart),
@@ -216,7 +220,7 @@ ON DUPLICATE KEY UPDATE
             new SugarParameter("@now", now));
         sub.DwdRows = dwdAffected;
 
-        sub.KpiRows = await UpsertKpiValueAsync("S6_L1_002", option.MonthlyPeriodEnd, efficiency, now);
+        sub.KpiRows = await UpsertKpiValueAsync("S6_L1_002", option.TenantId, option.MonthlyPeriodEnd, efficiency, now);
         return sub;
     }
 
@@ -228,7 +232,7 @@ ON DUPLICATE KEY UPDATE
         return await t8.Ado.SqlQueryAsync<T>(sql, parameters);
     }
 
-    private async Task<int> UpsertKpiValueAsync(string metricCode, DateTime bizDate, decimal? metricValue, DateTime now)
+    private async Task<int> UpsertKpiValueAsync(string metricCode, long tenantId, DateTime bizDate, decimal? metricValue, DateTime now)
     {
         // 沿用 S3 UpsertS3KpiValueAsync 范式:先查现存行 → UPDATE;不存在 → SELECT MAX(id)+1 显式生成 id 后 INSERT。
         // ado_s9_kpi_value_l1_day.id 为手工分配主键(无 AUTO_INCREMENT),必须显式 set;
@@ -236,11 +240,12 @@ ON DUPLICATE KEY UPDATE
         // FIX-2:截断时分秒(月度 KPI 入参可能为 YYYY-MM-DD 23:59:59),保证 SELECT WHERE biz_date=@BizDate 与 DB date 列匹配,避免重复 INSERT。
         bizDate = bizDate.Date;
         var existingId = await _db.Ado.GetLongAsync(
-            "SELECT IFNULL((SELECT id FROM ado_s9_kpi_value_l1_day WHERE tenant_id=1300000000001 AND factory_id=1 " +
+            "SELECT IFNULL((SELECT id FROM ado_s9_kpi_value_l1_day WHERE tenant_id=@TenantId AND factory_id=1 " +
             "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>
             {
+                new("@TenantId", tenantId),
                 new("@ModuleCode", ModuleCode),
                 new("@MetricCode", metricCode),
                 new("@BizDate", bizDate)
@@ -264,10 +269,11 @@ INSERT INTO ado_s9_kpi_value_l1_day
    create_time, update_time, is_deleted, is_active,
    module_code, metric_code, metric_value, calc_time)
 VALUES
-  (@Id, 1300000000001, NULL, NULL, 1, NULL, @BizDate,
+  (@Id, @TenantId, NULL, NULL, 1, NULL, @BizDate,
    @Now, @Now, 0, 1,
    @ModuleCode, @MetricCode, @MetricValue, @Now)",
             new SugarParameter("@Id", nextId),
+            new SugarParameter("@TenantId", tenantId),
             new SugarParameter("@BizDate", bizDate),
             new SugarParameter("@Now", now),
             new SugarParameter("@ModuleCode", ModuleCode),
@@ -275,13 +281,14 @@ VALUES
             new SugarParameter("@MetricValue", metricValue));
     }
 
-    private async Task<long> InsertTransformRunLogAsync(string batchId, DateTime startedAt, string triggerType)
+    private async Task<long> InsertTransformRunLogAsync(string batchId, DateTime startedAt, string triggerType, long tenantId)
     {
         await _db.Ado.ExecuteCommandAsync(@"
 INSERT INTO mdp_transform_run_log
   (tenant_id, job_code, job_name, trigger_type, batch_id, status, start_time, stage_rows, standard_rows, dwd_rows, create_time, update_time)
 VALUES
-  (1300000000001, @JobCode, @JobName, @TriggerType, @BatchId, 'RUNNING', @StartTime, 0, 0, 0, @StartTime, @StartTime)",
+  (@TenantId, @JobCode, @JobName, @TriggerType, @BatchId, 'RUNNING', @StartTime, 0, 0, 0, @StartTime, @StartTime)",
+            new SugarParameter("@TenantId", tenantId),
             new SugarParameter("@JobCode", JobCode),
             new SugarParameter("@JobName", JobName),
             new SugarParameter("@TriggerType", triggerType),
@@ -376,6 +383,8 @@ WHERE id=@Id",
 public sealed class S6MdpRefreshOption
 {
     public string SourceZtid { get; set; } = "pbxfxp";
+    /// <summary>KPI 落库目标租户;0=由 SourceZtid 经 AidopSourceTenantMap 解析(禁止写死固定租户)。</summary>
+    public long TenantId { get; set; }
     public DateTime BizDate { get; set; }
     public string BizMonth { get; set; } = "";
     public DateTime MonthlyPeriodStart { get; set; }

+ 23 - 12
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/S5MdpSyncTransformService.cs

@@ -41,6 +41,8 @@ public class S5MdpSyncTransformService : ITransient
     {
         cancellationToken.ThrowIfCancellationRequested();
         option ??= S5MdpRefreshOption.Default();
+        // 目标租户由 T8 账套(ztid)映射决定,替代此前写死的 1300000000001(禁止固定默认/兜底)
+        option.TenantId = AidopSourceTenantMap.ResolveTenantId(option.SourceZtid, option.TenantId);
 
         var now = DateTime.Now;
         var batchId = $"S5_MDP_FULL_{now:yyyyMMddHHmmss}";
@@ -143,10 +145,11 @@ select b.code as code, min(a.shtime) as shtime
 INSERT INTO dwd_t8_material_online_cycle
   (tenant_id, factory_id, biz_date, source_ztid, item_code, online_date, receipt_date, cycle_days, batch_id, create_time)
 VALUES
-  (1300000000001, 1, @bizDate, @ztid, @itemCode, @online, @receipt, @cycleDays, @batchId, @now)
+  (@TenantId, 1, @bizDate, @ztid, @itemCode, @online, @receipt, @cycleDays, @batchId, @now)
 ON DUPLICATE KEY UPDATE
   online_date=VALUES(online_date), receipt_date=VALUES(receipt_date),
   cycle_days=VALUES(cycle_days), batch_id=VALUES(batch_id), update_time=@now",
+                new SugarParameter("@TenantId", option.TenantId),
                 new SugarParameter("@bizDate", option.BizDate),
                 new SugarParameter("@ztid", option.SourceZtid),
                 new SugarParameter("@itemCode", code),
@@ -162,7 +165,7 @@ ON DUPLICATE KEY UPDATE
         decimal? metricValue = cycleDaysList.Count > 0
             ? (decimal)cycleDaysList.Average()
             : null;
-        sub.KpiRows = await UpsertKpiValueAsync("S5_L1_001", option.BizDate, metricValue, now);
+        sub.KpiRows = await UpsertKpiValueAsync("S5_L1_001", option.TenantId, option.BizDate, metricValue, now);
         sub.DenominatorStatus = cycleDaysList.Count > 0 ? "OK" : "NO_NUMERATOR";
         return sub;
     }
@@ -220,12 +223,13 @@ INSERT INTO dwd_t8_material_online_fulfillment
   (tenant_id, factory_id, biz_date, source_ztid, work_order_no,
    before_kgdate_rows, total_rows, fulfillment_rate, batch_id, create_time)
 VALUES
-  (1300000000001, 1, @bizDate, @ztid, @workOrderNo, @beforeKg, @total, @rate, @batchId, @now)
+  (@TenantId, 1, @bizDate, @ztid, @workOrderNo, @beforeKg, @total, @rate, @batchId, @now)
 ON DUPLICATE KEY UPDATE
   before_kgdate_rows=VALUES(before_kgdate_rows),
   total_rows=VALUES(total_rows),
   fulfillment_rate=VALUES(fulfillment_rate),
   batch_id=VALUES(batch_id), update_time=@now",
+                new SugarParameter("@TenantId", option.TenantId),
                 new SugarParameter("@bizDate", option.BizDate),
                 new SugarParameter("@ztid", option.SourceZtid),
                 new SugarParameter("@workOrderNo", d.noid),
@@ -240,7 +244,7 @@ ON DUPLICATE KEY UPDATE
         decimal? metricValue = rateList.Count > 0
             ? Math.Round(rateList.Average() * 100m, 4) // 百分号
             : null;
-        sub.KpiRows = await UpsertKpiValueAsync("S5_L1_002", option.BizDate, metricValue, now);
+        sub.KpiRows = await UpsertKpiValueAsync("S5_L1_002", option.TenantId, option.BizDate, metricValue, now);
         sub.DenominatorStatus = rateList.Count > 0 ? "OK" : "NO_VALID_ORDER";
         return sub;
     }
@@ -301,13 +305,14 @@ INSERT INTO dwd_t8_material_warehouse_efficiency
   (tenant_id, factory_id, biz_month, source_ztid, period_start, period_end,
    online_qty, warehouse_headcount, efficiency, denominator_status, batch_id, create_time)
 VALUES
-  (1300000000001, 1, @bizMonth, @ztid, @periodStart, @periodEnd,
+  (@TenantId, 1, @bizMonth, @ztid, @periodStart, @periodEnd,
    @onlineQty, @headcount, @efficiency, @denomStatus, @batchId, @now)
 ON DUPLICATE KEY UPDATE
   period_start=VALUES(period_start), period_end=VALUES(period_end),
   online_qty=VALUES(online_qty), warehouse_headcount=VALUES(warehouse_headcount),
   efficiency=VALUES(efficiency), denominator_status=VALUES(denominator_status),
   batch_id=VALUES(batch_id), update_time=@now",
+            new SugarParameter("@TenantId", option.TenantId),
             new SugarParameter("@bizMonth", option.BizMonth),
             new SugarParameter("@ztid", option.SourceZtid),
             new SugarParameter("@periodStart", option.MonthlyPeriodStart),
@@ -321,7 +326,7 @@ ON DUPLICATE KEY UPDATE
         sub.DwdRows = dwdAffected;
 
         // 月度 KPI 入日表:用月末日作 biz_date,月内每天可由聚合 API 再分发;分母缺失时 metric_value=NULL
-        sub.KpiRows = await UpsertKpiValueAsync("S5_L1_003", option.MonthlyPeriodEnd, efficiency, now);
+        sub.KpiRows = await UpsertKpiValueAsync("S5_L1_003", option.TenantId, option.MonthlyPeriodEnd, efficiency, now);
         return sub;
     }
 
@@ -365,7 +370,7 @@ INSERT INTO dwd_t8_material_inventory_turnover
    warehouse_code, warehouse_name, item_code, item_name, category_code, category_name,
    avg_inventory_value, monthly_outbound_cost, turnover_days, batch_id, create_time)
 VALUES
-  (1300000000001, 1, @bizMonth, @ztid, @startYm, @endYm,
+  (@TenantId, 1, @bizMonth, @ztid, @startYm, @endYm,
    @ckcode, @ckname, @itemCode, @itemName, @pcode, @pname,
    @je3, @je2, @turnoverDays, @batchId, @now)
 ON DUPLICATE KEY UPDATE
@@ -377,6 +382,7 @@ ON DUPLICATE KEY UPDATE
   period_start_yyyymm=VALUES(period_start_yyyymm),
   period_end_yyyymm=VALUES(period_end_yyyymm),
   batch_id=VALUES(batch_id), update_time=@now",
+                new SugarParameter("@TenantId", option.TenantId),
                 new SugarParameter("@bizMonth", option.BizMonth),
                 new SugarParameter("@ztid", option.SourceZtid),
                 new SugarParameter("@startYm", option.TvfPeriodStartYyyymm),
@@ -399,7 +405,7 @@ ON DUPLICATE KEY UPDATE
         decimal? metricValue = turnoverDaysList.Count > 0
             ? Math.Round(turnoverDaysList.Average(), 4)
             : null;
-        sub.KpiRows = await UpsertKpiValueAsync("S5_L1_004", option.MonthlyPeriodEnd, metricValue, now);
+        sub.KpiRows = await UpsertKpiValueAsync("S5_L1_004", option.TenantId, option.MonthlyPeriodEnd, metricValue, now);
         sub.DenominatorStatus = turnoverDaysList.Count > 0 ? "OK" : "NO_VALID_OUTBOUND_COST";
         return sub;
     }
@@ -414,7 +420,7 @@ ON DUPLICATE KEY UPDATE
         return await t8.Ado.SqlQueryAsync<T>(sql, parameters);
     }
 
-    private async Task<int> UpsertKpiValueAsync(string metricCode, DateTime bizDate, decimal? metricValue, DateTime now)
+    private async Task<int> UpsertKpiValueAsync(string metricCode, long tenantId, DateTime bizDate, decimal? metricValue, DateTime now)
     {
         // 沿用 S3 UpsertS3KpiValueAsync 范式:先查现存行 → UPDATE;不存在 → SELECT MAX(id)+1 显式生成 id 后 INSERT。
         // ado_s9_kpi_value_l1_day.id 为手工分配主键(无 AUTO_INCREMENT),必须显式 set;
@@ -422,11 +428,12 @@ ON DUPLICATE KEY UPDATE
         // FIX-2:截断时分秒(月度 KPI 入参可能为 YYYY-MM-DD 23:59:59),保证 SELECT WHERE biz_date=@BizDate 与 DB date 列匹配,避免重复 INSERT。
         bizDate = bizDate.Date;
         var existingId = await _db.Ado.GetLongAsync(
-            "SELECT IFNULL((SELECT id FROM ado_s9_kpi_value_l1_day WHERE tenant_id=1300000000001 AND factory_id=1 " +
+            "SELECT IFNULL((SELECT id FROM ado_s9_kpi_value_l1_day WHERE tenant_id=@TenantId AND factory_id=1 " +
             "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>
             {
+                new("@TenantId", tenantId),
                 new("@ModuleCode", ModuleCode),
                 new("@MetricCode", metricCode),
                 new("@BizDate", bizDate)
@@ -450,10 +457,11 @@ INSERT INTO ado_s9_kpi_value_l1_day
    create_time, update_time, is_deleted, is_active,
    module_code, metric_code, metric_value, calc_time)
 VALUES
-  (@Id, 1300000000001, NULL, NULL, 1, NULL, @BizDate,
+  (@Id, @TenantId, NULL, NULL, 1, NULL, @BizDate,
    @Now, @Now, 0, 1,
    @ModuleCode, @MetricCode, @MetricValue, @Now)",
             new SugarParameter("@Id", nextId),
+            new SugarParameter("@TenantId", tenantId),
             new SugarParameter("@BizDate", bizDate),
             new SugarParameter("@Now", now),
             new SugarParameter("@ModuleCode", ModuleCode),
@@ -467,7 +475,8 @@ VALUES
 INSERT INTO mdp_transform_run_log
   (tenant_id, job_code, job_name, trigger_type, batch_id, status, start_time, stage_rows, standard_rows, dwd_rows, create_time, update_time)
 VALUES
-  (1300000000001, @JobCode, @JobName, @TriggerType, @BatchId, 'RUNNING', @StartTime, 0, 0, 0, @StartTime, @StartTime)",
+  (@TenantId, @JobCode, @JobName, @TriggerType, @BatchId, 'RUNNING', @StartTime, 0, 0, 0, @StartTime, @StartTime)",
+            new SugarParameter("@TenantId", option.TenantId),
             new SugarParameter("@JobCode", JobCode),
             new SugarParameter("@JobName", JobName),
             new SugarParameter("@TriggerType", triggerType),
@@ -574,6 +583,8 @@ public sealed class S5MdpRefreshOption
 {
     /// <summary>T8 账套(kc_tz_head.ztid);实测当前唯一账套为 pbxfxp。</summary>
     public string SourceZtid { get; set; } = "pbxfxp";
+    /// <summary>KPI 落库目标租户;0=由 SourceZtid 经 AidopSourceTenantMap 解析(禁止写死固定租户)。</summary>
+    public long TenantId { get; set; }
     /// <summary>日 T+1 KPI 的业务日期(默认昨天)。</summary>
     public DateTime BizDate { get; set; }
     /// <summary>月 M+1 KPI 的业务月 YYYY-MM(默认上月)。</summary>