OSS配置
This commit is contained in:
parent
2d4e2c3c60
commit
e3261fe596
@ -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<YD_Event.Core.Entities.Certificate> _certificateRepository;
|
||||
private readonly ISqlSugarRepository<CertificateTemplate> _templateRepository;
|
||||
private readonly ISqlSugarRepository<CertificatePlaceholder> _placeholderRepository;
|
||||
private readonly IOSSService _ossService;
|
||||
private readonly FileService _fileService;
|
||||
private readonly IOptionsMonitor<OssConnectionOptions> _ossOptionsMonitor;
|
||||
private readonly IIdGenerator _idGenerator;
|
||||
private readonly IWebHostEnvironment _webHostEnvironment;
|
||||
@ -42,7 +42,7 @@ public class CertificateGenerateService : IDynamicApiController, ITransient
|
||||
ISqlSugarRepository<YD_Event.Core.Entities.Certificate> certificateRepository,
|
||||
ISqlSugarRepository<CertificateTemplate> templateRepository,
|
||||
ISqlSugarRepository<CertificatePlaceholder> placeholderRepository,
|
||||
IOSSService ossService,
|
||||
FileService fileService,
|
||||
IOptionsMonitor<OssConnectionOptions> 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);
|
||||
}
|
||||
|
||||
@ -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<CertificateTemplate>, ITra
|
||||
{
|
||||
private readonly ISqlSugarRepository<CertificateTemplate> _templateRepository;
|
||||
private readonly ISqlSugarRepository<CertificatePlaceholder> _placeholderRepository;
|
||||
private readonly IOSSService _ossService;
|
||||
private readonly FileService _fileService;
|
||||
private readonly IOptionsMonitor<OssConnectionOptions> _ossOptionsMonitor;
|
||||
private readonly IIdGenerator _idGenerator;
|
||||
private readonly IWebHostEnvironment _webHostEnvironment;
|
||||
@ -31,7 +31,7 @@ public class CertificateTemplateService : BaseService<CertificateTemplate>, ITra
|
||||
public CertificateTemplateService(
|
||||
ISqlSugarRepository<CertificateTemplate> templateRepository,
|
||||
ISqlSugarRepository<CertificatePlaceholder> placeholderRepository,
|
||||
IOSSService ossService,
|
||||
FileService fileService,
|
||||
IOptionsMonitor<OssConnectionOptions> ossOptionsMonitor,
|
||||
IIdGenerator idGenerator,
|
||||
IWebHostEnvironment webHostEnvironment,
|
||||
@ -41,7 +41,7 @@ public class CertificateTemplateService : BaseService<CertificateTemplate>, ITra
|
||||
{
|
||||
_templateRepository = templateRepository;
|
||||
_placeholderRepository = placeholderRepository;
|
||||
_ossService = ossService;
|
||||
_fileService = fileService;
|
||||
_ossOptionsMonitor = ossOptionsMonitor;
|
||||
_idGenerator = idGenerator;
|
||||
_webHostEnvironment = webHostEnvironment;
|
||||
@ -345,30 +345,20 @@ public class CertificateTemplateService : BaseService<CertificateTemplate>, 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<CertificateTemplate>, 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);
|
||||
|
||||
@ -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<OssConnectionOptions> _ossOptionsMonitor;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IWebHostEnvironment _webHostEnvironment;
|
||||
private readonly IIdGenerator _idGenerator;
|
||||
private readonly OssService _ossService;
|
||||
|
||||
public FileService(IOSSService ossService,
|
||||
public FileService(
|
||||
IOptionsMonitor<OssConnectionOptions> ossOptionsMonitor,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IWebHostEnvironment webHostEnvironment,
|
||||
IIdGenerator idGenerator)
|
||||
IIdGenerator idGenerator,
|
||||
OssService ossService)
|
||||
{
|
||||
_ossService = ossService;
|
||||
_ossOptionsMonitor = ossOptionsMonitor;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_webHostEnvironment = webHostEnvironment;
|
||||
_idGenerator = idGenerator;
|
||||
_ossService = ossService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传附件
|
||||
/// </summary>
|
||||
/// <param name="file"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="file">表单文件</param>
|
||||
/// <returns>返回上传文件信息</returns>
|
||||
[NonUnify]
|
||||
public async Task<List<UploadFileOutput>> 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<UploadFileOutput>()
|
||||
{
|
||||
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<UploadFileOutput>()
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = $"{name}{extension}",
|
||||
Url = $"{options.Domain.TrimEnd('/')}{bucket}{fileName}"
|
||||
Url = url
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件流 (供其他服务调用)
|
||||
/// </summary>
|
||||
/// <param name="fileName">文件名及路径</param>
|
||||
/// <param name="stream">文件流</param>
|
||||
/// <returns>返回完整 URL</returns>
|
||||
[NonAction]
|
||||
public async Task<string> UploadFileStreamAsync(string fileName, Stream stream)
|
||||
{
|
||||
var options = _ossOptionsMonitor.CurrentValue;
|
||||
return await UploadStreamInternal(fileName, stream, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 内部上传文件流
|
||||
/// </summary>
|
||||
private async Task<string> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载文件
|
||||
/// </summary>
|
||||
/// <param name="url">文件 URL</param>
|
||||
/// <returns>文件流结果</returns>
|
||||
[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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除文件
|
||||
/// </summary>
|
||||
/// <param name="url">文件 URL</param>
|
||||
[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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5343,8 +5343,34 @@
|
||||
<summary>
|
||||
上传附件
|
||||
</summary>
|
||||
<param name="file"></param>
|
||||
<returns></returns>
|
||||
<param name="file">表单文件</param>
|
||||
<returns>返回上传文件信息</returns>
|
||||
</member>
|
||||
<member name="M:YD_Event.Application.File.FileService.UploadFileStreamAsync(System.String,System.IO.Stream)">
|
||||
<summary>
|
||||
上传文件流 (供其他服务调用)
|
||||
</summary>
|
||||
<param name="fileName">文件名及路径</param>
|
||||
<param name="stream">文件流</param>
|
||||
<returns>返回完整 URL</returns>
|
||||
</member>
|
||||
<member name="M:YD_Event.Application.File.FileService.UploadStreamInternal(System.String,System.IO.Stream,YD_Event.Core.Options.OssConnectionOptions)">
|
||||
<summary>
|
||||
内部上传文件流
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:YD_Event.Application.File.FileService.Download(System.String)">
|
||||
<summary>
|
||||
下载文件
|
||||
</summary>
|
||||
<param name="url">文件 URL</param>
|
||||
<returns>文件流结果</returns>
|
||||
</member>
|
||||
<member name="M:YD_Event.Application.File.FileService.Delete(System.String)">
|
||||
<summary>
|
||||
删除文件
|
||||
</summary>
|
||||
<param name="url">文件 URL</param>
|
||||
</member>
|
||||
<member name="T:YD_Event.Application.Logging.Dtos.LogPageQueryInput">
|
||||
<summary>
|
||||
|
||||
@ -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/
|
||||
|
||||
212
YD_Event.Core/Infrastructure/Aliyun/Oss/OssService.cs
Normal file
212
YD_Event.Core/Infrastructure/Aliyun/Oss/OssService.cs
Normal file
@ -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<OssService> _logger;
|
||||
private readonly AsyncPolicy _retryPolicy;
|
||||
|
||||
public OssService(IOptionsMonitor<OssConnectionOptions> optionsMonitor, ILogger<OssService> logger)
|
||||
{
|
||||
_options = optionsMonitor.CurrentValue;
|
||||
_logger = logger;
|
||||
|
||||
if (_options.Enable)
|
||||
{
|
||||
_ossClient = new OssClient(_options.Endpoint, _options.AccessKeyId, _options.AccessKeySecret);
|
||||
}
|
||||
|
||||
// 定义重试策略:最多重试3次,每次间隔指数增长
|
||||
_retryPolicy = Policy
|
||||
.Handle<Exception>()
|
||||
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
|
||||
(exception, timeSpan, retryCount, context) =>
|
||||
{
|
||||
_logger.LogWarning($"OSS操作异常,正在进行第 {retryCount} 次重试. 异常: {exception.Message}");
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取完整的URL
|
||||
/// </summary>
|
||||
private string GetFullUrl(string objectName)
|
||||
{
|
||||
return $"{_options.Domain.TrimEnd('/')}/{objectName}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同步上传文件流
|
||||
/// </summary>
|
||||
/// <param name="objectName">OSS 中的目标文件路径</param>
|
||||
/// <param name="stream">文件数据流</param>
|
||||
/// <returns>文件的访问 URL</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步上传文件流
|
||||
/// </summary>
|
||||
/// <param name="objectName">OSS 中的目标文件路径</param>
|
||||
/// <param name="stream">文件数据流</param>
|
||||
/// <returns>文件的访问 URL</returns>
|
||||
public async Task<string> 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步分片上传大文件
|
||||
/// </summary>
|
||||
/// <param name="objectName">OSS 中的目标文件路径</param>
|
||||
/// <param name="stream">文件数据流</param>
|
||||
/// <param name="progressCallback">进度回调</param>
|
||||
/// <returns>文件的访问 URL</returns>
|
||||
public async Task<string> MultipartUploadAsync(string objectName, Stream stream, EventHandler<StreamTransferProgressArgs> 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<PartETag>();
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件流
|
||||
/// </summary>
|
||||
/// <param name="objectName">OSS 中的目标文件路径</param>
|
||||
/// <returns>下载文件的内容数据流</returns>
|
||||
public Stream GetObject(string objectName)
|
||||
{
|
||||
if (!_options.Enable)
|
||||
throw new InvalidOperationException("OSS未启用,无法执行下载操作。");
|
||||
|
||||
var ossObject = _ossClient.GetObject(_options.Bucket, objectName);
|
||||
return ossObject.Content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除文件
|
||||
/// </summary>
|
||||
/// <param name="objectName">OSS 中的目标文件路径</param>
|
||||
public void DeleteObject(string objectName)
|
||||
{
|
||||
if (!_options.Enable)
|
||||
throw new InvalidOperationException("OSS未启用,无法执行删除操作。");
|
||||
|
||||
_ossClient.DeleteObject(_options.Bucket, objectName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查文件是否存在
|
||||
/// </summary>
|
||||
/// <param name="objectName">OSS 中的目标文件路径</param>
|
||||
/// <returns>如果文件存在返回 true,否则返回 false</returns>
|
||||
public bool DoesObjectExist(string objectName)
|
||||
{
|
||||
if (!_options.Enable)
|
||||
throw new InvalidOperationException("OSS未启用,无法执行查询操作。");
|
||||
|
||||
return _ossClient.DoesObjectExist(_options.Bucket, objectName);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,33 @@
|
||||
using Furion.ConfigurableOptions;
|
||||
using OnceMi.AspNetCore.OSS;
|
||||
|
||||
namespace YD_Event.Core.Options;
|
||||
/// <summary>
|
||||
/// 对象存储连接配置
|
||||
/// </summary>
|
||||
public sealed class OssConnectionOptions : OSSOptions, IConfigurableOptions
|
||||
public sealed class OssConnectionOptions : IConfigurableOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认存目录
|
||||
/// 是否启用对象存储(不启用将存储至站点目录)
|
||||
/// </summary>
|
||||
public bool Enable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// OSS节点
|
||||
/// </summary>
|
||||
public string Endpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AccessKeyId
|
||||
/// </summary>
|
||||
public string AccessKeyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AccessKeySecret
|
||||
/// </summary>
|
||||
public string AccessKeySecret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 默认存目录 (Bucket)
|
||||
/// </summary>
|
||||
public string Bucket { get; set; }
|
||||
|
||||
@ -16,9 +35,4 @@ public sealed class OssConnectionOptions : OSSOptions, IConfigurableOptions
|
||||
/// 外网访问域名或IP
|
||||
/// </summary>
|
||||
public string Domain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用对象存储(不启用将存储至站点目录)
|
||||
/// </summary>
|
||||
public bool Enable { get; set; }
|
||||
}
|
||||
@ -10,6 +10,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AlibabaCloud.SDK.Dysmsapi20170525" Version="4.3.0" />
|
||||
<PackageReference Include="AlibabaCloud.SDK.Ocr20191230" Version="4.0.2" />
|
||||
<PackageReference Include="Aliyun.OSS.SDK.Core" Version="2.5.3" />
|
||||
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
|
||||
<PackageReference Include="EasyCaching.CSRedis" Version="1.9.2" />
|
||||
<PackageReference Include="EasyCaching.InMemory" Version="1.9.2" />
|
||||
@ -21,7 +22,6 @@
|
||||
<PackageReference Include="log4net" Version="3.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" />
|
||||
<PackageReference Include="MrHuo.OAuth.QQ" Version="1.1.1" />
|
||||
<PackageReference Include="OnceMi.AspNetCore.OSS" Version="1.2.0" />
|
||||
<PackageReference Include="Polly" Version="8.6.6" />
|
||||
<PackageReference Include="QuestPDF" Version="2025.12.1" />
|
||||
<PackageReference Include="QRCoder" Version="1.4.3" />
|
||||
|
||||
@ -4377,6 +4377,56 @@
|
||||
<param name="imageUrl">图片URL</param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:YD_Event.Core.Infrastructure.Aliyun.Oss.OssService.GetFullUrl(System.String)">
|
||||
<summary>
|
||||
获取完整的URL
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:YD_Event.Core.Infrastructure.Aliyun.Oss.OssService.PutObject(System.String,System.IO.Stream)">
|
||||
<summary>
|
||||
同步上传文件流
|
||||
</summary>
|
||||
<param name="objectName">OSS 中的目标文件路径</param>
|
||||
<param name="stream">文件数据流</param>
|
||||
<returns>文件的访问 URL</returns>
|
||||
</member>
|
||||
<member name="M:YD_Event.Core.Infrastructure.Aliyun.Oss.OssService.PutObjectAsync(System.String,System.IO.Stream)">
|
||||
<summary>
|
||||
异步上传文件流
|
||||
</summary>
|
||||
<param name="objectName">OSS 中的目标文件路径</param>
|
||||
<param name="stream">文件数据流</param>
|
||||
<returns>文件的访问 URL</returns>
|
||||
</member>
|
||||
<member name="M:YD_Event.Core.Infrastructure.Aliyun.Oss.OssService.MultipartUploadAsync(System.String,System.IO.Stream,System.EventHandler{YD_Event.Core.Infrastructure.Aliyun.Oss.StreamTransferProgressArgs})">
|
||||
<summary>
|
||||
异步分片上传大文件
|
||||
</summary>
|
||||
<param name="objectName">OSS 中的目标文件路径</param>
|
||||
<param name="stream">文件数据流</param>
|
||||
<param name="progressCallback">进度回调</param>
|
||||
<returns>文件的访问 URL</returns>
|
||||
</member>
|
||||
<member name="M:YD_Event.Core.Infrastructure.Aliyun.Oss.OssService.GetObject(System.String)">
|
||||
<summary>
|
||||
获取文件流
|
||||
</summary>
|
||||
<param name="objectName">OSS 中的目标文件路径</param>
|
||||
<returns>下载文件的内容数据流</returns>
|
||||
</member>
|
||||
<member name="M:YD_Event.Core.Infrastructure.Aliyun.Oss.OssService.DeleteObject(System.String)">
|
||||
<summary>
|
||||
删除文件
|
||||
</summary>
|
||||
<param name="objectName">OSS 中的目标文件路径</param>
|
||||
</member>
|
||||
<member name="M:YD_Event.Core.Infrastructure.Aliyun.Oss.OssService.DoesObjectExist(System.String)">
|
||||
<summary>
|
||||
检查文件是否存在
|
||||
</summary>
|
||||
<param name="objectName">OSS 中的目标文件路径</param>
|
||||
<returns>如果文件存在返回 true,否则返回 false</returns>
|
||||
</member>
|
||||
<member name="T:YD_Event.Core.Infrastructure.Aliyun.Sms.AliyunSmsService">
|
||||
<summary>
|
||||
阿里云短信服务实现
|
||||
@ -4471,9 +4521,29 @@
|
||||
对象存储连接配置
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Core.Options.OssConnectionOptions.Enable">
|
||||
<summary>
|
||||
是否启用对象存储(不启用将存储至站点目录)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Core.Options.OssConnectionOptions.Endpoint">
|
||||
<summary>
|
||||
OSS节点
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Core.Options.OssConnectionOptions.AccessKeyId">
|
||||
<summary>
|
||||
AccessKeyId
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Core.Options.OssConnectionOptions.AccessKeySecret">
|
||||
<summary>
|
||||
AccessKeySecret
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Core.Options.OssConnectionOptions.Bucket">
|
||||
<summary>
|
||||
默认存目录
|
||||
默认存目录 (Bucket)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Core.Options.OssConnectionOptions.Domain">
|
||||
@ -4481,11 +4551,6 @@
|
||||
外网访问域名或IP
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:YD_Event.Core.Options.OssConnectionOptions.Enable">
|
||||
<summary>
|
||||
是否启用对象存储(不启用将存储至站点目录)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:YD_Event.Core.Options.SmsOptions">
|
||||
<summary>
|
||||
短信配置选项
|
||||
|
||||
@ -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<OssConnectionOptions>("OssConnection");
|
||||
services.AddOSSService(options =>
|
||||
{
|
||||
ossOptions.Adapt(options);
|
||||
});
|
||||
var auth = new QQOAuth(OAuthConfig.LoadFrom(App.Configuration, "oauth:qq"));
|
||||
services.AddSingleton(auth);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user