using Fake.Application; using Fake.Domain.Exceptions; using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Orders; using Oms.Application.Services.Orders.Issues; 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, 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 => { if (!OrderExceptionStatusRules.CanMarkException(order.OrderStatus)) { return OrderExceptionStatusRules.GetForbiddenReason(order.OrderStatus); } 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 = payload.ClaimCurrency; order.ClaimReason = payload.ClaimReason; order.ClaimTime = DateTime.Now; order.IssueType = null; order.IssueTitle = null; order.IssueContent = null; order.IssueTemplateId = null; order.IssueRemark = null; order.IssueTime = null; return null; }, (order, remark) => new OrderExceptionEvent { OrderId = order.Id, EventType = OrderExceptionEventTypeCodes.MarkClaimed, FromStatus = order.PreviousOrderStatus, ToStatus = OrderStatusCodes.Claimed, FromHandleStatus = null, ToHandleStatus = OrderHandleStatusCodes.Pending, ClaimType = payload.ClaimType, ClaimAmount = payload.ClaimAmount, ClaimCurrency = payload.ClaimCurrency, ClaimReason = payload.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()); } 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"); } return new ResolvedProblematicPayload { IssueType = issueType, Title = title, Content = content, IssueTemplateId = template?.Id, NotifyMethod = template == null ? null : IssueNotifyMethodCodes.Normalize(template.NotifyMethod), 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)) { throw new BusinessException("索赔币种不能为空"); } if (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 = currency, ClaimReason = reason, Remark = remark }; } 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? Remark { get; set; } } private sealed class ResolvedClaimedPayload { public string ClaimType { get; set; } = string.Empty; public decimal ClaimAmount { get; set; } public string ClaimCurrency { get; set; } = string.Empty; public string ClaimReason { get; set; } = string.Empty; public string? Remark { get; set; } } }