using Admin.NET.Plugin.AiDOP.Infrastructure; using Admin.NET.Plugin.AiDOP.WorkOrder; namespace Admin.NET.Plugin.AiDOP.Order; /// /// S1 订单评审编排:资源检查、状态更新、工单生成、运行日志。 /// public class OrderReviewOrchestrationService : ITransient { public const string ActionReview = "S1_ORDER_REVIEW"; public const string ActionConfirm = "S1_DELIVERY_CONFIRM"; public const string ActionRefresh = "S1_ORDER_REFRESH_PLAN"; /// 订单评审资源检查批次ID,用于跨工单库存占用递减。 private const long ReviewBangId = 2; private readonly ISqlSugarClient _db; private readonly UserManager _userManager; private readonly OrderWorkOrderGenerationService _workOrderGen; private readonly OrderResourceCheckService _resourceCheck; private readonly WorkOrderMaterialDetailSyncService _materialDetailSync; private readonly WorkOrderRoutingSyncService _routingSync; private readonly S1MdpSyncTransformService _mdpSync; private readonly AidopActionRunLogWriter _runLog; public OrderReviewOrchestrationService( ISqlSugarClient db, UserManager userManager, OrderWorkOrderGenerationService workOrderGen, OrderResourceCheckService resourceCheck, WorkOrderMaterialDetailSyncService materialDetailSync, WorkOrderRoutingSyncService routingSync, S1MdpSyncTransformService mdpSync, AidopActionRunLogWriter runLog) { _db = db; _userManager = userManager; _workOrderGen = workOrderGen; _resourceCheck = resourceCheck; _materialDetailSync = materialDetailSync; _routingSync = routingSync; _mdpSync = mdpSync; _runLog = runLog; } public Task ReviewAsync(IReadOnlyList orderIds) => ExecuteBatchAsync(ActionReview, orderIds, ReviewOneOrderAsync); public Task ConfirmDeliveryAsync(IReadOnlyList orderIds) => ExecuteBatchAsync(ActionConfirm, orderIds, ConfirmOneOrderAsync); public Task RefreshPlanAsync(long orderId, string? reason) => ExecuteSingleAsync(ActionRefresh, orderId, async (order, result, warnings, account) => { var entries = await LoadReviewableEntriesAsync(order.Id, order.TenantId, ["3", "0"]); if (entries.Count == 0) throw Oops.Oh("订单没有可重排的确认/再评审明细行"); foreach (var entry in entries) { ValidateEntryForResourceCheck(entry); var wo = await _workOrderGen.CreateOrUpdateForEntryAsync(order, entry, account, warnings); if (wo.Created) result.WorkOrderCreatedCount++; else result.WorkOrderUpdatedCount++; result.WorkOrders.Add(wo.WorkOrd); var check = await _resourceCheck.RunForEntryAsync(order, entry, wo.WorkOrd, account, warnings, ReviewBangId); result.ResourceCheckCount++; result.ResourceCheckLineCount += check.LineCount; await _materialDetailSync.EnsureFromResourceCheckAsync(entry.TenantId, wo.WorkOrd, account); await _routingSync.EnsureFromRoutingAsync(entry.TenantId, wo.WorkOrd, account); } await UpdateEntriesProgressAsync(entries.Select(e => e.Id).ToList(), "0", account); result.EntryCount = entries.Count; result.Message = string.IsNullOrWhiteSpace(reason) ? "3级计划重排完成" : $"3级计划重排完成:{reason.Trim()}"; }); private async Task ExecuteBatchAsync( string actionCode, IReadOnlyList orderIds, Func, string, Task> handler) { if (orderIds is null || orderIds.Count == 0) throw Oops.Oh("至少选择一条订单"); var tenantId = _userManager.TenantId > 0 ? _userManager.TenantId : AidopTenantHelper.Resolve(App.HttpContext); var account = _userManager.Account ?? "system"; var distinctIds = orderIds.Distinct().ToList(); var aggregate = new SeOrderReviewExecuteResult { ActionCode = actionCode, OrderCount = distinctIds.Count, Message = "执行成功" }; var allWarnings = new List(); long? firstLogId = null; // 评审/重排前清理旧占用记录,确保跨工单库存递减正确 if (actionCode == ActionReview || actionCode == ActionRefresh) { await _db.Ado.ExecuteCommandAsync( "DELETE FROM ic_item_stockoccupy WHERE tenant_id = @TenantId AND bang_id = @BangId", new SugarParameter("@TenantId", tenantId), new SugarParameter("@BangId", ReviewBangId)); await _db.Ado.ExecuteCommandAsync( "DELETE FROM srm_po_occupy WHERE tenant_id = @TenantId AND bang_id = @BangId", new SugarParameter("@TenantId", tenantId), new SugarParameter("@BangId", ReviewBangId)); } foreach (var orderId in distinctIds) { var order = await LoadOrderAsync(orderId, tenantId) ?? throw Oops.Oh($"订单 {orderId} 不存在或不属于当前租户"); var runLogId = await _runLog.StartAsync(actionCode, tenantId, "crm_seorder", order.Id, order.BillNo); if (firstLogId is null) firstLogId = runLogId; var perOrder = new SeOrderReviewExecuteResult { ActionCode = actionCode }; var warnings = new List(); try { await _db.Ado.BeginTranAsync(); await handler(order, perOrder, warnings, account); await _db.Ado.CommitTranAsync(); aggregate.EntryCount += perOrder.EntryCount; aggregate.WorkOrderCreatedCount += perOrder.WorkOrderCreatedCount; aggregate.WorkOrderUpdatedCount += perOrder.WorkOrderUpdatedCount; aggregate.WorkOrderClosedCount += perOrder.WorkOrderClosedCount; aggregate.ResourceCheckCount += perOrder.ResourceCheckCount; aggregate.ResourceCheckLineCount += perOrder.ResourceCheckLineCount; aggregate.WorkOrders.AddRange(perOrder.WorkOrders); allWarnings.AddRange(warnings); await _runLog.SuccessAsync(runLogId, perOrder.Message, new { orderId = order.Id, billNo = order.BillNo, perOrder.EntryCount, perOrder.WorkOrderCreatedCount, perOrder.WorkOrderUpdatedCount, workOrders = perOrder.WorkOrders, warnings }); } catch (Exception ex) { await _db.Ado.RollbackTranAsync(); await _runLog.FailedAsync(runLogId, ex.Message, new { orderId = order.Id, billNo = order.BillNo }); throw Oops.Oh(ex.Message); } } aggregate.RunLogId = firstLogId ?? 0; aggregate.Warnings = allWarnings.Distinct().ToList(); aggregate.Message = BuildAggregateMessage(actionCode, aggregate); if (actionCode == ActionReview && aggregate.ResourceCheckCount > 0) aggregate.Warnings.AddRange(await TryTriggerMdpRefreshAsync()); return aggregate; } private async Task ExecuteSingleAsync( string actionCode, long orderId, Func, string, Task> handler) { var tenantId = _userManager.TenantId > 0 ? _userManager.TenantId : AidopTenantHelper.Resolve(App.HttpContext); var account = _userManager.Account ?? "system"; var order = await LoadOrderAsync(orderId, tenantId) ?? throw Oops.Oh("订单不存在或不属于当前租户"); var result = new SeOrderReviewExecuteResult { ActionCode = actionCode, OrderCount = 1 }; var warnings = new List(); var runLogId = await _runLog.StartAsync(actionCode, tenantId, "crm_seorder", order.Id, order.BillNo); result.RunLogId = runLogId; try { await _db.Ado.BeginTranAsync(); await handler(order, result, warnings, account); await _db.Ado.CommitTranAsync(); result.Warnings = warnings; result.Message = BuildSingleMessage(result); await _runLog.SuccessAsync(runLogId, result.Message, new { orderId = order.Id, billNo = order.BillNo, result.EntryCount, result.WorkOrderCreatedCount, result.WorkOrderUpdatedCount, workOrders = result.WorkOrders, warnings }); if (actionCode == ActionRefresh && result.ResourceCheckCount > 0) result.Warnings.AddRange(await TryTriggerMdpRefreshAsync()); return result; } catch (Exception ex) { await _db.Ado.RollbackTranAsync(); await _runLog.FailedAsync(runLogId, ex.Message, new { orderId = order.Id, billNo = order.BillNo }); throw Oops.Oh(ex.Message); } } private async Task ReviewOneOrderAsync( OrderWorkOrderGenerationService.OrderHeader order, SeOrderReviewExecuteResult result, List warnings, string account) { var entries = await LoadReviewableEntriesAsync(order.Id, order.TenantId, ["1", "0", "2"]); if (entries.Count == 0) throw Oops.Oh($"订单 {order.BillNo} 没有可评审的明细行(须为新建,评审,再评审状态)"); foreach (var entry in entries) { ValidateEntryForResourceCheck(entry); if (entry.PlanDate is null) throw Oops.Oh($"订单行 {entry.EntrySeq} 缺少客户要求交期(plan_date)"); // 1. 先做资源检查(纯 BOM 展开 + 库存计算,不依赖工单),记录库存占用 var (check, lines) = await _resourceCheck.CheckOnlyAsync(order, entry, warnings, ReviewBangId); result.ResourceCheckCount++; result.ResourceCheckLineCount += check.LineCount; // 2. 有缺料 → 才生成工单并写入资源检查结果;库存可满足则跳过工单 if (check.HasShortage) { var wo = await _workOrderGen.CreateOrUpdateForEntryAsync(order, entry, account, warnings); if (wo.Created) result.WorkOrderCreatedCount++; else result.WorkOrderUpdatedCount++; result.WorkOrders.Add(wo.WorkOrd); await _resourceCheck.WriteResultAsync(order, entry, wo.WorkOrd, lines, account); await _materialDetailSync.EnsureFromResourceCheckAsync(entry.TenantId, wo.WorkOrd, account); await _routingSync.EnsureFromRoutingAsync(entry.TenantId, wo.WorkOrd, account); warnings.Add($"订单行 {entry.EntrySeq} 存在缺料(工单 {wo.WorkOrd})"); } else { // 3. 不缺料 → 若之前已生成工单则关闭(Status='C', IsActive=0) var closedCount = await CloseExistingWorkOrdersAsync(entry.TenantId, entry.Id, account); if (closedCount > 0) { result.WorkOrderClosedCount += closedCount; warnings.Add($"订单行 {entry.EntrySeq} 库存可满足,已关闭 {closedCount} 个历史工单"); } } await UpdateEntryAfterReviewAsync(entry.Id, check.KittingTime, account); } result.EntryCount = entries.Count; result.Message = $"订单 {order.BillNo} 评审完成(资源检查 {result.ResourceCheckCount} 条)"; } private async Task ConfirmOneOrderAsync( OrderWorkOrderGenerationService.OrderHeader order, SeOrderReviewExecuteResult result, List warnings, string account) { var entries = await LoadReviewableEntriesAsync(order.Id, order.TenantId, ["2"]); if (entries.Count == 0) throw Oops.Oh($"订单 {order.BillNo} 没有处于评审状态的明细行,请先完成订单评审"); foreach (var entry in entries) { var woCnt = await _db.Ado.GetIntAsync( """ SELECT COUNT(*) FROM WorkOrdMaster WHERE tenant_id = @TenantId AND BusinessID = @EntryId AND LOWER(TRIM(IFNULL(Status,''))) <> 'c' """, new SugarParameter("@TenantId", entry.TenantId), new SugarParameter("@EntryId", entry.Id)); // 无活跃工单:可能从未生成(库存可满足)或已关闭 → 仍允许交期确认 var confirmDate = entry.SysCapacityDate ?? entry.PlanDate; await UpdateEntryAfterConfirmAsync(entry.Id, confirmDate, account); result.EntryCount++; if (woCnt > 0) { var workOrd = await LoadWorkOrdForEntryAsync(entry.TenantId, entry.Id); result.WorkOrders.Add(workOrd); // 交期确认后将工单状态设为 p if (!string.IsNullOrWhiteSpace(workOrd)) { await _db.Ado.ExecuteCommandAsync( """ UPDATE WorkOrdMaster SET Status = 'p', UpdateUser = @User, UpdateTime = @Now WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd AND IFNULL(TRIM(Status), '') = '' """, new SugarParameter("@TenantId", entry.TenantId), new SugarParameter("@WorkOrd", workOrd), new SugarParameter("@User", account), new SugarParameter("@Now", DateTime.Now)); } } else warnings.Add($"订单行 {entry.EntrySeq} 无活跃工单(库存可满足或已关闭),已直接确认交期"); } result.WorkOrders = result.WorkOrders.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList(); result.Message = $"订单 {order.BillNo} 交期确认完成"; } private async Task LoadWorkOrdForEntryAsync(long tenantId, long entryId) { var rows = await _db.Ado.SqlQueryAsync( """ SELECT WorkOrd FROM WorkOrdMaster WHERE tenant_id = @TenantId AND BusinessID = @EntryId ORDER BY RecID DESC LIMIT 1 """, new SugarParameter("@TenantId", tenantId), new SugarParameter("@EntryId", entryId)); return rows.FirstOrDefault() ?? string.Empty; } /// /// 关闭指定订单行已有的未关闭工单(WorkOrdMaster.Status='C', IsActive=0;mes_morder.morder_state='关闭')。 /// 返回被关闭的工单数量。 /// private async Task CloseExistingWorkOrdersAsync(long tenantId, long entryId, string account) { // 查找该订单行下所有未关闭的工单号 var openWorkOrds = await _db.Ado.SqlQueryAsync( """ SELECT WorkOrd FROM WorkOrdMaster WHERE tenant_id = @TenantId AND BusinessID = @EntryId AND LOWER(TRIM(IFNULL(Status,''))) <> 'c' AND IFNULL(IsActive, 0) = 1 """, new SugarParameter("@TenantId", tenantId), new SugarParameter("@EntryId", entryId)); if (openWorkOrds.Count == 0) return 0; var workOrdList = string.Join(",", openWorkOrds.Select(w => $"'{w}'")); var now = DateTime.Now; // 关闭 WorkOrdMaster await _db.Ado.ExecuteCommandAsync( $""" UPDATE WorkOrdMaster SET Status = 'C', IsActive = 0, UpdateUser = @User, UpdateTime = @Now WHERE tenant_id = @TenantId AND BusinessID = @EntryId AND LOWER(TRIM(IFNULL(Status,''))) <> 'c' AND IFNULL(IsActive, 0) = 1 """, new SugarParameter("@TenantId", tenantId), new SugarParameter("@EntryId", entryId), new SugarParameter("@User", account), new SugarParameter("@Now", now)); // 同步关闭 mes_morder await _db.Ado.ExecuteCommandAsync( $""" UPDATE mes_morder SET morder_state = '关闭', update_by_name = @User, update_time = @Now WHERE tenant_id = @TenantId AND morder_no IN ({workOrdList}) AND IFNULL(morder_state, '') <> '关闭' """, new SugarParameter("@TenantId", tenantId), new SugarParameter("@User", account), new SugarParameter("@Now", now)); return openWorkOrds.Count; } private static void ValidateEntryForResourceCheck(OrderWorkOrderGenerationService.OrderEntryLine entry) { if (string.IsNullOrWhiteSpace(entry.ItemNumber)) throw Oops.Oh($"订单行 {entry.EntrySeq} 物料编码不能为空"); if (entry.Qty is null or <= 0) throw Oops.Oh($"订单行 {entry.EntrySeq} 数量必须大于 0"); if (entry.PlanDate is null && entry.SysCapacityDate is null) throw Oops.Oh($"订单行 {entry.EntrySeq} 缺少计划交期"); } private async Task LoadOrderAsync(long orderId, long tenantId) { var rows = await _db.Ado.SqlQueryAsync( """ SELECT Id, bill_no AS BillNo, custom_no AS CustomNo, urgent AS Urgent, factory_id AS FactoryId, tenant_id AS TenantId FROM crm_seorder WHERE Id = @Id AND tenant_id = @TenantId AND IsDeleted = 0 LIMIT 1 """, new SugarParameter("@Id", orderId), new SugarParameter("@TenantId", tenantId)); return rows.FirstOrDefault(); } private async Task> LoadReviewableEntriesAsync( long orderId, long tenantId, IReadOnlyList progressList) { if (progressList.Count == 0) return new List(); var inClause = string.Join(", ", progressList.Select((_, i) => $"@P{i}")); var pars = new List { new("@OrderId", orderId), new("@TenantId", tenantId) }; for (var i = 0; i < progressList.Count; i++) pars.Add(new SugarParameter($"@P{i}", progressList[i])); return await _db.Ado.SqlQueryAsync( $""" SELECT Id, seorder_id AS SeOrderId, bill_no AS BillNo, entry_seq AS EntrySeq, item_number AS ItemNumber, item_name AS ItemName, specification AS Specification, unit AS Unit, bom_number AS BomNumber, qty AS Qty, plan_date AS PlanDate, sys_capacity_date AS SysCapacityDate, progress AS Progress, urgent AS Urgent, factory_id AS FactoryId, company_id AS CompanyId, tenant_id AS TenantId FROM crm_seorderentry WHERE seorder_id = @OrderId AND tenant_id = @TenantId AND IsDeleted = 0 AND COALESCE(NULLIF(progress, ''), '1') IN ({inClause}) ORDER BY entry_seq, Id """, pars); } private async Task UpdateEntryAfterReviewAsync(long entryId, DateTime? capacityDate, string account) { await _db.Ado.ExecuteCommandAsync( """ UPDATE crm_seorderentry SET progress = '2', sys_capacity_date = @CapacityDate, update_time = @Now WHERE Id = @Id AND IsDeleted = 0 """, new SugarParameter("@CapacityDate", capacityDate ?? (object)DBNull.Value), new SugarParameter("@Now", DateTime.Now), new SugarParameter("@Id", entryId)); } private async Task UpdateEntryAfterConfirmAsync(long entryId, DateTime? confirmDate, string account) { await _db.Ado.ExecuteCommandAsync( """ UPDATE crm_seorderentry SET progress = '3', date = COALESCE(date, @ConfirmDate), update_time = @Now WHERE Id = @Id AND IsDeleted = 0 """, new SugarParameter("@ConfirmDate", confirmDate ?? (object)DBNull.Value), new SugarParameter("@Now", DateTime.Now), new SugarParameter("@Id", entryId)); } private async Task UpdateEntriesProgressAsync(IReadOnlyList entryIds, string progress, string account) { if (entryIds.Count == 0) return; var idList = string.Join(",", entryIds); await _db.Ado.ExecuteCommandAsync( $""" UPDATE crm_seorderentry SET progress = @Progress, update_time = @Now WHERE Id IN ({idList}) AND IsDeleted = 0 """, new SugarParameter("@Progress", progress), new SugarParameter("@Now", DateTime.Now)); } private static string BuildAggregateMessage(string actionCode, SeOrderReviewExecuteResult r) { var woPart = r.WorkOrders.Count > 0 ? $",工单:{string.Join("、", r.WorkOrders.Distinct())}" : string.Empty; var closedPart = r.WorkOrderClosedCount > 0 ? $"、关闭 {r.WorkOrderClosedCount}" : string.Empty; return actionCode switch { ActionReview => $"评审完成 {r.OrderCount} 单、{r.EntryCount} 行,新建工单 {r.WorkOrderCreatedCount}、更新 {r.WorkOrderUpdatedCount}{closedPart}、资源检查 {r.ResourceCheckCount} 条{woPart}", ActionConfirm => $"交期确认完成 {r.OrderCount} 单、{r.EntryCount} 行{woPart}", _ => r.Message }; } private static string BuildSingleMessage(SeOrderReviewExecuteResult r) { if (r.WorkOrders.Count == 0) return r.Message; return $"{r.Message},工单:{string.Join("、", r.WorkOrders.Distinct())}"; } private async Task> TryTriggerMdpRefreshAsync() { var warnings = new List(); try { await _mdpSync.RunFullAsync(triggerType: "ORDER_REVIEW"); } catch (Exception ex) { warnings.Add($"S1 MDP/DWD 刷新未完成:{ex.Message}"); } return warnings; } }