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
{
///
/// 腾讯云COS文件上传服务
///
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" };
///
/// 构造函数
///
/// COS配置
public CosFileUploadService(IOptions 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);
}
///
/// 上传文件
///
/// 请求参数
///
public async Task> UploadFile(UploadFileReq request)
{
try
{
// 验证文件
if (request.File == null || request.File.Length == 0)
{
return new BaseResponse(ResponseCode.Error, "文件不能为空");
}
// 验证文件类型
var extension = Path.GetExtension(request.File.FileName)?.ToLower();
if (string.IsNullOrEmpty(extension))
{
return new BaseResponse(ResponseCode.Error, "文件格式无效");
}
// 判断是图片还是视频
bool isImage = _allowedImageExtensions.Contains(extension);
bool isVideo = _allowedVideoExtensions.Contains(extension);
if (!isImage && !isVideo)
{
return new BaseResponse(ResponseCode.Error, "不支持的文件格式");
}
// 验证文件大小(使用配置中的最大大小限制)
long maxSizeBytes = _config.MaxSize * 1024 * 1024; // 转换为字节
if (request.File.Length > maxSizeBytes)
{
return new BaseResponse(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(response);
}
catch (COSXML.CosException.CosClientException clientEx)
{
return new BaseResponse(
ResponseCode.Error,
$"文件上传失败(客户端错误):{clientEx.Message}");
}
catch (COSXML.CosException.CosServerException serverEx)
{
return new BaseResponse(
ResponseCode.Error,
$"文件上传失败(服务器错误):{serverEx.GetInfo()}");
}
catch (Exception ex)
{
return new BaseResponse(ResponseCode.Error, $"文件上传失败:{ex.Message}");
}
}
///
/// 批量上传文件
///
/// 请求参数
///
public async Task> UploadFiles(UploadFilesReq request)
{
try
{
// 验证文件列表
if (request.Files == null || request.Files.Count == 0)
{
return new BaseResponse(ResponseCode.Error, "文件列表不能为空");
}
// 限制最多上传9个文件
if (request.Files.Count > 9)
{
return new BaseResponse(ResponseCode.Error, "最多只能上传9个文件");
}
var uploadedFiles = new List();
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(ResponseCode.Error, $"文件上传失败:{file.FileName}");
}
}
var response = new UploadFilesRespDto
{
Files = uploadedFiles
};
return new BaseResponse(response);
}
catch (Exception ex)
{
return new BaseResponse(ResponseCode.Error, $"批量上传失败:{ex.Message}");
}
}
///
/// 根据类型获取COS路径
///
/// 文件类型:1-帖子图片,2-头像,3-认证视频,4-其他
/// 是否为图片
/// 文件扩展名
///
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}";
}
///
/// 获取ContentType
///
/// 文件扩展名
///
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"
};
}
///
/// 获取图片尺寸
///
/// 文件流
/// 宽度和高度
private (int width, int height) GetImageDimensions(Stream stream)
{
using var image = System.Drawing.Image.FromStream(stream);
return (image.Width, image.Height);
}
}
}