diff --git a/src/Oms.Application/Shared/OmsDocHelper.cs b/src/Oms.Application/Shared/OmsDocHelper.cs
index c36b6e9..2641b37 100644
--- a/src/Oms.Application/Shared/OmsDocHelper.cs
+++ b/src/Oms.Application/Shared/OmsDocHelper.cs
@@ -1,6 +1,8 @@
+using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
+using Oms.Application.Dtos.Channels;
using Oms.Application.Dtos.Orders;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.SystemAggregate.Consts;
@@ -155,8 +157,9 @@ public static class OmsDocHelper
///
public static string CanonicalizeSalesProductPriceSnapshotJson(object? snapshot)
{
- var raw = SerializeSnapshotJson(snapshot) ?? string.Empty;
- return SerializeSnapshotJson(DeserializePriceSnapshot(raw)) ?? string.Empty;
+ var dto = DeserializePriceSnapshot(SerializeSnapshotJson(snapshot));
+ StabilizePriceSnapshotDynamicJson(dto);
+ return SerializeSnapshotJson(dto) ?? string.Empty;
}
///
@@ -173,17 +176,19 @@ public static class OmsDocHelper
///
public static string CanonicalizeChannelPriceSnapshotJson(object? snapshot)
{
- var raw = SerializeSnapshotJson(snapshot) ?? string.Empty;
- return SerializeSnapshotJson(DeserializeChannelPriceSnapshot(raw)) ?? string.Empty;
+ var dto = DeserializeChannelPriceSnapshot(SerializeSnapshotJson(snapshot));
+ StabilizeChannelPriceSnapshotDynamicJson(dto);
+ return SerializeSnapshotJson(dto) ?? string.Empty;
}
///
- /// 发布同步态哈希:canonical + 去掉 PackVersion/AreaName 等钉包展示字段。
+ /// 发布同步态哈希:canonical + 去掉 PackVersion/AreaName,并稳定动态 JSON 数字形态(133 vs 133.0、0 vs 0.0000)。
///
public static string HashSalesProductPriceSnapshotForSync(object? snapshot)
{
var dto = DeserializePriceSnapshot(SerializeSnapshotJson(snapshot));
PublishSnapshotNormalizer.StripVolatilePackPinsForSyncCompare(dto);
+ StabilizePriceSnapshotDynamicJson(dto);
return SerializeSnapshotJson(dto) ?? string.Empty;
}
@@ -198,9 +203,99 @@ public static class OmsDocHelper
{
var dto = DeserializeChannelPriceSnapshot(SerializeSnapshotJson(snapshot));
PublishSnapshotNormalizer.StripVolatilePackPinsForSyncCompare(dto);
+ StabilizeChannelPriceSnapshotDynamicJson(dto);
return SerializeSnapshotJson(dto) ?? string.Empty;
}
+ ///
+ /// 把 FactorValuesJson / CustomerGroupValues / CalculatedValue 收到同一数字口径,避免 sync-status 误报待发布。
+ ///
+ private static void StabilizePriceSnapshotDynamicJson(SalesProductPricePublishSnapshotDto? snapshot)
+ {
+ if (snapshot?.Prices == null)
+ {
+ return;
+ }
+
+ foreach (var price in snapshot.Prices)
+ {
+ StabilizePriceDetails(price.Details);
+ }
+ }
+
+ private static void StabilizeChannelPriceSnapshotDynamicJson(ServiceChannelPricePublishSnapshotDto? snapshot)
+ {
+ if (snapshot?.Prices == null)
+ {
+ return;
+ }
+
+ foreach (var price in snapshot.Prices)
+ {
+ StabilizePriceDetails(price.Details);
+ }
+ }
+
+ private static void StabilizePriceDetails(List? details)
+ {
+ if (details == null)
+ {
+ return;
+ }
+
+ foreach (var detail in details)
+ {
+ detail.FactorValuesJson = StabilizeDynamicJsonValue(detail.FactorValuesJson);
+ detail.CustomerGroupValues = StabilizeDynamicJsonValue(detail.CustomerGroupValues);
+ if (detail.CalculatedValue is { } calculated)
+ {
+ detail.CalculatedValue = StabilizeDecimal(calculated);
+ }
+ }
+ }
+
+ ///
+ /// JsonElement / Dictionary 等动态 JSON 统一走 ConvertJsonElement,整数形态的小数收成 long。
+ ///
+ private static object? StabilizeDynamicJsonValue(object? value)
+ {
+ if (value is null)
+ {
+ return null;
+ }
+
+ string? json = value switch
+ {
+ JsonElement element => element.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined
+ ? null
+ : element.GetRawText(),
+ JsonNode node => node.ToJsonString(),
+ string text => string.IsNullOrWhiteSpace(text) ? null : text,
+ _ => SerializeJson(value)
+ };
+ if (string.IsNullOrWhiteSpace(json))
+ {
+ return null;
+ }
+
+ using var document = JsonDocument.Parse(json);
+ return ConvertJsonElement(document.RootElement);
+ }
+
+ private static decimal StabilizeDecimal(decimal value)
+ {
+ // 去掉尾随 0:0.0000 → 0,133.0 → 133(再由序列化写成整型 JSON)
+ var normalized = decimal.Parse(value.ToString("G29"), CultureInfo.InvariantCulture);
+ if (normalized == decimal.Truncate(normalized)
+ && normalized >= long.MinValue
+ && normalized <= long.MaxValue)
+ {
+ return normalized;
+ }
+
+ return normalized;
+ }
+
///
/// 解析渠道买价发布 Content,供列表/对比 API 使用(兼容旧快照审计字段)。
///
@@ -227,15 +322,34 @@ public static class OmsDocHelper
.Select(ConvertJsonElement)
.ToList(),
JsonValueKind.String => element.GetString(),
- JsonValueKind.Number => element.TryGetInt64(out var integer)
- ? integer
- : element.GetDecimal(),
+ JsonValueKind.Number => ConvertJsonNumber(element),
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Null or JsonValueKind.Undefined => null,
_ => element.GetRawText()
};
+ ///
+ /// 数字稳定化:能无损落成整型的一律 long,避免 133 / 133.0 导致发布 Content 与 LIVE 哈希不一致。
+ ///
+ private static object ConvertJsonNumber(JsonElement element)
+ {
+ if (element.TryGetInt64(out var integer))
+ {
+ return integer;
+ }
+
+ var decimalValue = StabilizeDecimal(element.GetDecimal());
+ if (decimalValue == decimal.Truncate(decimalValue)
+ && decimalValue >= long.MinValue
+ && decimalValue <= long.MaxValue)
+ {
+ return (long)decimalValue;
+ }
+
+ return decimalValue;
+ }
+
///
/// 反序列化 JSON 字符串数组。
///
diff --git a/src/Oms.Tests/OmsDocHelperTests.cs b/src/Oms.Tests/OmsDocHelperTests.cs
index 7ad1d41..b86fae8 100644
--- a/src/Oms.Tests/OmsDocHelperTests.cs
+++ b/src/Oms.Tests/OmsDocHelperTests.cs
@@ -107,4 +107,107 @@ public class OmsDocHelperTests
typeName.Should().Be("中转仓");
}
+
+ [Fact]
+ public void HashSalesProductPriceSnapshotForSync_ShouldIgnoreNumberFormatNoise()
+ {
+ // LIVE:DeserializeJsonObject 常把价收到 long;历史 Content 里常见 133.0 / 0.0000
+ var live = """
+ {
+ "salesProductId": 8,
+ "priceAreaId": 3,
+ "priceAreaName": "美国分区",
+ "priceAreaRefs": [
+ {
+ "areaId": 3,
+ "areaName": "美国分区",
+ "packVersion": "20261007164410403",
+ "contentHash": "e14ccf7436f001743ba9d45e0ec95f72285abd70b161fe91591766b909cefc80"
+ }
+ ],
+ "prices": [
+ {
+ "id": 13,
+ "serialNumber": 1,
+ "status": 2,
+ "priceName": "运费",
+ "costItem": "freight",
+ "costUnit": "kg",
+ "currency": "CNY",
+ "chargingCondition": "无",
+ "factors": [],
+ "details": [
+ {
+ "id": 1511,
+ "priceId": 13,
+ "factorValuesJson": { "仓库": "2", "重量": "(0-0.5]" },
+ "calculationFormula": "total",
+ "calculatedValue": 0,
+ "customerGroupValues": { "全部客户": 133 }
+ }
+ ]
+ }
+ ]
+ }
+ """;
+ var online = """
+ {
+ "salesProductId": 8,
+ "priceAreaId": 3,
+ "priceAreaName": "美国分区",
+ "priceAreaRefs": [
+ {
+ "areaId": 3,
+ "areaName": "美国分区",
+ "packVersion": "20261007164410403",
+ "contentHash": "e14ccf7436f001743ba9d45e0ec95f72285abd70b161fe91591766b909cefc80"
+ }
+ ],
+ "prices": [
+ {
+ "id": 13,
+ "serialNumber": 1,
+ "status": 2,
+ "priceName": "运费",
+ "costItem": "freight",
+ "costUnit": "kg",
+ "currency": "CNY",
+ "chargingCondition": "无",
+ "factors": [],
+ "details": [
+ {
+ "id": 1511,
+ "priceId": 13,
+ "factorValuesJson": { "仓库": "2", "重量": "(0-0.5]" },
+ "calculationFormula": "total",
+ "calculatedValue": 0.0000,
+ "customerGroupValues": { "全部客户": 133.0 }
+ }
+ ]
+ }
+ ]
+ }
+ """;
+
+ var liveHash = OmsDocHelper.HashSalesProductPriceSnapshotForSync(
+ OmsDocHelper.DeserializePriceSnapshot(live));
+ var onlineHash = OmsDocHelper.HashSalesProductPriceSnapshotForSync(
+ OmsDocHelper.DeserializePriceSnapshot(online));
+
+ liveHash.Should().Be(onlineHash);
+ liveHash.Should().Contain(":133}");
+ liveHash.Should().Contain("\"calculatedValue\":0");
+ liveHash.Should().NotContain("133.0");
+ liveHash.Should().NotContain("0.0000");
+ }
+
+ [Fact]
+ public void DeserializeJsonObject_ShouldCoerceIntegerValuedDecimalsToLong()
+ {
+ var result = OmsDocHelper.DeserializeJsonObject("""{"全部客户":133.0,"VIP":12.5}""");
+
+ var map = result.Should().BeOfType>().Subject;
+ map["全部客户"].Should().Be(133L);
+ map["VIP"].Should().Be(12.5m);
+ }
}