兼容win7版应用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

569 lines
22 KiB

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<string, BitmapImage> _memoryCache;
private readonly ConcurrentDictionary<string, string> _memoryPathCache;
// 缓存正在下载或已完成的任务(URL/Key -> Task<string>)
private readonly ConcurrentDictionary<string, Task<string>> _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<RemoteImageCacheService> _logger;
private string GetCacheFilePath(string cacheKey) => Path.Combine(_cacheBaseDirectory, $"{cacheKey}.jpg");
public RemoteImageCacheService(IConfigurationService configurationService, IHttpClientService httpClient, ILogger<RemoteImageCacheService> logger)
{
_configurationService = configurationService;
_memoryCache = new ConcurrentDictionary<string, BitmapImage>();
_memoryPathCache = new ConcurrentDictionary<string, string>();
_httpClient = httpClient;
_logger = logger;
// 设置缓存目录,例如在用户本地应用数据文件夹下
_cacheBaseDirectory = CommonString.ImageCacheDirectory;
Directory.CreateDirectory(_cacheBaseDirectory); // 确保目录存在
_cacheDuration = _configurationService.CacheSettings.ImageCacheExpiry;
}
#region 基础方法
public async Task<BitmapImage> 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();
}
/// <summary>
/// 若本地图片存在,直接加载图片(去掉读取文件流操作)优化速率大概是280ms->10ms
/// </summary>
/// <param name="imageUrl"></param>
/// <param name="cacheKey"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<BitmapImage> 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<string> 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 辅助方法
/// <summary>
/// 从字节数组创建BitmapImage
/// </summary>
private async Task<BitmapImage> 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);
}
});
}
/// <summary>
/// 保存字节数组到磁盘
/// </summary>
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);
}
}
/// <summary>
/// 验证图片数据有效性
/// </summary>
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;
}
/// <summary>
/// 优化的磁盘加载方法
/// </summary>
private async Task<BitmapImage> 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();
}
}
/// <summary>
/// 优化的内存流加载方法
/// </summary>
private async Task<BitmapImage> 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();
}
}
}