feat: SKU/预报下游申报明细带产品标识编码三字段
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s

库内 SKU 与 createOrderOne items 对齐 productId/nonStdProductId/stdProductId;无值时仍序列化为 null。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-10-23 11:19:12 +08:00
parent e48f8ce926
commit ca95e19186
10 changed files with 211 additions and 8 deletions

3
db.sql
View File

@ -1413,6 +1413,9 @@ CREATE TABLE `c_sku` (
`ExHsCode` VARCHAR(64) NULL COMMENT '出口HSCode',
`ImHsCode` VARCHAR(64) NULL COMMENT '进口HSCode',
`DeclareElement` VARCHAR(255) NULL COMMENT '出口申报要素',
`ProductId` VARCHAR(100) NULL COMMENT '产品标识编码',
`NonStdProductId` VARCHAR(100) NULL COMMENT '非标产品标识编码',
`StdProductId` VARCHAR(100) NULL COMMENT '标准产品标识编码',
`WeightG` DECIMAL(10,2) NULL COMMENT '重量(g)',
`LengthCm` DECIMAL(10,2) NULL COMMENT '长(cm)',
`WidthCm` DECIMAL(10,2) NULL COMMENT '宽(cm)',

View File

@ -0,0 +1,55 @@
-- c_sku:新增产品标识编码 / 非标产品标识编码 / 标准产品标识编码(对齐订单申报明细)。
-- 可重复执行。
SET @db := DATABASE();
-- c_sku.ProductId
SET @exists := (
SELECT COUNT(1)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 'c_sku'
AND COLUMN_NAME = 'ProductId'
);
SET @sql := IF(
@exists = 0,
'ALTER TABLE `c_sku` ADD COLUMN `ProductId` VARCHAR(100) NULL COMMENT ''产品标识编码'' AFTER `DeclareElement`',
'SELECT ''c_sku.ProductId already exists'' AS info'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- c_sku.NonStdProductId
SET @exists := (
SELECT COUNT(1)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 'c_sku'
AND COLUMN_NAME = 'NonStdProductId'
);
SET @sql := IF(
@exists = 0,
'ALTER TABLE `c_sku` ADD COLUMN `NonStdProductId` VARCHAR(100) NULL COMMENT ''非标产品标识编码'' AFTER `ProductId`',
'SELECT ''c_sku.NonStdProductId already exists'' AS info'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- c_sku.StdProductId
SET @exists := (
SELECT COUNT(1)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 'c_sku'
AND COLUMN_NAME = 'StdProductId'
);
SET @sql := IF(
@exists = 0,
'ALTER TABLE `c_sku` ADD COLUMN `StdProductId` VARCHAR(100) NULL COMMENT ''标准产品标识编码'' AFTER `NonStdProductId`',
'SELECT ''c_sku.StdProductId already exists'' AS info'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace Oms.Application.Dtos.Orders;
@ -68,21 +69,24 @@ public class OrderParcelItemInput
public string? DeclareElement { get; set; }
/// <summary>
/// 产品标识编码(选填)。
/// 产品标识编码(选填)。预报下游始终序列化,无值时为 null。
/// </summary>
[StringLength(100)]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public string? ProductId { get; set; }
/// <summary>
/// 非标产品标识编码(选填)。
/// 非标产品标识编码(选填)。预报下游始终序列化,无值时为 null。
/// </summary>
[StringLength(100)]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public string? NonStdProductId { get; set; }
/// <summary>
/// 标准产品标识编码(选填)。
/// 标准产品标识编码(选填)。预报下游始终序列化,无值时为 null。
/// </summary>
[StringLength(100)]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public string? StdProductId { get; set; }
/// <summary>

View File

@ -47,6 +47,21 @@ public class WarehouseSkuOutput
/// </summary>
public string? DeclareElement { get; set; }
/// <summary>
/// 产品标识编码。
/// </summary>
public string? ProductId { get; set; }
/// <summary>
/// 非标产品标识编码。
/// </summary>
public string? NonStdProductId { get; set; }
/// <summary>
/// 标准产品标识编码(如 GTIN / UPC / EAN)。
/// </summary>
public string? StdProductId { get; set; }
public decimal? WeightG { get; set; }
public decimal? LengthCm { get; set; }
@ -91,6 +106,21 @@ public class WarehouseSkuSaveInput
/// </summary>
public string? DeclareElement { get; set; }
/// <summary>
/// 产品标识编码。
/// </summary>
public string? ProductId { get; set; }
/// <summary>
/// 非标产品标识编码。
/// </summary>
public string? NonStdProductId { get; set; }
/// <summary>
/// 标准产品标识编码(如 GTIN / UPC / EAN)。
/// </summary>
public string? StdProductId { get; set; }
public decimal? WeightG { get; set; }
public decimal? LengthCm { get; set; }

View File

@ -29,6 +29,9 @@ public static class SkuLabelPrintDataBuilder
["exHsCode"] = sku.ExHsCode,
["imHsCode"] = sku.ImHsCode,
["declareElement"] = sku.DeclareElement,
["productId"] = sku.ProductId,
["nonStdProductId"] = sku.NonStdProductId,
["stdProductId"] = sku.StdProductId,
["weightG"] = sku.WeightG,
["lengthCm"] = sku.LengthCm,
["widthCm"] = sku.WidthCm,
@ -61,6 +64,9 @@ public static class SkuLabelPrintDataBuilder
["exHsCode"] = "610910",
["imHsCode"] = "610910",
["declareElement"] = "棉质 T 恤",
["productId"] = null,
["nonStdProductId"] = null,
["stdProductId"] = "06901234567890",
["weightG"] = 200m,
["lengthCm"] = 30m,
["widthCm"] = 20m,

View File

@ -267,7 +267,9 @@ public static class PlatformStoreOrderParcelBuilder
var unitWeightKg = sku.WeightG is > 0 ? Math.Max(sku.WeightG.Value / 1000m, MinActualWeightKg) : MinActualWeightKg;
var unitPrice = line.UnitPrice is > 0 ? line.UnitPrice.Value : 1m;
var cnName = string.IsNullOrWhiteSpace(line.ProductName) ? sku.SkuName : line.ProductName.Trim();
var enName = string.IsNullOrWhiteSpace(sku.SkuName) ? cnName : sku.SkuName;
var enName = string.IsNullOrWhiteSpace(sku.SkuNameEn)
? (string.IsNullOrWhiteSpace(sku.SkuName) ? cnName : sku.SkuName)
: sku.SkuNameEn.Trim();
items.Add(new OrderParcelItemInput
{
@ -279,7 +281,13 @@ public static class PlatformStoreOrderParcelBuilder
UnitWeight = unitWeightKg,
Quantity = line.Quantity,
Amount = unitPrice * line.Quantity,
Weight = unitWeightKg * line.Quantity
Weight = unitWeightKg * line.Quantity,
ExHsCode = sku.ExHsCode,
ImHsCode = sku.ImHsCode,
DeclareElement = sku.DeclareElement,
ProductId = sku.ProductId,
NonStdProductId = sku.NonStdProductId,
StdProductId = sku.StdProductId,
});
}

View File

@ -139,6 +139,11 @@ public partial class WarehouseService
entity.ExHsCode = string.IsNullOrWhiteSpace(input.ExHsCode) ? null : input.ExHsCode.Trim();
entity.ImHsCode = string.IsNullOrWhiteSpace(input.ImHsCode) ? null : input.ImHsCode.Trim();
entity.DeclareElement = string.IsNullOrWhiteSpace(input.DeclareElement) ? null : input.DeclareElement.Trim();
entity.ProductId = string.IsNullOrWhiteSpace(input.ProductId) ? null : input.ProductId.Trim();
entity.NonStdProductId = string.IsNullOrWhiteSpace(input.NonStdProductId)
? null
: input.NonStdProductId.Trim();
entity.StdProductId = string.IsNullOrWhiteSpace(input.StdProductId) ? null : input.StdProductId.Trim();
entity.WeightG = input.WeightG;
entity.LengthCm = input.LengthCm;
entity.WidthCm = input.WidthCm;
@ -309,6 +314,9 @@ public partial class WarehouseService
ExHsCode = entity.ExHsCode,
ImHsCode = entity.ImHsCode,
DeclareElement = entity.DeclareElement,
ProductId = entity.ProductId,
NonStdProductId = entity.NonStdProductId,
StdProductId = entity.StdProductId,
WeightG = entity.WeightG,
LengthCm = entity.LengthCm,
WidthCm = entity.WidthCm,
@ -331,6 +339,9 @@ public partial class WarehouseService
ExHsCode = entity.ExHsCode,
ImHsCode = entity.ImHsCode,
DeclareElement = entity.DeclareElement,
ProductId = entity.ProductId,
NonStdProductId = entity.NonStdProductId,
StdProductId = entity.StdProductId,
WeightG = entity.WeightG,
LengthCm = entity.LengthCm,
WidthCm = entity.WidthCm,

View File

@ -201,9 +201,10 @@ public static class OrderDownstreamCreateOneBodyBuilder
ImHsCode = x.ImHsCode,
ExHsCode = x.ExHsCode ?? x.HsCode,
DeclareElement = x.DeclareElement,
ProductId = x.ProductId,
NonStdProductId = x.NonStdProductId,
StdProductId = x.StdProductId,
// 始终赋值;空白→null。配合 OrderParcelItemInput 上 JsonIgnore(Never),请求体固定出现 "xxx": null。
ProductId = NullIfWhiteSpace(x.ProductId),
NonStdProductId = NullIfWhiteSpace(x.NonStdProductId),
StdProductId = NullIfWhiteSpace(x.StdProductId),
SkuImage = fileStorage?.GetPublicUrl(x.SkuImage) ?? x.SkuImage,
Category = x.Category,
Brand = x.Brand,
@ -220,4 +221,7 @@ public static class OrderDownstreamCreateOneBodyBuilder
Specifications = x.Specifications,
Comment = x.Comment
};
private static string? NullIfWhiteSpace(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}

View File

@ -27,6 +27,21 @@ public class CSku : OmsAuditedAggregateRoot
/// </summary>
public string? DeclareElement { get; set; }
/// <summary>
/// 产品标识编码。
/// </summary>
public string? ProductId { get; set; }
/// <summary>
/// 非标产品标识编码。
/// </summary>
public string? NonStdProductId { get; set; }
/// <summary>
/// 标准产品标识编码(如 GTIN / UPC / EAN)。
/// </summary>
public string? StdProductId { get; set; }
public decimal? WeightG { get; set; }
public decimal? LengthCm { get; set; }

View File

@ -0,0 +1,67 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using FluentAssertions;
using Moq;
using Oms.Application.Shared;
using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.SystemAggregate.Repositories;
namespace Oms.Tests;
public class OrderDownstreamCreateOneBodyBuilderTests
{
[Fact]
public async Task BuildAsync_ShouldAlwaysSerializeProductIdFieldsIncludingNull()
{
var order = new Order
{
Id = 1,
ClientNo = "C1",
ServiceNo = "S1",
ChannelCode = "CH1",
ChannelForecastCode = "FC1",
};
var declarations = new List<OrderDeclaration>
{
new()
{
OrderId = 1,
CnName = "衬衫",
EnName = "Shirt",
UnitPrice = 1,
Quantity = 1,
ProductId = "P-1",
NonStdProductId = " ",
StdProductId = " 06901234567890 ",
},
};
var body = await OrderDownstreamCreateOneBodyBuilder.BuildAsync(
order,
sender: null,
recipient: null,
declarations,
weights: [],
new Mock<ICountryRepository>(MockBehavior.Loose).Object,
new Mock<IServiceChannelRepository>(MockBehavior.Loose).Object);
body.Items.Should().ContainSingle();
body.Items[0].ProductId.Should().Be("P-1");
body.Items[0].NonStdProductId.Should().BeNull();
body.Items[0].StdProductId.Should().Be("06901234567890");
// 与 ItfxForwardClient 一致:全局 WhenWritingNull,但三字段 JsonIgnore(Never) 仍写出 null
var json = JsonSerializer.Serialize(
body,
new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
});
json.Should().Contain("\"productId\":\"P-1\"");
json.Should().Contain("\"nonStdProductId\":null");
json.Should().Contain("\"stdProductId\":\"06901234567890\"");
}
}