All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s
LIVE 侧 DeserializeJsonObject 常把客户组价收到 long,历史 Content 里常见 133.0/0.0000;JS 结构相等但 SHA256 不等导致误报待发布。 Co-authored-by: Cursor <cursoragent@cursor.com>
396 lines
13 KiB
C#
396 lines
13 KiB
C#
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;
|
||
|
||
namespace Oms.Application.Shared;
|
||
|
||
/// <summary>
|
||
/// OMS 通用文档与展示辅助方法。
|
||
/// </summary>
|
||
public static class OmsDocHelper
|
||
{
|
||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||
{
|
||
PropertyNameCaseInsensitive = true
|
||
};
|
||
|
||
private static readonly JsonSerializerOptions SnapshotJsonOptions = new()
|
||
{
|
||
PropertyNameCaseInsensitive = true,
|
||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||
};
|
||
|
||
/// <summary>
|
||
/// 将页码规范为至少 1。
|
||
/// </summary>
|
||
public static int NormalizePage(int page) => page <= 0 ? 1 : page;
|
||
|
||
/// <summary>
|
||
/// 将每页条数规范为 1~200。
|
||
/// </summary>
|
||
public static int NormalizePageSize(int pageSize) => pageSize <= 0 ? 20 : Math.Min(pageSize, 200);
|
||
|
||
/// <summary>
|
||
/// 将默认 DateTime 转为 null。
|
||
/// </summary>
|
||
public static DateTime? NullIfDefault(DateTime value) => value == default ? null : value;
|
||
|
||
/// <summary>
|
||
/// 将库内 JSON 字符串转为 API 可输出的 CLR 对象(避免 JsonElement 被 Newtonsoft 序列化成 valueKind)。
|
||
/// </summary>
|
||
public static object? DeserializeJsonObject(string? json)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(json))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
using var document = JsonDocument.Parse(json);
|
||
return ConvertJsonElement(document.RootElement);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 反序列化 JSON 字符串为指定类型。
|
||
/// </summary>
|
||
public static T? DeserializeJson<T>(string? json)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(json))
|
||
{
|
||
return default;
|
||
}
|
||
|
||
return JsonSerializer.Deserialize<T>(json, JsonOptions);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将请求体中的动态 JSON(JsonElement / JToken / Dictionary 等)规范为可落库的 JSON 字符串。
|
||
/// </summary>
|
||
public static string? SerializeJson<T>(T? value)
|
||
{
|
||
if (value is null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
if (value is JsonElement element)
|
||
{
|
||
return element.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined
|
||
? null
|
||
: element.GetRawText();
|
||
}
|
||
|
||
if (value is JsonNode node)
|
||
{
|
||
return node.ToJsonString();
|
||
}
|
||
|
||
if (value is string text)
|
||
{
|
||
return string.IsNullOrWhiteSpace(text) ? null : text;
|
||
}
|
||
|
||
var typeName = value.GetType().FullName;
|
||
if (typeName is not null && typeName.StartsWith("Newtonsoft.Json.Linq.", StringComparison.Ordinal))
|
||
{
|
||
return value.ToString();
|
||
}
|
||
|
||
return JsonSerializer.Serialize(value, JsonOptions);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 序列化发布快照(忽略 null,避免写出旧兼容字段)。
|
||
/// </summary>
|
||
public static string? SerializeSnapshotJson<T>(T? value)
|
||
{
|
||
if (value is null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
return JsonSerializer.Serialize(value, SnapshotJsonOptions);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将快照对象转为 API 输出结构(与落库 JSON 同构,避免预览/列表/落库序列化不一致)。
|
||
/// </summary>
|
||
public static object? ToSnapshotApiContent<T>(T? value)
|
||
{
|
||
if (value is null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var json = SerializeSnapshotJson(value);
|
||
return string.IsNullOrWhiteSpace(json) ? null : DeserializeJsonObject(json);
|
||
}
|
||
|
||
public static SalesProductRulesPublishSnapshotDto? DeserializeRulesSnapshot(string? json)
|
||
{
|
||
var snapshot = DeserializeJson<SalesProductRulesPublishSnapshotDto>(json);
|
||
PublishSnapshotNormalizer.NormalizeRulesSnapshot(snapshot);
|
||
return snapshot;
|
||
}
|
||
|
||
public static SalesProductPricePublishSnapshotDto? DeserializePriceSnapshot(string? json)
|
||
{
|
||
var snapshot = DeserializeJson<SalesProductPricePublishSnapshotDto>(json);
|
||
PublishSnapshotNormalizer.NormalizePriceSnapshot(snapshot);
|
||
return snapshot;
|
||
}
|
||
|
||
public static ServiceChannelPricePublishSnapshotDto? DeserializeChannelPriceSnapshot(string? json)
|
||
{
|
||
var snapshot = DeserializeJson<ServiceChannelPricePublishSnapshotDto>(json);
|
||
PublishSnapshotNormalizer.NormalizeChannelPriceSnapshot(snapshot);
|
||
return snapshot;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 同步态对比用:先序列化再反序列化规范化,消除 Dictionary/JsonElement、字段顺序等噪声。
|
||
/// </summary>
|
||
public static string CanonicalizeSalesProductPriceSnapshotJson(object? snapshot)
|
||
{
|
||
var dto = DeserializePriceSnapshot(SerializeSnapshotJson(snapshot));
|
||
StabilizePriceSnapshotDynamicJson(dto);
|
||
return SerializeSnapshotJson(dto) ?? string.Empty;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 同步态对比用:规则快照规范化 canonical JSON。
|
||
/// </summary>
|
||
public static string CanonicalizeSalesProductRulesSnapshotJson(object? snapshot)
|
||
{
|
||
var raw = SerializeSnapshotJson(snapshot) ?? string.Empty;
|
||
return SerializeSnapshotJson(DeserializeRulesSnapshot(raw)) ?? string.Empty;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 同步态对比用:渠道买价快照规范化 canonical JSON。
|
||
/// </summary>
|
||
public static string CanonicalizeChannelPriceSnapshotJson(object? snapshot)
|
||
{
|
||
var dto = DeserializeChannelPriceSnapshot(SerializeSnapshotJson(snapshot));
|
||
StabilizeChannelPriceSnapshotDynamicJson(dto);
|
||
return SerializeSnapshotJson(dto) ?? string.Empty;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 发布同步态哈希:canonical + 去掉 PackVersion/AreaName,并稳定动态 JSON 数字形态(133 vs 133.0、0 vs 0.0000)。
|
||
/// </summary>
|
||
public static string HashSalesProductPriceSnapshotForSync(object? snapshot)
|
||
{
|
||
var dto = DeserializePriceSnapshot(SerializeSnapshotJson(snapshot));
|
||
PublishSnapshotNormalizer.StripVolatilePackPinsForSyncCompare(dto);
|
||
StabilizePriceSnapshotDynamicJson(dto);
|
||
return SerializeSnapshotJson(dto) ?? string.Empty;
|
||
}
|
||
|
||
public static string HashSalesProductRulesSnapshotForSync(object? snapshot)
|
||
{
|
||
var dto = DeserializeRulesSnapshot(SerializeSnapshotJson(snapshot));
|
||
PublishSnapshotNormalizer.StripVolatilePackPinsForSyncCompare(dto);
|
||
return SerializeSnapshotJson(dto) ?? string.Empty;
|
||
}
|
||
|
||
public static string HashChannelPriceSnapshotForSync(object? snapshot)
|
||
{
|
||
var dto = DeserializeChannelPriceSnapshot(SerializeSnapshotJson(snapshot));
|
||
PublishSnapshotNormalizer.StripVolatilePackPinsForSyncCompare(dto);
|
||
StabilizeChannelPriceSnapshotDynamicJson(dto);
|
||
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>
|
||
/// 解析渠道买价发布 Content,供列表/对比 API 使用(兼容旧快照审计字段)。
|
||
/// </summary>
|
||
public static object? ParseChannelPricePublishContentForApi(string? json)
|
||
{
|
||
var snapshot = DeserializeChannelPriceSnapshot(json);
|
||
return snapshot is null ? DeserializeJsonObject(json) : ToSnapshotApiContent(snapshot);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析产品销售卖价发布 Content,供列表/对比 API 使用。
|
||
/// </summary>
|
||
public static object? ParseSalesProductPricePublishContentForApi(string? json)
|
||
{
|
||
var snapshot = DeserializePriceSnapshot(json);
|
||
return snapshot is null ? DeserializeJsonObject(json) : ToSnapshotApiContent(snapshot);
|
||
}
|
||
|
||
private static object? ConvertJsonElement(JsonElement element) => element.ValueKind switch
|
||
{
|
||
JsonValueKind.Object => element.EnumerateObject()
|
||
.ToDictionary(property => property.Name, property => ConvertJsonElement(property.Value)),
|
||
JsonValueKind.Array => element.EnumerateArray()
|
||
.Select(ConvertJsonElement)
|
||
.ToList(),
|
||
JsonValueKind.String => element.GetString(),
|
||
JsonValueKind.Number => ConvertJsonNumber(element),
|
||
JsonValueKind.True => true,
|
||
JsonValueKind.False => false,
|
||
JsonValueKind.Null or JsonValueKind.Undefined => null,
|
||
_ => 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>
|
||
/// 反序列化 JSON 字符串数组。
|
||
/// </summary>
|
||
public static List<string> DeserializeStringList(string? json) => JsonStringArrayHelper.Deserialize(json);
|
||
|
||
/// <summary>
|
||
/// 将订单状态码转为中文名称。
|
||
/// </summary>
|
||
public static string? GetOrderStatusName(string statusCode)
|
||
{
|
||
return statusCode switch
|
||
{
|
||
OrderStatusCodes.Draft => "草稿",
|
||
OrderStatusCodes.Predeclared => "已预报",
|
||
OrderStatusCodes.PendingAllocation => "待排货",
|
||
OrderStatusCodes.PendingPayment => "待支付",
|
||
OrderStatusCodes.PendingShip => "待发货",
|
||
OrderStatusCodes.PendingOnline => "待上网",
|
||
OrderStatusCodes.Shipping => "运输中",
|
||
OrderStatusCodes.DeliverySuccess => "派送成功",
|
||
OrderStatusCodes.DeliveryFailed => "派送失败",
|
||
OrderStatusCodes.Problematic => "问题件",
|
||
OrderStatusCodes.Deleted => "已删除",
|
||
OrderStatusCodes.Claimed => "理赔件",
|
||
_ => statusCode
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将仓库类型码转为中文名称。
|
||
/// </summary>
|
||
public static string GetWarehouseTypeName(string warehouseType)
|
||
{
|
||
return warehouseType switch
|
||
{
|
||
WarehouseTypeCodes.Main => "主仓",
|
||
WarehouseTypeCodes.Transit => "中转仓",
|
||
WarehouseTypeCodes.Return => "退货仓",
|
||
WarehouseTypeCodes.Other => "其他",
|
||
_ => warehouseType
|
||
};
|
||
}
|
||
}
|