308 lines
11 KiB
C#
308 lines
11 KiB
C#
using COSXML;
|
||
using COSXML.Auth;
|
||
using COSXML.Common;
|
||
using COSXML.Model.Object;
|
||
|
||
using LiveForum.Code.Base;
|
||
using LiveForum.IService.Others;
|
||
using LiveForum.Model.Dto.Others;
|
||
|
||
using Microsoft.Extensions.Options;
|
||
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace LiveForum.Service.Others
|
||
{
|
||
/// <summary>
|
||
/// 腾讯云COS文件上传服务
|
||
/// </summary>
|
||
public class CosFileUploadService : IFileUploadService
|
||
{
|
||
private readonly TencentCosConfig _config;
|
||
private readonly CosXml _cosXml;
|
||
private readonly CosXmlConfig _cosXmlConfig;
|
||
|
||
// 允许的文件扩展名
|
||
private readonly string[] _allowedImageExtensions = { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp" };
|
||
private readonly string[] _allowedVideoExtensions = { ".mp4", ".avi", ".mov", ".wmv", ".flv", ".mkv" };
|
||
|
||
/// <summary>
|
||
/// 构造函数
|
||
/// </summary>
|
||
/// <param name="config">COS配置</param>
|
||
public CosFileUploadService(IOptions<TencentCosConfig> config)
|
||
{
|
||
_config = config.Value;
|
||
|
||
// 初始化COS配置
|
||
_cosXmlConfig = new CosXmlConfig.Builder()
|
||
.SetRegion(_config.Region)
|
||
.Build();
|
||
|
||
// 创建凭证提供者
|
||
var qCloudCredentialProvider = new DefaultQCloudCredentialProvider(
|
||
_config.SecretId,
|
||
_config.SecretKey,
|
||
_config.DurationSecond);
|
||
|
||
// 初始化COS客户端
|
||
_cosXml = new CosXmlServer(_cosXmlConfig, qCloudCredentialProvider);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 上传文件
|
||
/// </summary>
|
||
/// <param name="request">请求参数</param>
|
||
/// <returns></returns>
|
||
public async Task<BaseResponse<UploadFileRespDto>> UploadFile(UploadFileReq request)
|
||
{
|
||
try
|
||
{
|
||
// 验证文件
|
||
if (request.File == null || request.File.Length == 0)
|
||
{
|
||
return new BaseResponse<UploadFileRespDto>(ResponseCode.Error, "文件不能为空");
|
||
}
|
||
|
||
// 验证文件类型
|
||
var extension = Path.GetExtension(request.File.FileName)?.ToLower();
|
||
if (string.IsNullOrEmpty(extension))
|
||
{
|
||
return new BaseResponse<UploadFileRespDto>(ResponseCode.Error, "文件格式无效");
|
||
}
|
||
|
||
// 判断是图片还是视频
|
||
bool isImage = _allowedImageExtensions.Contains(extension);
|
||
bool isVideo = _allowedVideoExtensions.Contains(extension);
|
||
|
||
if (!isImage && !isVideo)
|
||
{
|
||
return new BaseResponse<UploadFileRespDto>(ResponseCode.Error, "不支持的文件格式");
|
||
}
|
||
|
||
// 验证文件大小(使用配置中的最大大小限制)
|
||
long maxSizeBytes = _config.MaxSize * 1024 * 1024; // 转换为字节
|
||
if (request.File.Length > maxSizeBytes)
|
||
{
|
||
return new BaseResponse<UploadFileRespDto>(ResponseCode.Error, $"文件大小不能超过 {_config.MaxSize}MB");
|
||
}
|
||
|
||
// 根据文件类型确定COS路径
|
||
string cosPath = GetCosPath(request.Type, isImage, extension);
|
||
|
||
// 获取存储桶名称
|
||
string bucket = $"{_config.BucketName}-{_config.AppId}";
|
||
|
||
// 如果是图片,先读取图片尺寸(在流被消耗之前)
|
||
int imageWidth = 0;
|
||
int imageHeight = 0;
|
||
if (isImage)
|
||
{
|
||
try
|
||
{
|
||
// 从流中读取图片尺寸(需要重置流位置)
|
||
using (var stream = request.File.OpenReadStream())
|
||
{
|
||
var (width, height) = GetImageDimensions(stream);
|
||
imageWidth = width;
|
||
imageHeight = height;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// 如果获取尺寸失败,不影响上传成功
|
||
}
|
||
}
|
||
|
||
|
||
var cosRequest = new PutObjectRequest(bucket, cosPath, request.File.OpenReadStream());
|
||
cosRequest.APPID = _config.AppId;
|
||
var result = _cosXml.PutObject(cosRequest);
|
||
|
||
//// 上传文件到COS
|
||
//PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, "",cosPath);
|
||
|
||
//putObjectRequest.SetRequestBody(request.File.OpenReadStream());
|
||
//putObjectRequest.SetContentLength(request.File.Length);
|
||
|
||
//// 设置ContentType
|
||
//string contentType = GetContentType(extension);
|
||
//if (!string.IsNullOrEmpty(contentType))
|
||
//{
|
||
// putObjectRequest.SetRequestHeader("Content-Type", contentType);
|
||
//}
|
||
|
||
// 执行上传
|
||
//PutObjectResult result = _cosXml.PutObject(putObjectRequest);
|
||
|
||
// 构建文件访问URL
|
||
string fileUrl = $"{_config.DomainUrl.TrimEnd('/')}/{cosPath}";
|
||
|
||
var response = new UploadFileRespDto
|
||
{
|
||
FileUrl = fileUrl,
|
||
FileSize = request.File.Length,
|
||
FileType = isImage ? "image" : "video"
|
||
};
|
||
|
||
// 如果是图片,设置图片尺寸
|
||
if (isImage)
|
||
{
|
||
response.FileWidth = imageWidth;
|
||
response.FileHeight = imageHeight;
|
||
response.ThumbnailUrl = fileUrl; // 暂时使用原图作为缩略图
|
||
}
|
||
else if (isVideo)
|
||
{
|
||
// 对于视频,需要额外处理
|
||
response.Duration = null; // 暂时不处理视频时长
|
||
}
|
||
|
||
return new BaseResponse<UploadFileRespDto>(response);
|
||
}
|
||
catch (COSXML.CosException.CosClientException clientEx)
|
||
{
|
||
return new BaseResponse<UploadFileRespDto>(
|
||
ResponseCode.Error,
|
||
$"文件上传失败(客户端错误):{clientEx.Message}");
|
||
}
|
||
catch (COSXML.CosException.CosServerException serverEx)
|
||
{
|
||
return new BaseResponse<UploadFileRespDto>(
|
||
ResponseCode.Error,
|
||
$"文件上传失败(服务器错误):{serverEx.GetInfo()}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new BaseResponse<UploadFileRespDto>(ResponseCode.Error, $"文件上传失败:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 批量上传文件
|
||
/// </summary>
|
||
/// <param name="request">请求参数</param>
|
||
/// <returns></returns>
|
||
public async Task<BaseResponse<UploadFilesRespDto>> UploadFiles(UploadFilesReq request)
|
||
{
|
||
try
|
||
{
|
||
// 验证文件列表
|
||
if (request.Files == null || request.Files.Count == 0)
|
||
{
|
||
return new BaseResponse<UploadFilesRespDto>(ResponseCode.Error, "文件列表不能为空");
|
||
}
|
||
|
||
// 限制最多上传9个文件
|
||
if (request.Files.Count > 9)
|
||
{
|
||
return new BaseResponse<UploadFilesRespDto>(ResponseCode.Error, "最多只能上传9个文件");
|
||
}
|
||
|
||
var uploadedFiles = new List<UploadFileRespDto>();
|
||
|
||
foreach (var file in request.Files)
|
||
{
|
||
// 为每个文件创建请求
|
||
var singleFileRequest = new UploadFileReq
|
||
{
|
||
File = file,
|
||
Type = request.Type
|
||
};
|
||
|
||
// 上传单个文件
|
||
var result = await UploadFile(singleFileRequest);
|
||
|
||
if (result.Code == ResponseCode.Success && result.Data != null)
|
||
{
|
||
uploadedFiles.Add(result.Data);
|
||
}
|
||
else
|
||
{
|
||
// 如果某个文件上传失败,返回错误信息
|
||
return new BaseResponse<UploadFilesRespDto>(ResponseCode.Error, $"文件上传失败:{file.FileName}");
|
||
}
|
||
}
|
||
|
||
var response = new UploadFilesRespDto
|
||
{
|
||
Files = uploadedFiles
|
||
};
|
||
|
||
return new BaseResponse<UploadFilesRespDto>(response);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new BaseResponse<UploadFilesRespDto>(ResponseCode.Error, $"批量上传失败:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据类型获取COS路径
|
||
/// </summary>
|
||
/// <param name="type">文件类型:1-帖子图片,2-头像,3-认证视频,4-其他</param>
|
||
/// <param name="isImage">是否为图片</param>
|
||
/// <param name="extension">文件扩展名</param>
|
||
/// <returns></returns>
|
||
private string GetCosPath(int type, bool isImage, string extension)
|
||
{
|
||
string folder = isImage ? "images" : "videos";
|
||
string subFolder = type switch
|
||
{
|
||
1 => "posts", // 帖子图片
|
||
2 => "avatars", // 头像
|
||
3 => "certification", // 认证视频
|
||
4 => "other", // 其他
|
||
_ => "other"
|
||
};
|
||
|
||
// 生成唯一文件名
|
||
string fileName = $"{Guid.NewGuid()}{extension}";
|
||
|
||
// 返回完整路径:prefixes/folder/subFolder/yyyyMMdd/filename
|
||
return $"{_config.Prefixes}/{folder}/{subFolder}/{DateTime.Now:yyyyMMdd}/{fileName}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取ContentType
|
||
/// </summary>
|
||
/// <param name="extension">文件扩展名</param>
|
||
/// <returns></returns>
|
||
private string GetContentType(string extension)
|
||
{
|
||
return extension.ToLower() switch
|
||
{
|
||
".jpg" => "image/jpeg",
|
||
".jpeg" => "image/jpeg",
|
||
".png" => "image/png",
|
||
".gif" => "image/gif",
|
||
".bmp" => "image/bmp",
|
||
".webp" => "image/webp",
|
||
".mp4" => "video/mp4",
|
||
".avi" => "video/x-msvideo",
|
||
".mov" => "video/quicktime",
|
||
".wmv" => "video/x-ms-wmv",
|
||
".flv" => "video/x-flv",
|
||
".mkv" => "video/x-matroska",
|
||
_ => "application/octet-stream"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取图片尺寸
|
||
/// </summary>
|
||
/// <param name="stream">文件流</param>
|
||
/// <returns>宽度和高度</returns>
|
||
private (int width, int height) GetImageDimensions(Stream stream)
|
||
{
|
||
using var image = System.Drawing.Image.FromStream(stream);
|
||
return (image.Width, image.Height);
|
||
}
|
||
}
|
||
}
|
||
|