diff --git a/db.sql b/db.sql
index 198f297..c8867a0 100644
--- a/db.sql
+++ b/db.sql
@@ -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)',
diff --git a/sql/migrate_c_sku_product_ids_20261023.sql b/sql/migrate_c_sku_product_ids_20261023.sql
new file mode 100644
index 0000000..ff50289
--- /dev/null
+++ b/sql/migrate_c_sku_product_ids_20261023.sql
@@ -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;
diff --git a/src/Oms.Application/Dtos/Orders/OrderParcelItemInput.cs b/src/Oms.Application/Dtos/Orders/OrderParcelItemInput.cs
index ed71218..6ce63c7 100644
--- a/src/Oms.Application/Dtos/Orders/OrderParcelItemInput.cs
+++ b/src/Oms.Application/Dtos/Orders/OrderParcelItemInput.cs
@@ -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; }
///
- /// 产品标识编码(选填)。
+ /// 产品标识编码(选填)。预报下游始终序列化,无值时为 null。
///
[StringLength(100)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public string? ProductId { get; set; }
///
- /// 非标产品标识编码(选填)。
+ /// 非标产品标识编码(选填)。预报下游始终序列化,无值时为 null。
///
[StringLength(100)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public string? NonStdProductId { get; set; }
///
- /// 标准产品标识编码(选填)。
+ /// 标准产品标识编码(选填)。预报下游始终序列化,无值时为 null。
///
[StringLength(100)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public string? StdProductId { get; set; }
///
diff --git a/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs b/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs
index 47b6cff..630cb9a 100644
--- a/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs
+++ b/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs
@@ -47,6 +47,21 @@ public class WarehouseSkuOutput
///
public string? DeclareElement { get; set; }
+ ///
+ /// 产品标识编码。
+ ///
+ public string? ProductId { get; set; }
+
+ ///
+ /// 非标产品标识编码。
+ ///
+ public string? NonStdProductId { get; set; }
+
+ ///
+ /// 标准产品标识编码(如 GTIN / UPC / EAN)。
+ ///
+ public string? StdProductId { get; set; }
+
public decimal? WeightG { get; set; }
public decimal? LengthCm { get; set; }
@@ -91,6 +106,21 @@ public class WarehouseSkuSaveInput
///
public string? DeclareElement { get; set; }
+ ///
+ /// 产品标识编码。
+ ///
+ public string? ProductId { get; set; }
+
+ ///
+ /// 非标产品标识编码。
+ ///
+ public string? NonStdProductId { get; set; }
+
+ ///
+ /// 标准产品标识编码(如 GTIN / UPC / EAN)。
+ ///
+ public string? StdProductId { get; set; }
+
public decimal? WeightG { get; set; }
public decimal? LengthCm { get; set; }
diff --git a/src/Oms.Application/Labeling/SkuLabelPrintDataBuilder.cs b/src/Oms.Application/Labeling/SkuLabelPrintDataBuilder.cs
index 30b459c..4086a85 100644
--- a/src/Oms.Application/Labeling/SkuLabelPrintDataBuilder.cs
+++ b/src/Oms.Application/Labeling/SkuLabelPrintDataBuilder.cs
@@ -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,
diff --git a/src/Oms.Application/Services/Orders/PlatformStoreOrderParcelBuilder.cs b/src/Oms.Application/Services/Orders/PlatformStoreOrderParcelBuilder.cs
index 1e460e5..6530da0 100644
--- a/src/Oms.Application/Services/Orders/PlatformStoreOrderParcelBuilder.cs
+++ b/src/Oms.Application/Services/Orders/PlatformStoreOrderParcelBuilder.cs
@@ -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,
});
}
diff --git a/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs b/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs
index 991a6d0..599c21b 100644
--- a/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs
+++ b/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs
@@ -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,
diff --git a/src/Oms.Application/Shared/OrderDownstreamCreateOneBodyBuilder.cs b/src/Oms.Application/Shared/OrderDownstreamCreateOneBodyBuilder.cs
index 501a2a9..89f17ac 100644
--- a/src/Oms.Application/Shared/OrderDownstreamCreateOneBodyBuilder.cs
+++ b/src/Oms.Application/Shared/OrderDownstreamCreateOneBodyBuilder.cs
@@ -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();
}
diff --git a/src/Oms.Domain/WarehouseAggregate/Entities/CSku.cs b/src/Oms.Domain/WarehouseAggregate/Entities/CSku.cs
index 7c7559b..1124b02 100644
--- a/src/Oms.Domain/WarehouseAggregate/Entities/CSku.cs
+++ b/src/Oms.Domain/WarehouseAggregate/Entities/CSku.cs
@@ -27,6 +27,21 @@ public class CSku : OmsAuditedAggregateRoot
///
public string? DeclareElement { get; set; }
+ ///
+ /// 产品标识编码。
+ ///
+ public string? ProductId { get; set; }
+
+ ///
+ /// 非标产品标识编码。
+ ///
+ public string? NonStdProductId { get; set; }
+
+ ///
+ /// 标准产品标识编码(如 GTIN / UPC / EAN)。
+ ///
+ public string? StdProductId { get; set; }
+
public decimal? WeightG { get; set; }
public decimal? LengthCm { get; set; }
diff --git a/src/Oms.Tests/OrderDownstreamCreateOneBodyBuilderTests.cs b/src/Oms.Tests/OrderDownstreamCreateOneBodyBuilderTests.cs
new file mode 100644
index 0000000..6593b18
--- /dev/null
+++ b/src/Oms.Tests/OrderDownstreamCreateOneBodyBuilderTests.cs
@@ -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
+ {
+ 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(MockBehavior.Loose).Object,
+ new Mock(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\"");
+ }
+}