diff --git a/src/Oms.Application/Dtos/Operations/PrintLabelLookupOutput.cs b/src/Oms.Application/Dtos/Operations/PrintLabelLookupOutput.cs
new file mode 100644
index 0000000..be30c83
--- /dev/null
+++ b/src/Oms.Application/Dtos/Operations/PrintLabelLookupOutput.cs
@@ -0,0 +1,109 @@
+namespace Oms.Application.Dtos.Operations;
+
+///
+/// 打印标签扫码查单入参(Query)。
+///
+public class PrintLabelLookupInput
+{
+ /// 扫描号码。
+ public string Number { get; set; } = string.Empty;
+
+ ///
+ /// 号码类型:auto / waybill / customerOrder / tracking / other / parcel / sku。
+ /// 空视为 auto。
+ ///
+ public string? NumberType { get; set; }
+}
+
+///
+/// 打印标签查单结果:订单头 + 绑定明细,供操作员核对后打印。
+///
+public class PrintLabelLookupOutput
+{
+ /// 是否唯一命中订单。
+ public bool Matched { get; set; }
+
+ /// 实际命中的号码类型。
+ public string? MatchedNumberType { get; set; }
+
+ /// 告警:noneBind / multiMatch / alreadyPrinted 等。
+ public List Warnings { get; set; } = [];
+
+ public int? OrderId { get; set; }
+
+ public string? ServiceNo { get; set; }
+
+ public string? ClientNo { get; set; }
+
+ public string? TrackNo { get; set; }
+
+ public string? ProductCode { get; set; }
+
+ public string? SalesProductName { get; set; }
+
+ public string? DestinationCountry { get; set; }
+
+ public string? DestinationCountryName { get; set; }
+
+ public string? OrderStatus { get; set; }
+
+ public string? OrderStatusName { get; set; }
+
+ public int? WarehouseId { get; set; }
+
+ public string? WarehouseName { get; set; }
+
+ /// none / bound / deducted / released
+ public string BindStatus { get; set; } = "none";
+
+ /// 绑定件数合计。
+ public int BindQtyTotal { get; set; }
+
+ /// 绑定行数。
+ public int BindLineCount { get; set; }
+
+ public List BindItems { get; set; } = [];
+
+ /// SKU/模糊扫到多单时的候选列表。
+ public List Candidates { get; set; } = [];
+}
+
+public class PrintLabelBindItemOutput
+{
+ public string BindType { get; set; } = string.Empty;
+
+ public string? SkuCode { get; set; }
+
+ public string? SkuName { get; set; }
+
+ public string? TrackingNo { get; set; }
+
+ public string? ProductName { get; set; }
+
+ public string? AttributeText { get; set; }
+
+ public string? ImageUrl { get; set; }
+
+ public string? BinCode { get; set; }
+
+ public int BindQty { get; set; }
+
+ public string Status { get; set; } = string.Empty;
+}
+
+public class PrintLabelLookupCandidateOutput
+{
+ public int OrderId { get; set; }
+
+ public string? ServiceNo { get; set; }
+
+ public string? ClientNo { get; set; }
+
+ public string? TrackNo { get; set; }
+
+ public string? OrderStatusName { get; set; }
+
+ public int BindQtyTotal { get; set; }
+
+ public string? MatchedHint { get; set; }
+}
diff --git a/src/Oms.Application/Services/Operations/LabelPrintService.cs b/src/Oms.Application/Services/Operations/LabelPrintService.cs
index 95442ef..14ca1b8 100644
--- a/src/Oms.Application/Services/Operations/LabelPrintService.cs
+++ b/src/Oms.Application/Services/Operations/LabelPrintService.cs
@@ -2,11 +2,17 @@ using Fake.Application;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Operations;
using Oms.Application.Dtos.Shared;
+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;
@@ -18,12 +24,70 @@ 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();
+
+ ///
+ /// 扫码查单:运单/客户单/跟踪号/其他单号/包裹快递单号/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)
{
@@ -89,11 +153,10 @@ public class LabelPrintService : ApplicationService
///
/// 查询最近面单打印记录列表。
///
- /// 入参:返回记录条数上限。
- /// 入参:标签模板主键,可选。
- /// 面单打印记录列表结果。
[HttpGet("/operation/print-label/recent-list")]
- public async Task> GetRecentListAsync([FromQuery] int limit = 20, [FromQuery] int? labelTemplateId = null)
+ public async Task> GetRecentListAsync(
+ [FromQuery] int limit = 20,
+ [FromQuery] int? labelTemplateId = null)
{
var hasTemplateFilter = labelTemplateId.HasValue;
var labelTemplateIdValue = labelTemplateId.GetValueOrDefault();
@@ -127,10 +190,9 @@ public class LabelPrintService : ApplicationService
///
/// 查询标签模板列表。
///
- /// 入参:模板类型,可选。
- /// 标签模板列表结果。
[HttpGet("/operation/print-label/template-list")]
- public async Task> GetTemplateDropdownListAsync([FromQuery] string? templateType = null)
+ public async Task> GetTemplateDropdownListAsync(
+ [FromQuery] string? templateType = null)
{
var entities = await LabelTemplateRepository.GetListAsync(
x => string.IsNullOrWhiteSpace(templateType) || x.TemplateType == templateType);
@@ -140,4 +202,244 @@ public class LabelPrintService : ApplicationService
.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
+ };
+ }
}