From 144117e24d9d1d6a3750e1d3f00cd95ebb0357e8 Mon Sep 17 00:00:00 2001 From: zpc Date: Wed, 28 Jan 2026 14:44:47 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=B0=86=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=9C=8D=E5=8A=A1=E7=A7=BB=E8=87=B3Infrastru?= =?UTF-8?q?cture=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 HoneyBox.Api 对 HoneyBox.Admin.Business 的引用 - 在 Core 层添加 IImageUploadService 接口 - 在 Infrastructure 层实现 CosImageUploadService - 从数据库 Configs 表读取 COS 配置 - 在 InfrastructureModule 注册服务 --- .../Controllers/UserController.cs | 9 +- .../src/HoneyBox.Api/HoneyBox.Api.csproj | 1 - .../Interfaces/IImageUploadService.cs | 15 ++ .../External/Storage/CosImageUploadService.cs | 199 ++++++++++++++++++ .../HoneyBox.Infrastructure.csproj | 1 + .../Modules/InfrastructureModule.cs | 6 + 6 files changed, 225 insertions(+), 6 deletions(-) create mode 100644 server/HoneyBox/src/HoneyBox.Core/Interfaces/IImageUploadService.cs create mode 100644 server/HoneyBox/src/HoneyBox.Infrastructure/External/Storage/CosImageUploadService.cs diff --git a/server/HoneyBox/src/HoneyBox.Api/Controllers/UserController.cs b/server/HoneyBox/src/HoneyBox.Api/Controllers/UserController.cs index 5d11fd65..7bcf645d 100644 --- a/server/HoneyBox/src/HoneyBox.Api/Controllers/UserController.cs +++ b/server/HoneyBox/src/HoneyBox.Api/Controllers/UserController.cs @@ -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 _logger; public UserController( @@ -34,7 +33,7 @@ public class UserController : ControllerBase IAssetService assetService, IVipService vipService, IQuanYiService quanYiService, - IUploadService uploadService, + IImageUploadService imageUploadService, ILogger 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; diff --git a/server/HoneyBox/src/HoneyBox.Api/HoneyBox.Api.csproj b/server/HoneyBox/src/HoneyBox.Api/HoneyBox.Api.csproj index a885174e..157d3a9c 100644 --- a/server/HoneyBox/src/HoneyBox.Api/HoneyBox.Api.csproj +++ b/server/HoneyBox/src/HoneyBox.Api/HoneyBox.Api.csproj @@ -28,7 +28,6 @@ - diff --git a/server/HoneyBox/src/HoneyBox.Core/Interfaces/IImageUploadService.cs b/server/HoneyBox/src/HoneyBox.Core/Interfaces/IImageUploadService.cs new file mode 100644 index 00000000..ac8c8f66 --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Core/Interfaces/IImageUploadService.cs @@ -0,0 +1,15 @@ +namespace HoneyBox.Core.Interfaces; + +/// +/// 图片上传服务接口 +/// +public interface IImageUploadService +{ + /// + /// 上传Base64编码的图片到云存储 + /// + /// Base64编码的图片数据(可包含data:image/xxx;base64,前缀) + /// 文件名前缀,如 "avatar" + /// 上传后的图片URL,失败返回null + Task UploadBase64ImageAsync(string base64Image, string fileNamePrefix = "image"); +} diff --git a/server/HoneyBox/src/HoneyBox.Infrastructure/External/Storage/CosImageUploadService.cs b/server/HoneyBox/src/HoneyBox.Infrastructure/External/Storage/CosImageUploadService.cs new file mode 100644 index 00000000..da461b88 --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Infrastructure/External/Storage/CosImageUploadService.cs @@ -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; + +/// +/// 腾讯云COS图片上传服务实现 +/// +public class CosImageUploadService : IImageUploadService +{ + private readonly HoneyBoxDbContext _dbContext; + private readonly ILogger _logger; + private const string UploadBasePath = "uploads"; + + public CosImageUploadService( + HoneyBoxDbContext dbContext, + ILogger logger) + { + _dbContext = dbContext; + _logger = logger; + } + + /// + public async Task 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 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(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; } + } +} diff --git a/server/HoneyBox/src/HoneyBox.Infrastructure/HoneyBox.Infrastructure.csproj b/server/HoneyBox/src/HoneyBox.Infrastructure/HoneyBox.Infrastructure.csproj index b313268c..8f756a29 100644 --- a/server/HoneyBox/src/HoneyBox.Infrastructure/HoneyBox.Infrastructure.csproj +++ b/server/HoneyBox/src/HoneyBox.Infrastructure/HoneyBox.Infrastructure.csproj @@ -16,6 +16,7 @@ + diff --git a/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/InfrastructureModule.cs b/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/InfrastructureModule.cs index db09ee1e..b08a8c77 100644 --- a/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/InfrastructureModule.cs +++ b/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/InfrastructureModule.cs @@ -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() .SingleInstance(); + // 注册图片上传服务 + builder.RegisterType() + .As() + .InstancePerLifetimeScope(); + // 后续可在此注册其他基础设施服务 // 如: 外部服务客户端、消息队列等 }