fix: unify publish snapshot API output and normalize legacy price content

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-07-23 12:03:47 +08:00
parent e2dca20966
commit 8329bbc3a6
8 changed files with 205 additions and 6 deletions

View File

@ -153,7 +153,9 @@ public partial class SalesProductService
PublishUser = entity.PublishUser,
PublishTime = entity.PublishTime,
Description = entity.Description,
Content = OmsDocHelper.DeserializeJsonObject(entity.Content),
Content = operatorType == PriceOperatorType
? OmsDocHelper.ParseSalesProductPricePublishContentForApi(entity.Content)
: OmsDocHelper.ToSnapshotApiContent(OmsDocHelper.DeserializeRulesSnapshot(entity.Content)),
CreateTime = entity.CreateTime
}).ToList(),
Total = total

View File

@ -335,9 +335,10 @@ public partial class SalesProductService : ApplicationService
public async Task<object> PreviewPublishSnapshotAsync([FromQuery] int salesProductId, [FromQuery] string operatorType)
{
var normalized = NormalizeOperatorType(operatorType);
return normalized == PriceOperatorType
object snapshot = normalized == PriceOperatorType
? await BuildPricePublishSnapshotAsync(salesProductId)
: await BuildRulesPublishSnapshotAsync(salesProductId);
return OmsDocHelper.ToSnapshotApiContent(snapshot)!;
}
/// <summary>

View File

@ -168,7 +168,9 @@ public partial class ServiceChannelService
PublishUser = entity.PublishUser,
PublishTime = entity.PublishTime,
Description = entity.Description,
Content = OmsDocHelper.DeserializeJsonObject(entity.Content),
Content = operatorType == PriceOperatorType
? OmsDocHelper.ParseChannelPricePublishContentForApi(entity.Content)
: OmsDocHelper.ToSnapshotApiContent(OmsDocHelper.DeserializeJson<ServiceChannelRulesPublishSnapshotDto>(entity.Content)),
CreateTime = entity.CreateTime
}).ToList(),
Total = total

View File

@ -334,9 +334,10 @@ public partial class ServiceChannelService : ApplicationService
public async Task<object> PreviewPublishSnapshotAsync([FromQuery] int serviceChannelId, [FromQuery] string operatorType)
{
var normalized = NormalizeOperatorType(operatorType);
return normalized == PriceOperatorType
object snapshot = normalized == PriceOperatorType
? await BuildPricePublishSnapshotAsync(serviceChannelId)
: await BuildRulesPublishSnapshotAsync(serviceChannelId);
return OmsDocHelper.ToSnapshotApiContent(snapshot)!;
}
/// <summary>

View File

@ -802,7 +802,7 @@ public partial class OrderService
bindTime);
if (priceRecord?.Content != null)
{
var snapshot = OmsDocHelper.DeserializeJson<ServiceChannelPricePublishSnapshotDto>(priceRecord.Content);
var snapshot = OmsDocHelper.DeserializeChannelPriceSnapshot(priceRecord.Content);
if (snapshot?.Prices.Count > 0)
{
return new ChannelPriceBinding

View File

@ -115,6 +115,20 @@ public static class OmsDocHelper
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);
@ -129,6 +143,31 @@ public static class OmsDocHelper
return snapshot;
}
public static ServiceChannelPricePublishSnapshotDto? DeserializeChannelPriceSnapshot(string? json)
{
var snapshot = DeserializeJson<ServiceChannelPricePublishSnapshotDto>(json);
PublishSnapshotNormalizer.NormalizeChannelPriceSnapshot(snapshot);
return snapshot;
}
/// <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()

View File

@ -1,9 +1,10 @@
using Oms.Application.Dtos.Channels;
using Oms.Application.Dtos.Orders;
namespace Oms.Application.Shared;
/// <summary>
/// 发布快照读取后规范化(旧字段迁移、去掉冗余)。
/// 发布快照读取后规范化(旧字段迁移、去掉冗余、稳定排序)。
/// </summary>
public static class PublishSnapshotNormalizer
{
@ -27,6 +28,17 @@ public static class PublishSnapshotNormalizer
}
NormalizeKpiAreas(snapshot.PriceAreaInfos);
snapshot.Prices = SortSalesProductPriceItems(snapshot.Prices);
}
public static void NormalizeChannelPriceSnapshot(ServiceChannelPricePublishSnapshotDto? snapshot)
{
if (snapshot is null)
{
return;
}
snapshot.Prices = SortChannelPriceItems(snapshot.Prices);
}
public static void NormalizeKpiAreas(List<KpiAreaSnapshotRuleDto>? areas)
@ -78,4 +90,41 @@ public static class PublishSnapshotNormalizer
}
}
}
private static List<SalesProductPriceSnapshotItemDto> SortSalesProductPriceItems(
List<SalesProductPriceSnapshotItemDto>? prices) =>
prices?
.OrderBy(price => price.SerialNumber)
.ThenBy(price => price.Id)
.Select(price =>
{
price.Factors = SortFactors(price.Factors);
price.Details = SortDetails(price.Details);
return price;
})
.ToList() ?? [];
private static List<ServiceChannelPriceSnapshotItemDto> SortChannelPriceItems(
List<ServiceChannelPriceSnapshotItemDto>? prices) =>
prices?
.OrderBy(price => price.SerialNumber)
.ThenBy(price => price.Id)
.Select(price =>
{
price.Factors = SortFactors(price.Factors);
price.Details = SortDetails(price.Details);
return price;
})
.ToList() ?? [];
private static List<ChannelPriceFactorOutput> SortFactors(List<ChannelPriceFactorOutput>? factors) =>
factors?
.OrderBy(factor => factor.SerialNumber)
.ThenBy(factor => factor.Id)
.ToList() ?? [];
private static List<ChannelPriceDetailOutput> SortDetails(List<ChannelPriceDetailOutput>? details) =>
details?
.OrderBy(detail => detail.Id)
.ToList() ?? [];
}

View File

@ -0,0 +1,105 @@
using FluentAssertions;
using Oms.Application.Shared;
namespace Oms.Tests;
public class PublishSnapshotCompareTests
{
private const string LegacyOnlineJson = """
{
"prices": [
{
"id": 1,
"status": 2,
"details": [
{
"id": 6,
"priceId": 1,
"calculatedValue": 0,
"factorValuesJson": { "分区": "3", "重量": "(0-0.5]" },
"calculationFormula": "total",
"customerGroupValues": { "普通客户": 11 }
}
],
"factors": [
{ "id": 7, "factor": "zone", "priceId": 1, "factorValues": ["3"], "serialNumber": 1 },
{ "id": 8, "factor": "weight", "priceId": 1, "factorValues": ["0.5"], "serialNumber": 2 },
{ "id": 9, "factor": "customer", "priceId": 1, "factorValues": ["0"], "serialNumber": 3 }
],
"costItem": "freight",
"costUnit": "kg",
"currency": "RMB",
"priceName": "运费",
"createTime": "0001-01-01T00:00:00",
"serialNumber": 1,
"serviceChannelId": 1,
"chargingCondition": "无"
}
],
"serviceChannelId": 1
}
""";
private const string PreviewJson = """
{
"serviceChannelId": 1,
"prices": [
{
"id": 1,
"serviceChannelId": 1,
"serialNumber": 1,
"status": 2,
"priceName": "运费",
"costItem": "freight",
"costUnit": "kg",
"currency": "RMB",
"chargingCondition": "无",
"factors": [
{ "id": 7, "priceId": 1, "serialNumber": 1, "factor": "zone", "factorValues": ["3"] },
{ "id": 8, "priceId": 1, "serialNumber": 2, "factor": "weight", "factorValues": ["0.5"] },
{ "id": 9, "priceId": 1, "serialNumber": 3, "factor": "customer", "factorValues": ["0"] }
],
"details": [
{
"id": 6,
"priceId": 1,
"factorValuesJson": { "分区": "3", "重量": "(0-0.5]" },
"calculationFormula": "total",
"calculatedValue": 0,
"customerGroupValues": { "普通客户": 11 }
}
],
"createTime": "0001-01-01 00:00:00",
"updateTime": null
}
]
}
""";
[Fact]
public void ParseChannelPricePublishContentForApi_ShouldMatchPreviewAfterLegacyAuditNoise()
{
var online = OmsDocHelper.ParseChannelPricePublishContentForApi(LegacyOnlineJson);
var preview = OmsDocHelper.ToSnapshotApiContent(OmsDocHelper.DeserializeChannelPriceSnapshot(PreviewJson));
var onlineJson = OmsDocHelper.SerializeSnapshotJson(online);
var previewJson = OmsDocHelper.SerializeSnapshotJson(preview);
onlineJson.Should().Be(previewJson);
}
[Fact]
public void ToSnapshotApiContent_ShouldRoundTripWithPublishSerializer()
{
var snapshot = OmsDocHelper.DeserializeChannelPriceSnapshot(PreviewJson);
snapshot.Should().NotBeNull();
var json = OmsDocHelper.SerializeSnapshotJson(snapshot);
json.Should().NotContain("createTime");
json.Should().NotContain("updateTime");
var apiContent = OmsDocHelper.ToSnapshotApiContent(snapshot);
var roundTripJson = OmsDocHelper.SerializeSnapshotJson(apiContent);
roundTripJson.Should().Be(json);
}
}