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