using System.Text.Json; using System.Threading.RateLimiting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Polly; using Polly.RateLimiting; using Polly.Timeout; using StopShopping.OpenPlatform.Extensions; namespace StopShopping.OpenPlatform.TencentLocationApi; public class TencentDistrictService : IDistrictService { public TencentDistrictService( IHttpClientFactory httpClientFactory, IOptions openPlatformOptions, ILogger logger) { _httpContextFactory = httpClientFactory; _options = openPlatformOptions.Value; _logger = logger; _pipeline = new ResiliencePipelineBuilder() .AddRetry(new Polly.Retry.RetryStrategyOptions { Delay = TimeSpan.FromSeconds(2), MaxRetryAttempts = 5, ShouldHandle = (arg) => ValueTask.FromResult(arg.Outcome.Exception is TimeoutRejectedException or RateLimiterRejectedException) }) .AddRateLimiter(new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions { PermitLimit = 2, QueueLimit = int.MaxValue, SegmentsPerWindow = 5, Window = TimeSpan.FromSeconds(1) })) .AddTimeout(TimeSpan.FromSeconds(20)) .Build(); } private readonly IHttpClientFactory _httpContextFactory; private readonly OpenPlatformOptions _options; private readonly ILogger _logger; private readonly ResiliencePipeline _pipeline; public async Task GetChildrenAsync(string code, CancellationToken cancellationToken = default) { var uri = new Uri( string.Format("{0}?key={1}&id={2}" , _options.TencentDistrict.GetChildrenUrl! , Uri.EscapeDataString(_options.TencentDistrict.SecretKey!) , code)); var districts = await Get(uri, cancellationToken); if (null == districts) return null; DistrictResponse response = new() { IsSucced = districts.Status == 0, Message = $"{districts.Status}:{districts.Message}", Districts = districts.Result ?.FirstOrDefault() ?.Select(Cast) .ToList() }; return response; } public async Task GetTop3LevelDistrictsAsync(CancellationToken cancellationToken = default) { var uri = new Uri( string.Format("{0}?key={1}&struct_type=1" , _options.TencentDistrict.Top3LevelUrl! , _options.TencentDistrict.SecretKey!) ); var districts = await Get(uri, cancellationToken); if (null == districts) return null; DistrictResponse response = new() { IsSucced = districts.Status == 0, Message = $"{districts.Status}:{districts.Message}", Districts = null != districts.Result ? [.. RecurCast(districts.Result)] : null }; return response; } private async Task?> Get(Uri uri, CancellationToken cancellationToken = default) { using var httpClient = _httpContextFactory.CreateClient(); var districts = await _pipeline.ExecuteAsync(async (token) => { var jsonStream = await httpClient.GetStreamAsync(uri, token); try { return await JsonSerializer.DeserializeAsync>( jsonStream, cancellationToken: token); } catch (JsonException e) { _logger.LogError("序列化异常:{Path}", e.Path); return null; } }, cancellationToken); if (null == districts) { _logger.LogError("接口请求失败:{Url}", uri.ToString()); return null; } return districts; } private IEnumerable RecurCast(District[] districts) { foreach (var d in districts) { var cd = Cast(d); if (d.Districts?.Length > 0) cd.Districts = [.. RecurCast(d.Districts!)]; yield return cd; } } private DistrictResponse.District Cast(District d) { return new DistrictResponse.District { Code = d.Id, FullName = d.FullName, Latitude = d.Location.Latitude, Level = d.Level, Longitude = d.Location.Longitude, Name = d.Name, PinYin = d.PinYin?.Length > 0 ? string.Join(',', d.PinYin) : null, }; } }