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;
}
/// <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>
/// 组装价格发布快照(不落库);价区钉分区包 Refs。
/// </summary>

View File

@ -410,7 +410,7 @@ public partial class SalesProductService : ApplicationService
}
/// <summary>
/// 发布同步态(轻量):对比待发布与最新生效快照,不回传正文。
/// 发布同步态:当前配置 vs 最新生效发布 Content。前端只展示 pending,不算差异。
/// </summary>
[HttpGet("/channel/sales-product/publish/sync-status")]
public async Task<SalesProductPublishSyncStatusOutput> 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
var onlineHash = ComputeContentHash(
normalized == PriceOperatorType
? OmsDocHelper.HashSalesProductPriceSnapshotForSync(
OmsDocHelper.DeserializePriceSnapshot(onlineJson))
OmsDocHelper.DeserializePriceSnapshot(onlineRecord.Content))
: OmsDocHelper.HashSalesProductRulesSnapshotForSync(
OmsDocHelper.DeserializeRulesSnapshot(onlineJson));
var onlineHash = ComputeContentHash(onlineCanon);
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
};
}

View File

@ -325,6 +325,46 @@ public partial class ServiceChannelService
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() =>
string.IsNullOrWhiteSpace(CurrentUser.UserName)
? CurrentUser.GetId<int>().ToString()

View File

@ -403,7 +403,7 @@ public partial class ServiceChannelService : ApplicationService
}
/// <summary>
/// 发布同步态(轻量):对比待发布与最新生效快照,不回传正文。与产品卖价对称。
/// 发布同步态:当前配置 vs 最新生效发布 Content。前端只展示 pending。
/// </summary>
[HttpGet("/channel/service-channel/publish/sync-status")]
public async Task<ServiceChannelPublishSyncStatusOutput> 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<ServiceChannelRulesPublishSnapshotDto>(onlineJson))
?? string.Empty;
OmsDocHelper.DeserializeChannelPriceSnapshot(onlineRecord.Content))
: HashChannelRulesSnapshotForSync(
OmsDocHelper.DeserializeJson<ServiceChannelRulesPublishSnapshotDto>(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(

View File

@ -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);
}
/// <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.RemoteAreaId = areaId;
@ -107,6 +156,7 @@ public static class RulesSnapshotHelper
string areaType,
KpiAreaPackService packService,
string? publishUser,
bool ensurePack,
Action<SalesProductRulesPublishSnapshotDto, int?, string?, KpiAreaPackRefDto?> assign,
CancellationToken cancellationToken)
{
@ -116,12 +166,14 @@ public static class RulesSnapshotHelper
return;
}
var packRef = await packService.EnsurePackAsync(
var packRef = ensurePack
? await packService.EnsurePackAsync(
areaId.Value,
areaType,
publishUser,
description: "sales-product-rules-publish",
cancellationToken);
cancellationToken)
: await packService.ResolvePackRefForSyncAsync(areaId.Value, cancellationToken);
assign(snapshot, areaId, packRef.AreaName, packRef);
}