All checks were successful
Build and Deploy / deploy (push) Successful in 1m7s
合并多 Area 分区规则进发布快照,计费用规则自身 AreaId 匹配;产品与渠道列表补 CreateBy/UpdateBy。 Co-authored-by: Cursor <cursoragent@cursor.com>
718 lines
30 KiB
C#
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
|
|
{
|
|
}
|
|
}
|