20261008-ide1.0发布 #1
@ -1,6 +1,8 @@
|
|||||||
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Oms.Application.Dtos.Channels;
|
||||||
using Oms.Application.Dtos.Orders;
|
using Oms.Application.Dtos.Orders;
|
||||||
using Oms.Domain.OrdersAggregate.Consts;
|
using Oms.Domain.OrdersAggregate.Consts;
|
||||||
using Oms.Domain.SystemAggregate.Consts;
|
using Oms.Domain.SystemAggregate.Consts;
|
||||||
@ -155,8 +157,9 @@ public static class OmsDocHelper
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static string CanonicalizeSalesProductPriceSnapshotJson(object? snapshot)
|
public static string CanonicalizeSalesProductPriceSnapshotJson(object? snapshot)
|
||||||
{
|
{
|
||||||
var raw = SerializeSnapshotJson(snapshot) ?? string.Empty;
|
var dto = DeserializePriceSnapshot(SerializeSnapshotJson(snapshot));
|
||||||
return SerializeSnapshotJson(DeserializePriceSnapshot(raw)) ?? string.Empty;
|
StabilizePriceSnapshotDynamicJson(dto);
|
||||||
|
return SerializeSnapshotJson(dto) ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -173,17 +176,19 @@ public static class OmsDocHelper
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static string CanonicalizeChannelPriceSnapshotJson(object? snapshot)
|
public static string CanonicalizeChannelPriceSnapshotJson(object? snapshot)
|
||||||
{
|
{
|
||||||
var raw = SerializeSnapshotJson(snapshot) ?? string.Empty;
|
var dto = DeserializeChannelPriceSnapshot(SerializeSnapshotJson(snapshot));
|
||||||
return SerializeSnapshotJson(DeserializeChannelPriceSnapshot(raw)) ?? string.Empty;
|
StabilizeChannelPriceSnapshotDynamicJson(dto);
|
||||||
|
return SerializeSnapshotJson(dto) ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 发布同步态哈希:canonical + 去掉 PackVersion/AreaName 等钉包展示字段。
|
/// 发布同步态哈希:canonical + 去掉 PackVersion/AreaName,并稳定动态 JSON 数字形态(133 vs 133.0、0 vs 0.0000)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string HashSalesProductPriceSnapshotForSync(object? snapshot)
|
public static string HashSalesProductPriceSnapshotForSync(object? snapshot)
|
||||||
{
|
{
|
||||||
var dto = DeserializePriceSnapshot(SerializeSnapshotJson(snapshot));
|
var dto = DeserializePriceSnapshot(SerializeSnapshotJson(snapshot));
|
||||||
PublishSnapshotNormalizer.StripVolatilePackPinsForSyncCompare(dto);
|
PublishSnapshotNormalizer.StripVolatilePackPinsForSyncCompare(dto);
|
||||||
|
StabilizePriceSnapshotDynamicJson(dto);
|
||||||
return SerializeSnapshotJson(dto) ?? string.Empty;
|
return SerializeSnapshotJson(dto) ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,9 +203,99 @@ public static class OmsDocHelper
|
|||||||
{
|
{
|
||||||
var dto = DeserializeChannelPriceSnapshot(SerializeSnapshotJson(snapshot));
|
var dto = DeserializeChannelPriceSnapshot(SerializeSnapshotJson(snapshot));
|
||||||
PublishSnapshotNormalizer.StripVolatilePackPinsForSyncCompare(dto);
|
PublishSnapshotNormalizer.StripVolatilePackPinsForSyncCompare(dto);
|
||||||
|
StabilizeChannelPriceSnapshotDynamicJson(dto);
|
||||||
return SerializeSnapshotJson(dto) ?? string.Empty;
|
return SerializeSnapshotJson(dto) ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 把 FactorValuesJson / CustomerGroupValues / CalculatedValue 收到同一数字口径,避免 sync-status 误报待发布。
|
||||||
|
/// </summary>
|
||||||
|
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<ChannelPriceDetailOutput>? 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JsonElement / Dictionary 等动态 JSON 统一走 ConvertJsonElement,整数形态的小数收成 long。
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 解析渠道买价发布 Content,供列表/对比 API 使用(兼容旧快照审计字段)。
|
/// 解析渠道买价发布 Content,供列表/对比 API 使用(兼容旧快照审计字段)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -227,15 +322,34 @@ public static class OmsDocHelper
|
|||||||
.Select(ConvertJsonElement)
|
.Select(ConvertJsonElement)
|
||||||
.ToList(),
|
.ToList(),
|
||||||
JsonValueKind.String => element.GetString(),
|
JsonValueKind.String => element.GetString(),
|
||||||
JsonValueKind.Number => element.TryGetInt64(out var integer)
|
JsonValueKind.Number => ConvertJsonNumber(element),
|
||||||
? integer
|
|
||||||
: element.GetDecimal(),
|
|
||||||
JsonValueKind.True => true,
|
JsonValueKind.True => true,
|
||||||
JsonValueKind.False => false,
|
JsonValueKind.False => false,
|
||||||
JsonValueKind.Null or JsonValueKind.Undefined => null,
|
JsonValueKind.Null or JsonValueKind.Undefined => null,
|
||||||
_ => element.GetRawText()
|
_ => element.GetRawText()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数字稳定化:能无损落成整型的一律 long,避免 133 / 133.0 导致发布 Content 与 LIVE 哈希不一致。
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 反序列化 JSON 字符串数组。
|
/// 反序列化 JSON 字符串数组。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -107,4 +107,107 @@ public class OmsDocHelperTests
|
|||||||
|
|
||||||
typeName.Should().Be("中转仓");
|
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<Dictionary<string, object?>>().Subject;
|
||||||
|
map["全部客户"].Should().Be(133L);
|
||||||
|
map["VIP"].Should().Be(12.5m);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user