using Oms.Application.Dtos.Channels; using Oms.Application.Dtos.Shared; using Oms.Application.Shared; using Oms.Domain.ChannelsAggregate.Entities; using Oms.Domain.Shared.Enums; namespace Oms.Application.Services.Channels; public partial class ServiceChannelService { /// /// 读取渠道组映射,给列表和详情补充 groupName。 /// private async Task> GetGroupMapAsync() { return (await ServiceChannelGroupRepository.GetListAsync()).ToDictionary(x => x.Id, x => x.Name); } /// /// 获取服务渠道分组,不存在时直接抛异常。 /// private async Task GetGroupOrThrowAsync(int? id) { if (!id.HasValue || id.Value <= 0) { throw new Fake.Application.BusinessException("服务渠道分组不存在"); } var group = await ServiceChannelGroupRepository.FirstOrDefaultAsync(x => x.Id == id.Value); if (group == null) { throw new Fake.Application.BusinessException("服务渠道分组不存在"); } return group; } /// /// 校验服务渠道分组基础信息和名称唯一性。 /// private async Task ValidateGroupInputAsync(ServiceChannelGroupSaveInput input, int? id) { if (string.IsNullOrWhiteSpace(input.Name)) { throw new Fake.Application.BusinessException("分组名称不能为空"); } if (input.AppId <= 0) { throw new Fake.Application.BusinessException("应用ID不能为空"); } if (input.ApiServiceProviderId <= 0) { throw new Fake.Application.BusinessException("API服务商ID不能为空"); } var existed = await ServiceChannelGroupRepository.GetByNameAsync(input.Name.Trim()); if (existed != null && existed.Id != id) { throw new Fake.Application.BusinessException("分组名称已存在"); } } /// /// 校验渠道编码唯一性。 /// private async Task ValidateChannelCodeAsync(string channelCode, int? id) { var existed = await ServiceChannelRepository.GetByChannelCodeAsync(channelCode); if (existed != null && existed.Id != id) { throw new Fake.Application.BusinessException("渠道编码已存在"); } } /// /// 获取服务渠道,不存在时直接抛异常。 /// private async Task GetEntityOrThrowAsync(int? id) { if (!id.HasValue || id.Value <= 0) { throw new Fake.Application.BusinessException("服务渠道不存在"); } var entity = await ServiceChannelRepository.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 ServiceChannelRepository.GetPriceByIdAsync(id.Value); if (entity == null) { throw new Fake.Application.BusinessException("买价项不存在"); } return entity; } /// /// 一次性装配买价项、因子和明细,避免接口层 N+1 查询。 /// private async Task> GetPriceOutputsAsync(int serviceChannelId) { var prices = await ServiceChannelRepository.GetPricesAsync(serviceChannelId); var priceIds = prices.Select(x => x.Id).ToArray(); var factorMap = (await ServiceChannelRepository.GetPriceFactorsAsync(priceIds)) .GroupBy(x => x.PriceId) .ToDictionary(x => x.Key, x => x.OrderBy(item => item.SerialNumber).ToList()); var detailMap = (await ServiceChannelRepository.GetPriceDetailsAsync(priceIds)) .GroupBy(x => x.PriceId) .ToDictionary(x => x.Key, x => x.OrderBy(item => item.Id).ToList()); return prices.Select(price => new ServiceChannelPriceListOutput { Id = price.Id, ServiceChannelId = price.ServiceChannelId, 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, UpdateTime = OmsDocHelper.NullIfDefault(price.UpdateTime) }).ToList(); } /// /// 获取指定类型的发布记录。 /// private async Task> GetPublishListAsync(ServiceChannelPublishListInput input, string operatorType) { await GetEntityOrThrowAsync(input.ServiceChannelId); var pageIndex = OmsDocHelper.NormalizePage(input.Page); var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize); var entities = await ServiceChannelRepository.GetPublishRecordsAsync(input.ServiceChannelId, operatorType, pageIndex, pageSize); var total = await ServiceChannelRepository.CountPublishRecordsAsync(input.ServiceChannelId, operatorType); return new OmsPageResult { Items = entities.Select(entity => new ServiceChannelPublishRecordOutput { Id = entity.Id, ServiceChannelId = entity.ServiceChannelId, Version = entity.Version, OperatorType = entity.OperatorType, Status = entity.Status, PublishUser = entity.PublishUser, PublishTime = entity.PublishTime, Description = entity.Description, Content = OmsDocHelper.DeserializeJsonObject(entity.Content), CreateTime = entity.CreateTime }).ToList(), Total = total }; } /// /// 组装买价发布快照(不落库),与产品卖价 §5.3.2 对称。 /// private async Task BuildPricePublishSnapshotAsync(int serviceChannelId) { return new ServiceChannelPricePublishSnapshot { ServiceChannelId = serviceChannelId, Prices = (await GetPriceOutputsAsync(serviceChannelId)) .Select(PublishSnapshotSlimmer.SlimChannelPrice) .ToList() }; } /// /// 组装渠道规则发布快照(不落库)。 /// private async Task BuildRulesPublishSnapshotAsync(int serviceChannelId) { var entity = await GetEntityOrThrowAsync(serviceChannelId); return new ServiceChannelRulesPublishSnapshot { Channel = MapChannelToSaveInput(entity) }; } private static ServiceChannelSaveInput MapChannelToSaveInput(ServiceChannel entity) => new() { Id = entity.Id, GroupId = entity.GroupId, ChannelCode = entity.ChannelCode, ChannelName = entity.ChannelName, MinTime = entity.MinTime, MaxTime = entity.MaxTime, ForecastType = entity.ForecastType, ForecastCode = entity.ForecastCode, Description = entity.Description, PreScript = entity.PreScript, IsActive = entity.IsActive }; /// /// 为当前配置生成发布快照。 /// private async Task CreatePublishRecordAsync(int serviceChannelId, string operatorType, string? description) { object snapshot = operatorType == PriceOperatorType ? await BuildPricePublishSnapshotAsync(serviceChannelId) : await BuildRulesPublishSnapshotAsync(serviceChannelId); // Content 中保存完整快照,回滚时直接从快照恢复。 await ServiceChannelRepository.InsertPublishRecordAsync(new ServiceChannelPublishRecord { ServiceChannelId = serviceChannelId, Version = DateTime.Now.ToString("yyyyMMddHHmmss"), OperatorType = operatorType, Status = "生效", PublishUser = string.IsNullOrWhiteSpace(CurrentUser.UserName) ? CurrentUser.GetId().ToString() : CurrentUser.UserName!, PublishTime = DateTime.Now, Description = description, Content = OmsDocHelper.SerializeJson(snapshot) }); } /// /// 按发布记录回滚价格或规则配置。 /// private async Task RollbackCoreAsync(ServiceChannelRollbackInput input) { var operatorType = NormalizeOperatorType(input.OperatorType); await GetEntityOrThrowAsync(input.ServiceChannelId); var record = await ServiceChannelRepository.GetPublishRecordByIdAsync(input.RecordId); if (record == null || record.ServiceChannelId != input.ServiceChannelId || !string.Equals(record.OperatorType, operatorType, StringComparison.OrdinalIgnoreCase)) { throw new Fake.Application.BusinessException("发布记录不存在"); } if (operatorType == PriceOperatorType) { var snapshot = OmsDocHelper.DeserializeJson(record.Content); if (snapshot == null) { throw new Fake.Application.BusinessException("发布快照无效"); } // 价格回滚直接恢复完整买价树。 await SavePriceAsync(new ServiceChannelPriceSaveAllInput { ServiceChannelId = input.ServiceChannelId, Prices = snapshot.Prices.Select(MapPriceSaveInput).ToList() }); return; } var rulesSnapshot = OmsDocHelper.DeserializeJson(record.Content); if (rulesSnapshot == null) { throw new Fake.Application.BusinessException("发布快照无效"); } var entity = await GetEntityOrThrowAsync(input.ServiceChannelId); ApplyInput(entity, rulesSnapshot.Channel); await ServiceChannelRepository.UpdateAsync(entity); } /// /// 将保存入参映射到服务渠道实体。 /// private static void ApplyInput(ServiceChannel entity, ServiceChannelSaveInput input) { entity.GroupId = input.GroupId; entity.ChannelCode = input.ChannelCode; entity.ChannelName = input.ChannelName; entity.MinTime = input.MinTime; entity.MaxTime = input.MaxTime; entity.ForecastType = input.ForecastType; entity.ForecastCode = input.ForecastCode; entity.Description = input.Description; entity.PreScript = input.PreScript; entity.IsActive = input.IsActive; } private static ServiceChannelGroupListOutput MapGroupListOutput(ServiceChannelGroup entity) { return new ServiceChannelGroupListOutput { Id = entity.Id, Name = entity.Name, AppId = entity.AppId, ApiServiceProviderId = entity.ApiServiceProviderId, CreateTime = entity.CreateTime, UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime) }; } private static ServiceChannelListOutput MapListOutput(ServiceChannel entity, IReadOnlyDictionary groupMap) { return new ServiceChannelListOutput { Id = entity.Id, GroupId = entity.GroupId, GroupName = entity.GroupId.HasValue && groupMap.TryGetValue(entity.GroupId.Value, out var groupName) ? groupName : null, ChannelCode = entity.ChannelCode, ChannelName = entity.ChannelName, MinTime = entity.MinTime, MaxTime = entity.MaxTime, ForecastType = entity.ForecastType, ForecastCode = entity.ForecastCode, Description = entity.Description, IsActive = (byte)entity.IsActive, CreateTime = entity.CreateTime, UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime) }; } private static ServiceChannelDetailOutput MapDetailOutput(ServiceChannel entity, IReadOnlyDictionary groupMap) { return new ServiceChannelDetailOutput { Id = entity.Id, GroupId = entity.GroupId, GroupName = entity.GroupId.HasValue && groupMap.TryGetValue(entity.GroupId.Value, out var groupName) ? groupName : null, ChannelCode = entity.ChannelCode, ChannelName = entity.ChannelName, MinTime = entity.MinTime, MaxTime = entity.MaxTime, ForecastType = entity.ForecastType, ForecastCode = entity.ForecastCode, Description = entity.Description, PreScript = entity.PreScript, IsActive = (byte)entity.IsActive, CreateTime = entity.CreateTime, CreateBy = entity.CreateUserId <= 0 ? null : entity.CreateUserId.ToString(), UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime), UpdateBy = entity.UpdateUserId <= 0 ? null : entity.UpdateUserId.ToString() }; } private static ChannelPriceFactorOutput MapPriceFactorOutput(ServiceChannelPriceFactor 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(ServiceChannelPriceDetail 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 BuildPriceFactors(IEnumerable inputs, int? priceId = null) { return inputs.Select(input => new ServiceChannelPriceFactor { PriceId = priceId ?? 0, SerialNumber = input.SerialNumber, Factor = input.Factor, FactorValues = OmsDocHelper.SerializeJson(input.FactorValues) }).ToList(); } private static List BuildPriceDetails(IEnumerable inputs, int? priceId = null) { return inputs.Select(input => new ServiceChannelPriceDetail { PriceId = priceId ?? 0, FactorValuesJson = OmsDocHelper.SerializeJson(input.FactorValuesJson), CalculationFormula = input.CalculationFormula, CalculatedValue = input.CalculatedValue, CustomerGroupValues = OmsDocHelper.SerializeJson(input.CustomerGroupValues) }).ToList(); } private static ServiceChannelPriceSaveInput MapPriceSaveInput(ServiceChannelPriceListOutput output) { return new ServiceChannelPriceSaveInput { Id = output.Id, ServiceChannelId = output.ServiceChannelId, 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 ServiceChannelPricePublishSnapshot { public int ServiceChannelId { get; set; } public List Prices { get; set; } = []; } private sealed class ServiceChannelRulesPublishSnapshot { public ServiceChannelSaveInput Channel { get; set; } = new(); } }