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