20261008-ide1.0发布 #1

Merged
zzh merged 42 commits from test into master 2026-10-08 09:18:00 +08:00
5 changed files with 184 additions and 51 deletions
Showing only changes of commit 7d14c0e5d9 - Show all commits

View File

@ -297,6 +297,29 @@ public partial class SalesProductService
return rulesSnapshot; return rulesSnapshot;
} }
/// <summary>
/// 同步态规则快照:分区不 Ensure,其余与发布组装一致。
/// </summary>
private async Task<SalesProductRulesPublishSnapshotDto> 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;
}
/// <summary> /// <summary>
/// 组装价格发布快照(不落库);价区钉分区包 Refs。 /// 组装价格发布快照(不落库);价区钉分区包 Refs。
/// </summary> /// </summary>

View File

@ -410,7 +410,7 @@ public partial class SalesProductService : ApplicationService
} }
/// <summary> /// <summary>
/// 发布同步态(轻量):对比待发布与最新生效快照,不回传正文。 /// 发布同步态:当前配置 vs 最新生效发布 Content。前端只展示 pending,不算差异。
/// </summary> /// </summary>
[HttpGet("/channel/sales-product/publish/sync-status")] [HttpGet("/channel/sales-product/publish/sync-status")]
public async Task<SalesProductPublishSyncStatusOutput> GetPublishSyncStatusAsync( public async Task<SalesProductPublishSyncStatusOutput> GetPublishSyncStatusAsync(
@ -420,48 +420,45 @@ public partial class SalesProductService : ApplicationService
var normalized = NormalizeOperatorType(operatorType); var normalized = NormalizeOperatorType(operatorType);
await GetProductOrThrowAsync(salesProductId); await GetProductOrThrowAsync(salesProductId);
// 待发布:同步态不 Ensure 分区包;对比时忽略 PackVersion/AreaName,只比价格项 + 分区 ContentHash // 当前 = 与发布同源的快照组装(分区只解析哈希,不 Ensure 写库)
object pendingSnapshot = normalized == PriceOperatorType object currentSnapshot = normalized == PriceOperatorType
? await BuildPricePublishSnapshotForSyncAsync(salesProductId) ? await BuildPricePublishSnapshotForSyncAsync(salesProductId)
: await BuildRulesPublishSnapshotAsync(salesProductId); : await BuildRulesPublishSnapshotForSyncAsync(salesProductId);
var pendingCanon = normalized == PriceOperatorType var currentHash = ComputeContentHash(
? OmsDocHelper.HashSalesProductPriceSnapshotForSync(pendingSnapshot) normalized == PriceOperatorType
: OmsDocHelper.HashSalesProductRulesSnapshotForSync(pendingSnapshot); ? OmsDocHelper.HashSalesProductPriceSnapshotForSync(currentSnapshot)
var pendingHash = ComputeContentHash(pendingCanon); : OmsDocHelper.HashSalesProductRulesSnapshotForSync(currentSnapshot));
var headers = await SalesProductRepository.GetAllPublishRecordHeadersAsync(salesProductId, normalized); // 生效 = 最新 Active 发布记录 Content
var onlineHeader = headers var onlineRecord = await SalesProductRepository.GetEffectivePublishRecordAsync(
.Where(x => x.Status == Domain.Shared.Enums.EsSnapshotStatus.Active) salesProductId,
.OrderByDescending(x => x.PublishTime) normalized,
.FirstOrDefault(); DateTime.Now);
if (onlineRecord == null || string.IsNullOrWhiteSpace(onlineRecord.Content))
if (onlineHeader == null)
{ {
return new SalesProductPublishSyncStatusOutput return new SalesProductPublishSyncStatusOutput
{ {
Pending = true, Pending = true,
OnlineVersion = null, OnlineVersion = null,
OnlinePublishTime = null, OnlinePublishTime = null,
PendingContentHash = pendingHash, PendingContentHash = currentHash,
OnlineContentHash = null OnlineContentHash = null
}; };
} }
var onlineRecord = await SalesProductRepository.GetPublishRecordByIdAsync(onlineHeader.Id); var onlineHash = ComputeContentHash(
var onlineJson = onlineRecord?.Content ?? string.Empty; normalized == PriceOperatorType
var onlineCanon = normalized == PriceOperatorType
? OmsDocHelper.HashSalesProductPriceSnapshotForSync( ? OmsDocHelper.HashSalesProductPriceSnapshotForSync(
OmsDocHelper.DeserializePriceSnapshot(onlineJson)) OmsDocHelper.DeserializePriceSnapshot(onlineRecord.Content))
: OmsDocHelper.HashSalesProductRulesSnapshotForSync( : OmsDocHelper.HashSalesProductRulesSnapshotForSync(
OmsDocHelper.DeserializeRulesSnapshot(onlineJson)); OmsDocHelper.DeserializeRulesSnapshot(onlineRecord.Content)));
var onlineHash = ComputeContentHash(onlineCanon);
return new SalesProductPublishSyncStatusOutput return new SalesProductPublishSyncStatusOutput
{ {
Pending = !string.Equals(pendingHash, onlineHash, StringComparison.Ordinal), Pending = !string.Equals(currentHash, onlineHash, StringComparison.Ordinal),
OnlineVersion = onlineHeader.Version, OnlineVersion = onlineRecord.Version,
OnlinePublishTime = onlineHeader.PublishTime, OnlinePublishTime = onlineRecord.PublishTime,
PendingContentHash = pendingHash, PendingContentHash = currentHash,
OnlineContentHash = onlineHash OnlineContentHash = onlineHash
}; };
} }

View File

@ -325,6 +325,46 @@ public partial class ServiceChannelService
return snapshot; return snapshot;
} }
/// <summary>
/// 渠道规则同步态:偏远分区不 Ensure。
/// </summary>
private async Task<ServiceChannelRulesPublishSnapshotDto> 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() => private string ResolvePublishUser() =>
string.IsNullOrWhiteSpace(CurrentUser.UserName) string.IsNullOrWhiteSpace(CurrentUser.UserName)
? CurrentUser.GetId<int>().ToString() ? CurrentUser.GetId<int>().ToString()

View File

@ -403,7 +403,7 @@ public partial class ServiceChannelService : ApplicationService
} }
/// <summary> /// <summary>
/// 发布同步态(轻量):对比待发布与最新生效快照,不回传正文。与产品卖价对称。 /// 发布同步态:当前配置 vs 最新生效发布 Content。前端只展示 pending。
/// </summary> /// </summary>
[HttpGet("/channel/service-channel/publish/sync-status")] [HttpGet("/channel/service-channel/publish/sync-status")]
public async Task<ServiceChannelPublishSyncStatusOutput> GetPublishSyncStatusAsync( public async Task<ServiceChannelPublishSyncStatusOutput> GetPublishSyncStatusAsync(
@ -413,49 +413,70 @@ public partial class ServiceChannelService : ApplicationService
var normalized = NormalizeOperatorType(operatorType); var normalized = NormalizeOperatorType(operatorType);
await GetEntityOrThrowAsync(serviceChannelId); await GetEntityOrThrowAsync(serviceChannelId);
object pendingSnapshot = normalized == PriceOperatorType string currentCanon;
? await BuildPricePublishSnapshotForSyncAsync(serviceChannelId) if (normalized == PriceOperatorType)
: await BuildRulesPublishSnapshotAsync(serviceChannelId); {
var pendingCanon = normalized == PriceOperatorType var current = await BuildPricePublishSnapshotForSyncAsync(serviceChannelId);
? OmsDocHelper.HashChannelPriceSnapshotForSync(pendingSnapshot) currentCanon = OmsDocHelper.HashChannelPriceSnapshotForSync(current);
: OmsDocHelper.SerializeSnapshotJson(pendingSnapshot) ?? string.Empty; }
var pendingHash = ComputeContentHash(pendingCanon); else
{
var current = await BuildRulesPublishSnapshotForSyncAsync(serviceChannelId);
currentCanon = HashChannelRulesSnapshotForSync(current);
}
var currentHash = ComputeContentHash(currentCanon);
var onlineRecord = await ServiceChannelRepository.GetEffectivePublishRecordAsync( var onlineRecord = await ServiceChannelRepository.GetEffectivePublishRecordAsync(
serviceChannelId, serviceChannelId,
normalized, normalized,
DateTime.Now); DateTime.Now);
if (onlineRecord == null) if (onlineRecord == null || string.IsNullOrWhiteSpace(onlineRecord.Content))
{ {
return new ServiceChannelPublishSyncStatusOutput return new ServiceChannelPublishSyncStatusOutput
{ {
Pending = true, Pending = true,
OnlineVersion = null, OnlineVersion = null,
OnlinePublishTime = null, OnlinePublishTime = null,
PendingContentHash = pendingHash, PendingContentHash = currentHash,
OnlineContentHash = null OnlineContentHash = null
}; };
} }
var onlineJson = onlineRecord.Content ?? string.Empty;
var onlineCanon = normalized == PriceOperatorType var onlineCanon = normalized == PriceOperatorType
? OmsDocHelper.HashChannelPriceSnapshotForSync( ? OmsDocHelper.HashChannelPriceSnapshotForSync(
OmsDocHelper.DeserializeChannelPriceSnapshot(onlineJson)) OmsDocHelper.DeserializeChannelPriceSnapshot(onlineRecord.Content))
: OmsDocHelper.SerializeSnapshotJson( : HashChannelRulesSnapshotForSync(
OmsDocHelper.DeserializeJson<ServiceChannelRulesPublishSnapshotDto>(onlineJson)) OmsDocHelper.DeserializeJson<ServiceChannelRulesPublishSnapshotDto>(onlineRecord.Content));
?? string.Empty;
var onlineHash = ComputeContentHash(onlineCanon); var onlineHash = ComputeContentHash(onlineCanon);
return new ServiceChannelPublishSyncStatusOutput return new ServiceChannelPublishSyncStatusOutput
{ {
Pending = !string.Equals(pendingHash, onlineHash, StringComparison.Ordinal), Pending = !string.Equals(currentHash, onlineHash, StringComparison.Ordinal),
OnlineVersion = onlineRecord.Version, OnlineVersion = onlineRecord.Version,
OnlinePublishTime = onlineRecord.PublishTime, OnlinePublishTime = onlineRecord.PublishTime,
PendingContentHash = pendingHash, PendingContentHash = currentHash,
OnlineContentHash = onlineHash 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) private static string ComputeContentHash(string content)
{ {
var hash = global::System.Security.Cryptography.SHA256.HashData( var hash = global::System.Security.Cryptography.SHA256.HashData(

View File

@ -28,6 +28,7 @@ public static class RulesSnapshotHelper
KpiAreaPackService.AreaTypeBlacklist, KpiAreaPackService.AreaTypeBlacklist,
packService, packService,
publishUser, publishUser,
ensurePack: true,
(s, areaId, areaName, packRef) => (s, areaId, areaName, packRef) =>
{ {
s.BlacklistAreaId = areaId; s.BlacklistAreaId = areaId;
@ -43,6 +44,54 @@ public static class RulesSnapshotHelper
KpiAreaPackService.AreaTypeRemote, KpiAreaPackService.AreaTypeRemote,
packService, packService,
publishUser, 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);
}
/// <summary>
/// 同步态:黑名单/偏远只解析哈希/已有包,不 Ensure 写库。
/// </summary>
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, areaId, areaName, packRef) =>
{ {
s.RemoteAreaId = areaId; s.RemoteAreaId = areaId;
@ -107,6 +156,7 @@ public static class RulesSnapshotHelper
string areaType, string areaType,
KpiAreaPackService packService, KpiAreaPackService packService,
string? publishUser, string? publishUser,
bool ensurePack,
Action<SalesProductRulesPublishSnapshotDto, int?, string?, KpiAreaPackRefDto?> assign, Action<SalesProductRulesPublishSnapshotDto, int?, string?, KpiAreaPackRefDto?> assign,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
@ -116,12 +166,14 @@ public static class RulesSnapshotHelper
return; return;
} }
var packRef = await packService.EnsurePackAsync( var packRef = ensurePack
? await packService.EnsurePackAsync(
areaId.Value, areaId.Value,
areaType, areaType,
publishUser, publishUser,
description: "sales-product-rules-publish", description: "sales-product-rules-publish",
cancellationToken); cancellationToken)
: await packService.ResolvePackRefForSyncAsync(areaId.Value, cancellationToken);
assign(snapshot, areaId, packRef.AreaName, packRef); assign(snapshot, areaId, packRef.AreaName, packRef);
} }