feat: SKU/预报下游申报明细带产品标识编码三字段
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s
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:
parent
e48f8ce926
commit
ca95e19186
3
db.sql
3
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)',
|
||||
|
||||
55
sql/migrate_c_sku_product_ids_20261023.sql
Normal file
55
sql/migrate_c_sku_product_ids_20261023.sql
Normal 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;
|
||||
@ -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>
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
|
||||
67
src/Oms.Tests/OrderDownstreamCreateOneBodyBuilderTests.cs
Normal file
67
src/Oms.Tests/OrderDownstreamCreateOneBodyBuilderTests.cs
Normal 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\"");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user