新增 mark-problematic/mark-claimed/handle-status 接口,异常事件审计表与占位符渲染;inApp/email 通知及下单/入库/出库自动触发运行时。 Co-authored-by: Cursor <cursoragent@cursor.com>
387 lines
13 KiB
C#
387 lines
13 KiB
C#
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<IIssueTemplateRepository>();
|
|
|
|
private IOrderExceptionEventRepository OrderExceptionEventRepository =>
|
|
LazyServiceProvider.GetRequiredService<IOrderExceptionEventRepository>();
|
|
|
|
private OrderExceptionWorkflowService ExceptionWorkflowService =>
|
|
LazyServiceProvider.GetRequiredService<OrderExceptionWorkflowService>();
|
|
|
|
/// <summary>
|
|
/// 批量创建问题件。
|
|
/// </summary>
|
|
[HttpPost("/order/mark-problematic")]
|
|
public async Task<OrderBatchActionResultOutput> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 批量创建理赔件。
|
|
/// </summary>
|
|
[HttpPost("/order/mark-claimed")]
|
|
public async Task<OrderBatchActionResultOutput> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 批量更新处理子状态。
|
|
/// </summary>
|
|
[HttpPost("/order/handle-status/update")]
|
|
public async Task<OrderBatchActionResultOutput> 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<OrderBatchActionResultOutput> ExecuteProblematicBatchAsync(
|
|
IReadOnlyCollection<int> orderIds,
|
|
ProblematicMarkRequest request)
|
|
{
|
|
var ids = NormalizeOrderIds(orderIds);
|
|
var result = new OrderBatchActionResultOutput();
|
|
var operatorUserId = CurrentUser.GetId<int>();
|
|
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<OrderBatchActionResultOutput> ExecuteExceptionBatchAsync(
|
|
IReadOnlyCollection<int> orderIds,
|
|
Func<Order, string?> tryApply,
|
|
Func<Order, string?, OrderExceptionEvent> 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<int> NormalizeOrderIds(IReadOnlyCollection<int>? 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<ResolvedProblematicPayload> 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; }
|
|
}
|
|
}
|