OrderReviewOrchestrationService.cs 22 KB

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