diff --git a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs index 5ae3a5e..7cce30d 100644 --- a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs +++ b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs @@ -371,6 +371,11 @@ public partial class SalesProductService snapshot = await BuildRulesPublishSnapshotAsync(salesProductId); } + // Content 存 canonical 形态,与 sync-status 对比口径一致 + var content = operatorType == PriceOperatorType + ? OmsDocHelper.CanonicalizeSalesProductPriceSnapshotJson(snapshot) + : OmsDocHelper.CanonicalizeSalesProductRulesSnapshotJson(snapshot); + // Content 中保存完整快照,回滚时直接从快照恢复,避免依赖当前库内状态。 await SalesProductRepository.InsertPublishRecordAsync(new SalesProductPublishRecord { @@ -382,7 +387,7 @@ public partial class SalesProductService PublishTime = DateTime.Now, Description = description, AiAnalysisReport = string.IsNullOrWhiteSpace(aiAnalysisReport) ? null : aiAnalysisReport.Trim(), - Content = OmsDocHelper.SerializeSnapshotJson(snapshot) + Content = content }); } diff --git a/src/Oms.Application/Services/Channels/SalesProductService.cs b/src/Oms.Application/Services/Channels/SalesProductService.cs index 35f2f94..f2a96ba 100644 --- a/src/Oms.Application/Services/Channels/SalesProductService.cs +++ b/src/Oms.Application/Services/Channels/SalesProductService.cs @@ -420,11 +420,15 @@ public partial class SalesProductService : ApplicationService var normalized = NormalizeOperatorType(operatorType); await GetProductOrThrowAsync(salesProductId); + // 两侧都走「序列化 → 反序列化规范化 → 再序列化」,避免刚发布仍误判待发布 + // (价格项排序、FactorValuesJson 的 Dictionary/JsonElement 形态差异等) object pendingSnapshot = normalized == PriceOperatorType ? await BuildPricePublishSnapshotAsync(salesProductId) : await BuildRulesPublishSnapshotAsync(salesProductId); - var pendingJson = OmsDocHelper.SerializeSnapshotJson(pendingSnapshot) ?? string.Empty; - var pendingHash = ComputeContentHash(pendingJson); + var pendingCanon = normalized == PriceOperatorType + ? OmsDocHelper.CanonicalizeSalesProductPriceSnapshotJson(pendingSnapshot) + : OmsDocHelper.CanonicalizeSalesProductRulesSnapshotJson(pendingSnapshot); + var pendingHash = ComputeContentHash(pendingCanon); var headers = await SalesProductRepository.GetAllPublishRecordHeadersAsync(salesProductId, normalized); var onlineHeader = headers @@ -446,11 +450,11 @@ public partial class SalesProductService : ApplicationService var onlineRecord = await SalesProductRepository.GetPublishRecordByIdAsync(onlineHeader.Id); var onlineJson = onlineRecord?.Content ?? string.Empty; - // 线上 Content 与待发布统一经反序列化再序列化,避免字段顺序噪声 - object? onlineNormalized = normalized == PriceOperatorType - ? OmsDocHelper.DeserializePriceSnapshot(onlineJson) - : OmsDocHelper.DeserializeRulesSnapshot(onlineJson); - var onlineCanon = OmsDocHelper.SerializeSnapshotJson(onlineNormalized) ?? string.Empty; + var onlineCanon = normalized == PriceOperatorType + ? OmsDocHelper.CanonicalizeSalesProductPriceSnapshotJson( + OmsDocHelper.DeserializePriceSnapshot(onlineJson)) + : OmsDocHelper.CanonicalizeSalesProductRulesSnapshotJson( + OmsDocHelper.DeserializeRulesSnapshot(onlineJson)); var onlineHash = ComputeContentHash(onlineCanon); return new SalesProductPublishSyncStatusOutput diff --git a/src/Oms.Application/Services/Channels/ServiceChannelService.cs b/src/Oms.Application/Services/Channels/ServiceChannelService.cs index ba4c2e5..d0f8e4d 100644 --- a/src/Oms.Application/Services/Channels/ServiceChannelService.cs +++ b/src/Oms.Application/Services/Channels/ServiceChannelService.cs @@ -416,8 +416,10 @@ public partial class ServiceChannelService : ApplicationService object pendingSnapshot = normalized == PriceOperatorType ? await BuildPricePublishSnapshotAsync(serviceChannelId) : await BuildRulesPublishSnapshotAsync(serviceChannelId); - var pendingJson = OmsDocHelper.SerializeSnapshotJson(pendingSnapshot) ?? string.Empty; - var pendingHash = ComputeContentHash(pendingJson); + var pendingCanon = normalized == PriceOperatorType + ? OmsDocHelper.CanonicalizeChannelPriceSnapshotJson(pendingSnapshot) + : OmsDocHelper.SerializeSnapshotJson(pendingSnapshot) ?? string.Empty; + var pendingHash = ComputeContentHash(pendingCanon); var onlineRecord = await ServiceChannelRepository.GetEffectivePublishRecordAsync( serviceChannelId, @@ -436,10 +438,12 @@ public partial class ServiceChannelService : ApplicationService } var onlineJson = onlineRecord.Content ?? string.Empty; - object? onlineNormalized = normalized == PriceOperatorType - ? OmsDocHelper.DeserializeChannelPriceSnapshot(onlineJson) - : OmsDocHelper.DeserializeJson(onlineJson); - var onlineCanon = OmsDocHelper.SerializeSnapshotJson(onlineNormalized) ?? string.Empty; + var onlineCanon = normalized == PriceOperatorType + ? OmsDocHelper.CanonicalizeChannelPriceSnapshotJson( + OmsDocHelper.DeserializeChannelPriceSnapshot(onlineJson)) + : OmsDocHelper.SerializeSnapshotJson( + OmsDocHelper.DeserializeJson(onlineJson)) + ?? string.Empty; var onlineHash = ComputeContentHash(onlineCanon); return new ServiceChannelPublishSyncStatusOutput diff --git a/src/Oms.Application/Shared/OmsDocHelper.cs b/src/Oms.Application/Shared/OmsDocHelper.cs index ddcd2a3..15d56d8 100644 --- a/src/Oms.Application/Shared/OmsDocHelper.cs +++ b/src/Oms.Application/Shared/OmsDocHelper.cs @@ -150,6 +150,33 @@ public static class OmsDocHelper return snapshot; } + /// + /// 同步态对比用:先序列化再反序列化规范化,消除 Dictionary/JsonElement、字段顺序等噪声。 + /// + public static string CanonicalizeSalesProductPriceSnapshotJson(object? snapshot) + { + var raw = SerializeSnapshotJson(snapshot) ?? string.Empty; + return SerializeSnapshotJson(DeserializePriceSnapshot(raw)) ?? string.Empty; + } + + /// + /// 同步态对比用:规则快照规范化 canonical JSON。 + /// + public static string CanonicalizeSalesProductRulesSnapshotJson(object? snapshot) + { + var raw = SerializeSnapshotJson(snapshot) ?? string.Empty; + return SerializeSnapshotJson(DeserializeRulesSnapshot(raw)) ?? string.Empty; + } + + /// + /// 同步态对比用:渠道买价快照规范化 canonical JSON。 + /// + public static string CanonicalizeChannelPriceSnapshotJson(object? snapshot) + { + var raw = SerializeSnapshotJson(snapshot) ?? string.Empty; + return SerializeSnapshotJson(DeserializeChannelPriceSnapshot(raw)) ?? string.Empty; + } + /// /// 解析渠道买价发布 Content,供列表/对比 API 使用(兼容旧快照审计字段)。 /// diff --git a/src/Oms.Application/Shared/PublishSnapshotNormalizer.cs b/src/Oms.Application/Shared/PublishSnapshotNormalizer.cs index 3f11120..e122993 100644 --- a/src/Oms.Application/Shared/PublishSnapshotNormalizer.cs +++ b/src/Oms.Application/Shared/PublishSnapshotNormalizer.cs @@ -28,6 +28,7 @@ public static class PublishSnapshotNormalizer } NormalizeKpiAreas(snapshot.PriceAreaInfos); + snapshot.PriceAreaRefs = SortPackRefs(snapshot.PriceAreaRefs); snapshot.Prices = SortSalesProductPriceItems(snapshot.Prices); } @@ -39,6 +40,7 @@ public static class PublishSnapshotNormalizer } NormalizeKpiAreas(snapshot.PriceAreaInfos); + snapshot.PriceAreaRefs = SortPackRefs(snapshot.PriceAreaRefs); snapshot.Prices = SortChannelPriceItems(snapshot.Prices); } @@ -92,6 +94,12 @@ public static class PublishSnapshotNormalizer } } + private static List SortPackRefs(List? refs) => + refs? + .OrderBy(x => x.AreaId) + .ThenBy(x => x.PackVersion, StringComparer.Ordinal) + .ToList() ?? []; + private static List SortSalesProductPriceItems( List? prices) => prices?