using System.Linq.Expressions; using Fake.Application; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Channels; using Oms.Application.Dtos.Shared; using Oms.Application.Shared; using Oms.Domain.ChannelsAggregate.Entities; using Oms.Domain.ChannelsAggregate.Repositories; using Oms.Domain.Shared; using Oms.Domain.SystemAggregate.Repositories; namespace Oms.Application.Services.Channels; /// /// KPI 分区 API 服务。 /// [ApiExplorerSettings(GroupName = "OMS-Channel")] public class KpiAreaService : ApplicationService { private IKpiAreaRepository KpiAreaRepository => LazyServiceProvider.GetRequiredService(); private ICountryRepository CountryRepository => LazyServiceProvider.GetRequiredService(); /// /// 查询KPI分区列表。 /// /// 入参:KPI分区列表查询条件。 /// KPI分区分页结果,包含列表数据和总记录数。 [HttpPost("/channel/kpi-area/list")] public async Task> GetListAsync(KpiAreaListInput input) { var predicate = BuildAreaPredicate(input); var pageIndex = OmsDocHelper.NormalizePage(input.Page); var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize); var entities = await KpiAreaRepository.GetPagedListAsync( predicate, pageIndex, pageSize, new Dictionary { [nameof(KpiArea.UpdateTime)] = false, [nameof(KpiArea.Id)] = false }); var total = await KpiAreaRepository.CountAsync(predicate); return new OmsPageResult { Items = entities.Select(MapAreaOutput).ToList(), Total = total }; } /// /// 新增KPI分区。 /// /// 入参:KPI分区保存参数。 /// 无返回体,仅完成新增KPI分区。 [HttpPost("/channel/kpi-area/create")] public async Task CreateAsync(KpiAreaSaveInput input) { await ValidateAreaNameAsync(input.AppKey, input.AreaType, input.Name, null); var entity = new KpiArea { AppKey = input.AppKey, AreaType = input.AreaType, Name = input.Name, SystemName = input.SystemName }; await KpiAreaRepository.InsertAsync(entity); } /// /// 更新KPI分区。 /// /// 入参:KPI分区保存参数。 /// 无返回体,仅完成更新KPI分区。 [HttpPost("/channel/kpi-area/update")] public async Task UpdateAsync(KpiAreaSaveInput input) { var entity = await GetAreaOrThrowAsync(input.Id); await ValidateAreaNameAsync(input.AppKey, input.AreaType, input.Name, entity.Id); entity.AppKey = input.AppKey; entity.AreaType = input.AreaType; entity.Name = input.Name; entity.SystemName = input.SystemName; await KpiAreaRepository.UpdateAsync(entity); } /// /// 删除KPI分区。 /// /// 入参:目标主键。 /// 无返回体,仅完成删除KPI分区。 [HttpPost("/channel/kpi-area/delete")] public async Task DeleteAsync(IdInput input) { var entity = await GetAreaOrThrowAsync(input.Id); await KpiAreaRepository.DeleteAsync(entity); } /// /// 查询KPI分区明细列表。 /// /// 入参:KPI分区明细列表查询条件。 /// KPI分区明细分页结果,包含列表数据和总记录数。 [HttpPost("/channel/kpi-area/info/list")] public async Task> GetInfoListAsync(KpiAreaInfoListInput input) { await GetAreaOrThrowAsync(input.AreaId); var pageIndex = OmsDocHelper.NormalizePage(input.Page); var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize); var entities = await KpiAreaRepository.GetInfosAsync(input.AreaId, pageIndex, pageSize); var total = await KpiAreaRepository.CountInfosAsync(input.AreaId); return new OmsPageResult { Items = entities.Select(MapInfoOutput).ToList(), Total = total }; } /// /// 新增KPI分区明细。 /// /// 入参:KPI分区明细保存参数。 /// 无返回体,仅完成新增KPI分区明细。 [HttpPost("/channel/kpi-area/info/add")] public async Task AddInfoAsync(KpiAreaInfoSaveInput input) { await GetAreaOrThrowAsync(input.AreaId); var entity = new KpiAreaInfo { AreaId = input.AreaId, CountryCode = input.CountryCode, CountryName = input.CountryName, Province = input.Province, City = input.City, LeftPostcode = input.LeftPostcode, RightPostcode = input.RightPostcode, MatchDim = input.MatchDim, MatchMode = input.MatchMode, PrefixLen = input.PrefixLen ?? 0 }; PrepareAreaInfo(entity); await EnsureInfoNotDuplicateAsync(entity, excludeId: null); await KpiAreaRepository.InsertInfoAsync(entity); } /// /// 更新KPI分区明细。 /// /// 入参:KPI分区明细保存参数。 /// 无返回体,仅完成更新KPI分区明细。 [HttpPost("/channel/kpi-area/info/update")] public async Task UpdateInfoAsync(KpiAreaInfoSaveInput input) { var entity = await GetAreaInfoOrThrowAsync(input.Id); await GetAreaOrThrowAsync(input.AreaId); entity.AreaId = input.AreaId; entity.CountryCode = input.CountryCode; entity.CountryName = input.CountryName; entity.Province = input.Province; entity.City = input.City; entity.LeftPostcode = input.LeftPostcode; entity.RightPostcode = input.RightPostcode; entity.MatchDim = input.MatchDim; entity.MatchMode = input.MatchMode; entity.PrefixLen = input.PrefixLen ?? 0; PrepareAreaInfo(entity); await EnsureInfoNotDuplicateAsync(entity, excludeId: entity.Id); await KpiAreaRepository.UpdateInfoAsync(entity); } /// /// 删除KPI分区明细。 /// /// 入参:目标主键。 /// 无返回体,仅完成删除KPI分区明细。 [HttpPost("/channel/kpi-area/info/delete")] public async Task DeleteInfoAsync(IdInput input) { await GetAreaInfoOrThrowAsync(input.Id); await KpiAreaRepository.DeleteInfoAsync(input.Id); } /// /// 下载 KPI 分区明细导入模板。 /// [HttpGet("/channel/kpi-area/info/import-template")] public async Task DownloadInfoImportTemplateAsync([FromQuery] string? areaType) { var bytes = await KpiAreaInfoImportHelper.BuildTemplateBytesAsync(); return new FileContentResult(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") { FileDownloadName = KpiAreaInfoImportHelper.ResolveTemplateFileName(areaType) }; } /// /// 导入 KPI 分区明细(Excel)。 /// [HttpPost("/channel/kpi-area/info/import")] public async Task ImportInfoAsync([FromForm] KpiAreaInfoImportInput input) { if (input.File == null || input.File.Length == 0) { throw new BusinessException("请上传 Excel 文件"); } await GetAreaOrThrowAsync(input.AreaId); await using var stream = input.File.OpenReadStream(); var parsedRows = await KpiAreaInfoImportHelper.ParseImportRowsAsync(stream); if (parsedRows.Count == 0) { throw new BusinessException("Excel 中无有效数据行"); } var countries = await CountryRepository.GetListAsync(); var countriesByCode = countries .Where(x => !string.IsNullOrWhiteSpace(x.CountryCode)) .GroupBy(x => x.CountryCode.Trim(), StringComparer.OrdinalIgnoreCase) .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase); var existingKeys = new HashSet(StringComparer.Ordinal); foreach (var item in await KpiAreaRepository.GetAllInfosByAreaIdAsync(input.AreaId)) { TryAddDuplicateKey(existingKeys, item); } var output = new KpiAreaInfoImportOutput(); foreach (var (rowNumber, entity) in parsedRows) { try { KpiAreaInfoImportHelper.PrepareEntityForImport(entity, countriesByCode); entity.AreaId = input.AreaId; if (!TryAddDuplicateKey(existingKeys, entity)) { output.SkipCount++; output.Failures.Add(new KpiAreaInfoImportFailRow { RowNumber = rowNumber, Reason = "与已有规则重复,已跳过" }); continue; } await KpiAreaRepository.InsertInfoAsync(entity); output.SuccessCount++; } catch (Exception ex) { output.FailCount++; output.Failures.Add(new KpiAreaInfoImportFailRow { RowNumber = rowNumber, Reason = ex.Message }); } } return output; } private static Expression> BuildAreaPredicate(KpiAreaListInput input) { var appKey = input.AppKey?.Trim(); var areaType = input.AreaType?.Trim(); return entity => (string.IsNullOrWhiteSpace(appKey) || entity.AppKey == appKey) && (string.IsNullOrWhiteSpace(areaType) || entity.AreaType == areaType); } private async Task ValidateAreaNameAsync(string appKey, string areaType, string name, int? id) { var existed = await KpiAreaRepository.GetByAppKeyTypeAndNameAsync(appKey, areaType, name); if (existed != null && existed.Id != id) { throw new BusinessException("分区名称已存在"); } } private async Task GetAreaOrThrowAsync(int? id) { if (!id.HasValue || id.Value <= 0) { throw new BusinessException("分区不存在"); } var entity = await KpiAreaRepository.FirstOrDefaultAsync(x => x.Id == id.Value); if (entity == null) { throw new BusinessException("分区不存在"); } return entity; } private async Task GetAreaInfoOrThrowAsync(int? id) { if (!id.HasValue || id.Value <= 0) { throw new BusinessException("分区明细不存在"); } var entity = await KpiAreaRepository.GetInfoByIdAsync(id.Value); if (entity == null) { throw new BusinessException("分区明细不存在"); } return entity; } private static KpiAreaOutput MapAreaOutput(KpiArea entity) { return new KpiAreaOutput { Id = entity.Id, AppKey = entity.AppKey, AreaType = entity.AreaType, Name = entity.Name, SystemName = entity.SystemName, CreateTime = entity.CreateTime, UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime) }; } private static void PrepareAreaInfo(KpiAreaInfo entity) { try { GeoPartitionMatcher.PrepareEntity(entity); } catch (InvalidOperationException ex) { throw new BusinessException(ex.Message); } } private async Task EnsureInfoNotDuplicateAsync(KpiAreaInfo entity, int? excludeId) { var key = GeoPartitionMatcher.BuildDuplicateKey(entity); var existing = await KpiAreaRepository.GetAllInfosByAreaIdAsync(entity.AreaId); foreach (var item in existing) { if (excludeId.HasValue && item.Id == excludeId.Value) { continue; } if (TryBuildDuplicateKey(item, out var existingKey) && string.Equals(existingKey, key, StringComparison.Ordinal)) { throw new BusinessException("分区明细已存在相同规则"); } } } private static bool TryAddDuplicateKey(HashSet keys, KpiAreaInfo entity) => TryBuildDuplicateKey(entity, out var key) && keys.Add(key); private static bool TryBuildDuplicateKey(KpiAreaInfo entity, out string key) { try { GeoPartitionMatcher.PrepareEntity(entity); key = GeoPartitionMatcher.BuildDuplicateKey(entity); return true; } catch (InvalidOperationException) { key = string.Empty; return false; } } private static KpiAreaInfoOutput MapInfoOutput(KpiAreaInfo entity) { var (matchDim, matchMode, prefixLen) = GeoPartitionMatcher.Describe(entity); return new KpiAreaInfoOutput { Id = entity.Id, AreaId = entity.AreaId, CountryCode = entity.CountryCode, CountryName = entity.CountryName, Province = entity.Province, City = entity.City, LeftPostcode = entity.LeftPostcode, RightPostcode = entity.RightPostcode, MatchDim = matchDim, MatchMode = matchMode, PrefixLen = prefixLen, CreateTime = entity.CreateTime, UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime) }; } /// /// 分区包版本列表(不含正文)。 /// [HttpGet("/channel/kpi-area/publish/list")] public async Task> GetPublishListAsync([FromQuery] int areaId) { if (areaId <= 0) { throw new Fake.Application.BusinessException("分区表 Id 无效"); } await GetAreaOrThrowAsync(areaId); var headers = await KpiAreaRepository.GetPublishHeadersByAreaIdAsync(areaId); return headers.Select(MapPublishHeader).ToList(); } /// /// 单条分区包(含正文)。 /// [HttpGet("/channel/kpi-area/publish/get")] public async Task GetPublishRecordAsync([FromQuery] int id) { var entity = await KpiAreaRepository.GetPublishByIdAsync(id) ?? throw new Fake.Application.BusinessException("分区包不存在"); return MapPublishDetail(entity); } private async Task GetAreaOrThrowAsync(int areaId) { var area = await KpiAreaRepository.FirstOrDefaultAsync(x => x.Id == areaId); if (area == null) { throw new Fake.Application.BusinessException("分区表不存在"); } return area; } private static KpiAreaPublishRecordOutput MapPublishHeader(KpiAreaPublishRecord entity) => new() { Id = entity.Id, AreaId = entity.AreaId, AreaType = entity.AreaType, Version = entity.Version, Status = EnumDisplayNames.Of(entity.Status), ContentHash = entity.ContentHash, PublishUser = entity.PublishUser, PublishTime = entity.PublishTime, Description = entity.Description }; private static KpiAreaPublishRecordOutput MapPublishDetail(KpiAreaPublishRecord entity) { var output = MapPublishHeader(entity); output.Content = OmsDocHelper.DeserializeJsonObject(entity.Content); return output; } }