diff --git a/YD_Event.Application/Certificate/CertificateGenerateService.cs b/YD_Event.Application/Certificate/CertificateGenerateService.cs index d6ba757..e2ccfc7 100644 --- a/YD_Event.Application/Certificate/CertificateGenerateService.cs +++ b/YD_Event.Application/Certificate/CertificateGenerateService.cs @@ -3,7 +3,6 @@ using YD_Event.Core.Entities; using YD_Event.Core.Domain.Certificate; using SqlSugar; using Mapster; -using OnceMi.AspNetCore.OSS; using Microsoft.Extensions.Options; using YD_Event.Core.Options; using Microsoft.AspNetCore.Hosting; @@ -18,6 +17,7 @@ using System.Linq; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; +using YD_Event.Application.File; namespace YD_Event.Application.Certificate; @@ -29,7 +29,7 @@ public class CertificateGenerateService : IDynamicApiController, ITransient private readonly ISqlSugarRepository _certificateRepository; private readonly ISqlSugarRepository _templateRepository; private readonly ISqlSugarRepository _placeholderRepository; - private readonly IOSSService _ossService; + private readonly FileService _fileService; private readonly IOptionsMonitor _ossOptionsMonitor; private readonly IIdGenerator _idGenerator; private readonly IWebHostEnvironment _webHostEnvironment; @@ -42,7 +42,7 @@ public class CertificateGenerateService : IDynamicApiController, ITransient ISqlSugarRepository certificateRepository, ISqlSugarRepository templateRepository, ISqlSugarRepository placeholderRepository, - IOSSService ossService, + FileService fileService, IOptionsMonitor ossOptionsMonitor, IIdGenerator idGenerator, IWebHostEnvironment webHostEnvironment, @@ -54,7 +54,7 @@ public class CertificateGenerateService : IDynamicApiController, ITransient _certificateRepository = certificateRepository; _templateRepository = templateRepository; _placeholderRepository = placeholderRepository; - _ossService = ossService; + _fileService = fileService; _ossOptionsMonitor = ossOptionsMonitor; _idGenerator = idGenerator; _webHostEnvironment = webHostEnvironment; @@ -790,8 +790,8 @@ public class CertificateGenerateService : IDynamicApiController, ITransient if (!options.Enable) { // 本地存储 - string localDir = string.Concat(options.Bucket.TrimEnd('/'), filePath); - string absoluteDir = Path.Combine(_webHostEnvironment.WebRootPath, localDir); + string localDir = string.Concat(options.Bucket?.TrimEnd('/') ?? "", filePath); + string absoluteDir = Path.Combine(_webHostEnvironment.WebRootPath, localDir.TrimStart('/')); if (!Directory.Exists(absoluteDir)) { @@ -804,7 +804,7 @@ public class CertificateGenerateService : IDynamicApiController, ITransient var request = _httpContextAccessor.HttpContext?.Request; if (request != null) { - finalUrl = $"{request.Scheme}://{request.Host.Value}/{localDir}{name}"; + finalUrl = $"{request.Scheme}://{request.Host.Value}/{localDir.TrimStart('/')}{name}"; } using var fileStream = new FileStream(fullPath, FileMode.Create, FileAccess.Write); @@ -815,12 +815,8 @@ public class CertificateGenerateService : IDynamicApiController, ITransient else { // OSS 上传 - string fileName = $"{filePath}{name}"; - await _ossService.PutObjectAsync(options.Bucket, fileName, pdfStream); - - // 预测最终 URL - string bucketPrefix = (options.Provider == OSSProvider.Minio) ? $"/{options.Bucket}" : string.Empty; - finalUrl = $"{options.Domain.TrimEnd('/')}{bucketPrefix}{fileName}"; + string fileName = $"{filePath.TrimStart('/')}{name}"; + finalUrl = await _fileService.UploadFileStreamAsync(fileName, pdfStream); _logger.LogInformation("PDF文件上传到OSS: {FileName}", fileName); } @@ -858,30 +854,12 @@ public class CertificateGenerateService : IDynamicApiController, ITransient string extension = ".png"; string finalUrl = string.Empty; - // 预测最终 URL - if (!options.Enable) - { - string localDir = string.Concat(options.Bucket.TrimEnd('/'), filePath); - var request = _httpContextAccessor.HttpContext?.Request; - if (request != null) - { - finalUrl = $"{request.Scheme}://{request.Host.Value}/{localDir}{name}{extension}"; - } - } - else - { - string bucketPrefix = (options.Provider == OSSProvider.Minio) ? $"/{options.Bucket}" : string.Empty; - string fileName = $"{filePath}{name}{extension}"; - finalUrl = $"{options.Domain.TrimEnd('/')}{bucketPrefix}{fileName}"; - } - qrStream.Position = 0; if (!options.Enable) { - // 本地存储 - string localDir = string.Concat(options.Bucket.TrimEnd('/'), filePath); - string absoluteDir = Path.Combine(_webHostEnvironment.WebRootPath, localDir); + string localDir = string.Concat(options.Bucket?.TrimEnd('/') ?? "", filePath); + string absoluteDir = Path.Combine(_webHostEnvironment.WebRootPath, localDir.TrimStart('/')); if (!Directory.Exists(absoluteDir)) { @@ -891,12 +869,18 @@ public class CertificateGenerateService : IDynamicApiController, ITransient string fullPath = Path.Combine(absoluteDir, $"{name}{extension}"); using var fileStream2 = new FileStream(fullPath, FileMode.Create, FileAccess.Write); await qrStream.CopyToAsync(fileStream2); + + var request = _httpContextAccessor.HttpContext?.Request; + if (request != null) + { + finalUrl = $"{request.Scheme}://{request.Host.Value}/{localDir.TrimStart('/')}{name}{extension}"; + } } else { // OSS 上传 - string fileName = $"{filePath}{name}{extension}"; - await _ossService.PutObjectAsync(options.Bucket, fileName, qrStream); + string fileName = $"{filePath.TrimStart('/')}{name}{extension}"; + finalUrl = await _fileService.UploadFileStreamAsync(fileName, qrStream); } return finalUrl; @@ -928,23 +912,20 @@ public class CertificateGenerateService : IDynamicApiController, ITransient if (!options.Enable) { // 本地存储 - string localDir = string.Concat(options.Bucket.TrimEnd('/'), filePath); + string localDir = string.Concat(options.Bucket?.TrimEnd('/') ?? "", filePath); // 预测最终 URL var request = _httpContextAccessor.HttpContext?.Request; if (request != null) { - finalUrl = $"{request.Scheme}://{request.Host.Value}/{localDir}{name}"; + finalUrl = $"{request.Scheme}://{request.Host.Value}/{localDir.TrimStart('/')}{name}"; } } else { // OSS 上传 - string fileName = $"{filePath}{name}"; - - // 预测最终 URL - string bucketPrefix = (options.Provider == OSSProvider.Minio) ? $"/{options.Bucket}" : string.Empty; - finalUrl = $"{options.Domain.TrimEnd('/')}{bucketPrefix}{fileName}"; + string fileName = $"{filePath.TrimStart('/')}{name}"; + finalUrl = $"{options.Domain.TrimEnd('/')}/{fileName}"; } return finalUrl; @@ -992,7 +973,7 @@ public class CertificateGenerateService : IDynamicApiController, ITransient else { // OSS 上传 - await _ossService.PutObjectAsync(options.Bucket, relativePath, pdfStream); + await _fileService.UploadFileStreamAsync(relativePath, pdfStream); _logger.LogInformation("PDF文件上传到OSS: {FileName}", relativePath); } diff --git a/YD_Event.Application/Certificate/CertificateTemplateService.cs b/YD_Event.Application/Certificate/CertificateTemplateService.cs index 839a834..439e17d 100644 --- a/YD_Event.Application/Certificate/CertificateTemplateService.cs +++ b/YD_Event.Application/Certificate/CertificateTemplateService.cs @@ -3,13 +3,13 @@ using YD_Event.Core.Entities; using YD_Event.Core.Domain.Certificate; using SqlSugar; using Mapster; -using OnceMi.AspNetCore.OSS; using Microsoft.Extensions.Options; using YD_Event.Core.Options; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using System.ComponentModel.DataAnnotations; +using YD_Event.Application.File; namespace YD_Event.Application.Certificate; @@ -20,7 +20,7 @@ public class CertificateTemplateService : BaseService, ITra { private readonly ISqlSugarRepository _templateRepository; private readonly ISqlSugarRepository _placeholderRepository; - private readonly IOSSService _ossService; + private readonly FileService _fileService; private readonly IOptionsMonitor _ossOptionsMonitor; private readonly IIdGenerator _idGenerator; private readonly IWebHostEnvironment _webHostEnvironment; @@ -31,7 +31,7 @@ public class CertificateTemplateService : BaseService, ITra public CertificateTemplateService( ISqlSugarRepository templateRepository, ISqlSugarRepository placeholderRepository, - IOSSService ossService, + FileService fileService, IOptionsMonitor ossOptionsMonitor, IIdGenerator idGenerator, IWebHostEnvironment webHostEnvironment, @@ -41,7 +41,7 @@ public class CertificateTemplateService : BaseService, ITra { _templateRepository = templateRepository; _placeholderRepository = placeholderRepository; - _ossService = ossService; + _fileService = fileService; _ossOptionsMonitor = ossOptionsMonitor; _idGenerator = idGenerator; _webHostEnvironment = webHostEnvironment; @@ -345,30 +345,20 @@ public class CertificateTemplateService : BaseService, ITra string filePath = $"/templates/{now.Year}/{now.Month:D2}/{now.Day:D2}/"; string finalUrl = string.Empty; - // 预测最终 URL - if (!options.Enable) - { - string localDir = string.Concat(options.Bucket.TrimEnd('/'), filePath); - var request = _httpContextAccessor.HttpContext?.Request; - if (request != null) - { - finalUrl = $"{request.Scheme}://{request.Host.Value}/{localDir}{name}"; - } - } - else - { - string bucketPrefix = (options.Provider == OSSProvider.Minio) ? $"/{options.Bucket}" : string.Empty; - string fileName = $"{filePath}{name}"; - finalUrl = $"{options.Domain.TrimEnd('/')}{bucketPrefix}{fileName}"; - } - using var stream = file.OpenReadStream(); if (!options.Enable) { + // 预测最终 URL + string localDir = string.Concat(options.Bucket?.TrimEnd('/') ?? "", filePath); + var request = _httpContextAccessor.HttpContext?.Request; + if (request != null) + { + finalUrl = $"{request.Scheme}://{request.Host.Value}/{localDir.TrimStart('/')}{name}"; + } + // 本地存储 - string localDir = string.Concat(options.Bucket.TrimEnd('/'), filePath); - string absoluteDir = Path.Combine(_webHostEnvironment.WebRootPath, localDir); + string absoluteDir = Path.Combine(_webHostEnvironment.WebRootPath, localDir.TrimStart('/')); if (!Directory.Exists(absoluteDir)) { @@ -382,8 +372,8 @@ public class CertificateTemplateService : BaseService, ITra else { // OSS 上传 - string fileName = $"{filePath}{name}"; - await _ossService.PutObjectAsync(options.Bucket, fileName, stream); + string fileName = $"{filePath.TrimStart('/')}{name}"; + finalUrl = await _fileService.UploadFileStreamAsync(fileName, stream); } _logger.LogInformation("模板文件上传成功. URL: {Url}", finalUrl); diff --git a/YD_Event.Application/File/FileService.cs b/YD_Event.Application/File/FileService.cs index 686c926..8cd4b50 100644 --- a/YD_Event.Application/File/FileService.cs +++ b/YD_Event.Application/File/FileService.cs @@ -2,36 +2,38 @@ using YD_Event.Core.Options; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Options; -using OnceMi.AspNetCore.OSS; +using Microsoft.AspNetCore.Mvc; +using YD_Event.Core.Infrastructure.Aliyun.Oss; namespace YD_Event.Application.File; public class FileService : IDynamicApiController, ITransient { - private readonly IOSSService _ossService; private readonly IOptionsMonitor _ossOptionsMonitor; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IWebHostEnvironment _webHostEnvironment; private readonly IIdGenerator _idGenerator; + private readonly OssService _ossService; - public FileService(IOSSService ossService, + public FileService( IOptionsMonitor ossOptionsMonitor, IHttpContextAccessor httpContextAccessor, IWebHostEnvironment webHostEnvironment, - IIdGenerator idGenerator) + IIdGenerator idGenerator, + OssService ossService) { - _ossService = ossService; _ossOptionsMonitor = ossOptionsMonitor; _httpContextAccessor = httpContextAccessor; _webHostEnvironment = webHostEnvironment; _idGenerator = idGenerator; + _ossService = ossService; } /// /// 上传附件 /// - /// - /// + /// 表单文件 + /// 返回上传文件信息 [NonUnify] public async Task> Upload([Required] IFormFile file) { @@ -47,52 +49,171 @@ public class FileService : IDynamicApiController, ITransient { throw Oops.Bah("无效文件"); } - //文件路径 - string filePath = $"/{now.Year}/{now.Month:D2}/{now.Day:D2}/"; - // 文件完整名称 - if (!options.Enable) - { - filePath = string.Concat(options.Bucket.TrimEnd('/'), filePath);//ptions.Bucket.TrimEnd('/') + filePath; - string s = Path.Combine(_webHostEnvironment.WebRootPath, filePath); - if (!Directory.Exists(s)) - { - Directory.CreateDirectory(s); - } - - var stream = System.IO.File.Create($"{s}{name}{extension}"); - await file.CopyToAsync(stream); - await stream.DisposeAsync(); - var request = _httpContextAccessor.HttpContext!.Request; - string url = $"{request.Scheme}://{request.Host.Value}/{filePath}{name}{extension}"; - return new List() - { - new() - { - Name = $"{name}{extension}", - Url = url - } - }; - } + + string filePath = $"{now.Year}/{now.Month:D2}/{now.Day:D2}/"; string fileName = $"{filePath}{name}{extension}"; - await _ossService.PutObjectAsync(options.Bucket, fileName, file.OpenReadStream()); - // 解决部分对象云存储不需要拼接bucket - string bucket; - switch (options.Provider) - { - case OSSProvider.Minio: - bucket = $"/{options.Bucket}"; - break; - default: - bucket = string.Empty; - break; - } + + using var stream = file.OpenReadStream(); + string url = await UploadStreamInternal(fileName, stream, options); + return new List() { new() { Name = $"{name}{extension}", - Url = $"{options.Domain.TrimEnd('/')}{bucket}{fileName}" + Url = url } }; } + + /// + /// 上传文件流 (供其他服务调用) + /// + /// 文件名及路径 + /// 文件流 + /// 返回完整 URL + [NonAction] + public async Task UploadFileStreamAsync(string fileName, Stream stream) + { + var options = _ossOptionsMonitor.CurrentValue; + return await UploadStreamInternal(fileName, stream, options); + } + + /// + /// 内部上传文件流 + /// + private async Task UploadStreamInternal(string fileName, Stream stream, OssConnectionOptions options) + { + if (!options.Enable) + { + // 本地存储 + string basePath = Path.Combine(_webHostEnvironment.WebRootPath, options.Bucket ?? "uploads"); + string fullPath = Path.Combine(basePath, fileName); + string dir = Path.GetDirectoryName(fullPath); + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + using var fileStream = System.IO.File.Create(fullPath); + await stream.CopyToAsync(fileStream); + + var request = _httpContextAccessor.HttpContext?.Request; + if (request != null) + { + return $"{request.Scheme}://{request.Host.Value}/{(options.Bucket ?? "uploads")}/{fileName}"; + } + return $"/{(options.Bucket ?? "uploads")}/{fileName}"; + } + else + { + // OSS 上传 + return await _ossService.PutObjectAsync(fileName, stream); + } + } + + /// + /// 下载文件 + /// + /// 文件 URL + /// 文件流结果 + [HttpGet("download")] + public IActionResult Download([FromQuery] string url) + { + if (string.IsNullOrWhiteSpace(url)) throw Oops.Bah("参数错误"); + + var options = _ossOptionsMonitor.CurrentValue; + + try + { + if (!options.Enable) + { + // 本地下载 + var uri = new Uri(url); + string localPath = Path.Combine(_webHostEnvironment.WebRootPath, uri.AbsolutePath.TrimStart('/')); + if (!System.IO.File.Exists(localPath)) throw Oops.Bah("文件不存在"); + + var stream = System.IO.File.OpenRead(localPath); + return new FileStreamResult(stream, "application/octet-stream") + { + FileDownloadName = Path.GetFileName(localPath) + }; + } + else + { + // OSS 下载 + // 解析 objectName (移除 Domain 部分) + string domainUrl = options.Domain.TrimEnd('/') + "/"; + string objectName = url; + if (url.StartsWith(domainUrl, StringComparison.OrdinalIgnoreCase)) + { + objectName = url.Substring(domainUrl.Length); + } + else + { + var uri = new Uri(url); + objectName = uri.AbsolutePath.TrimStart('/'); + } + + var contentStream = _ossService.GetObject(objectName); + return new FileStreamResult(contentStream, "application/octet-stream") + { + FileDownloadName = Path.GetFileName(objectName) + }; + } + } + catch (Exception ex) + { + throw Oops.Bah($"下载失败: {ex.Message}"); + } + } + + /// + /// 删除文件 + /// + /// 文件 URL + [HttpDelete("delete")] + public IActionResult Delete([FromQuery] string url) + { + if (string.IsNullOrWhiteSpace(url)) throw Oops.Bah("参数错误"); + + var options = _ossOptionsMonitor.CurrentValue; + + try + { + if (!options.Enable) + { + // 本地删除 + var uri = new Uri(url); + string localPath = Path.Combine(_webHostEnvironment.WebRootPath, uri.AbsolutePath.TrimStart('/')); + if (System.IO.File.Exists(localPath)) + { + System.IO.File.Delete(localPath); + } + } + else + { + // OSS 删除 + string domainUrl = options.Domain.TrimEnd('/') + "/"; + string objectName = url; + if (url.StartsWith(domainUrl, StringComparison.OrdinalIgnoreCase)) + { + objectName = url.Substring(domainUrl.Length); + } + else + { + var uri = new Uri(url); + objectName = uri.AbsolutePath.TrimStart('/'); + } + + _ossService.DeleteObject(objectName); + } + + return new JsonResult(new { success = true, msg = "删除成功" }); + } + catch (Exception ex) + { + throw Oops.Bah($"删除失败: {ex.Message}"); + } + } } \ No newline at end of file diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index 6532559..db84018 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -5343,8 +5343,34 @@ 上传附件 - - + 表单文件 + 返回上传文件信息 + + + + 上传文件流 (供其他服务调用) + + 文件名及路径 + 文件流 + 返回完整 URL + + + + 内部上传文件流 + + + + + 下载文件 + + 文件 URL + 文件流结果 + + + + 删除文件 + + 文件 URL diff --git a/YD_Event.Application/applicationsettings.json b/YD_Event.Application/applicationsettings.json index 4bee8ef..dc7a254 100644 --- a/YD_Event.Application/applicationsettings.json +++ b/YD_Event.Application/applicationsettings.json @@ -80,16 +80,12 @@ }, //对象存储配置 文档:https://github.com/oncemi/OnceMi.AspNetCore.OSS "OssConnection": { - "Enable": false, //是否启用对象存储(不启用将存储至站点目录) - "Provider": "Minio", //OSS提供者;允许值:Minio/Aliyun/QCloud/Qiniu/HuaweiCloud/(Invalid:代表附件存储到当前站点下) - "Endpoint": "skip81.oss-cn-shanghai.aliyuncs.com", //节点 - "AccessKey": "LTAI5tLfbdZwBoA9bCZVjZkF", //Key - "SecretKey": "4DmFIY77zwdp1gaRUTmBMf7WfdsNsC", //秘钥 - //"Region": "ap-chengdu",//地域 - "IsEnableHttps": false, //是否启用HTTPS - "IsEnableCache": true, //是否启用缓存(启用后将缓存签名URL,以减少请求次数) - "Bucket": "skip81", //默认存储根目录: null, - "Domain": "https://oss.okay123.top" //外网访问域名或IP + "Enable": true, //是否启用对象存储(不启用将存储至站点目录) + "Endpoint": "oss-cn-shanghai.aliyuncs.com", //阿里云OSS节点 + "AccessKeyId": "LTAI5tLfbdZwBoA9bCZVjZkF", //阿里云OSS AccessKeyId + "AccessKeySecret": "4DmFIY77zwdp1gaRUTmBMf7WfdsNsC", //阿里云OSS AccessKeySecret + "Bucket": "skip81", //存储桶名称 + "Domain": "https://skip81.oss-cn-shanghai.aliyuncs.com" //外网访问域名或IP }, // 接入第三方授权登录 文档:https://github.com/mrhuo/MrHuo.OAuth // QQ授权申请地址:https://connect.qq.com/ diff --git a/YD_Event.Core/Infrastructure/Aliyun/Oss/OssService.cs b/YD_Event.Core/Infrastructure/Aliyun/Oss/OssService.cs new file mode 100644 index 0000000..07bece9 --- /dev/null +++ b/YD_Event.Core/Infrastructure/Aliyun/Oss/OssService.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Aliyun.OSS; +using Furion.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Polly; +using YD_Event.Core.Options; + +namespace YD_Event.Core.Infrastructure.Aliyun.Oss; + +public class OssService : ISingleton +{ + private readonly OssConnectionOptions _options; + private readonly IOss _ossClient; + private readonly ILogger _logger; + private readonly AsyncPolicy _retryPolicy; + + public OssService(IOptionsMonitor optionsMonitor, ILogger logger) + { + _options = optionsMonitor.CurrentValue; + _logger = logger; + + if (_options.Enable) + { + _ossClient = new OssClient(_options.Endpoint, _options.AccessKeyId, _options.AccessKeySecret); + } + + // 定义重试策略:最多重试3次,每次间隔指数增长 + _retryPolicy = Policy + .Handle() + .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + (exception, timeSpan, retryCount, context) => + { + _logger.LogWarning($"OSS操作异常,正在进行第 {retryCount} 次重试. 异常: {exception.Message}"); + }); + } + + /// + /// 获取完整的URL + /// + private string GetFullUrl(string objectName) + { + return $"{_options.Domain.TrimEnd('/')}/{objectName}"; + } + + /// + /// 同步上传文件流 + /// + /// OSS 中的目标文件路径 + /// 文件数据流 + /// 文件的访问 URL + public string PutObject(string objectName, Stream stream) + { + if (!_options.Enable) + throw new InvalidOperationException("OSS未启用,无法执行上传操作。"); + + // Validate File Size (example limit: 500MB) + if (stream.Length > 500 * 1024 * 1024) + { + throw new ArgumentException("文件大小超过限制(500MB)。"); + } + + _ossClient.PutObject(_options.Bucket, objectName, stream); + return GetFullUrl(objectName); + } + + /// + /// 异步上传文件流 + /// + /// OSS 中的目标文件路径 + /// 文件数据流 + /// 文件的访问 URL + public async Task PutObjectAsync(string objectName, Stream stream) + { + if (!_options.Enable) + throw new InvalidOperationException("OSS未启用,无法执行上传操作。"); + + if (stream.Length > 500 * 1024 * 1024) + { + throw new ArgumentException("文件大小超过限制(500MB)。"); + } + + return await _retryPolicy.ExecuteAsync(() => + { + return Task.Run(() => + { + // OssClient 官方SDK大部分是同步方法,我们用Task包装 + _ossClient.PutObject(_options.Bucket, objectName, stream); + return GetFullUrl(objectName); + }); + }); + } + + /// + /// 异步分片上传大文件 + /// + /// OSS 中的目标文件路径 + /// 文件数据流 + /// 进度回调 + /// 文件的访问 URL + public async Task MultipartUploadAsync(string objectName, Stream stream, EventHandler progressCallback = null) + { + if (!_options.Enable) + throw new InvalidOperationException("OSS未启用,无法执行分片上传操作。"); + + return await _retryPolicy.ExecuteAsync(async () => + { + return await Task.Run(() => + { + var request = new InitiateMultipartUploadRequest(_options.Bucket, objectName); + var result = _ossClient.InitiateMultipartUpload(request); + var uploadId = result.UploadId; + var partETags = new List(); + + try + { + long partSize = 5 * 1024 * 1024; // 5MB 分片 + long fileSize = stream.Length; + int partCount = (int)(fileSize / partSize); + if (fileSize % partSize != 0) + { + partCount++; + } + + long transferredBytes = 0; + + for (int i = 0; i < partCount; i++) + { + long skipBytes = partSize * i; + stream.Position = skipBytes; + long size = (partSize < fileSize - skipBytes) ? partSize : (fileSize - skipBytes); + + var uploadPartRequest = new UploadPartRequest(_options.Bucket, objectName, uploadId) + { + InputStream = stream, + PartSize = size, + PartNumber = i + 1 + }; + + // 每次上传进度回调(简化版:完成一个分片后触发) + uploadPartRequest.StreamTransferProgress += (sender, args) => + { + transferredBytes += args.TransferredBytes; + progressCallback?.Invoke(this, new StreamTransferProgressArgs(transferredBytes, fileSize)); + }; + + var uploadPartResult = _ossClient.UploadPart(uploadPartRequest); + partETags.Add(uploadPartResult.PartETag); + } + + var completeRequest = new CompleteMultipartUploadRequest(_options.Bucket, objectName, uploadId); + foreach (var partETag in partETags) + { + completeRequest.PartETags.Add(partETag); + } + _ossClient.CompleteMultipartUpload(completeRequest); + + return GetFullUrl(objectName); + } + catch (Exception ex) + { + var abortRequest = new AbortMultipartUploadRequest(_options.Bucket, objectName, uploadId); + _ossClient.AbortMultipartUpload(abortRequest); + _logger.LogError(ex, "分片上传失败,已取消上传。"); + throw; + } + }); + }); + } + + /// + /// 获取文件流 + /// + /// OSS 中的目标文件路径 + /// 下载文件的内容数据流 + public Stream GetObject(string objectName) + { + if (!_options.Enable) + throw new InvalidOperationException("OSS未启用,无法执行下载操作。"); + + var ossObject = _ossClient.GetObject(_options.Bucket, objectName); + return ossObject.Content; + } + + /// + /// 删除文件 + /// + /// OSS 中的目标文件路径 + public void DeleteObject(string objectName) + { + if (!_options.Enable) + throw new InvalidOperationException("OSS未启用,无法执行删除操作。"); + + _ossClient.DeleteObject(_options.Bucket, objectName); + } + + /// + /// 检查文件是否存在 + /// + /// OSS 中的目标文件路径 + /// 如果文件存在返回 true,否则返回 false + public bool DoesObjectExist(string objectName) + { + if (!_options.Enable) + throw new InvalidOperationException("OSS未启用,无法执行查询操作。"); + + return _ossClient.DoesObjectExist(_options.Bucket, objectName); + } +} \ No newline at end of file diff --git a/YD_Event.Core/Infrastructure/Aliyun/Oss/StreamTransferProgressArgs.cs b/YD_Event.Core/Infrastructure/Aliyun/Oss/StreamTransferProgressArgs.cs new file mode 100644 index 0000000..1894335 --- /dev/null +++ b/YD_Event.Core/Infrastructure/Aliyun/Oss/StreamTransferProgressArgs.cs @@ -0,0 +1,15 @@ +using System; + +namespace YD_Event.Core.Infrastructure.Aliyun.Oss; + +public class StreamTransferProgressArgs : EventArgs +{ + public long TransferredBytes { get; } + public long TotalBytes { get; } + + public StreamTransferProgressArgs(long transferredBytes, long totalBytes) + { + TransferredBytes = transferredBytes; + TotalBytes = totalBytes; + } +} \ No newline at end of file diff --git a/YD_Event.Core/Options/OssConnectionOptions.cs b/YD_Event.Core/Options/OssConnectionOptions.cs index 6f13779..aabaa50 100644 --- a/YD_Event.Core/Options/OssConnectionOptions.cs +++ b/YD_Event.Core/Options/OssConnectionOptions.cs @@ -1,14 +1,33 @@ using Furion.ConfigurableOptions; -using OnceMi.AspNetCore.OSS; namespace YD_Event.Core.Options; /// /// 对象存储连接配置 /// -public sealed class OssConnectionOptions : OSSOptions, IConfigurableOptions +public sealed class OssConnectionOptions : IConfigurableOptions { /// - /// 默认存目录 + /// 是否启用对象存储(不启用将存储至站点目录) + /// + public bool Enable { get; set; } + + /// + /// OSS节点 + /// + public string Endpoint { get; set; } + + /// + /// AccessKeyId + /// + public string AccessKeyId { get; set; } + + /// + /// AccessKeySecret + /// + public string AccessKeySecret { get; set; } + + /// + /// 默认存目录 (Bucket) /// public string Bucket { get; set; } @@ -16,9 +35,4 @@ public sealed class OssConnectionOptions : OSSOptions, IConfigurableOptions /// 外网访问域名或IP /// public string Domain { get; set; } - - /// - /// 是否启用对象存储(不启用将存储至站点目录) - /// - public bool Enable { get; set; } } \ No newline at end of file diff --git a/YD_Event.Core/YD_Event.Core.csproj b/YD_Event.Core/YD_Event.Core.csproj index d7103d4..66f237d 100644 --- a/YD_Event.Core/YD_Event.Core.csproj +++ b/YD_Event.Core/YD_Event.Core.csproj @@ -10,6 +10,7 @@ + @@ -21,7 +22,6 @@ - diff --git a/YD_Event.Core/YD_Event.Core.xml b/YD_Event.Core/YD_Event.Core.xml index 8567599..c013131 100644 --- a/YD_Event.Core/YD_Event.Core.xml +++ b/YD_Event.Core/YD_Event.Core.xml @@ -4377,6 +4377,56 @@ 图片URL + + + 获取完整的URL + + + + + 同步上传文件流 + + OSS 中的目标文件路径 + 文件数据流 + 文件的访问 URL + + + + 异步上传文件流 + + OSS 中的目标文件路径 + 文件数据流 + 文件的访问 URL + + + + 异步分片上传大文件 + + OSS 中的目标文件路径 + 文件数据流 + 进度回调 + 文件的访问 URL + + + + 获取文件流 + + OSS 中的目标文件路径 + 下载文件的内容数据流 + + + + 删除文件 + + OSS 中的目标文件路径 + + + + 检查文件是否存在 + + OSS 中的目标文件路径 + 如果文件存在返回 true,否则返回 false + 阿里云短信服务实现 @@ -4471,9 +4521,29 @@ 对象存储连接配置 + + + 是否启用对象存储(不启用将存储至站点目录) + + + + + OSS节点 + + + + + AccessKeyId + + + + + AccessKeySecret + + - 默认存目录 + 默认存目录 (Bucket) @@ -4481,11 +4551,6 @@ 外网访问域名或IP - - - 是否启用对象存储(不启用将存储至站点目录) - - 短信配置选项 diff --git a/YD_Event.Web.Core/Startup.cs b/YD_Event.Web.Core/Startup.cs index 410be9d..4fd1ad4 100644 --- a/YD_Event.Web.Core/Startup.cs +++ b/YD_Event.Web.Core/Startup.cs @@ -20,7 +20,6 @@ using Mapster; using Microsoft.AspNetCore.Authentication.JwtBearer; using MrHuo.OAuth; using MrHuo.OAuth.QQ; -using OnceMi.AspNetCore.OSS; using QuestPDF; using QuestPDF.Infrastructure; @@ -138,12 +137,6 @@ public class Startup : AppStartup //模板引擎 文档:https://furion.baiqian.ltd/docs/view-engine services.AddViewEngine(); - // OSS文档:https://github.com/oncemi/OnceMi.AspNetCore.OSS - var ossOptions = App.GetConfig("OssConnection"); - services.AddOSSService(options => - { - ossOptions.Adapt(options); - }); var auth = new QQOAuth(OAuthConfig.LoadFrom(App.Configuration, "oauth:qq")); services.AddSingleton(auth);