diff --git a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs
index 7cce30d..fff4f66 100644
--- a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs
+++ b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs
@@ -317,6 +317,25 @@ public partial class SalesProductService
return priceSnapshot;
}
+ ///
+ /// 同步态待发布快照:价区只解析哈希/已有包,不 Ensure 写库。
+ ///
+ private async Task BuildPricePublishSnapshotForSyncAsync(int salesProductId)
+ {
+ var product = await GetProductOrThrowAsync(salesProductId);
+ var priceSnapshot = new SalesProductPricePublishSnapshot
+ {
+ SalesProductId = salesProductId,
+ Prices = (await GetPriceOutputsAsync(salesProductId)).Select(PublishSnapshotSlimmer.SlimPrice).ToList()
+ };
+ await PriceAreaSnapshotHelper.ApplyPriceAreaRefsForSyncAsync(
+ priceSnapshot,
+ product,
+ KpiAreaRepository,
+ KpiAreaPackService);
+ return priceSnapshot;
+ }
+
private string ResolvePublishUser() =>
string.IsNullOrWhiteSpace(CurrentUser.UserName)
? CurrentUser.GetId().ToString()
diff --git a/src/Oms.Application/Services/Channels/SalesProductService.cs b/src/Oms.Application/Services/Channels/SalesProductService.cs
index f2a96ba..c89d3b4 100644
--- a/src/Oms.Application/Services/Channels/SalesProductService.cs
+++ b/src/Oms.Application/Services/Channels/SalesProductService.cs
@@ -420,14 +420,13 @@ public partial class SalesProductService : ApplicationService
var normalized = NormalizeOperatorType(operatorType);
await GetProductOrThrowAsync(salesProductId);
- // 两侧都走「序列化 → 反序列化规范化 → 再序列化」,避免刚发布仍误判待发布
- // (价格项排序、FactorValuesJson 的 Dictionary/JsonElement 形态差异等)
+ // 待发布:同步态不 Ensure 分区包;对比时忽略 PackVersion/AreaName,只比价格项 + 分区 ContentHash
object pendingSnapshot = normalized == PriceOperatorType
- ? await BuildPricePublishSnapshotAsync(salesProductId)
+ ? await BuildPricePublishSnapshotForSyncAsync(salesProductId)
: await BuildRulesPublishSnapshotAsync(salesProductId);
var pendingCanon = normalized == PriceOperatorType
- ? OmsDocHelper.CanonicalizeSalesProductPriceSnapshotJson(pendingSnapshot)
- : OmsDocHelper.CanonicalizeSalesProductRulesSnapshotJson(pendingSnapshot);
+ ? OmsDocHelper.HashSalesProductPriceSnapshotForSync(pendingSnapshot)
+ : OmsDocHelper.HashSalesProductRulesSnapshotForSync(pendingSnapshot);
var pendingHash = ComputeContentHash(pendingCanon);
var headers = await SalesProductRepository.GetAllPublishRecordHeadersAsync(salesProductId, normalized);
@@ -451,9 +450,9 @@ public partial class SalesProductService : ApplicationService
var onlineRecord = await SalesProductRepository.GetPublishRecordByIdAsync(onlineHeader.Id);
var onlineJson = onlineRecord?.Content ?? string.Empty;
var onlineCanon = normalized == PriceOperatorType
- ? OmsDocHelper.CanonicalizeSalesProductPriceSnapshotJson(
+ ? OmsDocHelper.HashSalesProductPriceSnapshotForSync(
OmsDocHelper.DeserializePriceSnapshot(onlineJson))
- : OmsDocHelper.CanonicalizeSalesProductRulesSnapshotJson(
+ : OmsDocHelper.HashSalesProductRulesSnapshotForSync(
OmsDocHelper.DeserializeRulesSnapshot(onlineJson));
var onlineHash = ComputeContentHash(onlineCanon);
diff --git a/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs b/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs
index 825dde4..9f29b18 100644
--- a/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs
+++ b/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs
@@ -262,6 +262,25 @@ public partial class ServiceChannelService
return snapshot;
}
+ ///
+ /// 买价同步态:不 Ensure 新分区包。
+ ///
+ private async Task BuildPricePublishSnapshotForSyncAsync(int serviceChannelId)
+ {
+ var snapshot = new ServiceChannelPricePublishSnapshotDto
+ {
+ ServiceChannelId = serviceChannelId,
+ Prices = (await GetPriceOutputsAsync(serviceChannelId))
+ .Select(PublishSnapshotSlimmer.SlimChannelPrice)
+ .ToList()
+ };
+ await PriceAreaSnapshotHelper.ApplyChannelPriceAreaRefsForSyncAsync(
+ snapshot,
+ KpiAreaRepository,
+ KpiAreaPackService);
+ return snapshot;
+ }
+
///
/// 组装渠道规则发布快照(不落库):钉偏远分区包。
///
diff --git a/src/Oms.Application/Services/Channels/ServiceChannelService.cs b/src/Oms.Application/Services/Channels/ServiceChannelService.cs
index d0f8e4d..526b607 100644
--- a/src/Oms.Application/Services/Channels/ServiceChannelService.cs
+++ b/src/Oms.Application/Services/Channels/ServiceChannelService.cs
@@ -414,10 +414,10 @@ public partial class ServiceChannelService : ApplicationService
await GetEntityOrThrowAsync(serviceChannelId);
object pendingSnapshot = normalized == PriceOperatorType
- ? await BuildPricePublishSnapshotAsync(serviceChannelId)
+ ? await BuildPricePublishSnapshotForSyncAsync(serviceChannelId)
: await BuildRulesPublishSnapshotAsync(serviceChannelId);
var pendingCanon = normalized == PriceOperatorType
- ? OmsDocHelper.CanonicalizeChannelPriceSnapshotJson(pendingSnapshot)
+ ? OmsDocHelper.HashChannelPriceSnapshotForSync(pendingSnapshot)
: OmsDocHelper.SerializeSnapshotJson(pendingSnapshot) ?? string.Empty;
var pendingHash = ComputeContentHash(pendingCanon);
@@ -439,7 +439,7 @@ public partial class ServiceChannelService : ApplicationService
var onlineJson = onlineRecord.Content ?? string.Empty;
var onlineCanon = normalized == PriceOperatorType
- ? OmsDocHelper.CanonicalizeChannelPriceSnapshotJson(
+ ? OmsDocHelper.HashChannelPriceSnapshotForSync(
OmsDocHelper.DeserializeChannelPriceSnapshot(onlineJson))
: OmsDocHelper.SerializeSnapshotJson(
OmsDocHelper.DeserializeJson(onlineJson))
diff --git a/src/Oms.Application/Shared/KpiAreaPackService.cs b/src/Oms.Application/Shared/KpiAreaPackService.cs
index bdc8b4f..576fcbd 100644
--- a/src/Oms.Application/Shared/KpiAreaPackService.cs
+++ b/src/Oms.Application/Shared/KpiAreaPackService.cs
@@ -41,6 +41,67 @@ public class KpiAreaPackService : IScopedDependency
string? publishUser = null,
string? description = null,
CancellationToken cancellationToken = default)
+ {
+ var prepared = await PreparePackContentAsync(areaId, cancellationToken);
+ var existing = await _kpiAreaRepository.FindPublishByAreaIdAndContentHashAsync(
+ areaId,
+ prepared.ContentHash,
+ cancellationToken);
+ if (existing != null)
+ {
+ return ToRef(existing, prepared.AreaName);
+ }
+
+ var version = DateTime.Now.ToString("yyyyMMddHHmmssfff");
+ var record = new KpiAreaPublishRecord
+ {
+ AreaId = areaId,
+ AreaType = string.IsNullOrWhiteSpace(areaType) ? prepared.AreaType : areaType.Trim(),
+ Version = version,
+ Status = EsSnapshotStatus.Active,
+ ContentHash = prepared.ContentHash,
+ Content = prepared.ContentJson,
+ PublishUser = string.IsNullOrWhiteSpace(publishUser) ? "system" : publishUser.Trim(),
+ PublishTime = DateTime.Now,
+ Description = description
+ };
+ record = await _kpiAreaRepository.InsertPublishRecordAsync(record, cancellationToken);
+ PackCache.Set(CacheKey(areaId, record.Version), prepared.Rules, new MemoryCacheEntryOptions
+ {
+ Size = 1,
+ SlidingExpiration = TimeSpan.FromHours(6)
+ });
+ return ToRef(record, prepared.AreaName);
+ }
+
+ ///
+ /// 同步态专用:只解析当前库明细哈希并回填已有包 Version;不落新包(避免 GET sync-status 写库)。
+ ///
+ public async Task ResolvePackRefForSyncAsync(
+ int areaId,
+ CancellationToken cancellationToken = default)
+ {
+ var prepared = await PreparePackContentAsync(areaId, cancellationToken);
+ var existing = await _kpiAreaRepository.FindPublishByAreaIdAndContentHashAsync(
+ areaId,
+ prepared.ContentHash,
+ cancellationToken);
+ if (existing != null)
+ {
+ return ToRef(existing, prepared.AreaName);
+ }
+
+ return new KpiAreaPackRefDto
+ {
+ AreaId = areaId,
+ AreaName = prepared.AreaName,
+ PackVersion = string.Empty,
+ ContentHash = prepared.ContentHash
+ };
+ }
+
+ private async Task<(string AreaType, string? AreaName, string ContentJson, string ContentHash, List Rules)>
+ PreparePackContentAsync(int areaId, CancellationToken cancellationToken)
{
if (areaId <= 0)
{
@@ -57,36 +118,7 @@ public class KpiAreaPackService : IScopedDependency
var contentJson = OmsDocHelper.SerializeSnapshotJson(rules) ?? "[]";
var contentHash = ComputeSha256Hex(contentJson);
-
- var existing = await _kpiAreaRepository.FindPublishByAreaIdAndContentHashAsync(
- areaId,
- contentHash,
- cancellationToken);
- if (existing != null)
- {
- return ToRef(existing, area.Name);
- }
-
- var version = DateTime.Now.ToString("yyyyMMddHHmmssfff");
- var record = new KpiAreaPublishRecord
- {
- AreaId = areaId,
- AreaType = string.IsNullOrWhiteSpace(areaType) ? area.AreaType : areaType.Trim(),
- Version = version,
- Status = EsSnapshotStatus.Active,
- ContentHash = contentHash,
- Content = contentJson,
- PublishUser = string.IsNullOrWhiteSpace(publishUser) ? "system" : publishUser.Trim(),
- PublishTime = DateTime.Now,
- Description = description
- };
- record = await _kpiAreaRepository.InsertPublishRecordAsync(record, cancellationToken);
- PackCache.Set(CacheKey(areaId, record.Version), rules, new MemoryCacheEntryOptions
- {
- Size = 1,
- SlidingExpiration = TimeSpan.FromHours(6)
- });
- return ToRef(record, area.Name);
+ return (area.AreaType, area.Name, contentJson, contentHash, rules);
}
///
diff --git a/src/Oms.Application/Shared/OmsDocHelper.cs b/src/Oms.Application/Shared/OmsDocHelper.cs
index 15d56d8..c36b6e9 100644
--- a/src/Oms.Application/Shared/OmsDocHelper.cs
+++ b/src/Oms.Application/Shared/OmsDocHelper.cs
@@ -177,6 +177,30 @@ public static class OmsDocHelper
return SerializeSnapshotJson(DeserializeChannelPriceSnapshot(raw)) ?? string.Empty;
}
+ ///
+ /// 发布同步态哈希:canonical + 去掉 PackVersion/AreaName 等钉包展示字段。
+ ///
+ 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;
+ }
+
///
/// 解析渠道买价发布 Content,供列表/对比 API 使用(兼容旧快照审计字段)。
///
diff --git a/src/Oms.Application/Shared/PriceAreaSnapshotHelper.cs b/src/Oms.Application/Shared/PriceAreaSnapshotHelper.cs
index 0f99dfa..70535aa 100644
--- a/src/Oms.Application/Shared/PriceAreaSnapshotHelper.cs
+++ b/src/Oms.Application/Shared/PriceAreaSnapshotHelper.cs
@@ -46,6 +46,46 @@ public static class PriceAreaSnapshotHelper
cancellationToken));
}
+ AssignPriceAreaRefs(snapshot, refs);
+ }
+
+ ///
+ /// 同步态:解析价区 ContentHash / 已有包 Version,不 Ensure 新包。
+ ///
+ public static async Task ApplyPriceAreaRefsForSyncAsync(
+ SalesProductPricePublishSnapshotDto snapshot,
+ SalesProduct product,
+ IKpiAreaRepository kpiAreaRepository,
+ KpiAreaPackService packService,
+ CancellationToken cancellationToken = default)
+ {
+ var areaIds = await CollectResolvedPriceAreaIdsAsync(
+ product,
+ snapshot,
+ kpiAreaRepository,
+ cancellationToken);
+ snapshot.PriceAreaInfos = [];
+ if (areaIds.Count == 0)
+ {
+ snapshot.PriceAreaId = null;
+ snapshot.PriceAreaName = null;
+ snapshot.PriceAreaRefs = [];
+ return;
+ }
+
+ var refs = new List();
+ foreach (var areaId in areaIds)
+ {
+ refs.Add(await packService.ResolvePackRefForSyncAsync(areaId, cancellationToken));
+ }
+
+ AssignPriceAreaRefs(snapshot, refs);
+ }
+
+ private static void AssignPriceAreaRefs(
+ SalesProductPricePublishSnapshotDto snapshot,
+ List refs)
+ {
snapshot.PriceAreaRefs = refs;
if (refs.Count == 1)
{
@@ -161,6 +201,44 @@ public static class PriceAreaSnapshotHelper
cancellationToken));
}
+ AssignChannelPriceAreaRefs(snapshot, refs);
+ }
+
+ ///
+ /// 渠道买价同步态:不 Ensure 新包。
+ ///
+ public static async Task ApplyChannelPriceAreaRefsForSyncAsync(
+ ServiceChannelPricePublishSnapshotDto snapshot,
+ IKpiAreaRepository kpiAreaRepository,
+ KpiAreaPackService packService,
+ CancellationToken cancellationToken = default)
+ {
+ var areaIds = await CollectResolvedChannelPriceAreaIdsAsync(
+ snapshot,
+ kpiAreaRepository,
+ cancellationToken);
+ snapshot.PriceAreaInfos = [];
+ if (areaIds.Count == 0)
+ {
+ snapshot.PriceAreaId = null;
+ snapshot.PriceAreaName = null;
+ snapshot.PriceAreaRefs = [];
+ return;
+ }
+
+ var refs = new List();
+ foreach (var areaId in areaIds)
+ {
+ refs.Add(await packService.ResolvePackRefForSyncAsync(areaId, cancellationToken));
+ }
+
+ AssignChannelPriceAreaRefs(snapshot, refs);
+ }
+
+ private static void AssignChannelPriceAreaRefs(
+ ServiceChannelPricePublishSnapshotDto snapshot,
+ List refs)
+ {
snapshot.PriceAreaRefs = refs;
if (refs.Count == 1)
{
diff --git a/src/Oms.Application/Shared/PublishSnapshotNormalizer.cs b/src/Oms.Application/Shared/PublishSnapshotNormalizer.cs
index e122993..d35cf12 100644
--- a/src/Oms.Application/Shared/PublishSnapshotNormalizer.cs
+++ b/src/Oms.Application/Shared/PublishSnapshotNormalizer.cs
@@ -44,6 +44,61 @@ public static class PublishSnapshotNormalizer
snapshot.Prices = SortChannelPriceItems(snapshot.Prices);
}
+ ///
+ /// 同步态对比:分区包只比 AreaId+ContentHash(PackVersion 是钉包戳,AreaName 是展示名)。
+ ///
+ public static void StripVolatilePackPinsForSyncCompare(SalesProductPricePublishSnapshotDto? snapshot)
+ {
+ if (snapshot is null)
+ {
+ return;
+ }
+
+ snapshot.PriceAreaName = null;
+ foreach (var packRef in snapshot.PriceAreaRefs ?? [])
+ {
+ StripPackPin(packRef);
+ }
+ }
+
+ public static void StripVolatilePackPinsForSyncCompare(ServiceChannelPricePublishSnapshotDto? snapshot)
+ {
+ if (snapshot is null)
+ {
+ return;
+ }
+
+ snapshot.PriceAreaName = null;
+ foreach (var packRef in snapshot.PriceAreaRefs ?? [])
+ {
+ StripPackPin(packRef);
+ }
+ }
+
+ public static void StripVolatilePackPinsForSyncCompare(SalesProductRulesPublishSnapshotDto? snapshot)
+ {
+ if (snapshot is null)
+ {
+ return;
+ }
+
+ snapshot.BlacklistAreaName = null;
+ snapshot.RemoteAreaName = null;
+ StripPackPin(snapshot.BlacklistAreaRef);
+ StripPackPin(snapshot.RemoteAreaRef);
+ }
+
+ private static void StripPackPin(KpiAreaPackRefDto? packRef)
+ {
+ if (packRef is null)
+ {
+ return;
+ }
+
+ packRef.PackVersion = string.Empty;
+ packRef.AreaName = null;
+ }
+
public static void NormalizeKpiAreas(List? areas)
{
if (areas is null)