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 { /// /// 校验产品编码唯一性。 /// 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("产品编码已存在"); } } /// /// 获取销售产品,不存在时直接抛异常。 /// private async Task 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; } /// /// 获取价格项,不存在时直接抛异常。 /// private async Task 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; } /// /// 一次性装配价格项、因子和明细,避免接口层 N+1 查询。 /// private async Task> 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(); } /// /// 获取排货方案,并补充服务渠道名称。 /// private async Task 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>(provider.CustomerIds) ?? [], CreateTime = provider.CreateTime, UpdateTime = OmsDocHelper.NullIfDefault(provider.UpdateTime) }).ToList() }; } /// /// 获取指定类型的发布记录。 /// private async Task> 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 { 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 }; } /// /// 组装规则发布快照(不落库),含黑名单/偏远/计重/进位明细。 /// private async Task 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; } /// /// 组装价格发布快照(不落库)。 /// private async Task 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 }; /// /// 为当前配置生成发布快照。 /// 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().ToString() : CurrentUser.UserName!, PublishTime = DateTime.Now, Description = description, Content = OmsDocHelper.SerializeSnapshotJson(snapshot) }); } /// /// 按发布记录回滚价格或规则配置。 /// 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); } /// /// 将保存入参映射到销售产品实体。 /// 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 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>(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 BuildSalesProductPriceFactors(IEnumerable 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 BuildSalesProductPriceDetails(IEnumerable 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 { } }