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