S8OrderFlowService.cs 62 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311
  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. private readonly SqlSugarRepository<AdoS8OrderFlowProductDesignDrawing> _pddRep;
  43. // S8-ORDER-CHAIN-BODY-PRODUCTION-ORDER-LEVEL-SEED-FIX-1:本体生产 3 表 repository。
  44. private readonly SqlSugarRepository<AdoS8OrderFlowManufacturingProcess> _mfgProcessRep;
  45. private readonly SqlSugarRepository<AdoS8OrderFlowManufacturingLossFactor> _mfgLossRep;
  46. private readonly SqlSugarRepository<AdoS8OrderFlowManufacturingOperator> _mfgOperatorRep;
  47. public S8OrderFlowService(
  48. SqlSugarRepository<AdoS8OrderFlowOrder> orderRep,
  49. SqlSugarRepository<AdoS8OrderFlowStage> stageRep,
  50. SqlSugarRepository<AdoS8OrderFlowSubstep> substepRep,
  51. SqlSugarRepository<AdoS8OrderFlowSubstepUnit> unitRep,
  52. SqlSugarRepository<AdoS8OrderFlowSnapshot> snapshotRep,
  53. SqlSugarRepository<AdoS8OrderFlowProcurementPivot> pivotRep,
  54. SqlSugarRepository<AdoS8Exception> exceptionRep,
  55. SqlSugarRepository<AdoS8OrderFlowProductDesignDrawing> pddRep,
  56. SqlSugarRepository<AdoS8OrderFlowManufacturingProcess> mfgProcessRep,
  57. SqlSugarRepository<AdoS8OrderFlowManufacturingLossFactor> mfgLossRep,
  58. SqlSugarRepository<AdoS8OrderFlowManufacturingOperator> mfgOperatorRep)
  59. {
  60. _orderRep = orderRep;
  61. _stageRep = stageRep;
  62. _substepRep = substepRep;
  63. _unitRep = unitRep;
  64. _snapshotRep = snapshotRep;
  65. _pivotRep = pivotRep;
  66. _exceptionRep = exceptionRep;
  67. _pddRep = pddRep;
  68. _mfgProcessRep = mfgProcessRep;
  69. _mfgLossRep = mfgLossRep;
  70. _mfgOperatorRep = mfgOperatorRep;
  71. }
  72. /// <summary>订单档案列表(无分页)。当前 baseline 20 单。</summary>
  73. public async Task<List<AdoS8OrderFlowOrderListItemDto>> GetOrdersAsync(AdoS8OrderFlowOrderQueryDto query)
  74. {
  75. var tenantId = query.TenantId ?? 1;
  76. var factoryId = query.FactoryId ?? 1;
  77. var orders = await _orderRep.AsQueryable()
  78. .Where(o => o.TenantId == tenantId && o.FactoryId == factoryId && !o.IsDeleted)
  79. .WhereIF(!string.IsNullOrWhiteSpace(query.Keyword),
  80. o => o.OrderCode.Contains(query.Keyword!)
  81. || o.ProductName.Contains(query.Keyword!)
  82. || o.CustomerName.Contains(query.Keyword!))
  83. .WhereIF(!string.IsNullOrWhiteSpace(query.CurrentFlowCode),
  84. o => o.CurrentOrderFlowCode == query.CurrentFlowCode!)
  85. .WhereIF(!string.IsNullOrWhiteSpace(query.CustomerCode),
  86. o => o.CustomerCode == query.CustomerCode!)
  87. .WhereIF(!string.IsNullOrWhiteSpace(query.ProductLine),
  88. o => o.ProductLine == query.ProductLine!)
  89. .WhereIF(!string.IsNullOrWhiteSpace(query.Region),
  90. o => o.Region == query.Region!)
  91. .WhereIF(!string.IsNullOrWhiteSpace(query.WorkflowStatus),
  92. o => o.WorkflowStatus == query.WorkflowStatus!)
  93. .WhereIF(!string.IsNullOrWhiteSpace(query.ScenarioCode),
  94. o => o.ScenarioCode == query.ScenarioCode!)
  95. .OrderBy(o => o.OrderCode)
  96. .ToListAsync();
  97. if (orders.Count == 0) return new List<AdoS8OrderFlowOrderListItemDto>();
  98. var orderCodes = orders.Select(o => o.OrderCode).Distinct().ToList();
  99. var orderIds = orders.Select(o => o.Id).ToList();
  100. var currentStages = await _stageRep.AsQueryable()
  101. .Where(s => s.TenantId == tenantId && s.FactoryId == factoryId && !s.IsDeleted)
  102. .Where(s => orderIds.Contains(s.OrderId))
  103. .ToListAsync();
  104. var currentStageMap = currentStages
  105. .GroupBy(s => s.OrderId)
  106. .ToDictionary(g => g.Key, g => g.ToList());
  107. var exceptionCounts = await MapExceptionCountsAsync(tenantId, factoryId, orderCodes);
  108. return orders.Select(o => MapOrderListItem(o, currentStageMap.GetValueOrDefault(o.Id), exceptionCounts)).ToList();
  109. }
  110. /// <summary>单订单详情(不含 substep/unit)。</summary>
  111. public async Task<AdoS8OrderFlowOrderDetailDto?> GetOrderAsync(string orderCode, long tenantId = 1, long factoryId = 1)
  112. {
  113. if (string.IsNullOrWhiteSpace(orderCode)) return null;
  114. var order = await _orderRep.AsQueryable()
  115. .Where(o => o.TenantId == tenantId && o.FactoryId == factoryId
  116. && !o.IsDeleted && o.OrderCode == orderCode)
  117. .FirstAsync();
  118. if (order == null) return null;
  119. var stages = await _stageRep.AsQueryable()
  120. .Where(s => s.TenantId == tenantId && s.FactoryId == factoryId
  121. && !s.IsDeleted && s.OrderId == order.Id)
  122. .OrderBy(s => s.SortNo)
  123. .ToListAsync();
  124. var exceptionCounts = await MapExceptionCountsAsync(tenantId, factoryId, new List<string> { orderCode });
  125. var listItem = MapOrderListItem(order, stages, exceptionCounts);
  126. return BuildDetail(listItem, stages, substepsByFlow: null, unitsBySubstepId: null);
  127. }
  128. /// <summary>订单链路视图:详情 + lifecycle + L2 substeps + L3 units。procurementPivot 留到 t3c。</summary>
  129. public async Task<AdoS8OrderFlowChainDto?> GetChainAsync(string orderCode, long tenantId = 1, long factoryId = 1)
  130. {
  131. if (string.IsNullOrWhiteSpace(orderCode)) return null;
  132. var order = await _orderRep.AsQueryable()
  133. .Where(o => o.TenantId == tenantId && o.FactoryId == factoryId
  134. && !o.IsDeleted && o.OrderCode == orderCode)
  135. .FirstAsync();
  136. if (order == null) return null;
  137. var stages = await _stageRep.AsQueryable()
  138. .Where(s => s.TenantId == tenantId && s.FactoryId == factoryId
  139. && !s.IsDeleted && s.OrderId == order.Id)
  140. .OrderBy(s => s.SortNo)
  141. .ToListAsync();
  142. var substeps = await _substepRep.AsQueryable()
  143. .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId
  144. && !x.IsDeleted && x.OrderId == order.Id)
  145. .OrderBy(x => x.SortNo)
  146. .ToListAsync();
  147. var substepsByFlow = substeps
  148. .GroupBy(x => x.OrderFlowCode)
  149. .ToDictionary(g => g.Key, g => g.ToList());
  150. var substepIds = substeps.Select(x => x.Id).ToList();
  151. var units = substepIds.Count == 0
  152. ? new List<AdoS8OrderFlowSubstepUnit>()
  153. : await _unitRep.AsQueryable()
  154. .Where(u => u.TenantId == tenantId && u.FactoryId == factoryId
  155. && !u.IsDeleted && substepIds.Contains(u.SubstepId))
  156. .OrderBy(u => u.SortNo)
  157. .ToListAsync();
  158. var unitsBySubstepId = units
  159. .GroupBy(u => u.SubstepId)
  160. .ToDictionary(g => g.Key, g => g.ToList());
  161. var exceptionCounts = await MapExceptionCountsAsync(tenantId, factoryId, new List<string> { orderCode });
  162. var listItem = MapOrderListItem(order, stages, exceptionCounts);
  163. var detail = BuildDetail(listItem, stages, substepsByFlow, unitsBySubstepId);
  164. return new AdoS8OrderFlowChainDto
  165. {
  166. Order = detail,
  167. ProcurementPivot = null, // t3c 切片接入
  168. };
  169. }
  170. // ─────────────── private mappers ───────────────
  171. /// <summary>从 ado_s8_exception 实时聚合每个 order_code 的异常计数 + 状态。</summary>
  172. private async Task<Dictionary<string, (int Count, string Status)>> MapExceptionCountsAsync(
  173. long tenantId, long factoryId, List<string> orderCodes)
  174. {
  175. var result = orderCodes.ToDictionary(c => c, _ => (Count: 0, Status: string.Empty));
  176. if (orderCodes.Count == 0) return result;
  177. var rows = await _exceptionRep.AsQueryable()
  178. .Where(e => e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted)
  179. .Where(e => e.SourceObjectType == ExceptionSourceObjectType)
  180. .Where(e => orderCodes.Contains(e.RelatedObjectCode!))
  181. .Select(e => new { e.RelatedObjectCode, e.Severity })
  182. .ToListAsync();
  183. foreach (var grp in rows.Where(r => !string.IsNullOrEmpty(r.RelatedObjectCode))
  184. .GroupBy(r => r.RelatedObjectCode!))
  185. {
  186. var items = grp.ToList();
  187. var status = items.Any(x => string.Equals(x.Severity, "SERIOUS", StringComparison.OrdinalIgnoreCase))
  188. ? "SERIOUS"
  189. : (items.Count > 0 ? "FOLLOW" : string.Empty);
  190. result[grp.Key] = (items.Count, status);
  191. }
  192. return result;
  193. }
  194. private static AdoS8OrderFlowOrderListItemDto MapOrderListItem(
  195. AdoS8OrderFlowOrder o,
  196. List<AdoS8OrderFlowStage>? lifecycle,
  197. Dictionary<string, (int Count, string Status)> exceptionCounts)
  198. {
  199. var current = lifecycle?.FirstOrDefault(s => s.OrderFlowCode == o.CurrentOrderFlowCode);
  200. var counts = exceptionCounts.TryGetValue(o.OrderCode, out var c) ? c : (Count: 0, Status: string.Empty);
  201. return new AdoS8OrderFlowOrderListItemDto
  202. {
  203. OrderCode = o.OrderCode,
  204. ProductName = o.ProductName,
  205. ProductLine = o.ProductLine,
  206. CustomerCode = o.CustomerCode,
  207. CustomerName = o.CustomerName,
  208. CustomerType = o.CustomerType,
  209. Region = o.Region,
  210. Priority = o.Priority,
  211. WorkflowStatus = o.WorkflowStatus,
  212. CurrentOrderFlowCode = o.CurrentOrderFlowCode,
  213. CurrentOrderFlowName = OrderFlowConstants.ResolveName(o.CurrentOrderFlowCode),
  214. CurrentStatus = current?.Status ?? string.Empty,
  215. ReleaseAt = o.ReleaseAt,
  216. TargetCycleDays = o.TargetCycleDays,
  217. ActualCycleDays = o.ActualCycleDays,
  218. CurrentCycleDays = current?.ActualDays,
  219. NodeVarianceDays = current?.NodeVarianceDays,
  220. CumulativeVarianceDays = current?.CumulativeVarianceDays,
  221. ExceptionCount = counts.Count,
  222. ExceptionStatus = counts.Status,
  223. ResponseMinutes = o.ResponseMinutes,
  224. ProcessingMinutes = o.ProcessingMinutes,
  225. TotalLossMinutes = o.TotalLossMinutes,
  226. ScenarioCode = o.ScenarioCode,
  227. DataSource = o.DataSource,
  228. };
  229. }
  230. private static AdoS8OrderFlowOrderDetailDto BuildDetail(
  231. AdoS8OrderFlowOrderListItemDto listItem,
  232. List<AdoS8OrderFlowStage> stages,
  233. Dictionary<string, List<AdoS8OrderFlowSubstep>>? substepsByFlow,
  234. Dictionary<long, List<AdoS8OrderFlowSubstepUnit>>? unitsBySubstepId)
  235. {
  236. var detail = new AdoS8OrderFlowOrderDetailDto
  237. {
  238. OrderCode = listItem.OrderCode,
  239. ProductName = listItem.ProductName,
  240. ProductLine = listItem.ProductLine,
  241. CustomerCode = listItem.CustomerCode,
  242. CustomerName = listItem.CustomerName,
  243. CustomerType = listItem.CustomerType,
  244. Region = listItem.Region,
  245. Priority = listItem.Priority,
  246. WorkflowStatus = listItem.WorkflowStatus,
  247. CurrentOrderFlowCode = listItem.CurrentOrderFlowCode,
  248. CurrentOrderFlowName = listItem.CurrentOrderFlowName,
  249. CurrentStatus = listItem.CurrentStatus,
  250. ReleaseAt = listItem.ReleaseAt,
  251. TargetCycleDays = listItem.TargetCycleDays,
  252. ActualCycleDays = listItem.ActualCycleDays,
  253. CurrentCycleDays = listItem.CurrentCycleDays,
  254. NodeVarianceDays = listItem.NodeVarianceDays,
  255. CumulativeVarianceDays = listItem.CumulativeVarianceDays,
  256. ExceptionCount = listItem.ExceptionCount,
  257. ExceptionStatus = listItem.ExceptionStatus,
  258. ResponseMinutes = listItem.ResponseMinutes,
  259. ProcessingMinutes = listItem.ProcessingMinutes,
  260. TotalLossMinutes = listItem.TotalLossMinutes,
  261. ScenarioCode = listItem.ScenarioCode,
  262. DataSource = listItem.DataSource,
  263. Lifecycle = stages.Select(s => MapStage(s, substepsByFlow, unitsBySubstepId)).ToList(),
  264. };
  265. return detail;
  266. }
  267. private static AdoS8OrderFlowStageDto MapStage(
  268. AdoS8OrderFlowStage s,
  269. Dictionary<string, List<AdoS8OrderFlowSubstep>>? substepsByFlow,
  270. Dictionary<long, List<AdoS8OrderFlowSubstepUnit>>? unitsBySubstepId)
  271. {
  272. var substeps = substepsByFlow != null && substepsByFlow.TryGetValue(s.OrderFlowCode, out var list)
  273. ? list.Select(x => MapSubstep(x, unitsBySubstepId)).ToList()
  274. : new List<AdoS8OrderFlowSubstepDto>();
  275. return new AdoS8OrderFlowStageDto
  276. {
  277. OrderFlowCode = s.OrderFlowCode,
  278. OrderFlowName = string.IsNullOrEmpty(s.OrderFlowName)
  279. ? OrderFlowConstants.ResolveName(s.OrderFlowCode)
  280. : s.OrderFlowName,
  281. OwnerDept = s.OwnerDept,
  282. SortNo = s.SortNo,
  283. PlannedDays = s.PlannedDays,
  284. ActualDays = s.ActualDays,
  285. TargetAt = s.TargetAt,
  286. ActualStartAt = s.ActualStartAt,
  287. ActualEndAt = s.ActualEndAt,
  288. Status = s.Status,
  289. NodeVarianceDays = s.NodeVarianceDays,
  290. CumulativeVarianceDays = s.CumulativeVarianceDays,
  291. Substeps = substeps,
  292. };
  293. }
  294. private static AdoS8OrderFlowSubstepDto MapSubstep(
  295. AdoS8OrderFlowSubstep x,
  296. Dictionary<long, List<AdoS8OrderFlowSubstepUnit>>? unitsBySubstepId)
  297. {
  298. var units = unitsBySubstepId != null && unitsBySubstepId.TryGetValue(x.Id, out var list)
  299. ? list.Select(MapUnit).ToList()
  300. : new List<AdoS8OrderFlowSubstepUnitDto>();
  301. return new AdoS8OrderFlowSubstepDto
  302. {
  303. SubstepCode = x.SubstepCode,
  304. SubstepName = x.SubstepName,
  305. PiHours = x.PiHours,
  306. ActualHours = x.ActualHours,
  307. Status = x.Status,
  308. SortNo = x.SortNo,
  309. Units = units,
  310. };
  311. }
  312. private static AdoS8OrderFlowSubstepUnitDto MapUnit(AdoS8OrderFlowSubstepUnit u)
  313. => new()
  314. {
  315. UnitCode = u.UnitCode,
  316. UnitName = u.UnitName,
  317. PiHours = u.PiHours,
  318. ActualHours = u.ActualHours,
  319. Status = u.Status,
  320. SortNo = u.SortNo,
  321. };
  322. // ──────────────────────────────────────────────────────────────────────
  323. // t3c:Aggregate + ProcurementPivot
  324. // ──────────────────────────────────────────────────────────────────────
  325. /// <summary>
  326. /// 链路聚合视图:
  327. /// scope=BASELINE_PPT → 只读 ado_s8_order_flow_snapshot.stage_snapshots_json
  328. /// scope=CURRENT_FILTERED → 从 order + stage 实时聚合
  329. /// snapshot 缺失时返回空骨架(不做 fallback 硬编码)。
  330. /// </summary>
  331. public async Task<AdoS8OrderFlowAggregateDto> GetAggregateAsync(AdoS8OrderFlowAggregateQueryDto query)
  332. {
  333. var tenantId = query.TenantId ?? 1;
  334. var factoryId = query.FactoryId ?? 1;
  335. var scope = string.IsNullOrWhiteSpace(query.Scope) ? ScopeBaselinePpt : query.Scope;
  336. if (string.Equals(scope, ScopeBaselinePpt, StringComparison.OrdinalIgnoreCase))
  337. return await BuildBaselineAggregateAsync(tenantId, factoryId, scope);
  338. if (string.Equals(scope, ScopeCurrentFiltered, StringComparison.OrdinalIgnoreCase))
  339. return await BuildCurrentAggregateAsync(tenantId, factoryId, scope, query.OrderCodes);
  340. return EmptyAggregate(scope);
  341. }
  342. private async Task<AdoS8OrderFlowAggregateDto> BuildBaselineAggregateAsync(long tenantId, long factoryId, string scope)
  343. {
  344. var snapshot = await _snapshotRep.AsQueryable()
  345. .Where(s => s.TenantId == tenantId && s.FactoryId == factoryId && !s.IsDeleted)
  346. .Where(s => s.ScopeCode == ScopeBaselinePpt && s.SnapshotCode == BaselineSnapshotCode)
  347. .FirstAsync();
  348. if (snapshot == null) return EmptyAggregate(scope);
  349. var stages = ParseStageSnapshots(snapshot.StageSnapshotsJson);
  350. var collab = await BuildFinalAssemblyCollabSummaryAsync(tenantId, factoryId, orderCodes: null);
  351. return new AdoS8OrderFlowAggregateDto
  352. {
  353. Scope = scope,
  354. TotalOrders = snapshot.TotalOrders,
  355. TotalCustomers = snapshot.TotalCustomers,
  356. AvgResponseMinutes = snapshot.AvgResponseMinutes,
  357. AvgProcessingMinutes = snapshot.AvgProcessingMinutes,
  358. AvgLossMinutes = snapshot.AvgLossMinutes,
  359. StageAggregates = ReorderByCanonical(stages),
  360. FinalAssemblyCollabSummary = collab,
  361. };
  362. }
  363. private async Task<AdoS8OrderFlowAggregateDto> BuildCurrentAggregateAsync(
  364. long tenantId, long factoryId, string scope, List<string>? orderCodes)
  365. {
  366. var orders = await _orderRep.AsQueryable()
  367. .Where(o => o.TenantId == tenantId && o.FactoryId == factoryId && !o.IsDeleted)
  368. .WhereIF(orderCodes != null && orderCodes.Count > 0, o => orderCodes!.Contains(o.OrderCode))
  369. .ToListAsync();
  370. if (orders.Count == 0) return EmptyAggregate(scope);
  371. var orderIds = orders.Select(o => o.Id).ToList();
  372. var stages = await _stageRep.AsQueryable()
  373. .Where(s => s.TenantId == tenantId && s.FactoryId == factoryId && !s.IsDeleted)
  374. .Where(s => orderIds.Contains(s.OrderId))
  375. .ToListAsync();
  376. var responseList = orders.Where(o => o.ResponseMinutes.HasValue).Select(o => (decimal)o.ResponseMinutes!.Value).ToList();
  377. var processingList = orders.Where(o => o.ProcessingMinutes.HasValue).Select(o => (decimal)o.ProcessingMinutes!.Value).ToList();
  378. var lossList = orders.Where(o => o.TotalLossMinutes.HasValue).Select(o => (decimal)o.TotalLossMinutes!.Value).ToList();
  379. var stagesByFlow = stages.GroupBy(s => s.OrderFlowCode).ToDictionary(g => g.Key, g => g.ToList());
  380. var stageAggregates = new List<AdoS8OrderFlowStageAggregateDto>();
  381. foreach (var code in OrderFlowConstants.All)
  382. {
  383. stagesByFlow.TryGetValue(code, out var rows);
  384. stageAggregates.Add(BuildStageAggregateFromRows(code, rows ?? new List<AdoS8OrderFlowStage>()));
  385. }
  386. var collab = await BuildFinalAssemblyCollabSummaryAsync(tenantId, factoryId, orderCodes);
  387. return new AdoS8OrderFlowAggregateDto
  388. {
  389. Scope = scope,
  390. TotalOrders = orders.Count,
  391. TotalCustomers = orders.Select(o => o.CustomerCode).Where(c => !string.IsNullOrEmpty(c)).Distinct().Count(),
  392. AvgResponseMinutes = AvgOrZero(responseList),
  393. AvgProcessingMinutes = AvgOrZero(processingList),
  394. AvgLossMinutes = AvgOrZero(lossList),
  395. StageAggregates = stageAggregates,
  396. FinalAssemblyCollabSummary = collab,
  397. };
  398. }
  399. // ORDER-CHAIN-FINAL-ASSEMBLY-DEPT-COLLAB-MVP-1:FINAL_ASSEMBLY_DELIVERY 末端协同派生。
  400. // 仅查 ado_s8_exception 现有数据;只覆盖有真实映射的 2 个主门禁 + 1 个并行准备,其它 6 个对象前端展示 "--"。
  401. // 不读取 S0|S4 业务表;不新增表 / seed / 异常类型。
  402. private async Task<AdoS8FinalAssemblyCollabSummaryDto> BuildFinalAssemblyCollabSummaryAsync(
  403. long tenantId, long factoryId, List<string>? orderCodes)
  404. {
  405. var allTypes = GoodsHandoverExceptionTypes
  406. .Concat(ShipmentConfirmationExceptionTypes)
  407. .Concat(ShippingPlanExceptionTypes)
  408. .Distinct()
  409. .ToList();
  410. var rows = await _exceptionRep.AsQueryable()
  411. .Where(e => e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted)
  412. .Where(e => e.SourceObjectType == ExceptionSourceObjectType)
  413. .Where(e => e.ExceptionTypeCode != null && allTypes.Contains(e.ExceptionTypeCode!))
  414. .WhereIF(orderCodes != null && orderCodes.Count > 0,
  415. e => e.RelatedObjectCode != null && orderCodes!.Contains(e.RelatedObjectCode!))
  416. .Select(e => new ExceptionAttribution
  417. {
  418. ExceptionTypeCode = e.ExceptionTypeCode,
  419. RelatedObjectCode = e.RelatedObjectCode,
  420. Title = e.Title,
  421. Severity = e.Severity,
  422. CreatedAt = e.CreatedAt,
  423. })
  424. .ToListAsync();
  425. return new AdoS8FinalAssemblyCollabSummaryDto
  426. {
  427. Gates = new Dictionary<string, AdoS8FinalAssemblyObjectStatDto>
  428. {
  429. [FinalAssemblyObjectGoodsHandover] = BuildObjectStat(rows, GoodsHandoverExceptionTypes, isGate: true),
  430. [FinalAssemblyObjectShipmentConfirmation] = BuildObjectStat(rows, ShipmentConfirmationExceptionTypes, isGate: true),
  431. },
  432. Parallels = new Dictionary<string, AdoS8FinalAssemblyObjectStatDto>
  433. {
  434. [FinalAssemblyObjectShippingPlan] = BuildObjectStat(rows, ShippingPlanExceptionTypes, isGate: false),
  435. },
  436. };
  437. }
  438. private static AdoS8FinalAssemblyObjectStatDto BuildObjectStat(
  439. List<ExceptionAttribution> rows, string[] candidateTypes, bool isGate)
  440. {
  441. var subset = rows.Where(r => r.ExceptionTypeCode != null && candidateTypes.Contains(r.ExceptionTypeCode!)).ToList();
  442. var count = subset
  443. .Where(r => !string.IsNullOrEmpty(r.RelatedObjectCode))
  444. .Select(r => r.RelatedObjectCode!)
  445. .Distinct()
  446. .Count();
  447. var top = subset
  448. .OrderByDescending(r => string.Equals(r.Severity, "SERIOUS", StringComparison.OrdinalIgnoreCase) ? 1 : 0)
  449. .ThenByDescending(r => r.CreatedAt)
  450. .FirstOrDefault();
  451. return new AdoS8FinalAssemblyObjectStatDto
  452. {
  453. ImpactedOrderCount = isGate ? count : (int?)null,
  454. RiskOrderCount = isGate ? (int?)null : count,
  455. TopRiskTitle = top?.Title,
  456. TopRiskSeverity = top?.Severity,
  457. };
  458. }
  459. private sealed class ExceptionAttribution
  460. {
  461. public string? ExceptionTypeCode { get; set; }
  462. public string? RelatedObjectCode { get; set; }
  463. public string? Title { get; set; }
  464. public string? Severity { get; set; }
  465. public DateTime CreatedAt { get; set; }
  466. }
  467. private static AdoS8OrderFlowStageAggregateDto BuildStageAggregateFromRows(string flowCode, List<AdoS8OrderFlowStage> rows)
  468. {
  469. var dto = new AdoS8OrderFlowStageAggregateDto
  470. {
  471. OrderFlowCode = flowCode,
  472. OrderFlowName = OrderFlowConstants.ResolveName(flowCode),
  473. };
  474. if (rows.Count == 0) return dto;
  475. var plannedList = rows.Select(r => r.PlannedDays).ToList();
  476. var actualList = rows.Where(r => r.ActualDays.HasValue && !string.Equals(r.Status, "pending", StringComparison.OrdinalIgnoreCase))
  477. .Select(r => r.ActualDays!.Value).ToList();
  478. var green = rows.Count(r => string.Equals(r.Status, "green", StringComparison.OrdinalIgnoreCase));
  479. var yellow = rows.Count(r => string.Equals(r.Status, "yellow", StringComparison.OrdinalIgnoreCase));
  480. var red = rows.Count(r => string.Equals(r.Status, "red", StringComparison.OrdinalIgnoreCase));
  481. var pending = rows.Count(r => string.Equals(r.Status, "pending", StringComparison.OrdinalIgnoreCase));
  482. var total = rows.Count;
  483. dto.KpiAvgDays = AvgOrZero(plannedList);
  484. dto.ActualAvgDays = AvgOrZero(actualList);
  485. dto.Green = green;
  486. dto.Yellow = yellow;
  487. dto.Red = red;
  488. dto.Pending = pending;
  489. // 整数百分比;total 不含 0 因为上面已 short-circuit。 percent 因子 100 是公式系数,非业务真值。
  490. dto.OnTimeRate = total > 0 ? (int)Math.Round(green * 100.0 / total) : 0;
  491. return dto;
  492. }
  493. private static List<AdoS8OrderFlowStageAggregateDto> ParseStageSnapshots(string? json)
  494. {
  495. if (string.IsNullOrWhiteSpace(json)) return new List<AdoS8OrderFlowStageAggregateDto>();
  496. try
  497. {
  498. var parsed = JsonSerializer.Deserialize<List<AdoS8OrderFlowStageAggregateDto>>(json, JsonOpts);
  499. return parsed ?? new List<AdoS8OrderFlowStageAggregateDto>();
  500. }
  501. catch (JsonException)
  502. {
  503. return new List<AdoS8OrderFlowStageAggregateDto>();
  504. }
  505. }
  506. /// <summary>按 OrderFlowConstants.All 重排;缺失 code 补空骨架;多余 code 追加在尾部以避免静默丢数据。</summary>
  507. private static List<AdoS8OrderFlowStageAggregateDto> ReorderByCanonical(List<AdoS8OrderFlowStageAggregateDto> input)
  508. {
  509. var byCode = input.Where(x => !string.IsNullOrEmpty(x.OrderFlowCode))
  510. .GroupBy(x => x.OrderFlowCode)
  511. .ToDictionary(g => g.Key, g => g.First());
  512. var ordered = new List<AdoS8OrderFlowStageAggregateDto>();
  513. foreach (var code in OrderFlowConstants.All)
  514. {
  515. if (byCode.TryGetValue(code, out var hit))
  516. {
  517. if (string.IsNullOrEmpty(hit.OrderFlowName))
  518. hit.OrderFlowName = OrderFlowConstants.ResolveName(code);
  519. ordered.Add(hit);
  520. byCode.Remove(code);
  521. }
  522. else
  523. {
  524. ordered.Add(new AdoS8OrderFlowStageAggregateDto
  525. {
  526. OrderFlowCode = code,
  527. OrderFlowName = OrderFlowConstants.ResolveName(code),
  528. });
  529. }
  530. }
  531. ordered.AddRange(byCode.Values);
  532. return ordered;
  533. }
  534. private static AdoS8OrderFlowAggregateDto EmptyAggregate(string scope)
  535. {
  536. var stages = OrderFlowConstants.All
  537. .Select(code => new AdoS8OrderFlowStageAggregateDto
  538. {
  539. OrderFlowCode = code,
  540. OrderFlowName = OrderFlowConstants.ResolveName(code),
  541. })
  542. .ToList();
  543. return new AdoS8OrderFlowAggregateDto { Scope = scope, StageAggregates = stages };
  544. }
  545. private static decimal AvgOrZero(List<decimal> values)
  546. => values.Count == 0 ? 0m : values.Average();
  547. /// <summary>
  548. /// 采购透视视图:
  549. /// scope=BASELINE_PPT → 读 ado_s8_order_flow_procurement_pivot baseline 行(order_id IS NULL)。
  550. /// scope=CURRENT_FILTERED → 读订单级行(order_id IS NOT NULL)并按 orderCodes 过滤后聚合:
  551. /// cycle_days 简单平均;impact_count 求和(语义:多订单合计影响台次);
  552. /// kit_rate 简单平均;status 按聚合后 cycle_days 三档阈值重判
  553. /// (与 baseline seed 一致:≤15 green / ≤20 yellow / &gt;20 red)。
  554. /// 其它 scope → 返回空骨架并回显 scope。
  555. /// orderCodes 为空时返回空骨架,不静默 fallback baseline(避免 baseline 泄漏到单订单态)。
  556. /// data_source 优先级:IMPORT / AGG &gt; SEED;同一 (order_code, material, supplier, spec) 上
  557. /// 若 IMPORT/AGG 行存在,只采用 IMPORT/AGG;SEED 仅作过渡兜底,便于真实数据源接入后无缝替换。
  558. /// status 来源:baseline 直接读 row.Status;CURRENT_FILTERED 聚合后按阈值重判(运行期重新分类,
  559. /// 不修改 seed 行)。cycle_days 保持 decimal(6,3) 不截断。
  560. /// </summary>
  561. public async Task<AdoS8OrderFlowProcurementPivotDto> GetProcurementPivotAsync(AdoS8OrderFlowProcurementPivotQueryDto query)
  562. {
  563. var tenantId = query.TenantId ?? 1;
  564. var factoryId = query.FactoryId ?? 1;
  565. var scope = string.IsNullOrWhiteSpace(query.Scope) ? ScopeBaselinePpt : query.Scope;
  566. if (string.Equals(scope, ScopeBaselinePpt, StringComparison.OrdinalIgnoreCase))
  567. {
  568. var baselineRows = await _pivotRep.AsQueryable()
  569. .Where(p => p.TenantId == tenantId && p.FactoryId == factoryId && !p.IsDeleted)
  570. .Where(p => p.OrderId == null) // baseline 行
  571. .ToListAsync();
  572. return BuildPivot(scope, baselineRows);
  573. }
  574. if (string.Equals(scope, ScopeCurrentFiltered, StringComparison.OrdinalIgnoreCase))
  575. {
  576. // 与产品设计 drawings API 一致:orderCodes 由 CSV 解析(trim / 去空 / Distinct)。
  577. var orderCodes = ParseOrderCodesCsv(query.OrderCodes);
  578. if (orderCodes.Count == 0)
  579. return new AdoS8OrderFlowProcurementPivotDto { Scope = scope };
  580. var orderLevelRows = await _pivotRep.AsQueryable()
  581. .Where(p => p.TenantId == tenantId && p.FactoryId == factoryId && !p.IsDeleted)
  582. .Where(p => p.OrderId != null && p.OrderCode != null)
  583. .Where(p => orderCodes.Contains(p.OrderCode!))
  584. .ToListAsync();
  585. if (orderLevelRows.Count == 0)
  586. return new AdoS8OrderFlowProcurementPivotDto { Scope = scope };
  587. var preferred = SelectPreferredDataSourceRows(orderLevelRows);
  588. var aggregated = AggregateOrderLevelRows(preferred);
  589. return BuildPivot(scope, aggregated);
  590. }
  591. return new AdoS8OrderFlowProcurementPivotDto { Scope = scope };
  592. }
  593. /// <summary>
  594. /// data_source 优先级:同一 (order_code, material, supplier, spec) 同时存在 IMPORT/AGG 与 SEED 时,
  595. /// 只保留 IMPORT/AGG 行;SEED 仅在 IMPORT/AGG 缺失时作为兜底。便于真实数据源接入后无缝替换 SEED。
  596. /// </summary>
  597. private static List<AdoS8OrderFlowProcurementPivot> SelectPreferredDataSourceRows(
  598. List<AdoS8OrderFlowProcurementPivot> rows)
  599. {
  600. var grouped = rows.GroupBy(r => (r.OrderCode, r.MaterialCode, r.SupplierCode, r.SpecCode));
  601. var result = new List<AdoS8OrderFlowProcurementPivot>(rows.Count);
  602. foreach (var g in grouped)
  603. {
  604. var preferred = g.FirstOrDefault(r =>
  605. string.Equals(r.DataSource, "IMPORT", StringComparison.OrdinalIgnoreCase) ||
  606. string.Equals(r.DataSource, "AGG", StringComparison.OrdinalIgnoreCase));
  607. result.Add(preferred ?? g.First());
  608. }
  609. return result;
  610. }
  611. /// <summary>
  612. /// 多订单聚合:按 (material, supplier, spec) 分组后简单平均;单订单时分组内只 1 行,聚合等于原值。
  613. /// cycle_days 平均;impact_count 求和;kit_rate 平均;status 按聚合后 cycle_days 阈值重判。
  614. /// 不补造缺位单元;聚合产物为非持久化 Pivot 对象供 BuildPivot 复用。
  615. /// </summary>
  616. private static List<AdoS8OrderFlowProcurementPivot> AggregateOrderLevelRows(
  617. List<AdoS8OrderFlowProcurementPivot> rows)
  618. {
  619. return rows
  620. .GroupBy(r => (r.MaterialCode, r.SupplierCode, r.SpecCode))
  621. .Select(g =>
  622. {
  623. var cycle = decimal.Round(g.Average(r => r.CycleDays), 3);
  624. var impactRows = g.Where(r => r.ImpactCount.HasValue).ToList();
  625. int? impactSum = impactRows.Count == 0 ? null : impactRows.Sum(r => r.ImpactCount!.Value);
  626. var kitRows = g.Where(r => r.KitRate.HasValue).ToList();
  627. decimal? kitAvg = kitRows.Count == 0
  628. ? null
  629. : decimal.Round(kitRows.Average(r => r.KitRate!.Value), 4);
  630. return new AdoS8OrderFlowProcurementPivot
  631. {
  632. Id = 0,
  633. OrderId = null,
  634. OrderCode = null,
  635. MaterialCode = g.Key.MaterialCode,
  636. SupplierCode = g.Key.SupplierCode,
  637. SpecCode = g.Key.SpecCode,
  638. CycleDays = cycle,
  639. Status = ClassifyAggregatedStatus(cycle),
  640. ImpactCount = impactSum,
  641. KitRate = kitAvg,
  642. ScenarioCode = string.Empty,
  643. DataSource = string.Empty,
  644. TenantId = 0,
  645. FactoryId = 0,
  646. IsDeleted = false,
  647. };
  648. })
  649. .ToList();
  650. }
  651. private static string ClassifyAggregatedStatus(decimal cycleDays)
  652. {
  653. if (cycleDays <= 15.0m) return "green";
  654. if (cycleDays <= 20.0m) return "yellow";
  655. return "red";
  656. }
  657. private static AdoS8OrderFlowProcurementPivotDto BuildPivot(string scope, List<AdoS8OrderFlowProcurementPivot> rows)
  658. {
  659. var result = new AdoS8OrderFlowProcurementPivotDto { Scope = scope };
  660. if (rows.Count == 0) return result;
  661. var byMaterial = rows.GroupBy(r => r.MaterialCode)
  662. .ToDictionary(g => g.Key, g => g.ToList());
  663. foreach (var (material, list) in byMaterial.OrderBy(kv => kv.Key, StringComparer.Ordinal))
  664. {
  665. // KeyMaterials:取 (supplier=TOTAL, spec=TOTAL) 单元作为该 material 的概览
  666. var grand = list.FirstOrDefault(r => r.SupplierCode == PivotTotalCode && r.SpecCode == PivotTotalCode);
  667. if (grand != null)
  668. {
  669. result.KeyMaterials.Add(new AdoS8OrderFlowKeyMaterialDto
  670. {
  671. MaterialCode = material,
  672. AvgCycleDays = grand.CycleDays,
  673. CycleStatus = grand.Status,
  674. ImpactCount = grand.ImpactCount,
  675. KitRate = grand.KitRate,
  676. ResultStatus = grand.Status,
  677. });
  678. }
  679. // SupplierBreakdown:spec=TOTAL 的所有 supplier 行(含 supplier=TOTAL)
  680. foreach (var row in list.Where(r => r.SpecCode == PivotTotalCode)
  681. .OrderBy(r => r.SupplierCode == PivotTotalCode ? 1 : 0)
  682. .ThenBy(r => r.SupplierCode, StringComparer.Ordinal))
  683. {
  684. result.SupplierBreakdown.Add(new AdoS8OrderFlowSupplierBreakdownDto
  685. {
  686. MaterialCode = material,
  687. SupplierCode = row.SupplierCode,
  688. AvgCycleDays = row.CycleDays,
  689. Status = row.Status,
  690. });
  691. }
  692. // SpecBreakdown:supplier=TOTAL 的所有 spec 行(含 spec=TOTAL)
  693. foreach (var row in list.Where(r => r.SupplierCode == PivotTotalCode)
  694. .OrderBy(r => r.SpecCode == PivotTotalCode ? 1 : 0)
  695. .ThenBy(r => r.SpecCode, StringComparer.Ordinal))
  696. {
  697. result.SpecBreakdown.Add(new AdoS8OrderFlowSpecBreakdownDto
  698. {
  699. MaterialCode = material,
  700. SpecCode = row.SpecCode,
  701. AvgCycleDays = row.CycleDays,
  702. Status = row.Status,
  703. });
  704. }
  705. // MatrixByMaterial:每个 supplier 一行,cells keyed by spec_code(保留全部 cycle_days 精度)
  706. var matrix = list.GroupBy(r => r.SupplierCode)
  707. .OrderBy(g => g.Key == PivotTotalCode ? 1 : 0)
  708. .ThenBy(g => g.Key, StringComparer.Ordinal)
  709. .Select(g => new AdoS8OrderFlowProcurementMatrixRowDto
  710. {
  711. SupplierCode = g.Key,
  712. Cells = g.ToDictionary(
  713. r => r.SpecCode,
  714. r => new AdoS8OrderFlowProcurementMatrixCellDto
  715. {
  716. CycleDays = r.CycleDays,
  717. Status = r.Status,
  718. }),
  719. })
  720. .ToList();
  721. result.MatrixByMaterial[material] = matrix;
  722. }
  723. return result;
  724. }
  725. // ──────────────────────────────────────────────────────────────────────
  726. // S8-ORDER-CHAIN-PRODUCT-DESIGN-DRAWING-PERSIST-1:产品设计图号粒度聚合。
  727. // 单一数据源 = ado_s8_order_flow_product_design_drawing 表;
  728. // 台数 / 占比 / 加权达成率 全部由 product_quantity 字段驱动;
  729. // 平均设计周期 = actual_days 算术平均(非 quantity 加权);
  730. // summary.categories 固定返回 STANDARD / NON_STANDARD / TOTAL 三档;
  731. // drawings 按 productType 过滤;未达标优先 + actualDays 降序 + orderCode/drawingNo 升序。
  732. // ──────────────────────────────────────────────────────────────────────
  733. private const string ProductDesignTypeStandard = "STANDARD";
  734. private const string ProductDesignTypeNonStandard = "NON_STANDARD";
  735. private const string ProductDesignTypeTotal = "TOTAL";
  736. private const decimal ProductDesignStageKpiDays = 3m;
  737. // S8-ORDER-CHAIN-PRODUCT-DESIGN-PPT-STATIC-AND-SINGLE-ORDER-ALIGN-1:
  738. // 非标产品 KPI 与常规产品同阶段标准 3 天;非标 actual_days 超 3 时记 red/未达标。
  739. private const decimal ProductDesignNonStandardKpiDays = 3m;
  740. public async Task<AdoS8OrderFlowProductDesignDrawingsDto> GetProductDesignDrawingsAsync(
  741. AdoS8OrderFlowProductDesignDrawingsQueryDto query)
  742. {
  743. var tenantId = query.TenantId ?? 1;
  744. var factoryId = query.FactoryId ?? 1;
  745. var orderCodes = ParseOrderCodesCsv(query.OrderCodes);
  746. var productTypeFilter = NormalizeProductDesignType(query.ProductType);
  747. var scope = orderCodes.Count == 0 ? ScopeBaselinePpt : ScopeCurrentFiltered;
  748. var rows = await _pddRep.AsQueryable()
  749. .Where(d => d.TenantId == tenantId && d.FactoryId == factoryId && !d.IsDeleted)
  750. .WhereIF(orderCodes.Count > 0, d => orderCodes.Contains(d.OrderCode))
  751. .ToListAsync();
  752. // summary 始终基于命中订单范围的全分类汇总(不受 productType 过滤影响),让前端可同时渲染三档汇总。
  753. var summary = BuildProductDesignSummary(rows);
  754. var drawingItems = rows.AsEnumerable();
  755. if (productTypeFilter != null)
  756. drawingItems = drawingItems.Where(d => d.ProductType == productTypeFilter);
  757. var orderedDrawings = drawingItems
  758. .OrderBy(d => d.IsAchieved)
  759. .ThenByDescending(d => d.ActualDays ?? decimal.MinValue)
  760. .ThenBy(d => d.OrderCode, StringComparer.Ordinal)
  761. .ThenBy(d => d.DrawingNo, StringComparer.Ordinal)
  762. .Select(MapProductDesignDrawingItem)
  763. .ToList();
  764. return new AdoS8OrderFlowProductDesignDrawingsDto
  765. {
  766. Scope = scope,
  767. Filter = new AdoS8OrderFlowProductDesignFilterDto
  768. {
  769. OrderCodes = orderCodes,
  770. ProductType = productTypeFilter,
  771. },
  772. Summary = summary,
  773. Drawings = orderedDrawings,
  774. };
  775. }
  776. private static List<string> ParseOrderCodesCsv(string? raw)
  777. {
  778. if (string.IsNullOrWhiteSpace(raw)) return new List<string>();
  779. return raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
  780. .Where(s => !string.IsNullOrWhiteSpace(s))
  781. .Distinct(StringComparer.Ordinal)
  782. .ToList();
  783. }
  784. private static string? NormalizeProductDesignType(string? raw)
  785. {
  786. if (string.IsNullOrWhiteSpace(raw)) return null;
  787. var s = raw.Trim().ToUpperInvariant();
  788. return s == ProductDesignTypeStandard || s == ProductDesignTypeNonStandard ? s : null;
  789. }
  790. private static AdoS8OrderFlowProductDesignSummaryDto BuildProductDesignSummary(
  791. List<AdoS8OrderFlowProductDesignDrawing> rows)
  792. {
  793. var stdRows = rows.Where(r => r.ProductType == ProductDesignTypeStandard).ToList();
  794. var nonStdRows = rows.Where(r => r.ProductType == ProductDesignTypeNonStandard).ToList();
  795. var grandTotalQty = rows.Sum(r => r.ProductQuantity);
  796. return new AdoS8OrderFlowProductDesignSummaryDto
  797. {
  798. Overall = BuildProductDesignOverall(rows),
  799. Categories = new List<AdoS8OrderFlowProductDesignCategorySummaryDto>
  800. {
  801. BuildProductDesignCategory(ProductDesignTypeStandard, "常规产品", ProductDesignStageKpiDays, stdRows, grandTotalQty),
  802. BuildProductDesignCategory(ProductDesignTypeNonStandard, "非标产品", ProductDesignNonStandardKpiDays, nonStdRows, grandTotalQty),
  803. BuildProductDesignCategory(ProductDesignTypeTotal, "合计", ProductDesignStageKpiDays, rows, grandTotalQty),
  804. },
  805. };
  806. }
  807. private static AdoS8OrderFlowProductDesignOverallSummaryDto BuildProductDesignOverall(
  808. List<AdoS8OrderFlowProductDesignDrawing> rows)
  809. {
  810. var totalQty = rows.Sum(r => r.ProductQuantity);
  811. var achievedQty = rows.Where(r => r.IsAchieved).Sum(r => r.ProductQuantity);
  812. var actualDays = rows.Where(r => r.ActualDays.HasValue).Select(r => r.ActualDays!.Value).ToList();
  813. return new AdoS8OrderFlowProductDesignOverallSummaryDto
  814. {
  815. DrawingCount = rows.Select(r => r.DrawingNo).Distinct(StringComparer.Ordinal).Count(),
  816. TotalQuantity = totalQty,
  817. KpiDays = ProductDesignStageKpiDays,
  818. AvgActualDays = actualDays.Count == 0 ? 0m : actualDays.Average(),
  819. AchievementRate = totalQty == 0 ? 0m : (decimal)achievedQty / totalQty,
  820. };
  821. }
  822. private static AdoS8OrderFlowProductDesignCategorySummaryDto BuildProductDesignCategory(
  823. string productType,
  824. string name,
  825. decimal kpiDays,
  826. List<AdoS8OrderFlowProductDesignDrawing> rows,
  827. int grandTotalQty)
  828. {
  829. var totalQty = rows.Sum(r => r.ProductQuantity);
  830. var achievedQty = rows.Where(r => r.IsAchieved).Sum(r => r.ProductQuantity);
  831. var actualDays = rows.Where(r => r.ActualDays.HasValue).Select(r => r.ActualDays!.Value).ToList();
  832. var rate = totalQty == 0 ? 0m : (decimal)achievedQty / totalQty;
  833. // S8-ORDER-CHAIN-PRODUCT-DESIGN-20ORDER-GENERATION-CONSISTENCY-AUDIT-1:
  834. // STANDARD / NON_STANDARD / TOTAL 三档统一返回真实指标,不再用 null 掩盖。
  835. // - KpiDays:始终返回常量 KPI=3,方便前端展示阈值。
  836. // - AvgActualDays:actualDays 简单平均(pending 行已经被 HasValue 过滤)。TOTAL 平均 == 父 stage.actual_days(守恒)。
  837. // - AchievementRate:以 product_quantity 加权达成率(与 Overall 一致),保留业务台数权重直觉。
  838. // - Status:达成率 ≥0.95 green / ≥0.80 yellow / 否则 red;无完工行(all pending)时返回空串。
  839. return new AdoS8OrderFlowProductDesignCategorySummaryDto
  840. {
  841. ProductType = productType,
  842. Name = name,
  843. DrawingCount = rows.Select(r => r.DrawingNo).Distinct(StringComparer.Ordinal).Count(),
  844. TotalQuantity = totalQty,
  845. Ratio = grandTotalQty == 0 ? 0m : (decimal)totalQty / grandTotalQty,
  846. KpiDays = kpiDays,
  847. AvgActualDays = actualDays.Count == 0 ? (decimal?)null : actualDays.Average(),
  848. AchievementRate = rate,
  849. Status = actualDays.Count == 0
  850. ? string.Empty
  851. : ClassifyProductDesignCategoryStatus(rate, totalQty),
  852. };
  853. }
  854. /// <summary>达标率 ≥ 0.95 绿 / ≥ 0.80 黄 / 否则 红;无数据返回空串以表达未取数而非状态判定。</summary>
  855. private static string ClassifyProductDesignCategoryStatus(decimal achievementRate, int totalQty)
  856. {
  857. if (totalQty == 0) return string.Empty;
  858. if (achievementRate >= 0.95m) return "green";
  859. if (achievementRate >= 0.80m) return "yellow";
  860. return "red";
  861. }
  862. private static AdoS8OrderFlowProductDesignDrawingItemDto MapProductDesignDrawingItem(
  863. AdoS8OrderFlowProductDesignDrawing d)
  864. => new()
  865. {
  866. OrderCode = d.OrderCode,
  867. DrawingNo = d.DrawingNo,
  868. ProductType = d.ProductType,
  869. ResponsiblePerson = d.ResponsiblePerson,
  870. PlannedStartDate = d.PlannedStartDate,
  871. PlannedEndDate = d.PlannedEndDate,
  872. ActualStartDate = d.ActualStartDate,
  873. ActualEndDate = d.ActualEndDate,
  874. KpiDays = d.KpiDays,
  875. ActualDays = d.ActualDays,
  876. IsAchieved = d.IsAchieved,
  877. Status = d.Status,
  878. ProductQuantity = d.ProductQuantity,
  879. };
  880. // ──────────────────────────────────────────────────────────────────────
  881. // S8-ORDER-CHAIN-BODY-PRODUCTION-ORDER-LEVEL-SEED-FIX-1:本体生产透视。
  882. // 单一数据源 = ado_s8_order_flow_manufacturing_{process,loss_factor,operator} 三表。
  883. // BASELINE_PPT 读 (order_id IS NULL) baseline 行;CURRENT_FILTERED 读订单级行(order_id IS NOT NULL),
  884. // 按 IMPORT/AGG > SEED 优先级去重后,按业务口径聚合:
  885. // process actual_days:AVG(多订单按行平均)
  886. // process plan_qty:SUM(多订单累计计划台次)
  887. // process achievement_rate:AVG(多订单平均达成率),TOTAL 行特殊处理见 BuildManufacturingProcessAggregated。
  888. // process cycle_status / achievement_status:聚合后按阈值重判(与 baseline 同口径,运行期不修改 seed 行)。
  889. // loss factor count:SUM;ratio_pct / loss_hours:AVG。
  890. // operator avg_hours:AVG;status:取首单的 status(baseline 每操作员固定,AVG 后语义不变)。
  891. // orderCodes 为空时返回空骨架(不静默 fallback baseline,避免 baseline 泄漏到单订单态)。
  892. // status 阈值与 seed 同步:cycle_status (≤pi green / ≤pi*1.2 yellow / 否则 red),
  893. // achievement_status (≥0.95 green / ≥0.80 yellow / 否则 red)。
  894. // ──────────────────────────────────────────────────────────────────────
  895. private const string ManufacturingProcessTotalCode = "TOTAL";
  896. // S8-...-FIX-1R:order-level TOTAL.achievement_rate / achievement_status 锚定 fixture,
  897. // 与 baseline TOTAL(0.90 / yellow)保持一致,不再用 plan_qty 加权计算(避免 65% red 与 90% yellow 冲突)。
  898. // SEED 过渡口径;真实 IMPORT/AGG 接入后由真实生产达成数据替换。
  899. private const decimal ManufacturingTotalAchievementRate = 0.9000m;
  900. private const string ManufacturingTotalAchievementStatus = "yellow";
  901. public async Task<AdoS8OrderFlowManufacturingPivotDto> GetManufacturingPivotAsync(
  902. AdoS8OrderFlowManufacturingPivotQueryDto query)
  903. {
  904. var tenantId = query.TenantId ?? 1;
  905. var factoryId = query.FactoryId ?? 1;
  906. var scope = string.IsNullOrWhiteSpace(query.Scope) ? ScopeBaselinePpt : query.Scope;
  907. if (string.Equals(scope, ScopeBaselinePpt, StringComparison.OrdinalIgnoreCase))
  908. {
  909. var processRows = await _mfgProcessRep.AsQueryable()
  910. .Where(p => p.TenantId == tenantId && p.FactoryId == factoryId && !p.IsDeleted)
  911. .Where(p => p.OrderId == null)
  912. .ToListAsync();
  913. var lossRows = await _mfgLossRep.AsQueryable()
  914. .Where(p => p.TenantId == tenantId && p.FactoryId == factoryId && !p.IsDeleted)
  915. .Where(p => p.OrderId == null)
  916. .ToListAsync();
  917. var operatorRows = await _mfgOperatorRep.AsQueryable()
  918. .Where(p => p.TenantId == tenantId && p.FactoryId == factoryId && !p.IsDeleted)
  919. .Where(p => p.OrderId == null)
  920. .ToListAsync();
  921. return BuildManufacturingPivotFromBaseline(scope, processRows, lossRows, operatorRows);
  922. }
  923. if (string.Equals(scope, ScopeCurrentFiltered, StringComparison.OrdinalIgnoreCase))
  924. {
  925. var orderCodes = ParseOrderCodesCsv(query.OrderCodes);
  926. if (orderCodes.Count == 0)
  927. return new AdoS8OrderFlowManufacturingPivotDto { Scope = scope };
  928. var processRows = await _mfgProcessRep.AsQueryable()
  929. .Where(p => p.TenantId == tenantId && p.FactoryId == factoryId && !p.IsDeleted)
  930. .Where(p => p.OrderId != null && p.OrderCode != null)
  931. .Where(p => orderCodes.Contains(p.OrderCode!))
  932. .ToListAsync();
  933. var lossRows = await _mfgLossRep.AsQueryable()
  934. .Where(p => p.TenantId == tenantId && p.FactoryId == factoryId && !p.IsDeleted)
  935. .Where(p => p.OrderId != null && p.OrderCode != null)
  936. .Where(p => orderCodes.Contains(p.OrderCode!))
  937. .ToListAsync();
  938. var operatorRows = await _mfgOperatorRep.AsQueryable()
  939. .Where(p => p.TenantId == tenantId && p.FactoryId == factoryId && !p.IsDeleted)
  940. .Where(p => p.OrderId != null && p.OrderCode != null)
  941. .Where(p => orderCodes.Contains(p.OrderCode!))
  942. .ToListAsync();
  943. if (processRows.Count == 0 && lossRows.Count == 0 && operatorRows.Count == 0)
  944. return new AdoS8OrderFlowManufacturingPivotDto { Scope = scope };
  945. // IMPORT/AGG > SEED 去重:同 (order_code, process_code/factor_code/operator_code) 上 IMPORT/AGG 优先。
  946. var preferredProcess = SelectPreferredMfgProcess(processRows);
  947. var preferredLoss = SelectPreferredMfgLossFactor(lossRows);
  948. var preferredOperator = SelectPreferredMfgOperator(operatorRows);
  949. return new AdoS8OrderFlowManufacturingPivotDto
  950. {
  951. Scope = scope,
  952. Processes = BuildManufacturingProcessAggregated(preferredProcess),
  953. LossFactors = BuildManufacturingLossFactorAggregated(preferredLoss),
  954. Operators = BuildManufacturingOperatorAggregated(preferredOperator),
  955. };
  956. }
  957. return new AdoS8OrderFlowManufacturingPivotDto { Scope = scope };
  958. }
  959. private static AdoS8OrderFlowManufacturingPivotDto BuildManufacturingPivotFromBaseline(
  960. string scope,
  961. List<AdoS8OrderFlowManufacturingProcess> processRows,
  962. List<AdoS8OrderFlowManufacturingLossFactor> lossRows,
  963. List<AdoS8OrderFlowManufacturingOperator> operatorRows)
  964. {
  965. var processes = processRows
  966. .OrderBy(r => r.SortNo)
  967. .Select(r => new AdoS8OrderFlowManufacturingProcessDto
  968. {
  969. ProcessCode = r.ProcessCode,
  970. ProcessName = r.ProcessName,
  971. PiDays = r.PiDays,
  972. ActualDays = r.ActualDays,
  973. CycleStatus = r.CycleStatus,
  974. PlanQty = r.PlanQty,
  975. AchievementRate = r.AchievementRate,
  976. AchievementStatus = r.AchievementStatus,
  977. SortNo = r.SortNo,
  978. })
  979. .ToList();
  980. var losses = lossRows
  981. .OrderBy(r => r.SortNo)
  982. .Select(r => new AdoS8OrderFlowManufacturingLossFactorDto
  983. {
  984. FactorCode = r.FactorCode,
  985. FactorName = r.FactorName,
  986. CountValue = r.CountValue,
  987. RatioPct = r.RatioPct,
  988. LossHours = r.LossHours,
  989. SortNo = r.SortNo,
  990. })
  991. .ToList();
  992. var operators = operatorRows
  993. .OrderBy(r => r.SortNo)
  994. .Select(r => new AdoS8OrderFlowManufacturingOperatorDto
  995. {
  996. OperatorCode = r.OperatorCode,
  997. OperatorName = r.OperatorName,
  998. AvgHours = r.AvgHours,
  999. Status = r.Status,
  1000. SortNo = r.SortNo,
  1001. })
  1002. .ToList();
  1003. return new AdoS8OrderFlowManufacturingPivotDto
  1004. {
  1005. Scope = scope,
  1006. Processes = processes,
  1007. LossFactors = losses,
  1008. Operators = operators,
  1009. };
  1010. }
  1011. private static List<AdoS8OrderFlowManufacturingProcess> SelectPreferredMfgProcess(
  1012. List<AdoS8OrderFlowManufacturingProcess> rows)
  1013. {
  1014. var grouped = rows.GroupBy(r => (r.OrderCode, r.ProcessCode));
  1015. var result = new List<AdoS8OrderFlowManufacturingProcess>(rows.Count);
  1016. foreach (var g in grouped)
  1017. {
  1018. var preferred = g.FirstOrDefault(r =>
  1019. string.Equals(r.DataSource, "IMPORT", StringComparison.OrdinalIgnoreCase) ||
  1020. string.Equals(r.DataSource, "AGG", StringComparison.OrdinalIgnoreCase));
  1021. result.Add(preferred ?? g.First());
  1022. }
  1023. return result;
  1024. }
  1025. private static List<AdoS8OrderFlowManufacturingLossFactor> SelectPreferredMfgLossFactor(
  1026. List<AdoS8OrderFlowManufacturingLossFactor> rows)
  1027. {
  1028. var grouped = rows.GroupBy(r => (r.OrderCode, r.FactorCode));
  1029. var result = new List<AdoS8OrderFlowManufacturingLossFactor>(rows.Count);
  1030. foreach (var g in grouped)
  1031. {
  1032. var preferred = g.FirstOrDefault(r =>
  1033. string.Equals(r.DataSource, "IMPORT", StringComparison.OrdinalIgnoreCase) ||
  1034. string.Equals(r.DataSource, "AGG", StringComparison.OrdinalIgnoreCase));
  1035. result.Add(preferred ?? g.First());
  1036. }
  1037. return result;
  1038. }
  1039. private static List<AdoS8OrderFlowManufacturingOperator> SelectPreferredMfgOperator(
  1040. List<AdoS8OrderFlowManufacturingOperator> rows)
  1041. {
  1042. var grouped = rows.GroupBy(r => (r.OrderCode, r.OperatorCode));
  1043. var result = new List<AdoS8OrderFlowManufacturingOperator>(rows.Count);
  1044. foreach (var g in grouped)
  1045. {
  1046. var preferred = g.FirstOrDefault(r =>
  1047. string.Equals(r.DataSource, "IMPORT", StringComparison.OrdinalIgnoreCase) ||
  1048. string.Equals(r.DataSource, "AGG", StringComparison.OrdinalIgnoreCase));
  1049. result.Add(preferred ?? g.First());
  1050. }
  1051. return result;
  1052. }
  1053. /// <summary>
  1054. /// 多订单工序聚合:
  1055. /// 非 TOTAL 行:actual_days = AVG;plan_qty = SUM;achievement_rate = AVG(保留 4 位小数);
  1056. /// cycle_status 按聚合 actual vs pi 重判;achievement_status 按聚合 rate 重判。
  1057. /// TOTAL 行:pi=6(取 baseline 常量);actual = AVG(与 4 工序 AVG 之和守恒,差异为 rounding 容差);
  1058. /// plan_qty = NULL;
  1059. /// S8-...-FIX-1R:achievement_rate / achievement_status 锚定 fixture(0.90 / yellow),
  1060. /// 不再做 plan_qty 加权计算(避免与 baseline TOTAL 视觉冲突);
  1061. /// cycle_status 按聚合 actual vs pi 重判。
  1062. /// </summary>
  1063. private static List<AdoS8OrderFlowManufacturingProcessDto> BuildManufacturingProcessAggregated(
  1064. List<AdoS8OrderFlowManufacturingProcess> rows)
  1065. {
  1066. var grouped = rows
  1067. .GroupBy(r => r.ProcessCode)
  1068. .ToDictionary(g => g.Key, g => g.ToList());
  1069. // S8-...-FIX-1R:TOTAL 行 rate / status 锚定 fixture,不再做 4 工序 plan_qty 加权计算,
  1070. // 不需要维护中间 aggregatedRates 字典。
  1071. var processDtos = new List<AdoS8OrderFlowManufacturingProcessDto>();
  1072. foreach (var kv in grouped.OrderBy(g => g.Value.First().SortNo))
  1073. {
  1074. var code = kv.Key;
  1075. var list = kv.Value;
  1076. var sample = list[0];
  1077. var isTotal = string.Equals(code, ManufacturingProcessTotalCode, StringComparison.OrdinalIgnoreCase);
  1078. var actualAvg = decimal.Round(list.Average(r => r.ActualDays), 3);
  1079. var pi = sample.PiDays;
  1080. decimal? aggregatedRate;
  1081. int? planQtySum;
  1082. string achStatus;
  1083. if (isTotal)
  1084. {
  1085. // S8-...-FIX-1R:TOTAL 行 rate / status 锚定 fixture(0.90 / yellow),
  1086. // 不再做 plan_qty 加权计算;非 TOTAL 行的 plan_qty 仍保留供 UI 显示,但不参与 TOTAL.rate。
  1087. planQtySum = null;
  1088. aggregatedRate = ManufacturingTotalAchievementRate;
  1089. achStatus = ManufacturingTotalAchievementStatus;
  1090. }
  1091. else
  1092. {
  1093. planQtySum = list.Where(r => r.PlanQty.HasValue).Sum(r => r.PlanQty!.Value);
  1094. var rateRows = list.Where(r => r.AchievementRate.HasValue).ToList();
  1095. aggregatedRate = rateRows.Count == 0
  1096. ? null
  1097. : decimal.Round(rateRows.Average(r => r.AchievementRate!.Value), 4);
  1098. achStatus = aggregatedRate.HasValue
  1099. ? ClassifyMfgAchievementStatus(aggregatedRate.Value)
  1100. : string.Empty;
  1101. }
  1102. var cycleStatus = ClassifyMfgCycleStatus(actualAvg, pi);
  1103. processDtos.Add(new AdoS8OrderFlowManufacturingProcessDto
  1104. {
  1105. ProcessCode = sample.ProcessCode,
  1106. ProcessName = sample.ProcessName,
  1107. PiDays = pi,
  1108. ActualDays = actualAvg,
  1109. CycleStatus = cycleStatus,
  1110. PlanQty = planQtySum,
  1111. AchievementRate = aggregatedRate,
  1112. AchievementStatus = achStatus,
  1113. SortNo = sample.SortNo,
  1114. });
  1115. }
  1116. return processDtos;
  1117. }
  1118. private static List<AdoS8OrderFlowManufacturingLossFactorDto> BuildManufacturingLossFactorAggregated(
  1119. List<AdoS8OrderFlowManufacturingLossFactor> rows)
  1120. {
  1121. return rows
  1122. .GroupBy(r => r.FactorCode)
  1123. .OrderBy(g => g.First().SortNo)
  1124. .Select(g =>
  1125. {
  1126. var sample = g.First();
  1127. var countList = g.Where(r => r.CountValue.HasValue).ToList();
  1128. var ratioList = g.Where(r => r.RatioPct.HasValue).ToList();
  1129. var hoursList = g.Where(r => r.LossHours.HasValue).ToList();
  1130. return new AdoS8OrderFlowManufacturingLossFactorDto
  1131. {
  1132. FactorCode = sample.FactorCode,
  1133. FactorName = sample.FactorName,
  1134. CountValue = countList.Count == 0 ? null : countList.Sum(r => r.CountValue!.Value),
  1135. RatioPct = ratioList.Count == 0
  1136. ? null
  1137. : decimal.Round(ratioList.Average(r => r.RatioPct!.Value), 2),
  1138. LossHours = hoursList.Count == 0
  1139. ? null
  1140. : decimal.Round(hoursList.Average(r => r.LossHours!.Value), 2),
  1141. SortNo = sample.SortNo,
  1142. };
  1143. })
  1144. .ToList();
  1145. }
  1146. private static List<AdoS8OrderFlowManufacturingOperatorDto> BuildManufacturingOperatorAggregated(
  1147. List<AdoS8OrderFlowManufacturingOperator> rows)
  1148. {
  1149. return rows
  1150. .GroupBy(r => r.OperatorCode)
  1151. .OrderBy(g => g.First().SortNo)
  1152. .Select(g =>
  1153. {
  1154. var sample = g.First();
  1155. return new AdoS8OrderFlowManufacturingOperatorDto
  1156. {
  1157. OperatorCode = sample.OperatorCode,
  1158. OperatorName = sample.OperatorName,
  1159. AvgHours = decimal.Round(g.Average(r => r.AvgHours), 2),
  1160. // 每操作员 baseline status 固化,多订单聚合时各订单值一致;取首单不引入歧义。
  1161. Status = sample.Status,
  1162. SortNo = sample.SortNo,
  1163. };
  1164. })
  1165. .ToList();
  1166. }
  1167. private static string ClassifyMfgCycleStatus(decimal actual, decimal pi)
  1168. {
  1169. if (actual <= pi) return "green";
  1170. if (actual <= pi * 1.2m) return "yellow";
  1171. return "red";
  1172. }
  1173. private static string ClassifyMfgAchievementStatus(decimal rate)
  1174. {
  1175. if (rate >= 0.95m) return "green";
  1176. if (rate >= 0.80m) return "yellow";
  1177. return "red";
  1178. }
  1179. }