admin/src/Oms.Application/Services/Orders/OrderService.cs
zzh 9ab112a36e feat(order): 下单模板映射与 Excel 批量导入小包创建
新增专线小包标准模板生成、自定义列映射 API,并将 /order/upload 改为按映射解析 Excel 后走 CreateParcelCoreAsync;列表查询时自动补齐标准模板种子数据。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 16:42:22 +08:00

500 lines
18 KiB
C#

using Fake.Application;
using Fake.Domain.Exceptions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using MiniExcelLibs;
using Oms.Application.Dtos.Orders;
using Oms.Application.Dtos.Shared;
using Oms.Application.DataPerm;
using Oms.Application.Shared;
using Newtonsoft.Json;
using Oms.Application.Services.Orders.Batch;
using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.CustomersAggregate.Repositories;
using Oms.Domain.OrdersAggregate;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.Domain.SystemAggregate.Entities;
using Oms.Domain.SystemAggregate.Repositories;
namespace Oms.Application.Services.Orders;
/// <summary>
/// 订单相关 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Order")]
public partial class OrderService : ApplicationService
{
private IOrderRepository OrderRepository => LazyServiceProvider.GetRequiredService<IOrderRepository>();
private IOrderTemplateRepository OrderTemplateRepository => LazyServiceProvider.GetRequiredService<IOrderTemplateRepository>();
private IOrderTaskRepository OrderTaskRepository => LazyServiceProvider.GetRequiredService<IOrderTaskRepository>();
private IWarehouseAddressRepository WarehouseAddressRepository => LazyServiceProvider.GetRequiredService<IWarehouseAddressRepository>();
private ISalesProductRepository SalesProductRepository => LazyServiceProvider.GetRequiredService<ISalesProductRepository>();
private IDictRepository DictRepository => LazyServiceProvider.GetRequiredService<IDictRepository>();
private IWebHostEnvironment WebHostEnvironment => LazyServiceProvider.GetRequiredService<IWebHostEnvironment>();
private OmsRbacUserDisplayNameResolver UserDisplayNameResolver =>
LazyServiceProvider.GetRequiredService<OmsRbacUserDisplayNameResolver>();
private EffectiveScopeBuilder DataScopeBuilder =>
LazyServiceProvider.GetRequiredService<EffectiveScopeBuilder>();
/// <summary>
/// 查询订单列表。
/// </summary>
/// <param name="input">入参:订单分页查询条件。</param>
/// <returns>订单分页结果,包含列表数据和总记录数。</returns>
[HttpPost("/order/list")]
public async Task<OmsPageResult<OrderOutput>> GetPaginatedListAsync([FromBody] OrderPaginatedInput input)
{
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
null);
var filter = BuildListFilter(input, customerId);
await ApplyInternalOrderDataScopeAsync(filter);
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var (orders, total) = await OrderRepository.GetFilteredPagedListAsync(
pageIndex,
pageSize,
filter);
return new OmsPageResult<OrderOutput> { Items = await MapOrderOutputsAsync(orders), Total = total };
}
/// <summary>
/// 按状态聚合统计订单数量(单次查询,供 Tab 角标展示)。
/// </summary>
[HttpGet("/order/status-counts")]
public async Task<OrderStatusCountsOutput> GetStatusCountsAsync()
{
var filter = await BuildScopedListFilterAsync();
var rows = await OrderRepository.GetStatusCountsByFilterAsync(filter);
var counts = rows.ToDictionary(
x => x.Status,
x => x.Count,
StringComparer.OrdinalIgnoreCase);
var handleRows = await OrderRepository.GetHandleStatusCountsByFilterAsync(filter);
var handleCounts = handleRows.ToDictionary(
x => x.Key,
x => new OrderHandleStatusCountsOutput
{
Pending = x.Value.Pending,
Processed = x.Value.Processed
},
StringComparer.OrdinalIgnoreCase);
return new OrderStatusCountsOutput
{
Total = rows.Sum(x => x.Count),
Counts = counts,
HandleCounts = handleCounts
};
}
/// <summary>
/// 查询订单详情。
/// </summary>
/// <param name="id">入参:目标主键。</param>
/// <returns>订单详情。</returns>
[HttpGet("/order/detail")]
public async Task<OrderOutput> GetDetailAsync([FromQuery] int id)
{
var order = await GetOrderOrThrowAsync(id);
return (await MapOrderOutputsAsync([order])).First();
}
/// <summary>
/// 执行订单支付。
/// </summary>
/// <param name="input">入参:订单操作参数。</param>
/// <returns>无返回体,仅完成订单支付处理。</returns>
[HttpPost("/order/pay")]
public async Task PayAsync(OrderActionInput input)
{
var order = await GetOrderOrThrowAsync(input.Id);
if (order.IsPaid > 0)
{
throw new BusinessException("订单已支付");
}
if (order.OrderStatus is not (OrderStatusCodes.PendingPayment or OrderStatusCodes.Received))
{
throw new BusinessException("当前订单状态不可支付");
}
if (!order.CustomerId.HasValue)
{
throw new BusinessException("订单未关联客户,无法支付");
}
var fees = await OrderRepository.GetFeesByOrderIdsAsync([order.Id]);
var payAmount = fees
.Where(x => x.Type == OrderFeeTypeCodes.Receivable)
.Sum(x => x.Amount);
if (payAmount <= 0)
{
throw new BusinessException("订单应收金额无效,无法支付");
}
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value);
if (customer == null)
{
throw new BusinessException("客户不存在");
}
var balanceMutator = LazyServiceProvider.GetRequiredService<CustomerBalanceMutator>();
await balanceMutator.DebitOrderPayAsync(
customer,
payAmount,
order.Id,
order.ServiceNo,
$"订单支付 {order.ServiceNo}");
order.PayTime = DateTime.Now;
order.IsPaid = 1;
order.OrderStatus = OrderStatusCodes.PendingShip;
await UpdateOrderAsync(order);
}
/// <summary>
/// 执行订单预报。
/// </summary>
/// <param name="input">入参:订单操作参数。</param>
/// <returns>无返回体,仅完成订单预报处理。</returns>
[HttpPost("/order/pre-declare")]
public async Task PreDeclareAsync(OrderActionInput input)
{
var order = await GetOrderOrThrowAsync(input.Id);
order.OrderStatus = OrderStatusCodes.Predeclared;
await UpdateOrderAsync(order);
}
/// <summary>
/// 执行订单拦截。
/// </summary>
/// <param name="input">入参:订单操作参数。</param>
/// <returns>无返回体,仅完成订单拦截处理。</returns>
[HttpPost("/order/intercept")]
public async Task InterceptAsync(OrderActionInput input)
{
var order = await GetOrderOrThrowAsync(input.Id);
order.IsIntercept = 1;
await UpdateOrderAsync(order);
}
/// <summary>
/// 取消订单拦截。
/// </summary>
/// <param name="input">入参:订单操作参数。</param>
/// <returns>无返回体,仅完成取消订单拦截。</returns>
[HttpPost("/order/cancel-intercept")]
public async Task CancelInterceptAsync(OrderActionInput input)
{
var order = await GetOrderOrThrowAsync(input.Id);
order.IsIntercept = 0;
await UpdateOrderAsync(order);
}
/// <summary>
/// 删除订单。
/// </summary>
/// <param name="input">入参:订单操作参数。</param>
/// <returns>无返回体,仅完成删除订单。</returns>
[HttpPost("/order/delete")]
public async Task DeleteAsync(OrderActionInput input)
{
var order = await GetOrderOrThrowAsync(input.Id);
if (order.OrderStatus is not (OrderStatusCodes.Draft or OrderStatusCodes.Predeclared))
{
throw new BusinessException("仅草稿或已预报订单可删除");
}
order.OrderStatus = OrderStatusCodes.Deleted;
await OrderRepository.UpdateAsync(order);
await OrderRepository.DeleteAsync(order);
}
/// <summary>
/// 打印订单。
/// </summary>
/// <param name="input">入参:订单操作参数。</param>
/// <returns>订单打印结果。</returns>
[HttpPost("/order/print")]
public async Task<OrderPrintOutput> PrintAsync(OrderActionInput input)
{
var order = await GetOrderOrThrowAsync(input.Id);
var printUrl = await CreateOrderPrintFileAsync((await MapOrderOutputsAsync([order])).First());
return new OrderPrintOutput { PrintUrl = printUrl };
}
/// <summary>
/// 导出订单。
/// </summary>
/// <param name="input">入参:订单导出筛选条件。</param>
/// <returns>订单导出结果。</returns>
[HttpPost("/order/export")]
public async Task<OrderExportOutput> ExportAsync([FromBody] OrderExportInput input)
{
await CleanupExpiredFilesAsync(GetOrderExportDirectory());
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
null);
var filter = BuildListFilter(input, customerId);
await ApplyInternalOrderDataScopeAsync(filter);
var orders = await OrderRepository.GetFilteredListAsync(filter);
var fileName = $"orders-{DateTime.Now:yyyyMMddHHmmss}.xlsx";
var directory = GetOrderExportDirectory();
Directory.CreateDirectory(directory);
await MiniExcel.SaveAsAsync(Path.Combine(directory, fileName), await BuildOrderExportRowsAsync(orders));
return new OrderExportOutput
{
DownloadUrl = $"/uploads/exports/orders/{fileName}",
ExpireAt = DateTime.Now.AddHours(24)
};
}
/// <summary>
/// 执行订单运费试算。
/// </summary>
/// <param name="input">入参:订单运费试算参数。</param>
/// <returns>订单运费试算列表结果。</returns>
[HttpPost("/order/price-check")]
public async Task<OrderPriceCheckResultOutput> PriceCheckAsync(OrderPriceCheckInput input)
{
return await BuildPriceCheckResultAsync(input);
}
/// <summary>
/// 查询订单模板列表。
/// </summary>
/// <param name="input">入参:订单模板列表查询条件。</param>
/// <returns>订单模板分页结果,包含列表数据和总记录数。</returns>
[HttpGet("/order/template/list")]
public async Task<OmsPageResult<OrderTemplateOutput>> GetTemplateListAsync([FromQuery] OrderTemplateListInput input)
{
var items = await GetTemplateOutputsAsync(input);
return items;
}
/// <summary>
/// 下载订单模板文件。
/// </summary>
/// <param name="templateId">入参:模板主键。</param>
/// <returns>文件流响应。</returns>
[HttpGet("/order/template/download")]
public async Task<FileContentResult> DownloadTemplateAsync([FromQuery] int templateId)
{
return await BuildTemplateDownloadAsync(templateId);
}
/// <summary>
/// 导入订单文件。
/// </summary>
/// <param name="input">入参:订单导入参数。</param>
/// <returns>订单导入结果。</returns>
[HttpPost("/order/upload")]
public async Task<OrderUploadOutput> UploadAsync([FromForm] OrderUploadInput input)
{
return await ImportOrdersAsync(input);
}
/// <summary>
/// 查询订单任务列表。
/// </summary>
/// <param name="input">入参:订单任务列表查询条件。</param>
/// <returns>订单任务分页结果,包含列表数据和总记录数。</returns>
[HttpGet("/order/task/list")]
public async Task<OmsPageResult<OrderTaskOutput>> GetTaskListAsync([FromQuery] OrderTaskListInput input)
{
return await GetTaskOutputsAsync(input);
}
/// <summary>
/// 查询订单任务详情。
/// </summary>
/// <param name="taskId">入参:任务主键。</param>
/// <returns>订单任务详情。</returns>
[HttpGet("/order/task/detail")]
public async Task<OrderTaskDetailOutput> GetTaskDetailAsync([FromQuery] int taskId)
{
return await GetTaskDetailCoreAsync(taskId);
}
/// <summary>
/// 创建订单包裹。
/// </summary>
/// <param name="input">入参:订单包裹创建参数。</param>
/// <returns>订单包裹创建结果。</returns>
[HttpPost("/order/parcel/create")]
public async Task<OrderParcelCreateOutput> CreateParcelAsync(OrderParcelCreateInput input)
{
return await CreateParcelCoreAsync(input);
}
/// <summary>
/// 批量创建订单包裹。
/// </summary>
/// <param name="input">入参:批量创建小包请求集合,每条记录沿用单票创建结构。</param>
/// <returns>批量处理结果,包含批次号、成功失败数量与逐条执行明细。</returns>
[HttpPost("/order/parcel/batch-create")]
public async Task<OrderParcelBatchCreateOutput> BatchCreateParcelAsync(OrderParcelBatchCreateInput input)
{
if (input == null || input.Count == 0)
{
throw new BusinessException("批量创建请求不能为空");
}
if (input.Count > ParcelBatchMaxCount)
{
throw new BusinessException($"单次批量创建不能超过{ParcelBatchMaxCount}条");
}
if (BatchJobPublisher.IsEnabled)
{
return await BatchCreateParcelViaQueueAsync(input);
}
return await BatchCreateParcelSyncAsync(input);
}
private async Task<OrderParcelBatchCreateOutput> BatchCreateParcelViaQueueAsync(OrderParcelBatchCreateInput input)
{
var batchNo = $"PCB{DateTime.Now:yyyyMMddHHmmssfff}";
var operatorUserId = CurrentUser.GetId<int>();
var payloadItems = input.Select((request, index) => new OrderBatchPayloadItemInput
{
OrderId = 0,
ServiceNo = request.ServiceNo,
ItemKey = !string.IsNullOrWhiteSpace(request.RequestId)
? $"parcel:{request.RequestId}"
: $"parcel:{batchNo}:{index + 1}",
PayloadJson = JsonConvert.SerializeObject(request),
}).ToList();
var job = await BatchJobCoordinator.CreatePayloadJobAsync(
new OrderBatchPayloadJobCreateInput
{
Action = OrderBatchActionCodes.ParcelCreate,
RequestId = batchNo,
Items = payloadItems,
},
operatorUserId);
return new OrderParcelBatchCreateOutput
{
BatchNo = batchNo,
JobId = job.JobId,
Async = true,
Total = input.Count,
SuccessCount = 0,
FailCount = 0,
Items = [],
};
}
private async Task<OrderParcelBatchCreateOutput> BatchCreateParcelSyncAsync(OrderParcelBatchCreateInput input)
{
var batchNo = $"PCB{DateTime.Now:yyyyMMddHHmmssfff}";
var items = new List<OrderParcelBatchCreateItemOutput>(input.Count);
var successCount = 0;
for (var index = 0; index < input.Count; index++)
{
var request = input[index];
try
{
var output = await CreateParcelCoreAsync(request);
successCount++;
items.Add(new OrderParcelBatchCreateItemOutput
{
Index = index + 1,
RequestId = request.RequestId,
ClientNo = output.ClientNo ?? request.ClientNo,
ServiceNo = output.ServiceNo ?? request.ServiceNo,
TrackNo = output.TrackNo ?? request.TrackNo,
LabelUrl = output.LabelUrl,
Success = true,
Message = "success"
});
}
catch (Exception ex)
{
items.Add(new OrderParcelBatchCreateItemOutput
{
Index = index + 1,
RequestId = request.RequestId,
ClientNo = request.ClientNo,
ServiceNo = request.ServiceNo,
TrackNo = request.TrackNo,
Success = false,
Message = ex.Message
});
}
}
return new OrderParcelBatchCreateOutput
{
BatchNo = batchNo,
SuccessCount = successCount,
FailCount = items.Count - successCount,
Items = items
};
}
private static OrderListFilter BuildListFilter(OrderPaginatedInput input, int? customerId) =>
new()
{
Status = input.Status,
HandleStatus = input.HandleStatus,
Keyword = input.Keyword,
CreateTimeStart = input.CreateTimeStart,
CreateTimeEnd = input.CreateTimeEnd,
CustomerId = customerId
};
private static OrderListFilter BuildListFilter(OrderExportInput input, int? customerId) =>
new()
{
Status = input.Status,
Keyword = input.Keyword,
CreateTimeStart = input.CreateTimeStart,
CreateTimeEnd = input.CreateTimeEnd,
CustomerId = customerId
};
private async Task EnsureCustomerCanAccessOrderAsync(Order order)
{
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
null);
if (!customerId.HasValue)
{
return;
}
if (order.CustomerId != customerId.Value)
{
throw new BusinessException("订单不存在");
}
}
private async Task<Order> GetOrderOrThrowAsync(int id)
{
var entity = await OrderRepository.FirstOrDefaultAsync(x => x.Id == id);
if (entity == null)
{
throw new BusinessException("订单不存在");
}
await EnsureCustomerCanAccessOrderAsync(entity);
return entity;
}
private async Task UpdateOrderAsync(Order entity)
{
await OrderRepository.UpdateAsync(entity);
}
}