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(