20261008-ide1.0发布 #1

Merged
zzh merged 42 commits from test into master 2026-10-08 09:18:00 +08:00
8 changed files with 266 additions and 40 deletions
Showing only changes of commit 0449a4a3f5 - Show all commits

View File

@ -317,6 +317,25 @@ public partial class SalesProductService
return priceSnapshot;
}
/// <summary>
/// 同步态待发布快照:价区只解析哈希/已有包,不 Ensure 写库。
/// </summary>
private async Task<SalesProductPricePublishSnapshotDto> 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<int>().ToString()

View File

@ -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);

View File

@ -262,6 +262,25 @@ public partial class ServiceChannelService
return snapshot;
}
/// <summary>
/// 买价同步态:不 Ensure 新分区包。
/// </summary>
private async Task<ServiceChannelPricePublishSnapshotDto> 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;
}
/// <summary>
/// 组装渠道规则发布快照(不落库):钉偏远分区包。
/// </summary>

View File

@ -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<ServiceChannelRulesPublishSnapshotDto>(onlineJson))

View File

@ -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);
}
/// <summary>
/// 同步态专用:只解析当前库明细哈希并回填已有包 Version;不落新包(避免 GET sync-status 写库)。
/// </summary>
public async Task<KpiAreaPackRefDto> 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<KpiAreaSnapshotRuleDto> 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);
}
/// <summary>

View File

@ -177,6 +177,30 @@ public static class OmsDocHelper
return SerializeSnapshotJson(DeserializeChannelPriceSnapshot(raw)) ?? string.Empty;
}
/// <summary>
/// 发布同步态哈希:canonical + 去掉 PackVersion/AreaName 等钉包展示字段。
/// </summary>
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;
}
/// <summary>
/// 解析渠道买价发布 Content,供列表/对比 API 使用(兼容旧快照审计字段)。
/// </summary>

View File

@ -46,6 +46,46 @@ public static class PriceAreaSnapshotHelper
cancellationToken));
}
AssignPriceAreaRefs(snapshot, refs);
}
/// <summary>
/// 同步态:解析价区 ContentHash / 已有包 Version,不 Ensure 新包。
/// </summary>
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<KpiAreaPackRefDto>();
foreach (var areaId in areaIds)
{
refs.Add(await packService.ResolvePackRefForSyncAsync(areaId, cancellationToken));
}
AssignPriceAreaRefs(snapshot, refs);
}
private static void AssignPriceAreaRefs(
SalesProductPricePublishSnapshotDto snapshot,
List<KpiAreaPackRefDto> refs)
{
snapshot.PriceAreaRefs = refs;
if (refs.Count == 1)
{
@ -161,6 +201,44 @@ public static class PriceAreaSnapshotHelper
cancellationToken));
}
AssignChannelPriceAreaRefs(snapshot, refs);
}
/// <summary>
/// 渠道买价同步态:不 Ensure 新包。
/// </summary>
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<KpiAreaPackRefDto>();
foreach (var areaId in areaIds)
{
refs.Add(await packService.ResolvePackRefForSyncAsync(areaId, cancellationToken));
}
AssignChannelPriceAreaRefs(snapshot, refs);
}
private static void AssignChannelPriceAreaRefs(
ServiceChannelPricePublishSnapshotDto snapshot,
List<KpiAreaPackRefDto> refs)
{
snapshot.PriceAreaRefs = refs;
if (refs.Count == 1)
{

View File

@ -44,6 +44,61 @@ public static class PublishSnapshotNormalizer
snapshot.Prices = SortChannelPriceItems(snapshot.Prices);
}
/// <summary>
/// 同步态对比:分区包只比 AreaId+ContentHash(PackVersion 是钉包戳,AreaName 是展示名)。
/// </summary>
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<KpiAreaSnapshotRuleDto>? areas)
{
if (areas is null)