diff --git a/src/Oms.Application/Dtos/Customers/CreateCustomerInput.cs b/src/Oms.Application/Dtos/Customers/CreateCustomerInput.cs
index e66c573..e1a76b4 100644
--- a/src/Oms.Application/Dtos/Customers/CreateCustomerInput.cs
+++ b/src/Oms.Application/Dtos/Customers/CreateCustomerInput.cs
@@ -9,11 +9,10 @@ namespace Oms.Application.Dtos.Customers;
public class CreateCustomerInput
{
///
- /// 客户编号。
+ /// 客户编号(留空时系统自动生成,规则 W+年份后2位+月份2位+3位顺序)。
///
- [Required]
[StringLength(32)]
- public string CustomerCode { get; set; } = string.Empty;
+ public string? CustomerCode { get; set; }
///
/// 客户名称。
@@ -32,9 +31,10 @@ public class CreateCustomerInput
///
/// 邮箱。
///
+ [Required]
[EmailAddress]
[StringLength(64)]
- public string? Email { get; set; }
+ public string Email { get; set; } = string.Empty;
///
/// 微信头像地址。
diff --git a/src/Oms.Application/Dtos/Orders/OrderPriceCheckFailureOutput.cs b/src/Oms.Application/Dtos/Orders/OrderPriceCheckFailureOutput.cs
index e7108c9..72571f0 100644
--- a/src/Oms.Application/Dtos/Orders/OrderPriceCheckFailureOutput.cs
+++ b/src/Oms.Application/Dtos/Orders/OrderPriceCheckFailureOutput.cs
@@ -29,4 +29,14 @@ public class OrderPriceCheckFailureOutput
/// 未命中原因说明。
///
public string Reason { get; set; } = string.Empty;
+
+ ///
+ /// 货物类型名称(展示)。
+ ///
+ public string? CargoTypeName { get; set; }
+
+ ///
+ /// 仓库名称(展示)。
+ ///
+ public string? WarehouseName { get; set; }
}
diff --git a/src/Oms.Application/Dtos/Orders/OrderPriceCheckInput.cs b/src/Oms.Application/Dtos/Orders/OrderPriceCheckInput.cs
index 3cfacda..1b74736 100644
--- a/src/Oms.Application/Dtos/Orders/OrderPriceCheckInput.cs
+++ b/src/Oms.Application/Dtos/Orders/OrderPriceCheckInput.cs
@@ -36,10 +36,20 @@ public class OrderPriceCheckInput
public decimal? Height { get; set; }
///
- /// 货物类型。
+ /// 货物类型(单选,兼容旧版;请优先使用 CargoTypes)。
///
public string? CargoType { get; set; }
+ ///
+ /// 货物类型(多选,字典 cargo_attribute 编码)。
+ ///
+ public List? CargoTypes { get; set; }
+
+ ///
+ /// 仓库主键(多选;为空则遍历全部仓库)。
+ ///
+ public List? WarehouseIds { get; set; }
+
///
/// 目的省份。
///
diff --git a/src/Oms.Application/Dtos/Orders/OrderPriceCheckOutput.cs b/src/Oms.Application/Dtos/Orders/OrderPriceCheckOutput.cs
index a303919..6b67581 100644
--- a/src/Oms.Application/Dtos/Orders/OrderPriceCheckOutput.cs
+++ b/src/Oms.Application/Dtos/Orders/OrderPriceCheckOutput.cs
@@ -15,6 +15,21 @@ public class OrderPriceCheckOutput
///
public string ShippingMethod { get; set; } = string.Empty;
+ ///
+ /// 货物类型编码(字典 cargo_attribute)。
+ ///
+ public string? CargoType { get; set; }
+
+ ///
+ /// 货物类型名称(展示用)。
+ ///
+ public string? CargoTypeName { get; set; }
+
+ ///
+ /// 仓库主键。
+ ///
+ public int? WarehouseId { get; set; }
+
///
/// 仓库名称。
///
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequest.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequest.cs
index 79c71fa..726d2ce 100644
--- a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequest.cs
+++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequest.cs
@@ -17,6 +17,9 @@ public class SmallBagTrackUpsertRequest
public string Product { get; set; } = string.Empty;
+ ///
+ /// 轨迹服务商 code(对应轨迹 ServiceProviderCode,取订单 DownstreamSupplierCode)。
+ ///
public string ApiCode { get; set; } = string.Empty;
public SmallBagTrackConsigneeRequest Consignee { get; set; } = new();
diff --git a/src/Oms.Application/Services/Customers/CustomerService.cs b/src/Oms.Application/Services/Customers/CustomerService.cs
index fc948e4..ad04976 100644
--- a/src/Oms.Application/Services/Customers/CustomerService.cs
+++ b/src/Oms.Application/Services/Customers/CustomerService.cs
@@ -102,7 +102,7 @@ public class CustomerService : ApplicationService
[HttpPost("/customer/create")]
public async Task CreateAsync(CreateCustomerInput input)
{
- var normalized = NormalizeCustomerInput(input, "客户编号", "客户名称");
+ var normalized = await NormalizeCreateCustomerInputAsync(input, "客户编号", "客户名称");
await EnsureCustomerIdentityUniqueAsync(normalized.CustomerCode, normalized.Phone, normalized.Email);
@@ -134,7 +134,7 @@ public class CustomerService : ApplicationService
var mainCustomer = await GetCurrentMainCustomerOrThrowAsync();
ValidateCustomerStatus(mainCustomer);
- var normalized = NormalizeCustomerInput(input, "子客户编号", "子客户名称");
+ var normalized = await NormalizeCreateCustomerInputAsync(input, "子客户编号", "子客户名称");
var permissions = await NormalizeAndValidateSubCustomerPermissionsAsync(input.Permissions);
await EnsureCustomerIdentityUniqueAsync(normalized.CustomerCode, normalized.Phone, normalized.Email);
@@ -314,7 +314,7 @@ public class CustomerService : ApplicationService
return customer;
}
- private async Task EnsureCustomerIdentityUniqueAsync(string customerCode, string phone, string? email)
+ private async Task EnsureCustomerIdentityUniqueAsync(string customerCode, string phone, string email)
{
if (await CustomerRepository.GetByCustomerCodeAsync(customerCode) != null)
{
@@ -326,7 +326,7 @@ public class CustomerService : ApplicationService
throw new BusinessException("手机号已存在");
}
- if (email != null && await CustomerRepository.GetByEmailAsync(email) != null)
+ if (await CustomerRepository.GetByEmailAsync(email) != null)
{
throw new BusinessException("邮箱已存在");
}
@@ -399,16 +399,20 @@ public class CustomerService : ApplicationService
};
}
- private static NormalizedCustomerInput NormalizeCustomerInput(
+ private async Task NormalizeCreateCustomerInputAsync(
CreateCustomerInput input,
string codeFieldName,
string nameFieldName)
{
+ var customerCode = string.IsNullOrWhiteSpace(input.CustomerCode)
+ ? await CustomerCodeGenerator.GenerateNextAsync(CustomerRepository)
+ : NormalizeRequiredText(input.CustomerCode, codeFieldName);
+
return new NormalizedCustomerInput(
- NormalizeRequiredText(input.CustomerCode, codeFieldName),
+ customerCode,
NormalizeRequiredText(input.CustomerName, nameFieldName),
NormalizeRequiredText(input.Phone, "手机号"),
- NormalizeOptionalText(input.Email));
+ NormalizeRequiredText(input.Email, "邮箱"));
}
private async Task> NormalizeAndValidateSubCustomerPermissionsAsync(IEnumerable? permissions)
@@ -499,5 +503,5 @@ public class CustomerService : ApplicationService
string CustomerCode,
string CustomerName,
string Phone,
- string? Email);
+ string Email);
}
diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
index 928b291..6226fb8 100644
--- a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
@@ -1489,12 +1489,7 @@ public partial class OrderService
var recipients = await OrderRepository.GetRecipientsByOrderIdsAsync([order.Id]);
var recipient = recipients.FirstOrDefault();
- var apiCode = order.ChannelForecastCode?.Trim() ?? string.Empty;
- if (string.IsNullOrWhiteSpace(apiCode) && !string.IsNullOrWhiteSpace(order.ChannelCode))
- {
- var channel = await ServiceChannelRepository.GetByChannelCodeAsync(order.ChannelCode.Trim());
- apiCode = channel?.ForecastCode?.Trim() ?? string.Empty;
- }
+ var apiCode = order.DownstreamSupplierCode?.Trim() ?? string.Empty;
return new SmallBagTrackUpsertRequest
{
diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelQuote.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelQuote.cs
index 460637b..8c585ec 100644
--- a/src/Oms.Application/Services/Orders/OrderService.ParcelQuote.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.ParcelQuote.cs
@@ -9,18 +9,21 @@ namespace Oms.Application.Services.Orders;
public partial class OrderService
{
+ private sealed record PriceCheckCargoTarget(string Code, string DisplayName, string CargoAttribute);
+
private async Task BuildPriceCheckResultAsync(OrderPriceCheckInput input)
{
ValidatePriceCheckInput(input);
var bindTime = DateTime.Now;
var countryCode = await ResolveDestinationCountryCodeAsync(input.DestinationCountry);
- var cargoAttribute = ParcelBillingCalculator.MapCargoTypeToAttribute(input.CargoType);
- var quoteContext = new ParcelQuoteContext
+ var cargoTargets = ResolvePriceCheckCargoTargets(input);
+ var warehouses = await ResolvePriceCheckWarehousesAsync(input);
+
+ var baseContext = new ParcelQuoteContext
{
CountryCode = countryCode,
Province = input.DestinationProvince,
PostalCode = input.PostalCode,
- CargoAttribute = cargoAttribute,
Length = input.Length!.Value,
Width = input.Width!.Value,
Height = input.Height!.Value,
@@ -48,47 +51,120 @@ public partial class OrderService
return result;
}
- var warehouses = await WarehouseAddressRepository.GetListAsync();
- var defaultWarehouse = warehouses.OrderBy(x => x.Id).FirstOrDefault();
-
foreach (var product in products.OrderBy(x => x.Id))
{
- var attempt = await TryComputePriceCheckQuoteAsync(product, quoteContext, customerGroup, bindTime);
- if (attempt.Success)
+ foreach (var cargo in cargoTargets)
{
- var quote = attempt.Quote!;
- var (freight, otherFees, totalCost, costFormula) = ParcelBillingCalculator.SummarizeFees(quote.Fees);
- result.Items.Add(new OrderPriceCheckOutput
+ foreach (var warehouse in warehouses)
{
- Id = quote.ProductId,
- ProductCode = quote.ProductCode,
- ShippingMethod = quote.ProductName,
- Warehouse = defaultWarehouse?.WarehouseName,
- DeliveryTime = $"{product.MinTimeliness ?? 0}-{product.MaxTimeliness ?? 0}",
- VolumetricWeight = quote.VolumetricWeight,
- ChargeableWeight = quote.ChargeableWeight,
- CostFormula = costFormula,
- Freight = freight,
- OtherFees = otherFees,
- TotalCost = totalCost,
- MaxDeliveryDays = product.MaxTimeliness,
- TotalCostNum = totalCost,
- RulesVersion = quote.RulesVersion,
- PriceVersion = quote.PriceVersion,
- IsRemote = quote.IsRemote
- });
- continue;
- }
+ var quoteContext = new ParcelQuoteContext
+ {
+ CountryCode = baseContext.CountryCode,
+ Province = baseContext.Province,
+ PostalCode = baseContext.PostalCode,
+ CargoAttribute = cargo.CargoAttribute,
+ CargoTypeCode = cargo.Code,
+ WarehouseId = warehouse.Id,
+ WarehouseName = warehouse.WarehouseName,
+ Length = baseContext.Length,
+ Width = baseContext.Width,
+ Height = baseContext.Height,
+ ActualWeight = baseContext.ActualWeight
+ };
- if (attempt.Failure != null)
- {
- result.Failures.Add(attempt.Failure);
+ var attempt = await TryComputePriceCheckQuoteAsync(
+ product,
+ quoteContext,
+ customerGroup,
+ bindTime,
+ cargo.DisplayName,
+ warehouse.WarehouseName);
+
+ if (attempt.Success)
+ {
+ var quote = attempt.Quote!;
+ var (freight, otherFees, totalCost, costFormula) = ParcelBillingCalculator.SummarizeFees(quote.Fees);
+ result.Items.Add(new OrderPriceCheckOutput
+ {
+ Id = quote.ProductId,
+ ProductCode = quote.ProductCode,
+ ShippingMethod = quote.ProductName,
+ CargoType = cargo.Code,
+ CargoTypeName = cargo.DisplayName,
+ WarehouseId = warehouse.Id,
+ Warehouse = warehouse.WarehouseName,
+ DeliveryTime = $"{product.MinTimeliness ?? 0}-{product.MaxTimeliness ?? 0}",
+ VolumetricWeight = quote.VolumetricWeight,
+ ChargeableWeight = quote.ChargeableWeight,
+ CostFormula = costFormula,
+ Freight = freight,
+ OtherFees = otherFees,
+ TotalCost = totalCost,
+ MaxDeliveryDays = product.MaxTimeliness,
+ TotalCostNum = totalCost,
+ RulesVersion = quote.RulesVersion,
+ PriceVersion = quote.PriceVersion,
+ IsRemote = quote.IsRemote
+ });
+ continue;
+ }
+
+ if (attempt.Failure != null)
+ {
+ result.Failures.Add(attempt.Failure);
+ }
+ }
}
}
return result;
}
+ private static List ResolvePriceCheckCargoTargets(OrderPriceCheckInput input)
+ {
+ var codes = (input.CargoTypes ?? [])
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Select(x => x.Trim())
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ if (codes.Count == 0 && !string.IsNullOrWhiteSpace(input.CargoType))
+ {
+ codes.Add(input.CargoType.Trim());
+ }
+
+ if (codes.Count == 0)
+ {
+ codes.Add(string.Empty);
+ }
+
+ return codes.Select(code =>
+ {
+ var attribute = ParcelBillingCalculator.MapCargoTypeToAttribute(code);
+ var display = string.IsNullOrWhiteSpace(code)
+ ? ParcelBillingCalculator.GetCargoAttributeLabel(attribute)
+ : code;
+ return new PriceCheckCargoTarget(code, display, attribute);
+ }).ToList();
+ }
+
+ private async Task> ResolvePriceCheckWarehousesAsync(OrderPriceCheckInput input)
+ {
+ var ids = (input.WarehouseIds ?? [])
+ .Where(x => x > 0)
+ .Distinct()
+ .ToList();
+
+ if (ids.Count > 0)
+ {
+ var selected = await WarehouseAddressRepository.GetListAsync(x => ids.Contains(x.Id));
+ return selected.OrderBy(x => x.Id).ToList();
+ }
+
+ var all = await WarehouseAddressRepository.GetListAsync();
+ return all.OrderBy(x => x.Id).ToList();
+ }
+
private static void ValidatePriceCheckInput(OrderPriceCheckInput input)
{
if (string.IsNullOrWhiteSpace(input.DestinationCountry))
@@ -177,11 +253,13 @@ public partial class OrderService
SalesProduct product,
ParcelQuoteContext context,
CustomerGroup? customerGroup,
- DateTime bindTime)
+ DateTime bindTime,
+ string cargoTypeName,
+ string warehouseName)
{
if (product.IsActive != EnableStatus.Enabled)
{
- return PriceCheckFailure(product, "product", "产品未启用");
+ return PriceCheckFailure(product, "product", "产品未启用", cargoTypeName, warehouseName);
}
ProductSnapshotBinding snapshotBinding;
@@ -191,7 +269,7 @@ public partial class OrderService
}
catch (BusinessException ex)
{
- return PriceCheckFailure(product, "snapshot", ex.Message);
+ return PriceCheckFailure(product, "snapshot", ex.Message, cargoTypeName, warehouseName);
}
if (await IsBlacklistHitAsync(
@@ -205,7 +283,9 @@ public partial class OrderService
return PriceCheckFailure(
product,
"blacklist",
- $"目的国 {context.CountryCode} 邮编 {FormatPostalCode(context.PostalCode)} 命中产品黑名单");
+ $"目的国 {context.CountryCode} 邮编 {FormatPostalCode(context.PostalCode)} 命中产品黑名单",
+ cargoTypeName,
+ warehouseName);
}
var forecast = ParcelBillingCalculator.BuildForecastWeight(context);
@@ -215,7 +295,7 @@ public partial class OrderService
}
catch (BusinessException ex)
{
- return PriceCheckFailure(product, "weight", ex.Message);
+ return PriceCheckFailure(product, "weight", ex.Message, cargoTypeName, warehouseName);
}
var priceAreaId = ResolveEffectivePriceAreaId(product, snapshotBinding.Rules, snapshotBinding.Price);
@@ -228,7 +308,9 @@ public partial class OrderService
return PriceCheckFailure(
product,
"zone",
- $"{snapshotZoneHint}产品未配置价格分区(PriceAreaId),无法根据国家+邮编匹配卖价明细中的分区键");
+ $"{snapshotZoneHint}产品未配置价格分区(PriceAreaId),无法根据国家+邮编匹配卖价明细中的分区键",
+ cargoTypeName,
+ warehouseName);
}
var zoneKeys = await ResolvePriceZoneKeysAsync(
@@ -249,7 +331,9 @@ public partial class OrderService
product,
"zone",
$"{snapshotZoneHint}目的国 {context.CountryCode} 邮编 {FormatPostalCode(context.PostalCode)} 未命中价格快照内分区规则(PriceAreaId={priceAreaId})," +
- "请检查发布时的分区国家/邮编配置是否与卖价明细分区键一致");
+ "请检查发布时的分区国家/邮编配置是否与卖价明细分区键一致",
+ cargoTypeName,
+ warehouseName);
}
var chargeableWeight = forecast.ChargeableWeight ?? 0;
@@ -260,13 +344,15 @@ public partial class OrderService
chargeableWeight,
zoneKeys,
context.CargoAttribute,
- customerGroup);
+ customerGroup,
+ context.WarehouseId,
+ context.WarehouseName);
if (fees.Count == 0)
{
if (!ParcelBillingCalculator.HasEnabledQuotePriceItems(snapshotBinding.Price))
{
- return PriceCheckFailure(product, "price", "卖价快照中无启用的价格项");
+ return PriceCheckFailure(product, "price", "卖价快照中无启用的价格项", cargoTypeName, warehouseName);
}
return PriceCheckFailure(
@@ -277,7 +363,11 @@ public partial class OrderService
chargeableWeight,
zoneKeys,
context.CargoAttribute,
- customerGroup));
+ customerGroup,
+ context.WarehouseId,
+ context.WarehouseName),
+ cargoTypeName,
+ warehouseName);
}
var isRemote = await IsRemoteHitAsync(
@@ -306,7 +396,12 @@ public partial class OrderService
};
}
- private static PriceCheckAttemptResult PriceCheckFailure(SalesProduct product, string step, string reason) =>
+ private static PriceCheckAttemptResult PriceCheckFailure(
+ SalesProduct product,
+ string step,
+ string reason,
+ string? cargoTypeName = null,
+ string? warehouseName = null) =>
new()
{
Failure = new OrderPriceCheckFailureOutput
@@ -315,10 +410,31 @@ public partial class OrderService
ProductCode = product.ProductCode,
ProductName = product.ProductName,
Step = step,
- Reason = reason
+ CargoTypeName = cargoTypeName,
+ WarehouseName = warehouseName,
+ Reason = PrefixPriceCheckFailureReason(cargoTypeName, warehouseName, reason)
}
};
+ private static string PrefixPriceCheckFailureReason(
+ string? cargoTypeName,
+ string? warehouseName,
+ string reason)
+ {
+ var parts = new List();
+ if (!string.IsNullOrWhiteSpace(cargoTypeName))
+ {
+ parts.Add($"货物类型={cargoTypeName.Trim()}");
+ }
+
+ if (!string.IsNullOrWhiteSpace(warehouseName))
+ {
+ parts.Add($"仓库={warehouseName.Trim()}");
+ }
+
+ return parts.Count == 0 ? reason : $"[{string.Join(",", parts)}] {reason}";
+ }
+
private static string FormatPostalCode(string? postalCode) =>
string.IsNullOrWhiteSpace(postalCode) ? "-" : postalCode.Trim();
}
diff --git a/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs b/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs
index a81786d..64c3b38 100644
--- a/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs
+++ b/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs
@@ -353,7 +353,9 @@ internal static class ParcelBillingCalculator
decimal chargeableWeight,
IReadOnlyList zoneKeys,
string cargoAttribute,
- CustomerGroup? customerGroup) =>
+ CustomerGroup? customerGroup,
+ int? warehouseId = null,
+ string? warehouseName = null) =>
CalculateReceivableFees(
priceSnapshot,
priceVersion,
@@ -362,7 +364,9 @@ internal static class ParcelBillingCalculator
zoneKeys,
cargoAttribute,
customerGroup,
- OrderFeeChargingStageCodes.Forecast);
+ OrderFeeChargingStageCodes.Forecast,
+ warehouseId,
+ warehouseName);
public static bool HasEnabledQuotePriceItems(SalesProductPricePublishSnapshotDto priceSnapshot) =>
HasEnabledReceivablePriceItems(priceSnapshot);
@@ -399,9 +403,12 @@ internal static class ParcelBillingCalculator
decimal chargeableWeight,
IReadOnlyList zoneKeys,
string cargoAttribute,
- CustomerGroup? customerGroup)
+ CustomerGroup? customerGroup,
+ int? warehouseId = null,
+ string? warehouseName = null)
{
var cargoLabel = GetCargoAttributeLabel(cargoAttribute);
+ var warehouseLabel = FormatWarehouseLabel(warehouseId, warehouseName);
var enabledPrices = priceSnapshot.Prices.Where(x => x.Status == 1).ToList();
if (enabledPrices.Count == 0)
{
@@ -462,9 +469,31 @@ internal static class ParcelBillingCalculator
continue;
}
+ var warehouseMatchedDetails = cargoMatchedDetails.Where(detail =>
+ {
+ var factors = ParseFactorDictionary(detail.FactorValuesJson);
+ return MatchWarehouseFactor(factors, warehouseId, warehouseName);
+ }).ToList();
+
+ if (warehouseMatchedDetails.Count == 0)
+ {
+ var requiredWarehouse = cargoMatchedDetails
+ .Select(detail =>
+ {
+ var factors = ParseFactorDictionary(detail.FactorValuesJson);
+ return TryGetFactorValue(factors, out var wh, "仓库", "warehouse") ? wh.Trim() : null;
+ })
+ .Where(wh => !string.IsNullOrWhiteSpace(wh))
+ .Distinct(StringComparer.OrdinalIgnoreCase);
+ var requiredText = requiredWarehouse.Any() ? string.Join(", ", requiredWarehouse) : "(明细未限定仓库)";
+ reasons.Add(
+ $"【{label}】价格项Id={price.Id}:仓库不匹配,当前={warehouseLabel},明细要求=[{requiredText}]");
+ continue;
+ }
+
var weightMatched = false;
var intervals = new List();
- foreach (var detail in cargoMatchedDetails)
+ foreach (var detail in warehouseMatchedDetails)
{
var factors = ParseFactorDictionary(detail.FactorValuesJson);
if (!TryGetWeightFactorText(factors, out var weightText))
@@ -547,7 +576,9 @@ internal static class ParcelBillingCalculator
IReadOnlyList zoneKeys,
string cargoAttribute,
CustomerGroup? customerGroup,
- string chargingStage)
+ string chargingStage,
+ int? warehouseId = null,
+ string? warehouseName = null)
{
var items = priceSnapshot.Prices.Select(p => new ReceivableBillingPriceItem(
p.Status, p.CostItem, p.Currency, p.Details));
@@ -559,7 +590,9 @@ internal static class ParcelBillingCalculator
zoneKeys,
cargoAttribute,
customerGroup,
- chargingStage);
+ chargingStage,
+ warehouseId,
+ warehouseName);
}
///
@@ -697,12 +730,20 @@ internal static class ParcelBillingCalculator
IReadOnlyList zoneKeys,
string cargoAttribute,
CustomerGroup? customerGroup,
- string chargingStage)
+ string chargingStage,
+ int? warehouseId = null,
+ string? warehouseName = null)
{
var fees = new List();
foreach (var priceItem in priceItems.Where(x => x.Status == 1))
{
- var detail = MatchPriceDetail(priceItem.Details, zoneKeys, cargoAttribute, chargeableWeight);
+ var detail = MatchPriceDetail(
+ priceItem.Details,
+ zoneKeys,
+ cargoAttribute,
+ chargeableWeight,
+ warehouseId,
+ warehouseName);
if (detail == null)
{
continue;
@@ -714,7 +755,9 @@ internal static class ParcelBillingCalculator
zoneKeys,
cargoAttribute,
chargeableWeight,
- customerGroup);
+ customerGroup,
+ warehouseId,
+ warehouseName);
fees.Add(new OrderFee
{
Type = OrderFeeTypeCodes.Receivable,
@@ -738,12 +781,20 @@ internal static class ParcelBillingCalculator
IReadOnlyList zoneKeys,
string cargoAttribute,
CustomerGroup? customerGroup,
- string chargingStage)
+ string chargingStage,
+ int? warehouseId = null,
+ string? warehouseName = null)
{
var fees = new List();
foreach (var priceItem in priceItems.Where(x => x.Status == 1))
{
- var detail = MatchPriceDetail(priceItem.Details, zoneKeys, cargoAttribute, chargeableWeight);
+ var detail = MatchPriceDetail(
+ priceItem.Details,
+ zoneKeys,
+ cargoAttribute,
+ chargeableWeight,
+ warehouseId,
+ warehouseName);
if (detail == null)
{
continue;
@@ -755,7 +806,9 @@ internal static class ParcelBillingCalculator
zoneKeys,
cargoAttribute,
chargeableWeight,
- customerGroup);
+ customerGroup,
+ warehouseId,
+ warehouseName);
fees.Add(new OrderFee
{
Type = OrderFeeTypeCodes.Payable,
@@ -787,7 +840,9 @@ internal static class ParcelBillingCalculator
IReadOnlyList details,
IReadOnlyList zoneKeys,
string cargoAttribute,
- decimal chargeableWeight)
+ decimal chargeableWeight,
+ int? warehouseId = null,
+ string? warehouseName = null)
{
var cargoLabel = GetCargoAttributeLabel(cargoAttribute);
ChannelPriceDetailOutput? bestMatch = null;
@@ -806,6 +861,11 @@ internal static class ParcelBillingCalculator
continue;
}
+ if (!MatchWarehouseFactor(factors, warehouseId, warehouseName))
+ {
+ continue;
+ }
+
if (!TryGetWeightFactorText(factors, out var weightText))
{
if (bestMatch == null)
@@ -911,6 +971,41 @@ internal static class ParcelBillingCalculator
|| string.Equals(actual.Trim(), cargoLabel, StringComparison.Ordinal);
}
+ private static bool MatchWarehouseFactor(
+ Dictionary factors,
+ int? warehouseId,
+ string? warehouseName)
+ {
+ if (!TryGetFactorValue(factors, out var actual, "仓库", "warehouse") || string.IsNullOrWhiteSpace(actual))
+ {
+ return true;
+ }
+
+ var expected = actual.Trim();
+ if (warehouseId.HasValue && string.Equals(expected, warehouseId.Value.ToString(), StringComparison.Ordinal))
+ {
+ return true;
+ }
+
+ if (!string.IsNullOrWhiteSpace(warehouseName)
+ && string.Equals(expected, warehouseName.Trim(), StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static string FormatWarehouseLabel(int? warehouseId, string? warehouseName)
+ {
+ if (!string.IsNullOrWhiteSpace(warehouseName))
+ {
+ return warehouseName.Trim();
+ }
+
+ return warehouseId.HasValue ? warehouseId.Value.ToString() : "未指定";
+ }
+
private static bool TryGetFactorValue(
Dictionary factors,
out string value,
@@ -963,7 +1058,9 @@ internal static class ParcelBillingCalculator
IReadOnlyList zoneKeys,
string cargoAttribute,
decimal chargeableWeight,
- CustomerGroup? customerGroup)
+ CustomerGroup? customerGroup,
+ int? warehouseId = null,
+ string? warehouseName = null)
{
var cargoLabel = GetCargoAttributeLabel(cargoAttribute);
return NormalizeCalculationFormula(detail.CalculationFormula) switch
@@ -976,7 +1073,9 @@ internal static class ParcelBillingCalculator
zoneKeys,
cargoLabel,
chargeableWeight,
- customerGroup),
+ customerGroup,
+ warehouseId,
+ warehouseName),
_ => detail.CalculatedValue ?? 0
};
}
@@ -1002,7 +1101,9 @@ internal static class ParcelBillingCalculator
IReadOnlyList zoneKeys,
string cargoLabel,
decimal chargeableWeight,
- CustomerGroup? customerGroup)
+ CustomerGroup? customerGroup,
+ int? warehouseId = null,
+ string? warehouseName = null)
{
var factors = ParseFactorDictionary(detail.FactorValuesJson);
if (TryGetWeightFactorText(factors, out var weightText)
@@ -1013,6 +1114,8 @@ internal static class ParcelBillingCalculator
cargoLabel,
rangeStart,
customerGroup,
+ warehouseId,
+ warehouseName,
out var basePrice))
{
var continueUnitPrice = ResolveCustomerGroupPrice(detail, customerGroup) ?? detail.CalculatedValue ?? 0;
@@ -1039,13 +1142,17 @@ internal static class ParcelBillingCalculator
string cargoLabel,
decimal continueRangeStart,
CustomerGroup? customerGroup,
+ int? warehouseId,
+ string? warehouseName,
out decimal basePrice)
{
basePrice = 0;
foreach (var candidate in allDetails)
{
var factors = ParseFactorDictionary(candidate.FactorValuesJson);
- if (!ZoneMatches(factors, zoneKeys) || !MatchCargoTypeFactor(factors, cargoLabel))
+ if (!ZoneMatches(factors, zoneKeys)
+ || !MatchCargoTypeFactor(factors, cargoLabel)
+ || !MatchWarehouseFactor(factors, warehouseId, warehouseName))
{
continue;
}
diff --git a/src/Oms.Application/Services/Orders/ParcelQuoteContext.cs b/src/Oms.Application/Services/Orders/ParcelQuoteContext.cs
index 4c8ea33..357469a 100644
--- a/src/Oms.Application/Services/Orders/ParcelQuoteContext.cs
+++ b/src/Oms.Application/Services/Orders/ParcelQuoteContext.cs
@@ -15,6 +15,13 @@ internal sealed class ParcelQuoteContext
public string CargoAttribute { get; init; } = string.Empty;
+ /// 查价入参原始货物类型编码(字典 cargo_attribute)。
+ public string? CargoTypeCode { get; init; }
+
+ public int? WarehouseId { get; init; }
+
+ public string? WarehouseName { get; init; }
+
public decimal Length { get; init; }
public decimal Width { get; init; }
diff --git a/src/Oms.Application/Shared/CustomerCodeGenerator.cs b/src/Oms.Application/Shared/CustomerCodeGenerator.cs
new file mode 100644
index 0000000..9f4fff3
--- /dev/null
+++ b/src/Oms.Application/Shared/CustomerCodeGenerator.cs
@@ -0,0 +1,44 @@
+using Fake.Application;
+using Oms.Domain.CustomersAggregate.Repositories;
+
+namespace Oms.Application.Shared;
+
+///
+/// 客户编号生成:W + 年份后2位 + 月份2位 + 3位顺序号,如 W2606001。
+///
+public static class CustomerCodeGenerator
+{
+ private const int MaxSequence = 999;
+ private const int MaxRetries = 5;
+
+ public static async Task GenerateNextAsync(
+ ICustomerRepository repository,
+ CancellationToken cancellationToken = default)
+ {
+ for (var attempt = 0; attempt < MaxRetries; attempt++)
+ {
+ var code = await BuildNextCodeAsync(repository, cancellationToken);
+ if (await repository.GetByCustomerCodeAsync(code, cancellationToken) == null)
+ {
+ return code;
+ }
+ }
+
+ throw new BusinessException("客户编号生成失败,请重试");
+ }
+
+ private static async Task BuildNextCodeAsync(
+ ICustomerRepository repository,
+ CancellationToken cancellationToken)
+ {
+ var prefix = $"W{DateTime.Now:yyMM}";
+ var maxSeq = await repository.GetMaxCustomerCodeSequenceForPrefixAsync(prefix, cancellationToken);
+ var next = maxSeq + 1;
+ if (next > MaxSequence)
+ {
+ throw new BusinessException($"当月客户编号已达上限({prefix}xxx)");
+ }
+
+ return $"{prefix}{next:D3}";
+ }
+}
diff --git a/src/Oms.Domain/CustomersAggregate/Repositories/ICustomerRepository.cs b/src/Oms.Domain/CustomersAggregate/Repositories/ICustomerRepository.cs
index 1b7cc08..d22c4c9 100644
--- a/src/Oms.Domain/CustomersAggregate/Repositories/ICustomerRepository.cs
+++ b/src/Oms.Domain/CustomersAggregate/Repositories/ICustomerRepository.cs
@@ -14,4 +14,9 @@ public interface ICustomerRepository : IRepository
Task GetByWechatOpenidAsync(string wechatOpenid, CancellationToken cancellationToken = default);
Task ExistsPhoneAsync(string phone, CancellationToken cancellationToken = default);
+
+ ///
+ /// 查询指定前缀(WyyMM)下已用顺序号的最大值。
+ ///
+ Task GetMaxCustomerCodeSequenceForPrefixAsync(string prefix, CancellationToken cancellationToken = default);
}
diff --git a/src/Oms.SqlSugarCore/Customers/CustomerRepository.cs b/src/Oms.SqlSugarCore/Customers/CustomerRepository.cs
index f0a4821..3304742 100644
--- a/src/Oms.SqlSugarCore/Customers/CustomerRepository.cs
+++ b/src/Oms.SqlSugarCore/Customers/CustomerRepository.cs
@@ -31,4 +31,33 @@ public class CustomerRepository : OmsSqlSugarRepository, ICustomerRepo
{
return AnyAsync(x => x.Phone == phone, cancellationToken);
}
+
+ public async Task GetMaxCustomerCodeSequenceForPrefixAsync(
+ string prefix,
+ CancellationToken cancellationToken = default)
+ {
+ var expectedLength = prefix.Length + 3;
+ var db = await GetDbContextAsync(cancellationToken);
+ var codes = await db.Queryable()
+ .Where(x => x.CustomerCode.StartsWith(prefix) && x.CustomerCode.Length == expectedLength)
+ .Select(x => x.CustomerCode)
+ .ToListAsync(cancellationToken);
+
+ var max = 0;
+ foreach (var code in codes)
+ {
+ if (code.Length <= prefix.Length)
+ {
+ continue;
+ }
+
+ var suffix = code[prefix.Length..];
+ if (int.TryParse(suffix, out var seq) && seq > max)
+ {
+ max = seq;
+ }
+ }
+
+ return max;
+ }
}
diff --git a/src/SimpleAdmin.Api/WeChat/Dtos/WeChatDtos.cs b/src/SimpleAdmin.Api/WeChat/Dtos/WeChatDtos.cs
index 9b8922d..3e5be5c 100644
--- a/src/SimpleAdmin.Api/WeChat/Dtos/WeChatDtos.cs
+++ b/src/SimpleAdmin.Api/WeChat/Dtos/WeChatDtos.cs
@@ -50,8 +50,7 @@ public class WeChatProfileInput
public class WeChatCustomerRegisterInput
{
- [Required]
- public string CustomerCode { get; set; } = string.Empty;
+ public string? CustomerCode { get; set; }
[Required]
public string CustomerName { get; set; } = string.Empty;
@@ -59,7 +58,9 @@ public class WeChatCustomerRegisterInput
[Required]
public string Phone { get; set; } = string.Empty;
- public string? Email { get; set; }
+ [Required]
+ [EmailAddress]
+ public string Email { get; set; } = string.Empty;
[Required]
[MinLength(6)]
diff --git a/src/SimpleAdmin.Api/WeChat/WeChatCustomerAppService.cs b/src/SimpleAdmin.Api/WeChat/WeChatCustomerAppService.cs
index 7d30448..de993f9 100644
--- a/src/SimpleAdmin.Api/WeChat/WeChatCustomerAppService.cs
+++ b/src/SimpleAdmin.Api/WeChat/WeChatCustomerAppService.cs
@@ -80,10 +80,9 @@ public class WeChatCustomerAppService(
{
ValidateWechatProfile(input.Wechat);
- var customerCode = NormalizeRequired(input.CustomerCode, "客户编号");
var customerName = NormalizeRequired(input.CustomerName, "客户名称");
var phone = NormalizeRequired(input.Phone, "手机号");
- var email = NormalizeOptional(input.Email);
+ var email = NormalizeRequired(input.Email, "邮箱");
var password = NormalizeRequired(input.Password, "密码");
if (password.Length < 6)
@@ -96,6 +95,7 @@ public class WeChatCustomerAppService(
throw new BusinessException("当前微信已绑定账号");
}
+ var customerCode = await CustomerCodeGenerator.GenerateNextAsync(customerRepository, cancellationToken);
await EnsureCustomerIdentityUniqueAsync(customerCode, phone, email, cancellationToken);
var customerRoleId = await GetCustomerRoleIdOrThrowAsync(cancellationToken);
@@ -193,7 +193,7 @@ public class WeChatCustomerAppService(
private async Task EnsureCustomerIdentityUniqueAsync(
string customerCode,
string phone,
- string? email,
+ string email,
CancellationToken cancellationToken)
{
if (await customerRepository.GetByCustomerCodeAsync(customerCode, cancellationToken) != null)
@@ -206,7 +206,7 @@ public class WeChatCustomerAppService(
throw new BusinessException("手机号已存在");
}
- if (email != null && await customerRepository.GetByEmailAsync(email, cancellationToken) != null)
+ if (await customerRepository.GetByEmailAsync(email, cancellationToken) != null)
{
throw new BusinessException("邮箱已存在");
}