using MiAssessment.Admin.Business.Models; using MiAssessment.Admin.Business.Models.Config; using MiAssessment.Admin.Business.Models.Upload; using MiAssessment.Admin.Business.Services.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace MiAssessment.Admin.Business.Services; /// /// 上传服务实现 /// 负责文件验证、存储提供者选择和文件上传 /// public class UploadService : IUploadService { private readonly IAdminConfigService _configService; private readonly IEnumerable _storageProviders; private readonly ILogger _logger; /// /// 允许的图片格式 /// private static readonly HashSet AllowedExtensions = new(StringComparer.OrdinalIgnoreCase) { ".jpg", ".jpeg", ".png", ".gif", ".webp" }; /// /// 允许的MIME类型 /// private static readonly HashSet AllowedMimeTypes = new(StringComparer.OrdinalIgnoreCase) { "image/jpeg", "image/png", "image/gif", "image/webp" }; /// /// 最大文件大小 (10MB) /// private const long MaxFileSize = 10 * 1024 * 1024; /// /// 默认存储类型 (本地存储) /// private const string DefaultStorageType = "1"; public UploadService( IAdminConfigService configService, IEnumerable storageProviders, ILogger logger) { _configService = configService; _storageProviders = storageProviders; _logger = logger; } /// public async Task UploadImageAsync(IFormFile file) { // 验证文件 var validationError = ValidateFile(file); if (validationError != null) { throw new BusinessException(BusinessErrorCodes.ValidationFailed, validationError); } // 获取存储提供者 var provider = await GetStorageProviderAsync(); // 上传文件 await using var stream = file.OpenReadStream(); var result = await provider.UploadAsync(stream, file.FileName, file.ContentType); if (!result.Success) { throw new BusinessException(BusinessErrorCodes.OperationFailed, result.ErrorMessage ?? "上传失败"); } return new UploadResponse { Url = result.Url!, FileName = file.FileName, FileSize = file.Length }; } /// public async Task> UploadImagesAsync(List files) { if (files == null || files.Count == 0) { throw new BusinessException(BusinessErrorCodes.ValidationFailed, "请选择要上传的文件"); } var results = new List(); foreach (var file in files) { var response = await UploadImageAsync(file); results.Add(response); } return results; } /// /// 验证文件 /// /// 上传的文件 /// 错误信息,null表示验证通过 public static string? ValidateFile(IFormFile? file) { // 检查文件是否为空 if (file == null || file.Length == 0) { return "请选择要上传的文件"; } // 检查文件大小 if (file.Length > MaxFileSize) { return "文件大小不能超过10MB"; } // 检查文件扩展名 var extension = Path.GetExtension(file.FileName); if (string.IsNullOrEmpty(extension) || !AllowedExtensions.Contains(extension)) { return "只支持 jpg、jpeg、png、gif、webp 格式的图片"; } // 检查MIME类型 if (!string.IsNullOrEmpty(file.ContentType) && !AllowedMimeTypes.Contains(file.ContentType)) { return "只支持 jpg、jpeg、png、gif、webp 格式的图片"; } return null; } /// /// 检查文件扩展名是否有效 /// /// 文件扩展名(包含点号) /// 是否有效 public static bool IsValidExtension(string? extension) { if (string.IsNullOrEmpty(extension)) { return false; } return AllowedExtensions.Contains(extension); } /// /// 检查文件大小是否有效 /// /// 文件大小(字节) /// 是否有效 public static bool IsValidFileSize(long fileSize) { return fileSize > 0 && fileSize <= MaxFileSize; } /// /// 生成唯一文件名 /// 格式: {timestamp}_{guid}{extension} /// /// 原始文件名 /// 唯一文件名 public static string GenerateUniqueFileName(string originalFileName) { var extension = Path.GetExtension(originalFileName).ToLowerInvariant(); var timestamp = DateTime.Now.ToString("yyyyMMddHHmmssfff"); var guid = Guid.NewGuid().ToString("N")[..8]; // 取GUID前8位 return $"{timestamp}_{guid}{extension}"; } /// /// 获取存储提供者 /// 根据配置选择存储提供者,如果配置无效则使用本地存储 /// private async Task GetStorageProviderAsync() { // 获取上传配置 var uploadSetting = await _configService.GetConfigAsync(ConfigKeys.Uploads); var storageType = uploadSetting?.Type ?? DefaultStorageType; // 查找对应的存储提供者 var provider = _storageProviders.FirstOrDefault(p => p.StorageType == storageType); // 如果找不到对应的提供者,使用本地存储作为降级 if (provider == null) { _logger.LogWarning("未找到存储类型 {StorageType} 的提供者,使用本地存储", storageType); provider = _storageProviders.FirstOrDefault(p => p.StorageType == DefaultStorageType); } // 如果连本地存储都没有,抛出异常 if (provider == null) { throw new BusinessException(BusinessErrorCodes.ConfigurationError, "存储配置无效,请检查上传配置"); } _logger.LogDebug("使用存储提供者: {StorageType}", provider.StorageType); return provider; } /// public async Task GetPresignedUploadUrlAsync(GetPresignedUrlRequest request) { // 验证请求参数 if (string.IsNullOrWhiteSpace(request.FileName)) { throw new BusinessException(BusinessErrorCodes.ValidationFailed, "文件名不能为空"); } // 验证文件扩展名 var extension = Path.GetExtension(request.FileName); if (!IsValidExtension(extension)) { throw new BusinessException(BusinessErrorCodes.ValidationFailed, "只支持 jpg、jpeg、png、gif、webp 格式的图片"); } // 验证文件大小 if (request.FileSize > 0 && !IsValidFileSize(request.FileSize)) { throw new BusinessException(BusinessErrorCodes.ValidationFailed, "文件大小不能超过10MB"); } // 获取存储提供者 var provider = await GetStorageProviderAsync(); // 检查是否支持客户端直传 if (!provider.SupportsDirectUpload) { _logger.LogDebug("当前存储提供者不支持客户端直传,将使用服务端上传"); return null; } // 获取预签名URL var contentType = request.ContentType; if (string.IsNullOrWhiteSpace(contentType)) { contentType = GetContentTypeByExtension(extension); } var result = await provider.GetPresignedUploadUrlAsync(request.FileName, contentType); if (result == null) { throw new BusinessException(BusinessErrorCodes.OperationFailed, "获取上传URL失败,请检查存储配置"); } return result; } /// /// 根据扩展名获取ContentType /// private static string GetContentTypeByExtension(string extension) { return extension.ToLowerInvariant() switch { ".jpg" or ".jpeg" => "image/jpeg", ".png" => "image/png", ".gif" => "image/gif", ".webp" => "image/webp", _ => "application/octet-stream" }; } }