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;
}
}