OrderReviewOrchestrationService.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. using Admin.NET.Plugin.AiDOP.Infrastructure;
  2. namespace Admin.NET.Plugin.AiDOP.Order;
  3. /// <summary>
  4. /// S1 订单评审编排:资源检查、状态更新、工单生成、运行日志。
  5. /// </summary>
  6. public class OrderReviewOrchestrationService : ITransient
  7. {
  8. public const string ActionReview = "S1_ORDER_REVIEW";
  9. public const string ActionConfirm = "S1_DELIVERY_CONFIRM";
  10. public const string ActionRefresh = "S1_ORDER_REFRESH_PLAN";
  11. private readonly ISqlSugarClient _db;
  12. private readonly UserManager _userManager;
  13. private readonly OrderWorkOrderGenerationService _workOrderGen;
  14. private readonly OrderResourceCheckService _resourceCheck;
  15. private readonly S1MdpSyncTransformService _mdpSync;
  16. private readonly AidopActionRunLogWriter _runLog;
  17. public OrderReviewOrchestrationService(
  18. ISqlSugarClient db,
  19. UserManager userManager,
  20. OrderWorkOrderGenerationService workOrderGen,
  21. OrderResourceCheckService resourceCheck,
  22. S1MdpSyncTransformService mdpSync,
  23. AidopActionRunLogWriter runLog)
  24. {
  25. _db = db;
  26. _userManager = userManager;
  27. _workOrderGen = workOrderGen;
  28. _resourceCheck = resourceCheck;
  29. _mdpSync = mdpSync;
  30. _runLog = runLog;
  31. }
  32. public Task<SeOrderReviewExecuteResult> ReviewAsync(IReadOnlyList<long> orderIds) =>
  33. ExecuteBatchAsync(ActionReview, orderIds, ReviewOneOrderAsync);
  34. public Task<SeOrderReviewExecuteResult> ConfirmDeliveryAsync(IReadOnlyList<long> orderIds) =>
  35. ExecuteBatchAsync(ActionConfirm, orderIds, ConfirmOneOrderAsync);
  36. public Task<SeOrderReviewExecuteResult> RefreshPlanAsync(long orderId, string? reason) =>
  37. ExecuteSingleAsync(ActionRefresh, orderId, async (order, result, warnings, account) =>
  38. {
  39. var entries = await LoadReviewableEntriesAsync(order.Id, order.TenantId, ["3", "0"]);
  40. if (entries.Count == 0)
  41. throw Oops.Oh("订单没有可重排的确认/再评审明细行");
  42. foreach (var entry in entries)
  43. {
  44. ValidateEntryForResourceCheck(entry);
  45. var wo = await _workOrderGen.CreateOrUpdateForEntryAsync(order, entry, account, warnings);
  46. if (wo.Created) result.WorkOrderCreatedCount++;
  47. else result.WorkOrderUpdatedCount++;
  48. result.WorkOrders.Add(wo.WorkOrd);
  49. var check = await _resourceCheck.RunForEntryAsync(order, entry, wo.WorkOrd, account, warnings);
  50. result.ResourceCheckCount++;
  51. result.ResourceCheckLineCount += check.LineCount;
  52. }
  53. await UpdateEntriesProgressAsync(entries.Select(e => e.Id).ToList(), "0", account);
  54. result.EntryCount = entries.Count;
  55. result.Message = string.IsNullOrWhiteSpace(reason)
  56. ? "3级计划重排完成"
  57. : $"3级计划重排完成:{reason.Trim()}";
  58. });
  59. private async Task<SeOrderReviewExecuteResult> ExecuteBatchAsync(
  60. string actionCode,
  61. IReadOnlyList<long> orderIds,
  62. Func<OrderWorkOrderGenerationService.OrderHeader, SeOrderReviewExecuteResult, List<string>, string, Task> handler)
  63. {
  64. if (orderIds is null || orderIds.Count == 0)
  65. throw Oops.Oh("至少选择一条订单");
  66. var tenantId = _userManager.TenantId > 0
  67. ? _userManager.TenantId
  68. : AidopTenantHelper.Resolve(App.HttpContext);
  69. var account = _userManager.Account ?? "system";
  70. var distinctIds = orderIds.Distinct().ToList();
  71. var aggregate = new SeOrderReviewExecuteResult
  72. {
  73. ActionCode = actionCode,
  74. OrderCount = distinctIds.Count,
  75. Message = "执行成功"
  76. };
  77. var allWarnings = new List<string>();
  78. long? firstLogId = null;
  79. foreach (var orderId in distinctIds)
  80. {
  81. var order = await LoadOrderAsync(orderId, tenantId)
  82. ?? throw Oops.Oh($"订单 {orderId} 不存在或不属于当前租户");
  83. var runLogId = await _runLog.StartAsync(actionCode, tenantId, "crm_seorder", order.Id, order.BillNo);
  84. if (firstLogId is null)
  85. firstLogId = runLogId;
  86. var perOrder = new SeOrderReviewExecuteResult { ActionCode = actionCode };
  87. var warnings = new List<string>();
  88. try
  89. {
  90. await _db.Ado.BeginTranAsync();
  91. await handler(order, perOrder, warnings, account);
  92. await _db.Ado.CommitTranAsync();
  93. aggregate.EntryCount += perOrder.EntryCount;
  94. aggregate.WorkOrderCreatedCount += perOrder.WorkOrderCreatedCount;
  95. aggregate.WorkOrderUpdatedCount += perOrder.WorkOrderUpdatedCount;
  96. aggregate.WorkOrderClosedCount += perOrder.WorkOrderClosedCount;
  97. aggregate.ResourceCheckCount += perOrder.ResourceCheckCount;
  98. aggregate.ResourceCheckLineCount += perOrder.ResourceCheckLineCount;
  99. aggregate.WorkOrders.AddRange(perOrder.WorkOrders);
  100. allWarnings.AddRange(warnings);
  101. await _runLog.SuccessAsync(runLogId, perOrder.Message, new
  102. {
  103. orderId = order.Id,
  104. billNo = order.BillNo,
  105. perOrder.EntryCount,
  106. perOrder.WorkOrderCreatedCount,
  107. perOrder.WorkOrderUpdatedCount,
  108. workOrders = perOrder.WorkOrders,
  109. warnings
  110. });
  111. }
  112. catch (Exception ex)
  113. {
  114. await _db.Ado.RollbackTranAsync();
  115. await _runLog.FailedAsync(runLogId, ex.Message, new { orderId = order.Id, billNo = order.BillNo });
  116. throw Oops.Oh(ex.Message);
  117. }
  118. }
  119. aggregate.RunLogId = firstLogId ?? 0;
  120. aggregate.Warnings = allWarnings.Distinct().ToList();
  121. aggregate.Message = BuildAggregateMessage(actionCode, aggregate);
  122. if (actionCode == ActionReview && aggregate.ResourceCheckCount > 0)
  123. aggregate.Warnings.AddRange(await TryTriggerMdpRefreshAsync());
  124. return aggregate;
  125. }
  126. private async Task<SeOrderReviewExecuteResult> ExecuteSingleAsync(
  127. string actionCode,
  128. long orderId,
  129. Func<OrderWorkOrderGenerationService.OrderHeader, SeOrderReviewExecuteResult, List<string>, string, Task> handler)
  130. {
  131. var tenantId = _userManager.TenantId > 0
  132. ? _userManager.TenantId
  133. : AidopTenantHelper.Resolve(App.HttpContext);
  134. var account = _userManager.Account ?? "system";
  135. var order = await LoadOrderAsync(orderId, tenantId)
  136. ?? throw Oops.Oh("订单不存在或不属于当前租户");
  137. var result = new SeOrderReviewExecuteResult
  138. {
  139. ActionCode = actionCode,
  140. OrderCount = 1
  141. };
  142. var warnings = new List<string>();
  143. var runLogId = await _runLog.StartAsync(actionCode, tenantId, "crm_seorder", order.Id, order.BillNo);
  144. result.RunLogId = runLogId;
  145. try
  146. {
  147. await _db.Ado.BeginTranAsync();
  148. await handler(order, result, warnings, account);
  149. await _db.Ado.CommitTranAsync();
  150. result.Warnings = warnings;
  151. result.Message = BuildSingleMessage(result);
  152. await _runLog.SuccessAsync(runLogId, result.Message, new
  153. {
  154. orderId = order.Id,
  155. billNo = order.BillNo,
  156. result.EntryCount,
  157. result.WorkOrderCreatedCount,
  158. result.WorkOrderUpdatedCount,
  159. workOrders = result.WorkOrders,
  160. warnings
  161. });
  162. if (actionCode == ActionRefresh && result.ResourceCheckCount > 0)
  163. result.Warnings.AddRange(await TryTriggerMdpRefreshAsync());
  164. return result;
  165. }
  166. catch (Exception ex)
  167. {
  168. await _db.Ado.RollbackTranAsync();
  169. await _runLog.FailedAsync(runLogId, ex.Message, new { orderId = order.Id, billNo = order.BillNo });
  170. throw Oops.Oh(ex.Message);
  171. }
  172. }
  173. private async Task ReviewOneOrderAsync(
  174. OrderWorkOrderGenerationService.OrderHeader order,
  175. SeOrderReviewExecuteResult result,
  176. List<string> warnings,
  177. string account)
  178. {
  179. var entries = await LoadReviewableEntriesAsync(order.Id, order.TenantId, ["1", "0", "2"]);
  180. if (entries.Count == 0)
  181. throw Oops.Oh($"订单 {order.BillNo} 没有可评审的明细行(须为新建,评审,再评审状态)");
  182. var reviewedEntryIds = new List<long>();
  183. foreach (var entry in entries)
  184. {
  185. ValidateEntryForResourceCheck(entry);
  186. if (entry.PlanDate is null)
  187. throw Oops.Oh($"订单行 {entry.EntrySeq} 缺少客户要求交期(plan_date)");
  188. // 1. 先做资源检查(纯 BOM 展开 + 库存计算,不依赖工单)
  189. var (check, lines) = await _resourceCheck.CheckOnlyAsync(order, entry, warnings);
  190. result.ResourceCheckCount++;
  191. result.ResourceCheckLineCount += check.LineCount;
  192. // 2. 有缺料 → 才生成工单并写入资源检查结果;库存可满足则跳过工单
  193. if (check.HasShortage)
  194. {
  195. var wo = await _workOrderGen.CreateOrUpdateForEntryAsync(order, entry, account, warnings);
  196. if (wo.Created) result.WorkOrderCreatedCount++;
  197. else result.WorkOrderUpdatedCount++;
  198. result.WorkOrders.Add(wo.WorkOrd);
  199. await _resourceCheck.WriteResultAsync(order, entry, wo.WorkOrd, lines, account);
  200. warnings.Add($"订单行 {entry.EntrySeq} 存在缺料(工单 {wo.WorkOrd})");
  201. }
  202. else
  203. {
  204. // 3. 不缺料 → 若之前已生成工单则关闭(Status='C', IsActive=0)
  205. var closedCount = await CloseExistingWorkOrdersAsync(entry.TenantId, entry.Id, account);
  206. if (closedCount > 0)
  207. {
  208. result.WorkOrderClosedCount += closedCount;
  209. warnings.Add($"订单行 {entry.EntrySeq} 库存可满足,已关闭 {closedCount} 个历史工单");
  210. }
  211. }
  212. reviewedEntryIds.Add(entry.Id);
  213. }
  214. // 3. 所有明细行处理完毕后,批量更新 progress = '2'
  215. await UpdateEntriesProgressAsync(reviewedEntryIds, "2", account);
  216. result.EntryCount = entries.Count;
  217. result.Message = $"订单 {order.BillNo} 评审完成(资源检查 {result.ResourceCheckCount} 条)";
  218. }
  219. private async Task ConfirmOneOrderAsync(
  220. OrderWorkOrderGenerationService.OrderHeader order,
  221. SeOrderReviewExecuteResult result,
  222. List<string> warnings,
  223. string account)
  224. {
  225. var entries = await LoadReviewableEntriesAsync(order.Id, order.TenantId, ["2"]);
  226. if (entries.Count == 0)
  227. throw Oops.Oh($"订单 {order.BillNo} 没有处于评审状态的明细行,请先完成订单评审");
  228. foreach (var entry in entries)
  229. {
  230. var woCnt = await _db.Ado.GetIntAsync(
  231. """
  232. SELECT COUNT(*) FROM WorkOrdMaster
  233. WHERE tenant_id = @TenantId AND BusinessID = @EntryId
  234. AND LOWER(TRIM(IFNULL(Status,''))) <> 'c'
  235. """,
  236. new SugarParameter("@TenantId", entry.TenantId),
  237. new SugarParameter("@EntryId", entry.Id));
  238. // 无活跃工单:可能从未生成(库存可满足)或已关闭 → 仍允许交期确认
  239. var confirmDate = entry.SysCapacityDate ?? entry.PlanDate;
  240. await UpdateEntryAfterConfirmAsync(entry.Id, confirmDate, account);
  241. result.EntryCount++;
  242. if (woCnt > 0)
  243. result.WorkOrders.Add(await LoadWorkOrdForEntryAsync(entry.TenantId, entry.Id));
  244. else
  245. warnings.Add($"订单行 {entry.EntrySeq} 无活跃工单(库存可满足或已关闭),已直接确认交期");
  246. }
  247. result.WorkOrders = result.WorkOrders.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList();
  248. result.Message = $"订单 {order.BillNo} 交期确认完成";
  249. }
  250. private async Task<string> LoadWorkOrdForEntryAsync(long tenantId, long entryId)
  251. {
  252. var rows = await _db.Ado.SqlQueryAsync<string>(
  253. """
  254. SELECT WorkOrd FROM WorkOrdMaster
  255. WHERE tenant_id = @TenantId AND BusinessID = @EntryId
  256. ORDER BY RecID DESC LIMIT 1
  257. """,
  258. new SugarParameter("@TenantId", tenantId),
  259. new SugarParameter("@EntryId", entryId));
  260. return rows.FirstOrDefault() ?? string.Empty;
  261. }
  262. /// <summary>
  263. /// 关闭指定订单行已有的未关闭工单(WorkOrdMaster.Status='C', IsActive=0;mes_morder.morder_state='关闭')。
  264. /// 返回被关闭的工单数量。
  265. /// </summary>
  266. private async Task<int> CloseExistingWorkOrdersAsync(long tenantId, long entryId, string account)
  267. {
  268. // 查找该订单行下所有未关闭的工单号
  269. var openWorkOrds = await _db.Ado.SqlQueryAsync<string>(
  270. """
  271. SELECT WorkOrd FROM WorkOrdMaster
  272. WHERE tenant_id = @TenantId AND BusinessID = @EntryId
  273. AND LOWER(TRIM(IFNULL(Status,''))) <> 'c'
  274. AND IFNULL(IsActive, 0) = 1
  275. """,
  276. new SugarParameter("@TenantId", tenantId),
  277. new SugarParameter("@EntryId", entryId));
  278. if (openWorkOrds.Count == 0) return 0;
  279. var workOrdList = string.Join(",", openWorkOrds.Select(w => $"'{w}'"));
  280. var now = DateTime.Now;
  281. // 关闭 WorkOrdMaster
  282. await _db.Ado.ExecuteCommandAsync(
  283. $"""
  284. UPDATE WorkOrdMaster
  285. SET Status = 'C', IsActive = 0,
  286. UpdateUser = @User, UpdateTime = @Now
  287. WHERE tenant_id = @TenantId AND BusinessID = @EntryId
  288. AND LOWER(TRIM(IFNULL(Status,''))) <> 'c'
  289. AND IFNULL(IsActive, 0) = 1
  290. """,
  291. new SugarParameter("@TenantId", tenantId),
  292. new SugarParameter("@EntryId", entryId),
  293. new SugarParameter("@User", account),
  294. new SugarParameter("@Now", now));
  295. // 同步关闭 mes_morder
  296. await _db.Ado.ExecuteCommandAsync(
  297. $"""
  298. UPDATE mes_morder
  299. SET morder_state = '关闭',
  300. update_by_name = @User, update_time = @Now
  301. WHERE tenant_id = @TenantId AND morder_no IN ({workOrdList})
  302. AND IFNULL(morder_state, '') <> '关闭'
  303. """,
  304. new SugarParameter("@TenantId", tenantId),
  305. new SugarParameter("@User", account),
  306. new SugarParameter("@Now", now));
  307. return openWorkOrds.Count;
  308. }
  309. private static void ValidateEntryForResourceCheck(OrderWorkOrderGenerationService.OrderEntryLine entry)
  310. {
  311. if (string.IsNullOrWhiteSpace(entry.ItemNumber))
  312. throw Oops.Oh($"订单行 {entry.EntrySeq} 物料编码不能为空");
  313. if (entry.Qty is null or <= 0)
  314. throw Oops.Oh($"订单行 {entry.EntrySeq} 数量必须大于 0");
  315. if (entry.PlanDate is null && entry.SysCapacityDate is null)
  316. throw Oops.Oh($"订单行 {entry.EntrySeq} 缺少计划交期");
  317. }
  318. private async Task<OrderWorkOrderGenerationService.OrderHeader?> LoadOrderAsync(long orderId, long tenantId)
  319. {
  320. var rows = await _db.Ado.SqlQueryAsync<OrderWorkOrderGenerationService.OrderHeader>(
  321. """
  322. SELECT Id, bill_no AS BillNo, custom_no AS CustomNo, urgent AS Urgent,
  323. factory_id AS FactoryId, tenant_id AS TenantId
  324. FROM crm_seorder
  325. WHERE Id = @Id AND tenant_id = @TenantId AND IsDeleted = 0
  326. LIMIT 1
  327. """,
  328. new SugarParameter("@Id", orderId),
  329. new SugarParameter("@TenantId", tenantId));
  330. return rows.FirstOrDefault();
  331. }
  332. private async Task<List<OrderWorkOrderGenerationService.OrderEntryLine>> LoadReviewableEntriesAsync(
  333. long orderId,
  334. long tenantId,
  335. IReadOnlyList<string> progressList)
  336. {
  337. if (progressList.Count == 0)
  338. return new List<OrderWorkOrderGenerationService.OrderEntryLine>();
  339. var inClause = string.Join(", ", progressList.Select((_, i) => $"@P{i}"));
  340. var pars = new List<SugarParameter>
  341. {
  342. new("@OrderId", orderId),
  343. new("@TenantId", tenantId)
  344. };
  345. for (var i = 0; i < progressList.Count; i++)
  346. pars.Add(new SugarParameter($"@P{i}", progressList[i]));
  347. return await _db.Ado.SqlQueryAsync<OrderWorkOrderGenerationService.OrderEntryLine>(
  348. $"""
  349. SELECT
  350. Id, seorder_id AS SeOrderId, bill_no AS BillNo, entry_seq AS EntrySeq,
  351. item_number AS ItemNumber, item_name AS ItemName, specification AS Specification,
  352. unit AS Unit, bom_number AS BomNumber, qty AS Qty,
  353. plan_date AS PlanDate, sys_capacity_date AS SysCapacityDate,
  354. progress AS Progress, urgent AS Urgent,
  355. factory_id AS FactoryId, company_id AS CompanyId, tenant_id AS TenantId
  356. FROM crm_seorderentry
  357. WHERE seorder_id = @OrderId AND tenant_id = @TenantId AND IsDeleted = 0
  358. AND COALESCE(NULLIF(progress, ''), '1') IN ({inClause})
  359. ORDER BY entry_seq, Id
  360. """,
  361. pars);
  362. }
  363. private async Task UpdateEntryAfterReviewAsync(long entryId, DateTime? capacityDate, string account)
  364. {
  365. await _db.Ado.ExecuteCommandAsync(
  366. """
  367. UPDATE crm_seorderentry
  368. SET progress = '2',
  369. sys_capacity_date = COALESCE(sys_capacity_date, @CapacityDate),
  370. update_time = @Now
  371. WHERE Id = @Id AND IsDeleted = 0
  372. """,
  373. new SugarParameter("@CapacityDate", capacityDate ?? (object)DBNull.Value),
  374. new SugarParameter("@Now", DateTime.Now),
  375. new SugarParameter("@Id", entryId));
  376. }
  377. private async Task UpdateEntryAfterConfirmAsync(long entryId, DateTime? confirmDate, string account)
  378. {
  379. await _db.Ado.ExecuteCommandAsync(
  380. """
  381. UPDATE crm_seorderentry
  382. SET progress = '3',
  383. date = COALESCE(date, @ConfirmDate),
  384. update_time = @Now
  385. WHERE Id = @Id AND IsDeleted = 0
  386. """,
  387. new SugarParameter("@ConfirmDate", confirmDate ?? (object)DBNull.Value),
  388. new SugarParameter("@Now", DateTime.Now),
  389. new SugarParameter("@Id", entryId));
  390. }
  391. private async Task UpdateEntriesProgressAsync(IReadOnlyList<long> entryIds, string progress, string account)
  392. {
  393. if (entryIds.Count == 0) return;
  394. var idList = string.Join(",", entryIds);
  395. await _db.Ado.ExecuteCommandAsync(
  396. $"""
  397. UPDATE crm_seorderentry
  398. SET progress = @Progress, update_time = @Now
  399. WHERE Id IN ({idList}) AND IsDeleted = 0
  400. """,
  401. new SugarParameter("@Progress", progress),
  402. new SugarParameter("@Now", DateTime.Now));
  403. }
  404. private static string BuildAggregateMessage(string actionCode, SeOrderReviewExecuteResult r)
  405. {
  406. var woPart = r.WorkOrders.Count > 0
  407. ? $",工单:{string.Join("、", r.WorkOrders.Distinct())}"
  408. : string.Empty;
  409. var closedPart = r.WorkOrderClosedCount > 0
  410. ? $"、关闭 {r.WorkOrderClosedCount}"
  411. : string.Empty;
  412. return actionCode switch
  413. {
  414. ActionReview => $"评审完成 {r.OrderCount} 单、{r.EntryCount} 行,新建工单 {r.WorkOrderCreatedCount}、更新 {r.WorkOrderUpdatedCount}{closedPart}、资源检查 {r.ResourceCheckCount} 条{woPart}",
  415. ActionConfirm => $"交期确认完成 {r.OrderCount} 单、{r.EntryCount} 行{woPart}",
  416. _ => r.Message
  417. };
  418. }
  419. private static string BuildSingleMessage(SeOrderReviewExecuteResult r)
  420. {
  421. if (r.WorkOrders.Count == 0)
  422. return r.Message;
  423. return $"{r.Message},工单:{string.Join("、", r.WorkOrders.Distinct())}";
  424. }
  425. private async Task<List<string>> TryTriggerMdpRefreshAsync()
  426. {
  427. var warnings = new List<string>();
  428. try
  429. {
  430. await _mdpSync.RunFullAsync(triggerType: "ORDER_REVIEW");
  431. }
  432. catch (Exception ex)
  433. {
  434. warnings.Add($"S1 MDP/DWD 刷新未完成:{ex.Message}");
  435. }
  436. return warnings;
  437. }
  438. }