✨
This commit is contained in:
20
StopShopping.OpenPlatform/DistrictResponse.cs
Normal file
20
StopShopping.OpenPlatform/DistrictResponse.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace StopShopping.OpenPlatform;
|
||||
|
||||
public class DistrictResponse
|
||||
{
|
||||
public bool IsSucced { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public List<District>? Districts { get; set; }
|
||||
|
||||
public class District
|
||||
{
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string FullName { get; set; } = string.Empty;
|
||||
public int? Level { get; set; }
|
||||
public string? PinYin { get; set; }
|
||||
public decimal Latitude { get; set; }
|
||||
public decimal Longitude { get; set; }
|
||||
public List<District>? Districts { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StopShopping.OpenPlatform;
|
||||
using StopShopping.OpenPlatform.Extensions;
|
||||
using StopShopping.OpenPlatform.TencentLocationApi;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class OpenPlatformExtensions
|
||||
{
|
||||
public static IServiceCollection AddOpenPlatformServices(this IServiceCollection services, IConfiguration openPlatformOptions)
|
||||
{
|
||||
services.Configure<OpenPlatformOptions>(openPlatformOptions);
|
||||
|
||||
services.AddHttpClient();
|
||||
|
||||
services.AddScoped<IDistrictService, TencentDistrictService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
13
StopShopping.OpenPlatform/Extensions/OpenPlatformOptions.cs
Normal file
13
StopShopping.OpenPlatform/Extensions/OpenPlatformOptions.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace StopShopping.OpenPlatform.Extensions;
|
||||
|
||||
public class OpenPlatformOptions
|
||||
{
|
||||
public DistrictOptions TencentDistrict { get; set; } = new();
|
||||
|
||||
public class DistrictOptions
|
||||
{
|
||||
public string? SecretKey { get; set; }
|
||||
public string? Top3LevelUrl { get; set; }
|
||||
public string? GetChildrenUrl { get; set; }
|
||||
}
|
||||
}
|
||||
8
StopShopping.OpenPlatform/IDistrictService.cs
Normal file
8
StopShopping.OpenPlatform/IDistrictService.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace StopShopping.OpenPlatform;
|
||||
|
||||
public interface IDistrictService
|
||||
{
|
||||
Task<DistrictResponse?> GetTop3LevelDistrictsAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
Task<DistrictResponse?> GetChildrenAsync(string code, CancellationToken cancellationToken = default);
|
||||
}
|
||||
18
StopShopping.OpenPlatform/StopShopping.OpenPlatform.csproj
Normal file
18
StopShopping.OpenPlatform/StopShopping.OpenPlatform.csproj
Normal file
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Polly.Extensions" Version="8.6.5" />
|
||||
<PackageReference Include="Polly.RateLimiting" Version="8.6.5" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
73
StopShopping.OpenPlatform/TencentLocationApi/District.cs
Normal file
73
StopShopping.OpenPlatform/TencentLocationApi/District.cs
Normal 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; }
|
||||
}
|
||||
19
StopShopping.OpenPlatform/TencentLocationApi/ResponseBase.cs
Normal file
19
StopShopping.OpenPlatform/TencentLocationApi/ResponseBase.cs
Normal 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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user