OrderReviewOrchestrationService.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  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.ResourceCheckCount += perOrder.ResourceCheckCount;
  97. aggregate.ResourceCheckLineCount += perOrder.ResourceCheckLineCount;
  98. aggregate.WorkOrders.AddRange(perOrder.WorkOrders);
  99. allWarnings.AddRange(warnings);
  100. await _runLog.SuccessAsync(runLogId, perOrder.Message, new
  101. {
  102. orderId = order.Id,
  103. billNo = order.BillNo,
  104. perOrder.EntryCount,
  105. perOrder.WorkOrderCreatedCount,
  106. perOrder.WorkOrderUpdatedCount,
  107. workOrders = perOrder.WorkOrders,
  108. warnings
  109. });
  110. }
  111. catch (Exception ex)
  112. {
  113. await _db.Ado.RollbackTranAsync();
  114. await _runLog.FailedAsync(runLogId, ex.Message, new { orderId = order.Id, billNo = order.BillNo });
  115. throw Oops.Oh(ex.Message);
  116. }
  117. }
  118. aggregate.RunLogId = firstLogId ?? 0;
  119. aggregate.Warnings = allWarnings.Distinct().ToList();
  120. aggregate.Message = BuildAggregateMessage(actionCode, aggregate);
  121. if (actionCode == ActionReview && aggregate.ResourceCheckCount > 0)
  122. aggregate.Warnings.AddRange(await TryTriggerMdpRefreshAsync());
  123. return aggregate;
  124. }
  125. private async Task<SeOrderReviewExecuteResult> ExecuteSingleAsync(
  126. string actionCode,
  127. long orderId,
  128. Func<OrderWorkOrderGenerationService.OrderHeader, SeOrderReviewExecuteResult, List<string>, string, Task> handler)
  129. {
  130. var tenantId = _userManager.TenantId > 0
  131. ? _userManager.TenantId
  132. : AidopTenantHelper.Resolve(App.HttpContext);
  133. var account = _userManager.Account ?? "system";
  134. var order = await LoadOrderAsync(orderId, tenantId)
  135. ?? throw Oops.Oh("订单不存在或不属于当前租户");
  136. var result = new SeOrderReviewExecuteResult
  137. {
  138. ActionCode = actionCode,
  139. OrderCount = 1
  140. };
  141. var warnings = new List<string>();
  142. var runLogId = await _runLog.StartAsync(actionCode, tenantId, "crm_seorder", order.Id, order.BillNo);
  143. result.RunLogId = runLogId;
  144. try
  145. {
  146. await _db.Ado.BeginTranAsync();
  147. await handler(order, result, warnings, account);
  148. await _db.Ado.CommitTranAsync();
  149. result.Warnings = warnings;
  150. result.Message = BuildSingleMessage(result);
  151. await _runLog.SuccessAsync(runLogId, result.Message, new
  152. {
  153. orderId = order.Id,
  154. billNo = order.BillNo,
  155. result.EntryCount,
  156. result.WorkOrderCreatedCount,
  157. result.WorkOrderUpdatedCount,
  158. workOrders = result.WorkOrders,
  159. warnings
  160. });
  161. if (actionCode == ActionRefresh && result.ResourceCheckCount > 0)
  162. result.Warnings.AddRange(await TryTriggerMdpRefreshAsync());
  163. return result;
  164. }
  165. catch (Exception ex)
  166. {
  167. await _db.Ado.RollbackTranAsync();
  168. await _runLog.FailedAsync(runLogId, ex.Message, new { orderId = order.Id, billNo = order.BillNo });
  169. throw Oops.Oh(ex.Message);
  170. }
  171. }
  172. private async Task ReviewOneOrderAsync(
  173. OrderWorkOrderGenerationService.OrderHeader order,
  174. SeOrderReviewExecuteResult result,
  175. List<string> warnings,
  176. string account)
  177. {
  178. var entries = await LoadReviewableEntriesAsync(order.Id, order.TenantId, ["1", "0"]);
  179. if (entries.Count == 0)
  180. throw Oops.Oh($"订单 {order.BillNo} 没有可评审的明细行(须为新建或再评审状态)");
  181. foreach (var entry in entries)
  182. {
  183. ValidateEntryForResourceCheck(entry);
  184. if (entry.PlanDate is null)
  185. throw Oops.Oh($"订单行 {entry.EntrySeq} 缺少客户要求交期(plan_date)");
  186. var wo = await _workOrderGen.CreateOrUpdateForEntryAsync(order, entry, account, warnings);
  187. if (wo.Created) result.WorkOrderCreatedCount++;
  188. else result.WorkOrderUpdatedCount++;
  189. result.WorkOrders.Add(wo.WorkOrd);
  190. var check = await _resourceCheck.RunForEntryAsync(order, entry, wo.WorkOrd, account, warnings);
  191. result.ResourceCheckCount++;
  192. result.ResourceCheckLineCount += check.LineCount;
  193. if (check.HasShortage)
  194. warnings.Add($"订单行 {entry.EntrySeq} 存在缺料(工单 {wo.WorkOrd})");
  195. var capacityDate = entry.SysCapacityDate ?? entry.PlanDate;
  196. await UpdateEntryAfterReviewAsync(entry.Id, capacityDate, account);
  197. }
  198. result.EntryCount = entries.Count;
  199. result.Message = $"订单 {order.BillNo} 评审完成(资源检查 {result.ResourceCheckCount} 条)";
  200. }
  201. private async Task ConfirmOneOrderAsync(
  202. OrderWorkOrderGenerationService.OrderHeader order,
  203. SeOrderReviewExecuteResult result,
  204. List<string> warnings,
  205. string account)
  206. {
  207. var entries = await LoadReviewableEntriesAsync(order.Id, order.TenantId, ["2"]);
  208. if (entries.Count == 0)
  209. throw Oops.Oh($"订单 {order.BillNo} 没有处于评审状态的明细行,请先完成订单评审");
  210. foreach (var entry in entries)
  211. {
  212. var woCnt = await _db.Ado.GetIntAsync(
  213. """
  214. SELECT COUNT(*) FROM WorkOrdMaster
  215. WHERE tenant_id = @TenantId AND BusinessID = @EntryId
  216. AND LOWER(TRIM(IFNULL(Status,''))) <> 'c'
  217. """,
  218. new SugarParameter("@TenantId", entry.TenantId),
  219. new SugarParameter("@EntryId", entry.Id));
  220. if (woCnt == 0)
  221. throw Oops.Oh($"订单行 {entry.EntrySeq} 尚未生成工单,无法交期确认");
  222. var confirmDate = entry.SysCapacityDate ?? entry.PlanDate;
  223. await UpdateEntryAfterConfirmAsync(entry.Id, confirmDate, account);
  224. result.EntryCount++;
  225. result.WorkOrders.Add(await LoadWorkOrdForEntryAsync(entry.TenantId, entry.Id));
  226. }
  227. result.WorkOrders = result.WorkOrders.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList();
  228. result.Message = $"订单 {order.BillNo} 交期确认完成";
  229. }
  230. private async Task<string> LoadWorkOrdForEntryAsync(long tenantId, long entryId)
  231. {
  232. var rows = await _db.Ado.SqlQueryAsync<string>(
  233. """
  234. SELECT WorkOrd FROM WorkOrdMaster
  235. WHERE tenant_id = @TenantId AND BusinessID = @EntryId
  236. ORDER BY RecID DESC LIMIT 1
  237. """,
  238. new SugarParameter("@TenantId", tenantId),
  239. new SugarParameter("@EntryId", entryId));
  240. return rows.FirstOrDefault() ?? string.Empty;
  241. }
  242. private static void ValidateEntryForResourceCheck(OrderWorkOrderGenerationService.OrderEntryLine entry)
  243. {
  244. if (string.IsNullOrWhiteSpace(entry.ItemNumber))
  245. throw Oops.Oh($"订单行 {entry.EntrySeq} 物料编码不能为空");
  246. if (entry.Qty is null or <= 0)
  247. throw Oops.Oh($"订单行 {entry.EntrySeq} 数量必须大于 0");
  248. if (entry.PlanDate is null && entry.SysCapacityDate is null)
  249. throw Oops.Oh($"订单行 {entry.EntrySeq} 缺少计划交期");
  250. }
  251. private async Task<OrderWorkOrderGenerationService.OrderHeader?> LoadOrderAsync(long orderId, long tenantId)
  252. {
  253. var rows = await _db.Ado.SqlQueryAsync<OrderWorkOrderGenerationService.OrderHeader>(
  254. """
  255. SELECT Id, bill_no AS BillNo, custom_no AS CustomNo, urgent AS Urgent,
  256. factory_id AS FactoryId, tenant_id AS TenantId
  257. FROM crm_seorder
  258. WHERE Id = @Id AND tenant_id = @TenantId AND IsDeleted = 0
  259. LIMIT 1
  260. """,
  261. new SugarParameter("@Id", orderId),
  262. new SugarParameter("@TenantId", tenantId));
  263. return rows.FirstOrDefault();
  264. }
  265. private async Task<List<OrderWorkOrderGenerationService.OrderEntryLine>> LoadReviewableEntriesAsync(
  266. long orderId,
  267. long tenantId,
  268. IReadOnlyList<string> progressList)
  269. {
  270. if (progressList.Count == 0)
  271. return new List<OrderWorkOrderGenerationService.OrderEntryLine>();
  272. var inClause = string.Join(", ", progressList.Select((_, i) => $"@P{i}"));
  273. var pars = new List<SugarParameter>
  274. {
  275. new("@OrderId", orderId),
  276. new("@TenantId", tenantId)
  277. };
  278. for (var i = 0; i < progressList.Count; i++)
  279. pars.Add(new SugarParameter($"@P{i}", progressList[i]));
  280. return await _db.Ado.SqlQueryAsync<OrderWorkOrderGenerationService.OrderEntryLine>(
  281. $"""
  282. SELECT
  283. Id, seorder_id AS SeOrderId, bill_no AS BillNo, entry_seq AS EntrySeq,
  284. item_number AS ItemNumber, item_name AS ItemName, specification AS Specification,
  285. unit AS Unit, bom_number AS BomNumber, qty AS Qty,
  286. plan_date AS PlanDate, sys_capacity_date AS SysCapacityDate,
  287. progress AS Progress, urgent AS Urgent,
  288. factory_id AS FactoryId, company_id AS CompanyId, tenant_id AS TenantId
  289. FROM crm_seorderentry
  290. WHERE seorder_id = @OrderId AND tenant_id = @TenantId AND IsDeleted = 0
  291. AND COALESCE(NULLIF(progress, ''), '1') IN ({inClause})
  292. ORDER BY entry_seq, Id
  293. """,
  294. pars);
  295. }
  296. private async Task UpdateEntryAfterReviewAsync(long entryId, DateTime? capacityDate, string account)
  297. {
  298. await _db.Ado.ExecuteCommandAsync(
  299. """
  300. UPDATE crm_seorderentry
  301. SET progress = '2',
  302. sys_capacity_date = COALESCE(sys_capacity_date, @CapacityDate),
  303. update_time = @Now
  304. WHERE Id = @Id AND IsDeleted = 0
  305. """,
  306. new SugarParameter("@CapacityDate", capacityDate ?? (object)DBNull.Value),
  307. new SugarParameter("@Now", DateTime.Now),
  308. new SugarParameter("@Id", entryId));
  309. }
  310. private async Task UpdateEntryAfterConfirmAsync(long entryId, DateTime? confirmDate, string account)
  311. {
  312. await _db.Ado.ExecuteCommandAsync(
  313. """
  314. UPDATE crm_seorderentry
  315. SET progress = '3',
  316. date = COALESCE(date, @ConfirmDate),
  317. update_time = @Now
  318. WHERE Id = @Id AND IsDeleted = 0
  319. """,
  320. new SugarParameter("@ConfirmDate", confirmDate ?? (object)DBNull.Value),
  321. new SugarParameter("@Now", DateTime.Now),
  322. new SugarParameter("@Id", entryId));
  323. }
  324. private async Task UpdateEntriesProgressAsync(IReadOnlyList<long> entryIds, string progress, string account)
  325. {
  326. if (entryIds.Count == 0) return;
  327. var idList = string.Join(",", entryIds);
  328. await _db.Ado.ExecuteCommandAsync(
  329. $"""
  330. UPDATE crm_seorderentry
  331. SET progress = @Progress, update_time = @Now
  332. WHERE Id IN ({idList}) AND IsDeleted = 0
  333. """,
  334. new SugarParameter("@Progress", progress),
  335. new SugarParameter("@Now", DateTime.Now));
  336. }
  337. private static string BuildAggregateMessage(string actionCode, SeOrderReviewExecuteResult r)
  338. {
  339. var woPart = r.WorkOrders.Count > 0
  340. ? $",工单:{string.Join("、", r.WorkOrders.Distinct())}"
  341. : string.Empty;
  342. return actionCode switch
  343. {
  344. ActionReview => $"评审完成 {r.OrderCount} 单、{r.EntryCount} 行,新建工单 {r.WorkOrderCreatedCount}、更新 {r.WorkOrderUpdatedCount}、资源检查 {r.ResourceCheckCount} 条{woPart}",
  345. ActionConfirm => $"交期确认完成 {r.OrderCount} 单、{r.EntryCount} 行{woPart}",
  346. _ => r.Message
  347. };
  348. }
  349. private static string BuildSingleMessage(SeOrderReviewExecuteResult r)
  350. {
  351. if (r.WorkOrders.Count == 0)
  352. return r.Message;
  353. return $"{r.Message},工单:{string.Join("、", r.WorkOrders.Distinct())}";
  354. }
  355. private async Task<List<string>> TryTriggerMdpRefreshAsync()
  356. {
  357. var warnings = new List<string>();
  358. try
  359. {
  360. await _mdpSync.RunFullAsync(triggerType: "ORDER_REVIEW");
  361. }
  362. catch (Exception ex)
  363. {
  364. warnings.Add($"S1 MDP/DWD 刷新未完成:{ex.Message}");
  365. }
  366. return warnings;
  367. }
  368. }