From 6faadcf2a10db1fa58ec8fee57933fae75000820 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Wed, 7 Oct 2026 10:57:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8D=96=E4=BB=B7=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=A4=9A=E4=BB=B7=E6=A0=BC=E5=88=86=E5=8C=BA=E8=A1=A8=EF=BC=8C?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E8=BF=94=E5=9B=9E=E5=88=9B=E5=BB=BA/?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 合并多 Area 分区规则进发布快照,计费用规则自身 AreaId 匹配;产品与渠道列表补 CreateBy/UpdateBy。 Co-authored-by: Cursor --- .../Dtos/Channels/SalesProductListOutput.cs | 10 ++ .../Dtos/Channels/ServiceChannelListOutput.cs | 10 ++ .../Dtos/Orders/PublishSnapshotRuleDtos.cs | 6 + .../Channels/SalesProductService.Helpers.cs | 9 +- .../Services/Channels/SalesProductService.cs | 7 +- .../Channels/ServiceChannelService.Helpers.cs | 9 +- .../Channels/ServiceChannelService.cs | 4 +- .../Orders/ParcelBillingCalculator.cs | 13 +- .../Shared/KpiAreaSnapshotMapper.cs | 4 +- .../Shared/PriceAreaSnapshotHelper.cs | 161 +++++++++--------- .../Shared/RulesSnapshotHelper.cs | 2 +- 11 files changed, 144 insertions(+), 91 deletions(-) diff --git a/src/Oms.Application/Dtos/Channels/SalesProductListOutput.cs b/src/Oms.Application/Dtos/Channels/SalesProductListOutput.cs index 3743ba9..9719866 100644 --- a/src/Oms.Application/Dtos/Channels/SalesProductListOutput.cs +++ b/src/Oms.Application/Dtos/Channels/SalesProductListOutput.cs @@ -100,8 +100,18 @@ public class SalesProductListOutput /// public DateTime CreateTime { get; set; } + /// + /// 创建人展示名。 + /// + public string? CreateBy { get; set; } + /// /// 更新时间。 /// public DateTime? UpdateTime { get; set; } + + /// + /// 修改人展示名。 + /// + public string? UpdateBy { get; set; } } diff --git a/src/Oms.Application/Dtos/Channels/ServiceChannelListOutput.cs b/src/Oms.Application/Dtos/Channels/ServiceChannelListOutput.cs index 546f298..e491347 100644 --- a/src/Oms.Application/Dtos/Channels/ServiceChannelListOutput.cs +++ b/src/Oms.Application/Dtos/Channels/ServiceChannelListOutput.cs @@ -70,8 +70,18 @@ public class ServiceChannelListOutput /// public DateTime CreateTime { get; set; } + /// + /// 创建人展示名。 + /// + public string? CreateBy { get; set; } + /// /// 更新时间。 /// public DateTime? UpdateTime { get; set; } + + /// + /// 修改人展示名。 + /// + public string? UpdateBy { get; set; } } diff --git a/src/Oms.Application/Dtos/Orders/PublishSnapshotRuleDtos.cs b/src/Oms.Application/Dtos/Orders/PublishSnapshotRuleDtos.cs index eb347f2..b02e6bc 100644 --- a/src/Oms.Application/Dtos/Orders/PublishSnapshotRuleDtos.cs +++ b/src/Oms.Application/Dtos/Orders/PublishSnapshotRuleDtos.cs @@ -11,6 +11,12 @@ public class KpiAreaSnapshotRuleDto { public int Id { get; set; } + /// 所属 KPI 分区表主键(多价格分区表合并快照时用于生成 zone 键)。 + public int AreaId { get; set; } + + /// 所属分区表名称(可选,便于展示与 zone 键兼容)。 + public string? AreaName { get; set; } + public string? CountryCode { get; set; } public string? Province { get; set; } diff --git a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs index 4deba9e..2f8f431 100644 --- a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs +++ b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs @@ -282,7 +282,6 @@ public partial class SalesProductService var priceSnapshot = await BuildPricePublishSnapshotAsync(salesProductId); ValidateBillingPriceItemRules( priceSnapshot.Prices.Select(p => MapPriceSaveInput(p, salesProductId)).ToList()); - await PriceAreaSnapshotHelper.EnsureSingleResolvedPriceAreaAsync(priceSnapshot, KpiAreaRepository); snapshot = priceSnapshot; } else @@ -519,7 +518,9 @@ public partial class SalesProductService return normalized.Count == 0 ? null : OmsDocHelper.SerializeJson(normalized); } - private static SalesProductListOutput MapListOutput(SalesProduct entity) + private static SalesProductListOutput MapListOutput( + SalesProduct entity, + IReadOnlyDictionary auditUserNameMap) { return new SalesProductListOutput { @@ -542,7 +543,9 @@ public partial class SalesProductService IsActive = (byte)entity.IsActive, Scheme = entity.Scheme, CreateTime = entity.CreateTime, - UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime) + CreateBy = OmsRbacUserDisplayNameResolver.FormatUserId(entity.CreateUserId, auditUserNameMap), + UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime), + UpdateBy = OmsRbacUserDisplayNameResolver.FormatUserId(entity.UpdateUserId, auditUserNameMap) }; } diff --git a/src/Oms.Application/Services/Channels/SalesProductService.cs b/src/Oms.Application/Services/Channels/SalesProductService.cs index 5f75ce2..03cd705 100644 --- a/src/Oms.Application/Services/Channels/SalesProductService.cs +++ b/src/Oms.Application/Services/Channels/SalesProductService.cs @@ -45,9 +45,11 @@ public partial class SalesProductService : ApplicationService pageSize, new Dictionary { [nameof(SalesProduct.UpdateTime)] = false, [nameof(SalesProduct.Id)] = false }); var total = await SalesProductRepository.CountAsync(predicate); + var auditUserNameMap = await UserDisplayNameResolver.ResolveManyAsync( + entities.SelectMany(x => new[] { x.CreateUserId, x.UpdateUserId })); return new OmsPageResult { - Items = entities.Select(MapListOutput).ToList(), + Items = entities.Select(entity => MapListOutput(entity, auditUserNameMap)).ToList(), Total = total }; } @@ -268,9 +270,6 @@ public partial class SalesProductService : ApplicationService { await GetProductOrThrowAsync(input.SalesProductId); ValidateBillingPriceItemRules(input.Prices); - await PriceAreaSnapshotHelper.EnsureSingleResolvedPriceAreaAsync( - BuildPriceSnapshotFromSaveInputs(input.SalesProductId, input.Prices), - KpiAreaRepository); var prices = new List(); var factors = new List(); var details = new List(); diff --git a/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs b/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs index eb303e8..e92c099 100644 --- a/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs +++ b/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs @@ -416,7 +416,10 @@ public partial class ServiceChannelService }; } - private static ServiceChannelListOutput MapListOutput(ServiceChannel entity, IReadOnlyDictionary groupMap) + private static ServiceChannelListOutput MapListOutput( + ServiceChannel entity, + IReadOnlyDictionary groupMap, + IReadOnlyDictionary auditUserNameMap) { return new ServiceChannelListOutput { @@ -433,7 +436,9 @@ public partial class ServiceChannelService RemoteAreaId = entity.RemoteAreaId, IsActive = (byte)entity.IsActive, CreateTime = entity.CreateTime, - UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime) + CreateBy = OmsRbacUserDisplayNameResolver.FormatUserId(entity.CreateUserId, auditUserNameMap), + UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime), + UpdateBy = OmsRbacUserDisplayNameResolver.FormatUserId(entity.UpdateUserId, auditUserNameMap) }; } diff --git a/src/Oms.Application/Services/Channels/ServiceChannelService.cs b/src/Oms.Application/Services/Channels/ServiceChannelService.cs index 061ab28..ade005a 100644 --- a/src/Oms.Application/Services/Channels/ServiceChannelService.cs +++ b/src/Oms.Application/Services/Channels/ServiceChannelService.cs @@ -43,9 +43,11 @@ public partial class ServiceChannelService : ApplicationService new Dictionary { [nameof(ServiceChannel.UpdateTime)] = false, [nameof(ServiceChannel.Id)] = false }); var total = await ServiceChannelRepository.CountAsync(predicate); var groupMap = await GetGroupMapAsync(); + var auditUserNameMap = await UserDisplayNameResolver.ResolveManyAsync( + entities.SelectMany(x => new[] { x.CreateUserId, x.UpdateUserId })); return new OmsPageResult { - Items = entities.Select(entity => MapListOutput(entity, groupMap)).ToList(), + Items = entities.Select(entity => MapListOutput(entity, groupMap, auditUserNameMap)).ToList(), Total = total }; } diff --git a/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs b/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs index c408031..5c5762c 100644 --- a/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs +++ b/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs @@ -125,7 +125,12 @@ internal static class ParcelBillingCalculator return null; } - var keys = BuildPriceZoneKeys(matched, priceSnapshot.PriceAreaName, priceSnapshot.PriceAreaId); + // 多价格分区表时优先用规则自身 AreaId,避免落到快照单一 PriceAreaId + var areaId = matched.AreaId > 0 ? matched.AreaId : priceSnapshot.PriceAreaId; + var areaName = !string.IsNullOrWhiteSpace(matched.AreaName) + ? matched.AreaName + : priceSnapshot.PriceAreaName; + var keys = BuildPriceZoneKeys(matched, areaName, areaId); return keys.Count > 0 ? keys : null; } @@ -155,7 +160,11 @@ internal static class ParcelBillingCalculator var allowed = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var rule in countryRules) { - foreach (var key in BuildPriceZoneKeys(rule, priceSnapshot.PriceAreaName, priceSnapshot.PriceAreaId)) + var areaId = rule.AreaId > 0 ? rule.AreaId : priceSnapshot.PriceAreaId; + var areaName = !string.IsNullOrWhiteSpace(rule.AreaName) + ? rule.AreaName + : priceSnapshot.PriceAreaName; + foreach (var key in BuildPriceZoneKeys(rule, areaName, areaId)) { if (!string.IsNullOrWhiteSpace(key)) { diff --git a/src/Oms.Application/Shared/KpiAreaSnapshotMapper.cs b/src/Oms.Application/Shared/KpiAreaSnapshotMapper.cs index 8644700..5d5a131 100644 --- a/src/Oms.Application/Shared/KpiAreaSnapshotMapper.cs +++ b/src/Oms.Application/Shared/KpiAreaSnapshotMapper.cs @@ -8,10 +8,12 @@ namespace Oms.Application.Shared; /// public static class KpiAreaSnapshotMapper { - public static KpiAreaSnapshotRuleDto MapToSnapshotRule(KpiAreaInfo entity) => + public static KpiAreaSnapshotRuleDto MapToSnapshotRule(KpiAreaInfo entity, string? areaName = null) => new() { Id = entity.Id, + AreaId = entity.AreaId, + AreaName = areaName, CountryCode = entity.CountryCode, Province = entity.Province, City = entity.City, diff --git a/src/Oms.Application/Shared/PriceAreaSnapshotHelper.cs b/src/Oms.Application/Shared/PriceAreaSnapshotHelper.cs index 3cd3f06..67d560e 100644 --- a/src/Oms.Application/Shared/PriceAreaSnapshotHelper.cs +++ b/src/Oms.Application/Shared/PriceAreaSnapshotHelper.cs @@ -1,5 +1,3 @@ -using Fake.Application; -using Oms.Application.Dtos.Channels; using Oms.Application.Dtos.Orders; using Oms.Application.Services.Orders; using Oms.Domain.ChannelsAggregate.Entities; @@ -9,11 +7,12 @@ namespace Oms.Application.Shared; /// /// 将价格分区(t_kpi_area + t_kpi_area_info)固化进卖价发布快照。 +/// 支持卖价明细引用多张价格分区表:合并各表明细规则写入快照。 /// public static class PriceAreaSnapshotHelper { /// - /// 按产品 PriceAreaId(或从卖价明细分区推断)加载分区头与明细写入快照。 + /// 按产品 PriceAreaId(或从卖价明细分区推断的全部 AreaId)加载分区头与明细写入快照。 /// public static async Task ApplyPriceAreaToSnapshotAsync( SalesProductPricePublishSnapshotDto snapshot, @@ -21,14 +20,12 @@ public static class PriceAreaSnapshotHelper IKpiAreaRepository kpiAreaRepository, CancellationToken cancellationToken = default) { - await EnsureSingleResolvedPriceAreaAsync(snapshot, kpiAreaRepository, cancellationToken); - - var priceAreaId = await ResolvePriceAreaIdAsync( + var areaIds = await CollectResolvedPriceAreaIdsAsync( product, snapshot, kpiAreaRepository, cancellationToken); - if (!priceAreaId.HasValue) + if (areaIds.Count == 0) { snapshot.PriceAreaId = null; snapshot.PriceAreaName = null; @@ -36,84 +33,94 @@ public static class PriceAreaSnapshotHelper return; } - snapshot.PriceAreaId = priceAreaId; - var area = await kpiAreaRepository.FirstOrDefaultAsync(x => x.Id == priceAreaId.Value, cancellationToken); - snapshot.PriceAreaName = area?.Name; - var infos = await kpiAreaRepository.GetAllInfosByAreaIdAsync(priceAreaId.Value, cancellationToken); - snapshot.PriceAreaInfos = infos.Select(KpiAreaSnapshotMapper.MapToSnapshotRule).ToList(); + var mergedInfos = new List(); + var seenInfoIds = new HashSet(); + var areaNames = new List(); + + foreach (var areaId in areaIds) + { + var area = await kpiAreaRepository.FirstOrDefaultAsync(x => x.Id == areaId, cancellationToken); + var areaName = area?.Name; + if (!string.IsNullOrWhiteSpace(areaName)) + { + areaNames.Add(areaName.Trim()); + } + + var infos = await kpiAreaRepository.GetAllInfosByAreaIdAsync(areaId, cancellationToken); + foreach (var info in infos) + { + if (!seenInfoIds.Add(info.Id)) + { + continue; + } + + mergedInfos.Add(KpiAreaSnapshotMapper.MapToSnapshotRule(info, areaName)); + } + } + + snapshot.PriceAreaInfos = mergedInfos; + if (areaIds.Count == 1) + { + snapshot.PriceAreaId = areaIds[0]; + snapshot.PriceAreaName = areaNames.Count > 0 ? areaNames[0] : null; + } + else + { + // 多表:不再落单一 PriceAreaId,名称拼接便于人工查看 + snapshot.PriceAreaId = null; + snapshot.PriceAreaName = areaNames.Count > 0 ? string.Join("、", areaNames) : null; + } } + /// + /// 收集卖价明细(及产品 PriceAreaId)可解析到的全部价格分区表 Id,去重保序。 + /// + internal static async Task> CollectResolvedPriceAreaIdsAsync( + SalesProduct product, + SalesProductPricePublishSnapshotDto snapshot, + IKpiAreaRepository kpiAreaRepository, + CancellationToken cancellationToken = default) + { + var result = new List(); + var seen = new HashSet(); + + void Add(int? areaId) + { + if (areaId is > 0 && seen.Add(areaId.Value)) + { + result.Add(areaId.Value); + } + } + + if (product.PriceAreaId is > 0) + { + Add(product.PriceAreaId); + } + + var zoneKeys = ParcelBillingCalculator.CollectDistinctDetailZoneKeys(snapshot); + foreach (var zoneKey in zoneKeys) + { + Add(await TryResolveZoneKeyToAreaIdAsync(zoneKey, kpiAreaRepository, cancellationToken)); + } + + return result; + } + + /// + /// 兼容旧调用:仅返回第一个可解析 AreaId(多表场景请用 CollectResolvedPriceAreaIdsAsync)。 + /// internal static async Task ResolvePriceAreaIdAsync( SalesProduct product, SalesProductPricePublishSnapshotDto snapshot, IKpiAreaRepository kpiAreaRepository, CancellationToken cancellationToken = default) { - if (product.PriceAreaId is > 0) - { - return product.PriceAreaId; - } - - var zoneKeys = ParcelBillingCalculator.CollectDistinctDetailZoneKeys(snapshot); - foreach (var zoneKey in zoneKeys) - { - if (!int.TryParse(zoneKey.Trim(), out var zoneId) || zoneId <= 0) - { - continue; - } - - var area = await kpiAreaRepository.FirstOrDefaultAsync(x => x.Id == zoneId, cancellationToken); - if (area != null) - { - return zoneId; - } - - var info = await kpiAreaRepository.GetInfoByIdAsync(zoneId, cancellationToken); - if (info != null) - { - return info.AreaId; - } - } - - return null; - } - - /// - /// 校验卖价明细中可解析的分区因子键均指向同一 AreaId;混用多个分区表时抛业务异常。 - /// 非数字/无法解析的分区名(如「美西」)跳过,与 ResolvePriceAreaIdAsync 一致。 - /// - public static async Task EnsureSingleResolvedPriceAreaAsync( - SalesProductPricePublishSnapshotDto snapshot, - IKpiAreaRepository kpiAreaRepository, - CancellationToken cancellationToken = default) - { - var zoneKeys = ParcelBillingCalculator.CollectDistinctDetailZoneKeys(snapshot); - if (zoneKeys.Count == 0) - { - return; - } - - int? resolvedAreaId = null; - foreach (var zoneKey in zoneKeys) - { - var areaId = await TryResolveZoneKeyToAreaIdAsync(zoneKey, kpiAreaRepository, cancellationToken); - if (!areaId.HasValue) - { - continue; - } - - if (resolvedAreaId == null) - { - resolvedAreaId = areaId; - continue; - } - - if (resolvedAreaId.Value != areaId.Value) - { - throw new BusinessException( - $"卖价明细分区混用了多个价格分区(AreaId={resolvedAreaId} 与 {areaId}),请统一分区后再发布"); - } - } + var ids = await CollectResolvedPriceAreaIdsAsync( + product, + snapshot, + kpiAreaRepository, + cancellationToken); + return ids.Count > 0 ? ids[0] : null; } private static async Task TryResolveZoneKeyToAreaIdAsync( diff --git a/src/Oms.Application/Shared/RulesSnapshotHelper.cs b/src/Oms.Application/Shared/RulesSnapshotHelper.cs index 88f9062..9643354 100644 --- a/src/Oms.Application/Shared/RulesSnapshotHelper.cs +++ b/src/Oms.Application/Shared/RulesSnapshotHelper.cs @@ -98,7 +98,7 @@ public static class RulesSnapshotHelper snapshot, areaId, area?.Name, - infos.Select(KpiAreaSnapshotMapper.MapToSnapshotRule).ToList()); + infos.Select(info => KpiAreaSnapshotMapper.MapToSnapshotRule(info)).ToList()); } private static async Task ApplyWeightRuleToSnapshotAsync(