20261008-ide1.0发布 #1

Merged
zzh merged 42 commits from test into master 2026-10-08 09:18:00 +08:00
11 changed files with 144 additions and 91 deletions
Showing only changes of commit 6faadcf2a1 - Show all commits

View File

@ -100,8 +100,18 @@ public class SalesProductListOutput
/// </summary> /// </summary>
public DateTime CreateTime { get; set; } public DateTime CreateTime { get; set; }
/// <summary>
/// 创建人展示名。
/// </summary>
public string? CreateBy { get; set; }
/// <summary> /// <summary>
/// 更新时间。 /// 更新时间。
/// </summary> /// </summary>
public DateTime? UpdateTime { get; set; } public DateTime? UpdateTime { get; set; }
/// <summary>
/// 修改人展示名。
/// </summary>
public string? UpdateBy { get; set; }
} }

View File

@ -70,8 +70,18 @@ public class ServiceChannelListOutput
/// </summary> /// </summary>
public DateTime CreateTime { get; set; } public DateTime CreateTime { get; set; }
/// <summary>
/// 创建人展示名。
/// </summary>
public string? CreateBy { get; set; }
/// <summary> /// <summary>
/// 更新时间。 /// 更新时间。
/// </summary> /// </summary>
public DateTime? UpdateTime { get; set; } public DateTime? UpdateTime { get; set; }
/// <summary>
/// 修改人展示名。
/// </summary>
public string? UpdateBy { get; set; }
} }

View File

@ -11,6 +11,12 @@ public class KpiAreaSnapshotRuleDto
{ {
public int Id { get; set; } public int Id { get; set; }
/// <summary>所属 KPI 分区表主键(多价格分区表合并快照时用于生成 zone 键)。</summary>
public int AreaId { get; set; }
/// <summary>所属分区表名称(可选,便于展示与 zone 键兼容)。</summary>
public string? AreaName { get; set; }
public string? CountryCode { get; set; } public string? CountryCode { get; set; }
public string? Province { get; set; } public string? Province { get; set; }

View File

@ -282,7 +282,6 @@ public partial class SalesProductService
var priceSnapshot = await BuildPricePublishSnapshotAsync(salesProductId); var priceSnapshot = await BuildPricePublishSnapshotAsync(salesProductId);
ValidateBillingPriceItemRules( ValidateBillingPriceItemRules(
priceSnapshot.Prices.Select(p => MapPriceSaveInput(p, salesProductId)).ToList()); priceSnapshot.Prices.Select(p => MapPriceSaveInput(p, salesProductId)).ToList());
await PriceAreaSnapshotHelper.EnsureSingleResolvedPriceAreaAsync(priceSnapshot, KpiAreaRepository);
snapshot = priceSnapshot; snapshot = priceSnapshot;
} }
else else
@ -519,7 +518,9 @@ public partial class SalesProductService
return normalized.Count == 0 ? null : OmsDocHelper.SerializeJson(normalized); return normalized.Count == 0 ? null : OmsDocHelper.SerializeJson(normalized);
} }
private static SalesProductListOutput MapListOutput(SalesProduct entity) private static SalesProductListOutput MapListOutput(
SalesProduct entity,
IReadOnlyDictionary<int, string> auditUserNameMap)
{ {
return new SalesProductListOutput return new SalesProductListOutput
{ {
@ -542,7 +543,9 @@ public partial class SalesProductService
IsActive = (byte)entity.IsActive, IsActive = (byte)entity.IsActive,
Scheme = entity.Scheme, Scheme = entity.Scheme,
CreateTime = entity.CreateTime, 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)
}; };
} }

View File

@ -45,9 +45,11 @@ public partial class SalesProductService : ApplicationService
pageSize, pageSize,
new Dictionary<string, bool> { [nameof(SalesProduct.UpdateTime)] = false, [nameof(SalesProduct.Id)] = false }); new Dictionary<string, bool> { [nameof(SalesProduct.UpdateTime)] = false, [nameof(SalesProduct.Id)] = false });
var total = await SalesProductRepository.CountAsync(predicate); var total = await SalesProductRepository.CountAsync(predicate);
var auditUserNameMap = await UserDisplayNameResolver.ResolveManyAsync(
entities.SelectMany(x => new[] { x.CreateUserId, x.UpdateUserId }));
return new OmsPageResult<SalesProductListOutput> return new OmsPageResult<SalesProductListOutput>
{ {
Items = entities.Select(MapListOutput).ToList(), Items = entities.Select(entity => MapListOutput(entity, auditUserNameMap)).ToList(),
Total = total Total = total
}; };
} }
@ -268,9 +270,6 @@ public partial class SalesProductService : ApplicationService
{ {
await GetProductOrThrowAsync(input.SalesProductId); await GetProductOrThrowAsync(input.SalesProductId);
ValidateBillingPriceItemRules(input.Prices); ValidateBillingPriceItemRules(input.Prices);
await PriceAreaSnapshotHelper.EnsureSingleResolvedPriceAreaAsync(
BuildPriceSnapshotFromSaveInputs(input.SalesProductId, input.Prices),
KpiAreaRepository);
var prices = new List<SalesProductPrice>(); var prices = new List<SalesProductPrice>();
var factors = new List<SalesProductPriceFactor>(); var factors = new List<SalesProductPriceFactor>();
var details = new List<SalesProductPriceDetail>(); var details = new List<SalesProductPriceDetail>();

View File

@ -416,7 +416,10 @@ public partial class ServiceChannelService
}; };
} }
private static ServiceChannelListOutput MapListOutput(ServiceChannel entity, IReadOnlyDictionary<int, string> groupMap) private static ServiceChannelListOutput MapListOutput(
ServiceChannel entity,
IReadOnlyDictionary<int, string> groupMap,
IReadOnlyDictionary<int, string> auditUserNameMap)
{ {
return new ServiceChannelListOutput return new ServiceChannelListOutput
{ {
@ -433,7 +436,9 @@ public partial class ServiceChannelService
RemoteAreaId = entity.RemoteAreaId, RemoteAreaId = entity.RemoteAreaId,
IsActive = (byte)entity.IsActive, IsActive = (byte)entity.IsActive,
CreateTime = entity.CreateTime, 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)
}; };
} }

View File

@ -43,9 +43,11 @@ public partial class ServiceChannelService : ApplicationService
new Dictionary<string, bool> { [nameof(ServiceChannel.UpdateTime)] = false, [nameof(ServiceChannel.Id)] = false }); new Dictionary<string, bool> { [nameof(ServiceChannel.UpdateTime)] = false, [nameof(ServiceChannel.Id)] = false });
var total = await ServiceChannelRepository.CountAsync(predicate); var total = await ServiceChannelRepository.CountAsync(predicate);
var groupMap = await GetGroupMapAsync(); var groupMap = await GetGroupMapAsync();
var auditUserNameMap = await UserDisplayNameResolver.ResolveManyAsync(
entities.SelectMany(x => new[] { x.CreateUserId, x.UpdateUserId }));
return new OmsPageResult<ServiceChannelListOutput> return new OmsPageResult<ServiceChannelListOutput>
{ {
Items = entities.Select(entity => MapListOutput(entity, groupMap)).ToList(), Items = entities.Select(entity => MapListOutput(entity, groupMap, auditUserNameMap)).ToList(),
Total = total Total = total
}; };
} }

View File

@ -125,7 +125,12 @@ internal static class ParcelBillingCalculator
return null; 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; return keys.Count > 0 ? keys : null;
} }
@ -155,7 +160,11 @@ internal static class ParcelBillingCalculator
var allowed = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var allowed = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var rule in countryRules) 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)) if (!string.IsNullOrWhiteSpace(key))
{ {

View File

@ -8,10 +8,12 @@ namespace Oms.Application.Shared;
/// </summary> /// </summary>
public static class KpiAreaSnapshotMapper public static class KpiAreaSnapshotMapper
{ {
public static KpiAreaSnapshotRuleDto MapToSnapshotRule(KpiAreaInfo entity) => public static KpiAreaSnapshotRuleDto MapToSnapshotRule(KpiAreaInfo entity, string? areaName = null) =>
new() new()
{ {
Id = entity.Id, Id = entity.Id,
AreaId = entity.AreaId,
AreaName = areaName,
CountryCode = entity.CountryCode, CountryCode = entity.CountryCode,
Province = entity.Province, Province = entity.Province,
City = entity.City, City = entity.City,

View File

@ -1,5 +1,3 @@
using Fake.Application;
using Oms.Application.Dtos.Channels;
using Oms.Application.Dtos.Orders; using Oms.Application.Dtos.Orders;
using Oms.Application.Services.Orders; using Oms.Application.Services.Orders;
using Oms.Domain.ChannelsAggregate.Entities; using Oms.Domain.ChannelsAggregate.Entities;
@ -9,11 +7,12 @@ namespace Oms.Application.Shared;
/// <summary> /// <summary>
/// 将价格分区(t_kpi_area + t_kpi_area_info)固化进卖价发布快照。 /// 将价格分区(t_kpi_area + t_kpi_area_info)固化进卖价发布快照。
/// 支持卖价明细引用多张价格分区表:合并各表明细规则写入快照。
/// </summary> /// </summary>
public static class PriceAreaSnapshotHelper public static class PriceAreaSnapshotHelper
{ {
/// <summary> /// <summary>
/// 按产品 PriceAreaId(或从卖价明细分区推断)加载分区头与明细写入快照。 /// 按产品 PriceAreaId(或从卖价明细分区推断的全部 AreaId)加载分区头与明细写入快照。
/// </summary> /// </summary>
public static async Task ApplyPriceAreaToSnapshotAsync( public static async Task ApplyPriceAreaToSnapshotAsync(
SalesProductPricePublishSnapshotDto snapshot, SalesProductPricePublishSnapshotDto snapshot,
@ -21,14 +20,12 @@ public static class PriceAreaSnapshotHelper
IKpiAreaRepository kpiAreaRepository, IKpiAreaRepository kpiAreaRepository,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
await EnsureSingleResolvedPriceAreaAsync(snapshot, kpiAreaRepository, cancellationToken); var areaIds = await CollectResolvedPriceAreaIdsAsync(
var priceAreaId = await ResolvePriceAreaIdAsync(
product, product,
snapshot, snapshot,
kpiAreaRepository, kpiAreaRepository,
cancellationToken); cancellationToken);
if (!priceAreaId.HasValue) if (areaIds.Count == 0)
{ {
snapshot.PriceAreaId = null; snapshot.PriceAreaId = null;
snapshot.PriceAreaName = null; snapshot.PriceAreaName = null;
@ -36,84 +33,94 @@ public static class PriceAreaSnapshotHelper
return; return;
} }
snapshot.PriceAreaId = priceAreaId; var mergedInfos = new List<KpiAreaSnapshotRuleDto>();
var area = await kpiAreaRepository.FirstOrDefaultAsync(x => x.Id == priceAreaId.Value, cancellationToken); var seenInfoIds = new HashSet<int>();
snapshot.PriceAreaName = area?.Name; var areaNames = new List<string>();
var infos = await kpiAreaRepository.GetAllInfosByAreaIdAsync(priceAreaId.Value, cancellationToken);
snapshot.PriceAreaInfos = infos.Select(KpiAreaSnapshotMapper.MapToSnapshotRule).ToList(); 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;
}
}
/// <summary>
/// 收集卖价明细(及产品 PriceAreaId)可解析到的全部价格分区表 Id,去重保序。
/// </summary>
internal static async Task<IReadOnlyList<int>> CollectResolvedPriceAreaIdsAsync(
SalesProduct product,
SalesProductPricePublishSnapshotDto snapshot,
IKpiAreaRepository kpiAreaRepository,
CancellationToken cancellationToken = default)
{
var result = new List<int>();
var seen = new HashSet<int>();
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;
}
/// <summary>
/// 兼容旧调用:仅返回第一个可解析 AreaId(多表场景请用 CollectResolvedPriceAreaIdsAsync)。
/// </summary>
internal static async Task<int?> ResolvePriceAreaIdAsync( internal static async Task<int?> ResolvePriceAreaIdAsync(
SalesProduct product, SalesProduct product,
SalesProductPricePublishSnapshotDto snapshot, SalesProductPricePublishSnapshotDto snapshot,
IKpiAreaRepository kpiAreaRepository, IKpiAreaRepository kpiAreaRepository,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
if (product.PriceAreaId is > 0) var ids = await CollectResolvedPriceAreaIdsAsync(
{ product,
return product.PriceAreaId; snapshot,
} kpiAreaRepository,
cancellationToken);
var zoneKeys = ParcelBillingCalculator.CollectDistinctDetailZoneKeys(snapshot); return ids.Count > 0 ? ids[0] : null;
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;
}
/// <summary>
/// 校验卖价明细中可解析的分区因子键均指向同一 AreaId;混用多个分区表时抛业务异常。
/// 非数字/无法解析的分区名(如「美西」)跳过,与 ResolvePriceAreaIdAsync 一致。
/// </summary>
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}),请统一分区后再发布");
}
}
} }
private static async Task<int?> TryResolveZoneKeyToAreaIdAsync( private static async Task<int?> TryResolveZoneKeyToAreaIdAsync(

View File

@ -98,7 +98,7 @@ public static class RulesSnapshotHelper
snapshot, snapshot,
areaId, areaId,
area?.Name, area?.Name,
infos.Select(KpiAreaSnapshotMapper.MapToSnapshotRule).ToList()); infos.Select(info => KpiAreaSnapshotMapper.MapToSnapshotRule(info)).ToList());
} }
private static async Task ApplyWeightRuleToSnapshotAsync( private static async Task ApplyWeightRuleToSnapshotAsync(