using Fake.Application; using Fake.Domain.Exceptions; using Fake.ObjectStorage; 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 Oms.Application.Storage; 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 IPickupOrderRepository PickupOrderRepository => LazyServiceProvider.GetRequiredService(); private IWebHostEnvironment WebHostEnvironment => LazyServiceProvider.GetRequiredService(); private IObjectStorage FileStorage => 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( [FromQuery] DateTime? createTimeStart = null, [FromQuery] DateTime? createTimeEnd = null, [FromQuery] string? fulfillmentType = null) { var filter = await BuildScopedListFilterAsync(); filter.CreateTimeStart = createTimeStart; filter.CreateTimeEnd = createTimeEnd; filter.FulfillmentType = NormalizeFulfillmentTypeFilter(fulfillmentType); EnsureOrderListTimeWindow(filter); 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); var output = new OrderStatusCountsOutput { Total = rows.Sum(x => x.Count), Counts = counts, HandleCounts = handleCounts }; if (string.Equals(filter.FulfillmentType, FulfillmentTypeCodes.Warehouse, StringComparison.OrdinalIgnoreCase)) { output.FulfillProgressCounts = await OrderRepository.GetFulfillProgressCountsByFilterAsync(filter); } return output; } /// /// 查询订单详情。 /// /// 入参:目标主键。 /// 订单详情。 [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) { 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); if (string.Equals(order.OrderStatus, OrderStatusCodes.Predeclared, StringComparison.OrdinalIgnoreCase)) { return; } if (!string.Equals(order.OrderStatus, OrderStatusCodes.Draft, StringComparison.OrdinalIgnoreCase)) { throw new BusinessException( $"订单状态为「{OmsDocHelper.GetOrderStatusName(order.OrderStatus)}」,不允许预报"); } await CompleteDraftPredeclareAsync(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 }; } /// /// 批量获取打印面单地址(同步返回,避免前端逐单请求)。 /// /// 入参:订单 ID 列表。 /// 各订单打印结果。 [HttpPost("/order/print/batch")] public async Task PrintBatchAsync([FromBody] OrderPrintBatchInput input) { var orderIds = (input.OrderIds ?? []).Where(x => x > 0).Distinct().ToList(); if (orderIds.Count == 0) { throw new BusinessException("请选择要打印的订单"); } const int maxBatchSize = 100; if (orderIds.Count > maxBatchSize) { throw new BusinessException($"单次最多获取 {maxBatchSize} 张面单"); } var output = new OrderPrintBatchOutput { Items = new List(orderIds.Count) }; // 已有系统面单一律批量查出,避免 N 次附件查询 var existingUrls = await OrderFileManager.GetSystemLabelUrlsAsync(orderIds); var pendingIds = new List(); foreach (var orderId in orderIds) { if (existingUrls.TryGetValue(orderId, out var url) && !string.IsNullOrWhiteSpace(url)) { output.Items.Add(new OrderPrintBatchItemOutput { OrderId = orderId, Success = true, PrintUrl = url, LabelGenerateStatus = LabelGenerateStatusCodes.Ready }); } else { pendingIds.Add(orderId); } } // 未就绪的走单票打印逻辑(含即时补生成),单条失败不影响其它 foreach (var orderId in pendingIds) { try { var print = await PrintAsync(new OrderActionInput { Id = orderId }); var ok = !string.IsNullOrWhiteSpace(print.PrintUrl); output.Items.Add(new OrderPrintBatchItemOutput { OrderId = orderId, Success = ok, PrintUrl = print.PrintUrl ?? string.Empty, LabelGenerateStatus = print.LabelGenerateStatus, Error = ok ? null : (print.LabelGenerateError ?? "未获取到打印文件") }); } catch (Exception ex) { output.Items.Add(new OrderPrintBatchItemOutput { OrderId = orderId, Success = false, Error = ex is BusinessException be ? be.Message : ex.Message }); } } // 保持与入参顺序一致 var byId = output.Items.ToDictionary(x => x.OrderId); output.Items = orderIds .Select(id => byId.TryGetValue(id, out var item) ? item : new OrderPrintBatchItemOutput { OrderId = id, Success = false, Error = "未处理" }) .ToList(); return output; } /// /// 导出订单。 /// /// 入参:订单导出筛选条件。 /// 订单导出结果。 [HttpPost("/order/export")] public async Task ExportAsync([FromBody] OrderExportInput input) { await CleanupExpiredLocalExportFilesAsync(); var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync( CustomerRepository, CurrentUser, null); var filter = BuildListFilter(input, customerId); await ApplyInternalOrderDataScopeAsync(filter); const int maxExportRows = 10000; var (orders, total) = await OrderRepository.GetFilteredPagedListAsync(1, maxExportRows, filter); if (total > maxExportRows) { throw new BusinessException($"导出结果超过 {maxExportRows} 条(当前 {total}),请缩小筛选条件后再导出"); } var fileName = $"orders-{DateTime.Now:yyyyMMddHHmmss}.xlsx"; var relativeKey = $"uploads/exports/orders/{fileName}"; await using var stream = new MemoryStream(); await MiniExcel.SaveAsAsync(stream, await BuildOrderExportRowsAsync(orders)); stream.Position = 0; var storageKey = await FileStorage.SaveObjectAsync( stream, relativeKey, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); return new OrderExportOutput { DownloadUrl = FileStorage.GetPublicUrl(storageKey), 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) { var inbound = string.IsNullOrWhiteSpace(input.InboundMethod) ? null : input.InboundMethod.Trim(); var createContext = inbound == null ? null : new ParcelCreateContext { InboundMethod = inbound, PickupTime = string.Equals(inbound, OrderInboundMethodCodes.DoorPickup, StringComparison.OrdinalIgnoreCase) ? input.ExpectPickupTime : null, CustomerSenderAddressId = string.Equals(inbound, OrderInboundMethodCodes.DoorPickup, StringComparison.OrdinalIgnoreCase) ? input.SenderAddressId : null }; var output = await CreateParcelCoreAsync(input, createContext: createContext); if (string.Equals(inbound, OrderInboundMethodCodes.DoorPickup, StringComparison.OrdinalIgnoreCase)) { await TryAttachPickupAfterParcelCreateAsync(input, output); } return output; } private async Task TryAttachPickupAfterParcelCreateAsync( OrderParcelCreateInput input, OrderParcelCreateOutput output) { if (output.OrderId <= 0) { return; } if (input.SenderAddressId is not > 0) { output.PickupCreateError = "上门揽收需选择揽收地址"; return; } if (input.ExpectPickupTime is null || input.ExpectPickupTime == default) { output.PickupCreateError = "上门揽收需填写期望揽收时间"; return; } if (input.WarehouseId is not > 0) { output.PickupCreateError = "上门揽收需选择仓库"; return; } try { var pickupService = LazyServiceProvider.GetRequiredService(); var pickup = await pickupService.CreateForParcelInternalAsync(new PickupOrderCreateInput { CustomerId = input.CustomerId, SenderAddressId = input.SenderAddressId.Value, Direction = PickupDirectionCodes.NormalizeOrDefault(input.PickupDirection), WarehouseId = input.WarehouseId.Value, CarrierCode = input.CarrierCode, CarrierService = input.CarrierService, ExpectPickupTime = input.ExpectPickupTime.Value, OrderIds = [output.OrderId], Remark = null }); output.PickupNo = pickup.PickupNo; output.PickupOrderId = pickup.Id; output.CarrierWaybillNo = pickup.CarrierWaybillNo; } catch (Exception ex) { output.PickupCreateError = ex.Message; } } /// /// 批量创建订单包裹。 /// /// 入参:批量创建小包请求集合,每条记录沿用单票创建结构。 /// 批量处理结果,包含批次号、成功失败数量与逐条执行明细。 [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) { var filter = new OrderListFilter { Status = input.Status, HandleStatus = input.HandleStatus, OrderSource = input.OrderSource, FulfillmentType = NormalizeFulfillmentTypeFilter(input.FulfillmentType), StockBindStatus = NormalizeStockBindStatusFilter(input.StockBindStatus), FulfillProgress = NormalizeFulfillProgressFilter(input.FulfillProgress), Keyword = NormalizeOrderKeyword(input.Keyword), CreateTimeStart = input.CreateTimeStart, CreateTimeEnd = input.CreateTimeEnd, CustomerId = customerId, WarehouseId = NormalizeOrderWarehouseId(input.WarehouseId), FieldFilters = NormalizeOrderFieldFilters(input.Filters), SortField = NormalizeOrderSortField(input.SortField), SortOrder = NormalizeOrderSortOrder(input.SortOrder) }; EnsureOrderListTimeWindow(filter); return filter; } private static OrderListFilter BuildListFilter(OrderExportInput input, int? customerId) { var filter = new OrderListFilter { Status = input.Status, OrderSource = input.OrderSource, FulfillmentType = NormalizeFulfillmentTypeFilter(input.FulfillmentType), FulfillProgress = NormalizeFulfillProgressFilter(input.FulfillProgress), Keyword = NormalizeOrderKeyword(input.Keyword), CreateTimeStart = input.CreateTimeStart, CreateTimeEnd = input.CreateTimeEnd, CustomerId = customerId, FieldFilters = NormalizeOrderFieldFilters(input.Filters), SortField = NormalizeOrderSortField(input.SortField), SortOrder = NormalizeOrderSortOrder(input.SortOrder) }; EnsureOrderListTimeWindow(filter); return filter; } /// 订单列表默认时间窗(天)。无精确单号且未传时间时兜底,防全表扫。 private const int OrderListDefaultDays = 90; /// /// 无创建时间且非「精确单号查询」时,自动限制近 N 天。 /// private static void EnsureOrderListTimeWindow(OrderListFilter filter) { if (filter.CreateTimeStart.HasValue || filter.CreateTimeEnd.HasValue) { return; } if (HasNarrowOrderLookup(filter)) { return; } filter.CreateTimeStart = DateTime.Now.Date.AddDays(-(OrderListDefaultDays - 1)); } private static bool HasNarrowOrderLookup(OrderListFilter filter) { if (!string.IsNullOrWhiteSpace(filter.Keyword)) { return true; } if (filter.FieldFilters == null || filter.FieldFilters.Count == 0) { return false; } var numberFields = new HashSet(StringComparer.OrdinalIgnoreCase) { "serviceNo", "clientNo", "trackNo", "downstreamServiceNo" }; return filter.FieldFilters.Any(f => numberFields.Contains(f.Field) && (f.Op is "eq" or "in" or "like") && (!string.IsNullOrWhiteSpace(f.Value) || (f.Values?.Count ?? 0) > 0)); } private static readonly HashSet OrderFieldFilterWhitelist = new(StringComparer.OrdinalIgnoreCase) { "serviceNo", "clientNo", "trackNo", "downstreamServiceNo", "createTime", "orderStatus", "productCode", "channelCode", "orderSource", "fulfillmentType", "payableAmount", "isPaid", "predeclareFailReason", "allocationPendingReason", "payTime", "shipTime", "receiveTime", "signTime" }; private static readonly HashSet OrderFieldFilterOps = new(StringComparer.OrdinalIgnoreCase) { "like", "eq", "ne", "in", "notIn", "gt", "gte", "lt", "lte", "between", "empty", "isNull", "isNotNull" }; private static readonly HashSet OrderSortFieldWhitelist = new(StringComparer.OrdinalIgnoreCase) { "createTime", "updateTime", "serviceNo", "clientNo", "trackNo", "primaryBinCode", "payableAmount", "payTime", "shipTime", "receiveTime", "signTime" }; /// 单次列表最多接受的 filters 条数。 private const int MaxOrderFieldFilters = 20; /// IN / NOT IN 单字段最多值数量(防几万单号打爆 SQL/参数包)。 private const int MaxOrderInValues = 500; /// 单值最大长度(单号/文本)。 private const int MaxOrderFilterValueLength = 128; /// keyword 最大长度。 private const int MaxOrderKeywordLength = 200; private static string? NormalizeOrderKeyword(string? keyword) { var text = keyword?.Trim(); if (string.IsNullOrWhiteSpace(text)) { return null; } return text.Length <= MaxOrderKeywordLength ? text : text[..MaxOrderKeywordLength]; } private static string? TruncateFilterValue(string? value) { var text = value?.Trim(); if (string.IsNullOrWhiteSpace(text)) { return null; } return text.Length <= MaxOrderFilterValueLength ? text : text[..MaxOrderFilterValueLength]; } private static string? NormalizeOrderSortField(string? sortField) { var field = sortField?.Trim(); if (string.IsNullOrWhiteSpace(field) || !OrderSortFieldWhitelist.Contains(field)) { return null; } return field; } private static string? NormalizeOrderSortOrder(string? sortOrder) { var order = sortOrder?.Trim(); if (string.IsNullOrWhiteSpace(order)) { return null; } if (string.Equals(order, "asc", StringComparison.OrdinalIgnoreCase)) { return "asc"; } if (string.Equals(order, "desc", StringComparison.OrdinalIgnoreCase)) { return "desc"; } return null; } private static string? NormalizeFulfillmentTypeFilter(string? fulfillmentType) { var code = fulfillmentType?.Trim(); if (string.IsNullOrWhiteSpace(code) || string.Equals(code, "all", StringComparison.OrdinalIgnoreCase)) { return null; } if (!FulfillmentTypeCodes.All.Contains(code)) { throw new BusinessException($"不支持的履约类型:{fulfillmentType}"); } return code; } private static string? NormalizeStockBindStatusFilter(string? stockBindStatus) { var code = stockBindStatus?.Trim(); if (string.IsNullOrWhiteSpace(code) || string.Equals(code, "all", StringComparison.OrdinalIgnoreCase)) { return null; } if (string.Equals(code, "none", StringComparison.OrdinalIgnoreCase) || string.Equals(code, OrderStockBindStatusCodes.Bound, StringComparison.OrdinalIgnoreCase) || string.Equals(code, OrderStockBindStatusCodes.Deducted, StringComparison.OrdinalIgnoreCase)) { if (string.Equals(code, "none", StringComparison.OrdinalIgnoreCase)) { return "none"; } if (string.Equals(code, OrderStockBindStatusCodes.Bound, StringComparison.OrdinalIgnoreCase)) { return OrderStockBindStatusCodes.Bound; } return OrderStockBindStatusCodes.Deducted; } throw new BusinessException($"不支持的绑定状态筛选:{stockBindStatus}"); } private static int? NormalizeOrderWarehouseId(int? warehouseId) => warehouseId is > 0 ? warehouseId : null; private static string? NormalizeFulfillProgressFilter(string? fulfillProgress) { var code = fulfillProgress?.Trim(); if (string.IsNullOrWhiteSpace(code) || string.Equals(code, "all", StringComparison.OrdinalIgnoreCase)) { return null; } if (FulfillmentProgressCodes.All.Contains(code)) { return code; } throw new BusinessException($"不支持的代发进度筛选:{fulfillProgress}"); } private static List? NormalizeOrderFieldFilters( List? filters) { if (filters == null || filters.Count == 0) { return null; } var result = new List(); foreach (var f in filters.Take(MaxOrderFieldFilters)) { var field = f.Field?.Trim() ?? string.Empty; var op = string.IsNullOrWhiteSpace(f.Op) ? "like" : f.Op.Trim(); if (!OrderFieldFilterWhitelist.Contains(field) || !OrderFieldFilterOps.Contains(op)) { continue; } op = op.ToLowerInvariant(); if (string.Equals(op, "isNull", StringComparison.OrdinalIgnoreCase)) { op = "empty"; } var values = (f.Values ?? []) .Select(x => TruncateFilterValue(x)) .Where(x => !string.IsNullOrWhiteSpace(x)) .Cast() .Distinct(StringComparer.OrdinalIgnoreCase) .Take(MaxOrderInValues) .ToList(); if (values.Count == 0 && !string.IsNullOrWhiteSpace(f.Value) && (op is "in" or "notin")) { values = f.Value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Select(x => TruncateFilterValue(x)) .Where(x => !string.IsNullOrWhiteSpace(x)) .Cast() .Distinct(StringComparer.OrdinalIgnoreCase) .Take(MaxOrderInValues) .ToList(); } var needsValue = op is not ("empty" or "isnotnull"); var truncatedValue = TruncateFilterValue(f.Value); var truncatedValue2 = TruncateFilterValue(f.Value2); if (needsValue && values.Count == 0 && string.IsNullOrWhiteSpace(truncatedValue)) { continue; } if (op is "between" && string.IsNullOrWhiteSpace(truncatedValue2) && values.Count < 2) { continue; } result.Add(new OrderListFieldFilter { Field = field, Op = op, Value = truncatedValue, Value2 = truncatedValue2, Values = values.Count > 0 ? values : null }); } return result.Count == 0 ? null : result; } 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); } }