S8OrderFlowService.cs 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  1. using System.Text.Json;
  2. using Admin.NET.Plugin.AiDOP.Dto.S8.OrderFlow;
  3. using Admin.NET.Plugin.AiDOP.Entity.S8;
  4. using Admin.NET.Plugin.AiDOP.Entity.S8.OrderFlow;
  5. namespace Admin.NET.Plugin.AiDOP.Service.S8.OrderFlow;
  6. /// <summary>
  7. /// ORDER-FLOW-S8-INTEGRATED-DOMAIN-RESET-1 t3b:S8 订单执行链路只读 service skeleton。
  8. /// 仅 GetOrders / GetOrder / GetChain 三个方法;procurement-pivot / aggregate / related-exceptions 延后切片。
  9. /// 任何业务真值(订单总数 / 平均时长 / YY 真值矩阵等)只能来自 DB,本文件零硬编码。
  10. /// 异常计数:从 ado_s8_exception 实时聚合(SourceObjectType=SALES_ORDER + RelatedObjectCode in order_codes + IsDeleted=0)。
  11. /// </summary>
  12. public class S8OrderFlowService : ITransient
  13. {
  14. private const string ExceptionSourceObjectType = "SALES_ORDER";
  15. private const string ScopeBaselinePpt = "BASELINE_PPT";
  16. private const string ScopeCurrentFiltered = "CURRENT_FILTERED";
  17. private const string BaselineSnapshotCode = "CHAIN_AGGREGATE_BASELINE";
  18. private const string PivotTotalCode = "TOTAL";
  19. // ORDER-CHAIN-FINAL-ASSEMBLY-DEPT-COLLAB-MVP-1:异常类型 → 末端协同对象 映射常量。
  20. // 这些 type_code 均来自 ado_s8_exception_type seed 中 enabled=true、monitoring_category_key=FINAL_ASSEMBLY_DELIVERY 的子集;
  21. // 不新增异常类型、不依赖业务执行表。空数组对象(如 ASSEMBLY_COMPLETION / 包装材料准备)在前端展示 "--"。
  22. private const string FinalAssemblyObjectGoodsHandover = "GOODS_HANDOVER";
  23. private const string FinalAssemblyObjectShipmentConfirmation = "SHIPMENT_CONFIRMATION";
  24. private const string FinalAssemblyObjectShippingPlan = "SHIPPING_PLAN";
  25. private static readonly string[] GoodsHandoverExceptionTypes = { "PENDING_SHIPMENT" };
  26. private static readonly string[] ShipmentConfirmationExceptionTypes =
  27. {
  28. "DELIVERY_DELAY", "SHIPMENT_ABNORMAL", "DELIVERY_DELAY_WARNING", "FINISHED_GOODS_PENDING_SHIPMENT",
  29. };
  30. private static readonly string[] ShippingPlanExceptionTypes = { "FINISHED_GOODS_PENDING_SHIPMENT" };
  31. private static readonly JsonSerializerOptions JsonOpts = new()
  32. {
  33. PropertyNameCaseInsensitive = true,
  34. };
  35. private readonly SqlSugarRepository<AdoS8OrderFlowOrder> _orderRep;
  36. private readonly SqlSugarRepository<AdoS8OrderFlowStage> _stageRep;
  37. private readonly SqlSugarRepository<AdoS8OrderFlowSubstep> _substepRep;
  38. private readonly SqlSugarRepository<AdoS8OrderFlowSubstepUnit> _unitRep;
  39. private readonly SqlSugarRepository<AdoS8OrderFlowSnapshot> _snapshotRep;
  40. private readonly SqlSugarRepository<AdoS8OrderFlowProcurementPivot> _pivotRep;
  41. private readonly SqlSugarRepository<AdoS8Exception> _exceptionRep;
  42. public S8OrderFlowService(
  43. SqlSugarRepository<AdoS8OrderFlowOrder> orderRep,
  44. SqlSugarRepository<AdoS8OrderFlowStage> stageRep,
  45. SqlSugarRepository<AdoS8OrderFlowSubstep> substepRep,
  46. SqlSugarRepository<AdoS8OrderFlowSubstepUnit> unitRep,
  47. SqlSugarRepository<AdoS8OrderFlowSnapshot> snapshotRep,
  48. SqlSugarRepository<AdoS8OrderFlowProcurementPivot> pivotRep,
  49. SqlSugarRepository<AdoS8Exception> exceptionRep)
  50. {
  51. _orderRep = orderRep;
  52. _stageRep = stageRep;
  53. _substepRep = substepRep;
  54. _unitRep = unitRep;
  55. _snapshotRep = snapshotRep;
  56. _pivotRep = pivotRep;
  57. _exceptionRep = exceptionRep;
  58. }
  59. /// <summary>订单档案列表(无分页)。当前 baseline 20 单。</summary>
  60. public async Task<List<AdoS8OrderFlowOrderListItemDto>> GetOrdersAsync(AdoS8OrderFlowOrderQueryDto query)
  61. {
  62. var tenantId = query.TenantId ?? 1;
  63. var factoryId = query.FactoryId ?? 1;
  64. var orders = await _orderRep.AsQueryable()
  65. .Where(o => o.TenantId == tenantId && o.FactoryId == factoryId && !o.IsDeleted)
  66. .WhereIF(!string.IsNullOrWhiteSpace(query.Keyword),
  67. o => o.OrderCode.Contains(query.Keyword!)
  68. || o.ProductName.Contains(query.Keyword!)
  69. || o.CustomerName.Contains(query.Keyword!))
  70. .WhereIF(!string.IsNullOrWhiteSpace(query.CurrentFlowCode),
  71. o => o.CurrentOrderFlowCode == query.CurrentFlowCode!)
  72. .WhereIF(!string.IsNullOrWhiteSpace(query.CustomerCode),
  73. o => o.CustomerCode == query.CustomerCode!)
  74. .WhereIF(!string.IsNullOrWhiteSpace(query.ProductLine),
  75. o => o.ProductLine == query.ProductLine!)
  76. .WhereIF(!string.IsNullOrWhiteSpace(query.Region),
  77. o => o.Region == query.Region!)
  78. .WhereIF(!string.IsNullOrWhiteSpace(query.WorkflowStatus),
  79. o => o.WorkflowStatus == query.WorkflowStatus!)
  80. .WhereIF(!string.IsNullOrWhiteSpace(query.ScenarioCode),
  81. o => o.ScenarioCode == query.ScenarioCode!)
  82. .OrderBy(o => o.OrderCode)
  83. .ToListAsync();
  84. if (orders.Count == 0) return new List<AdoS8OrderFlowOrderListItemDto>();
  85. var orderCodes = orders.Select(o => o.OrderCode).Distinct().ToList();
  86. var orderIds = orders.Select(o => o.Id).ToList();
  87. var currentStages = await _stageRep.AsQueryable()
  88. .Where(s => s.TenantId == tenantId && s.FactoryId == factoryId && !s.IsDeleted)
  89. .Where(s => orderIds.Contains(s.OrderId))
  90. .ToListAsync();
  91. var currentStageMap = currentStages
  92. .GroupBy(s => s.OrderId)
  93. .ToDictionary(g => g.Key, g => g.ToList());
  94. var exceptionCounts = await MapExceptionCountsAsync(tenantId, factoryId, orderCodes);
  95. return orders.Select(o => MapOrderListItem(o, currentStageMap.GetValueOrDefault(o.Id), exceptionCounts)).ToList();
  96. }
  97. /// <summary>单订单详情(不含 substep/unit)。</summary>
  98. public async Task<AdoS8OrderFlowOrderDetailDto?> GetOrderAsync(string orderCode, long tenantId = 1, long factoryId = 1)
  99. {
  100. if (string.IsNullOrWhiteSpace(orderCode)) return null;
  101. var order = await _orderRep.AsQueryable()
  102. .Where(o => o.TenantId == tenantId && o.FactoryId == factoryId
  103. && !o.IsDeleted && o.OrderCode == orderCode)
  104. .FirstAsync();
  105. if (order == null) return null;
  106. var stages = await _stageRep.AsQueryable()
  107. .Where(s => s.TenantId == tenantId && s.FactoryId == factoryId
  108. && !s.IsDeleted && s.OrderId == order.Id)
  109. .OrderBy(s => s.SortNo)
  110. .ToListAsync();
  111. var exceptionCounts = await MapExceptionCountsAsync(tenantId, factoryId, new List<string> { orderCode });
  112. var listItem = MapOrderListItem(order, stages, exceptionCounts);
  113. return BuildDetail(listItem, stages, substepsByFlow: null, unitsBySubstepId: null);
  114. }
  115. /// <summary>订单链路视图:详情 + lifecycle + L2 substeps + L3 units。procurementPivot 留到 t3c。</summary>
  116. public async Task<AdoS8OrderFlowChainDto?> GetChainAsync(string orderCode, long tenantId = 1, long factoryId = 1)
  117. {
  118. if (string.IsNullOrWhiteSpace(orderCode)) return null;
  119. var order = await _orderRep.AsQueryable()
  120. .Where(o => o.TenantId == tenantId && o.FactoryId == factoryId
  121. && !o.IsDeleted && o.OrderCode == orderCode)
  122. .FirstAsync();
  123. if (order == null) return null;
  124. var stages = await _stageRep.AsQueryable()
  125. .Where(s => s.TenantId == tenantId && s.FactoryId == factoryId
  126. && !s.IsDeleted && s.OrderId == order.Id)
  127. .OrderBy(s => s.SortNo)
  128. .ToListAsync();
  129. var substeps = await _substepRep.AsQueryable()
  130. .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId
  131. && !x.IsDeleted && x.OrderId == order.Id)
  132. .OrderBy(x => x.SortNo)
  133. .ToListAsync();
  134. var substepsByFlow = substeps
  135. .GroupBy(x => x.OrderFlowCode)
  136. .ToDictionary(g => g.Key, g => g.ToList());
  137. var substepIds = substeps.Select(x => x.Id).ToList();
  138. var units = substepIds.Count == 0
  139. ? new List<AdoS8OrderFlowSubstepUnit>()
  140. : await _unitRep.AsQueryable()
  141. .Where(u => u.TenantId == tenantId && u.FactoryId == factoryId
  142. && !u.IsDeleted && substepIds.Contains(u.SubstepId))
  143. .OrderBy(u => u.SortNo)
  144. .ToListAsync();
  145. var unitsBySubstepId = units
  146. .GroupBy(u => u.SubstepId)
  147. .ToDictionary(g => g.Key, g => g.ToList());
  148. var exceptionCounts = await MapExceptionCountsAsync(tenantId, factoryId, new List<string> { orderCode });
  149. var listItem = MapOrderListItem(order, stages, exceptionCounts);
  150. var detail = BuildDetail(listItem, stages, substepsByFlow, unitsBySubstepId);
  151. return new AdoS8OrderFlowChainDto
  152. {
  153. Order = detail,
  154. ProcurementPivot = null, // t3c 切片接入
  155. };
  156. }
  157. // ─────────────── private mappers ───────────────
  158. /// <summary>从 ado_s8_exception 实时聚合每个 order_code 的异常计数 + 状态。</summary>
  159. private async Task<Dictionary<string, (int Count, string Status)>> MapExceptionCountsAsync(
  160. long tenantId, long factoryId, List<string> orderCodes)
  161. {
  162. var result = orderCodes.ToDictionary(c => c, _ => (Count: 0, Status: string.Empty));
  163. if (orderCodes.Count == 0) return result;
  164. var rows = await _exceptionRep.AsQueryable()
  165. .Where(e => e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted)
  166. .Where(e => e.SourceObjectType == ExceptionSourceObjectType)
  167. .Where(e => orderCodes.Contains(e.RelatedObjectCode!))
  168. .Select(e => new { e.RelatedObjectCode, e.Severity })
  169. .ToListAsync();
  170. foreach (var grp in rows.Where(r => !string.IsNullOrEmpty(r.RelatedObjectCode))
  171. .GroupBy(r => r.RelatedObjectCode!))
  172. {
  173. var items = grp.ToList();
  174. var status = items.Any(x => string.Equals(x.Severity, "SERIOUS", StringComparison.OrdinalIgnoreCase))
  175. ? "SERIOUS"
  176. : (items.Count > 0 ? "FOLLOW" : string.Empty);
  177. result[grp.Key] = (items.Count, status);
  178. }
  179. return result;
  180. }
  181. private static AdoS8OrderFlowOrderListItemDto MapOrderListItem(
  182. AdoS8OrderFlowOrder o,
  183. List<AdoS8OrderFlowStage>? lifecycle,
  184. Dictionary<string, (int Count, string Status)> exceptionCounts)
  185. {
  186. var current = lifecycle?.FirstOrDefault(s => s.OrderFlowCode == o.CurrentOrderFlowCode);
  187. var counts = exceptionCounts.TryGetValue(o.OrderCode, out var c) ? c : (Count: 0, Status: string.Empty);
  188. return new AdoS8OrderFlowOrderListItemDto
  189. {
  190. OrderCode = o.OrderCode,
  191. ProductName = o.ProductName,
  192. ProductLine = o.ProductLine,
  193. CustomerCode = o.CustomerCode,
  194. CustomerName = o.CustomerName,
  195. CustomerType = o.CustomerType,
  196. Region = o.Region,
  197. Priority = o.Priority,
  198. WorkflowStatus = o.WorkflowStatus,
  199. CurrentOrderFlowCode = o.CurrentOrderFlowCode,
  200. CurrentOrderFlowName = OrderFlowConstants.ResolveName(o.CurrentOrderFlowCode),
  201. CurrentStatus = current?.Status ?? string.Empty,
  202. ReleaseAt = o.ReleaseAt,
  203. TargetCycleDays = o.TargetCycleDays,
  204. ActualCycleDays = o.ActualCycleDays,
  205. CurrentCycleDays = current?.ActualDays,
  206. NodeVarianceDays = current?.NodeVarianceDays,
  207. CumulativeVarianceDays = current?.CumulativeVarianceDays,
  208. ExceptionCount = counts.Count,
  209. ExceptionStatus = counts.Status,
  210. ResponseMinutes = o.ResponseMinutes,
  211. ProcessingMinutes = o.ProcessingMinutes,
  212. TotalLossMinutes = o.TotalLossMinutes,
  213. ScenarioCode = o.ScenarioCode,
  214. DataSource = o.DataSource,
  215. };
  216. }
  217. private static AdoS8OrderFlowOrderDetailDto BuildDetail(
  218. AdoS8OrderFlowOrderListItemDto listItem,
  219. List<AdoS8OrderFlowStage> stages,
  220. Dictionary<string, List<AdoS8OrderFlowSubstep>>? substepsByFlow,
  221. Dictionary<long, List<AdoS8OrderFlowSubstepUnit>>? unitsBySubstepId)
  222. {
  223. var detail = new AdoS8OrderFlowOrderDetailDto
  224. {
  225. OrderCode = listItem.OrderCode,
  226. ProductName = listItem.ProductName,
  227. ProductLine = listItem.ProductLine,
  228. CustomerCode = listItem.CustomerCode,
  229. CustomerName = listItem.CustomerName,
  230. CustomerType = listItem.CustomerType,
  231. Region = listItem.Region,
  232. Priority = listItem.Priority,
  233. WorkflowStatus = listItem.WorkflowStatus,
  234. CurrentOrderFlowCode = listItem.CurrentOrderFlowCode,
  235. CurrentOrderFlowName = listItem.CurrentOrderFlowName,
  236. CurrentStatus = listItem.CurrentStatus,
  237. ReleaseAt = listItem.ReleaseAt,
  238. TargetCycleDays = listItem.TargetCycleDays,
  239. ActualCycleDays = listItem.ActualCycleDays,
  240. CurrentCycleDays = listItem.CurrentCycleDays,
  241. NodeVarianceDays = listItem.NodeVarianceDays,
  242. CumulativeVarianceDays = listItem.CumulativeVarianceDays,
  243. ExceptionCount = listItem.ExceptionCount,
  244. ExceptionStatus = listItem.ExceptionStatus,
  245. ResponseMinutes = listItem.ResponseMinutes,
  246. ProcessingMinutes = listItem.ProcessingMinutes,
  247. TotalLossMinutes = listItem.TotalLossMinutes,
  248. ScenarioCode = listItem.ScenarioCode,
  249. DataSource = listItem.DataSource,
  250. Lifecycle = stages.Select(s => MapStage(s, substepsByFlow, unitsBySubstepId)).ToList(),
  251. };
  252. return detail;
  253. }
  254. private static AdoS8OrderFlowStageDto MapStage(
  255. AdoS8OrderFlowStage s,
  256. Dictionary<string, List<AdoS8OrderFlowSubstep>>? substepsByFlow,
  257. Dictionary<long, List<AdoS8OrderFlowSubstepUnit>>? unitsBySubstepId)
  258. {
  259. var substeps = substepsByFlow != null && substepsByFlow.TryGetValue(s.OrderFlowCode, out var list)
  260. ? list.Select(x => MapSubstep(x, unitsBySubstepId)).ToList()
  261. : new List<AdoS8OrderFlowSubstepDto>();
  262. return new AdoS8OrderFlowStageDto
  263. {
  264. OrderFlowCode = s.OrderFlowCode,
  265. OrderFlowName = string.IsNullOrEmpty(s.OrderFlowName)
  266. ? OrderFlowConstants.ResolveName(s.OrderFlowCode)
  267. : s.OrderFlowName,
  268. OwnerDept = s.OwnerDept,
  269. SortNo = s.SortNo,
  270. PlannedDays = s.PlannedDays,
  271. ActualDays = s.ActualDays,
  272. TargetAt = s.TargetAt,
  273. ActualStartAt = s.ActualStartAt,
  274. ActualEndAt = s.ActualEndAt,
  275. Status = s.Status,
  276. NodeVarianceDays = s.NodeVarianceDays,
  277. CumulativeVarianceDays = s.CumulativeVarianceDays,
  278. Substeps = substeps,
  279. };
  280. }
  281. private static AdoS8OrderFlowSubstepDto MapSubstep(
  282. AdoS8OrderFlowSubstep x,
  283. Dictionary<long, List<AdoS8OrderFlowSubstepUnit>>? unitsBySubstepId)
  284. {
  285. var units = unitsBySubstepId != null && unitsBySubstepId.TryGetValue(x.Id, out var list)
  286. ? list.Select(MapUnit).ToList()
  287. : new List<AdoS8OrderFlowSubstepUnitDto>();
  288. return new AdoS8OrderFlowSubstepDto
  289. {
  290. SubstepCode = x.SubstepCode,
  291. SubstepName = x.SubstepName,
  292. PiHours = x.PiHours,
  293. ActualHours = x.ActualHours,
  294. Status = x.Status,
  295. SortNo = x.SortNo,
  296. Units = units,
  297. };
  298. }
  299. private static AdoS8OrderFlowSubstepUnitDto MapUnit(AdoS8OrderFlowSubstepUnit u)
  300. => new()
  301. {
  302. UnitCode = u.UnitCode,
  303. UnitName = u.UnitName,
  304. PiHours = u.PiHours,
  305. ActualHours = u.ActualHours,
  306. Status = u.Status,
  307. SortNo = u.SortNo,
  308. };
  309. // ──────────────────────────────────────────────────────────────────────
  310. // t3c:Aggregate + ProcurementPivot
  311. // ──────────────────────────────────────────────────────────────────────
  312. /// <summary>
  313. /// 链路聚合视图:
  314. /// scope=BASELINE_PPT → 只读 ado_s8_order_flow_snapshot.stage_snapshots_json
  315. /// scope=CURRENT_FILTERED → 从 order + stage 实时聚合
  316. /// snapshot 缺失时返回空骨架(不做 fallback 硬编码)。
  317. /// </summary>
  318. public async Task<AdoS8OrderFlowAggregateDto> GetAggregateAsync(AdoS8OrderFlowAggregateQueryDto query)
  319. {
  320. var tenantId = query.TenantId ?? 1;
  321. var factoryId = query.FactoryId ?? 1;
  322. var scope = string.IsNullOrWhiteSpace(query.Scope) ? ScopeBaselinePpt : query.Scope;
  323. if (string.Equals(scope, ScopeBaselinePpt, StringComparison.OrdinalIgnoreCase))
  324. return await BuildBaselineAggregateAsync(tenantId, factoryId, scope);
  325. if (string.Equals(scope, ScopeCurrentFiltered, StringComparison.OrdinalIgnoreCase))
  326. return await BuildCurrentAggregateAsync(tenantId, factoryId, scope, query.OrderCodes);
  327. return EmptyAggregate(scope);
  328. }
  329. private async Task<AdoS8OrderFlowAggregateDto> BuildBaselineAggregateAsync(long tenantId, long factoryId, string scope)
  330. {
  331. var snapshot = await _snapshotRep.AsQueryable()
  332. .Where(s => s.TenantId == tenantId && s.FactoryId == factoryId && !s.IsDeleted)
  333. .Where(s => s.ScopeCode == ScopeBaselinePpt && s.SnapshotCode == BaselineSnapshotCode)
  334. .FirstAsync();
  335. if (snapshot == null) return EmptyAggregate(scope);
  336. var stages = ParseStageSnapshots(snapshot.StageSnapshotsJson);
  337. var collab = await BuildFinalAssemblyCollabSummaryAsync(tenantId, factoryId, orderCodes: null);
  338. return new AdoS8OrderFlowAggregateDto
  339. {
  340. Scope = scope,
  341. TotalOrders = snapshot.TotalOrders,
  342. TotalCustomers = snapshot.TotalCustomers,
  343. AvgResponseMinutes = snapshot.AvgResponseMinutes,
  344. AvgProcessingMinutes = snapshot.AvgProcessingMinutes,
  345. AvgLossMinutes = snapshot.AvgLossMinutes,
  346. StageAggregates = ReorderByCanonical(stages),
  347. FinalAssemblyCollabSummary = collab,
  348. };
  349. }
  350. private async Task<AdoS8OrderFlowAggregateDto> BuildCurrentAggregateAsync(
  351. long tenantId, long factoryId, string scope, List<string>? orderCodes)
  352. {
  353. var orders = await _orderRep.AsQueryable()
  354. .Where(o => o.TenantId == tenantId && o.FactoryId == factoryId && !o.IsDeleted)
  355. .WhereIF(orderCodes != null && orderCodes.Count > 0, o => orderCodes!.Contains(o.OrderCode))
  356. .ToListAsync();
  357. if (orders.Count == 0) return EmptyAggregate(scope);
  358. var orderIds = orders.Select(o => o.Id).ToList();
  359. var stages = await _stageRep.AsQueryable()
  360. .Where(s => s.TenantId == tenantId && s.FactoryId == factoryId && !s.IsDeleted)
  361. .Where(s => orderIds.Contains(s.OrderId))
  362. .ToListAsync();
  363. var responseList = orders.Where(o => o.ResponseMinutes.HasValue).Select(o => (decimal)o.ResponseMinutes!.Value).ToList();
  364. var processingList = orders.Where(o => o.ProcessingMinutes.HasValue).Select(o => (decimal)o.ProcessingMinutes!.Value).ToList();
  365. var lossList = orders.Where(o => o.TotalLossMinutes.HasValue).Select(o => (decimal)o.TotalLossMinutes!.Value).ToList();
  366. var stagesByFlow = stages.GroupBy(s => s.OrderFlowCode).ToDictionary(g => g.Key, g => g.ToList());
  367. var stageAggregates = new List<AdoS8OrderFlowStageAggregateDto>();
  368. foreach (var code in OrderFlowConstants.All)
  369. {
  370. stagesByFlow.TryGetValue(code, out var rows);
  371. stageAggregates.Add(BuildStageAggregateFromRows(code, rows ?? new List<AdoS8OrderFlowStage>()));
  372. }
  373. var collab = await BuildFinalAssemblyCollabSummaryAsync(tenantId, factoryId, orderCodes);
  374. return new AdoS8OrderFlowAggregateDto
  375. {
  376. Scope = scope,
  377. TotalOrders = orders.Count,
  378. TotalCustomers = orders.Select(o => o.CustomerCode).Where(c => !string.IsNullOrEmpty(c)).Distinct().Count(),
  379. AvgResponseMinutes = AvgOrZero(responseList),
  380. AvgProcessingMinutes = AvgOrZero(processingList),
  381. AvgLossMinutes = AvgOrZero(lossList),
  382. StageAggregates = stageAggregates,
  383. FinalAssemblyCollabSummary = collab,
  384. };
  385. }
  386. // ORDER-CHAIN-FINAL-ASSEMBLY-DEPT-COLLAB-MVP-1:FINAL_ASSEMBLY_DELIVERY 末端协同派生。
  387. // 仅查 ado_s8_exception 现有数据;只覆盖有真实映射的 2 个主门禁 + 1 个并行准备,其它 6 个对象前端展示 "--"。
  388. // 不读取 S0|S4 业务表;不新增表 / seed / 异常类型。
  389. private async Task<AdoS8FinalAssemblyCollabSummaryDto> BuildFinalAssemblyCollabSummaryAsync(
  390. long tenantId, long factoryId, List<string>? orderCodes)
  391. {
  392. var allTypes = GoodsHandoverExceptionTypes
  393. .Concat(ShipmentConfirmationExceptionTypes)
  394. .Concat(ShippingPlanExceptionTypes)
  395. .Distinct()
  396. .ToList();
  397. var rows = await _exceptionRep.AsQueryable()
  398. .Where(e => e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted)
  399. .Where(e => e.SourceObjectType == ExceptionSourceObjectType)
  400. .Where(e => e.ExceptionTypeCode != null && allTypes.Contains(e.ExceptionTypeCode!))
  401. .WhereIF(orderCodes != null && orderCodes.Count > 0,
  402. e => e.RelatedObjectCode != null && orderCodes!.Contains(e.RelatedObjectCode!))
  403. .Select(e => new ExceptionAttribution
  404. {
  405. ExceptionTypeCode = e.ExceptionTypeCode,
  406. RelatedObjectCode = e.RelatedObjectCode,
  407. Title = e.Title,
  408. Severity = e.Severity,
  409. CreatedAt = e.CreatedAt,
  410. })
  411. .ToListAsync();
  412. return new AdoS8FinalAssemblyCollabSummaryDto
  413. {
  414. Gates = new Dictionary<string, AdoS8FinalAssemblyObjectStatDto>
  415. {
  416. [FinalAssemblyObjectGoodsHandover] = BuildObjectStat(rows, GoodsHandoverExceptionTypes, isGate: true),
  417. [FinalAssemblyObjectShipmentConfirmation] = BuildObjectStat(rows, ShipmentConfirmationExceptionTypes, isGate: true),
  418. },
  419. Parallels = new Dictionary<string, AdoS8FinalAssemblyObjectStatDto>
  420. {
  421. [FinalAssemblyObjectShippingPlan] = BuildObjectStat(rows, ShippingPlanExceptionTypes, isGate: false),
  422. },
  423. };
  424. }
  425. private static AdoS8FinalAssemblyObjectStatDto BuildObjectStat(
  426. List<ExceptionAttribution> rows, string[] candidateTypes, bool isGate)
  427. {
  428. var subset = rows.Where(r => r.ExceptionTypeCode != null && candidateTypes.Contains(r.ExceptionTypeCode!)).ToList();
  429. var count = subset
  430. .Where(r => !string.IsNullOrEmpty(r.RelatedObjectCode))
  431. .Select(r => r.RelatedObjectCode!)
  432. .Distinct()
  433. .Count();
  434. var top = subset
  435. .OrderByDescending(r => string.Equals(r.Severity, "SERIOUS", StringComparison.OrdinalIgnoreCase) ? 1 : 0)
  436. .ThenByDescending(r => r.CreatedAt)
  437. .FirstOrDefault();
  438. return new AdoS8FinalAssemblyObjectStatDto
  439. {
  440. ImpactedOrderCount = isGate ? count : (int?)null,
  441. RiskOrderCount = isGate ? (int?)null : count,
  442. TopRiskTitle = top?.Title,
  443. TopRiskSeverity = top?.Severity,
  444. };
  445. }
  446. private sealed class ExceptionAttribution
  447. {
  448. public string? ExceptionTypeCode { get; set; }
  449. public string? RelatedObjectCode { get; set; }
  450. public string? Title { get; set; }
  451. public string? Severity { get; set; }
  452. public DateTime CreatedAt { get; set; }
  453. }
  454. private static AdoS8OrderFlowStageAggregateDto BuildStageAggregateFromRows(string flowCode, List<AdoS8OrderFlowStage> rows)
  455. {
  456. var dto = new AdoS8OrderFlowStageAggregateDto
  457. {
  458. OrderFlowCode = flowCode,
  459. OrderFlowName = OrderFlowConstants.ResolveName(flowCode),
  460. };
  461. if (rows.Count == 0) return dto;
  462. var plannedList = rows.Select(r => r.PlannedDays).ToList();
  463. var actualList = rows.Where(r => r.ActualDays.HasValue && !string.Equals(r.Status, "pending", StringComparison.OrdinalIgnoreCase))
  464. .Select(r => r.ActualDays!.Value).ToList();
  465. var green = rows.Count(r => string.Equals(r.Status, "green", StringComparison.OrdinalIgnoreCase));
  466. var yellow = rows.Count(r => string.Equals(r.Status, "yellow", StringComparison.OrdinalIgnoreCase));
  467. var red = rows.Count(r => string.Equals(r.Status, "red", StringComparison.OrdinalIgnoreCase));
  468. var pending = rows.Count(r => string.Equals(r.Status, "pending", StringComparison.OrdinalIgnoreCase));
  469. var total = rows.Count;
  470. dto.KpiAvgDays = AvgOrZero(plannedList);
  471. dto.ActualAvgDays = AvgOrZero(actualList);
  472. dto.Green = green;
  473. dto.Yellow = yellow;
  474. dto.Red = red;
  475. dto.Pending = pending;
  476. // 整数百分比;total 不含 0 因为上面已 short-circuit。 percent 因子 100 是公式系数,非业务真值。
  477. dto.OnTimeRate = total > 0 ? (int)Math.Round(green * 100.0 / total) : 0;
  478. return dto;
  479. }
  480. private static List<AdoS8OrderFlowStageAggregateDto> ParseStageSnapshots(string? json)
  481. {
  482. if (string.IsNullOrWhiteSpace(json)) return new List<AdoS8OrderFlowStageAggregateDto>();
  483. try
  484. {
  485. var parsed = JsonSerializer.Deserialize<List<AdoS8OrderFlowStageAggregateDto>>(json, JsonOpts);
  486. return parsed ?? new List<AdoS8OrderFlowStageAggregateDto>();
  487. }
  488. catch (JsonException)
  489. {
  490. return new List<AdoS8OrderFlowStageAggregateDto>();
  491. }
  492. }
  493. /// <summary>按 OrderFlowConstants.All 重排;缺失 code 补空骨架;多余 code 追加在尾部以避免静默丢数据。</summary>
  494. private static List<AdoS8OrderFlowStageAggregateDto> ReorderByCanonical(List<AdoS8OrderFlowStageAggregateDto> input)
  495. {
  496. var byCode = input.Where(x => !string.IsNullOrEmpty(x.OrderFlowCode))
  497. .GroupBy(x => x.OrderFlowCode)
  498. .ToDictionary(g => g.Key, g => g.First());
  499. var ordered = new List<AdoS8OrderFlowStageAggregateDto>();
  500. foreach (var code in OrderFlowConstants.All)
  501. {
  502. if (byCode.TryGetValue(code, out var hit))
  503. {
  504. if (string.IsNullOrEmpty(hit.OrderFlowName))
  505. hit.OrderFlowName = OrderFlowConstants.ResolveName(code);
  506. ordered.Add(hit);
  507. byCode.Remove(code);
  508. }
  509. else
  510. {
  511. ordered.Add(new AdoS8OrderFlowStageAggregateDto
  512. {
  513. OrderFlowCode = code,
  514. OrderFlowName = OrderFlowConstants.ResolveName(code),
  515. });
  516. }
  517. }
  518. ordered.AddRange(byCode.Values);
  519. return ordered;
  520. }
  521. private static AdoS8OrderFlowAggregateDto EmptyAggregate(string scope)
  522. {
  523. var stages = OrderFlowConstants.All
  524. .Select(code => new AdoS8OrderFlowStageAggregateDto
  525. {
  526. OrderFlowCode = code,
  527. OrderFlowName = OrderFlowConstants.ResolveName(code),
  528. })
  529. .ToList();
  530. return new AdoS8OrderFlowAggregateDto { Scope = scope, StageAggregates = stages };
  531. }
  532. private static decimal AvgOrZero(List<decimal> values)
  533. => values.Count == 0 ? 0m : values.Average();
  534. /// <summary>
  535. /// 采购透视视图:
  536. /// scope=BASELINE_PPT → 读 ado_s8_order_flow_procurement_pivot baseline 行(order_id=NULL)
  537. /// status 严格只读 DB;cycle_days 保持 decimal(6,3) 不截断。
  538. /// 本切片正式只支持 BASELINE_PPT;其他 scope 返回空骨架并标注 scope。
  539. /// </summary>
  540. public async Task<AdoS8OrderFlowProcurementPivotDto> GetProcurementPivotAsync(AdoS8OrderFlowProcurementPivotQueryDto query)
  541. {
  542. var tenantId = query.TenantId ?? 1;
  543. var factoryId = query.FactoryId ?? 1;
  544. var scope = string.IsNullOrWhiteSpace(query.Scope) ? ScopeBaselinePpt : query.Scope;
  545. if (!string.Equals(scope, ScopeBaselinePpt, StringComparison.OrdinalIgnoreCase))
  546. return new AdoS8OrderFlowProcurementPivotDto { Scope = scope };
  547. var rows = await _pivotRep.AsQueryable()
  548. .Where(p => p.TenantId == tenantId && p.FactoryId == factoryId && !p.IsDeleted)
  549. .Where(p => p.OrderId == null) // baseline 行
  550. .ToListAsync();
  551. return BuildPivot(scope, rows);
  552. }
  553. private static AdoS8OrderFlowProcurementPivotDto BuildPivot(string scope, List<AdoS8OrderFlowProcurementPivot> rows)
  554. {
  555. var result = new AdoS8OrderFlowProcurementPivotDto { Scope = scope };
  556. if (rows.Count == 0) return result;
  557. var byMaterial = rows.GroupBy(r => r.MaterialCode)
  558. .ToDictionary(g => g.Key, g => g.ToList());
  559. foreach (var (material, list) in byMaterial.OrderBy(kv => kv.Key, StringComparer.Ordinal))
  560. {
  561. // KeyMaterials:取 (supplier=TOTAL, spec=TOTAL) 单元作为该 material 的概览
  562. var grand = list.FirstOrDefault(r => r.SupplierCode == PivotTotalCode && r.SpecCode == PivotTotalCode);
  563. if (grand != null)
  564. {
  565. result.KeyMaterials.Add(new AdoS8OrderFlowKeyMaterialDto
  566. {
  567. MaterialCode = material,
  568. AvgCycleDays = grand.CycleDays,
  569. CycleStatus = grand.Status,
  570. ResultStatus = grand.Status,
  571. });
  572. }
  573. // SupplierBreakdown:spec=TOTAL 的所有 supplier 行(含 supplier=TOTAL)
  574. foreach (var row in list.Where(r => r.SpecCode == PivotTotalCode)
  575. .OrderBy(r => r.SupplierCode == PivotTotalCode ? 1 : 0)
  576. .ThenBy(r => r.SupplierCode, StringComparer.Ordinal))
  577. {
  578. result.SupplierBreakdown.Add(new AdoS8OrderFlowSupplierBreakdownDto
  579. {
  580. MaterialCode = material,
  581. SupplierCode = row.SupplierCode,
  582. AvgCycleDays = row.CycleDays,
  583. Status = row.Status,
  584. });
  585. }
  586. // SpecBreakdown:supplier=TOTAL 的所有 spec 行(含 spec=TOTAL)
  587. foreach (var row in list.Where(r => r.SupplierCode == PivotTotalCode)
  588. .OrderBy(r => r.SpecCode == PivotTotalCode ? 1 : 0)
  589. .ThenBy(r => r.SpecCode, StringComparer.Ordinal))
  590. {
  591. result.SpecBreakdown.Add(new AdoS8OrderFlowSpecBreakdownDto
  592. {
  593. MaterialCode = material,
  594. SpecCode = row.SpecCode,
  595. AvgCycleDays = row.CycleDays,
  596. Status = row.Status,
  597. });
  598. }
  599. // MatrixByMaterial:每个 supplier 一行,cells keyed by spec_code(保留全部 cycle_days 精度)
  600. var matrix = list.GroupBy(r => r.SupplierCode)
  601. .OrderBy(g => g.Key == PivotTotalCode ? 1 : 0)
  602. .ThenBy(g => g.Key, StringComparer.Ordinal)
  603. .Select(g => new AdoS8OrderFlowProcurementMatrixRowDto
  604. {
  605. SupplierCode = g.Key,
  606. Cells = g.ToDictionary(
  607. r => r.SpecCode,
  608. r => new AdoS8OrderFlowProcurementMatrixCellDto
  609. {
  610. CycleDays = r.CycleDays,
  611. Status = r.Status,
  612. }),
  613. })
  614. .ToList();
  615. result.MatrixByMaterial[material] = matrix;
  616. }
  617. return result;
  618. }
  619. }