live-forum/server/webapi/LiveForum/LiveForum.Service/Others/CosFileUploadService.cs
2026-03-24 11:27:37 +08:00

308 lines
11 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 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);
}
}
}