using System.Threading.Tasks; using System.Threading; using AIK.Common; using AIK.Service.IService; using Microsoft.Extensions.Logging; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Media.Imaging; namespace AIK.Service.Service { public class RemoteImageCacheService : IImageCacheService { private readonly ConcurrentDictionary _memoryCache; private readonly ConcurrentDictionary _memoryPathCache; // 缓存正在下载或已完成的任务(URL/Key -> Task) private readonly ConcurrentDictionary> _pendingDownloads = new(); private readonly string _cacheBaseDirectory; private readonly TimeSpan _cacheDuration = TimeSpan.FromDays(7); // 缓存时效7天 private readonly IHttpClientService _httpClient; private readonly IConfigurationService _configurationService; private readonly ILogger _logger; private string GetCacheFilePath(string cacheKey) => Path.Combine(_cacheBaseDirectory, $"{cacheKey}.jpg"); public RemoteImageCacheService(IConfigurationService configurationService, IHttpClientService httpClient, ILogger logger) { _configurationService = configurationService; _memoryCache = new ConcurrentDictionary(); _memoryPathCache = new ConcurrentDictionary(); _httpClient = httpClient; _logger = logger; // 设置缓存目录,例如在用户本地应用数据文件夹下 _cacheBaseDirectory = CommonString.ImageCacheDirectory; Directory.CreateDirectory(_cacheBaseDirectory); // 确保目录存在 _cacheDuration = _configurationService.CacheSettings.ImageCacheExpiry; } #region 基础方法 public async Task GetImageAsync(string imageUrl, string cacheKey = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(imageUrl)) return GetPlaceholderImage(); // 生成缓存键,如果未提供则使用URL的MD5值 var key = cacheKey ?? GenerateCacheKey(imageUrl); // 1. 检查内存缓存 if (_memoryCache.TryGetValue(key, out var cachedImage)) { //_logger.LogDebug("内存缓存命中: {Key}", key); return cachedImage; } // 2. 检查磁盘缓存 string filePath = GetCacheFilePath(key); if (File.Exists(filePath) && !IsCacheExpired(filePath)) { //_logger.LogDebug("磁盘缓存命中: {FilePath}", filePath); // 从磁盘加载并放入内存缓存 var bitmapImage = await LoadImageFromDiskAsync(filePath); if (bitmapImage != null) { _memoryCache[key] = bitmapImage; } return bitmapImage; } // 3. 从网络下载 try { var fullImgUrl = _configurationService.ApiSettings.FileUrl + imageUrl; _logger.LogDebug("开始下载图片: {Url}", fullImgUrl); // Flurl.Http 字节下载(连接级显式 TLS 1.2;非 2xx 会抛异常,由外层 catch 统一处理) byte[] imageBytes = await _httpClient.GetByteArrayWithTimeoutAsync(fullImgUrl); if (imageBytes == null || imageBytes.Length == 0) { _logger.LogWarning("图片数据为空: {Url}", fullImgUrl); return GetPlaceholderImage(); } // 验证图片数据有效性 if (!IsValidImageData(imageBytes)) { _logger.LogWarning("图片数据无效: {Url}, 大小: {Size}字节", fullImgUrl, imageBytes.Length); return GetPlaceholderImage(); } // 保存到磁盘缓存 await SaveImageToDiskAsync(imageBytes, filePath); // 从字节数组直接创建BitmapImage var newImage = await CreateBitmapImageFromBytesAsync(imageBytes); if (newImage != null) { _memoryCache[key] = newImage; _logger.LogDebug("图片下载缓存成功: {Url}, 大小: {Size}字节", fullImgUrl, imageBytes.Length); return newImage; } } catch (Exception ex) { _logger.LogError(ex, "下载图片失败: {ImageUrl}", imageUrl); } return GetPlaceholderImage(); } /// /// 若本地图片存在,直接加载图片(去掉读取文件流操作)优化速率大概是280ms->10ms /// /// /// /// /// public async Task GetImageByLocalPathAsync(string imageUrl, string cacheKey = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(imageUrl)) return GetPlaceholderImage(); // 生成缓存键,如果未提供则使用URL的MD5值 var key = cacheKey ?? GenerateCacheKey(imageUrl); // 1. 检查内存缓存 if (_memoryCache.TryGetValue(key, out var cachedImage)) { //_logger.LogDebug("内存缓存命中: {Key}", key); return cachedImage; } // 2. 检查磁盘缓存 string filePath = GetCacheFilePath(key); if (File.Exists(filePath) && !IsCacheExpired(filePath)) { var bitmapImage = new BitmapImage(); bitmapImage.BeginInit(); bitmapImage.UriSource = new Uri(filePath); bitmapImage.CacheOption = BitmapCacheOption.OnLoad; bitmapImage.EndInit(); bitmapImage.Freeze(); if (bitmapImage != null) { _memoryCache[key] = bitmapImage; } return bitmapImage; } // 3. 从网络下载 try { var fullImgUrl = _configurationService.ApiSettings.FileUrl + imageUrl; _logger.LogDebug("开始下载图片: {Url}", fullImgUrl); // Flurl.Http 字节下载(连接级显式 TLS 1.2;非 2xx 会抛异常,由外层 catch 统一处理) byte[] imageBytes = await _httpClient.GetByteArrayWithTimeoutAsync(fullImgUrl); if (imageBytes == null || imageBytes.Length == 0) { _logger.LogWarning("图片数据为空: {Url}", fullImgUrl); return GetPlaceholderImage(); } // 验证图片数据有效性 if (!IsValidImageData(imageBytes)) { _logger.LogWarning("图片数据无效: {Url}, 大小: {Size}字节", fullImgUrl, imageBytes.Length); return GetPlaceholderImage(); } // 保存到磁盘缓存 await SaveImageToDiskAsync(imageBytes, filePath); // 从字节数组直接创建BitmapImage var newImage = await CreateBitmapImageFromBytesAsync(imageBytes); if (newImage != null) { _memoryCache[key] = newImage; _logger.LogDebug("图片下载缓存成功: {Url}, 大小: {Size}字节", fullImgUrl, imageBytes.Length); return newImage; } } catch (Exception ex) { _logger.LogError(ex, "下载图片失败: {ImageUrl}", imageUrl); } return GetPlaceholderImage(); } public async Task GetImagePathAsync(string imageUrl, string cacheKey = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(imageUrl)) return string.Empty; // 生成缓存键,如果未提供则使用URL的MD5值 var key = cacheKey ?? GenerateCacheKey(imageUrl); // 1. 检查内存缓存 if (_memoryPathCache.TryGetValue(key, out var cachedImage)) { //_logger.LogDebug("内存缓存命中: {Key}", key); return cachedImage; } // 2. 检查磁盘缓存 string filePath = GetCacheFilePath(key); if (File.Exists(filePath) && !IsCacheExpired(filePath)) { //_logger.LogDebug("磁盘缓存命中: {FilePath}", filePath); // 从磁盘加载并放入内存缓存 _memoryPathCache[key] = filePath; return filePath; } var downloadTask = _pendingDownloads.GetOrAdd(key, async k => { // 3. 从网络下载 try { var fullImgUrl = _configurationService.ApiSettings.FileUrl + imageUrl; _logger.LogDebug("开始下载图片: {Url}", fullImgUrl); // Flurl.Http 字节下载(连接级显式 TLS 1.2;非 2xx 会抛异常,由外层 catch 统一处理) byte[] imageBytes = await _httpClient.GetByteArrayWithTimeoutAsync(fullImgUrl, null, cancellationToken); if (imageBytes == null || imageBytes.Length == 0) { _logger.LogWarning("图片数据为空: {Url}", fullImgUrl); return string.Empty; } // 验证图片数据有效性 if (!IsValidImageData(imageBytes)) { _logger.LogWarning("图片数据无效: {Url}, 大小: {Size}字节", fullImgUrl, imageBytes.Length); return string.Empty; } // 保存到磁盘缓存 await SaveImageToDiskAsyncV2(imageBytes, filePath, fullImgUrl,cancellationToken); _memoryPathCache[key] = filePath; return filePath; } catch (Exception ex) { _logger.LogError(ex, "下载图片失败: {ImageUrl}", imageUrl); return string.Empty; } finally { // 清理已完成的任务(可选,防止长期驻留) _pendingDownloads.TryRemove(k, out _); } }); return await downloadTask; } #region 辅助方法 /// /// 从字节数组创建BitmapImage /// private async Task CreateBitmapImageFromBytesAsync(byte[] imageBytes) { return await Task.Run(() => { try { var bitmapImage = new BitmapImage(); var memoryStream = new MemoryStream(imageBytes); bitmapImage.BeginInit(); bitmapImage.CacheOption = BitmapCacheOption.OnLoad; //bitmapImage.CreateOptions = BitmapCreateOptions.IgnoreImageCache; bitmapImage.DecodePixelWidth = 1000; bitmapImage.StreamSource = memoryStream; bitmapImage.EndInit(); memoryStream.Close(); // 或 Dispose() // 必须冻结以实现线程安全 if (bitmapImage.CanFreeze) { bitmapImage.Freeze(); } return bitmapImage; } catch (Exception ex) { _logger.LogError("执行方法:{Method}加载图片失败:{Message}", nameof(LoadImageViaMemoryStreamAsync), ex.Message); return GetPlaceholderImage(); //throw new InvalidOperationException($"通过内存流加载图片失败: {filePath}", ex); } }); } /// /// 保存字节数组到磁盘 /// private async Task SaveImageToDiskAsync(byte[] imageBytes, string filePath) { try { // 使用临时文件避免写入过程中出错 var tempFilePath = filePath + ".tmp"; File.WriteAllBytes(tempFilePath, imageBytes); // 原子性操作:重命名临时文件 if (File.Exists(filePath)) { File.Delete(filePath); } File.Move(tempFilePath, filePath); _logger.LogDebug("图片保存成功: {FilePath}, 大小: {Size}字节", filePath, imageBytes.Length); } catch (Exception ex) { _logger.LogError(ex, "保存图片到磁盘失败: {FilePath}", filePath); // 清理临时文件 var tempFilePath = filePath + ".tmp"; if (File.Exists(tempFilePath)) { File.Delete(tempFilePath); } } } private async Task SaveImageToDiskAsyncV2(byte[] imageBytes, string targetPath, string fullpath, CancellationToken ct=default) { // 确保目录存在 Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!); // 使用临时文件 + 原子移动,避免读写冲突 string tempFile = Path.Combine(Path.GetDirectoryName(targetPath)!, Path.GetRandomFileName() + ".tmp"); try { File.WriteAllBytes(tempFile, imageBytes); // 先删除目标文件(如果存在),再移动(实现覆盖效果) if (File.Exists(targetPath)) { File.Delete(targetPath); } File.Move(tempFile, targetPath); // Windows 上同目录 Move 是原子的 _logger.LogDebug("图片保存成功: {Url} -> {Path}", fullpath, targetPath); } catch (Exception ex) { _logger.LogError(ex, "保存图片到磁盘失败: {Path}=>{Url}", targetPath, fullpath); throw; // 让上层捕获 } finally { // 清理残留临时文件 if (File.Exists(tempFile)) File.Delete(tempFile); } } /// /// 验证图片数据有效性 /// private bool IsValidImageData(byte[] imageData) { if (imageData == null || imageData.Length < 8) return false; // 检查常见的图片文件头 // JPEG: FF D8 FF if (imageData[0] == 0xFF && imageData[1] == 0xD8 && imageData[2] == 0xFF) return true; // PNG: 89 50 4E 47 if (imageData[0] == 0x89 && imageData[1] == 0x50 && imageData[2] == 0x4E && imageData[3] == 0x47) return true; // GIF: 47 49 46 if (imageData[0] == 0x47 && imageData[1] == 0x49 && imageData[2] == 0x46) return true; // BMP: 42 4D if (imageData[0] == 0x42 && imageData[1] == 0x4D) return true; // WebP: RIFF if (imageData[0] == 0x52 && imageData[1] == 0x49 && imageData[2] == 0x46 && imageData[3] == 0x46) return true; return false; } /// /// 优化的磁盘加载方法 /// private async Task LoadImageFromDiskAsync(string filePath) { if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) { return GetPlaceholderImage(); } try { // 直接读取文件字节数组 byte[] imageBytes = File.ReadAllBytes(filePath); if (imageBytes == null || imageBytes.Length == 0) { _logger.LogWarning("磁盘缓存文件为空: {FilePath}", filePath); // 删除损坏的文件 File.Delete(filePath); return GetPlaceholderImage(); } // 验证数据有效性 if (!IsValidImageData(imageBytes)) { _logger.LogWarning("磁盘缓存文件数据无效: {FilePath}", filePath); File.Delete(filePath); return GetPlaceholderImage(); } return await CreateBitmapImageFromBytesAsync(imageBytes); } catch (Exception ex) { _logger.LogError(ex, "从磁盘加载图片失败: {FilePath}", filePath); return GetPlaceholderImage(); } } /// /// 优化的内存流加载方法 /// private async Task LoadImageViaMemoryStreamAsync(string filePath) { return await Task.Run(() => { try { // 直接读取文件字节数组 byte[] imageData = File.ReadAllBytes(filePath); if (imageData == null || imageData.Length == 0) { _logger.LogWarning("文件为空: {FilePath}", filePath); return GetPlaceholderImage(); } using var memoryStream = new MemoryStream(imageData); var bitmapImage = new BitmapImage(); bitmapImage.BeginInit(); bitmapImage.CacheOption = BitmapCacheOption.OnLoad; bitmapImage.CreateOptions = BitmapCreateOptions.IgnoreImageCache; bitmapImage.StreamSource = memoryStream; bitmapImage.EndInit(); if (bitmapImage.Width > 0 && bitmapImage.Height > 0) { bitmapImage.Freeze(); return bitmapImage; } return GetPlaceholderImage(); } catch (Exception ex) { _logger.LogError(ex, "通过内存流加载图片失败: {FilePath}", filePath); return GetPlaceholderImage(); } }); } #endregion #region 清理和工具方法 public async Task ClearExpiredCacheAsync() { await Task.Run(() => { try { var directory = new DirectoryInfo(_cacheBaseDirectory); if (!directory.Exists) return; int deletedCount = 0; foreach (var file in directory.GetFiles("*.jpg")) { if (IsCacheExpired(file.FullName)) { try { file.Delete(); deletedCount++; } catch (Exception ex) { _logger.LogWarning(ex, "删除缓存文件失败: {FilePath}", file.FullName); } } } _logger.LogInformation("清理过期缓存完成,删除文件数: {DeletedCount}", deletedCount); } catch (Exception ex) { _logger.LogError(ex, "清理过期缓存失败"); } }); } public async Task ClearAllCacheAsync() { await Task.Run(() => { try { // 清空内存缓存 _memoryCache.Clear(); _memoryPathCache.Clear(); var directory = new DirectoryInfo(_cacheBaseDirectory); if (directory.Exists) { directory.Delete(true); Directory.CreateDirectory(_cacheBaseDirectory); _logger.LogInformation("清空所有缓存完成"); } } catch (Exception ex) { _logger.LogError(ex, "清空所有缓存失败"); } }); } private bool IsCacheExpired(string filePath) { var fileInfo = new FileInfo(filePath); return !fileInfo.Exists || (DateTime.Now - fileInfo.LastWriteTime) > _cacheDuration; } private string GenerateCacheKey(string url) { using (var md5 = System.Security.Cryptography.MD5.Create()) { byte[] inputBytes = System.Text.Encoding.UTF8.GetBytes(url); byte[] hashBytes = md5.ComputeHash(inputBytes); return Convert.ToBase64String(hashBytes).Replace("/", "_").Replace("+", "-").Substring(0, 10); } } #endregion #endregion private BitmapImage GetPlaceholderImage() { // 返回一个内置的占位图,或者根据需求返回null return new BitmapImage(); } } }