admin/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs
2026-07-23 12:03:47 +08:00

519 lines
22 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 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>
/// 获取销售产品,不存在时直接抛异常。
/// </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) ?? [],
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,
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,
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,
CustomerGuideMarkdown = product.CustomerGuideMarkdown,
IsActive = product.IsActive,
Scheme = product.Scheme
};
/// <summary>
/// 为当前配置生成发布快照。
/// </summary>
private async Task CreatePublishRecordAsync(int salesProductId, string operatorType, string? description)
{
object snapshot = operatorType == PriceOperatorType
? await BuildPricePublishSnapshotAsync(salesProductId)
: 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,
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)
}).ToList();
await SalesProductRepository.ReplaceAllocationProvidersAsync(input.SalesProductId, providers);
}
/// <summary>
/// 将保存入参映射到销售产品实体。
/// </summary>
private static void ApplyProductInput(SalesProduct entity, SalesProductSaveInput input)
{
entity.ProductCode = input.ProductCode;
entity.ProductName = input.ProductName;
entity.ProductGroup = input.ProductGroup;
entity.MinTimeliness = input.MinTimeliness;
entity.MaxTimeliness = input.MaxTimeliness;
entity.CustomerWaybill = input.CustomerWaybill;
entity.LabelTemplateId = input.LabelTemplateId;
entity.BlacklistAreaId = input.BlacklistAreaId;
entity.RemoteAreaId = input.RemoteAreaId;
entity.PriceAreaId = input.PriceAreaId;
entity.WeightRuleId = input.WeightRuleId;
entity.WeightRoundingId = input.WeightRoundingId;
entity.ProductDescription = input.ProductDescription;
entity.PreStorageScript = input.PreStorageScript;
entity.CustomerGuideMarkdown = input.CustomerGuideMarkdown;
entity.IsActive = input.IsActive;
entity.Scheme = string.IsNullOrWhiteSpace(input.Scheme) ? AllocationSchemeCodes.CostLowest : input.Scheme;
}
private static SalesProductListOutput MapListOutput(SalesProduct entity)
{
return new SalesProductListOutput
{
Id = entity.Id,
ProductCode = entity.ProductCode,
ProductName = entity.ProductName,
ProductGroup = entity.ProductGroup,
MinTimeliness = entity.MinTimeliness,
MaxTimeliness = entity.MaxTimeliness,
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,
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
};
}
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,
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
{
}
}