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\""); + } +}