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,73 @@
using System.Text.Json.Serialization;
namespace StopShopping.OpenPlatform.TencentLocationApi;
/// <summary>
/// 区划
/// </summary>
/// <value></value>
public record District
{
/// <summary>
/// 行政区划唯一标识adcode
/// </summary>
/// <value></value>
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
/// <summary>
/// 简称,如“内蒙古”
/// </summary>
/// <value></value>
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
/// <summary>
/// 行政区划级别
/// </summary>
/// <value></value>
[JsonPropertyName("level")]
public int? Level { get; set; }
/// <summary>
/// 全称,如“内蒙古自治区”
/// </summary>
/// <value></value>
[JsonPropertyName("fullname")]
public string FullName { get; set; } = string.Empty;
/// <summary>
/// 行政区划拼音,每一下标为一个字的全拼,如:[“nei”,“meng”,“gu”]
/// </summary>
/// <value></value>
[JsonPropertyName("pinyin")]
public string[]? PinYin { get; set; }
/// <summary>
/// 经纬度
/// </summary>
/// <returns></returns>
[JsonPropertyName("location")]
public Location Location { get; set; } = new();
/// <summary>
/// 当前区划的下级区划信息,结构与当前区划一致,如果没有下级区划则不返回此字段
/// </summary>
/// <value></value>
[JsonPropertyName("districts")]
public District[]? Districts { get; set; }
}
/// <summary>
/// 经纬度
/// </summary>
/// <value></value>
public record Location
{
/// <summary>
/// 纬度
/// </summary>
/// <value></value>
[JsonPropertyName("lat")]
public decimal Latitude { get; set; }
/// <summary>
/// 经度
/// </summary>
/// <value></value>
[JsonPropertyName("lng")]
public decimal Longitude { get; set; }
}

View File

@@ -0,0 +1,19 @@
using System.Text.Json.Serialization;
namespace StopShopping.OpenPlatform.TencentLocationApi;
public abstract record ResponseBase
{
/// <summary>
/// 状态码
/// </summary>
/// <value></value>
[JsonPropertyName("status")]
public int Status { get; set; }
/// <summary>
/// 状态说明
/// </summary>
/// <value></value>
[JsonPropertyName("message")]
public string? Message { get; set; }
}

View File

@@ -0,0 +1,19 @@
using System.Text.Json.Serialization;
namespace StopShopping.OpenPlatform.TencentLocationApi;
public record ResponseDistrict<T> : ResponseBase
{
/// <summary>
/// 数据版本,日期
/// </summary>
/// <value></value>
[JsonPropertyName("data_version")]
public string DataVersion { get; set; } = string.Empty;
/// <summary>
/// 结果数组
/// </summary>
/// <value></value>
[JsonPropertyName("result")]
public T[]? Result { get; set; }
}

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,
};
}
}