admin/src/Oms.Application/Services/Orders/OrderService.cs
2026-04-17 11:09:01 +08:00

453 lines
18 KiB
C#

using System.Linq.Expressions;
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.Shared;
using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.Domain.SystemAggregate.Repositories;
namespace Oms.Application.Services.Orders;
[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 IWebHostEnvironment WebHostEnvironment => LazyServiceProvider.GetRequiredService<IWebHostEnvironment>();
/// <summary>
/// 查询订单列表。
/// </summary>
/// <param name="input">入参:订单分页查询条件。</param>
/// <returns>订单分页结果,包含列表数据和总记录数。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 根据查询条件构造筛选规则。
/// 2. 查询目标数据并补齐关联展示字段。
/// 3. 按接口约定返回列表结果。
/// </remarks>
[HttpPost("/order/list")]
public async Task<OmsPageResult<OrderOutput>> GetPaginatedListAsync(OrderPaginatedInput input)
{
var predicate = BuildPaginatedPredicate(input);
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var orders = await OrderRepository.GetPagedListAsync(
predicate,
pageIndex,
pageSize,
new Dictionary<string, bool> { [nameof(Order.UpdateTime)] = false, [nameof(Order.Id)] = false });
var total = await OrderRepository.CountAsync(predicate);
return new OmsPageResult<OrderOutput> { Items = await MapOrderOutputsAsync(orders), Total = total };
}
/// <summary>
/// 查询订单详情。
/// </summary>
/// <param name="id">入参:目标主键。</param>
/// <returns>订单详情。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 根据标识查询目标对象。
/// 2. 目标不存在时抛出业务异常。
/// 3. 映射并返回详情结果。
/// </remarks>
[HttpGet("/order/detail")]
public async Task<OrderOutput> GetDetailAsync([FromQuery] long id)
{
var order = await GetOrderOrThrowAsync(id);
return (await MapOrderOutputsAsync([order])).First();
}
/// <summary>
/// 执行订单支付。
/// </summary>
/// <param name="input">入参:订单操作参数。</param>
/// <returns>无返回体,仅完成订单支付处理。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 查询并校验目标对象及当前状态。
/// 2. 执行业务状态流转或敏感字段更新。
/// 3. 写入审计字段并保存结果。
/// </remarks>
[HttpPost("/order/pay")]
public async Task PayAsync(OrderActionInput input)
{
var order = await GetOrderOrThrowAsync(input.Id);
order.PayTime = DateTime.Now;
order.OrderStatus = OrderStatusCodes.PendingShip;
await UpdateOrderAsync(order);
}
/// <summary>
/// 执行订单预报。
/// </summary>
/// <param name="input">入参:订单操作参数。</param>
/// <returns>无返回体,仅完成订单预报处理。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 查询并校验目标对象及当前状态。
/// 2. 执行业务状态流转或敏感字段更新。
/// 3. 写入审计字段并保存结果。
/// </remarks>
[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>
/// <remarks>
/// 核心业务逻辑:
/// 1. 查询并校验目标对象及当前状态。
/// 2. 执行业务状态流转或敏感字段更新。
/// 3. 写入审计字段并保存结果。
/// </remarks>
[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>
/// <remarks>
/// 核心业务逻辑:
/// 1. 查询并校验目标对象及当前状态。
/// 2. 执行业务状态流转或敏感字段更新。
/// 3. 写入审计字段并保存结果。
/// </remarks>
[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>
/// <remarks>
/// 核心业务逻辑:
/// 1. 查询并校验目标对象。
/// 2. 执行软删除或删除关联明细。
/// 3. 持久化删除结果。
/// </remarks>
[HttpPost("/order/delete")]
public async Task DeleteAsync(OrderActionInput input)
{
var order = await GetOrderOrThrowAsync(input.Id);
order.IsDeleted = true;
order.OrderStatus = OrderStatusCodes.Deleted;
await UpdateOrderAsync(order);
}
/// <summary>
/// 打印订单。
/// </summary>
/// <param name="input">入参:订单操作参数。</param>
/// <returns>订单打印结果。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 校验模板、目标单据等前置依赖数据。
/// 2. 执行打印逻辑并记录打印结果。
/// 3. 返回打印结果信息。
/// </remarks>
[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>
/// <remarks>
/// 核心业务逻辑:
/// 1. 根据筛选条件查询待导出数据或定位模板资源。
/// 2. 生成文件内容或组装下载响应。
/// 3. 以文件形式返回结果。
/// </remarks>
[HttpPost("/order/export")]
public async Task<OrderExportOutput> ExportAsync(OrderExportInput input)
{
await CleanupExpiredFilesAsync(GetOrderExportDirectory());
var orders = await OrderRepository.GetListAsync(
BuildExportPredicate(input),
new Dictionary<string, bool> { [nameof(Order.CreateTime)] = false, [nameof(Order.Id)] = false });
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>
/// <remarks>
/// 核心业务逻辑:
/// 1. 根据试算参数构造报价请求。
/// 2. 汇总费用、时效等试算结果。
/// 3. 按接口约定返回结果列表。
/// </remarks>
[HttpPost("/order/price-check")]
public async Task<OmsItemsResult<OrderPriceCheckOutput>> PriceCheckAsync(OrderPriceCheckInput input)
{
var items = await BuildPriceCheckOutputsAsync(input);
return new OmsItemsResult<OrderPriceCheckOutput> { Items = items };
}
/// <summary>
/// 查询订单模板列表。
/// </summary>
/// <param name="input">入参:订单模板列表查询条件。</param>
/// <returns>订单模板分页结果,包含列表数据和总记录数。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 根据查询条件构造筛选规则。
/// 2. 查询目标数据并补齐关联展示字段。
/// 3. 按接口约定返回列表结果。
/// </remarks>
[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>
/// <remarks>
/// 核心业务逻辑:
/// 1. 根据筛选条件查询待导出数据或定位模板资源。
/// 2. 生成文件内容或组装下载响应。
/// 3. 以文件形式返回结果。
/// </remarks>
[HttpGet("/order/template/download")]
public async Task<FileContentResult> DownloadTemplateAsync([FromQuery] long templateId)
{
return await BuildTemplateDownloadAsync(templateId);
}
/// <summary>
/// 导入订单文件。
/// </summary>
/// <param name="input">入参:订单导入参数。</param>
/// <returns>订单导入结果。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 校验上传文件及导入参数。
/// 2. 解析文件内容并执行导入流程。
/// 3. 汇总成功失败结果后返回。
/// </remarks>
[HttpPost("/order/upload")]
public async Task<OrderUploadOutput> UploadAsync([FromForm] OrderUploadInput input)
{
return await ImportOrdersAsync(input);
}
/// <summary>
/// 查询订单任务列表。
/// </summary>
/// <param name="input">入参:订单任务列表查询条件。</param>
/// <returns>订单任务分页结果,包含列表数据和总记录数。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 根据查询条件构造筛选规则。
/// 2. 查询目标数据并补齐关联展示字段。
/// 3. 按接口约定返回列表结果。
/// </remarks>
[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>
/// <remarks>
/// 核心业务逻辑:
/// 1. 根据标识查询目标对象。
/// 2. 目标不存在时抛出业务异常。
/// 3. 映射并返回详情结果。
/// </remarks>
[HttpGet("/order/task/detail")]
public async Task<OrderTaskDetailOutput> GetTaskDetailAsync([FromQuery] long taskId)
{
return await GetTaskDetailCoreAsync(taskId);
}
/// <summary>
/// 创建订单包裹。
/// </summary>
/// <param name="input">入参:订单包裹创建参数。</param>
/// <returns>订单包裹创建结果。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 校验入参与关联对象、唯一性等前置条件。
/// 2. 组装目标实体及相关明细数据。
/// 3. 写入审计字段并持久化结果。
/// </remarks>
[HttpPost("/order/parcel/create")]
public async Task<OrderParcelCreateOutput> CreateParcelAsync(OrderParcelCreateInput input)
{
return await CreateParcelCoreAsync(input);
}
/// <summary>
/// 将订单列表请求对象转换为仓储可执行的查询表达式。
/// </summary>
/// <param name="input">入参:分页查询条件对象。</param>
/// <returns>出参:订单查询表达式。</returns>
private Expression<Func<Order, bool>> BuildPaginatedPredicate(OrderPaginatedInput input)
{
return BuildOrderPredicate(input.Status, input.Keyword, input.CreateTimeStart, input.CreateTimeEnd);
}
/// <summary>
/// 将订单导出请求对象转换为仓储可执行的查询表达式。
/// </summary>
/// <param name="input">入参:导出筛选条件对象。</param>
/// <returns>出参:订单查询表达式。</returns>
private Expression<Func<Order, bool>> BuildExportPredicate(OrderExportInput input)
{
return BuildOrderPredicate(input.Status, input.Keyword, input.CreateTimeStart, input.CreateTimeEnd);
}
/// <summary>
/// 统一构造订单查询条件。
/// </summary>
/// <param name="status">入参:订单状态,传入 <c>all</c> 或空值时表示不按状态过滤。</param>
/// <param name="keyword">入参:关键词,可匹配运单号、客户单号、跟踪号。</param>
/// <param name="createTimeStart">入参:创建时间开始值。</param>
/// <param name="createTimeEnd">入参:创建时间结束值。</param>
/// <returns>出参:可直接交给 SqlSugar 仓储执行的订单筛选表达式。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 先把状态和关键词做统一清洗;
/// 2. 再按时间区间是否存在拆分成不同表达式;
/// 3. 避免把 <see cref="Nullable{T}.HasValue"/> / <see cref="Nullable{T}.Value"/> 放进可翻译 SQL 的表达式中,
/// 从而规避 MySQL 下时间条件被错误翻译的问题。
/// </remarks>
private static Expression<Func<Order, bool>> BuildOrderPredicate(
string? status,
string? keyword,
DateTime? createTimeStart,
DateTime? createTimeEnd)
{
status = status?.Trim();
keyword = keyword?.Trim();
var hasStatusFilter = !string.IsNullOrWhiteSpace(status) && status != "all";
var hasKeywordFilter = !string.IsNullOrWhiteSpace(keyword);
// 显式拆开时间分支,避免 SqlSugar 在翻译 Nullable<DateTime> 判空时生成异常 SQL。
if (createTimeStart.HasValue && createTimeEnd.HasValue)
{
var start = createTimeStart.Value;
var end = createTimeEnd.Value;
return order =>
!order.IsDeleted &&
(!hasStatusFilter || order.OrderStatus == status) &&
(!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) ||
(order.ClientNo ?? string.Empty).Contains(keyword!) ||
(order.TrackNo ?? string.Empty).Contains(keyword!)) &&
order.CreateTime >= start &&
order.CreateTime <= end;
}
if (createTimeStart.HasValue)
{
var start = createTimeStart.Value;
return order =>
!order.IsDeleted &&
(!hasStatusFilter || order.OrderStatus == status) &&
(!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) ||
(order.ClientNo ?? string.Empty).Contains(keyword!) ||
(order.TrackNo ?? string.Empty).Contains(keyword!)) &&
order.CreateTime >= start;
}
if (createTimeEnd.HasValue)
{
var end = createTimeEnd.Value;
return order =>
!order.IsDeleted &&
(!hasStatusFilter || order.OrderStatus == status) &&
(!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) ||
(order.ClientNo ?? string.Empty).Contains(keyword!) ||
(order.TrackNo ?? string.Empty).Contains(keyword!)) &&
order.CreateTime <= end;
}
return order =>
!order.IsDeleted &&
(!hasStatusFilter || order.OrderStatus == status) &&
(!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) ||
(order.ClientNo ?? string.Empty).Contains(keyword!) ||
(order.TrackNo ?? string.Empty).Contains(keyword!));
}
private async Task<Order> GetOrderOrThrowAsync(long id)
{
var entity = await OrderRepository.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted);
if (entity == null)
{
throw new BusinessException("订单不存在");
}
return entity;
}
private async Task UpdateOrderAsync(Order entity)
{
entity.UpdateUserId = CurrentUser.GetId<int>();
entity.UpdateTime = DateTime.Now;
await OrderRepository.UpdateAsync(entity);
}
}