admin/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs
zzh 6faadcf2a1
All checks were successful
Build and Deploy / deploy (push) Successful in 1m7s
feat: 卖价支持多价格分区表,列表返回创建/修改人
合并多 Area 分区规则进发布快照,计费用规则自身 AreaId 匹配;产品与渠道列表补 CreateBy/UpdateBy。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-07 10:57:21 +08:00

718 lines
30 KiB
C#

using Oms.Application.Dtos.Channels;
using Oms.Application.Dtos.Orders;
using Oms.Application.Dtos.Shared;
using Oms.Application.Shared;
using Oms.Domain.ChannelsAggregate.Consts;
using Oms.Domain.ChannelsAggregate.Entities;
using Oms.Domain.Shared;
using Oms.Domain.Shared.Enums;
namespace Oms.Application.Services.Channels;
public partial class SalesProductService
{
/// <summary>
/// 规范化并校验产品代码格式。
/// </summary>
private static string NormalizeProductCode(string? productCode)
{
var code = (productCode ?? string.Empty).Trim().ToUpperInvariant();
if (string.IsNullOrWhiteSpace(code))
{
throw new Fake.Application.BusinessException("产品代码不能为空");
}
for (var i = 0; i < code.Length; i++)
{
var ch = code[i];
if (ch is < 'A' or > 'Z' && ch is < '0' or > '9' && ch != '-')
{
throw new Fake.Application.BusinessException("产品代码仅支持大写字母、数字和-");
}
}
return code;
}
/// <summary>
/// 校验产品编码唯一性。
/// </summary>
private async Task ValidateProductCodeAsync(string productCode, int? id)
{
var existed = await SalesProductRepository.GetByProductCodeAsync(productCode);
if (existed != null && existed.Id != id)
{
throw new Fake.Application.BusinessException("产品编码已存在");
}
}
/// <summary>
/// 更新时若目标编码被软删记录占用,改名释放 uk_sp_code。
/// </summary>
private async Task FreeSoftDeletedProductCodeIfNeededAsync(string productCode, int currentId)
{
var conflict = await SalesProductRepository.GetByProductCodeIncludingDeletedAsync(productCode);
if (conflict == null || !conflict.IsDeleted || conflict.Id == currentId)
{
return;
}
conflict.ProductCode = $"{productCode}__deleted_{conflict.Id}";
await SalesProductRepository.UpdateAsync(conflict);
}
/// <summary>
/// 获取销售产品,不存在时直接抛异常。
/// </summary>
private async Task<SalesProduct> GetProductOrThrowAsync(int? id)
{
if (!id.HasValue || id.Value <= 0)
{
throw new Fake.Application.BusinessException("销售产品不存在");
}
var entity = await SalesProductRepository.FirstOrDefaultAsync(x => x.Id == id.Value);
if (entity == null)
{
throw new Fake.Application.BusinessException("销售产品不存在");
}
return entity;
}
/// <summary>
/// 获取价格项,不存在时直接抛异常。
/// </summary>
private async Task<SalesProductPrice> GetPriceOrThrowAsync(int? id)
{
if (!id.HasValue || id.Value <= 0)
{
throw new Fake.Application.BusinessException("价格项不存在");
}
var entity = await SalesProductRepository.GetPriceByIdAsync(id.Value);
if (entity == null)
{
throw new Fake.Application.BusinessException("价格项不存在");
}
return entity;
}
/// <summary>
/// 一次性装配价格项、因子和明细,避免接口层 N+1 查询。
/// </summary>
private async Task<List<SalesProductPriceListOutput>> GetPriceOutputsAsync(int salesProductId)
{
var prices = await SalesProductRepository.GetPricesAsync(salesProductId);
var priceIds = prices.Select(x => x.Id).ToArray();
var factorMap = (await SalesProductRepository.GetPriceFactorsAsync(priceIds))
.GroupBy(x => x.PriceId)
.ToDictionary(x => x.Key, x => x.OrderBy(item => item.SerialNumber).ToList());
var detailMap = (await SalesProductRepository.GetPriceDetailsAsync(priceIds))
.GroupBy(x => x.PriceId)
.ToDictionary(x => x.Key, x => x.OrderBy(item => item.Id).ToList());
var auditUserNameMap = await UserDisplayNameResolver.ResolveManyAsync(
prices.SelectMany(x => new[] { x.CreateUserId, x.UpdateUserId }));
return prices.Select(price => new SalesProductPriceListOutput
{
Id = price.Id,
SalesProductId = price.SalesProductId,
SerialNumber = price.SerialNumber,
Status = (byte)price.Status,
PriceName = price.PriceName,
CostItem = price.CostItem,
CostUnit = price.CostUnit,
Currency = price.Currency,
ChargingCondition = price.ChargingCondition,
Factors = factorMap.TryGetValue(price.Id, out var factors) ? factors.Select(MapPriceFactorOutput).ToList() : [],
Details = detailMap.TryGetValue(price.Id, out var details) ? details.Select(MapPriceDetailOutput).ToList() : [],
CreateTime = price.CreateTime,
CreateBy = OmsRbacUserDisplayNameResolver.FormatUserId(price.CreateUserId, auditUserNameMap),
UpdateTime = OmsDocHelper.NullIfDefault(price.UpdateTime),
UpdateBy = OmsRbacUserDisplayNameResolver.FormatUserId(price.UpdateUserId, auditUserNameMap)
}).ToList();
}
/// <summary>
/// 获取排货方案,并补充服务渠道名称。
/// </summary>
private async Task<SalesProductAllocationOutput> GetAllocationCoreAsync(int salesProductId)
{
var product = await GetProductOrThrowAsync(salesProductId);
var providers = await SalesProductRepository.GetAllocationProvidersAsync(salesProductId);
var channelIds = providers.Where(x => x.ServiceChannelId.HasValue).Select(x => x.ServiceChannelId!.Value).Distinct().ToArray();
var channels = channelIds.Length == 0
? []
: await ServiceChannelRepository.GetListAsync(x => channelIds.Contains(x.Id));
var channelMap = channels.ToDictionary(x => x.Id, x => x.ChannelName);
return new SalesProductAllocationOutput
{
SalesProductId = salesProductId,
Scheme = product.Scheme,
Providers = providers.Select(provider => new SalesProductAllocationProviderOutput
{
Id = provider.Id,
SalesProductId = provider.SalesProductId,
SerialNumber = provider.SerialNumber,
ProviderName = provider.ProviderName,
ServiceChannelId = provider.ServiceChannelId,
ServiceChannelName = provider.ServiceChannelId.HasValue && channelMap.TryGetValue(provider.ServiceChannelId.Value, out var name) ? name : null,
Weight = provider.Weight,
CustomerIds = OmsDocHelper.DeserializeJson<List<int>>(provider.CustomerIds) ?? [],
MatchCondition = provider.MatchCondition,
CreateTime = provider.CreateTime,
UpdateTime = OmsDocHelper.NullIfDefault(provider.UpdateTime)
}).ToList()
};
}
/// <summary>
/// 获取指定类型的发布记录。
/// </summary>
private async Task<OmsPageResult<SalesProductPublishRecordOutput>> GetPublishListAsync(SalesProductPublishListInput input, string operatorType)
{
await GetProductOrThrowAsync(input.SalesProductId);
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var entities = await SalesProductRepository.GetPublishRecordsAsync(input.SalesProductId, operatorType, pageIndex, pageSize);
var total = await SalesProductRepository.CountPublishRecordsAsync(input.SalesProductId, operatorType);
return new OmsPageResult<SalesProductPublishRecordOutput>
{
Items = entities.Select(entity => new SalesProductPublishRecordOutput
{
Id = entity.Id,
SalesProductId = entity.SalesProductId,
Version = entity.Version,
OperatorType = entity.OperatorType,
Status = EnumDisplayNames.Of(entity.Status),
PublishUser = entity.PublishUser,
PublishTime = entity.PublishTime,
Description = entity.Description,
AiAnalysisReport = entity.AiAnalysisReport,
Content = operatorType == PriceOperatorType
? OmsDocHelper.ParseSalesProductPricePublishContentForApi(entity.Content)
: OmsDocHelper.ToSnapshotApiContent(OmsDocHelper.DeserializeRulesSnapshot(entity.Content)),
CreateTime = entity.CreateTime
}).ToList(),
Total = total
};
}
/// <summary>
/// 组装规则发布快照(不落库),含黑名单/偏远/计重/进位明细。
/// </summary>
private async Task<SalesProductRulesPublishSnapshotDto> BuildRulesPublishSnapshotAsync(int salesProductId)
{
var product = await GetProductOrThrowAsync(salesProductId);
var rulesSnapshot = new SalesProductRulesPublishSnapshot
{
Product = MapProductToRulesSaveInput(product),
Providers = (await GetAllocationCoreAsync(salesProductId)).Providers
.Select(PublishSnapshotSlimmer.SlimAllocationProvider)
.ToList()
};
await RulesSnapshotHelper.ApplyRulesToSnapshotAsync(
rulesSnapshot,
product,
KpiAreaRepository,
WeightRuleRepository,
WeightRoundingRepository);
await AllocationProviderSnapshotEnricher.EnrichAsync(rulesSnapshot.Providers);
return rulesSnapshot;
}
/// <summary>
/// 组装价格发布快照(不落库)。
/// </summary>
private async Task<SalesProductPricePublishSnapshotDto> BuildPricePublishSnapshotAsync(int salesProductId)
{
var product = await GetProductOrThrowAsync(salesProductId);
var priceSnapshot = new SalesProductPricePublishSnapshot
{
SalesProductId = salesProductId,
Prices = (await GetPriceOutputsAsync(salesProductId)).Select(PublishSnapshotSlimmer.SlimPrice).ToList()
};
await PriceAreaSnapshotHelper.ApplyPriceAreaToSnapshotAsync(priceSnapshot, product, KpiAreaRepository);
return priceSnapshot;
}
private static SalesProductSaveInput MapProductToRulesSaveInput(SalesProduct product) =>
new()
{
Id = product.Id,
ProductCode = product.ProductCode,
ProductName = product.ProductName,
ProductGroup = product.ProductGroup,
MinTimeliness = product.MinTimeliness,
MaxTimeliness = product.MaxTimeliness,
TradeTerms = product.TradeTerms,
RouteSort = product.RouteSort,
AdditionalServices = product.AdditionalServices,
CustomerWaybill = product.CustomerWaybill,
LabelTemplateId = product.LabelTemplateId,
BlacklistAreaId = product.BlacklistAreaId,
RemoteAreaId = product.RemoteAreaId,
PriceAreaId = product.PriceAreaId,
WeightRuleId = product.WeightRuleId,
WeightRoundingId = product.WeightRoundingId,
ProductDescription = product.ProductDescription,
PreStorageScript = product.PreStorageScript,
// 对客说明不常变动,且有独立保存入口;规则发布快照跳过,避免配置发布被其 diff 干扰
CustomerGuideMarkdown = null,
IsActive = product.IsActive,
Scheme = product.Scheme
};
/// <summary>
/// 为当前配置生成发布快照。
/// </summary>
private async Task CreatePublishRecordAsync(
int salesProductId,
string operatorType,
string? description,
string? aiAnalysisReport = null)
{
object snapshot;
if (operatorType == PriceOperatorType)
{
var priceSnapshot = await BuildPricePublishSnapshotAsync(salesProductId);
ValidateBillingPriceItemRules(
priceSnapshot.Prices.Select(p => MapPriceSaveInput(p, salesProductId)).ToList());
snapshot = priceSnapshot;
}
else
{
snapshot = await BuildRulesPublishSnapshotAsync(salesProductId);
}
// Content 中保存完整快照,回滚时直接从快照恢复,避免依赖当前库内状态。
await SalesProductRepository.InsertPublishRecordAsync(new SalesProductPublishRecord
{
SalesProductId = salesProductId,
Version = DateTime.Now.ToString("yyyyMMddHHmmss"),
OperatorType = operatorType,
Status = EsSnapshotStatus.Active,
PublishUser = string.IsNullOrWhiteSpace(CurrentUser.UserName) ? CurrentUser.GetId<int>().ToString() : CurrentUser.UserName!,
PublishTime = DateTime.Now,
Description = description,
AiAnalysisReport = string.IsNullOrWhiteSpace(aiAnalysisReport) ? null : aiAnalysisReport.Trim(),
Content = OmsDocHelper.SerializeSnapshotJson(snapshot)
});
}
/// <summary>
/// 按发布记录回滚价格或规则配置。
/// </summary>
private async Task RollbackCoreAsync(SalesProductRollbackInput input)
{
var operatorType = NormalizeOperatorType(input.OperatorType);
await GetProductOrThrowAsync(input.SalesProductId);
var record = await SalesProductRepository.GetPublishRecordByIdAsync(input.RecordId);
if (record == null || record.SalesProductId != input.SalesProductId || !string.Equals(record.OperatorType, operatorType, StringComparison.OrdinalIgnoreCase))
{
throw new Fake.Application.BusinessException("发布记录不存在");
}
if (operatorType == PriceOperatorType)
{
var snapshot = OmsDocHelper.DeserializePriceSnapshot(record.Content);
if (snapshot == null)
{
throw new Fake.Application.BusinessException("发布快照无效");
}
// 价格回滚直接恢复完整价格树。
await SavePriceAsync(new SalesProductPriceSaveAllInput
{
SalesProductId = input.SalesProductId,
Prices = snapshot.Prices.Select(price => MapPriceSaveInput(price, input.SalesProductId)).ToList()
});
return;
}
var rulesSnapshot = OmsDocHelper.DeserializeRulesSnapshot(record.Content);
if (rulesSnapshot == null)
{
throw new Fake.Application.BusinessException("发布快照无效");
}
// 规则回滚既恢复产品配置,也恢复排货服务商列表。
var product = await GetProductOrThrowAsync(input.SalesProductId);
if (rulesSnapshot.Product == null)
{
throw new Fake.Application.BusinessException("发布快照缺少产品配置");
}
ApplyProductInput(product, rulesSnapshot.Product);
await SalesProductRepository.UpdateAsync(product);
var providers = rulesSnapshot.Providers.Select(provider => new SalesProductAllocationProvider
{
SalesProductId = input.SalesProductId,
SerialNumber = provider.SerialNumber,
ProviderName = provider.ProviderName,
ServiceChannelId = provider.ServiceChannelId,
Weight = provider.Weight,
CustomerIds = OmsDocHelper.SerializeJson(provider.CustomerIds),
MatchCondition = provider.MatchCondition
}).ToList();
await SalesProductRepository.ReplaceAllocationProvidersAsync(input.SalesProductId, providers);
}
/// <summary>
/// 将保存入参映射到销售产品实体。
/// </summary>
private static void ApplyProductInput(SalesProduct entity, SalesProductSaveInput input, bool updateProductCode = true)
{
if (updateProductCode)
{
entity.ProductCode = NormalizeProductCode(input.ProductCode);
}
entity.ProductName = input.ProductName;
entity.ProductGroup = input.ProductGroup;
entity.MinTimeliness = input.MinTimeliness;
entity.MaxTimeliness = input.MaxTimeliness;
entity.TradeTerms = NormalizeTradeTerms(input.TradeTerms);
entity.RouteSort = input.RouteSort < 0 ? 0 : input.RouteSort;
entity.AdditionalServices = NormalizeAdditionalServices(input.AdditionalServices);
entity.CustomerWaybill = input.CustomerWaybill;
entity.LabelTemplateId = input.LabelTemplateId;
entity.BlacklistAreaId = input.BlacklistAreaId;
entity.RemoteAreaId = input.RemoteAreaId;
// 价格分区改由卖价明细分区推断并固化进发布快照,产品表不再持久化 PriceAreaId。
entity.PriceAreaId = null;
entity.WeightRuleId = input.WeightRuleId;
entity.WeightRoundingId = input.WeightRoundingId;
entity.ProductDescription = input.ProductDescription;
entity.PreStorageScript = input.PreStorageScript;
// 对客说明仅通过 /customer-guide/save 维护,产品配置保存/发布不覆盖
entity.IsActive = input.IsActive;
entity.Scheme = string.IsNullOrWhiteSpace(input.Scheme) ? AllocationSchemeCodes.CostLowest : input.Scheme;
}
private static string? NormalizeTradeTerms(string? raw)
{
var value = (raw ?? string.Empty).Trim().ToUpperInvariant();
return value is "DDP" or "DDU" ? value : null;
}
private static SalesProductPricePublishSnapshotDto BuildPriceSnapshotFromSaveInputs(
int salesProductId,
IReadOnlyList<SalesProductPriceSaveInput> prices) =>
new()
{
SalesProductId = salesProductId,
Prices = prices.Select(price => new SalesProductPriceSnapshotItemDto
{
Id = price.Id ?? 0,
SerialNumber = price.SerialNumber,
Status = (byte)price.Status,
PriceName = price.PriceName,
CostItem = price.CostItem,
CostUnit = price.CostUnit,
Currency = price.Currency,
ChargingCondition = price.ChargingCondition,
Details = price.Details.Select(detail => new ChannelPriceDetailOutput
{
Id = detail.Id ?? 0,
FactorValuesJson = detail.FactorValuesJson,
CalculationFormula = detail.CalculationFormula,
CalculatedValue = detail.CalculatedValue,
CustomerGroupValues = detail.CustomerGroupValues
}).ToList()
}).ToList()
};
/// <summary>
/// 保险/偏远费等附加费用项的计费条件校验;同一提交批次内费用项不可重复。
/// </summary>
private static void ValidateBillingPriceItemRules(IReadOnlyList<SalesProductPriceSaveInput> prices)
{
EnsureUniqueCostItemsInBatch(prices.Select(x => x.CostItem));
foreach (var price in prices)
{
var costItem = price.CostItem?.Trim() ?? string.Empty;
var condition = price.ChargingCondition?.Trim() ?? string.Empty;
if (costItem.Equals("insurance", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrWhiteSpace(condition)
|| string.Equals(condition, "无", StringComparison.OrdinalIgnoreCase))
{
throw new Fake.Application.BusinessException("保险费用项必须配置非空计费条件(不能为「无」)");
}
}
if (costItem.Equals("remoteFee", StringComparison.OrdinalIgnoreCase)
|| costItem.Equals("remote_fee", StringComparison.OrdinalIgnoreCase)
|| costItem.Contains("偏远", StringComparison.OrdinalIgnoreCase))
{
if (!condition.Contains("是否偏远", StringComparison.Ordinal))
{
throw new Fake.Application.BusinessException("偏远费用项的计费条件必须包含「是否偏远」");
}
}
}
}
/// <summary>
/// 同一销售产品下每种费用项只能有一条卖价配置。
/// </summary>
private async Task EnsureCostItemUniqueAsync(int salesProductId, string? costItem, int? excludePriceId)
{
var trimmed = costItem?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(trimmed))
{
throw new Fake.Application.BusinessException("费用项不能为空");
}
var existing = await SalesProductRepository.GetPricesAsync(salesProductId);
var duplicated = existing.Any(p =>
(!excludePriceId.HasValue || p.Id != excludePriceId.Value)
&& string.Equals(p.CostItem?.Trim(), trimmed, StringComparison.OrdinalIgnoreCase));
if (duplicated)
{
throw new Fake.Application.BusinessException(
$"该产品已存在费用项「{trimmed}」,同一产品下每种费用项只能有一条");
}
}
private static void EnsureUniqueCostItemsInBatch(IEnumerable<string?> costItems)
{
var duplicates = costItems
.Select(x => x?.Trim() ?? string.Empty)
.Where(x => !string.IsNullOrWhiteSpace(x))
.GroupBy(x => x, StringComparer.OrdinalIgnoreCase)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.ToList();
if (duplicates.Count > 0)
{
throw new Fake.Application.BusinessException(
$"同一产品下费用项不能重复:{string.Join("、", duplicates)}");
}
}
private static string? NormalizeAdditionalServices(string? raw)
{
if (string.IsNullOrWhiteSpace(raw))
{
return null;
}
var trimmed = raw.Trim();
var codes = OmsDocHelper.DeserializeJson<List<string>>(trimmed);
if (codes == null)
{
return trimmed;
}
var normalized = codes
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x => x.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
return normalized.Count == 0 ? null : OmsDocHelper.SerializeJson(normalized);
}
private static SalesProductListOutput MapListOutput(
SalesProduct entity,
IReadOnlyDictionary<int, string> auditUserNameMap)
{
return new SalesProductListOutput
{
Id = entity.Id,
ProductCode = entity.ProductCode,
ProductName = entity.ProductName,
ProductGroup = entity.ProductGroup,
MinTimeliness = entity.MinTimeliness,
MaxTimeliness = entity.MaxTimeliness,
RouteSort = entity.RouteSort,
AdditionalServices = entity.AdditionalServices,
CustomerWaybill = entity.CustomerWaybill,
LabelTemplateId = entity.LabelTemplateId,
BlacklistAreaId = entity.BlacklistAreaId,
RemoteAreaId = entity.RemoteAreaId,
PriceAreaId = entity.PriceAreaId,
WeightRuleId = entity.WeightRuleId,
WeightRoundingId = entity.WeightRoundingId,
ProductDescription = entity.ProductDescription,
IsActive = (byte)entity.IsActive,
Scheme = entity.Scheme,
CreateTime = entity.CreateTime,
CreateBy = OmsRbacUserDisplayNameResolver.FormatUserId(entity.CreateUserId, auditUserNameMap),
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime),
UpdateBy = OmsRbacUserDisplayNameResolver.FormatUserId(entity.UpdateUserId, auditUserNameMap)
};
}
private static SalesProductDetailOutput MapDetailOutput(
SalesProduct entity,
IReadOnlyDictionary<int, string> auditUserNameMap)
{
return new SalesProductDetailOutput
{
Id = entity.Id,
ProductCode = entity.ProductCode,
ProductName = entity.ProductName,
ProductGroup = entity.ProductGroup,
MinTimeliness = entity.MinTimeliness,
MaxTimeliness = entity.MaxTimeliness,
TradeTerms = entity.TradeTerms,
RouteSort = entity.RouteSort,
AdditionalServices = entity.AdditionalServices,
CustomerWaybill = entity.CustomerWaybill,
LabelTemplateId = entity.LabelTemplateId,
BlacklistAreaId = entity.BlacklistAreaId,
RemoteAreaId = entity.RemoteAreaId,
PriceAreaId = entity.PriceAreaId,
WeightRuleId = entity.WeightRuleId,
WeightRoundingId = entity.WeightRoundingId,
ProductDescription = entity.ProductDescription,
PreStorageScript = entity.PreStorageScript,
CustomerGuideMarkdown = entity.CustomerGuideMarkdown,
IsActive = (byte)entity.IsActive,
Scheme = entity.Scheme,
CreateTime = entity.CreateTime,
CreateBy = OmsRbacUserDisplayNameResolver.FormatUserId(entity.CreateUserId, auditUserNameMap),
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime),
UpdateBy = OmsRbacUserDisplayNameResolver.FormatUserId(entity.UpdateUserId, auditUserNameMap)
};
}
private static ChannelPriceFactorOutput MapPriceFactorOutput(SalesProductPriceFactor entity)
{
return new ChannelPriceFactorOutput
{
Id = entity.Id,
PriceId = entity.PriceId,
SerialNumber = entity.SerialNumber,
Factor = entity.Factor,
FactorValues = OmsDocHelper.DeserializeJson<List<string>>(entity.FactorValues) ?? []
};
}
private static ChannelPriceDetailOutput MapPriceDetailOutput(SalesProductPriceDetail entity)
{
return new ChannelPriceDetailOutput
{
Id = entity.Id,
PriceId = entity.PriceId,
FactorValuesJson = OmsDocHelper.DeserializeJsonObject(entity.FactorValuesJson),
CalculationFormula = entity.CalculationFormula,
CalculatedValue = entity.CalculatedValue,
CustomerGroupValues = OmsDocHelper.DeserializeJsonObject(entity.CustomerGroupValues)
};
}
private static List<SalesProductPriceFactor> BuildSalesProductPriceFactors(IEnumerable<ChannelPriceFactorInput> inputs, int? priceId = null)
{
return inputs.Select(input => new SalesProductPriceFactor
{
PriceId = priceId ?? 0,
SerialNumber = input.SerialNumber,
Factor = input.Factor,
FactorValues = OmsDocHelper.SerializeJson(input.FactorValues)
}).ToList();
}
private static List<SalesProductPriceDetail> BuildSalesProductPriceDetails(IEnumerable<ChannelPriceDetailInput> inputs, int? priceId = null)
{
return inputs.Select(input => new SalesProductPriceDetail
{
PriceId = priceId ?? 0,
FactorValuesJson = OmsDocHelper.SerializeJson(input.FactorValuesJson),
CalculationFormula = input.CalculationFormula,
CalculatedValue = input.CalculatedValue,
CustomerGroupValues = OmsDocHelper.SerializeJson(input.CustomerGroupValues)
}).ToList();
}
private static SalesProductPriceSaveInput MapPriceSaveInput(SalesProductPriceListOutput output) =>
MapPriceSaveInput(output, output.SalesProductId);
private static SalesProductPriceSaveInput MapPriceSaveInput(SalesProductPriceSnapshotItemDto output, int salesProductId)
{
return new SalesProductPriceSaveInput
{
Id = output.Id,
SalesProductId = salesProductId,
SerialNumber = output.SerialNumber,
Status = (EnableStatus)output.Status,
PriceName = output.PriceName,
CostItem = output.CostItem,
CostUnit = output.CostUnit,
Currency = output.Currency,
ChargingCondition = output.ChargingCondition,
Factors = output.Factors.Select(item => new ChannelPriceFactorInput
{
Id = item.Id,
SerialNumber = item.SerialNumber,
Factor = item.Factor,
FactorValues = item.FactorValues
}).ToList(),
Details = output.Details.Select(item => new ChannelPriceDetailInput
{
Id = item.Id,
FactorValuesJson = item.FactorValuesJson,
CalculationFormula = item.CalculationFormula,
CalculatedValue = item.CalculatedValue,
CustomerGroupValues = item.CustomerGroupValues
}).ToList()
};
}
private static SalesProductPriceSaveInput MapPriceSaveInput(SalesProductPriceListOutput output, int salesProductId)
{
return new SalesProductPriceSaveInput
{
Id = output.Id,
SalesProductId = salesProductId,
SerialNumber = output.SerialNumber,
Status = (EnableStatus)output.Status,
PriceName = output.PriceName,
CostItem = output.CostItem,
CostUnit = output.CostUnit,
Currency = output.Currency,
ChargingCondition = output.ChargingCondition,
Factors = output.Factors.Select(item => new ChannelPriceFactorInput
{
Id = item.Id,
SerialNumber = item.SerialNumber,
Factor = item.Factor,
FactorValues = item.FactorValues
}).ToList(),
Details = output.Details.Select(item => new ChannelPriceDetailInput
{
Id = item.Id,
FactorValuesJson = item.FactorValuesJson,
CalculationFormula = item.CalculationFormula,
CalculatedValue = item.CalculatedValue,
CustomerGroupValues = item.CustomerGroupValues
}).ToList()
};
}
private static string NormalizeOperatorType(string? operatorType)
{
var normalized = operatorType?.Trim().ToLowerInvariant();
return normalized switch
{
PriceOperatorType => PriceOperatorType,
RulesOperatorType => RulesOperatorType,
_ => throw new Fake.Application.BusinessException("不支持的发布类型")
};
}
private sealed class SalesProductPricePublishSnapshot : SalesProductPricePublishSnapshotDto
{
}
private sealed class SalesProductRulesPublishSnapshot : SalesProductRulesPublishSnapshotDto
{
}
}