fix: 卖价同步态对比改用 canonical JSON,避免误报待发布
All checks were successful
Build and Deploy / deploy (push) Successful in 59s

待发布与线上两侧统一规范化后再算哈希,消除排序与 FactorValuesJson 形态差异导致的假 diff。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-10-07 19:13:26 +08:00
parent 1704c9f1e2
commit 73f334edc8
5 changed files with 62 additions and 14 deletions

View File

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

View File

@ -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

View File

@ -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<ServiceChannelRulesPublishSnapshotDto>(onlineJson);
var onlineCanon = OmsDocHelper.SerializeSnapshotJson(onlineNormalized) ?? string.Empty;
var onlineCanon = normalized == PriceOperatorType
? OmsDocHelper.CanonicalizeChannelPriceSnapshotJson(
OmsDocHelper.DeserializeChannelPriceSnapshot(onlineJson))
: OmsDocHelper.SerializeSnapshotJson(
OmsDocHelper.DeserializeJson<ServiceChannelRulesPublishSnapshotDto>(onlineJson))
?? string.Empty;
var onlineHash = ComputeContentHash(onlineCanon);
return new ServiceChannelPublishSyncStatusOutput

View File

@ -150,6 +150,33 @@ public static class OmsDocHelper
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>
/// 解析渠道买价发布 Content,供列表/对比 API 使用(兼容旧快照审计字段)。
/// </summary>

View File

@ -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<KpiAreaPackRefDto> SortPackRefs(List<KpiAreaPackRefDto>? refs) =>
refs?
.OrderBy(x => x.AreaId)
.ThenBy(x => x.PackVersion, StringComparer.Ordinal)
.ToList() ?? [];
private static List<SalesProductPriceSnapshotItemDto> SortSalesProductPriceItems(
List<SalesProductPriceSnapshotItemDto>? prices) =>
prices?