feat: 卖价支持多价格分区表,列表返回创建/修改人
All checks were successful
Build and Deploy / deploy (push) Successful in 1m7s
All checks were successful
Build and Deploy / deploy (push) Successful in 1m7s
合并多 Area 分区规则进发布快照,计费用规则自身 AreaId 匹配;产品与渠道列表补 CreateBy/UpdateBy。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f972a28d79
commit
6faadcf2a1
@ -100,8 +100,18 @@ public class SalesProductListOutput
|
||||
/// </summary>
|
||||
public DateTime CreateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建人展示名。
|
||||
/// </summary>
|
||||
public string? CreateBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间。
|
||||
/// </summary>
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改人展示名。
|
||||
/// </summary>
|
||||
public string? UpdateBy { get; set; }
|
||||
}
|
||||
|
||||
@ -70,8 +70,18 @@ public class ServiceChannelListOutput
|
||||
/// </summary>
|
||||
public DateTime CreateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建人展示名。
|
||||
/// </summary>
|
||||
public string? CreateBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间。
|
||||
/// </summary>
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改人展示名。
|
||||
/// </summary>
|
||||
public string? UpdateBy { get; set; }
|
||||
}
|
||||
|
||||
@ -11,6 +11,12 @@ public class KpiAreaSnapshotRuleDto
|
||||
{
|
||||
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? Province { get; set; }
|
||||
|
||||
@ -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<int, string> 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)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -45,9 +45,11 @@ public partial class SalesProductService : ApplicationService
|
||||
pageSize,
|
||||
new Dictionary<string, bool> { [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<SalesProductListOutput>
|
||||
{
|
||||
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<SalesProductPrice>();
|
||||
var factors = new List<SalesProductPriceFactor>();
|
||||
var details = new List<SalesProductPriceDetail>();
|
||||
|
||||
@ -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
|
||||
{
|
||||
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -43,9 +43,11 @@ public partial class ServiceChannelService : ApplicationService
|
||||
new Dictionary<string, bool> { [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<ServiceChannelListOutput>
|
||||
{
|
||||
Items = entities.Select(entity => MapListOutput(entity, groupMap)).ToList(),
|
||||
Items = entities.Select(entity => MapListOutput(entity, groupMap, auditUserNameMap)).ToList(),
|
||||
Total = total
|
||||
};
|
||||
}
|
||||
|
||||
@ -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<string>(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))
|
||||
{
|
||||
|
||||
@ -8,10 +8,12 @@ namespace Oms.Application.Shared;
|
||||
/// </summary>
|
||||
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,
|
||||
|
||||
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 将价格分区(t_kpi_area + t_kpi_area_info)固化进卖价发布快照。
|
||||
/// 支持卖价明细引用多张价格分区表:合并各表明细规则写入快照。
|
||||
/// </summary>
|
||||
public static class PriceAreaSnapshotHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 按产品 PriceAreaId(或从卖价明细分区推断)加载分区头与明细写入快照。
|
||||
/// 按产品 PriceAreaId(或从卖价明细分区推断的全部 AreaId)加载分区头与明细写入快照。
|
||||
/// </summary>
|
||||
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<KpiAreaSnapshotRuleDto>();
|
||||
var seenInfoIds = new HashSet<int>();
|
||||
var areaNames = new List<string>();
|
||||
|
||||
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(
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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}),请统一分区后再发布");
|
||||
}
|
||||
}
|
||||
var ids = await CollectResolvedPriceAreaIdsAsync(
|
||||
product,
|
||||
snapshot,
|
||||
kpiAreaRepository,
|
||||
cancellationToken);
|
||||
return ids.Count > 0 ? ids[0] : null;
|
||||
}
|
||||
|
||||
private static async Task<int?> TryResolveZoneKeyToAreaIdAsync(
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user