增加ImageCache微信 图片缓存

This commit is contained in:
chfei committed 2026-10-21 17:04:26 +08:00
1 parent d6cb5b966b
commit d0f9f00b76
15 files changed
+518 -30

No files matched your search

@@ -0,0 +1,107 @@
using Asp.Versioning;
using MAX.Framework.Dtos;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using MySqlX.XDevAPI.Common;
using Store.Framework.Base;
using Store.Framework.Models;
using Store.ImageCache.Dtos;
using Store.ImageCache.Services;
using System;
using System.Collections.Generic;
namespace KJD_Web.Controllers
{
[ApiVersionNeutral]
[Route("api/ImageCache/[action]")]
public class ImageCacheController : AppControllerBase
{
private readonly ImageCacheService _imageCacheService;
private readonly ILogger<ImageCacheController> _logger;
public ImageCacheController(ImageCacheService imageCacheService, ILogger<ImageCacheController> logger)
{
if (imageCacheService == null)
{
throw new ArgumentNullException("imageCacheService");
}
_imageCacheService = imageCacheService;
_logger = logger;
}
/// <summary>
/// 批量按 MD5 查询图片缓存。
/// </summary>
[HttpPost]
public ApiResult<List<ImageCacheDto>> GetImageCache(ImageCacheBatchQueryRequest request)
{
List<ImageCacheDto> data = new List<ImageCacheDto>();
try
{
if (request != null && request.Md5s != null)
{
data = _imageCacheService.GetByMd5s(request.Md5s, request.Platform);
}
return ApiResult.Ok(data);
}
catch (ArgumentException ex)
{
return ApiResult.Fail(ex.Message);
}
}
/// <summary>
/// 按 MD5 和平台更新平台图片 URL。
/// </summary>
[HttpPost]
public ApiResult<List<string>> AddOrUpdate(ImageCacheUpdateUrlRequest[] requests)
{
if (requests == null || requests.Length == 0)
{
return ApiResult.Fail("参数不能为空");
}
var failedItems = new List<string>();
foreach (var item in requests)
{
if (item == null)
{
return ApiResult.Fail("图片参数不能包含空项", ResultCode.ValidationError);
}
try
{
if (!_imageCacheService.AddOrUpdate(item.Md5, item.Platform, item.PlatformUrl))
{
failedItems.Add(item.Md5);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "AddOrUpdate 失败, Md5={Md5}", item.Md5);
failedItems.Add(item.Md5);
}
}
return failedItems.Count == 0
? ApiResult.Ok()
: ApiResult.Fail("部分图片失败", failedItems);
}
public class ImageCacheBatchQueryRequest
{
public List<string> Md5s { get; set; }
public byte? Platform { get; set; }
}
public class ImageCacheUpdateUrlRequest
{
public string Md5 { get; set; }
public byte Platform { get; set; }
public string PlatformUrl { get; set; }
}
}
}
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization;
namespace Store.ImageCache.Dtos
{
public class ImageCacheDto
{
public byte platform { get; set; }
public string md5 { get; set; }
public string platform_url { get; set; }
}
}
@@ -0,0 +1,32 @@
using FreeSql.DataAnnotations;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization;
namespace Store.ImageCache.Entities
{
[Table(Name = "images_0")]
public class ImageCacheEntity
{
[JsonIgnore]
[Column(Name = "id", IsIdentity = true, IsPrimary = true)]
public long id { get; set; }
[Column(Name = "platform")]
public byte platform { get; set; }
//[JsonProperty("md5")]
[Column(Name = "source_url_md5", StringLength = 32)]
public string source_url_md5 { get; set; }
[Column(Name = "platform_url", StringLength = 1000)]
public string platform_url { get; set; }
[JsonIgnore]
[Column(Name = "create_time")]
public DateTime create_time { get; set; }
[JsonIgnore]
[Column(Name = "update_time")]
public DateTime update_time { get; set; }
}
}
@@ -0,0 +1,17 @@
using Riok.Mapperly.Abstractions;
using Store.Framework.Models;
using Store.ImageCache.Dtos;
using Store.ImageCache.Entities;
using System;
using System.Collections.Generic;
using System.Text;
namespace Store.ImageCache.Mapper
{
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
public partial class ImageCacheMapper
{
[MapProperty(nameof(ImageCacheEntity.source_url_md5), nameof(ImageCacheDto.md5))]
public partial ImageCacheDto ToDto(ImageCacheEntity entity);
}
}
+47
View File
@@ -0,0 +1,47 @@
using MAX.Core.Interfaces;
using MAX.Framework.Interfaces;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MySql.Data.MySqlClient;
namespace Store.Modules.CloudProduct
{
public class Module : IModule
{
/// <summary>
/// 本模块自己的配置文件
/// </summary>
public const string ConfigurationFileName = "appsettings.ImageCache.json";
public int Priority => IntPriority.Normal;
/// <summary>
/// 服务注册阶段(builder.Build() 之前):这里只能拿到容器和配置,碰不到 app
/// </summary>
public void AddModule(IHostApplicationBuilder host)
{
IServiceCollection services = host.Services;
IConfigurationManager configuration = host.Configuration;
if (!string.IsNullOrWhiteSpace(ConfigurationFileName))
{
string path = Path.IsPathRooted(ConfigurationFileName)? ConfigurationFileName: Path.Combine(AppContext.BaseDirectory, ConfigurationFileName);
configuration.AddJsonFile(path, optional: false, reloadOnChange: true);
}
}
/// <summary>
/// 管道装配阶段(builder.Build() 之后、app.Run() 之前):这里可以挂中间件、解析容器里的服务
/// </summary>
public void UseModule(IApplicationBuilder app)
{
}
}
}
@@ -0,0 +1,172 @@
using MAX.Core.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Store.ImageCache.Dtos;
using Store.ImageCache.Entities;
using Store.ImageCache.Mapper;
using System;
using System.Collections.Generic;
using System.Text;
namespace Store.ImageCache.Services
{
public class ImageCacheService : ISingletonDependency
{
private const int Md5Length = 32;
private const int PlatformUrlMaxLength = 1000;
private readonly IFreeSql _freeSql;
private ImageCacheMapper mapper;
public ImageCacheService([FromKeyedServices("ImageCacheConnection")] IFreeSql freeSql)
{
mapper = new ImageCacheMapper();
if (freeSql == null)
{
throw new ArgumentNullException("freeSql");
}
_freeSql = freeSql;
}
/// <summary>
/// 按 MD5 查询图片缓存;platform 为空时返回该 MD5 在所有平台的缓存。
/// </summary>
public ImageCacheDto GetByMd5(string sourceUrlMd5, byte? platform = null)
{
var md5 = NormalizeMd5(sourceUrlMd5);
return GetByMd5sInternal(new List<string> { md5 }, platform).FirstOrDefault();
}
/// <summary>
/// 批量按 MD5 查询图片缓存。MD5 会按首字符分组,只查询对应的 images_0 到 images_f 分表。
/// </summary>
public List<ImageCacheDto> GetByMd5s(IEnumerable<string> sourceUrlMd5s, byte? platform = null)
{
return GetByMd5sInternal(NormalizeMd5s(sourceUrlMd5s), platform);
}
/// <summary>
/// 按 MD5 和平台直接修改平台图片 URL,未找到时返回 false。
/// </summary>
public bool AddOrUpdate(string sourceUrlMd5, byte platform, string platformUrl)
{
var md5 = NormalizeMd5(sourceUrlMd5);
ValidatePlatformUrl(platformUrl);
var tableName = GetTableName(md5[0]);
var existing = _freeSql.Select<ImageCacheEntity>()
.AsTable((type, oldName) => tableName)
.Where(a => a.source_url_md5 == sourceUrlMd5 && a.platform == platform)
.First();
var now = DateTime.Now;
if (existing != null)
{
var affected = _freeSql.Update<ImageCacheEntity>()
.AsTable(tableName)
.Set(a => a.platform_url, platformUrl)
.Set(a => a.update_time, now)
.Where(a => a.source_url_md5 == md5 && a.platform == platform)
.ExecuteAffrows();
return affected > 0;
}
else
{
ImageCacheEntity imageCache = new ImageCacheEntity();
imageCache.id = 0;
imageCache.source_url_md5 = md5;
imageCache.platform = platform;
imageCache.platform_url = platformUrl;
imageCache.create_time = now;
imageCache.update_time = now;
imageCache.id = _freeSql.Insert(imageCache)
.AsTable(tableName)
.ExecuteIdentity();
return imageCache.id > 0;
}
}
private List<ImageCacheDto> GetByMd5sInternal(List<string> md5s, byte? platform)
{
var result = new List<ImageCacheDto>();
if (md5s.Count == 0)
{
return result;
}
foreach (var shard in md5s.GroupBy(a => a[0]))
{
var tableName = GetTableName(shard.Key);
var md5sInTable = shard.ToList();
var query = _freeSql.Select<ImageCacheEntity>()
.AsTable((type, oldName) => tableName)
.Where(a => md5sInTable.Contains(a.source_url_md5));
if (platform.HasValue)
{
var platformValue = platform.Value;
query = query.Where(a => a.platform == platformValue);
}
result.AddRange(query.ToList().Select(mapper.ToDto));
}
return result;
}
private List<string> NormalizeMd5s(IEnumerable<string> sourceUrlMd5s)
{
if (sourceUrlMd5s == null)
{
throw new ArgumentNullException("sourceUrlMd5s");
}
var result = new List<string>();
foreach (var sourceUrlMd5 in sourceUrlMd5s)
{
var md5 = NormalizeMd5(sourceUrlMd5);
if (!result.Contains(md5))
{
result.Add(md5);
}
}
return result;
}
private string NormalizeMd5(string sourceUrlMd5)
{
if (string.IsNullOrWhiteSpace(sourceUrlMd5))
{
throw new ArgumentException("MD5 不能为空", "sourceUrlMd5");
}
var md5 = sourceUrlMd5.Trim().ToLowerInvariant();
if (md5.Length != Md5Length || md5.Any(a => !Uri.IsHexDigit(a)))
{
throw new ArgumentException("MD5 必须是 32 位十六进制字符串", "sourceUrlMd5");
}
return md5;
}
private string GetTableName(char shard)
{
return "images_" + shard;
}
private void ValidatePlatformUrl(string platformUrl)
{
if (string.IsNullOrWhiteSpace(platformUrl))
{
throw new ArgumentException("平台图片 URL 不能为空", "platformUrl");
}
if (platformUrl.Length > PlatformUrlMaxLength)
{
throw new ArgumentException("平台图片 URL 长度不能超过 1000", "platformUrl");
}
}
}
}
+45
View File
@@ -0,0 +1,45 @@
using MAX.Core.DependencyInjection;
using MAX.Core.Interfaces;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.ComponentModel;
namespace Store.Modules.CloudProduct
{
public class Startup : IStartup
{
public int Priority => IntPriority.Normal;
public void Initialize()
{
}
public void Register(IServiceRegistrar registrar)
{
Func<IServiceProvider, IFreeSql> KJDFsqlFactory = r =>
{
var config = r.GetRequiredService<IConfiguration>();
var connection = config.GetValue<string>("ImageCache:ConnectionString");
IFreeSql fsql = new FreeSql.FreeSqlBuilder()
.UseConnectionString(FreeSql.DataType.MySql, connection)
.UseAdoConnectionPool(true)
.Build();
return fsql;
};
registrar.RegisterSingleton<IFreeSql>("ImageCacheConnection", (s, k) =>
{
return KJDFsqlFactory(s);
});
}
public void Start(IServiceProvider serviceProvider)
{
}
}
}
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Store.Framework\Store.Framework.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.ImageCache.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,5 @@
{
"ImageCache": {
"ConnectionString": "Server=192.168.0.200;Port=3306;Database=ImageCache;Uid=root;Pwd=123456;Allow User Variables=true"
}
}