feat(order): 强化小包下单规则并暴露预报失败原因

客户单号必填且同客户唯一,运单号统一 NE 前缀自动生成;模板导入不再接受跟踪号/运单号;列表与导出返回预报失败原因字段。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-07-09 09:44:12 +08:00
parent 7d5bc2408f
commit d8752959e7
8 changed files with 59 additions and 27 deletions

View File

@ -0,0 +1,11 @@
-- t_order 预报失败原因(可重复执行)
-- 在 OMS 业务库执行:mysql -h<host> -u<user> -p<db> < migrate_t_order_predeclare_fail_reason.sql
SET @db = DATABASE();
SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_order' AND COLUMN_NAME = 'PredeclareFailReason');
SET @sql := IF(@exist = 0,
'ALTER TABLE `t_order` ADD COLUMN `PredeclareFailReason` VARCHAR(512) NULL COMMENT ''预报失败原因'' AFTER `IsRemote`',
'SELECT ''skip PredeclareFailReason''');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;

View File

@ -105,6 +105,11 @@ public class OrderOutput
/// </summary>
public string? ExportCustomsMode { get; set; }
/// <summary>
/// 预报失败原因(草稿订单)。
/// </summary>
public string? PredeclareFailReason { get; set; }
/// <summary>
/// 申报币种。
/// </summary>

View File

@ -121,6 +121,9 @@ public partial class OrderService
CustomerId = order.CustomerId,
RequestId = order.RequestId,
ExportCustomsMode = order.ExportCustomsMode,
PredeclareFailReason = string.IsNullOrWhiteSpace(order.PredeclareFailReason)
? null
: order.PredeclareFailReason.Trim(),
DeclaredCurrency = order.DeclaredCurrency,
DeclaredSum = order.DeclaredSum,
Quantity = order.Quantity,
@ -188,6 +191,7 @@ public partial class OrderService
x.CustomerId,
x.Status,
x.OrderStatus,
x.PredeclareFailReason,
x.ReceiveTime,
x.ShipTime,
x.PayTime,

View File

@ -27,7 +27,19 @@ public partial class OrderService
if (input.ClientNo != null)
{
order.ClientNo = string.IsNullOrWhiteSpace(input.ClientNo) ? null : input.ClientNo.Trim();
var newClientNo = string.IsNullOrWhiteSpace(input.ClientNo) ? null : input.ClientNo.Trim();
if (!string.IsNullOrWhiteSpace(newClientNo)
&& !string.Equals(newClientNo, order.ClientNo, StringComparison.OrdinalIgnoreCase)
&& order.CustomerId.HasValue)
{
var duplicated = await OrderRepository.GetByCustomerAndClientNoAsync(order.CustomerId.Value, newClientNo);
if (duplicated != null && duplicated.Id != order.Id)
{
throw new BusinessException("客户单号已存在");
}
}
order.ClientNo = newClientNo;
}
if (input.TrackNo != null)

View File

@ -78,7 +78,7 @@ public partial class OrderService
throw new BusinessException("收件地址命中黑名单区域");
}
var serviceNo = await ResolveServiceNoAsync(input.ServiceNo);
var serviceNo = await ResolveServiceNoAsync();
var order = BuildDraftOrderEntity(input, product, country.CountryCode, country.ChineseName, snapshotBinding, serviceNo);
order = await OrderRepository.InsertAsync(order);
@ -165,16 +165,36 @@ public partial class OrderService
}
}
private const string ParcelServiceNoPrefix = "NE";
private static void NormalizeParcelCreateInput(OrderParcelCreateInput input)
{
input.TrackNo = string.IsNullOrWhiteSpace(input.TrackNo) ? null : input.TrackNo.Trim();
input.ServiceNo = string.IsNullOrWhiteSpace(input.ServiceNo) ? null : input.ServiceNo.Trim();
input.TrackNo = null;
input.ServiceNo = null;
input.ClientNo = string.IsNullOrWhiteSpace(input.ClientNo) ? null : input.ClientNo.Trim();
input.RequestId = string.IsNullOrWhiteSpace(input.RequestId) ? null : input.RequestId.Trim();
}
private async Task ValidateParcelCreateInputAsync(OrderParcelCreateInput input)
{
if (string.IsNullOrWhiteSpace(input.ClientNo))
{
throw new BusinessException("客户单号不能为空");
}
if (!input.CustomerId.HasValue || input.CustomerId <= 0)
{
throw new BusinessException("客户ID不能为空");
}
var duplicatedClientNo = await OrderRepository.GetByCustomerAndClientNoAsync(
input.CustomerId.Value,
input.ClientNo);
if (duplicatedClientNo != null)
{
throw new BusinessException("客户单号已存在");
}
if (string.IsNullOrWhiteSpace(input.ProductCode))
{
throw new BusinessException("产品代码不能为空");
@ -185,11 +205,6 @@ public partial class OrderService
throw new BusinessException("仓库ID不能为空");
}
if (!input.CustomerId.HasValue || input.CustomerId <= 0)
{
throw new BusinessException("客户ID不能为空");
}
if (!input.Length.HasValue || !input.Width.HasValue || !input.Height.HasValue || !input.ActualWeight.HasValue
|| input.Length <= 0 || input.Width <= 0 || input.Height <= 0 || input.ActualWeight <= 0)
{
@ -1185,22 +1200,11 @@ public partial class OrderService
await OrderRepository.UpdateAsync(order);
}
private async Task<string> ResolveServiceNoAsync(string? serviceNo)
private async Task<string> ResolveServiceNoAsync()
{
var normalized = serviceNo?.Trim();
if (!string.IsNullOrWhiteSpace(normalized))
{
if (await OrderRepository.HasServiceNoAsync(normalized))
{
throw new BusinessException("运单号已存在");
}
return normalized;
}
for (var i = 0; i < 5; i++)
{
var generated = $"LP{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}";
var generated = $"{ParcelServiceNoPrefix}{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}";
if (!await OrderRepository.HasServiceNoAsync(generated))
{
return generated;

View File

@ -70,8 +70,6 @@ public static class OrderTemplateFieldRegistry
Add("requestId", "幂等请求ID", false);
Add("clientNo", "客户单号", true, example: "C202604230001");
Add("trackNo", "跟踪号", false);
Add("serviceNo", "运单号", false);
Add("productCode", "运输方式", true, "select", "sales_product", "basic", "PROD001");
Add("cargoAttribute", "货物属性", true, "select", "cargo_attribute", "basic", "general");
Add("exportCustomsMode", "出口报关方式", false, "select", "export_customs_mode", "basic", "9610Simple");

View File

@ -15,8 +15,6 @@ public static class OrderTemplateHeaderMatcher
["客户单号"] = "clientNo",
["客户订单号"] = "clientNo",
["外部单号"] = "clientNo",
["跟踪号"] = "trackNo",
["运单号"] = "serviceNo",
["运输方式"] = "productCode",
["产品代码"] = "productCode",
["产品编码"] = "productCode",

View File

@ -11,7 +11,7 @@ public static class OrderTemplateRowMapper
{
private static readonly HashSet<string> ForceStringFields = new(StringComparer.OrdinalIgnoreCase)
{
"requestId", "clientNo", "trackNo", "serviceNo", "productCode", "cargoAttribute", "exportCustomsMode",
"requestId", "clientNo", "productCode", "cargoAttribute", "exportCustomsMode",
"codCurrency", "declaredCurrency", "receiver.recipientsName", "receiver.companyName", "receiver.phone",
"receiver.telePhone", "receiver.email", "receiver.countryCode", "receiver.province", "receiver.city",
"receiver.streetNumber", "receiver.streetAddress1", "receiver.streetAddress2", "receiver.streetAddress3",