All checks were successful
Build and Deploy / deploy (push) Successful in 57s
sync-status 用 ResolvePackRef;哈希忽略 PackVersion/AreaName。修复卖价页发布后因价格行签名跳过刷新导致徽章卡在待发布。 Co-authored-by: Cursor <cursoragent@cursor.com>
282 lines
9.7 KiB
C#
282 lines
9.7 KiB
C#
using System.Text.Json;
|
||
using System.Text.Json.Nodes;
|
||
using System.Text.Json.Serialization;
|
||
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 raw = SerializeSnapshotJson(snapshot) ?? string.Empty;
|
||
return SerializeSnapshotJson(DeserializePriceSnapshot(raw)) ?? 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 raw = SerializeSnapshotJson(snapshot) ?? string.Empty;
|
||
return SerializeSnapshotJson(DeserializeChannelPriceSnapshot(raw)) ?? string.Empty;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 发布同步态哈希:canonical + 去掉 PackVersion/AreaName 等钉包展示字段。
|
||
/// </summary>
|
||
public static string HashSalesProductPriceSnapshotForSync(object? snapshot)
|
||
{
|
||
var dto = DeserializePriceSnapshot(SerializeSnapshotJson(snapshot));
|
||
PublishSnapshotNormalizer.StripVolatilePackPinsForSyncCompare(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);
|
||
return SerializeSnapshotJson(dto) ?? string.Empty;
|
||
}
|
||
|
||
/// <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 => element.TryGetInt64(out var integer)
|
||
? integer
|
||
: element.GetDecimal(),
|
||
JsonValueKind.True => true,
|
||
JsonValueKind.False => false,
|
||
JsonValueKind.Null or JsonValueKind.Undefined => null,
|
||
_ => element.GetRawText()
|
||
};
|
||
|
||
/// <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
|
||
};
|
||
}
|
||
}
|