S5MdpSyncTransformService.cs 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851
  1. using Admin.NET.Core.Service;
  2. using Admin.NET.Plugin.AiDOP.Infrastructure;
  3. using Microsoft.Extensions.Logging;
  4. using System.Text.Json;
  5. namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
  6. /// <summary>
  7. /// S5 物料仓储 — KPI 计算与刷新转换服务。边界:8 迁 1 留。
  8. /// 已中台化(读本地标准层 mdp_std_t8_*,由 T8BaseInboundMdpSyncService 从 T8 贴源→标准):
  9. /// S5_L1_001 物料上线周期 / S5_L1_002 物料上线满足率(依赖 Cj_Bg_Head_Rep + kc_dd_list_cllist,已贴源)/ S5_L1_003 物料仓储人效。
  10. /// 仍 legacy 直连 T8(QueryT8Async,ConfigId=t8_v5):
  11. /// S5_L1_004 品类物料库存周转(依赖 T8 TVF Rep_总账_存货_V3,报表聚合结果无逐行主键,暂不中台化)。
  12. /// 结果统一落 dwd_t8_* 与 ado_s9_kpi_value_l1_day。计算口径沿用方老师 v5.4 KPI J 列。
  13. /// </summary>
  14. public class S5MdpSyncTransformService : ITransient
  15. {
  16. private readonly ISqlSugarClient _db;
  17. private readonly SysNoticeService _sysNoticeService;
  18. private readonly ILogger<S5MdpSyncTransformService> _logger;
  19. private const string JobCode = "S5_MDP_SYNC_TRANSFORM";
  20. private const string JobName = "S5 物料仓储 MDP 同步与转换";
  21. private const string T8ConfigId = "t8_v5";
  22. // 加固:legacy 直连 T8 查询的命令超时上限(秒)。防止 T8 TVF(Rep_总账_存货_V3) 慢或挂起时
  23. // 查询无限期阻塞、进而长时间持有刷新锁(见 AidopT8KpiManualRefreshService)。
  24. // 仅加超时护栏,不改 S5_L1_004 的 SQL / 参数 / 计算口径。(S5_L1_002 已中台化,不再走 QueryT8Async。)
  25. private const int T8CommandTimeoutSeconds = 60;
  26. private const string ModuleCode = "S5";
  27. // FAILURE-NOTIFICATION-1:超级管理员 superAdmin.NET(AccountType=999)
  28. private const long NoticeReceiverUserId = 1300000000101L;
  29. private const string NoticeReceiverUserName = "超级管理员";
  30. private readonly SmartOps.KpiCalcDispatcher _kpiCalcDispatcher;
  31. private readonly SmartOps.KpiDimensionRunService _dimensionRun;
  32. public S5MdpSyncTransformService(
  33. ISqlSugarClient db,
  34. SysNoticeService sysNoticeService,
  35. ILogger<S5MdpSyncTransformService> logger,
  36. SmartOps.KpiCalcDispatcher kpiCalcDispatcher,
  37. SmartOps.KpiDimensionRunService dimensionRun)
  38. {
  39. _db = db;
  40. _sysNoticeService = sysNoticeService;
  41. _logger = logger;
  42. _kpiCalcDispatcher = kpiCalcDispatcher;
  43. _dimensionRun = dimensionRun;
  44. }
  45. public async Task<S5MdpSyncTransformResult> RunFullAsync(
  46. CancellationToken cancellationToken = default,
  47. string triggerType = "AUTO",
  48. S5MdpRefreshOption? option = null)
  49. {
  50. cancellationToken.ThrowIfCancellationRequested();
  51. option ??= S5MdpRefreshOption.Default();
  52. NormalizeOption(option);
  53. var now = DateTime.Now;
  54. var batchId = $"S5_MDP_FULL_{now:yyyyMMddHHmmss}";
  55. var normalizedTrigger = NormalizeTriggerType(triggerType);
  56. var runLogId = await InsertTransformRunLogAsync(batchId, now, normalizedTrigger, option);
  57. var result = new S5MdpSyncTransformResult
  58. {
  59. BatchId = batchId,
  60. RunLogId = runLogId,
  61. TriggerType = normalizedTrigger,
  62. SourceZtid = option.SourceZtid,
  63. TargetTenantId = option.TargetTenantId,
  64. TargetFactoryId = option.TargetFactoryId,
  65. BizDate = option.BizDate,
  66. BizMonth = option.BizMonth,
  67. DailyPeriodStart = option.DailyPeriodStart,
  68. DailyPeriodEnd = option.DailyPeriodEnd,
  69. MonthlyPeriodStart = option.MonthlyPeriodStart,
  70. MonthlyPeriodEnd = option.MonthlyPeriodEnd
  71. };
  72. try
  73. {
  74. // 路径 A:直发 T8 SQL,不做 stg / std 中间层
  75. result.StageRows = 0;
  76. result.StandardRows = 0;
  77. var sub16 = await BuildS5L1001MaterialOnlineCycleAsync(batchId, now, option, normalizedTrigger, cancellationToken);
  78. result.MergeSub("S5_L1_001", sub16);
  79. var sub17 = await BuildS5L1002MaterialOnlineFulfillmentAsync(batchId, now, option, normalizedTrigger, cancellationToken);
  80. result.MergeSub("S5_L1_002", sub17);
  81. var sub18 = await BuildS5L1003MaterialWarehouseEfficiencyAsync(batchId, now, option, normalizedTrigger, cancellationToken);
  82. result.MergeSub("S5_L1_003", sub18);
  83. var sub19 = await BuildS5L1004MaterialInventoryTurnoverAsync(batchId, now, option, normalizedTrigger, cancellationToken);
  84. result.MergeSub("S5_L1_004", sub19);
  85. await MarkTransformRunSuccessAsync(runLogId, now, result);
  86. return result;
  87. }
  88. catch (Exception ex)
  89. {
  90. await MarkTransformRunFailedAsync(runLogId, now, ex.Message, batchId);
  91. throw;
  92. }
  93. }
  94. // ─────────────────────────────────────────────────────────────────────────
  95. // KPI 实现(方老师 v5.4 KPI J 列 SQL 原逻辑直发 T8)
  96. // ─────────────────────────────────────────────────────────────────────────
  97. /// <summary>S5_L1_001 物料上线周期 = 配送到产线日期(lbs=生产领料) - 收货日期(lbs=采购入库)。
  98. /// 数据准备(写 dwd 明细)始终执行;最终 KPI 聚合由计算配置分发器接管(LEGACY_CODE/CONFIG_SQL)。</summary>
  99. private async Task<KpiBuildSubResult> BuildS5L1001MaterialOnlineCycleAsync(
  100. string batchId, DateTime now, S5MdpRefreshOption option, string triggerType, CancellationToken ct)
  101. {
  102. var sub = new KpiBuildSubResult();
  103. // 双模式:读本地标准层 mdp_std_t8_*(源 identity Id→src_id),语义等价于原直发 T8 SQL。
  104. const string sqlOnline = @"
  105. select b.code as code, min(a.shtime) as shtime
  106. from mdp_std_t8_kc_tz_head a
  107. inner join mdp_std_t8_kc_tz_list b on a.src_id=b.idid
  108. where a.ztid=@ztid and a.lbs='生产领料' and a.hzyn=0 and a.zfyn=0 and a.shyn=1
  109. group by b.code";
  110. const string sqlReceipt = @"
  111. select b.code as code, min(a.shtime) as shtime
  112. from mdp_std_t8_kc_tz_head a
  113. inner join mdp_std_t8_kc_tz_list b on a.src_id=b.idid
  114. where a.ztid=@ztid and a.lbs='采购入库' and a.hzyn=0 and a.zfyn=0 and a.shyn=1
  115. group by b.code";
  116. var p = new[] { new SugarParameter("@ztid", option.SourceZtid) };
  117. var onlineRows = await _db.Ado.SqlQueryAsync<S5OnlineCycleRow>(sqlOnline, p);
  118. var receiptRows = await _db.Ado.SqlQueryAsync<S5OnlineCycleRow>(sqlReceipt, p);
  119. sub.T8Rows = onlineRows.Count + receiptRows.Count;
  120. var onlineByCode = onlineRows.Where(r => !string.IsNullOrEmpty(r.code))
  121. .ToDictionary(r => r.code!, r => r.shtime, StringComparer.OrdinalIgnoreCase);
  122. var receiptByCode = receiptRows.Where(r => !string.IsNullOrEmpty(r.code))
  123. .ToDictionary(r => r.code!, r => r.shtime, StringComparer.OrdinalIgnoreCase);
  124. var allCodes = new HashSet<string>(onlineByCode.Keys, StringComparer.OrdinalIgnoreCase);
  125. allCodes.UnionWith(receiptByCode.Keys);
  126. var dwdAffected = 0;
  127. var cycleDaysList = new List<int>();
  128. foreach (var code in allCodes)
  129. {
  130. ct.ThrowIfCancellationRequested();
  131. var online = onlineByCode.GetValueOrDefault(code);
  132. var receipt = receiptByCode.GetValueOrDefault(code);
  133. int? cycleDays = null;
  134. if (online.HasValue && receipt.HasValue)
  135. {
  136. cycleDays = (int)(online.Value.Date - receipt.Value.Date).TotalDays;
  137. cycleDaysList.Add(cycleDays.Value);
  138. }
  139. dwdAffected += await _db.Ado.ExecuteCommandAsync(@"
  140. INSERT INTO dwd_t8_material_online_cycle
  141. (tenant_id, factory_id, biz_date, source_ztid, item_code, online_date, receipt_date, cycle_days, batch_id, create_time)
  142. VALUES
  143. (@tenantId, @factoryId, @bizDate, @ztid, @itemCode, @online, @receipt, @cycleDays, @batchId, @now)
  144. ON DUPLICATE KEY UPDATE
  145. online_date=VALUES(online_date), receipt_date=VALUES(receipt_date),
  146. cycle_days=VALUES(cycle_days), batch_id=VALUES(batch_id), update_time=@now",
  147. new SugarParameter("@tenantId", option.TargetTenantId),
  148. new SugarParameter("@factoryId", option.TargetFactoryId),
  149. new SugarParameter("@bizDate", option.BizDate),
  150. new SugarParameter("@ztid", option.SourceZtid),
  151. new SugarParameter("@itemCode", code),
  152. new SugarParameter("@online", online),
  153. new SugarParameter("@receipt", receipt),
  154. new SugarParameter("@cycleDays", cycleDays),
  155. new SugarParameter("@batchId", batchId),
  156. new SugarParameter("@now", now));
  157. }
  158. sub.DwdRows = dwdAffected;
  159. // 数据准备(dwd 明细)已完成。最终 KPI 聚合交计算配置分发器:
  160. // 无配置/LEGACY_CODE → 用下面 legacy 均值;CONFIG_SQL → 执行已发布只读 SQL;
  161. // CONFIG_SQL 失败不 fallback、不写值、保留上一成功值(ShouldUpsert=false)。
  162. decimal? legacyValue = cycleDaysList.Count > 0 ? (decimal)cycleDaysList.Average() : null;
  163. var legacyDenom = cycleDaysList.Count > 0 ? "OK" : "NO_NUMERATOR";
  164. var dispatch = await _kpiCalcDispatcher.DispatchAsync(
  165. "S5_L1_001", ModuleCode, option.TargetTenantId, option.TargetFactoryId,
  166. option.BizDate, option.DailyPeriodStart, option.DailyPeriodEnd, option.SourceZtid,
  167. batchId, triggerType, legacyValue, legacyDenom, ct);
  168. sub.KpiRows = dispatch.ShouldUpsert
  169. ? await UpsertKpiValueAsync("S5_L1_001", option.BizDate, dispatch.MetricValue, now, option)
  170. : 0;
  171. sub.DenominatorStatus = dispatch.DenominatorStatus;
  172. // 调度:SUMMARY 成功/NO_DATA 后触发对应 DIMENSION 跑批(共享 BatchId;SUMMARY FAILED 不触发)。
  173. if (dispatch.ShouldUpsert)
  174. {
  175. try
  176. {
  177. await _dimensionRun.RunDimensionAsync(
  178. "S5_L1_001", ModuleCode, option.TargetTenantId, option.BizDate, batchId, triggerType, ct);
  179. }
  180. catch (Exception ex)
  181. {
  182. _logger.LogWarning(ex, "S5_L1_001 维度跑批异常(不影响汇总链路)");
  183. }
  184. }
  185. return sub;
  186. }
  187. /// <summary>S5_L1_002 物料上线满足率 = 开工日期前完成上线行数 / 工单物料总行数。</summary>
  188. private async Task<KpiBuildSubResult> BuildS5L1002MaterialOnlineFulfillmentAsync(
  189. string batchId, DateTime now, S5MdpRefreshOption option, string triggerType, CancellationToken ct)
  190. {
  191. var sub = new KpiBuildSubResult();
  192. // 中台化:读本地标准层 mdp_std_t8_*(kc_tz_head.Id→src_id、Cj_Bg_Head_Rep/kc_dd_list_cllist 已贴源),
  193. // 口径与原直发 T8 SQL 逐项等价(分子/分母/JOIN/去重/shtime<=kgdate/count 均不变),不再直连 t8_v5。
  194. const string sqlNumer = @"
  195. select lynoid as lynoid, count(*) as codenum
  196. from (
  197. select h.lynoid as lynoid, l.code as code
  198. from mdp_std_t8_kc_tz_head h
  199. inner join mdp_std_t8_kc_tz_list l on h.src_id=l.idid
  200. left join (
  201. select noid as noid, min(kgdate) as kgdate
  202. from mdp_std_t8_cj_bg_head_rep
  203. where ztid=@ztid group by noid
  204. ) c on h.lynoid=c.noid
  205. where h.ztid=@ztid and h.lbs='生产领料' and h.hzyn=0 and h.zfyn=0 and h.shyn=1 and h.shtime<=c.kgdate
  206. group by h.lynoid, l.code
  207. ) n
  208. group by lynoid";
  209. const string sqlDenom = @"
  210. select h.noid as noid, count(l.src_id) as listnum
  211. from mdp_std_t8_kc_dd_head h
  212. left join mdp_std_t8_kc_dd_list_cllist l on h.src_id=l.idid
  213. where h.ztid=@ztid and h.lbs='生产任务' and h.zf=0 and h.shyn=1
  214. group by h.noid";
  215. var p = new[] { new SugarParameter("@ztid", option.SourceZtid) };
  216. var numerRows = await _db.Ado.SqlQueryAsync<S5FulfillmentNumerRow>(sqlNumer, p);
  217. var denomRows = await _db.Ado.SqlQueryAsync<S5FulfillmentDenomRow>(sqlDenom, p);
  218. sub.T8Rows = numerRows.Count + denomRows.Count;
  219. var numerByOrder = numerRows.Where(r => !string.IsNullOrEmpty(r.lynoid))
  220. .ToDictionary(r => r.lynoid!, r => r.codenum, StringComparer.OrdinalIgnoreCase);
  221. var dwdAffected = 0;
  222. var rateList = new List<decimal>();
  223. foreach (var d in denomRows)
  224. {
  225. ct.ThrowIfCancellationRequested();
  226. if (string.IsNullOrEmpty(d.noid)) continue;
  227. var beforeKg = numerByOrder.GetValueOrDefault(d.noid, 0);
  228. decimal? rate = d.listnum > 0
  229. ? Math.Round((decimal)beforeKg / d.listnum, 4)
  230. : null; // 分母为 0 时不伪装真实 0
  231. if (rate.HasValue) rateList.Add(rate.Value);
  232. dwdAffected += await _db.Ado.ExecuteCommandAsync(@"
  233. INSERT INTO dwd_t8_material_online_fulfillment
  234. (tenant_id, factory_id, biz_date, source_ztid, work_order_no,
  235. before_kgdate_rows, total_rows, fulfillment_rate, batch_id, create_time)
  236. VALUES
  237. (@tenantId, @factoryId, @bizDate, @ztid, @workOrderNo, @beforeKg, @total, @rate, @batchId, @now)
  238. ON DUPLICATE KEY UPDATE
  239. before_kgdate_rows=VALUES(before_kgdate_rows),
  240. total_rows=VALUES(total_rows),
  241. fulfillment_rate=VALUES(fulfillment_rate),
  242. batch_id=VALUES(batch_id), update_time=@now",
  243. new SugarParameter("@tenantId", option.TargetTenantId),
  244. new SugarParameter("@factoryId", option.TargetFactoryId),
  245. new SugarParameter("@bizDate", option.BizDate),
  246. new SugarParameter("@ztid", option.SourceZtid),
  247. new SugarParameter("@workOrderNo", d.noid),
  248. new SugarParameter("@beforeKg", beforeKg),
  249. new SugarParameter("@total", d.listnum),
  250. new SugarParameter("@rate", rate),
  251. new SugarParameter("@batchId", batchId),
  252. new SugarParameter("@now", now));
  253. }
  254. sub.DwdRows = dwdAffected;
  255. // 数据准备(dwd 逐单明细)已完成。最终 KPI 聚合交计算配置分发器:
  256. // 无配置/LEGACY_CODE → 用下面 legacy 均值-of-比率×100;CONFIG_SQL → 执行已发布只读 SQL;
  257. // CONFIG_SQL 失败不 fallback、不写值、保留上一成功值(ShouldUpsert=false)。
  258. decimal? legacyValue = rateList.Count > 0
  259. ? Math.Round(rateList.Average() * 100m, 4) // 百分号
  260. : null;
  261. var legacyDenom = rateList.Count > 0 ? "OK" : "NO_VALID_ORDER";
  262. var dispatch = await _kpiCalcDispatcher.DispatchAsync(
  263. "S5_L1_002", ModuleCode, option.TargetTenantId, option.TargetFactoryId,
  264. option.BizDate, option.DailyPeriodStart, option.DailyPeriodEnd, option.SourceZtid,
  265. batchId, triggerType, legacyValue, legacyDenom, ct);
  266. sub.KpiRows = dispatch.ShouldUpsert
  267. ? await UpsertKpiValueAsync("S5_L1_002", option.BizDate, dispatch.MetricValue, now, option)
  268. : 0;
  269. sub.DenominatorStatus = dispatch.DenominatorStatus;
  270. // 调度:SUMMARY 成功/NO_DATA 后触发对应 DIMENSION 跑批(共享 BatchId;SUMMARY FAILED 不触发)。
  271. if (dispatch.ShouldUpsert)
  272. {
  273. try
  274. {
  275. await _dimensionRun.RunDimensionAsync(
  276. "S5_L1_002", ModuleCode, option.TargetTenantId, option.BizDate, batchId, triggerType, ct);
  277. }
  278. catch (Exception ex)
  279. {
  280. _logger.LogWarning(ex, "S5_L1_002 维度跑批异常(不影响汇总链路)");
  281. }
  282. }
  283. return sub;
  284. }
  285. /// <summary>S5_L1_003 物料仓储人效 = SUM(slzx where lbs=生产领料) / count(gw=仓管)。</summary>
  286. private async Task<KpiBuildSubResult> BuildS5L1003MaterialWarehouseEfficiencyAsync(
  287. string batchId, DateTime now, S5MdpRefreshOption option, string triggerType, CancellationToken ct)
  288. {
  289. var sub = new KpiBuildSubResult();
  290. const string sqlNumer = @"
  291. select sum(b.slzx) as slzx
  292. from mdp_std_t8_kc_tz_head a
  293. inner join mdp_std_t8_kc_tz_list b on a.src_id=b.idid
  294. where a.ztid=@ztid and a.lbs='生产领料' and a.hzyn=0 and a.zfyn=0 and a.shyn=1
  295. and a.shtime between @startDate and @endDate";
  296. const string sqlDenom = @"
  297. select count(*) as penum
  298. from mdp_std_t8_sys_pelist
  299. where ztid=@ztid and zzzt='在职' and gw='仓管'";
  300. var pNumer = new[]
  301. {
  302. new SugarParameter("@ztid", option.SourceZtid),
  303. new SugarParameter("@startDate", option.MonthlyPeriodStart),
  304. new SugarParameter("@endDate", option.MonthlyPeriodEnd)
  305. };
  306. var pDenom = new[] { new SugarParameter("@ztid", option.SourceZtid) };
  307. var numerRows = await _db.Ado.SqlQueryAsync<S5SumQtyRow>(sqlNumer, pNumer);
  308. var denomRows = await _db.Ado.SqlQueryAsync<S5CountRow>(sqlDenom, pDenom);
  309. sub.T8Rows = numerRows.Count + denomRows.Count;
  310. decimal? onlineQty = numerRows.FirstOrDefault()?.slzx;
  311. int? headcount = denomRows.FirstOrDefault()?.penum;
  312. // 分母 = 0 或 NULL:efficiency 写 NULL,并标记 denominator_status;不伪装真实 0
  313. decimal? efficiency = null;
  314. string denomStatus;
  315. if (!headcount.HasValue || headcount.Value <= 0)
  316. {
  317. denomStatus = "NO_HEADCOUNT";
  318. }
  319. else if (!onlineQty.HasValue)
  320. {
  321. denomStatus = "NO_NUMERATOR";
  322. }
  323. else
  324. {
  325. efficiency = Math.Round(onlineQty.Value / headcount.Value, 4);
  326. denomStatus = "OK";
  327. }
  328. sub.DenominatorStatus = denomStatus;
  329. // 月度 KPI 用 biz_month 唯一键,整月 1 行
  330. var dwdAffected = await _db.Ado.ExecuteCommandAsync(@"
  331. INSERT INTO dwd_t8_material_warehouse_efficiency
  332. (tenant_id, factory_id, biz_month, source_ztid, period_start, period_end,
  333. online_qty, warehouse_headcount, efficiency, denominator_status, batch_id, create_time)
  334. VALUES
  335. (@tenantId, @factoryId, @bizMonth, @ztid, @periodStart, @periodEnd,
  336. @onlineQty, @headcount, @efficiency, @denomStatus, @batchId, @now)
  337. ON DUPLICATE KEY UPDATE
  338. period_start=VALUES(period_start), period_end=VALUES(period_end),
  339. online_qty=VALUES(online_qty), warehouse_headcount=VALUES(warehouse_headcount),
  340. efficiency=VALUES(efficiency), denominator_status=VALUES(denominator_status),
  341. batch_id=VALUES(batch_id), update_time=@now",
  342. new SugarParameter("@tenantId", option.TargetTenantId),
  343. new SugarParameter("@factoryId", option.TargetFactoryId),
  344. new SugarParameter("@bizMonth", option.BizMonth),
  345. new SugarParameter("@ztid", option.SourceZtid),
  346. new SugarParameter("@periodStart", option.MonthlyPeriodStart),
  347. new SugarParameter("@periodEnd", option.MonthlyPeriodEnd),
  348. new SugarParameter("@onlineQty", onlineQty),
  349. new SugarParameter("@headcount", headcount),
  350. new SugarParameter("@efficiency", efficiency),
  351. new SugarParameter("@denomStatus", denomStatus),
  352. new SugarParameter("@batchId", batchId),
  353. new SugarParameter("@now", now));
  354. sub.DwdRows = dwdAffected;
  355. // 月度 KPI 最终聚合交分发器;bizDate=月末、period=当月窗口;
  356. // legacyValue=efficiency、legacyDenom 保留 NO_HEADCOUNT/NO_NUMERATOR(CONFIG_SQL 下塌缩为 NO_DATA)。
  357. var dispatch = await _kpiCalcDispatcher.DispatchAsync(
  358. "S5_L1_003", ModuleCode, option.TargetTenantId, option.TargetFactoryId,
  359. option.MonthlyPeriodEnd, option.MonthlyPeriodStart, option.MonthlyPeriodEnd, option.SourceZtid,
  360. batchId, triggerType, efficiency, denomStatus, ct);
  361. sub.KpiRows = dispatch.ShouldUpsert
  362. ? await UpsertKpiValueAsync("S5_L1_003", option.MonthlyPeriodEnd, dispatch.MetricValue, now, option)
  363. : 0;
  364. sub.DenominatorStatus = dispatch.DenominatorStatus;
  365. // 调度:SUMMARY 成功/NO_DATA 后触发人效月度 DIMENSION 跑批(月度:@biz_date=月末派生 biz_month,与 SUMMARY 同月)。
  366. if (dispatch.ShouldUpsert)
  367. {
  368. try
  369. {
  370. await _dimensionRun.RunDimensionAsync(
  371. "S5_L1_003", ModuleCode, option.TargetTenantId, option.MonthlyPeriodEnd, batchId, triggerType, ct);
  372. }
  373. catch (Exception ex)
  374. {
  375. _logger.LogWarning(ex, "S5_L1_003 维度跑批异常(不影响汇总链路)");
  376. }
  377. }
  378. return sub;
  379. }
  380. /// <summary>S5_L1_004 品类物料库存周转 = D1/D2 × 30;D1=je3 月均库存金额,D2=je2 出库成本。</summary>
  381. private async Task<KpiBuildSubResult> BuildS5L1004MaterialInventoryTurnoverAsync(
  382. string batchId, DateTime now, S5MdpRefreshOption option, string triggerType, CancellationToken ct)
  383. {
  384. var sub = new KpiBuildSubResult();
  385. // TVF:Rep_总账_存货_V3(账套, '普通', '正常', 起期 YYYYMM, 止期 YYYYMM)
  386. const string sqlTvf = @"
  387. select ckcode as ckcode, ckname as ckname,
  388. code as code, cname as cname,
  389. pcode as pcode, pname as pname,
  390. je3 as je3, je2 as je2
  391. from dbo.Rep_总账_存货_V3(@ztid, N'普通', N'正常', @startYm, @endYm)";
  392. var p = new[]
  393. {
  394. new SugarParameter("@ztid", option.SourceZtid),
  395. new SugarParameter("@startYm", option.TvfPeriodStartYyyymm),
  396. new SugarParameter("@endYm", option.TvfPeriodEndYyyymm)
  397. };
  398. var tvfRows = await QueryT8Async<S5InventoryTurnoverRow>(sqlTvf, p);
  399. sub.T8Rows = tvfRows.Count;
  400. var dwdAffected = 0;
  401. var turnoverDaysList = new List<decimal>();
  402. foreach (var r in tvfRows)
  403. {
  404. ct.ThrowIfCancellationRequested();
  405. // 周转天数:D2=0 或 NULL 时 NULL,不伪装 0
  406. decimal? turnoverDays = (r.je2.HasValue && r.je2.Value > 0m && r.je3.HasValue)
  407. ? Math.Round(r.je3.Value / r.je2.Value * 30m, 4)
  408. : null;
  409. if (turnoverDays.HasValue) turnoverDaysList.Add(turnoverDays.Value);
  410. dwdAffected += await _db.Ado.ExecuteCommandAsync(@"
  411. INSERT INTO dwd_t8_material_inventory_turnover
  412. (tenant_id, factory_id, biz_month, source_ztid, period_start_yyyymm, period_end_yyyymm,
  413. warehouse_code, warehouse_name, item_code, item_name, category_code, category_name,
  414. avg_inventory_value, monthly_outbound_cost, turnover_days, batch_id, create_time)
  415. VALUES
  416. (@tenantId, @factoryId, @bizMonth, @ztid, @startYm, @endYm,
  417. @ckcode, @ckname, @itemCode, @itemName, @pcode, @pname,
  418. @je3, @je2, @turnoverDays, @batchId, @now)
  419. ON DUPLICATE KEY UPDATE
  420. warehouse_name=VALUES(warehouse_name), item_name=VALUES(item_name),
  421. category_code=VALUES(category_code), category_name=VALUES(category_name),
  422. avg_inventory_value=VALUES(avg_inventory_value),
  423. monthly_outbound_cost=VALUES(monthly_outbound_cost),
  424. turnover_days=VALUES(turnover_days),
  425. period_start_yyyymm=VALUES(period_start_yyyymm),
  426. period_end_yyyymm=VALUES(period_end_yyyymm),
  427. batch_id=VALUES(batch_id), update_time=@now",
  428. new SugarParameter("@tenantId", option.TargetTenantId),
  429. new SugarParameter("@factoryId", option.TargetFactoryId),
  430. new SugarParameter("@bizMonth", option.BizMonth),
  431. new SugarParameter("@ztid", option.SourceZtid),
  432. new SugarParameter("@startYm", option.TvfPeriodStartYyyymm),
  433. new SugarParameter("@endYm", option.TvfPeriodEndYyyymm),
  434. new SugarParameter("@ckcode", r.ckcode ?? ""),
  435. new SugarParameter("@ckname", r.ckname),
  436. new SugarParameter("@itemCode", r.code ?? ""),
  437. new SugarParameter("@itemName", r.cname),
  438. new SugarParameter("@pcode", r.pcode),
  439. new SugarParameter("@pname", r.pname),
  440. new SugarParameter("@je3", r.je3),
  441. new SugarParameter("@je2", r.je2),
  442. new SugarParameter("@turnoverDays", turnoverDays),
  443. new SugarParameter("@batchId", batchId),
  444. new SugarParameter("@now", now));
  445. }
  446. sub.DwdRows = dwdAffected;
  447. // KPI 值:所有品类周转天数算术平均;无任一可计算品类时 NULL
  448. decimal? metricValue = turnoverDaysList.Count > 0
  449. ? Math.Round(turnoverDaysList.Average(), 4)
  450. : null;
  451. // S5_L1_004 保留 LEGACY_TVF:分发器 LEGACY_TVF 分支直接回传上面的 TVF 均值,
  452. // 不经 KpiSqlReadOnlyExecutor;仅统一 run-log 记录引擎状态(配置登记为 LEGACY_TVF)。
  453. // TVF(Rep_总账_存货_V3)/参数/口径/CommandTimeout 全不变,不建 CONFIG_SQL 版本。
  454. var legacyDenom = turnoverDaysList.Count > 0 ? "OK" : "NO_VALID_OUTBOUND_COST";
  455. var dispatch = await _kpiCalcDispatcher.DispatchAsync(
  456. "S5_L1_004", ModuleCode, option.TargetTenantId, option.TargetFactoryId,
  457. option.MonthlyPeriodEnd, option.MonthlyPeriodStart, option.MonthlyPeriodEnd, option.SourceZtid,
  458. batchId, triggerType, metricValue, legacyDenom, ct);
  459. sub.KpiRows = dispatch.ShouldUpsert
  460. ? await UpsertKpiValueAsync("S5_L1_004", option.MonthlyPeriodEnd, dispatch.MetricValue, now, option)
  461. : 0;
  462. sub.DenominatorStatus = dispatch.DenominatorStatus;
  463. // 调度:SUMMARY 成功/NO_DATA 后触发对应 DIMENSION 跑批(月度:DIMENSION_SQL 用 @biz_date 派生 biz_month,与 SUMMARY 同月)。
  464. if (dispatch.ShouldUpsert)
  465. {
  466. try
  467. {
  468. await _dimensionRun.RunDimensionAsync(
  469. "S5_L1_004", ModuleCode, option.TargetTenantId, option.MonthlyPeriodEnd, batchId, triggerType, ct);
  470. }
  471. catch (Exception ex)
  472. {
  473. _logger.LogWarning(ex, "S5_L1_004 维度跑批异常(不影响汇总链路)");
  474. }
  475. }
  476. return sub;
  477. }
  478. // ─────────────────────────────────────────────────────────────────────────
  479. // 跨库 / 写入 / 日志 封装
  480. // ─────────────────────────────────────────────────────────────────────────
  481. // legacy 直连 T8:仅 S5_L1_004(Rep_总账_存货_V3 TVF,报表聚合结果无逐行主键)仍用;
  482. // 已中台化的 S5_L1_001/002/003 均读 mdp_std_t8_*,不再走此方法。
  483. private async Task<List<T>> QueryT8Async<T>(string sql, SugarParameter[] parameters)
  484. {
  485. var t8 = _db.AsTenant().GetConnectionScope(T8ConfigId);
  486. t8.Ado.CommandTimeOut = T8CommandTimeoutSeconds;
  487. return await t8.Ado.SqlQueryAsync<T>(sql, parameters);
  488. }
  489. private async Task<int> UpsertKpiValueAsync(string metricCode, DateTime bizDate, decimal? metricValue, DateTime now, S5MdpRefreshOption option)
  490. {
  491. // 沿用 S3 UpsertS3KpiValueAsync 范式:先查现存行 → UPDATE;不存在 → SELECT MAX(id)+1 显式生成 id 后 INSERT。
  492. // ado_s9_kpi_value_l1_day.id 为手工分配主键(无 AUTO_INCREMENT),必须显式 set;
  493. // metric_value 允许 NULL(分母缺失不得伪装真实 0)。
  494. // FIX-2:截断时分秒(月度 KPI 入参可能为 YYYY-MM-DD 23:59:59),保证 SELECT WHERE biz_date=@BizDate 与 DB date 列匹配,避免重复 INSERT。
  495. // FIX-1:tenant_id/factory_id 取自 option,默认仍为 1300000000001/1,不破坏 Demo。
  496. bizDate = bizDate.Date;
  497. var existingId = await _db.Ado.GetLongAsync(
  498. "SELECT IFNULL((SELECT id FROM ado_s9_kpi_value_l1_day WHERE tenant_id=@TenantId AND factory_id=@FactoryId " +
  499. "AND module_code=@ModuleCode AND metric_code=@MetricCode AND biz_date=@BizDate AND is_deleted=0 " +
  500. "ORDER BY id LIMIT 1), 0)",
  501. new List<SugarParameter>
  502. {
  503. new("@TenantId", option.TargetTenantId),
  504. new("@FactoryId", option.TargetFactoryId),
  505. new("@ModuleCode", ModuleCode),
  506. new("@MetricCode", metricCode),
  507. new("@BizDate", bizDate)
  508. });
  509. if (existingId > 0)
  510. {
  511. return await _db.Ado.ExecuteCommandAsync(
  512. "UPDATE ado_s9_kpi_value_l1_day SET metric_value=@MetricValue, calc_time=@Now, " +
  513. "update_time=@Now, is_deleted=0, is_active=1 WHERE id=@Id",
  514. new SugarParameter("@MetricValue", metricValue),
  515. new SugarParameter("@Now", now),
  516. new SugarParameter("@Id", existingId));
  517. }
  518. var nextId = await _db.Ado.GetLongAsync(
  519. "SELECT COALESCE(MAX(id), 0) + 1 FROM ado_s9_kpi_value_l1_day");
  520. return await _db.Ado.ExecuteCommandAsync(@"
  521. INSERT INTO ado_s9_kpi_value_l1_day
  522. (id, tenant_id, org_id, company_id, factory_id, status, biz_date,
  523. create_time, update_time, is_deleted, is_active,
  524. module_code, metric_code, metric_value, calc_time)
  525. VALUES
  526. (@Id, @TenantId, NULL, NULL, @FactoryId, NULL, @BizDate,
  527. @Now, @Now, 0, 1,
  528. @ModuleCode, @MetricCode, @MetricValue, @Now)",
  529. new SugarParameter("@Id", nextId),
  530. new SugarParameter("@TenantId", option.TargetTenantId),
  531. new SugarParameter("@FactoryId", option.TargetFactoryId),
  532. new SugarParameter("@BizDate", bizDate),
  533. new SugarParameter("@Now", now),
  534. new SugarParameter("@ModuleCode", ModuleCode),
  535. new SugarParameter("@MetricCode", metricCode),
  536. new SugarParameter("@MetricValue", metricValue));
  537. }
  538. private async Task<long> InsertTransformRunLogAsync(string batchId, DateTime startedAt, string triggerType, S5MdpRefreshOption option)
  539. {
  540. await _db.Ado.ExecuteCommandAsync(@"
  541. INSERT INTO mdp_transform_run_log
  542. (tenant_id, job_code, job_name, trigger_type, batch_id, status, start_time, stage_rows, standard_rows, dwd_rows, create_time, update_time)
  543. VALUES
  544. (@TenantId, @JobCode, @JobName, @TriggerType, @BatchId, 'RUNNING', @StartTime, 0, 0, 0, @StartTime, @StartTime)",
  545. new SugarParameter("@TenantId", option.TargetTenantId),
  546. new SugarParameter("@JobCode", JobCode),
  547. new SugarParameter("@JobName", JobName),
  548. new SugarParameter("@TriggerType", triggerType),
  549. new SugarParameter("@BatchId", batchId),
  550. new SugarParameter("@StartTime", startedAt));
  551. return await _db.Ado.GetLongAsync(
  552. "SELECT id FROM mdp_transform_run_log WHERE batch_id=@BatchId ORDER BY id DESC LIMIT 1",
  553. new List<SugarParameter> { new("@BatchId", batchId) });
  554. }
  555. private async Task MarkTransformRunSuccessAsync(long runLogId, DateTime startedAt, S5MdpSyncTransformResult result)
  556. {
  557. var finishedAt = DateTime.Now;
  558. await _db.Ado.ExecuteCommandAsync(@"
  559. UPDATE mdp_transform_run_log
  560. SET status='SUCCESS', end_time=@EndTime, duration_ms=@DurationMs,
  561. stage_rows=@StageRows, standard_rows=@StandardRows, dwd_rows=@DwdRows,
  562. summary_json=@SummaryJson, update_time=CURRENT_TIMESTAMP
  563. WHERE id=@Id",
  564. new SugarParameter("@EndTime", finishedAt),
  565. new SugarParameter("@DurationMs", (int)(finishedAt - startedAt).TotalMilliseconds),
  566. new SugarParameter("@StageRows", result.StageRows),
  567. new SugarParameter("@StandardRows", result.StandardRows),
  568. new SugarParameter("@DwdRows", result.DwdRows),
  569. new SugarParameter("@SummaryJson", BuildRunSummaryJson(result)),
  570. new SugarParameter("@Id", runLogId));
  571. }
  572. private async Task MarkTransformRunFailedAsync(long runLogId, DateTime startedAt, string message, string batchId)
  573. {
  574. bool runLogUpdated = false;
  575. try
  576. {
  577. var finishedAt = DateTime.Now;
  578. await _db.Ado.ExecuteCommandAsync(@"
  579. UPDATE mdp_transform_run_log
  580. SET status='FAILED', end_time=@EndTime, duration_ms=@DurationMs,
  581. error_message=@ErrorMessage, update_time=CURRENT_TIMESTAMP
  582. WHERE id=@Id",
  583. new SugarParameter("@EndTime", finishedAt),
  584. new SugarParameter("@DurationMs", (int)(finishedAt - startedAt).TotalMilliseconds),
  585. new SugarParameter("@ErrorMessage", Truncate(message, 2000)),
  586. new SugarParameter("@Id", runLogId));
  587. runLogUpdated = true;
  588. }
  589. catch (Exception ex)
  590. {
  591. // 写库本身失败兜底:远端 MySQL 瞬断导致 MarkFailed 自身也连不上
  592. Console.Error.WriteLine($"[S5MdpSyncTransform] MarkTransformRunFailed write failed (runLogId={runLogId}): {ex.Message}");
  593. }
  594. // FAILURE-NOTIFICATION-1:写库 FAILED 成功后发通知给超级管理员;通知失败不影响主流程
  595. if (!runLogUpdated) return;
  596. try
  597. {
  598. await _sysNoticeService.AddNotice(new AddNoticeInput
  599. {
  600. Title = "S5 物料仓储 T8 KPI 跑批失败",
  601. Content = $"模块:S5 物料仓储\n批次ID:{batchId}\n失败时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss}\n错误信息:{Truncate(message, 1000)}\n\n请查看 mdp_transform_run_log 获取完整错误与重试记录。",
  602. Type = NoticeTypeEnum.NOTICE,
  603. PublicTime = DateTime.Now,
  604. Status = NoticeStatusEnum.PUBLIC,
  605. PublicUserId = NoticeReceiverUserId,
  606. PublicUserName = NoticeReceiverUserName
  607. });
  608. }
  609. catch (Exception notifyEx)
  610. {
  611. _logger.LogError(notifyEx, "[S5MdpSyncTransform] SysNotice 发送失败 (runLogId={RunLogId}, batchId={BatchId})", runLogId, batchId);
  612. }
  613. }
  614. private static string BuildRunSummaryJson(S5MdpSyncTransformResult r)
  615. {
  616. var summary = new
  617. {
  618. batchId = r.BatchId,
  619. sourceZtid = r.SourceZtid,
  620. bizDate = r.BizDate.ToString("yyyy-MM-dd"),
  621. bizMonth = r.BizMonth,
  622. triggerType = r.TriggerType,
  623. dwdRows = r.DwdRows,
  624. kpiRows = r.KpiRows,
  625. perKpiDwdRows = r.PerKpiDwdRows,
  626. perKpiKpiRows = r.PerKpiKpiRows,
  627. denominatorStatus = r.KpiDenominatorStatus,
  628. tvfPeriod = $"{r.MonthlyPeriodStart:yyyy-MM-dd}~{r.MonthlyPeriodEnd:yyyy-MM-dd}"
  629. };
  630. return JsonSerializer.Serialize(summary);
  631. }
  632. private static string NormalizeTriggerType(string s) =>
  633. string.IsNullOrWhiteSpace(s) ? "AUTO" : s.Trim().ToUpperInvariant();
  634. private static void NormalizeOption(S5MdpRefreshOption option)
  635. {
  636. var d = S5MdpRefreshOption.Default();
  637. if (option.TargetFactoryId <= 0) option.TargetFactoryId = d.TargetFactoryId;
  638. if (string.IsNullOrWhiteSpace(option.SourceZtid)) option.SourceZtid = d.SourceZtid;
  639. // 目标租户由 T8 账套(ztid)映射决定,禁止固定默认/兜底
  640. option.TargetTenantId = AidopSourceTenantMap.ResolveTenantId(option.SourceZtid, option.TargetTenantId);
  641. if (option.BizDate == default) option.BizDate = d.BizDate;
  642. if (string.IsNullOrWhiteSpace(option.BizMonth)) option.BizMonth = d.BizMonth;
  643. if (option.DailyPeriodStart == default) option.DailyPeriodStart = d.DailyPeriodStart;
  644. if (option.DailyPeriodEnd == default) option.DailyPeriodEnd = d.DailyPeriodEnd;
  645. if (option.MonthlyPeriodStart == default) option.MonthlyPeriodStart = d.MonthlyPeriodStart;
  646. if (option.MonthlyPeriodEnd == default) option.MonthlyPeriodEnd = d.MonthlyPeriodEnd;
  647. if (string.IsNullOrWhiteSpace(option.TvfPeriodStartYyyymm)) option.TvfPeriodStartYyyymm = d.TvfPeriodStartYyyymm;
  648. if (string.IsNullOrWhiteSpace(option.TvfPeriodEndYyyymm)) option.TvfPeriodEndYyyymm = d.TvfPeriodEndYyyymm;
  649. }
  650. private static string Truncate(string s, int max) =>
  651. string.IsNullOrEmpty(s) ? "" : (s.Length <= max ? s : s.Substring(0, max));
  652. }
  653. // ─────────────────────────────────────────────────────────────────────────────
  654. // Refresh 入参与结果 DTO
  655. // ─────────────────────────────────────────────────────────────────────────────
  656. public sealed class S5MdpRefreshOption
  657. {
  658. /// <summary>T8 账套(kc_tz_head.ztid);实测当前唯一账套为 pbxfxp。</summary>
  659. public string SourceZtid { get; set; } = "pbxfxp";
  660. /// <summary>KPI/DWD 落库目标租户;≤0 时由 <see cref="AidopSourceTenantMap"/> 按 SourceZtid 解析。</summary>
  661. public long TargetTenantId { get; set; }
  662. /// <summary>KPI/DWD 落库目标工厂;默认 1。</summary>
  663. public long TargetFactoryId { get; set; } = 1L;
  664. /// <summary>日 T+1 KPI 的业务日期(默认昨天)。</summary>
  665. public DateTime BizDate { get; set; }
  666. /// <summary>月 M+1 KPI 的业务月 YYYY-MM(默认上月)。</summary>
  667. public string BizMonth { get; set; } = "";
  668. /// <summary>日 T+1 KPI 区间起(含),默认昨天 00:00。</summary>
  669. public DateTime DailyPeriodStart { get; set; }
  670. /// <summary>日 T+1 KPI 区间止(含),默认昨天 23:59:59。</summary>
  671. public DateTime DailyPeriodEnd { get; set; }
  672. /// <summary>月 M+1 KPI 区间起(含),默认上月 1 日。</summary>
  673. public DateTime MonthlyPeriodStart { get; set; }
  674. /// <summary>月 M+1 KPI 区间止(含),默认上月末日。</summary>
  675. public DateTime MonthlyPeriodEnd { get; set; }
  676. /// <summary>TVF Rep_总账_存货_V3 入参起期 YYYYMM。</summary>
  677. public string TvfPeriodStartYyyymm { get; set; } = "";
  678. /// <summary>TVF Rep_总账_存货_V3 入参止期 YYYYMM。</summary>
  679. public string TvfPeriodEndYyyymm { get; set; } = "";
  680. public static S5MdpRefreshOption Default()
  681. {
  682. var today = DateTime.Today;
  683. var yesterday = today.AddDays(-1);
  684. var lastMonth = today.AddMonths(-1);
  685. var monthStart = new DateTime(lastMonth.Year, lastMonth.Month, 1);
  686. var monthEnd = monthStart.AddMonths(1).AddDays(-1);
  687. return new S5MdpRefreshOption
  688. {
  689. SourceZtid = "pbxfxp",
  690. TargetTenantId = 0,
  691. TargetFactoryId = 1L,
  692. BizDate = yesterday,
  693. BizMonth = lastMonth.ToString("yyyy-MM"),
  694. DailyPeriodStart = yesterday,
  695. DailyPeriodEnd = yesterday.AddDays(1).AddSeconds(-1),
  696. MonthlyPeriodStart = monthStart,
  697. MonthlyPeriodEnd = monthEnd.AddDays(1).AddSeconds(-1),
  698. TvfPeriodStartYyyymm = monthStart.ToString("yyyyMM"),
  699. TvfPeriodEndYyyymm = monthEnd.ToString("yyyyMM")
  700. };
  701. }
  702. }
  703. public sealed class S5MdpSyncTransformResult
  704. {
  705. public string BatchId { get; set; } = "";
  706. public long RunLogId { get; set; }
  707. public string TriggerType { get; set; } = "AUTO";
  708. public string SourceZtid { get; set; } = "";
  709. public long TargetTenantId { get; set; }
  710. public long TargetFactoryId { get; set; }
  711. public DateTime BizDate { get; set; }
  712. public string BizMonth { get; set; } = "";
  713. public DateTime DailyPeriodStart { get; set; }
  714. public DateTime DailyPeriodEnd { get; set; }
  715. public DateTime MonthlyPeriodStart { get; set; }
  716. public DateTime MonthlyPeriodEnd { get; set; }
  717. public int StageRows { get; set; }
  718. public int StandardRows { get; set; }
  719. public int DwdRows { get; set; }
  720. public int KpiRows { get; set; }
  721. public Dictionary<string, int> PerKpiDwdRows { get; } = new();
  722. public Dictionary<string, int> PerKpiKpiRows { get; } = new();
  723. public List<string> KpiDenominatorStatus { get; } = new();
  724. public void MergeSub(string kpiCode, KpiBuildSubResult sub)
  725. {
  726. PerKpiDwdRows[kpiCode] = sub.DwdRows;
  727. PerKpiKpiRows[kpiCode] = sub.KpiRows;
  728. DwdRows += sub.DwdRows;
  729. KpiRows += sub.KpiRows;
  730. KpiDenominatorStatus.Add($"{kpiCode}:{sub.DenominatorStatus}");
  731. }
  732. }
  733. public sealed class KpiBuildSubResult
  734. {
  735. public int T8Rows { get; set; }
  736. public int DwdRows { get; set; }
  737. public int KpiRows { get; set; }
  738. public string DenominatorStatus { get; set; } = "OK";
  739. }
  740. // ─────────────────────────────────────────────────────────────────────────────
  741. // T8 result set 投影类型(与方老师 SQL SELECT 列名严格一致;SqlSugar 映射)
  742. // ─────────────────────────────────────────────────────────────────────────────
  743. internal sealed class S5OnlineCycleRow
  744. {
  745. public string? code { get; set; }
  746. public DateTime? shtime { get; set; }
  747. }
  748. internal sealed class S5FulfillmentNumerRow
  749. {
  750. public string? lynoid { get; set; }
  751. public int codenum { get; set; }
  752. }
  753. internal sealed class S5FulfillmentDenomRow
  754. {
  755. public string? noid { get; set; }
  756. public int listnum { get; set; }
  757. }
  758. internal sealed class S5SumQtyRow
  759. {
  760. public decimal? slzx { get; set; }
  761. }
  762. internal sealed class S5CountRow
  763. {
  764. public int penum { get; set; }
  765. }
  766. internal sealed class S5InventoryTurnoverRow
  767. {
  768. public string? ckcode { get; set; }
  769. public string? ckname { get; set; }
  770. public string? code { get; set; }
  771. public string? cname { get; set; }
  772. public string? pcode { get; set; }
  773. public string? pname { get; set; }
  774. public decimal? je3 { get; set; }
  775. public decimal? je2 { get; set; }
  776. }