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.Labeling; 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; /// /// 订单相关 API 服务。 /// [ApiExplorerSettings(GroupName = "OMS-Order")] public partial class OrderService : ApplicationService { private IOrderRepository OrderRepository => LazyServiceProvider.GetRequiredService(); private IOrderTemplateRepository OrderTemplateRepository => LazyServiceProvider.GetRequiredService(); private IWarehouseAddressRepository WarehouseAddressRepository => LazyServiceProvider.GetRequiredService(); private ISalesProductRepository SalesProductRepository => LazyServiceProvider.GetRequiredService(); private IDictRepository DictRepository => LazyServiceProvider.GetRequiredService(); private IWebHostEnvironment WebHostEnvironment => LazyServiceProvider.GetRequiredService(); private LabelPdfGenerationService LabelPdfGenerationService => LazyServiceProvider.GetRequiredService(); private OmsRbacUserDisplayNameResolver UserDisplayNameResolver => LazyServiceProvider.GetRequiredService(); private EffectiveScopeBuilder DataScopeBuilder => LazyServiceProvider.GetRequiredService(); /// /// 查询订单列表。 /// /// 入参:订单分页查询条件。 /// 订单分页结果,包含列表数据和总记录数。 [HttpPost("/order/list")] public async Task> GetPaginatedListAsync([FromBody] OrderPaginatedInput input) { var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync( CustomerRepository, CurrentUser, input.CustomerId); 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 { Items = await MapOrderOutputsAsync(orders), Total = total }; } /// /// 按状态聚合统计订单数量(单次查询,供 Tab 角标展示)。 /// [HttpGet("/order/status-counts")] public async Task 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 }; } /// /// 查询订单详情。 /// /// 入参:目标主键。 /// 订单详情。 [HttpGet("/order/detail")] public async Task GetDetailAsync([FromQuery] int id) { var order = await GetOrderOrThrowAsync(id); return (await MapOrderOutputsAsync([order])).First(); } /// /// 执行订单支付。 /// /// 入参:订单操作参数。 /// 无返回体,仅完成订单支付处理。 [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 = order.PayableAmount ?? 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(); await balanceMutator.DebitOrderPayAsync( customer, payAmount, order.Id, order.ServiceNo, $"订单支付 {order.ServiceNo}"); if (order.CouponInstanceId.HasValue) { var couponManager = LazyServiceProvider.GetRequiredService(); var mainCustomerId = await couponManager.ResolveMainCustomerIdAsync(customer.Id); await couponManager.MarkUsedAsync(order.CouponInstanceId.Value, mainCustomerId, order.Id); } var mainCustomer = await MarketingCustomerHelper.ResolveMarketingMainCustomerAsync(CustomerRepository, customer); var pointsManager = LazyServiceProvider.GetRequiredService(); await pointsManager.EarnOnOrderPaidAsync(mainCustomer.Id, order.Id, payAmount); order.PayTime = DateTime.Now; order.IsPaid = 1; order.OrderStatus = OrderStatusCodes.PendingShip; await UpdateOrderAsync(order); } /// /// 标记订单派送成功(触发引荐分成 Pending)。 /// [HttpPost("/order/mark-delivery-success")] public async Task MarkDeliverySuccessAsync(OrderActionInput input) { var order = await GetOrderOrThrowAsync(input.Id); order.OrderStatus = OrderStatusCodes.DeliverySuccess; order.SignTime = DateTime.Now; await UpdateOrderAsync(order); var commissionManager = LazyServiceProvider.GetRequiredService(); await commissionManager.OnOrderDeliverySuccessAsync(order.Id); } /// /// 执行订单预报。 /// /// 入参:订单操作参数。 /// 无返回体,仅完成订单预报处理。 [HttpPost("/order/pre-declare")] public async Task PreDeclareAsync(OrderActionInput input) { var order = await GetOrderOrThrowAsync(input.Id); order.OrderStatus = OrderStatusCodes.Predeclared; await UpdateOrderAsync(order); } /// /// 执行订单拦截。 /// /// 入参:订单操作参数。 /// 无返回体,仅完成订单拦截处理。 [HttpPost("/order/intercept")] public async Task InterceptAsync(OrderActionInput input) { var order = await GetOrderOrThrowAsync(input.Id); order.IsIntercept = 1; await UpdateOrderAsync(order); } /// /// 取消订单拦截。 /// /// 入参:订单操作参数。 /// 无返回体,仅完成取消订单拦截。 [HttpPost("/order/cancel-intercept")] public async Task CancelInterceptAsync(OrderActionInput input) { var order = await GetOrderOrThrowAsync(input.Id); order.IsIntercept = 0; await UpdateOrderAsync(order); } /// /// 删除订单。 /// /// 入参:订单操作参数。 /// 无返回体,仅完成删除订单。 [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); } /// /// 打印订单。 /// /// 入参:订单操作参数。 /// 订单打印结果。 [HttpPost("/order/print")] public async Task PrintAsync(OrderActionInput input) { var order = await GetOrderOrThrowAsync(input.Id); var mapped = (await MapOrderOutputsAsync([order])).First(); var existingUrl = await OrderFileManager.GetSystemLabelUrlAsync(order.Id); if (!string.IsNullOrWhiteSpace(existingUrl)) { return new OrderPrintOutput { PrintUrl = existingUrl!, LabelGenerateStatus = LabelGenerateStatusCodes.Ready }; } // 无系统面单:即时补生成(打印场景优先出 PDF) var result = await LabelPdfGenerationService.GenerateAndPersistAsync( order, mapped, templateIdOverride: null, operatorName: CurrentUser.UserName, operatorUserId: CurrentUser.IsAuthenticated ? CurrentUser.GetId() : 0); if (!result.Success || string.IsNullOrWhiteSpace(result.LabelUrl)) { // 兼容回落:仍无模板时给 HTML stub,避免彻底不可打 var fallbackUrl = await CreateOrderPrintFileAsync(mapped); return new OrderPrintOutput { PrintUrl = fallbackUrl, LabelGenerateStatus = result.Status, LabelGenerateError = result.Error ?? "面单 PDF 未就绪,已回落 HTML" }; } return new OrderPrintOutput { PrintUrl = result.LabelUrl!, LabelGenerateStatus = result.Status, LabelGenerateError = result.Error }; } /// /// 导出订单。 /// /// 入参:订单导出筛选条件。 /// 订单导出结果。 [HttpPost("/order/export")] public async Task 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) }; } /// /// 查价运输方式选项(按销售产品分组)。 /// [HttpGet("/order/price-check/shipping-options")] public Task GetPriceCheckShippingOptionsAsync() => BuildPriceCheckShippingOptionsAsync(); /// /// 执行订单运费试算。 /// /// 入参:订单运费试算参数。 /// 订单运费试算列表结果。 [HttpPost("/order/price-check")] public async Task PriceCheckAsync(OrderPriceCheckInput input) { if (!input.CustomerId.HasValue || input.CustomerId.Value <= 0) { input.CustomerId = await ResolvePriceCheckCustomerIdAsync(); } return await BuildPriceCheckResultAsync(input); } /// /// 查询订单模板列表。 /// /// 入参:订单模板列表查询条件。 /// 订单模板分页结果,包含列表数据和总记录数。 [HttpGet("/order/template/list")] public async Task> GetTemplateListAsync([FromQuery] OrderTemplateListInput input) { var items = await GetTemplateOutputsAsync(input); return items; } /// /// 下载订单模板文件。 /// /// 入参:模板主键。 /// 文件流响应。 [HttpGet("/order/template/download")] public async Task DownloadTemplateAsync([FromQuery] int templateId) { return await BuildTemplateDownloadAsync(templateId); } /// /// 导入订单文件,创建 import-row 批量任务并返回 jobId。 /// /// 入参:订单导入参数。 /// 批量任务 ID 及是否异步处理。 [HttpPost("/order/upload")] public async Task UploadAsync([FromForm] OrderUploadInput input) { return await ImportOrdersAsync(input); } /// /// 创建订单包裹。 /// /// 入参:订单包裹创建参数。 /// 订单包裹创建结果。 [HttpPost("/order/parcel/create")] public async Task CreateParcelAsync(OrderParcelCreateInput input) { return await CreateParcelCoreAsync(input); } /// /// 批量创建订单包裹。 /// /// 入参:批量创建小包请求集合,每条记录沿用单票创建结构。 /// 批量处理结果,包含批次号、成功失败数量与逐条执行明细。 [HttpPost("/order/parcel/batch-create")] public async Task 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 BatchCreateParcelViaQueueAsync(OrderParcelBatchCreateInput input) { var batchNo = $"PCB{DateTime.Now:yyyyMMddHHmmssfff}"; var operatorUserId = CurrentUser.GetId(); var payloadItems = input.Select((request, index) => new OrderBatchPayloadItemInput { OrderId = 0, ServiceNo = request.ServiceNo, ClientNo = request.ClientNo, 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 BatchCreateParcelSyncAsync(OrderParcelBatchCreateInput input) { var batchNo = $"PCB{DateTime.Now:yyyyMMddHHmmssfff}"; var items = new List(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, OrderSource = input.OrderSource, Keyword = input.Keyword, CreateTimeStart = input.CreateTimeStart, CreateTimeEnd = input.CreateTimeEnd, CustomerId = customerId }; private static OrderListFilter BuildListFilter(OrderExportInput input, int? customerId) => new() { Status = input.Status, OrderSource = input.OrderSource, 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) { return; } // 兼容历史脏数据:下单人是当前客户账号,但订单 CustomerId 未写对 var mapId = OmsSessionHelper.TryGetCustomerMapId(CurrentUser); if (mapId is > 0 && order.CreateUserId == mapId.Value) { return; } throw new BusinessException("订单不存在"); } private async Task 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); } }