118 lines
3.6 KiB
C#
118 lines
3.6 KiB
C#
|
|
namespace Service.IconSearch;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Iconify API 响应
|
|||
|
|
/// 实际 API 返回的图标是字符串数组,格式为 "collection:iconName"
|
|||
|
|
/// 例如:["mdi:home", "svg-spinners:wind-toy"]
|
|||
|
|
/// </summary>
|
|||
|
|
public record IconifyApiResponse
|
|||
|
|
{
|
|||
|
|
[JsonPropertyName("icons")]
|
|||
|
|
public List<string>? Icons { get; init; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public record IconifySettings
|
|||
|
|
{
|
|||
|
|
public string ApiUrl { get; init; } = "https://api.iconify.design/search";
|
|||
|
|
public int DefaultLimit { get; init; } = 20;
|
|||
|
|
public int MaxRetryCount { get; init; } = 3;
|
|||
|
|
public int RetryDelayMs { get; init; } = 1000;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public class IconifyApiService(
|
|||
|
|
IOptions<IconifySettings> settings,
|
|||
|
|
ILogger<IconifyApiService> logger
|
|||
|
|
) : IIconifyApiService
|
|||
|
|
{
|
|||
|
|
private readonly HttpClient _httpClient = new();
|
|||
|
|
private readonly IconifySettings _settings = settings.Value;
|
|||
|
|
|
|||
|
|
public async Task<List<IconCandidate>> SearchIconsAsync(List<string> keywords, int limit = 20)
|
|||
|
|
{
|
|||
|
|
var allIcons = new List<IconCandidate>();
|
|||
|
|
var actualLimit = limit > 0 ? limit : _settings.DefaultLimit;
|
|||
|
|
|
|||
|
|
foreach (var keyword in keywords)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
var icons = await SearchIconsByKeywordAsync(keyword, actualLimit);
|
|||
|
|
allIcons.AddRange(icons);
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
logger.LogError(ex, "搜索图标失败,关键字:{Keyword}", keyword);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return allIcons;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private async Task<List<IconCandidate>> SearchIconsByKeywordAsync(string keyword, int limit)
|
|||
|
|
{
|
|||
|
|
var url = $"{_settings.ApiUrl}?query={Uri.EscapeDataString(keyword)}&limit={limit}";
|
|||
|
|
var response = await CallApiWithRetryAsync(url);
|
|||
|
|
|
|||
|
|
if (string.IsNullOrEmpty(response))
|
|||
|
|
{
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var apiResponse = JsonSerializer.Deserialize<IconifyApiResponse>(response);
|
|||
|
|
if (apiResponse?.Icons == null)
|
|||
|
|
{
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解析字符串格式 "collection:iconName" 为 IconCandidate
|
|||
|
|
var candidates = apiResponse.Icons
|
|||
|
|
.Select(iconStr =>
|
|||
|
|
{
|
|||
|
|
var parts = iconStr.Split(':', 2);
|
|||
|
|
if (parts.Length != 2)
|
|||
|
|
{
|
|||
|
|
logger.LogWarning("无效的图标标识符格式:{IconStr}", iconStr);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return new IconCandidate
|
|||
|
|
{
|
|||
|
|
CollectionName = parts[0],
|
|||
|
|
IconName = parts[1]
|
|||
|
|
};
|
|||
|
|
})
|
|||
|
|
.Where(c => c != null)
|
|||
|
|
.Cast<IconCandidate>()
|
|||
|
|
.ToList();
|
|||
|
|
|
|||
|
|
return candidates;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private async Task<string> CallApiWithRetryAsync(string url)
|
|||
|
|
{
|
|||
|
|
var retryCount = 0;
|
|||
|
|
var delay = _settings.RetryDelayMs;
|
|||
|
|
|
|||
|
|
while (retryCount < _settings.MaxRetryCount)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
var response = await _httpClient.GetAsync(url);
|
|||
|
|
response.EnsureSuccessStatusCode();
|
|||
|
|
|
|||
|
|
return await response.Content.ReadAsStringAsync();
|
|||
|
|
}
|
|||
|
|
catch (HttpRequestException ex) when (retryCount < _settings.MaxRetryCount - 1)
|
|||
|
|
{
|
|||
|
|
logger.LogWarning(ex, "Iconify API调用失败,等待 {DelayMs}ms 后重试({RetryCount}/{MaxRetryCount})",
|
|||
|
|
delay, retryCount + 1, _settings.MaxRetryCount);
|
|||
|
|
await Task.Delay(delay);
|
|||
|
|
delay *= 2;
|
|||
|
|
retryCount++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
throw new HttpRequestException($"Iconify API调用失败,已重试 {_settings.MaxRetryCount} 次");
|
|||
|
|
}
|
|||
|
|
}
|