OrderReviewOrchestrationService.cs 23 KB

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