using Admin.NET.Plugin.AiDOP.Infrastructure; 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"; private readonly ISqlSugarClient _db; private readonly UserManager _userManager; private readonly OrderWorkOrderGenerationService _workOrderGen; private readonly OrderResourceCheckService _resourceCheck; private readonly S1MdpSyncTransformService _mdpSync; private readonly AidopActionRunLogWriter _runLog; public OrderReviewOrchestrationService( ISqlSugarClient db, UserManager userManager, OrderWorkOrderGenerationService workOrderGen, OrderResourceCheckService resourceCheck, S1MdpSyncTransformService mdpSync, AidopActionRunLogWriter runLog) { _db = db; _userManager = userManager; _workOrderGen = workOrderGen; _resourceCheck = resourceCheck; _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); result.ResourceCheckCount++; result.ResourceCheckLineCount += check.LineCount; } 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; 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.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"]); 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)"); 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); result.ResourceCheckCount++; result.ResourceCheckLineCount += check.LineCount; if (check.HasShortage) warnings.Add($"订单行 {entry.EntrySeq} 存在缺料(工单 {wo.WorkOrd})"); var capacityDate = entry.SysCapacityDate ?? entry.PlanDate; await UpdateEntryAfterReviewAsync(entry.Id, capacityDate, 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)); if (woCnt == 0) throw Oops.Oh($"订单行 {entry.EntrySeq} 尚未生成工单,无法交期确认"); var confirmDate = entry.SysCapacityDate ?? entry.PlanDate; await UpdateEntryAfterConfirmAsync(entry.Id, confirmDate, account); result.EntryCount++; result.WorkOrders.Add(await LoadWorkOrdForEntryAsync(entry.TenantId, entry.Id)); } 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; } 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 = COALESCE(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; return actionCode switch { ActionReview => $"评审完成 {r.OrderCount} 单、{r.EntryCount} 行,新建工单 {r.WorkOrderCreatedCount}、更新 {r.WorkOrderUpdatedCount}、资源检查 {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; } }