namespace Service.IconSearch;
///
/// Iconify API 响应
/// 实际 API 返回的图标是字符串数组,格式为 "collection:iconName"
/// 例如:["mdi:home", "svg-spinners:wind-toy"]
///
public record IconifyApiResponse
{
[JsonPropertyName("icons")]
public List? 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 settings,
ILogger logger
) : IIconifyApiService
{
private readonly HttpClient _httpClient = new();
private readonly IconifySettings _settings = settings.Value;
public async Task> SearchIconsAsync(List keywords, int limit = 20)
{
var allIcons = new List();
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> 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(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()
.ToList();
return candidates;
}
private async Task 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} 次");
}
}