|
|
@@ -58,11 +58,11 @@ public class S3MdpSyncTransformService : ITransient
|
|
|
await ReportAsync(reportProgress, new ModuleProgressUpdate(ModuleRebuildStages.Staging, 1, 35, "拉取源数据完成", result.StageRows, ModuleRebuildStages.Staging));
|
|
|
|
|
|
await ReportAsync(reportProgress, new ModuleProgressUpdate(ModuleRebuildStages.Standard, 2, 35, "正在标准化数据"));
|
|
|
- result.StandardRows = await TransformStandardAsync(batchId, now, cancellationToken);
|
|
|
+ result.StandardRows = await TransformStandardAsync(batchId, now, retireStale: true, cancellationToken);
|
|
|
await ReportAsync(reportProgress, new ModuleProgressUpdate(ModuleRebuildStages.Standard, 2, 50, "标准化数据完成", result.StandardRows, ModuleRebuildStages.Standard));
|
|
|
|
|
|
await ReportAsync(reportProgress, new ModuleProgressUpdate(ModuleRebuildStages.Dwd, 3, 50, "正在生成 DWD 明细"));
|
|
|
- result.DwdRows = await BuildDwdAsync(batchId, now, cancellationToken);
|
|
|
+ result.DwdRows = await BuildDwdAsync(batchId, now, retireStale: true, result, cancellationToken);
|
|
|
await ReportAsync(reportProgress, new ModuleProgressUpdate(ModuleRebuildStages.Dwd, 3, 65, "生成 DWD 明细完成", result.DwdRows, ModuleRebuildStages.Dwd));
|
|
|
|
|
|
await ReportAsync(reportProgress, new ModuleProgressUpdate(ModuleRebuildStages.Kpi, 4, 65, "正在重算 KPI"));
|
|
|
@@ -157,8 +157,8 @@ public class S3MdpSyncTransformService : ITransient
|
|
|
var codes = entityCodes?.ToList() ?? S3MdpEntityConfig.All.Select(x => x.EntityCode).ToList();
|
|
|
result.StageRows = await _stagingPuller.PullEntitiesAsync(
|
|
|
codes, batchId, tenantId, fullRefresh, "S3_MDP_INBOUND", cancellationToken);
|
|
|
- result.StandardRows = await TransformStandardAsync(batchId, now, cancellationToken);
|
|
|
- result.DwdRows = await BuildDwdAsync(batchId, now, cancellationToken);
|
|
|
+ result.StandardRows = await TransformStandardAsync(batchId, now, retireStale: false, cancellationToken);
|
|
|
+ result.DwdRows = await BuildDwdAsync(batchId, now, retireStale: false, result, cancellationToken);
|
|
|
result.KpiRows = await BuildS3KpiValuesAsync(now, cancellationToken);
|
|
|
result.AtomicRows = await _atomicBuild.BuildSupplyPurchaseDomainForAllDatesAsync(
|
|
|
_runScope.TenantId, _runScope.FactoryId, batchId, cancellationToken);
|
|
|
@@ -244,10 +244,11 @@ public class S3MdpSyncTransformService : ITransient
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private async Task<int> TransformStandardAsync(string batchId, DateTime now, CancellationToken cancellationToken)
|
|
|
+ private async Task<int> TransformStandardAsync(
|
|
|
+ string batchId, DateTime now, bool retireStale, CancellationToken cancellationToken)
|
|
|
{
|
|
|
var total = 0;
|
|
|
- foreach (var command in BuildStandardCommands(batchId, now))
|
|
|
+ foreach (var command in BuildStandardCommands(batchId, now, retireStale))
|
|
|
{
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
total += await _db.Ado.ExecuteCommandAsync(command.Sql, command.Parameters);
|
|
|
@@ -255,10 +256,12 @@ public class S3MdpSyncTransformService : ITransient
|
|
|
return total;
|
|
|
}
|
|
|
|
|
|
- private async Task<int> BuildDwdAsync(string batchId, DateTime now, CancellationToken cancellationToken)
|
|
|
+ private async Task<int> BuildDwdAsync(
|
|
|
+ string batchId, DateTime now, bool retireStale, S3MdpSyncTransformResult result,
|
|
|
+ CancellationToken cancellationToken)
|
|
|
{
|
|
|
var total = 0;
|
|
|
- foreach (var command in BuildDwdCommands(batchId, now))
|
|
|
+ foreach (var command in BuildDwdCommands(batchId, now, retireStale))
|
|
|
{
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
total += await _db.Ado.ExecuteCommandAsync(command.Sql, command.Parameters);
|
|
|
@@ -266,9 +269,60 @@ public class S3MdpSyncTransformService : ITransient
|
|
|
total += await UpsertMaterialReadinessDwdAsync(batchId, now);
|
|
|
await DeleteCurrentShortageAsync(now.Date);
|
|
|
total += await InsertMaterialShortageAsync(batchId, now);
|
|
|
+
|
|
|
+ // 发布证据:必须在 DWD 写入**与同日淘汰**都完成之后。
|
|
|
+ // 只有全量路径发布 —— 增量批次只含变更行,把它指认成「当前快照」会让读者看到残缺集合。
|
|
|
+ if (retireStale)
|
|
|
+ {
|
|
|
+ cancellationToken.ThrowIfCancellationRequested();
|
|
|
+ result.Publication = await PublishCurrentSnapshotAsync(batchId, now);
|
|
|
+ }
|
|
|
+
|
|
|
return total;
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 复核并记录本轮发布的当前快照,供 S8 Rule01 精确指认「当前态是哪一批」。
|
|
|
+ ///
|
|
|
+ /// <para><b>为什么不能靠 MAX(stat_date) 之类的时间约定</b>:源侧被合法清空的那一轮
|
|
|
+ /// 产出 0 行 DWD,于是当天根本不存在 <c>stat_date</c> 分区,同日淘汰也无行可删
|
|
|
+ /// (它只能删「今天」)。读者按最新日期兜底就会**静默回落到上一天**,把一批早已作废的行
|
|
|
+ /// 当成当前态。实测:租户 838257212780613 源侧清零、STD 归 0、跑批 SUCCESS,
|
|
|
+ /// 而 DWD 当前快照仍返回前一日的 10 行。</para>
|
|
|
+ ///
|
|
|
+ /// <para>返回的 <c>CurrentRows = 0</c> 是<b>合法结果</b> —— 成功发布了一个空快照。
|
|
|
+ /// 把它记进运行日志,下游才能把「正常的空」与「没发布成」区分开:前者健康且允许恢复,
|
|
|
+ /// 后者无证据、判 UNKNOWN 并拦下恢复。</para>
|
|
|
+ ///
|
|
|
+ /// <para>行数用<b>复核查询</b>得到,不用 INSERT 的影响行数:ODKU 的影响行数
|
|
|
+ /// (插入计 1、更新计 2、无变化计 0)从来不等于「当前有几行」。</para>
|
|
|
+ /// </summary>
|
|
|
+ private async Task<S3MdpSnapshotPublication> PublishCurrentSnapshotAsync(string batchId, DateTime now)
|
|
|
+ {
|
|
|
+ var scope = RequireScope();
|
|
|
+ var currentRows = await _db.Ado.GetIntAsync(
|
|
|
+ """
|
|
|
+ SELECT COUNT(*) FROM dwd_supplier_delivery
|
|
|
+ WHERE tenant_id=@TenantId
|
|
|
+ AND COALESCE(NULLIF(factory_id,0),1)=@FactoryId
|
|
|
+ AND stat_date=@StatDate
|
|
|
+ AND sync_batch_id=@BatchId
|
|
|
+ """,
|
|
|
+ new SugarParameter("@TenantId", scope.TenantId),
|
|
|
+ new SugarParameter("@FactoryId", scope.FactoryId),
|
|
|
+ new SugarParameter("@StatDate", now.Date),
|
|
|
+ new SugarParameter("@BatchId", batchId));
|
|
|
+
|
|
|
+ return new S3MdpSnapshotPublication
|
|
|
+ {
|
|
|
+ Table = "dwd_supplier_delivery",
|
|
|
+ BatchId = batchId,
|
|
|
+ StatDate = now.Date,
|
|
|
+ FactoryId = scope.FactoryId,
|
|
|
+ CurrentRows = currentRows
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
private async Task<int> BuildS3KpiValuesAsync(DateTime now, CancellationToken cancellationToken)
|
|
|
{
|
|
|
var affected = 0;
|
|
|
@@ -564,7 +618,7 @@ public class S3MdpSyncTransformService : ITransient
|
|
|
new SugarParameter("@TargetResolvedAt", snap.ResolvedAt));
|
|
|
}
|
|
|
|
|
|
- private IEnumerable<S3MdpSqlCommand> BuildStandardCommands(string batchId, DateTime now)
|
|
|
+ private IEnumerable<S3MdpSqlCommand> BuildStandardCommands(string batchId, DateTime now, bool retireStale)
|
|
|
{
|
|
|
yield return Cmd(
|
|
|
"""
|
|
|
@@ -712,11 +766,75 @@ public class S3MdpSyncTransformService : ITransient
|
|
|
JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.WorkOrd')),
|
|
|
d.source_biz_key, @BatchId, @Now
|
|
|
FROM mdp_stg_purchase_order d
|
|
|
- LEFT JOIN mdp_stg_purchase_order m ON m.source_table='PurOrdMaster'
|
|
|
- AND JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.PurOrd')) = JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.PurOrd'))
|
|
|
- LEFT JOIN mdp_stg_item i ON i.source_table='ItemMaster'
|
|
|
- AND JSON_UNQUOTE(JSON_EXTRACT(i.raw_data,'$.ItemNum')) = JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.ItemNum'))
|
|
|
- WHERE d.source_table='PurOrdDetail' AND IFNULL(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.PurOrd')), '') <> ''
|
|
|
+ -- ── enrichment JOIN 的两个约束,缺一不可 ────────────────────────────────────
|
|
|
+ --
|
|
|
+ -- ① 必须自带租户谓词。MdpSqlScope.InjectTenantFactory 只把 WHERE 改写成
|
|
|
+ -- `WHERE d.tenant_id=@TenantId AND ...`(实现是 Regex.Replace(sql, @"\bWHERE\b", ...)),
|
|
|
+ -- 它**结构上够不到 JOIN...ON**。少了它,采购单号与物料号就成了跨租户的字符串匹配:
|
|
|
+ -- 实测 mdp_stg_item 里 ItemMaster 的 item_code 有 5529 组跨租户重名(最多横跨 3 个租户)。
|
|
|
+ --
|
|
|
+ -- ② 必须按 (租户, 键) 去重取最新一行。贴源层同时存在两代 source_biz_key
|
|
|
+ -- (旧 `Domain:PurOrd:Line` 与新的裸 RecID,因 uk_source_key 含 source_system 与
|
|
|
+ -- biz_key,两代永远不会互相 ODKU 覆盖),同一个 PurOrd/ItemNum 因此有多行。
|
|
|
+ -- 不去重则同租户内仍会扇出 —— 单租户实测 870/220 = 3.95 倍。
|
|
|
+ --
|
|
|
+ -- 扇出被 uk_po_line(tenant_id,po_no,po_line) 的 ODKU 吸收,所以症状不是行数膨胀,
|
|
|
+ -- 而是 item_name / supplier_code / order_date / buyer 这些值由 MySQL 任取一行决定。
|
|
|
+ -- 这里不按 @BatchId 收窄 enrichment:增量路径下未变更的主数据不在本批次里,
|
|
|
+ -- 收窄会把它们打成 NULL。取最新一行才是「当前已知主数据」的正确语义。
|
|
|
+ -- 去重用 ROW_NUMBER 而不是 GROUP_CONCAT+SUBSTRING_INDEX:raw_data 是 JSON 大字段,
|
|
|
+ -- 而 group_concat_max_len 实测仅 1024,拼接必然截断且首元素本身就可能超限。
|
|
|
+ --
|
|
|
+ -- ③ 派生表里的每个 WHERE 都必须**自带** @TenantId。这不是冗余,是在关掉注入器:
|
|
|
+ -- Inject 只在 WHERE 后 240 字符内出现 @TenantId 时才跳过,否则一律改写。
|
|
|
+ -- 而 ResolveSourcePrefix 沿 SQL 反向找最近的同层 `FROM x 别名` 来定别名前缀,
|
|
|
+ -- 派生表外层是 `FROM (` —— 别名正则匹配不到括号,于是回退成**空前缀**,
|
|
|
+ -- 生成裸 `factory_id`;派生表的 SELECT 列表里没有这一列,
|
|
|
+ -- 整轮转换直接报 Unknown column 'factory_id' 失败(本地沙箱实测复现)。
|
|
|
+ -- (此处刻意不写出该报错的完整英文原文:注入器对 SQL 注释同样生效,
|
|
|
+ -- 原文里的 where 一词会被当成关键字改写,徒增排查噪音。)
|
|
|
+ -- 自带 @TenantId 后注入被跳过,作用域完全由这里的字面 SQL 决定。
|
|
|
+ -- 只按租户不按工厂:驱动行 d 已被注入器限定到 (@TenantId,@FactoryId),
|
|
|
+ -- 而主数据(PurOrdMaster / ItemMaster)的 factory_id 未必与明细一致,
|
|
|
+ -- 多加一层工厂过滤会把 supplier_code / item_name 静默打成 NULL。
|
|
|
+ LEFT JOIN (
|
|
|
+ SELECT x.tenant_id, x.pur_ord, x.raw_data FROM (
|
|
|
+ SELECT x0.tenant_id, x0.raw_data,
|
|
|
+ JSON_UNQUOTE(JSON_EXTRACT(x0.raw_data,'$.PurOrd')) AS pur_ord,
|
|
|
+ ROW_NUMBER() OVER (
|
|
|
+ PARTITION BY x0.tenant_id, JSON_UNQUOTE(JSON_EXTRACT(x0.raw_data,'$.PurOrd'))
|
|
|
+ ORDER BY x0.sync_time DESC, x0.id DESC) AS rn
|
|
|
+ FROM mdp_stg_purchase_order x0
|
|
|
+ WHERE x0.tenant_id=@TenantId AND x0.source_table='PurOrdMaster') x
|
|
|
+ WHERE x.tenant_id=@TenantId AND x.rn=1
|
|
|
+ ) m ON m.tenant_id = d.tenant_id
|
|
|
+ AND m.pur_ord = JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.PurOrd'))
|
|
|
+ LEFT JOIN (
|
|
|
+ SELECT y.tenant_id, y.item_num, y.raw_data FROM (
|
|
|
+ SELECT y0.tenant_id, y0.raw_data,
|
|
|
+ JSON_UNQUOTE(JSON_EXTRACT(y0.raw_data,'$.ItemNum')) AS item_num,
|
|
|
+ ROW_NUMBER() OVER (
|
|
|
+ PARTITION BY y0.tenant_id, JSON_UNQUOTE(JSON_EXTRACT(y0.raw_data,'$.ItemNum'))
|
|
|
+ ORDER BY y0.sync_time DESC, y0.id DESC) AS rn
|
|
|
+ FROM mdp_stg_item y0
|
|
|
+ WHERE y0.tenant_id=@TenantId AND y0.source_table='ItemMaster') y
|
|
|
+ WHERE y.tenant_id=@TenantId AND y.rn=1
|
|
|
+ ) i ON i.tenant_id = d.tenant_id
|
|
|
+ AND i.item_num = JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.ItemNum'))
|
|
|
+ -- 驱动行按本轮批次收窄:staging 与本转换共用同一个 @BatchId
|
|
|
+ -- (RunFullAsync 生成一次,依次传给 SyncStagingAsync / TransformStandardAsync / BuildDwdAsync),
|
|
|
+ -- 因此「本批次里的行」就是「本轮源侧的行」,比"最新批次"子查询更确定。
|
|
|
+ -- 少了它,本 INSERT 会把整张贴源历史重新物化,并把死行盖上最新批次戳
|
|
|
+ -- —— 实测租户 797 因此在 STD 沉淀了 336 行源侧已不存在的采购订单行(518 行中的 64.9%)。
|
|
|
+ -- 驱动行的租户/工厂作用域在这里写死,不再交给注入器。
|
|
|
+ -- 上面两个派生表插进 `FROM ... d` 与本筛选子句之间以后,ResolveSourcePrefix 反向找
|
|
|
+ -- 最近的同层 `FROM 表 别名` 时会先撞上派生表的 `FROM (`,别名正则匹配不到括号 →
|
|
|
+ -- 回退成空前缀,生成裸 `tenant_id` —— 而派生表 m/i 也各自暴露 tenant_id,
|
|
|
+ -- 该列随即变成歧义列。本 SQL 因此必须自带前缀,且注入对它应当是恒等变换。
|
|
|
+ -- (注释里刻意避开 WHERE 一词:注入器对注释同样生效,只有紧跟 @TenantId 时才跳过。)
|
|
|
+ WHERE d.tenant_id=@TenantId AND COALESCE(NULLIF(d.factory_id,0),1)=@FactoryId
|
|
|
+ AND d.source_table='PurOrdDetail' AND d.sync_batch_id=@BatchId
|
|
|
+ AND IFNULL(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.PurOrd')), '') <> ''
|
|
|
ON DUPLICATE KEY UPDATE po_type=VALUES(po_type), supplier_code=VALUES(supplier_code), item_code=VALUES(item_code),
|
|
|
item_name=VALUES(item_name), order_qty=VALUES(order_qty), received_qty=VALUES(received_qty), returned_qty=VALUES(returned_qty),
|
|
|
due_date=VALUES(due_date), need_date=VALUES(need_date), order_date=VALUES(order_date), status=VALUES(status),
|
|
|
@@ -724,6 +842,35 @@ public class S3MdpSyncTransformService : ITransient
|
|
|
update_time=CURRENT_TIMESTAMP
|
|
|
""", batchId, now);
|
|
|
|
|
|
+ // ── 每轮淘汰:让标准层重新成为源的镜像 ────────────────────────────────────────
|
|
|
+ //
|
|
|
+ // 只加批次过滤是不够的。过滤只让死行「不再被刷新」,而运行期链路里没有任何 DELETE,
|
|
|
+ // 死行会带着旧批次号永远留下 —— 那只是把问题从「认不出」变成「认得出但仍在」。
|
|
|
+ //
|
|
|
+ // 判据:本轮 INSERT 已给所有存活行盖上 @BatchId,
|
|
|
+ // 因此该租户下 sync_batch_id 不等于 @BatchId 的行,就是本轮源侧已不存在的行。
|
|
|
+ //
|
|
|
+ // 安全闸门用 mdp_sync_log 而不是「贴源层有没有行」:后者对「源侧真的清空了」的合法租户
|
|
|
+ // 会永久拒绝淘汰、把幽灵行锁死;前者精确表达「本轮拉取对该租户成功了」——
|
|
|
+ // 实测 0 行租户同样留下 SUCCESS 记录(如 1300000000777:90 次运行 / 90 次 0 行 / 90 次 SUCCESS),
|
|
|
+ // 因此源侧合法清空的租户能被正确清干净。
|
|
|
+ //
|
|
|
+ // 仅在全量路径执行:增量批次只含变更行,按它淘汰会把未变更的存量行全删掉。
|
|
|
+ if (retireStale)
|
|
|
+ {
|
|
|
+ yield return Cmd(
|
|
|
+ """
|
|
|
+ DELETE s FROM mdp_std_purchase_order s
|
|
|
+ WHERE s.tenant_id=@TenantId
|
|
|
+ AND COALESCE(NULLIF(s.factory_id,0),1)=@FactoryId
|
|
|
+ AND IFNULL(s.sync_batch_id,'') <> @BatchId
|
|
|
+ AND EXISTS (SELECT 1 FROM mdp_sync_log l
|
|
|
+ WHERE l.tenant_id=@TenantId
|
|
|
+ AND l.sync_batch_id=@BatchId
|
|
|
+ AND l.status='SUCCESS')
|
|
|
+ """, batchId, now);
|
|
|
+ }
|
|
|
+
|
|
|
yield return Cmd(
|
|
|
"""
|
|
|
INSERT INTO mdp_std_delivery_schedule
|
|
|
@@ -832,7 +979,7 @@ public class S3MdpSyncTransformService : ITransient
|
|
|
""", batchId, now);
|
|
|
}
|
|
|
|
|
|
- private IEnumerable<S3MdpSqlCommand> BuildDwdCommands(string batchId, DateTime now)
|
|
|
+ private IEnumerable<S3MdpSqlCommand> BuildDwdCommands(string batchId, DateTime now, bool retireStale)
|
|
|
{
|
|
|
yield return Cmd(
|
|
|
"""
|
|
|
@@ -895,6 +1042,31 @@ public class S3MdpSyncTransformService : ITransient
|
|
|
risk_level=VALUES(risk_level), sync_batch_id=VALUES(sync_batch_id), calc_time=VALUES(calc_time), update_time=CURRENT_TIMESTAMP
|
|
|
""", batchId, now);
|
|
|
|
|
|
+ // ── 同日快照淘汰:让 MAX(stat_date) 成为「单一批次」而不是「某一天」 ──────────────
|
|
|
+ //
|
|
|
+ // Rule01 的 Provider 取 stat_date 最大的那一天,然后读该天全部行。
|
|
|
+ // 但 DWD 是按 (tenant, factory, po_no, po_line, stat_date) 唯一键的 upsert,
|
|
|
+ // 同一天被跑两轮时,前一轮产出而后一轮没产出的行会**保留旧批次号留在同一天**——
|
|
|
+ // 于是 MAX(stat_date) 取到的是「半个旧批 + 半个新批」。实测已发生 3 次
|
|
|
+ // (如 2026-06-07 的 178 行 + 7 行分属两个批次)。
|
|
|
+ //
|
|
|
+ // 本轮 INSERT 已给所有应存在的行盖上 @BatchId,故同日里批次号不等于 @BatchId 的行
|
|
|
+ // 要么是本轮源侧已不存在的,要么是旁路写入方留下的(它们用 S3_DELIVERY_ / S3_OUTSOURCE_ 前缀)。
|
|
|
+ //
|
|
|
+ // 放在 INSERT **之后**而不是之前:先删后插会让快照短暂为空,
|
|
|
+ // 而这里没有事务,并发的 Rule01 取数会读到一个空快照并把全部在办异常判成已恢复。
|
|
|
+ if (retireStale)
|
|
|
+ {
|
|
|
+ yield return Cmd(
|
|
|
+ """
|
|
|
+ DELETE w FROM dwd_supplier_delivery w
|
|
|
+ WHERE w.tenant_id=@TenantId
|
|
|
+ AND COALESCE(NULLIF(w.factory_id,0),1)=@FactoryId
|
|
|
+ AND w.stat_date=@StatDate
|
|
|
+ AND IFNULL(w.sync_batch_id,'') <> @BatchId
|
|
|
+ """, batchId, now);
|
|
|
+ }
|
|
|
+
|
|
|
yield return Cmd("DELETE FROM dwd_supplier_risk WHERE stat_date=@StatDate", batchId, now);
|
|
|
yield return Cmd(
|
|
|
"""
|
|
|
@@ -1176,9 +1348,30 @@ public class S3MdpSyncTransformService : ITransient
|
|
|
return string.IsNullOrWhiteSpace(triggerType) ? "AUTO" : triggerType.Trim().ToUpperInvariant();
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 运行摘要。改用 JsonSerializer 而非字符串插值:batchId 今天不可能含引号,
|
|
|
+ /// 但拼 JSON 是会被后来者继承的脆弱写法,且这里要加的 publish 是嵌套对象。
|
|
|
+ /// </summary>
|
|
|
private static string BuildRunSummaryJson(S3MdpSyncTransformResult result)
|
|
|
{
|
|
|
- return $$"""{"batchId":"{{result.BatchId}}","stageRows":{{result.StageRows}},"standardRows":{{result.StandardRows}},"dwdRows":{{result.DwdRows}},"kpiRows":{{result.KpiRows}}}""";
|
|
|
+ return System.Text.Json.JsonSerializer.Serialize(new
|
|
|
+ {
|
|
|
+ batchId = result.BatchId,
|
|
|
+ stageRows = result.StageRows,
|
|
|
+ standardRows = result.StandardRows,
|
|
|
+ dwdRows = result.DwdRows,
|
|
|
+ kpiRows = result.KpiRows,
|
|
|
+ publish = result.Publication is null
|
|
|
+ ? null
|
|
|
+ : new
|
|
|
+ {
|
|
|
+ table = result.Publication.Table,
|
|
|
+ batchId = result.Publication.BatchId,
|
|
|
+ statDate = result.Publication.StatDate.ToString("yyyy-MM-dd"),
|
|
|
+ factoryId = result.Publication.FactoryId,
|
|
|
+ currentRows = result.Publication.CurrentRows
|
|
|
+ }
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
private static string ResolveKpiValueTable(int metricLevel)
|
|
|
@@ -1270,6 +1463,35 @@ public sealed class S3MdpSyncTransformResult
|
|
|
public int DwdRows { get; set; }
|
|
|
public int KpiRows { get; set; }
|
|
|
public int AtomicRows { get; set; }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 本轮发布的当前快照。<b>null 表示本轮没有发布</b>(增量路径,或全量路径中途抛错)。
|
|
|
+ /// <para>失败方向是保守的:抛错时调用方写 FAILED,本字段保持 null,
|
|
|
+ /// 于是运行日志里不会出现「成功了但发布证据是半批」的记录。</para>
|
|
|
+ /// </summary>
|
|
|
+ public S3MdpSnapshotPublication? Publication { get; set; }
|
|
|
+}
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 一次当前快照发布的结果。与 <c>status='SUCCESS'</c> 写在同一条 UPDATE 里
|
|
|
+/// (见 <c>MarkTransformRunSuccessAsync</c>),因此不存在「已成功但没有发布证据」的中间态。
|
|
|
+/// </summary>
|
|
|
+public sealed class S3MdpSnapshotPublication
|
|
|
+{
|
|
|
+ /// <summary>被发布为当前快照的表。</summary>
|
|
|
+ public string Table { get; set; } = string.Empty;
|
|
|
+
|
|
|
+ /// <summary>本轮批次号。读者据此确认「当前快照」出自哪一次运行。</summary>
|
|
|
+ public string BatchId { get; set; } = string.Empty;
|
|
|
+
|
|
|
+ /// <summary>本轮快照所在的 stat_date。与 BatchId 一起构成读者的精确定位键。</summary>
|
|
|
+ public DateTime StatDate { get; set; }
|
|
|
+
|
|
|
+ /// <summary>发布作用域的工厂号。运行日志表本身没有工厂列,只能由这里承载。</summary>
|
|
|
+ public long FactoryId { get; set; }
|
|
|
+
|
|
|
+ /// <summary>发布后该批次的当前行数。<b>0 是合法值</b>,表示成功发布了一个空快照。</summary>
|
|
|
+ public int CurrentRows { get; set; }
|
|
|
}
|
|
|
|
|
|
public sealed class S3MaterialRefreshResult
|