refactor: 将图片上传服务移至Infrastructure层

- 移除 HoneyBox.Api 对 HoneyBox.Admin.Business 的引用
- 在 Core 层添加 IImageUploadService 接口
- 在 Infrastructure 层实现 CosImageUploadService
- 从数据库 Configs 表读取 COS 配置
- 在 InfrastructureModule 注册服务
This commit is contained in:
zpc 2026-01-28 14:44:47 +08:00
parent a95a2852d4
commit 144117e24d
6 changed files with 225 additions and 6 deletions

View File

@ -1,5 +1,4 @@
using System.Security.Claims;
using HoneyBox.Admin.Business.Services.Interfaces;
using HoneyBox.Core.Interfaces;
using HoneyBox.Model.Base;
using HoneyBox.Model.Models.Asset;
@ -25,7 +24,7 @@ public class UserController : ControllerBase
private readonly IAssetService _assetService;
private readonly IVipService _vipService;
private readonly IQuanYiService _quanYiService;
private readonly IUploadService _uploadService;
private readonly IImageUploadService _imageUploadService;
private readonly ILogger<UserController> _logger;
public UserController(
@ -34,7 +33,7 @@ public class UserController : ControllerBase
IAssetService assetService,
IVipService vipService,
IQuanYiService quanYiService,
IUploadService uploadService,
IImageUploadService imageUploadService,
ILogger<UserController> logger)
{
_userService = userService;
@ -42,7 +41,7 @@ public class UserController : ControllerBase
_assetService = assetService;
_vipService = vipService;
_quanYiService = quanYiService;
_uploadService = uploadService;
_imageUploadService = imageUploadService;
_logger = logger;
}
@ -170,7 +169,7 @@ public class UserController : ControllerBase
if (!string.IsNullOrWhiteSpace(request.Imagebase))
{
// Base64图片上传到腾讯云COS
var headimgUrl = await _uploadService.UploadBase64ImageAsync(request.Imagebase, $"avatar_{userId}");
var headimgUrl = await _imageUploadService.UploadBase64ImageAsync(request.Imagebase, $"avatar_{userId}");
if (!string.IsNullOrWhiteSpace(headimgUrl))
{
updateDto.Headimg = headimgUrl;

View File

@ -28,7 +28,6 @@
<ProjectReference Include="..\HoneyBox.Model\HoneyBox.Model.csproj" />
<ProjectReference Include="..\HoneyBox.Core\HoneyBox.Core.csproj" />
<ProjectReference Include="..\HoneyBox.Infrastructure\HoneyBox.Infrastructure.csproj" />
<ProjectReference Include="..\HoneyBox.Admin.Business\HoneyBox.Admin.Business.csproj" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,15 @@
namespace HoneyBox.Core.Interfaces;
/// <summary>
/// 图片上传服务接口
/// </summary>
public interface IImageUploadService
{
/// <summary>
/// 上传Base64编码的图片到云存储
/// </summary>
/// <param name="base64Image">Base64编码的图片数据可包含data:image/xxx;base64,前缀)</param>
/// <param name="fileNamePrefix">文件名前缀,如 "avatar"</param>
/// <returns>上传后的图片URL失败返回null</returns>
Task<string?> UploadBase64ImageAsync(string base64Image, string fileNamePrefix = "image");
}

View File

@ -0,0 +1,199 @@
using COSXML;
using COSXML.Auth;
using COSXML.Model.Object;
using HoneyBox.Core.Interfaces;
using HoneyBox.Model.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace HoneyBox.Infrastructure.External.Storage;
/// <summary>
/// 腾讯云COS图片上传服务实现
/// </summary>
public class CosImageUploadService : IImageUploadService
{
private readonly HoneyBoxDbContext _dbContext;
private readonly ILogger<CosImageUploadService> _logger;
private const string UploadBasePath = "uploads";
public CosImageUploadService(
HoneyBoxDbContext dbContext,
ILogger<CosImageUploadService> logger)
{
_dbContext = dbContext;
_logger = logger;
}
/// <inheritdoc />
public async Task<string?> UploadBase64ImageAsync(string base64Image, string fileNamePrefix = "image")
{
try
{
if (string.IsNullOrWhiteSpace(base64Image))
{
_logger.LogWarning("Base64图片数据为空");
return null;
}
// 解析Base64数据和MIME类型
string base64Data;
string contentType = "image/png";
string extension = ".png";
if (base64Image.Contains(','))
{
var parts = base64Image.Split(',');
base64Data = parts[1];
var header = parts[0];
if (header.Contains(':') && header.Contains(';'))
{
var mimeType = header.Split(':')[1].Split(';')[0];
contentType = mimeType;
extension = mimeType switch
{
"image/jpeg" => ".jpg",
"image/jpg" => ".jpg",
"image/png" => ".png",
"image/gif" => ".gif",
"image/webp" => ".webp",
_ => ".png"
};
}
}
else
{
base64Data = base64Image;
}
// 解码Base64数据
byte[] imageBytes;
try
{
imageBytes = Convert.FromBase64String(base64Data);
}
catch (FormatException ex)
{
_logger.LogWarning(ex, "Base64解码失败");
return null;
}
// 验证文件大小 (10MB)
if (imageBytes.Length > 10 * 1024 * 1024)
{
_logger.LogWarning("Base64图片大小超过限制: {Size} bytes", imageBytes.Length);
return null;
}
// 获取COS配置
var setting = await GetCosSettingAsync();
if (setting == null)
{
_logger.LogError("无法获取COS配置");
return null;
}
// 生成日期目录路径
var now = DateTime.Now;
var datePath = $"{now.Year}/{now.Month:D2}/{now.Day:D2}";
var uniqueFileName = $"{fileNamePrefix}_{now:yyyyMMddHHmmssfff}_{Guid.NewGuid():N}"[..32] + extension;
var objectKey = $"{UploadBasePath}/{datePath}/{uniqueFileName}";
// 创建COS客户端并上传
var cosXml = CreateCosXmlServer(setting);
var putObjectRequest = new PutObjectRequest(setting.Bucket!, objectKey, imageBytes);
putObjectRequest.SetRequestHeader("Content-Type", contentType);
var result = cosXml.PutObject(putObjectRequest);
if (result.IsSuccessful())
{
var url = GenerateAccessUrl(setting.Domain!, objectKey);
_logger.LogInformation("Base64图片上传成功: {Url}", url);
return url;
}
else
{
_logger.LogError("COS上传失败: {Error}", result.httpMessage);
return null;
}
}
catch (COSXML.CosException.CosClientException clientEx)
{
_logger.LogError(clientEx, "COS客户端错误");
return null;
}
catch (COSXML.CosException.CosServerException serverEx)
{
_logger.LogError(serverEx, "COS服务端错误: {Info}", serverEx.GetInfo());
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Base64图片上传异常");
return null;
}
}
private async Task<CosUploadSetting?> GetCosSettingAsync()
{
try
{
var config = await _dbContext.Configs
.Where(c => c.ConfigKey == "uploads")
.Select(c => c.ConfigValue)
.FirstOrDefaultAsync();
if (string.IsNullOrEmpty(config))
{
return null;
}
return JsonConvert.DeserializeObject<CosUploadSetting>(config);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取COS配置失败");
return null;
}
}
private static CosXml CreateCosXmlServer(CosUploadSetting setting)
{
var config = new CosXmlConfig.Builder()
.IsHttps(true)
.SetRegion(setting.Region!)
.Build();
var credentialProvider = new DefaultQCloudCredentialProvider(
setting.AccessKeyId!,
setting.AccessKeySecret!,
600);
return new CosXmlServer(config, credentialProvider);
}
private static string GenerateAccessUrl(string domain, string objectKey)
{
var normalizedDomain = domain.TrimEnd('/');
if (!normalizedDomain.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
!normalizedDomain.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
normalizedDomain = $"https://{normalizedDomain}";
}
var normalizedKey = objectKey.StartsWith('/') ? objectKey : $"/{objectKey}";
return $"{normalizedDomain}{normalizedKey}";
}
private class CosUploadSetting
{
public string? Type { get; set; }
public string? AppId { get; set; }
public string? Bucket { get; set; }
public string? Region { get; set; }
public string? AccessKeyId { get; set; }
public string? AccessKeySecret { get; set; }
public string? Domain { get; set; }
}
}

View File

@ -16,6 +16,7 @@
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="StackExchange.Redis" Version="2.10.1" />
<PackageReference Include="Tencent.QCloud.Cos.Sdk" Version="5.4.44" />
</ItemGroup>
</Project>

View File

@ -1,6 +1,7 @@
using Autofac;
using HoneyBox.Core.Interfaces;
using HoneyBox.Infrastructure.Cache;
using HoneyBox.Infrastructure.External.Storage;
namespace HoneyBox.Infrastructure.Modules;
@ -21,6 +22,11 @@ public class InfrastructureModule : Module
.As<IRedisService>()
.SingleInstance();
// 注册图片上传服务
builder.RegisterType<CosImageUploadService>()
.As<IImageUploadService>()
.InstancePerLifetimeScope();
// 后续可在此注册其他基础设施服务
// 如: 外部服务客户端、消息队列等
}