using Fake.Application; using Fake.Domain.Exceptions; using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Orders; using Oms.Application.Services.Orders.Issues; using Oms.Application.Shared; using Oms.Domain.ChannelsAggregate.Consts; using Oms.Domain.ChannelsAggregate.Entities; using Oms.Domain.ChannelsAggregate.Repositories; using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.OrdersAggregate.Repositories; using Oms.Domain.Shared.Enums; namespace Oms.Application.Services.Orders; public partial class OrderService { private const int MaxExceptionBatchSize = 200; private IIssueTemplateRepository IssueTemplateRepository => LazyServiceProvider.GetRequiredService(); private IOrderExceptionEventRepository OrderExceptionEventRepository => LazyServiceProvider.GetRequiredService(); private OrderExceptionWorkflowService ExceptionWorkflowService => LazyServiceProvider.GetRequiredService(); /// /// 批量创建问题件。 /// [HttpPost("/order/mark-problematic")] public async Task MarkProblematicAsync([FromBody] OrderMarkProblematicInput input) { var payload = await ResolveProblematicPayloadAsync(input); var request = new ProblematicMarkRequest { IssueType = payload.IssueType, Title = payload.Title, Content = payload.Content, IssueTemplateId = payload.IssueTemplateId, NotifyMethod = payload.NotifyMethod, HandlerNames = payload.HandlerNames, Remark = payload.Remark }; return await ExecuteProblematicBatchAsync(input.OrderIds, request); } /// /// 批量创建理赔件。 /// [HttpPost("/order/mark-claimed")] public async Task MarkClaimedAsync([FromBody] OrderMarkClaimedInput input) { var payload = NormalizeClaimedPayload(input); return await ExecuteExceptionBatchAsync( input.OrderIds, order => TryApplyClaimed(order, payload), (order, remark) => new OrderExceptionEvent { OrderId = order.Id, EventType = OrderExceptionEventTypeCodes.MarkClaimed, FromStatus = order.PreviousOrderStatus, ToStatus = OrderStatusCodes.Claimed, FromHandleStatus = null, ToHandleStatus = OrderHandleStatusCodes.Pending, ClaimType = order.ClaimType, ClaimAmount = order.ClaimAmount, ClaimCurrency = order.ClaimCurrency, ClaimReason = order.ClaimReason, Remark = remark }, payload.Remark); } /// /// 批量更新处理子状态。 /// [HttpPost("/order/handle-status/update")] public async Task UpdateHandleStatusAsync([FromBody] OrderHandleStatusUpdateInput input) { var targetStatus = input.HandleStatus?.Trim().ToLowerInvariant(); if (!string.Equals(targetStatus, OrderHandleStatusCodes.Processed, StringComparison.Ordinal)) { throw new BusinessException("仅支持标记为已处理"); } return await ExecuteExceptionBatchAsync( input.OrderIds, order => { if (order.OrderStatus is not (OrderStatusCodes.Problematic or OrderStatusCodes.Claimed)) { return "仅问题件或理赔件可更新处理状态"; } if (OrderHandleStatusCodes.IsProcessed(order.HandleStatus)) { return "订单已处理"; } order.HandleStatus = OrderHandleStatusCodes.Processed; return null; }, (order, remark) => new OrderExceptionEvent { OrderId = order.Id, EventType = OrderExceptionEventTypeCodes.HandleProcessed, FromStatus = order.OrderStatus, ToStatus = order.OrderStatus, FromHandleStatus = OrderHandleStatusCodes.Pending, ToHandleStatus = OrderHandleStatusCodes.Processed, Remark = remark }, input.Remark?.Trim()); } /// /// 订单问题处理记录列表(mark_problematic 事件,支持一单多条)。 /// [HttpGet("/order/exception-events/list")] public async Task GetExceptionEventListAsync([FromQuery] int orderId) { if (orderId <= 0) { throw new BusinessException("订单Id无效"); } await GetOrderOrThrowAsync(orderId); var events = await OrderExceptionEventRepository.GetListAsync(x => x.OrderId == orderId && x.EventType == OrderExceptionEventTypeCodes.MarkProblematic); events = events.OrderByDescending(x => x.CreateTime).ThenByDescending(x => x.Id).ToList(); var userIds = events .SelectMany(x => new int?[] { x.CreateUserId, x.UpdateUserId, x.HandlerUserId }) .Where(x => x is > 0) .Select(x => x!.Value) .Distinct(); var auditUserNameMap = await UserDisplayNameResolver.ResolveManyAsync(userIds); var items = events.Select(evt => MapExceptionEventOutput(evt, auditUserNameMap)).ToList(); return new OrderExceptionEventListOutput { Items = items, PendingCount = items.Count(x => OrderHandleStatusCodes.IsPending(x.HandleStatus)), ProcessedCount = items.Count(x => OrderHandleStatusCodes.IsProcessed(x.HandleStatus)) }; } /// /// 单条问题标记已处理。 /// [HttpPost("/order/exception-event/process")] public async Task ProcessExceptionEventAsync([FromBody] OrderExceptionEventProcessInput input) { if (input.Id <= 0) { throw new BusinessException("问题记录Id无效"); } var evt = await OrderExceptionEventRepository.FirstOrDefaultAsync(x => x.Id == input.Id); if (evt == null || !string.Equals(evt.EventType, OrderExceptionEventTypeCodes.MarkProblematic, StringComparison.OrdinalIgnoreCase)) { throw new BusinessException("问题记录不存在"); } await GetOrderOrThrowAsync(evt.OrderId); if (OrderHandleStatusCodes.IsProcessed(evt.ToHandleStatus)) { throw new BusinessException("该问题已处理"); } var remark = input.Remark?.Trim(); if (!string.IsNullOrWhiteSpace(remark) && remark.Length > 512) { throw new BusinessException("备注长度不能超过512"); } var operatorUserId = CurrentUser.GetId(); evt.ToHandleStatus = OrderHandleStatusCodes.Processed; evt.UpdateUserId = operatorUserId; evt.UpdateTime = DateTime.Now; if (!string.IsNullOrWhiteSpace(remark)) { evt.Remark = remark; } await OrderExceptionEventRepository.UpdateAsync(evt); await OrderExceptionEventRepository.InsertAsync(new OrderExceptionEvent { OrderId = evt.OrderId, EventType = OrderExceptionEventTypeCodes.HandleProcessed, FromStatus = OrderStatusCodes.Problematic, ToStatus = OrderStatusCodes.Problematic, FromHandleStatus = OrderHandleStatusCodes.Pending, ToHandleStatus = OrderHandleStatusCodes.Processed, IssueType = evt.IssueType, Title = evt.Title, Content = evt.Content, Remark = remark }); await SyncOrderHandleStatusFromEventsAsync(evt.OrderId); } private async Task SyncOrderHandleStatusFromEventsAsync(int orderId) { var order = await GetOrderOrThrowAsync(orderId); if (order.OrderStatus is not (OrderStatusCodes.Problematic or OrderStatusCodes.Claimed)) { return; } var issues = await OrderExceptionEventRepository.GetListAsync(x => x.OrderId == orderId && x.EventType == OrderExceptionEventTypeCodes.MarkProblematic); if (issues.Count == 0) { return; } var anyPending = issues.Any(x => OrderHandleStatusCodes.IsPending(x.ToHandleStatus)); order.HandleStatus = anyPending ? OrderHandleStatusCodes.Pending : OrderHandleStatusCodes.Processed; order.UpdateTime = DateTime.Now; await OrderRepository.UpdateAsync(order); } private static OrderExceptionEventOutput MapExceptionEventOutput( OrderExceptionEvent evt, IReadOnlyDictionary auditUserNameMap) { var notifyCodes = IssueNotifyMethodCodes.ParseList(evt.NotifyMethod); var notifyNames = notifyCodes.Count == 0 ? string.Empty : string.Join('、', notifyCodes.Select(IssueNotifyMethodCodes.GetDisplayName)); return new OrderExceptionEventOutput { Id = evt.Id, OrderId = evt.OrderId, EventType = evt.EventType, IssueType = evt.IssueType, IssueTypeName = IssueTypeCodes.GetDisplayName(evt.IssueType), Title = evt.Title, Content = evt.Content, NotifyMethod = evt.NotifyMethod, NotifyMethodNames = notifyNames, HandlerNames = evt.HandlerNames ?? (evt.HandlerUserId is int handlerUserId && handlerUserId > 0 ? OmsRbacUserDisplayNameResolver.FormatUserId(handlerUserId, auditUserNameMap) : null), HandlerType = evt.HandlerType, HandlerTypeName = IssueHandlerTypeCodes.GetDisplayName(evt.HandlerType), HandlerUserId = evt.HandlerUserId, HandleStatus = evt.ToHandleStatus, HandleStatusName = OrderHandleStatusCodes.GetDisplayName(evt.ToHandleStatus), CreateBy = OmsRbacUserDisplayNameResolver.FormatUserId(evt.CreateUserId, auditUserNameMap), CreateTime = evt.CreateTime, ProcessedBy = OrderHandleStatusCodes.IsProcessed(evt.ToHandleStatus) ? OmsRbacUserDisplayNameResolver.FormatUserId(evt.UpdateUserId, auditUserNameMap) : null, ProcessedTime = OrderHandleStatusCodes.IsProcessed(evt.ToHandleStatus) ? evt.UpdateTime : null, Remark = evt.Remark }; } private async Task ExecuteProblematicBatchAsync( IReadOnlyCollection orderIds, ProblematicMarkRequest request) { var ids = NormalizeOrderIds(orderIds); var result = new OrderBatchActionResultOutput(); var operatorUserId = CurrentUser.GetId(); var sendNotification = !string.IsNullOrWhiteSpace(request.NotifyMethod); foreach (var orderId in ids) { Order? order; try { order = await GetOrderOrThrowAsync(orderId); } catch (BusinessException ex) { result.Failures.Add(new OrderBatchActionFailureOutput { OrderId = orderId, Reason = ex.Message }); continue; } var reason = await ExceptionWorkflowService.TryMarkProblematicAsync( order, request, operatorUserId, sendNotification); if (!string.IsNullOrWhiteSpace(reason)) { result.Failures.Add(new OrderBatchActionFailureOutput { OrderId = order.Id, ServiceNo = order.ServiceNo, Reason = reason }); continue; } result.SuccessCount++; } return result; } private async Task ExecuteExceptionBatchAsync( IReadOnlyCollection orderIds, Func tryApply, Func buildEvent, string? remark) { var ids = NormalizeOrderIds(orderIds); var result = new OrderBatchActionResultOutput(); foreach (var orderId in ids) { Order? order; try { order = await GetOrderOrThrowAsync(orderId); } catch (BusinessException ex) { result.Failures.Add(new OrderBatchActionFailureOutput { OrderId = orderId, Reason = ex.Message }); continue; } var reason = tryApply(order); if (!string.IsNullOrWhiteSpace(reason)) { result.Failures.Add(new OrderBatchActionFailureOutput { OrderId = order.Id, ServiceNo = order.ServiceNo, Reason = reason }); continue; } await OrderRepository.UpdateAsync(order); await OrderExceptionEventRepository.InsertAsync(buildEvent(order, remark)); result.SuccessCount++; } return result; } private static List NormalizeOrderIds(IReadOnlyCollection? orderIds) { if (orderIds == null || orderIds.Count == 0) { throw new BusinessException("请至少选择一条订单"); } var ids = orderIds.Where(x => x > 0).Distinct().ToList(); if (ids.Count == 0) { throw new BusinessException("请至少选择一条订单"); } if (ids.Count > MaxExceptionBatchSize) { throw new BusinessException($"单次最多处理 {MaxExceptionBatchSize} 条订单"); } return ids; } private async Task ResolveProblematicPayloadAsync(OrderMarkProblematicInput input) { IssueTemplate? template = null; if (input.IssueTemplateId is int templateId && templateId > 0) { template = await IssueTemplateRepository.FirstOrDefaultAsync(x => x.Id == templateId && x.Status == EnableStatus.Enabled); if (template == null) { throw new BusinessException("问题模板不存在或未启用"); } } var issueType = (template?.IssueType ?? input.IssueType)?.Trim().ToLowerInvariant(); if (string.IsNullOrWhiteSpace(issueType) || !IssueTypeCodes.All.Contains(issueType)) { throw new BusinessException("问题类型不合法"); } var title = (input.Title?.Trim().Length > 0 ? input.Title.Trim() : template?.Title)?.Trim(); if (string.IsNullOrWhiteSpace(title)) { throw new BusinessException("问题标题不能为空"); } if (title.Length > 128) { throw new BusinessException("问题标题长度不能超过128"); } var content = (input.Content?.Trim().Length > 0 ? input.Content.Trim() : template?.Content)?.Trim(); if (string.IsNullOrWhiteSpace(content)) { throw new BusinessException("问题内容不能为空"); } var remark = input.Remark?.Trim(); if (!string.IsNullOrWhiteSpace(remark) && remark.Length > 512) { throw new BusinessException("备注长度不能超过512"); } var handlerNames = input.HandlerNames?.Trim(); if (!string.IsNullOrWhiteSpace(handlerNames) && handlerNames.Length > 256) { throw new BusinessException("处理人长度不能超过256"); } return new ResolvedProblematicPayload { IssueType = issueType, Title = title, Content = content, IssueTemplateId = template?.Id, NotifyMethod = template == null ? null : IssueNotifyMethodCodes.Normalize(template.NotifyMethod), HandlerNames = string.IsNullOrWhiteSpace(handlerNames) ? null : handlerNames, Remark = remark }; } private static ResolvedClaimedPayload NormalizeClaimedPayload(OrderMarkClaimedInput input) { var claimType = input.ClaimType?.Trim().ToLowerInvariant(); if (string.IsNullOrWhiteSpace(claimType) || !ClaimTypeCodes.All.Contains(claimType)) { throw new BusinessException("理赔类型不合法"); } if (input.ClaimAmount is not > 0) { throw new BusinessException("索赔金额必须大于0"); } var currency = input.ClaimCurrency?.Trim().ToUpperInvariant(); if (!string.IsNullOrWhiteSpace(currency) && currency.Length > 16) { throw new BusinessException("索赔币种长度不能超过16"); } var reason = input.ClaimReason?.Trim(); if (string.IsNullOrWhiteSpace(reason)) { throw new BusinessException("理赔原因不能为空"); } if (reason.Length > 512) { throw new BusinessException("理赔原因长度不能超过512"); } var remark = input.Remark?.Trim(); if (!string.IsNullOrWhiteSpace(remark) && remark.Length > 512) { throw new BusinessException("备注长度不能超过512"); } return new ResolvedClaimedPayload { ClaimType = claimType, ClaimAmount = input.ClaimAmount.Value, ClaimCurrency = string.IsNullOrWhiteSpace(currency) ? null : currency, ClaimReason = reason, Remark = remark }; } /// /// 按订单附加服务/申报金额校验并写入理赔字段。 /// private static string? TryApplyClaimed(Order order, ResolvedClaimedPayload payload) { if (!OrderExceptionStatusRules.CanMarkException(order.OrderStatus)) { return OrderExceptionStatusRules.GetForbiddenReason(order.OrderStatus); } if (order.DeclaredSum is > 0 && payload.ClaimAmount > order.DeclaredSum.Value) { return "索赔金额不能超过申报金额"; } var hasInsurance = ParseAdditionalServiceCodes(order.AdditionalServices) .Contains("insurance"); var currency = payload.ClaimCurrency; if (string.IsNullOrWhiteSpace(currency)) { if (hasInsurance && !string.IsNullOrWhiteSpace(order.DeclaredCurrency)) { currency = order.DeclaredCurrency.Trim().ToUpperInvariant(); } else { return "索赔币种不能为空"; } } var fromStatus = order.OrderStatus; order.PreviousOrderStatus = fromStatus; order.OrderStatus = OrderStatusCodes.Claimed; order.HandleStatus = OrderHandleStatusCodes.Pending; order.ClaimType = payload.ClaimType; order.ClaimAmount = payload.ClaimAmount; order.ClaimCurrency = currency; order.ClaimReason = payload.ClaimReason; order.ClaimTime = DateTime.Now; order.IssueType = null; order.IssueTitle = null; order.IssueContent = null; order.IssueTemplateId = null; order.IssueTime = null; if (!hasInsurance) { const string uninsuredPrefix = "[未购保险]"; order.IssueRemark = string.IsNullOrWhiteSpace(payload.Remark) ? uninsuredPrefix : $"{uninsuredPrefix}{payload.Remark}"; } else { order.IssueRemark = null; } return null; } private sealed class ResolvedProblematicPayload { public string IssueType { get; set; } = string.Empty; public string Title { get; set; } = string.Empty; public string Content { get; set; } = string.Empty; public int? IssueTemplateId { get; set; } public string? NotifyMethod { get; set; } public string? HandlerNames { get; set; } public string? Remark { get; set; } } private sealed class ResolvedClaimedPayload { public string ClaimType { get; set; } = string.Empty; public decimal ClaimAmount { get; set; } public string? ClaimCurrency { get; set; } public string ClaimReason { get; set; } = string.Empty; public string? Remark { get; set; } } }