admin/src/Oms.Application/Shared/OmsDocHelper.cs
zzh 984799fcde
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s
fix: sync-status 忽略价表明细数字 JSON 形态差异
LIVE 侧 DeserializeJsonObject 常把客户组价收到 long,历史 Content 里常见 133.0/0.0000;JS 结构相等但 SHA256 不等导致误报待发布。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-07 19:52:04 +08:00

396 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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