diff --git a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs index fff4f66..d7bc155 100644 --- a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs +++ b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs @@ -297,6 +297,29 @@ public partial class SalesProductService return rulesSnapshot; } + /// + /// 同步态规则快照:分区不 Ensure,其余与发布组装一致。 + /// + private async Task BuildRulesPublishSnapshotForSyncAsync(int salesProductId) + { + var product = await GetProductOrThrowAsync(salesProductId); + var rulesSnapshot = new SalesProductRulesPublishSnapshot + { + Product = MapProductToRulesSaveInput(product), + Providers = (await GetAllocationCoreAsync(salesProductId)).Providers + .Select(PublishSnapshotSlimmer.SlimAllocationProvider) + .ToList() + }; + await RulesSnapshotHelper.ApplyRulesToSnapshotForSyncAsync( + rulesSnapshot, + product, + WeightRuleRepository, + WeightRoundingRepository, + KpiAreaPackService); + await AllocationProviderSnapshotEnricher.EnrichAsync(rulesSnapshot.Providers); + return rulesSnapshot; + } + /// /// 组装价格发布快照(不落库);价区钉分区包 Refs。 /// diff --git a/src/Oms.Application/Services/Channels/SalesProductService.cs b/src/Oms.Application/Services/Channels/SalesProductService.cs index c89d3b4..bba0e20 100644 --- a/src/Oms.Application/Services/Channels/SalesProductService.cs +++ b/src/Oms.Application/Services/Channels/SalesProductService.cs @@ -410,7 +410,7 @@ public partial class SalesProductService : ApplicationService } /// - /// 发布同步态(轻量):对比待发布与最新生效快照,不回传正文。 + /// 发布同步态:当前配置 vs 最新生效发布 Content。前端只展示 pending,不算差异。 /// [HttpGet("/channel/sales-product/publish/sync-status")] public async Task GetPublishSyncStatusAsync( @@ -420,48 +420,45 @@ public partial class SalesProductService : ApplicationService var normalized = NormalizeOperatorType(operatorType); await GetProductOrThrowAsync(salesProductId); - // 待发布:同步态不 Ensure 分区包;对比时忽略 PackVersion/AreaName,只比价格项 + 分区 ContentHash - object pendingSnapshot = normalized == PriceOperatorType + // 当前 = 与发布同源的快照组装(分区只解析哈希,不 Ensure 写库) + object currentSnapshot = normalized == PriceOperatorType ? await BuildPricePublishSnapshotForSyncAsync(salesProductId) - : await BuildRulesPublishSnapshotAsync(salesProductId); - var pendingCanon = normalized == PriceOperatorType - ? OmsDocHelper.HashSalesProductPriceSnapshotForSync(pendingSnapshot) - : OmsDocHelper.HashSalesProductRulesSnapshotForSync(pendingSnapshot); - var pendingHash = ComputeContentHash(pendingCanon); + : await BuildRulesPublishSnapshotForSyncAsync(salesProductId); + var currentHash = ComputeContentHash( + normalized == PriceOperatorType + ? OmsDocHelper.HashSalesProductPriceSnapshotForSync(currentSnapshot) + : OmsDocHelper.HashSalesProductRulesSnapshotForSync(currentSnapshot)); - var headers = await SalesProductRepository.GetAllPublishRecordHeadersAsync(salesProductId, normalized); - var onlineHeader = headers - .Where(x => x.Status == Domain.Shared.Enums.EsSnapshotStatus.Active) - .OrderByDescending(x => x.PublishTime) - .FirstOrDefault(); - - if (onlineHeader == null) + // 生效 = 最新 Active 发布记录 Content + var onlineRecord = await SalesProductRepository.GetEffectivePublishRecordAsync( + salesProductId, + normalized, + DateTime.Now); + if (onlineRecord == null || string.IsNullOrWhiteSpace(onlineRecord.Content)) { return new SalesProductPublishSyncStatusOutput { Pending = true, OnlineVersion = null, OnlinePublishTime = null, - PendingContentHash = pendingHash, + PendingContentHash = currentHash, OnlineContentHash = null }; } - var onlineRecord = await SalesProductRepository.GetPublishRecordByIdAsync(onlineHeader.Id); - var onlineJson = onlineRecord?.Content ?? string.Empty; - var onlineCanon = normalized == PriceOperatorType - ? OmsDocHelper.HashSalesProductPriceSnapshotForSync( - OmsDocHelper.DeserializePriceSnapshot(onlineJson)) - : OmsDocHelper.HashSalesProductRulesSnapshotForSync( - OmsDocHelper.DeserializeRulesSnapshot(onlineJson)); - var onlineHash = ComputeContentHash(onlineCanon); + var onlineHash = ComputeContentHash( + normalized == PriceOperatorType + ? OmsDocHelper.HashSalesProductPriceSnapshotForSync( + OmsDocHelper.DeserializePriceSnapshot(onlineRecord.Content)) + : OmsDocHelper.HashSalesProductRulesSnapshotForSync( + OmsDocHelper.DeserializeRulesSnapshot(onlineRecord.Content))); return new SalesProductPublishSyncStatusOutput { - Pending = !string.Equals(pendingHash, onlineHash, StringComparison.Ordinal), - OnlineVersion = onlineHeader.Version, - OnlinePublishTime = onlineHeader.PublishTime, - PendingContentHash = pendingHash, + Pending = !string.Equals(currentHash, onlineHash, StringComparison.Ordinal), + OnlineVersion = onlineRecord.Version, + OnlinePublishTime = onlineRecord.PublishTime, + PendingContentHash = currentHash, OnlineContentHash = onlineHash }; } diff --git a/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs b/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs index 9f29b18..b986819 100644 --- a/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs +++ b/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs @@ -325,6 +325,46 @@ public partial class ServiceChannelService return snapshot; } + /// + /// 渠道规则同步态:偏远分区不 Ensure。 + /// + private async Task BuildRulesPublishSnapshotForSyncAsync(int serviceChannelId) + { + var entity = await GetEntityOrThrowAsync(serviceChannelId); + ServiceChannelDownstreamSnapshotDto? downstream = null; + if (entity.GroupId is > 0) + { + var group = await ServiceChannelGroupRepository.FirstOrDefaultAsync(x => x.Id == entity.GroupId.Value); + if (group != null) + { + downstream = await AllocationProviderSnapshotEnricher.BuildDownstreamFromGroupAsync(group); + } + } + + var snapshot = new ServiceChannelRulesPublishSnapshotDto + { + Channel = MapChannelToSaveInput(entity), + Downstream = downstream, + RemoteAreaInfos = [] + }; + + if (entity.RemoteAreaId is > 0) + { + var packRef = await KpiAreaPackService.ResolvePackRefForSyncAsync(entity.RemoteAreaId.Value); + snapshot.RemoteAreaId = entity.RemoteAreaId; + snapshot.RemoteAreaName = packRef.AreaName; + snapshot.RemoteAreaRef = packRef; + } + else + { + snapshot.RemoteAreaId = null; + snapshot.RemoteAreaName = null; + snapshot.RemoteAreaRef = null; + } + + return snapshot; + } + private string ResolvePublishUser() => string.IsNullOrWhiteSpace(CurrentUser.UserName) ? CurrentUser.GetId().ToString() diff --git a/src/Oms.Application/Services/Channels/ServiceChannelService.cs b/src/Oms.Application/Services/Channels/ServiceChannelService.cs index 526b607..4e8336d 100644 --- a/src/Oms.Application/Services/Channels/ServiceChannelService.cs +++ b/src/Oms.Application/Services/Channels/ServiceChannelService.cs @@ -403,7 +403,7 @@ public partial class ServiceChannelService : ApplicationService } /// - /// 发布同步态(轻量):对比待发布与最新生效快照,不回传正文。与产品卖价对称。 + /// 发布同步态:当前配置 vs 最新生效发布 Content。前端只展示 pending。 /// [HttpGet("/channel/service-channel/publish/sync-status")] public async Task GetPublishSyncStatusAsync( @@ -413,49 +413,70 @@ public partial class ServiceChannelService : ApplicationService var normalized = NormalizeOperatorType(operatorType); await GetEntityOrThrowAsync(serviceChannelId); - object pendingSnapshot = normalized == PriceOperatorType - ? await BuildPricePublishSnapshotForSyncAsync(serviceChannelId) - : await BuildRulesPublishSnapshotAsync(serviceChannelId); - var pendingCanon = normalized == PriceOperatorType - ? OmsDocHelper.HashChannelPriceSnapshotForSync(pendingSnapshot) - : OmsDocHelper.SerializeSnapshotJson(pendingSnapshot) ?? string.Empty; - var pendingHash = ComputeContentHash(pendingCanon); + string currentCanon; + if (normalized == PriceOperatorType) + { + var current = await BuildPricePublishSnapshotForSyncAsync(serviceChannelId); + currentCanon = OmsDocHelper.HashChannelPriceSnapshotForSync(current); + } + else + { + var current = await BuildRulesPublishSnapshotForSyncAsync(serviceChannelId); + currentCanon = HashChannelRulesSnapshotForSync(current); + } + + var currentHash = ComputeContentHash(currentCanon); var onlineRecord = await ServiceChannelRepository.GetEffectivePublishRecordAsync( serviceChannelId, normalized, DateTime.Now); - if (onlineRecord == null) + if (onlineRecord == null || string.IsNullOrWhiteSpace(onlineRecord.Content)) { return new ServiceChannelPublishSyncStatusOutput { Pending = true, OnlineVersion = null, OnlinePublishTime = null, - PendingContentHash = pendingHash, + PendingContentHash = currentHash, OnlineContentHash = null }; } - var onlineJson = onlineRecord.Content ?? string.Empty; var onlineCanon = normalized == PriceOperatorType ? OmsDocHelper.HashChannelPriceSnapshotForSync( - OmsDocHelper.DeserializeChannelPriceSnapshot(onlineJson)) - : OmsDocHelper.SerializeSnapshotJson( - OmsDocHelper.DeserializeJson(onlineJson)) - ?? string.Empty; + OmsDocHelper.DeserializeChannelPriceSnapshot(onlineRecord.Content)) + : HashChannelRulesSnapshotForSync( + OmsDocHelper.DeserializeJson(onlineRecord.Content)); var onlineHash = ComputeContentHash(onlineCanon); return new ServiceChannelPublishSyncStatusOutput { - Pending = !string.Equals(pendingHash, onlineHash, StringComparison.Ordinal), + Pending = !string.Equals(currentHash, onlineHash, StringComparison.Ordinal), OnlineVersion = onlineRecord.Version, OnlinePublishTime = onlineRecord.PublishTime, - PendingContentHash = pendingHash, + PendingContentHash = currentHash, OnlineContentHash = onlineHash }; } + private static string HashChannelRulesSnapshotForSync(ServiceChannelRulesPublishSnapshotDto? snapshot) + { + if (snapshot == null) + { + return string.Empty; + } + + snapshot.RemoteAreaName = null; + if (snapshot.RemoteAreaRef != null) + { + snapshot.RemoteAreaRef.PackVersion = string.Empty; + snapshot.RemoteAreaRef.AreaName = null; + } + + return OmsDocHelper.SerializeSnapshotJson(snapshot) ?? string.Empty; + } + private static string ComputeContentHash(string content) { var hash = global::System.Security.Cryptography.SHA256.HashData( diff --git a/src/Oms.Application/Shared/RulesSnapshotHelper.cs b/src/Oms.Application/Shared/RulesSnapshotHelper.cs index 5d64b2b..62c5154 100644 --- a/src/Oms.Application/Shared/RulesSnapshotHelper.cs +++ b/src/Oms.Application/Shared/RulesSnapshotHelper.cs @@ -28,6 +28,7 @@ public static class RulesSnapshotHelper KpiAreaPackService.AreaTypeBlacklist, packService, publishUser, + ensurePack: true, (s, areaId, areaName, packRef) => { s.BlacklistAreaId = areaId; @@ -43,6 +44,54 @@ public static class RulesSnapshotHelper KpiAreaPackService.AreaTypeRemote, packService, publishUser, + ensurePack: true, + (s, areaId, areaName, packRef) => + { + s.RemoteAreaId = areaId; + s.RemoteAreaName = areaName; + s.RemoteAreaRef = packRef; + s.RemoteAreaInfos = []; + }, + cancellationToken); + + await ApplyWeightRuleToSnapshotAsync(snapshot, product, weightRuleRepository, cancellationToken); + await ApplyWeightRoundingToSnapshotAsync(snapshot, product, weightRoundingRepository, cancellationToken); + } + + /// + /// 同步态:黑名单/偏远只解析哈希/已有包,不 Ensure 写库。 + /// + public static async Task ApplyRulesToSnapshotForSyncAsync( + SalesProductRulesPublishSnapshotDto snapshot, + SalesProduct product, + IWeightRuleRepository weightRuleRepository, + IWeightRoundingRepository weightRoundingRepository, + KpiAreaPackService packService, + CancellationToken cancellationToken = default) + { + await ApplyKpiAreaPackRefAsync( + snapshot, + product.BlacklistAreaId, + KpiAreaPackService.AreaTypeBlacklist, + packService, + publishUser: null, + ensurePack: false, + (s, areaId, areaName, packRef) => + { + s.BlacklistAreaId = areaId; + s.BlacklistAreaName = areaName; + s.BlacklistAreaRef = packRef; + s.BlacklistAreaInfos = []; + }, + cancellationToken); + + await ApplyKpiAreaPackRefAsync( + snapshot, + product.RemoteAreaId, + KpiAreaPackService.AreaTypeRemote, + packService, + publishUser: null, + ensurePack: false, (s, areaId, areaName, packRef) => { s.RemoteAreaId = areaId; @@ -107,6 +156,7 @@ public static class RulesSnapshotHelper string areaType, KpiAreaPackService packService, string? publishUser, + bool ensurePack, Action assign, CancellationToken cancellationToken) { @@ -116,12 +166,14 @@ public static class RulesSnapshotHelper return; } - var packRef = await packService.EnsurePackAsync( - areaId.Value, - areaType, - publishUser, - description: "sales-product-rules-publish", - cancellationToken); + var packRef = ensurePack + ? await packService.EnsurePackAsync( + areaId.Value, + areaType, + publishUser, + description: "sales-product-rules-publish", + cancellationToken) + : await packService.ResolvePackRefForSyncAsync(areaId.Value, cancellationToken); assign(snapshot, areaId, packRef.AreaName, packRef); }