using Fake.Application; using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Operations; using Oms.Application.Dtos.Orders; using Oms.Application.Dtos.Shared; using Oms.Application.Services.Orders; using Oms.Application.Shared; using Oms.Domain.ChannelsAggregate.Entities; using Oms.Domain.ChannelsAggregate.Repositories; using Oms.Domain.OperationsAggregate.Entities; using Oms.Domain.OperationsAggregate.Repositories; using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.OrdersAggregate.Repositories; using Oms.Domain.Shared.Enums; using Oms.Domain.SystemAggregate.Repositories; using Oms.Domain.WarehouseAggregate.Repositories; namespace Oms.Application.Services.Operations; /// /// 面单打印 API 服务。 /// [ApiExplorerSettings(GroupName = "OMS-Operation")] public class LabelPrintService : ApplicationService { private ILabelPrintRecordRepository LabelPrintRecordRepository => LazyServiceProvider.GetRequiredService(); private ILabelTemplateRepository LabelTemplateRepository => LazyServiceProvider.GetRequiredService(); private IOrderRepository OrderRepository => LazyServiceProvider.GetRequiredService(); private IOrderStockBindRepository OrderStockBindRepository => LazyServiceProvider.GetRequiredService(); private ICParcelRepository ParcelRepository => LazyServiceProvider.GetRequiredService(); private ICSkuRepository SkuRepository => LazyServiceProvider.GetRequiredService(); private ISalesProductRepository SalesProductRepository => LazyServiceProvider.GetRequiredService(); private ICountryRepository CountryRepository => LazyServiceProvider.GetRequiredService(); private IWarehouseAddressRepository WarehouseAddressRepository => LazyServiceProvider.GetRequiredService(); private OrderService OrderService => LazyServiceProvider.GetRequiredService(); /// /// 扫码查单:运单/客户单/跟踪号/其他单号/包裹快递单号/SKU → 订单 + 绑定明细。 /// [HttpGet("/operation/print-label/lookup")] public async Task LookupAsync( [FromQuery] string number, [FromQuery] string? numberType = null) { var raw = number?.Trim() ?? string.Empty; if (string.IsNullOrWhiteSpace(raw)) { throw new BusinessException("请扫描或输入单号"); } var type = NormalizeNumberType(numberType); Order? order = null; string? matchedType = null; List? candidates = null; if (type is "auto" or "") { (order, matchedType, candidates) = await ResolveAutoAsync(raw); } else { (order, matchedType, candidates) = await ResolveByTypeAsync(raw, type); } if (candidates is { Count: > 1 }) { return new PrintLabelLookupOutput { Matched = false, MatchedNumberType = matchedType, Warnings = ["multiMatch"], Candidates = candidates }; } if (order == null && candidates is { Count: 1 }) { order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == candidates[0].OrderId); matchedType ??= "sku"; } if (order == null) { throw new BusinessException("未找到对应订单,请检查号码或类型"); } return await BuildLookupOutputAsync(order, matchedType ?? type); } /// /// 执行面单打印。 /// [HttpPost("/operation/print-label/print")] public async Task PrintAsync(PrintLabelInput input) { LabelTemplate? labelTemplate = null; if (input.LabelTemplateId.HasValue) { labelTemplate = await LabelTemplateRepository.FirstOrDefaultAsync(x => x.Id == input.LabelTemplateId.Value); if (labelTemplate == null) { throw new BusinessException("标签模板不存在"); } } else if (!string.IsNullOrWhiteSpace(input.TemplateType)) { var list = await LabelTemplateRepository.GetListAsync( x => x.TemplateType == input.TemplateType); labelTemplate = list.OrderByDescending(x => x.UpdateTime).ThenByDescending(x => x.Id).FirstOrDefault(); } var raw = input.Number?.Trim() ?? string.Empty; var type = NormalizeNumberType(input.NumberType); Order? order; if (type is "auto" or "") { (order, _, _) = await ResolveAutoAsync(raw); } else { (order, _, _) = await ResolveByTypeAsync(raw, type); } if (order == null) { throw new BusinessException("未找到对应订单,无法打印面单"); } if (labelTemplate != null) { order.LabelTemplateId = labelTemplate.Id; await OrderRepository.UpdateAsync(order); } var print = await OrderService.PrintAsync(new OrderActionInput { Id = order.Id }); var now = DateTime.Now; var operatorId = CurrentUser.GetId(); var waybillNo = string.Empty; string? customerOrderNo = null; string? trackingNo = null; string? otherNumber = null; switch (input.NumberType?.Trim().ToLowerInvariant()) { case "customerorder": case "customerorderno": case "customer-order": customerOrderNo = input.Number; break; case "tracking": trackingNo = input.Number; break; case "other": otherNumber = input.Number; break; default: waybillNo = string.IsNullOrWhiteSpace(input.Number) ? order.ServiceNo : input.Number; break; } var entity = new LabelPrintRecord { WaybillNo = string.IsNullOrWhiteSpace(waybillNo) ? order.ServiceNo : waybillNo, CustomerOrderNo = customerOrderNo ?? order.ClientNo, TrackingNo = trackingNo ?? order.TrackNo, OtherNumber = otherNumber, LabelTemplateId = labelTemplate?.Id ?? order.LabelTemplateId ?? input.LabelTemplateId, PrintSuccess = EnableStatus.Enabled, PrintTime = now, OperatorId = operatorId }; var savedEntity = await LabelPrintRecordRepository.InsertAsync(entity); var output = ObjectMapper.Map(savedEntity); output.LabelTemplateName = labelTemplate?.Name; output.OperatorName = CurrentUser.UserName; output.PrintUrl = print.PrintUrl; return output; } /// /// 查询最近面单打印记录列表。 /// [HttpGet("/operation/print-label/recent-list")] public async Task> GetRecentListAsync( [FromQuery] int limit = 20, [FromQuery] int? labelTemplateId = null) { var hasTemplateFilter = labelTemplateId.HasValue; var labelTemplateIdValue = labelTemplateId.GetValueOrDefault(); var entities = await LabelPrintRecordRepository.GetPagedListAsync( x => !hasTemplateFilter || x.LabelTemplateId == labelTemplateIdValue, 1, limit <= 0 ? 20 : limit, new Dictionary { [nameof(LabelPrintRecord.PrintTime)] = false, [nameof(LabelPrintRecord.Id)] = false }); var templateIds = entities.Where(x => x.LabelTemplateId.HasValue).Select(x => x.LabelTemplateId!.Value).Distinct().ToArray(); var templates = templateIds.Length == 0 ? [] : await LabelTemplateRepository.GetListAsync(x => templateIds.Contains(x.Id)); var templateMap = templates.ToDictionary(x => x.Id); var items = new List(entities.Count); foreach (var entity in entities) { var output = ObjectMapper.Map(entity); if (entity.LabelTemplateId.HasValue && templateMap.TryGetValue(entity.LabelTemplateId.Value, out var template)) { output.LabelTemplateName = template.Name; } output.OperatorName = entity.OperatorId?.ToString(); items.Add(output); } return new OmsItemsResult { Items = items }; } /// /// 查询标签模板列表。 /// [HttpGet("/operation/print-label/template-list")] public async Task> GetTemplateDropdownListAsync( [FromQuery] string? templateType = null) { var entities = await LabelTemplateRepository.GetListAsync( x => string.IsNullOrWhiteSpace(templateType) || x.TemplateType == templateType); var items = entities .Select(entity => ObjectMapper.Map(entity)) .ToList(); return new OmsItemsResult { Items = items }; } private static string NormalizeNumberType(string? numberType) { var value = numberType?.Trim().ToLowerInvariant() ?? "auto"; return value switch { "customerorderno" or "customer-order" or "client" or "clientno" => "customerorder", "track" or "trackno" => "tracking", "parceltracking" or "parcelno" or "express" => "parcel", "skucode" => "sku", "" or "all" => "auto", _ => value }; } private async Task<(Order? Order, string? MatchedType, List? Candidates)> ResolveAutoAsync(string number) { var order = await OrderRepository.GetByWaybillNoAsync(number); if (order != null) { var matched = string.Equals(order.ServiceNo, number, StringComparison.OrdinalIgnoreCase) ? "waybill" : string.Equals(order.TrackNo, number, StringComparison.OrdinalIgnoreCase) ? "tracking" : string.Equals(order.ClientNo, number, StringComparison.OrdinalIgnoreCase) ? "customerorder" : "waybill"; return (order, matched, null); } var byParcel = await ResolveByParcelTrackingAsync(number); if (byParcel.Order != null || byParcel.Candidates is { Count: > 0 }) { return byParcel; } return await ResolveBySkuAsync(number); } private async Task<(Order? Order, string? MatchedType, List? Candidates)> ResolveByTypeAsync(string number, string type) { return type switch { "waybill" => (await OrderRepository.GetByServiceNoAsync(number) ?? await OrderRepository.GetByWaybillNoAsync(number), "waybill", null), "customerorder" => ( await OrderRepository.FirstOrDefaultAsync(x => x.ClientNo == number), "customerorder", null), "tracking" => ( await OrderRepository.GetByTrackNoAsync(number), "tracking", null), "other" => ( await OrderRepository.FirstOrDefaultAsync(x => x.RequestId == number || x.ClientNo == number) ?? await OrderRepository.GetByWaybillNoAsync(number), "other", null), "parcel" => await ResolveByParcelTrackingAsync(number), "sku" => await ResolveBySkuAsync(number), _ => await ResolveAutoAsync(number) }; } private async Task<(Order? Order, string? MatchedType, List? Candidates)> ResolveByParcelTrackingAsync(string trackingNo) { var parcels = await ParcelRepository.GetListAsync(x => x.TrackingNo == trackingNo); if (parcels.Count == 0) { return (null, "parcel", null); } var parcelIds = parcels.Select(x => x.Id).ToArray(); var binds = await OrderStockBindRepository.GetListAsync(x => x.ParcelId.HasValue && parcelIds.Contains(x.ParcelId.Value) && (x.Status == OrderStockBindStatusCodes.Bound || x.Status == OrderStockBindStatusCodes.Deducted)); var orderIds = binds.Select(x => x.OrderId).Distinct().ToArray(); if (orderIds.Length == 0) { throw new BusinessException("找到包裹但未绑定到任何订单,请先绑定包裹/库存"); } if (orderIds.Length == 1) { var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == orderIds[0]); return (order, "parcel", null); } var candidates = await BuildCandidatesAsync(orderIds, "包裹快递单号命中多笔订单"); return (null, "parcel", candidates); } private async Task<(Order? Order, string? MatchedType, List? Candidates)> ResolveBySkuAsync(string skuCode) { var skus = await SkuRepository.GetListAsync(x => x.SkuCode == skuCode); if (skus.Count == 0) { return (null, "sku", null); } var skuIds = skus.Select(x => x.Id).ToArray(); var binds = await OrderStockBindRepository.GetListAsync(x => x.SkuId.HasValue && skuIds.Contains(x.SkuId.Value) && (x.Status == OrderStockBindStatusCodes.Bound || x.Status == OrderStockBindStatusCodes.Deducted)); var orderIds = binds.Select(x => x.OrderId).Distinct().ToArray(); if (orderIds.Length == 0) { throw new BusinessException("找到 SKU 但没有绑定中的订单,请确认是否已绑定"); } if (orderIds.Length == 1) { var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == orderIds[0]); return (order, "sku", null); } var candidates = await BuildCandidatesAsync(orderIds, $"SKU {skuCode} 命中多笔订单,请选择"); return (null, "sku", candidates); } private async Task> BuildCandidatesAsync(int[] orderIds, string hint) { var orders = await OrderRepository.GetListAsync(x => orderIds.Contains(x.Id)); var binds = await OrderStockBindRepository.GetListAsync(x => orderIds.Contains(x.OrderId) && (x.Status == OrderStockBindStatusCodes.Bound || x.Status == OrderStockBindStatusCodes.Deducted)); var qtyMap = binds.GroupBy(x => x.OrderId).ToDictionary(g => g.Key, g => g.Sum(x => x.BindQty)); return orders .OrderByDescending(x => x.UpdateTime) .Select(o => new PrintLabelLookupCandidateOutput { OrderId = o.Id, ServiceNo = o.ServiceNo, ClientNo = o.ClientNo, TrackNo = o.TrackNo, OrderStatusName = OmsDocHelper.GetOrderStatusName(o.OrderStatus), BindQtyTotal = qtyMap.GetValueOrDefault(o.Id), MatchedHint = hint }) .ToList(); } private async Task BuildLookupOutputAsync(Order order, string matchedType) { var binds = await OrderStockBindRepository.GetListAsync(x => x.OrderId == order.Id && (x.Status == OrderStockBindStatusCodes.Bound || x.Status == OrderStockBindStatusCodes.Deducted)); binds = binds.OrderBy(x => x.Id).ToList(); var warehouseId = order.WarehouseId ?? binds.FirstOrDefault()?.WarehouseId; string? warehouseName = null; if (warehouseId is > 0) { var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == warehouseId.Value); warehouseName = warehouse?.WarehouseName; } string? salesProductName = null; if (!string.IsNullOrWhiteSpace(order.ProductCode)) { var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim()); salesProductName = product?.ProductName; } string? countryName = order.ConsigneeCountry; if (!string.IsNullOrWhiteSpace(order.ConsigneeCountry)) { var country = await CountryRepository.FirstOrDefaultAsync(x => x.CountryCode == order.ConsigneeCountry); countryName = country?.ChineseName ?? country?.EnglishName ?? order.ConsigneeCountry; } var bindStatus = "none"; if (binds.Any(x => x.Status == OrderStockBindStatusCodes.Deducted)) { bindStatus = OrderStockBindStatusCodes.Deducted; } else if (binds.Any(x => x.Status == OrderStockBindStatusCodes.Bound)) { bindStatus = OrderStockBindStatusCodes.Bound; } var warnings = new List(); if (binds.Count == 0) { warnings.Add("noneBind"); } var binIds = binds.Where(x => x.BinLocationId.HasValue).Select(x => x.BinLocationId!.Value).Distinct().ToArray(); var binMap = new Dictionary(); if (binIds.Length > 0) { var binRepo = LazyServiceProvider.GetRequiredService(); var bins = await binRepo.GetListAsync(x => binIds.Contains(x.Id)); binMap = bins.ToDictionary(x => x.Id, x => x.BinCode); } var bindItems = binds.Select(b => new PrintLabelBindItemOutput { BindType = b.BindType, SkuCode = b.SkuCode, SkuName = b.SkuName, TrackingNo = b.TrackingNo, ProductName = b.ProductName, AttributeText = b.AttributeText, ImageUrl = b.ImageUrl, BinCode = b.BinLocationId is > 0 && binMap.TryGetValue(b.BinLocationId.Value, out var code) ? code : null, BindQty = b.BindQty, Status = b.Status }).ToList(); return new PrintLabelLookupOutput { Matched = true, MatchedNumberType = matchedType, Warnings = warnings, OrderId = order.Id, ServiceNo = order.ServiceNo, ClientNo = order.ClientNo, TrackNo = order.TrackNo, ProductCode = order.ProductCode, SalesProductName = salesProductName, DestinationCountry = order.ConsigneeCountry, DestinationCountryName = countryName, OrderStatus = order.OrderStatus, OrderStatusName = OmsDocHelper.GetOrderStatusName(order.OrderStatus), WarehouseId = warehouseId, WarehouseName = warehouseName, BindStatus = bindStatus, BindQtyTotal = binds.Sum(x => x.BindQty), BindLineCount = binds.Count, BindItems = bindItems }; } }