feat(geo): add tenant/merchant/store geocode fallback and retry workflow
All checks were successful
Build and Deploy TenantApi / build-and-deploy (push) Successful in 43s
All checks were successful
Build and Deploy TenantApi / build-and-deploy (push) Successful in 43s
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
namespace TakeoutSaaS.Application.App.Common.Geo;
|
||||
|
||||
/// <summary>
|
||||
/// 地址地理编码结果。
|
||||
/// </summary>
|
||||
public sealed record AddressGeocodingResult(
|
||||
bool Succeeded,
|
||||
decimal? Latitude,
|
||||
decimal? Longitude,
|
||||
string? Message)
|
||||
{
|
||||
/// <summary>
|
||||
/// 构建成功结果。
|
||||
/// </summary>
|
||||
public static AddressGeocodingResult Success(decimal latitude, decimal longitude)
|
||||
=> new(true, latitude, longitude, null);
|
||||
|
||||
/// <summary>
|
||||
/// 构建失败结果。
|
||||
/// </summary>
|
||||
public static AddressGeocodingResult Failed(string? message)
|
||||
=> new(false, null, null, message);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using TakeoutSaaS.Domain.Merchants.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Tenants.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Common.Geo;
|
||||
|
||||
/// <summary>
|
||||
/// 地理定位地址构建器。
|
||||
/// </summary>
|
||||
public static class GeoAddressBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// 构建门店地理编码候选地址。
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string> BuildStoreCandidates(Store store, Merchant? merchant = null)
|
||||
{
|
||||
var candidates = new List<string>();
|
||||
AddCandidate(candidates, BuildAddress(store.Province, store.City, store.District, store.Address));
|
||||
AddCandidate(candidates, store.Address);
|
||||
|
||||
if (merchant is not null)
|
||||
{
|
||||
AddCandidate(candidates, BuildAddress(merchant.Province, merchant.City, merchant.District, merchant.Address));
|
||||
AddCandidate(candidates, BuildAddress(merchant.Province, merchant.City, merchant.District));
|
||||
AddCandidate(candidates, BuildAddress(merchant.City, merchant.District));
|
||||
AddCandidate(candidates, merchant.Address);
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建商户地理编码候选地址。
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string> BuildMerchantCandidates(Merchant merchant)
|
||||
{
|
||||
var candidates = new List<string>();
|
||||
AddCandidate(candidates, BuildAddress(merchant.Province, merchant.City, merchant.District, merchant.Address));
|
||||
AddCandidate(candidates, merchant.Address);
|
||||
AddCandidate(candidates, BuildAddress(merchant.Province, merchant.City, merchant.District));
|
||||
AddCandidate(candidates, BuildAddress(merchant.City, merchant.District));
|
||||
return candidates;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建租户地理编码候选地址。
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string> BuildTenantCandidates(Tenant tenant)
|
||||
{
|
||||
var candidates = new List<string>();
|
||||
AddCandidate(candidates, BuildAddress(tenant.Country, tenant.Province, tenant.City, tenant.Address));
|
||||
AddCandidate(candidates, tenant.Address);
|
||||
AddCandidate(candidates, BuildAddress(tenant.Country, tenant.Province, tenant.City));
|
||||
AddCandidate(candidates, BuildAddress(tenant.Province, tenant.City));
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static string BuildAddress(params string?[] parts)
|
||||
{
|
||||
var normalized = parts
|
||||
.Select(part => part?.Trim())
|
||||
.Where(part => !string.IsNullOrWhiteSpace(part))
|
||||
.ToArray();
|
||||
|
||||
return normalized.Length == 0 ? string.Empty : string.Join(string.Empty, normalized);
|
||||
}
|
||||
|
||||
private static void AddCandidate(ICollection<string> candidates, string? candidate)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(candidate))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (candidates.Any(existing => string.Equals(existing, candidate, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
candidates.Add(candidate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
using TakeoutSaaS.Domain.Common.Enums;
|
||||
using TakeoutSaaS.Domain.Merchants.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Tenants.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Common.Geo;
|
||||
|
||||
/// <summary>
|
||||
/// 地理定位状态写入助手。
|
||||
/// </summary>
|
||||
public static class GeoLocationStateHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 最大重试次数。
|
||||
/// </summary>
|
||||
public const int MaxRetryCount = 5;
|
||||
|
||||
/// <summary>
|
||||
/// 获取重试间隔。
|
||||
/// </summary>
|
||||
public static TimeSpan GetRetryDelay(int retryCount)
|
||||
{
|
||||
return retryCount switch
|
||||
{
|
||||
<= 1 => TimeSpan.FromMinutes(1),
|
||||
2 => TimeSpan.FromMinutes(5),
|
||||
3 => TimeSpan.FromMinutes(30),
|
||||
4 => TimeSpan.FromHours(2),
|
||||
_ => TimeSpan.FromHours(12)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入门店定位成功状态。
|
||||
/// </summary>
|
||||
public static void MarkSuccess(Store store, double latitude, double longitude, DateTime now)
|
||||
{
|
||||
store.Latitude = latitude;
|
||||
store.Longitude = longitude;
|
||||
store.GeoStatus = GeoLocationStatus.Success;
|
||||
store.GeoFailReason = null;
|
||||
store.GeoRetryCount = 0;
|
||||
store.GeoUpdatedAt = now;
|
||||
store.GeoNextRetryAt = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入商户定位成功状态。
|
||||
/// </summary>
|
||||
public static void MarkSuccess(Merchant merchant, double latitude, double longitude, DateTime now)
|
||||
{
|
||||
merchant.Latitude = latitude;
|
||||
merchant.Longitude = longitude;
|
||||
merchant.GeoStatus = GeoLocationStatus.Success;
|
||||
merchant.GeoFailReason = null;
|
||||
merchant.GeoRetryCount = 0;
|
||||
merchant.GeoUpdatedAt = now;
|
||||
merchant.GeoNextRetryAt = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入租户定位成功状态。
|
||||
/// </summary>
|
||||
public static void MarkSuccess(Tenant tenant, double latitude, double longitude, DateTime now)
|
||||
{
|
||||
tenant.Latitude = latitude;
|
||||
tenant.Longitude = longitude;
|
||||
tenant.GeoStatus = GeoLocationStatus.Success;
|
||||
tenant.GeoFailReason = null;
|
||||
tenant.GeoRetryCount = 0;
|
||||
tenant.GeoUpdatedAt = now;
|
||||
tenant.GeoNextRetryAt = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入门店待重试状态。
|
||||
/// </summary>
|
||||
public static void MarkPending(Store store, string? reason, DateTime now)
|
||||
{
|
||||
store.GeoStatus = GeoLocationStatus.Pending;
|
||||
store.GeoFailReason = reason;
|
||||
store.GeoRetryCount = 0;
|
||||
store.GeoNextRetryAt = now.Add(GetRetryDelay(1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入商户待重试状态。
|
||||
/// </summary>
|
||||
public static void MarkPending(Merchant merchant, string? reason, DateTime now)
|
||||
{
|
||||
merchant.GeoStatus = GeoLocationStatus.Pending;
|
||||
merchant.GeoFailReason = reason;
|
||||
merchant.GeoRetryCount = 0;
|
||||
merchant.GeoNextRetryAt = now.Add(GetRetryDelay(1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入租户待重试状态。
|
||||
/// </summary>
|
||||
public static void MarkPending(Tenant tenant, string? reason, DateTime now)
|
||||
{
|
||||
tenant.GeoStatus = GeoLocationStatus.Pending;
|
||||
tenant.GeoFailReason = reason;
|
||||
tenant.GeoRetryCount = 0;
|
||||
tenant.GeoNextRetryAt = now.Add(GetRetryDelay(1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入门店重试失败状态。
|
||||
/// </summary>
|
||||
public static void MarkRetryFailure(Store store, string? reason, DateTime now)
|
||||
{
|
||||
var nextRetryCount = store.GeoRetryCount + 1;
|
||||
store.GeoRetryCount = nextRetryCount;
|
||||
store.GeoFailReason = reason;
|
||||
if (nextRetryCount >= MaxRetryCount)
|
||||
{
|
||||
store.GeoStatus = GeoLocationStatus.Failed;
|
||||
store.GeoNextRetryAt = null;
|
||||
return;
|
||||
}
|
||||
|
||||
store.GeoStatus = GeoLocationStatus.Pending;
|
||||
store.GeoNextRetryAt = now.Add(GetRetryDelay(nextRetryCount + 1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入商户重试失败状态。
|
||||
/// </summary>
|
||||
public static void MarkRetryFailure(Merchant merchant, string? reason, DateTime now)
|
||||
{
|
||||
var nextRetryCount = merchant.GeoRetryCount + 1;
|
||||
merchant.GeoRetryCount = nextRetryCount;
|
||||
merchant.GeoFailReason = reason;
|
||||
if (nextRetryCount >= MaxRetryCount)
|
||||
{
|
||||
merchant.GeoStatus = GeoLocationStatus.Failed;
|
||||
merchant.GeoNextRetryAt = null;
|
||||
return;
|
||||
}
|
||||
|
||||
merchant.GeoStatus = GeoLocationStatus.Pending;
|
||||
merchant.GeoNextRetryAt = now.Add(GetRetryDelay(nextRetryCount + 1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入租户重试失败状态。
|
||||
/// </summary>
|
||||
public static void MarkRetryFailure(Tenant tenant, string? reason, DateTime now)
|
||||
{
|
||||
var nextRetryCount = tenant.GeoRetryCount + 1;
|
||||
tenant.GeoRetryCount = nextRetryCount;
|
||||
tenant.GeoFailReason = reason;
|
||||
if (nextRetryCount >= MaxRetryCount)
|
||||
{
|
||||
tenant.GeoStatus = GeoLocationStatus.Failed;
|
||||
tenant.GeoNextRetryAt = null;
|
||||
return;
|
||||
}
|
||||
|
||||
tenant.GeoStatus = GeoLocationStatus.Pending;
|
||||
tenant.GeoNextRetryAt = now.Add(GetRetryDelay(nextRetryCount + 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace TakeoutSaaS.Application.App.Common.Geo;
|
||||
|
||||
/// <summary>
|
||||
/// 地址地理编码服务契约。
|
||||
/// </summary>
|
||||
public interface IAddressGeocodingService
|
||||
{
|
||||
/// <summary>
|
||||
/// 将地址解析为经纬度。
|
||||
/// </summary>
|
||||
/// <param name="address">地址文本。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>地理编码结果。</returns>
|
||||
Task<AddressGeocodingResult> GeocodeAsync(string address, CancellationToken cancellationToken);
|
||||
}
|
||||
Reference in New Issue
Block a user