From 8329bbc3a6889be0d31b73ca7eedd99f5d51ef52 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Thu, 23 Jul 2026 12:03:47 +0800 Subject: [PATCH] fix: unify publish snapshot API output and normalize legacy price content Co-authored-by: Cursor --- .../Channels/SalesProductService.Helpers.cs | 4 +- .../Services/Channels/SalesProductService.cs | 3 +- .../Channels/ServiceChannelService.Helpers.cs | 4 +- .../Channels/ServiceChannelService.cs | 3 +- .../Orders/OrderService.ParcelBilling.cs | 2 +- src/Oms.Application/Shared/OmsDocHelper.cs | 39 +++++++ .../Shared/PublishSnapshotNormalizer.cs | 51 ++++++++- src/Oms.Tests/PublishSnapshotCompareTests.cs | 105 ++++++++++++++++++ 8 files changed, 205 insertions(+), 6 deletions(-) create mode 100644 src/Oms.Tests/PublishSnapshotCompareTests.cs diff --git a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs index e34139e..efda9ae 100644 --- a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs +++ b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs @@ -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 diff --git a/src/Oms.Application/Services/Channels/SalesProductService.cs b/src/Oms.Application/Services/Channels/SalesProductService.cs index e9e06bd..5d4c535 100644 --- a/src/Oms.Application/Services/Channels/SalesProductService.cs +++ b/src/Oms.Application/Services/Channels/SalesProductService.cs @@ -335,9 +335,10 @@ public partial class SalesProductService : ApplicationService public async Task 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)!; } /// diff --git a/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs b/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs index 94af99d..69eabf1 100644 --- a/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs +++ b/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs @@ -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(entity.Content)), CreateTime = entity.CreateTime }).ToList(), Total = total diff --git a/src/Oms.Application/Services/Channels/ServiceChannelService.cs b/src/Oms.Application/Services/Channels/ServiceChannelService.cs index b7707aa..389c859 100644 --- a/src/Oms.Application/Services/Channels/ServiceChannelService.cs +++ b/src/Oms.Application/Services/Channels/ServiceChannelService.cs @@ -334,9 +334,10 @@ public partial class ServiceChannelService : ApplicationService public async Task 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)!; } /// diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs index d386942..ff3e6e2 100644 --- a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs +++ b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs @@ -802,7 +802,7 @@ public partial class OrderService bindTime); if (priceRecord?.Content != null) { - var snapshot = OmsDocHelper.DeserializeJson(priceRecord.Content); + var snapshot = OmsDocHelper.DeserializeChannelPriceSnapshot(priceRecord.Content); if (snapshot?.Prices.Count > 0) { return new ChannelPriceBinding diff --git a/src/Oms.Application/Shared/OmsDocHelper.cs b/src/Oms.Application/Shared/OmsDocHelper.cs index 9c39b6f..e7a6315 100644 --- a/src/Oms.Application/Shared/OmsDocHelper.cs +++ b/src/Oms.Application/Shared/OmsDocHelper.cs @@ -115,6 +115,20 @@ public static class OmsDocHelper return JsonSerializer.Serialize(value, SnapshotJsonOptions); } + /// + /// 将快照对象转为 API 输出结构(与落库 JSON 同构,避免预览/列表/落库序列化不一致)。 + /// + public static object? ToSnapshotApiContent(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(json); @@ -129,6 +143,31 @@ public static class OmsDocHelper return snapshot; } + public static ServiceChannelPricePublishSnapshotDto? DeserializeChannelPriceSnapshot(string? json) + { + var snapshot = DeserializeJson(json); + PublishSnapshotNormalizer.NormalizeChannelPriceSnapshot(snapshot); + return snapshot; + } + + /// + /// 解析渠道买价发布 Content,供列表/对比 API 使用(兼容旧快照审计字段)。 + /// + public static object? ParseChannelPricePublishContentForApi(string? json) + { + var snapshot = DeserializeChannelPriceSnapshot(json); + return snapshot is null ? DeserializeJsonObject(json) : ToSnapshotApiContent(snapshot); + } + + /// + /// 解析产品销售卖价发布 Content,供列表/对比 API 使用。 + /// + 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() diff --git a/src/Oms.Application/Shared/PublishSnapshotNormalizer.cs b/src/Oms.Application/Shared/PublishSnapshotNormalizer.cs index 0ca3f0b..2eee1df 100644 --- a/src/Oms.Application/Shared/PublishSnapshotNormalizer.cs +++ b/src/Oms.Application/Shared/PublishSnapshotNormalizer.cs @@ -1,9 +1,10 @@ +using Oms.Application.Dtos.Channels; using Oms.Application.Dtos.Orders; namespace Oms.Application.Shared; /// -/// 发布快照读取后规范化(旧字段迁移、去掉冗余)。 +/// 发布快照读取后规范化(旧字段迁移、去掉冗余、稳定排序)。 /// 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? areas) @@ -78,4 +90,41 @@ public static class PublishSnapshotNormalizer } } } + + private static List SortSalesProductPriceItems( + List? 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 SortChannelPriceItems( + List? 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 SortFactors(List? factors) => + factors? + .OrderBy(factor => factor.SerialNumber) + .ThenBy(factor => factor.Id) + .ToList() ?? []; + + private static List SortDetails(List? details) => + details? + .OrderBy(detail => detail.Id) + .ToList() ?? []; } diff --git a/src/Oms.Tests/PublishSnapshotCompareTests.cs b/src/Oms.Tests/PublishSnapshotCompareTests.cs new file mode 100644 index 0000000..f9f626c --- /dev/null +++ b/src/Oms.Tests/PublishSnapshotCompareTests.cs @@ -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); + } +}