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.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} 没有可评审的明细行(须为新建,评审,再评审状态)");
var reviewedEntryIds = new List();
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);
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);
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} 个历史工单");
}
}
reviewedEntryIds.Add(entry.Id);
}
// 3. 所有明细行处理完毕后,批量更新 progress = '2'
await UpdateEntriesProgressAsync(reviewedEntryIds, "2", 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)
result.WorkOrders.Add(await LoadWorkOrdForEntryAsync(entry.TenantId, entry.Id));
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 = 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;
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;
}
}