270 lines
8.7 KiB
C#
270 lines
8.7 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// 上传服务实现
|
||
/// 负责文件验证、存储提供者选择和文件上传
|
||
/// </summary>
|
||
public class UploadService : IUploadService
|
||
{
|
||
private readonly IAdminConfigService _configService;
|
||
private readonly IEnumerable<IStorageProvider> _storageProviders;
|
||
private readonly ILogger<UploadService> _logger;
|
||
|
||
/// <summary>
|
||
/// 允许的图片格式
|
||
/// </summary>
|
||
private static readonly HashSet<string> AllowedExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
".jpg", ".jpeg", ".png", ".gif", ".webp"
|
||
};
|
||
|
||
/// <summary>
|
||
/// 允许的MIME类型
|
||
/// </summary>
|
||
private static readonly HashSet<string> AllowedMimeTypes = new(StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
"image/jpeg", "image/png", "image/gif", "image/webp"
|
||
};
|
||
|
||
/// <summary>
|
||
/// 最大文件大小 (10MB)
|
||
/// </summary>
|
||
private const long MaxFileSize = 10 * 1024 * 1024;
|
||
|
||
/// <summary>
|
||
/// 默认存储类型 (本地存储)
|
||
/// </summary>
|
||
private const string DefaultStorageType = "1";
|
||
|
||
public UploadService(
|
||
IAdminConfigService configService,
|
||
IEnumerable<IStorageProvider> storageProviders,
|
||
ILogger<UploadService> logger)
|
||
{
|
||
_configService = configService;
|
||
_storageProviders = storageProviders;
|
||
_logger = logger;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<UploadResponse> 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
|
||
};
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<List<UploadResponse>> UploadImagesAsync(List<IFormFile> files)
|
||
{
|
||
if (files == null || files.Count == 0)
|
||
{
|
||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "请选择要上传的文件");
|
||
}
|
||
|
||
var results = new List<UploadResponse>();
|
||
foreach (var file in files)
|
||
{
|
||
var response = await UploadImageAsync(file);
|
||
results.Add(response);
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证文件
|
||
/// </summary>
|
||
/// <param name="file">上传的文件</param>
|
||
/// <returns>错误信息,null表示验证通过</returns>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查文件扩展名是否有效
|
||
/// </summary>
|
||
/// <param name="extension">文件扩展名(包含点号)</param>
|
||
/// <returns>是否有效</returns>
|
||
public static bool IsValidExtension(string? extension)
|
||
{
|
||
if (string.IsNullOrEmpty(extension))
|
||
{
|
||
return false;
|
||
}
|
||
return AllowedExtensions.Contains(extension);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查文件大小是否有效
|
||
/// </summary>
|
||
/// <param name="fileSize">文件大小(字节)</param>
|
||
/// <returns>是否有效</returns>
|
||
public static bool IsValidFileSize(long fileSize)
|
||
{
|
||
return fileSize > 0 && fileSize <= MaxFileSize;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成唯一文件名
|
||
/// 格式: {timestamp}_{guid}{extension}
|
||
/// </summary>
|
||
/// <param name="originalFileName">原始文件名</param>
|
||
/// <returns>唯一文件名</returns>
|
||
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}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取存储提供者
|
||
/// 根据配置选择存储提供者,如果配置无效则使用本地存储
|
||
/// </summary>
|
||
private async Task<IStorageProvider> GetStorageProviderAsync()
|
||
{
|
||
// 获取上传配置
|
||
var uploadSetting = await _configService.GetConfigAsync<UploadSetting>(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;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<PresignedUrlResponse?> 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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据扩展名获取ContentType
|
||
/// </summary>
|
||
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"
|
||
};
|
||
}
|
||
}
|