This commit is contained in:
2026-03-25 14:55:34 +08:00
commit 2c44b3a4b2
131 changed files with 7453 additions and 0 deletions

View File

@@ -0,0 +1,154 @@
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> openPlatformOptions,
ILogger<TencentDistrictService> 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<TencentDistrictService> _logger;
private readonly ResiliencePipeline _pipeline;
public async Task<DistrictResponse?> 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<District[]?>(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<DistrictResponse?> 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<District>(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<ResponseDistrict<T>?> Get<T>(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<ResponseDistrict<T>>(
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<DistrictResponse.District> 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,
};
}
}