mi-assessment/server/MiAssessment/src/MiAssessment.Admin.Business/Services/UploadService.cs
2026-02-03 14:25:01 +08:00

270 lines
8.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"
};
}
}