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)