feat: initialize Evergreen Health backend
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 文件分页查询
|
||||
/// </summary>
|
||||
public class PageFileInput : BasePageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件名称
|
||||
/// </summary>
|
||||
public string FileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件路径
|
||||
/// </summary>
|
||||
public string FilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件后缀
|
||||
/// </summary>
|
||||
public string? Suffix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间
|
||||
/// </summary>
|
||||
public DateTime? StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间
|
||||
/// </summary>
|
||||
public DateTime? EndTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件
|
||||
/// </summary>
|
||||
public class UploadFileInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件
|
||||
/// </summary>
|
||||
[Required]
|
||||
public IFormFile File { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件类别
|
||||
/// </summary>
|
||||
public string FileType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否公开
|
||||
/// </summary>
|
||||
public bool IsPublic { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 允许格式:.jpeg.jpg.png.bmp.gif.tif
|
||||
/// </summary>
|
||||
public string AllowSuffix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 指定存储桶名称
|
||||
/// </summary>
|
||||
public string? BucketName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 指定存储提供者ID
|
||||
/// </summary>
|
||||
public long? ProviderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 业务数据Id
|
||||
/// </summary>
|
||||
public long? DataId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件Base64
|
||||
/// </summary>
|
||||
public class UploadFileFromBase64Input
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件名
|
||||
/// </summary>
|
||||
public string FileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件内容
|
||||
/// </summary>
|
||||
public string FileDataBase64 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件类型( "image/jpeg",)
|
||||
/// </summary>
|
||||
public string ContentType { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询关联查询输入
|
||||
/// </summary>
|
||||
public class RelationQueryInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 关联对象名称
|
||||
/// </summary>
|
||||
public string RelationName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联对象Id
|
||||
/// </summary>
|
||||
public long? RelationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件类型:多个以","分割
|
||||
/// </summary>
|
||||
public string FileTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属Id
|
||||
/// </summary>
|
||||
public long? BelongId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件类型分割
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public string[] GetFileTypeBS()
|
||||
{
|
||||
return FileTypes.Split(',');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 文件存储提供者分页查询输入参数
|
||||
/// </summary>
|
||||
public class PageFileProviderInput : BasePageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 存储提供者
|
||||
/// </summary>
|
||||
public string? Provider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 存储桶名称
|
||||
/// </summary>
|
||||
public string? BucketName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
public bool? IsEnable { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加文件存储提供者输入参数
|
||||
/// </summary>
|
||||
public class AddFileProviderInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 存储提供者
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "存储提供者不能为空")]
|
||||
public string Provider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 存储桶名称
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "存储桶名称不能为空")]
|
||||
public string BucketName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 访问密钥ID(所有云服务商统一使用此字段)
|
||||
/// </summary>
|
||||
public string? AccessKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 密钥
|
||||
/// </summary>
|
||||
public string? SecretKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地域
|
||||
/// </summary>
|
||||
public string? Region { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 端点地址
|
||||
/// </summary>
|
||||
public string? Endpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用HTTPS
|
||||
/// </summary>
|
||||
public bool? IsEnableHttps { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用缓存
|
||||
/// </summary>
|
||||
public bool? IsEnableCache { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
public bool? IsEnable { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否默认提供者
|
||||
/// </summary>
|
||||
public bool? IsDefault { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 自定义域名
|
||||
/// </summary>
|
||||
public string? SinceDomain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序号
|
||||
/// </summary>
|
||||
public int? OrderNo { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// 备注
|
||||
/// </summary>
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 支持的业务类型(JSON格式)
|
||||
/// </summary>
|
||||
public string? BusinessTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 优先级
|
||||
/// </summary>
|
||||
public int Priority { get; set; } = 100;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新文件存储提供者输入参数
|
||||
/// </summary>
|
||||
public class UpdateFileProviderInput : AddFileProviderInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键Id
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "主键Id不能为空")]
|
||||
public long Id { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除文件存储提供者输入参数
|
||||
/// </summary>
|
||||
public class DeleteFileProviderInput : BaseIdInput
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询文件存储提供者输入参数
|
||||
/// </summary>
|
||||
public class QueryFileProviderInput : BaseIdInput
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试连接输入参数
|
||||
/// </summary>
|
||||
public class TestConnectionInput : BaseIdInput
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置默认存储提供者输入参数
|
||||
/// </summary>
|
||||
public class SetDefaultProviderInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 存储提供者ID
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "存储提供者ID不能为空")]
|
||||
public long Id { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 文件上传选择存储提供者输入参数
|
||||
/// </summary>
|
||||
public class SelectProviderInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件类型
|
||||
/// </summary>
|
||||
public string? FileType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 业务类型
|
||||
/// </summary>
|
||||
public string? BusinessType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 指定提供者ID
|
||||
/// </summary>
|
||||
public long? ProviderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 指定存储桶名称
|
||||
/// </summary>
|
||||
public string? BucketName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class DefaultFileProvider : ICustomFileProvider, ITransient
|
||||
{
|
||||
/// <summary>
|
||||
/// 构建文件的完整物理路径
|
||||
/// </summary>
|
||||
/// <param name="sysFile"></param>
|
||||
/// <returns></returns>
|
||||
private string BuildFullFilePath(SysFile sysFile)
|
||||
{
|
||||
return Path.Combine(App.WebHostEnvironment.WebRootPath, sysFile.FilePath ?? "", $"{sysFile.Id}{sysFile.Suffix}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建目录的完整物理路径
|
||||
/// </summary>
|
||||
/// <param name="relativePath"></param>
|
||||
/// <returns></returns>
|
||||
private string BuildFullDirectoryPath(string relativePath)
|
||||
{
|
||||
return Path.Combine(App.WebHostEnvironment.WebRootPath, relativePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保目录存在
|
||||
/// </summary>
|
||||
/// <param name="directoryPath"></param>
|
||||
private void EnsureDirectoryExists(string directoryPath)
|
||||
{
|
||||
if (!Directory.Exists(directoryPath))
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
}
|
||||
|
||||
public Task DeleteFileAsync(SysFile sysFile)
|
||||
{
|
||||
var filePath = BuildFullFilePath(sysFile);
|
||||
if (File.Exists(filePath))
|
||||
File.Delete(filePath);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<string> DownloadFileBase64Async(SysFile sysFile)
|
||||
{
|
||||
var realFile = BuildFullFilePath(sysFile);
|
||||
if (!File.Exists(realFile))
|
||||
{
|
||||
Log.Error($"DownloadFileBase64:文件[{realFile}]不存在");
|
||||
throw Oops.Oh($"文件[{sysFile.FilePath}]不存在");
|
||||
}
|
||||
|
||||
byte[] fileBytes = await File.ReadAllBytesAsync(realFile);
|
||||
return Convert.ToBase64String(fileBytes);
|
||||
}
|
||||
|
||||
public Task<FileStreamResult> GetFileStreamResultAsync(SysFile sysFile, string fileName)
|
||||
{
|
||||
var fullPath = BuildFullFilePath(sysFile);
|
||||
return Task.FromResult(new FileStreamResult(new FileStream(fullPath, FileMode.Open), "application/octet-stream")
|
||||
{
|
||||
FileDownloadName = fileName + sysFile.Suffix
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<SysFile> UploadFileAsync(IFormFile file, SysFile newFile, string path, string finalName)
|
||||
{
|
||||
newFile.Provider = ""; // 本地存储 Provider 显示为空
|
||||
|
||||
var directoryPath = BuildFullDirectoryPath(path);
|
||||
EnsureDirectoryExists(directoryPath);
|
||||
|
||||
var realFile = Path.Combine(directoryPath, finalName);
|
||||
await using var stream = File.Create(realFile);
|
||||
await file.CopyToAsync(stream);
|
||||
|
||||
newFile.Url = $"{newFile.FilePath}/{newFile.Id + newFile.Suffix}";
|
||||
return newFile;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 自定义文件提供器接口
|
||||
/// </summary>
|
||||
public interface ICustomFileProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取文件流
|
||||
/// </summary>
|
||||
/// <param name="sysFile"></param>
|
||||
/// <param name="fileName"></param>
|
||||
/// <returns></returns>
|
||||
public Task<FileStreamResult> GetFileStreamResultAsync(SysFile sysFile, string fileName);
|
||||
|
||||
/// <summary>
|
||||
/// 下载指定文件Base64格式
|
||||
/// </summary>
|
||||
/// <param name="sysFile"></param>
|
||||
/// <returns></returns>
|
||||
public Task<string> DownloadFileBase64Async(SysFile sysFile);
|
||||
|
||||
/// <summary>
|
||||
/// 删除文件
|
||||
/// </summary>
|
||||
/// <param name="sysFile"></param>
|
||||
/// <returns></returns>
|
||||
public Task DeleteFileAsync(SysFile sysFile);
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件
|
||||
/// </summary>
|
||||
/// <param name="file">文件</param>
|
||||
/// <param name="sysFile"></param>
|
||||
/// <param name="path">文件存储位置</param>
|
||||
/// <param name="finalName">文件最终名称</param>
|
||||
/// <returns></returns>
|
||||
public Task<SysFile> UploadFileAsync(IFormFile file, SysFile sysFile, string path, string finalName);
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 多OSS文件提供者
|
||||
/// </summary>
|
||||
public class MultiOSSFileProvider : ICustomFileProvider, ITransient
|
||||
{
|
||||
private readonly SysFileProviderService _fileProviderService;
|
||||
private readonly IOSSServiceManager _ossServiceManager;
|
||||
private readonly OSSProviderOptions _ossProviderOptions;
|
||||
|
||||
public MultiOSSFileProvider(SysFileProviderService fileProviderService,
|
||||
IOSSServiceManager ossServiceManager,
|
||||
IOptions<OSSProviderOptions> ossProviderOptions)
|
||||
{
|
||||
_fileProviderService = fileProviderService;
|
||||
_ossServiceManager = ossServiceManager;
|
||||
_ossProviderOptions = ossProviderOptions.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件
|
||||
/// </summary>
|
||||
/// <param name="file">文件</param>
|
||||
/// <param name="sysFile">系统文件信息</param>
|
||||
/// <param name="path">文件存储位置</param>
|
||||
/// <param name="finalName">文件最终名称</param>
|
||||
/// <returns></returns>
|
||||
public async Task<SysFile> UploadFileAsync(IFormFile file, SysFile sysFile, string path, string finalName)
|
||||
{
|
||||
// 获取OSS配置(传入文件信息用于策略选择)
|
||||
var provider = await GetFileProvider(sysFile, file) ?? throw Oops.Oh("未找到可用的存储提供者配置");
|
||||
|
||||
// 获取OSS服务
|
||||
var ossService = await _ossServiceManager.GetOSSServiceAsync(provider);
|
||||
|
||||
// 设置文件信息
|
||||
sysFile.Provider = provider.Provider;
|
||||
sysFile.BucketName = provider.BucketName; // 保存原始存储桶名称
|
||||
|
||||
var filePath = string.Concat(path, "/", finalName);
|
||||
|
||||
// 上传文件
|
||||
await ossService.PutObjectAsync(provider.BucketName, filePath, file.OpenReadStream());
|
||||
|
||||
// 生成外链地址
|
||||
sysFile.Url = GenerateFileUrl(provider, provider.BucketName, filePath);
|
||||
|
||||
return sysFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除文件
|
||||
/// </summary>
|
||||
/// <param name="sysFile">系统文件信息</param>
|
||||
/// <returns></returns>
|
||||
public async Task DeleteFileAsync(SysFile sysFile)
|
||||
{
|
||||
// 获取OSS配置(统一方法)
|
||||
var provider = await GetFileProvider(sysFile) ?? throw Oops.Oh($"未找到存储提供者配置: {sysFile.Provider}-{sysFile.BucketName}");
|
||||
var ossService = await _ossServiceManager.GetOSSServiceAsync(provider);
|
||||
var filePath = string.Concat(sysFile.FilePath, "/", $"{sysFile.Id}{sysFile.Suffix}");
|
||||
|
||||
await ossService.RemoveObjectAsync(provider.BucketName, filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件流
|
||||
/// </summary>
|
||||
/// <param name="sysFile">系统文件信息</param>
|
||||
/// <param name="fileName">文件名</param>
|
||||
/// <returns></returns>
|
||||
public async Task<FileStreamResult> GetFileStreamResultAsync(SysFile sysFile, string fileName)
|
||||
{
|
||||
// 获取OSS配置(统一方法)
|
||||
var provider = await GetFileProvider(sysFile) ?? throw Oops.Oh($"未找到存储提供者配置: {sysFile.Provider}-{sysFile.BucketName}");
|
||||
var ossService = await _ossServiceManager.GetOSSServiceAsync(provider);
|
||||
var filePath = Path.Combine(sysFile.FilePath ?? "", sysFile.Id + sysFile.Suffix);
|
||||
|
||||
var httpRemoteService = App.GetRequiredService<IHttpRemoteService>();
|
||||
var stream = await httpRemoteService.GetAsStreamAsync(await ossService.PresignedGetObjectAsync(provider.BucketName, filePath, 5));
|
||||
|
||||
return new FileStreamResult(stream, "application/octet-stream") { FileDownloadName = fileName + sysFile.Suffix };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载文件Base64格式
|
||||
/// </summary>
|
||||
/// <param name="sysFile">系统文件信息</param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> DownloadFileBase64Async(SysFile sysFile)
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
HttpResponseMessage response = await httpClient.GetAsync(sysFile.Url);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
byte[] fileBytes = await response.Content.ReadAsByteArrayAsync();
|
||||
return Convert.ToBase64String(fileBytes);
|
||||
}
|
||||
throw Oops.Oh($"下载文件失败,状态码: {response.StatusCode}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件提供者配置(统一方法,支持上传、删除、下载场景)
|
||||
/// </summary>
|
||||
/// <param name="sysFile">系统文件信息</param>
|
||||
/// <param name="file">上传的文件(可选,仅上传时传入)</param>
|
||||
/// <returns></returns>
|
||||
private async Task<SysFileProvider?> GetFileProvider(SysFile sysFile, IFormFile? file = null)
|
||||
{
|
||||
// 1. 如果已指定存储桶,直接使用
|
||||
if (!string.IsNullOrEmpty(sysFile.BucketName))
|
||||
{
|
||||
var provider = await _fileProviderService.GetFileProviderByBucket(sysFile.Provider, sysFile.BucketName);
|
||||
if (provider != null) return provider;
|
||||
|
||||
// 如果数据库中找不到,尝试配置文件兜底
|
||||
if (_ossProviderOptions.Enabled && _ossProviderOptions.Bucket == sysFile.BucketName)
|
||||
{
|
||||
return await CreateProviderFromConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 如果有上传文件信息,使用策略选择(仅上传场景)
|
||||
if (file != null)
|
||||
{
|
||||
var uploadInput = new UploadFileInput
|
||||
{
|
||||
File = file,
|
||||
BucketName = sysFile.BucketName,
|
||||
FileType = sysFile.FileType
|
||||
};
|
||||
|
||||
return await SelectProviderAsync(file, uploadInput);
|
||||
}
|
||||
|
||||
// 3. 最后兜底:使用默认存储提供者
|
||||
return await _fileProviderService.GetDefaultProvider();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 选择合适的OSS存储提供者(内联版本)
|
||||
/// </summary>
|
||||
/// <param name="file">上传的文件</param>
|
||||
/// <param name="input">上传输入参数</param>
|
||||
/// <returns></returns>
|
||||
private async Task<SysFileProvider?> SelectProviderAsync(IFormFile file, UploadFileInput input)
|
||||
{
|
||||
// 1. 优先使用指定的提供者ID
|
||||
if (input.ProviderId.HasValue)
|
||||
{
|
||||
var provider = await _fileProviderService.GetFileProviderById(input.ProviderId.Value);
|
||||
if (provider != null) return provider;
|
||||
}
|
||||
|
||||
// 2. 其次使用指定的存储桶名称
|
||||
if (!string.IsNullOrEmpty(input.BucketName))
|
||||
{
|
||||
var providers = await _fileProviderService.GetCachedFileProviders();
|
||||
var provider = providers.FirstOrDefault(p => p.BucketName == input.BucketName);
|
||||
if (provider != null) return provider;
|
||||
}
|
||||
|
||||
// 3. 使用默认提供者
|
||||
var defaultProvider = await _fileProviderService.GetDefaultProvider();
|
||||
if (defaultProvider != null) return defaultProvider;
|
||||
|
||||
// 4. 兜底:如果数据库中没有配置,尝试从配置文件创建默认提供者
|
||||
return await CreateProviderFromConfiguration();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成文件URL(内联版本)
|
||||
/// </summary>
|
||||
/// <param name="provider">存储提供者配置</param>
|
||||
/// <param name="bucketName">存储桶名称</param>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
/// <returns></returns>
|
||||
private static string GenerateFileUrl(SysFileProvider provider, string bucketName, string filePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(provider);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(bucketName);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(filePath);
|
||||
|
||||
var protocol = provider.IsEnableHttps == true ? "https" : "http";
|
||||
|
||||
// 如果有自定义域名,直接使用
|
||||
if (!string.IsNullOrWhiteSpace(provider.SinceDomain))
|
||||
{
|
||||
return $"{provider.SinceDomain.TrimEnd('/')}/{filePath.TrimStart('/')}";
|
||||
}
|
||||
|
||||
// 根据不同提供者生成URL
|
||||
return provider.Provider.ToUpper() switch
|
||||
{
|
||||
"ALIYUN" => $"{protocol}://{bucketName}.{provider.Endpoint}/{filePath.TrimStart('/')}",
|
||||
"QCLOUD" => $"{protocol}://{bucketName}-{provider.Endpoint}.cos.{provider.Region}.myqcloud.com/{filePath.TrimStart('/')}",
|
||||
"MINIO" => $"{protocol}://{provider.Endpoint}/{bucketName}/{filePath.TrimStart('/')}",
|
||||
_ => throw Oops.Oh($"不支持的OSS提供者: {provider.Provider}")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从配置文件创建默认提供者(兜底机制)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private Task<SysFileProvider?> CreateProviderFromConfiguration()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查是否启用了OSS配置
|
||||
if (!_ossProviderOptions.Enabled && !App.Configuration["MultiOSS:Enabled"].ToBoolean())
|
||||
return Task.FromResult<SysFileProvider?>(null);
|
||||
|
||||
// 验证必要配置
|
||||
if (string.IsNullOrWhiteSpace(_ossProviderOptions.AccessKey) ||
|
||||
string.IsNullOrWhiteSpace(_ossProviderOptions.SecretKey) ||
|
||||
string.IsNullOrWhiteSpace(_ossProviderOptions.Bucket))
|
||||
{
|
||||
return Task.FromResult<SysFileProvider?>(null);
|
||||
}
|
||||
|
||||
// 使用现有的OSSProviderOptions创建临时提供者配置(不保存到数据库)
|
||||
var provider = new SysFileProvider
|
||||
{
|
||||
Id = 0, // 临时ID
|
||||
Provider = Enum.GetName(_ossProviderOptions.Provider),
|
||||
BucketName = _ossProviderOptions.Bucket,
|
||||
AccessKey = _ossProviderOptions.AccessKey,
|
||||
SecretKey = _ossProviderOptions.SecretKey,
|
||||
Endpoint = _ossProviderOptions.Endpoint,
|
||||
Region = _ossProviderOptions.Region,
|
||||
IsEnableHttps = _ossProviderOptions.IsEnableHttps,
|
||||
IsEnableCache = _ossProviderOptions.IsEnableCache,
|
||||
IsEnable = true,
|
||||
IsDefault = true,
|
||||
SinceDomain = _ossProviderOptions.CustomHost,
|
||||
CreateTime = DateTime.Now
|
||||
};
|
||||
|
||||
return Task.FromResult<SysFileProvider?>(provider);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 配置读取失败,返回null
|
||||
return Task.FromResult<SysFileProvider?>(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using OnceMi.AspNetCore.OSS;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class OSSFileProvider : ICustomFileProvider, ITransient
|
||||
{
|
||||
private readonly IOSSService _OSSService;
|
||||
private readonly OSSProviderOptions _OSSProviderOptions;
|
||||
|
||||
public OSSFileProvider(IOptions<OSSProviderOptions> oSSProviderOptions, IOSSServiceFactory ossServiceFactory)
|
||||
{
|
||||
_OSSProviderOptions = oSSProviderOptions.Value;
|
||||
if (_OSSProviderOptions.Enabled)
|
||||
_OSSService = ossServiceFactory.Create(Enum.GetName(_OSSProviderOptions.Provider));
|
||||
}
|
||||
|
||||
public async Task DeleteFileAsync(SysFile file)
|
||||
{
|
||||
await _OSSService.RemoveObjectAsync(file.BucketName, string.Concat(file.FilePath, "/", $"{file.Id}{file.Suffix}"));
|
||||
}
|
||||
|
||||
public async Task<string> DownloadFileBase64Async(SysFile file)
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
HttpResponseMessage response = await httpClient.GetAsync(file.Url);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
// 读取文件内容并将其转换为 Base64 字符串
|
||||
byte[] fileBytes = await response.Content.ReadAsByteArrayAsync();
|
||||
return Convert.ToBase64String(fileBytes);
|
||||
}
|
||||
throw new HttpRequestException($"Request failed with status code: {response.StatusCode}");
|
||||
}
|
||||
|
||||
public async Task<FileStreamResult> GetFileStreamResultAsync(SysFile file, string fileName)
|
||||
{
|
||||
var filePath = Path.Combine(file.FilePath ?? "", file.Id + file.Suffix);
|
||||
var httpRemoteService = App.GetRequiredService<IHttpRemoteService>();
|
||||
var stream = await httpRemoteService.GetAsStreamAsync(await _OSSService.PresignedGetObjectAsync(file.BucketName, filePath, 5));
|
||||
return new FileStreamResult(stream, "application/octet-stream") { FileDownloadName = fileName + file.Suffix };
|
||||
}
|
||||
|
||||
public async Task<SysFile> UploadFileAsync(IFormFile file, SysFile sysFile, string path, string finalName)
|
||||
{
|
||||
sysFile.Provider = Enum.GetName(_OSSProviderOptions.Provider);
|
||||
var filePath = string.Concat(path, "/", finalName);
|
||||
await _OSSService.PutObjectAsync(sysFile.BucketName, filePath, file.OpenReadStream());
|
||||
// http://<你的bucket名字>.oss.aliyuncs.com/<你的object名字>
|
||||
// 生成外链地址 方便前端预览
|
||||
switch (_OSSProviderOptions.Provider)
|
||||
{
|
||||
case OSSProvider.Aliyun:
|
||||
sysFile.Url = $"{(_OSSProviderOptions.IsEnableHttps ? "https" : "http")}://{sysFile.BucketName}.{_OSSProviderOptions.Endpoint}/{filePath}";
|
||||
break;
|
||||
|
||||
case OSSProvider.QCloud:
|
||||
var protocol = _OSSProviderOptions.IsEnableHttps ? "https" : "http";
|
||||
sysFile.Url = !string.IsNullOrWhiteSpace(_OSSProviderOptions.CustomHost)
|
||||
? $"{protocol}://{_OSSProviderOptions.CustomHost}/{filePath}"
|
||||
: $"{protocol}://{sysFile.BucketName}-{_OSSProviderOptions.Endpoint}.cos.{_OSSProviderOptions.Region}.myqcloud.com/{filePath}";
|
||||
break;
|
||||
|
||||
case OSSProvider.Minio:
|
||||
// 获取Minio文件的下载或者预览地址
|
||||
// newFile.Url = await GetMinioPreviewFileUrl(newFile.BucketName, filePath);// 这种方法生成的Url是有7天有效期的,不能这样使用
|
||||
// 需要在MinIO中的Buckets开通对 Anonymous 的readonly权限
|
||||
var customHost = _OSSProviderOptions.CustomHost;
|
||||
if (string.IsNullOrWhiteSpace(customHost))
|
||||
customHost = _OSSProviderOptions.Endpoint;
|
||||
sysFile.Url = $"{(_OSSProviderOptions.IsEnableHttps ? "https" : "http")}://{customHost}/{sysFile.BucketName}/{filePath}";
|
||||
break;
|
||||
}
|
||||
return sysFile;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class SSHFileProvider : ICustomFileProvider, ITransient
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建SSH连接助手
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private SSHHelper CreateSSHHelper()
|
||||
{
|
||||
return new SSHHelper(
|
||||
App.Configuration["SSHProvider:Host"],
|
||||
App.Configuration["SSHProvider:Port"].ToInt(),
|
||||
App.Configuration["SSHProvider:Username"],
|
||||
App.Configuration["SSHProvider:Password"]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建文件完整路径
|
||||
/// </summary>
|
||||
/// <param name="sysFile"></param>
|
||||
/// <returns></returns>
|
||||
private string BuildFilePath(SysFile sysFile)
|
||||
{
|
||||
return string.Concat(sysFile.FilePath, "/", sysFile.Id + sysFile.Suffix);
|
||||
}
|
||||
|
||||
public Task DeleteFileAsync(SysFile sysFile)
|
||||
{
|
||||
var fullPath = BuildFilePath(sysFile);
|
||||
using var helper = CreateSSHHelper();
|
||||
helper.DeleteFile(fullPath);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<string> DownloadFileBase64Async(SysFile sysFile)
|
||||
{
|
||||
using var helper = CreateSSHHelper();
|
||||
return Task.FromResult(Convert.ToBase64String(helper.ReadAllBytes(sysFile.FilePath)));
|
||||
}
|
||||
|
||||
public Task<FileStreamResult> GetFileStreamResultAsync(SysFile sysFile, string fileName)
|
||||
{
|
||||
var filePath = BuildFilePath(sysFile);
|
||||
using var helper = CreateSSHHelper();
|
||||
return Task.FromResult(new FileStreamResult(helper.OpenRead(filePath), "application/octet-stream")
|
||||
{
|
||||
FileDownloadName = fileName + sysFile.Suffix
|
||||
});
|
||||
}
|
||||
|
||||
public Task<SysFile> UploadFileAsync(IFormFile file, SysFile sysFile, string path, string finalName)
|
||||
{
|
||||
var fullPath = string.Concat(path.StartsWith('/') ? path : "/" + path, "/", finalName);
|
||||
using var helper = CreateSSHHelper();
|
||||
helper.UploadFile(file.OpenReadStream(), fullPath);
|
||||
return Task.FromResult(sysFile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
using OnceMi.AspNetCore.OSS;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// OSS服务管理器接口
|
||||
/// </summary>
|
||||
public interface IOSSServiceManager : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取OSS服务实例
|
||||
/// </summary>
|
||||
/// <param name="provider">存储提供者配置</param>
|
||||
/// <returns></returns>
|
||||
Task<IOSSService> GetOSSServiceAsync(SysFileProvider provider);
|
||||
|
||||
/// <summary>
|
||||
/// 清除缓存
|
||||
/// </summary>
|
||||
void ClearCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OSS服务管理器实现
|
||||
/// </summary>
|
||||
public class OSSServiceManager : IOSSServiceManager, ITransient
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ConcurrentDictionary<string, IOSSService> _ossServiceCache;
|
||||
private readonly object _lockObject = new object();
|
||||
private bool _disposed = false;
|
||||
|
||||
public OSSServiceManager(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||
_ossServiceCache = new ConcurrentDictionary<string, IOSSService>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取OSS服务实例(带缓存)
|
||||
/// </summary>
|
||||
/// <param name="provider">存储提供者配置</param>
|
||||
/// <returns></returns>
|
||||
public async Task<IOSSService> GetOSSServiceAsync(SysFileProvider provider)
|
||||
{
|
||||
if (provider == null)
|
||||
throw new ArgumentNullException(nameof(provider));
|
||||
|
||||
var cacheKey = provider.ConfigKey;
|
||||
|
||||
// 尝试从缓存获取
|
||||
if (_ossServiceCache.TryGetValue(cacheKey, out var cachedService))
|
||||
{
|
||||
return cachedService;
|
||||
}
|
||||
|
||||
// 验证配置
|
||||
if (!await ValidateConfigurationAsync(provider))
|
||||
{
|
||||
throw new InvalidOperationException($"OSS提供者配置无效: {provider.DisplayName}");
|
||||
}
|
||||
|
||||
// 线程安全地创建新服务
|
||||
lock (_lockObject)
|
||||
{
|
||||
// 双重检查锁定模式
|
||||
if (_ossServiceCache.TryGetValue(cacheKey, out cachedService))
|
||||
{
|
||||
return cachedService;
|
||||
}
|
||||
|
||||
// 转换配置并创建服务
|
||||
var ossOptions = ConvertToOSSOptions(provider);
|
||||
var ossService = CreateOSSService(ossOptions);
|
||||
|
||||
// 添加到缓存
|
||||
_ossServiceCache.TryAdd(cacheKey, ossService);
|
||||
|
||||
return ossService;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建OSS服务实例
|
||||
/// </summary>
|
||||
/// <param name="options">OSS配置选项</param>
|
||||
/// <returns></returns>
|
||||
private IOSSService CreateOSSService(OSSOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
try
|
||||
{
|
||||
// 使用现有的IOSSServiceFactory,但需要先注册配置
|
||||
var providerName = Enum.GetName(options.Provider);
|
||||
var configSectionName = $"TempOSS_{Guid.NewGuid():N}";
|
||||
|
||||
// 创建临时配置
|
||||
var configData = new Dictionary<string, string>
|
||||
{
|
||||
[$"{configSectionName}:Provider"] = providerName,
|
||||
[$"{configSectionName}:Endpoint"] = options.Endpoint ?? "",
|
||||
[$"{configSectionName}:AccessKey"] = options.AccessKey ?? "",
|
||||
[$"{configSectionName}:SecretKey"] = options.SecretKey ?? "",
|
||||
[$"{configSectionName}:Region"] = options.Region ?? "",
|
||||
[$"{configSectionName}:IsEnableHttps"] = options.IsEnableHttps.ToString(),
|
||||
[$"{configSectionName}:IsEnableCache"] = options.IsEnableCache.ToString()
|
||||
};
|
||||
|
||||
var tempConfig = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(configData)
|
||||
.Build();
|
||||
|
||||
// 创建临时服务集合,但不立即释放
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IConfiguration>(tempConfig);
|
||||
services.AddLogging();
|
||||
services.AddOSSService(providerName, configSectionName);
|
||||
|
||||
// 构建服务提供者并创建OSS服务
|
||||
var tempServiceProvider = services.BuildServiceProvider();
|
||||
var ossServiceFactory = tempServiceProvider.GetRequiredService<IOSSServiceFactory>();
|
||||
var ossService = ossServiceFactory.Create(providerName);
|
||||
|
||||
// 注意:不要释放tempServiceProvider,因为ossService可能依赖它
|
||||
// 这里我们接受这个内存开销,因为缓存会减少创建频率
|
||||
|
||||
return ossService;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw Oops.Oh($"创建OSS服务失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证配置
|
||||
/// </summary>
|
||||
/// <param name="provider">存储提供者配置</param>
|
||||
/// <returns></returns>
|
||||
private Task<bool> ValidateConfigurationAsync(SysFileProvider provider)
|
||||
{
|
||||
if (provider == null) return Task.FromResult(false);
|
||||
|
||||
// 基本字段验证
|
||||
var isValid = !string.IsNullOrWhiteSpace(provider.Provider) &&
|
||||
!string.IsNullOrWhiteSpace(provider.BucketName) &&
|
||||
!string.IsNullOrWhiteSpace(provider.AccessKey) &&
|
||||
!string.IsNullOrWhiteSpace(provider.SecretKey);
|
||||
|
||||
// Minio额外需要Endpoint
|
||||
if (provider.Provider.ToUpper() == "MINIO")
|
||||
{
|
||||
isValid = isValid && !string.IsNullOrWhiteSpace(provider.Endpoint);
|
||||
}
|
||||
|
||||
return Task.FromResult(isValid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将SysFileProvider转换为OSSOptions
|
||||
/// </summary>
|
||||
/// <param name="provider"></param>
|
||||
/// <returns></returns>
|
||||
private OSSOptions ConvertToOSSOptions(SysFileProvider provider)
|
||||
{
|
||||
if (provider == null)
|
||||
throw new ArgumentNullException(nameof(provider));
|
||||
|
||||
var ossOptions = new OSSOptions
|
||||
{
|
||||
Provider = Enum.Parse<OSSProvider>(provider.Provider),
|
||||
Endpoint = provider.Endpoint,
|
||||
Region = provider.Region,
|
||||
IsEnableHttps = provider.IsEnableHttps ?? true,
|
||||
IsEnableCache = provider.IsEnableCache ?? true
|
||||
};
|
||||
|
||||
// 设置认证信息(所有提供者现在都使用统一的字段)
|
||||
ossOptions.AccessKey = provider.AccessKey;
|
||||
ossOptions.SecretKey = provider.SecretKey;
|
||||
|
||||
return ossOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除缓存
|
||||
/// </summary>
|
||||
public void ClearCache()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
_ossServiceCache.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
_ossServiceCache.Clear();
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 文件存储提供者管理控制器 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 412, Description = "文件存储提供者管理")]
|
||||
public class SysFileProviderController : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly SysFileProviderService _fileProviderService;
|
||||
|
||||
public SysFileProviderController(SysFileProviderService fileProviderService)
|
||||
{
|
||||
_fileProviderService = fileProviderService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取存储提供者列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取存储提供者列表")]
|
||||
public async Task<List<SysFileProvider>> GetProviderList()
|
||||
{
|
||||
return await _fileProviderService.GetFileProviderList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取存储提供者分页列表 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取存储提供者分页列表")]
|
||||
public async Task<SqlSugarPagedList<SysFileProvider>> GetProviderPage(PageFileProviderInput input)
|
||||
{
|
||||
return await _fileProviderService.GetFileProviderPage(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取存储提供者详情 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取存储提供者详情")]
|
||||
public async Task<SysFileProvider> GetProvider([FromQuery] QueryFileProviderInput input)
|
||||
{
|
||||
return await _fileProviderService.GetFileProvider(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加存储提供者 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Add"), HttpPost]
|
||||
[DisplayName("添加存储提供者")]
|
||||
public async Task AddProvider(AddFileProviderInput input)
|
||||
{
|
||||
await _fileProviderService.AddFileProvider(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新存储提供者 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Update"), HttpPost]
|
||||
[DisplayName("更新存储提供者")]
|
||||
public async Task UpdateProvider(UpdateFileProviderInput input)
|
||||
{
|
||||
await _fileProviderService.UpdateFileProvider(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除存储提供者 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
|
||||
[DisplayName("删除存储提供者")]
|
||||
public async Task DeleteProvider(DeleteFileProviderInput input)
|
||||
{
|
||||
await _fileProviderService.DeleteFileProvider(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据存储桶名称获取存储提供者 🔖
|
||||
/// </summary>
|
||||
/// <param name="bucketName">存储桶名称</param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("根据存储桶名称获取存储提供者")]
|
||||
public async Task<SysFileProvider?> GetProviderByBucketName(string bucketName)
|
||||
{
|
||||
return await _fileProviderService.GetProviderByBucketName(bucketName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除存储提供者缓存 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("清除存储提供者缓存")]
|
||||
public async Task ClearCache()
|
||||
{
|
||||
await _fileProviderService.ClearCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量启用/禁用存储提供者 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "BatchEnable"), HttpPost]
|
||||
[DisplayName("批量启用/禁用存储提供者")]
|
||||
public async Task BatchEnableProvider(BatchEnableProviderInput input)
|
||||
{
|
||||
foreach (var id in input.Ids)
|
||||
{
|
||||
var provider = await _fileProviderService.GetFileProviderById(id);
|
||||
if (provider != null)
|
||||
{
|
||||
var updateInput = new UpdateFileProviderInput
|
||||
{
|
||||
Id = id,
|
||||
Provider = provider.Provider,
|
||||
BucketName = provider.BucketName,
|
||||
IsEnable = input.IsEnable
|
||||
};
|
||||
await _fileProviderService.UpdateFileProvider(updateInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取存储提供者统计信息 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取存储提供者统计信息")]
|
||||
public async Task<object> GetProviderStatistics()
|
||||
{
|
||||
var providers = await _fileProviderService.GetCachedFileProviders();
|
||||
|
||||
var statistics = new
|
||||
{
|
||||
Total = providers.Count,
|
||||
Enabled = providers.Count(p => p.IsEnable == true),
|
||||
Disabled = providers.Count(p => p.IsEnable != true),
|
||||
ByProvider = providers.GroupBy(p => p.Provider)
|
||||
.Select(g => new { Provider = g.Key, Count = g.Count() })
|
||||
.ToList(),
|
||||
ByRegion = providers.Where(p => !string.IsNullOrEmpty(p.Region))
|
||||
.GroupBy(p => p.Region)
|
||||
.Select(g => new { Region = g.Key, Count = g.Count() })
|
||||
.ToList()
|
||||
};
|
||||
|
||||
return statistics;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有可用的存储桶列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取所有可用的存储桶列表")]
|
||||
public async Task<List<string>> GetAvailableBuckets()
|
||||
{
|
||||
return await _fileProviderService.GetAvailableBuckets();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取存储桶和提供者的映射关系 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取存储桶和提供者的映射关系")]
|
||||
public async Task<Dictionary<string, List<SysFileProvider>>> GetBucketProviderMapping()
|
||||
{
|
||||
return await _fileProviderService.GetBucketProviderMapping();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量启用/禁用存储提供者输入参数
|
||||
/// </summary>
|
||||
public class BatchEnableProviderInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 存储提供者ID列表
|
||||
/// </summary>
|
||||
[Required]
|
||||
public List<long> Ids { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
public bool IsEnable { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,520 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using OnceMi.AspNetCore.OSS;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统文件存储提供者服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 411, Description = "文件存储提供者")]
|
||||
public class SysFileProviderService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly UserManager _userManager;
|
||||
private readonly SqlSugarRepository<SysFileProvider> _sysFileProviderRep;
|
||||
private readonly SysCacheService _sysCacheService;
|
||||
private readonly IOSSServiceFactory _ossServiceFactory;
|
||||
private readonly IOSSServiceManager _ossServiceManager;
|
||||
private static readonly string CacheKey = "sys_file_provider";
|
||||
|
||||
public SysFileProviderService(UserManager userManager,
|
||||
SqlSugarRepository<SysFileProvider> sysFileProviderRep,
|
||||
SysCacheService sysCacheService,
|
||||
IOSSServiceFactory ossServiceFactory,
|
||||
IOSSServiceManager ossServiceManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_sysFileProviderRep = sysFileProviderRep;
|
||||
_sysCacheService = sysCacheService;
|
||||
_ossServiceFactory = ossServiceFactory;
|
||||
_ossServiceManager = ossServiceManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件存储提供者分页列表 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取文件存储提供者分页列表")]
|
||||
[NonAction]
|
||||
public async Task<SqlSugarPagedList<SysFileProvider>> GetFileProviderPage([FromQuery] PageFileProviderInput input)
|
||||
{
|
||||
return await _sysFileProviderRep.AsQueryable()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Provider), u => u.Provider.Contains(input.Provider!))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.BucketName), u => u.BucketName.Contains(input.BucketName!))
|
||||
.WhereIF(input.IsEnable.HasValue, u => u.IsEnable == input.IsEnable)
|
||||
.OrderBy(u => u.OrderNo)
|
||||
.OrderBy(u => u.Id)
|
||||
.ToPagedListAsync(input.Page, input.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件存储提供者列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取文件存储提供者列表")]
|
||||
[NonAction]
|
||||
public async Task<List<SysFileProvider>> GetFileProviderList()
|
||||
{
|
||||
return await _sysFileProviderRep.AsQueryable()
|
||||
.Where(u => u.IsEnable == true)
|
||||
.OrderBy(u => u.OrderNo)
|
||||
.OrderBy(u => u.Id)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加文件存储提供者 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Add"), HttpPost]
|
||||
[DisplayName("增加文件存储提供者")]
|
||||
[NonAction]
|
||||
public async Task AddFileProvider(AddFileProviderInput input)
|
||||
{
|
||||
// 验证输入参数
|
||||
if (input == null)
|
||||
throw Oops.Oh("输入参数不能为空").StatusCode(400);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.Provider))
|
||||
throw Oops.Oh("存储提供者不能为空").StatusCode(400);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.BucketName))
|
||||
throw Oops.Oh("存储桶名称不能为空").StatusCode(400);
|
||||
|
||||
// 验证提供者类型
|
||||
if (!Enum.TryParse<OSSProvider>(input.Provider, true, out _))
|
||||
throw Oops.Oh($"不支持的存储提供者类型: {input.Provider}").StatusCode(400);
|
||||
|
||||
var isExist = await _sysFileProviderRep.AsQueryable()
|
||||
.AnyAsync(u => u.Provider == input.Provider && u.BucketName == input.BucketName);
|
||||
if (isExist)
|
||||
throw Oops.Oh(ErrorCodeEnum.D1006).StatusCode(400);
|
||||
|
||||
var fileProvider = input.Adapt<SysFileProvider>();
|
||||
|
||||
// 验证配置完整性
|
||||
await ValidateProviderConfiguration(fileProvider);
|
||||
|
||||
// 处理默认提供者逻辑
|
||||
await HandleDefaultProviderLogic(fileProvider);
|
||||
|
||||
await _sysFileProviderRep.InsertAsync(fileProvider);
|
||||
|
||||
// 清除缓存
|
||||
await ClearCache();
|
||||
|
||||
// 清除OSS服务缓存
|
||||
_ossServiceManager?.ClearCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新文件存储提供者 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Update"), HttpPost]
|
||||
[DisplayName("更新文件存储提供者")]
|
||||
[NonAction]
|
||||
public async Task UpdateFileProvider(UpdateFileProviderInput input)
|
||||
{
|
||||
// 验证输入参数
|
||||
if (input == null)
|
||||
throw Oops.Oh("输入参数不能为空").StatusCode(400);
|
||||
|
||||
var isExist = await _sysFileProviderRep.AsQueryable()
|
||||
.AnyAsync(u => u.Provider == input.Provider && u.BucketName == input.BucketName && u.Id != input.Id);
|
||||
if (isExist)
|
||||
throw Oops.Oh(ErrorCodeEnum.D1006).StatusCode(400);
|
||||
|
||||
var fileProvider = input.Adapt<SysFileProvider>();
|
||||
|
||||
// 验证配置完整性
|
||||
await ValidateProviderConfiguration(fileProvider);
|
||||
|
||||
// 处理默认提供者逻辑
|
||||
await HandleDefaultProviderLogic(fileProvider);
|
||||
|
||||
await _sysFileProviderRep.AsUpdateable(fileProvider).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync();
|
||||
|
||||
// 清除缓存
|
||||
await ClearCache();
|
||||
|
||||
// 清除OSS服务缓存
|
||||
_ossServiceManager?.ClearCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除文件存储提供者 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
|
||||
[DisplayName("删除文件存储提供者")]
|
||||
[NonAction]
|
||||
public async Task DeleteFileProvider(DeleteFileProviderInput input)
|
||||
{
|
||||
// 检查是否为默认提供者
|
||||
var provider = await _sysFileProviderRep.GetByIdAsync(input.Id) ?? throw Oops.Oh("存储提供者不存在").StatusCode(400);
|
||||
var isDefault = provider.IsDefault == true;
|
||||
|
||||
await _sysFileProviderRep.DeleteByIdAsync(input.Id);
|
||||
|
||||
// 如果删除的是默认提供者,自动设置第一个启用的提供者为默认
|
||||
if (isDefault)
|
||||
{
|
||||
var firstEnabledProvider = await _sysFileProviderRep.AsQueryable()
|
||||
.Where(p => p.IsEnable == true)
|
||||
.OrderBy(p => p.OrderNo)
|
||||
.OrderBy(p => p.Id)
|
||||
.FirstAsync();
|
||||
|
||||
if (firstEnabledProvider != null)
|
||||
{
|
||||
await _sysFileProviderRep.AsUpdateable()
|
||||
.SetColumns(p => p.IsDefault == true)
|
||||
.Where(p => p.Id == firstEnabledProvider.Id)
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
Debug.WriteLine($"自动设置新的默认提供者: {firstEnabledProvider.DisplayName}");
|
||||
}
|
||||
}
|
||||
|
||||
// 清除缓存
|
||||
await ClearCache();
|
||||
|
||||
// 清除OSS服务缓存
|
||||
_ossServiceManager?.ClearCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件存储提供者详情 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取文件存储提供者详情")]
|
||||
[NonAction]
|
||||
public async Task<SysFileProvider> GetFileProvider([FromQuery] QueryFileProviderInput input)
|
||||
{
|
||||
return await _sysFileProviderRep.GetFirstAsync(u => u.Id == input.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据提供者和存储桶获取配置
|
||||
/// </summary>
|
||||
/// <param name="provider"></param>
|
||||
/// <param name="bucketName"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<SysFileProvider?> GetFileProviderByBucket(string provider, string bucketName)
|
||||
{
|
||||
var providers = await GetCachedFileProviders();
|
||||
return providers.FirstOrDefault(x => x.Provider == provider && x.BucketName == bucketName && x.IsEnable == true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取配置
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<SysFileProvider?> GetFileProviderById(long id)
|
||||
{
|
||||
var providers = await GetCachedFileProviders();
|
||||
return providers.FirstOrDefault(x => x.Id == id && x.IsEnable == true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据存储桶名称获取存储提供者
|
||||
/// </summary>
|
||||
/// <param name="bucketName">存储桶名称</param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<SysFileProvider?> GetProviderByBucketName(string bucketName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(bucketName))
|
||||
return null;
|
||||
|
||||
var providers = await GetCachedFileProviders();
|
||||
return providers.FirstOrDefault(p => p.BucketName == bucketName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取默认存储提供者
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<SysFileProvider?> GetDefaultProvider()
|
||||
{
|
||||
var providers = await GetCachedFileProviders();
|
||||
|
||||
// 优先返回标记为默认的提供者
|
||||
var defaultProvider = providers.FirstOrDefault(p => p.IsDefault == true);
|
||||
if (defaultProvider != null)
|
||||
return defaultProvider;
|
||||
|
||||
// 如果没有标记为默认的,返回第一个启用的提供者(兼容旧逻辑)
|
||||
return providers.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取默认存储提供者信息 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取默认存储提供者信息")]
|
||||
[NonAction]
|
||||
public async Task<SysFileProvider?> GetDefaultProviderInfo()
|
||||
{
|
||||
return await GetDefaultProvider();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置默认存储提供者 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "SetDefault"), HttpPost]
|
||||
[DisplayName("设置默认存储提供者")]
|
||||
[NonAction]
|
||||
public async Task SetDefaultProvider(SetDefaultProviderInput input)
|
||||
{
|
||||
// 验证提供者是否存在且启用
|
||||
var provider = await _sysFileProviderRep.GetByIdAsync(input.Id) ?? throw Oops.Oh("存储提供者不存在").StatusCode(400);
|
||||
if (provider.IsEnable != true)
|
||||
throw Oops.Oh("只能设置启用状态的存储提供者为默认").StatusCode(400);
|
||||
|
||||
// 开启事务,确保数据一致性
|
||||
await _sysFileProviderRep.AsTenant().BeginTranAsync();
|
||||
try
|
||||
{
|
||||
// 先将所有提供者的默认标识设为false
|
||||
await _sysFileProviderRep.AsUpdateable()
|
||||
.SetColumns(p => p.IsDefault == false)
|
||||
.Where(p => p.IsDefault == true)
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
// 设置指定提供者为默认
|
||||
await _sysFileProviderRep.AsUpdateable()
|
||||
.SetColumns(p => p.IsDefault == true)
|
||||
.Where(p => p.Id == input.Id)
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
await _sysFileProviderRep.AsTenant().CommitTranAsync();
|
||||
|
||||
// 清除缓存
|
||||
await ClearCache();
|
||||
|
||||
// 清除OSS服务缓存
|
||||
_ossServiceManager?.ClearCache();
|
||||
|
||||
Debug.WriteLine($"已设置默认存储提供者: {provider.DisplayName}");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await _sysFileProviderRep.AsTenant().RollbackTranAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存的文件提供者列表
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<List<SysFileProvider>> GetCachedFileProviders()
|
||||
{
|
||||
return await _sysCacheService.AdGetAsync(CacheKey, async () =>
|
||||
{
|
||||
return await _sysFileProviderRep.AsQueryable()
|
||||
.Where(u => u.IsEnable == true)
|
||||
.OrderBy(u => u.OrderNo)
|
||||
.OrderBy(u => u.Id)
|
||||
.ToListAsync();
|
||||
}, TimeSpan.FromMinutes(30));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除缓存
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task ClearCache()
|
||||
{
|
||||
_sysCacheService.Remove(CacheKey);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有可用的存储桶列表
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<List<string>> GetAvailableBuckets()
|
||||
{
|
||||
var providers = await GetCachedFileProviders();
|
||||
return providers.Select(p => p.BucketName).Distinct().OrderBy(b => b).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取存储桶和提供者的映射关系
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<Dictionary<string, List<SysFileProvider>>> GetBucketProviderMapping()
|
||||
{
|
||||
var providers = await GetCachedFileProviders();
|
||||
var mapping = new Dictionary<string, List<SysFileProvider>>();
|
||||
|
||||
foreach (var provider in providers)
|
||||
{
|
||||
if (!mapping.TryGetValue(provider.BucketName, out List<SysFileProvider> value))
|
||||
{
|
||||
value = new List<SysFileProvider>();
|
||||
mapping[provider.BucketName] = value;
|
||||
}
|
||||
|
||||
value.Add(provider);
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证存储提供者配置
|
||||
/// </summary>
|
||||
/// <param name="provider">存储提供者配置</param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
private async Task ValidateProviderConfiguration(SysFileProvider provider)
|
||||
{
|
||||
if (provider == null)
|
||||
throw Oops.Oh("存储提供者配置不能为空").StatusCode(400);
|
||||
|
||||
// 基础字段验证
|
||||
if (string.IsNullOrWhiteSpace(provider.Provider))
|
||||
throw Oops.Oh("存储提供者类型不能为空").StatusCode(400);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(provider.BucketName))
|
||||
throw Oops.Oh("存储桶名称不能为空").StatusCode(400);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(provider.Endpoint))
|
||||
throw Oops.Oh("端点地址不能为空").StatusCode(400);
|
||||
|
||||
// 所有提供者都需要AccessKey和SecretKey
|
||||
if (string.IsNullOrWhiteSpace(provider.AccessKey))
|
||||
throw Oops.Oh($"{provider.Provider} AccessKey不能为空").StatusCode(400);
|
||||
if (string.IsNullOrWhiteSpace(provider.SecretKey))
|
||||
throw Oops.Oh($"{provider.Provider} SecretKey不能为空").StatusCode(400);
|
||||
|
||||
// 根据不同提供者验证特定字段
|
||||
switch (provider.Provider.ToUpper())
|
||||
{
|
||||
case "ALIYUN":
|
||||
if (string.IsNullOrWhiteSpace(provider.Region))
|
||||
throw Oops.Oh("阿里云Region不能为空").StatusCode(400);
|
||||
break;
|
||||
|
||||
case "QCLOUD":
|
||||
if (string.IsNullOrWhiteSpace(provider.Endpoint))
|
||||
throw Oops.Oh("腾讯云Endpoint(AppId)不能为空").StatusCode(400);
|
||||
if (string.IsNullOrWhiteSpace(provider.Region))
|
||||
throw Oops.Oh("腾讯云Region不能为空").StatusCode(400);
|
||||
break;
|
||||
|
||||
case "MINIO":
|
||||
// Minio只需要AccessKey和SecretKey,已在上面验证
|
||||
break;
|
||||
|
||||
default:
|
||||
throw Oops.Oh($"不支持的存储提供者类型: {provider.Provider}").StatusCode(400);
|
||||
}
|
||||
|
||||
// 验证存储桶名称格式
|
||||
await ValidateBucketName(provider.Provider, provider.BucketName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证存储桶名称格式
|
||||
/// </summary>
|
||||
/// <param name="provider">存储提供者类型</param>
|
||||
/// <param name="bucketName">存储桶名称</param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
private async Task ValidateBucketName(string provider, string bucketName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(bucketName))
|
||||
return;
|
||||
|
||||
switch (provider.ToUpper())
|
||||
{
|
||||
case "ALIYUN":
|
||||
// 阿里云存储桶命名规则
|
||||
if (bucketName.Length < 3 || bucketName.Length > 63)
|
||||
throw Oops.Oh("阿里云存储桶名称长度必须在3-63字符之间").StatusCode(400);
|
||||
|
||||
if (!Regex.IsMatch(bucketName, @"^[a-z0-9][a-z0-9\-]*[a-z0-9]$"))
|
||||
throw Oops.Oh("阿里云存储桶名称只能包含小写字母、数字和短横线,且必须以字母或数字开头和结尾").StatusCode(400);
|
||||
break;
|
||||
|
||||
case "QCLOUD":
|
||||
// 腾讯云存储桶命名规则
|
||||
if (bucketName.Length < 1 || bucketName.Length > 40)
|
||||
throw Oops.Oh("腾讯云存储桶名称长度必须在1-40字符之间").StatusCode(400);
|
||||
|
||||
if (!Regex.IsMatch(bucketName, @"^[a-z0-9][a-z0-9\-]*[a-z0-9]$"))
|
||||
throw Oops.Oh("腾讯云存储桶名称只能包含小写字母、数字和短横线,且必须以字母或数字开头和结尾").StatusCode(400);
|
||||
break;
|
||||
|
||||
case "MINIO":
|
||||
// Minio存储桶命名规则
|
||||
if (bucketName.Length < 3 || bucketName.Length > 63)
|
||||
throw Oops.Oh("Minio存储桶名称长度必须在3-63字符之间").StatusCode(400);
|
||||
|
||||
if (!Regex.IsMatch(bucketName, @"^[a-z0-9][a-z0-9\-\.]*[a-z0-9]$"))
|
||||
throw Oops.Oh("Minio存储桶名称只能包含小写字母、数字、短横线和点,且必须以字母或数字开头和结尾").StatusCode(400);
|
||||
break;
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理默认提供者逻辑
|
||||
/// </summary>
|
||||
/// <param name="provider">存储提供者配置</param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
private async Task HandleDefaultProviderLogic(SysFileProvider provider)
|
||||
{
|
||||
// 如果设置为默认提供者
|
||||
if (provider.IsDefault == true)
|
||||
{
|
||||
// 确保只有一个默认提供者,将其他提供者的默认标识设为false
|
||||
await _sysFileProviderRep.AsUpdateable()
|
||||
.SetColumns(p => p.IsDefault == false)
|
||||
.Where(p => p.IsDefault == true && p.Id != provider.Id)
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
else
|
||||
// 如果没有设置IsDefault值,默认为false
|
||||
{
|
||||
provider.IsDefault ??= false;
|
||||
}
|
||||
|
||||
// 检查是否还有其他默认提供者,如果没有且当前提供者启用,则设为默认
|
||||
var hasDefaultProvider = await _sysFileProviderRep.AsQueryable()
|
||||
.Where(p => p.IsDefault == true && p.IsEnable == true && p.Id != provider.Id)
|
||||
.AnyAsync();
|
||||
|
||||
if (!hasDefaultProvider && provider.IsEnable == true && provider.IsDefault != true)
|
||||
{
|
||||
// 如果没有其他默认提供者且当前提供者启用,则设为默认
|
||||
provider.IsDefault = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using Aliyun.OSS.Util;
|
||||
using Furion.AspNetCore;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统文件服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 410, Description = "系统文件")]
|
||||
public class SysFileService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly UserManager _userManager;
|
||||
private readonly SqlSugarRepository<SysFile> _sysFileRep;
|
||||
private readonly OSSProviderOptions _OSSProviderOptions;
|
||||
private readonly UploadOptions _uploadOptions;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly string _imageType = ".jpeg.jpg.png.bmp.gif.tif";
|
||||
private readonly INamedServiceProvider<ICustomFileProvider> _namedServiceProvider;
|
||||
private readonly ICustomFileProvider _customFileProvider;
|
||||
|
||||
public SysFileService(UserManager userManager,
|
||||
SqlSugarRepository<SysFile> sysFileRep,
|
||||
IOptions<OSSProviderOptions> oSSProviderOptions,
|
||||
IOptions<UploadOptions> uploadOptions,
|
||||
INamedServiceProvider<ICustomFileProvider> namedServiceProvider,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
_namedServiceProvider = namedServiceProvider;
|
||||
_userManager = userManager;
|
||||
_sysFileRep = sysFileRep;
|
||||
_OSSProviderOptions = oSSProviderOptions.Value;
|
||||
_uploadOptions = uploadOptions.Value;
|
||||
_configuration = configuration;
|
||||
|
||||
// 简化提供者选择逻辑
|
||||
if (_OSSProviderOptions.Enabled || _configuration["MultiOSS:Enabled"].ToBoolean())
|
||||
{
|
||||
// 统一使用MultiOSSFileProvider处理所有OSS情况
|
||||
_customFileProvider = _namedServiceProvider.GetService<ITransient>(nameof(MultiOSSFileProvider));
|
||||
}
|
||||
else if (_configuration["SSHProvider:Enabled"].ToBoolean())
|
||||
{
|
||||
_customFileProvider = _namedServiceProvider.GetService<ITransient>(nameof(SSHFileProvider));
|
||||
}
|
||||
else
|
||||
{
|
||||
_customFileProvider = _namedServiceProvider.GetService<ITransient>(nameof(DefaultFileProvider));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件分页列表 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取文件分页列表")]
|
||||
public async Task<SqlSugarPagedList<SysFile>> Page(PageFileInput input)
|
||||
{
|
||||
// 获取所有公开附件
|
||||
var publicList = _sysFileRep.AsQueryable().ClearFilter().Where(u => u.IsPublic == true);
|
||||
// 获取私有附件
|
||||
var privateList = _sysFileRep.AsQueryable().Where(u => u.IsPublic == false);
|
||||
// 合并公开和私有附件并分页
|
||||
return await _sysFileRep.Context.UnionAll(publicList, privateList)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.FileName), u => u.FileName.Contains(input.FileName.Trim()))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.FilePath), u => u.FilePath.Contains(input.FilePath.Trim()))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()) && !string.IsNullOrWhiteSpace(input.EndTime.ToString()),
|
||||
u => u.CreateTime >= input.StartTime && u.CreateTime <= input.EndTime)
|
||||
.OrderBy(u => u.CreateTime, OrderByType.Desc)
|
||||
.ToPagedListAsync(input.Page, input.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件Base64 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("上传文件Base64")]
|
||||
public async Task<SysFile> UploadFileFromBase64(UploadFileFromBase64Input input)
|
||||
{
|
||||
var pattern = @"data:(?<type>.+?);base64,(?<data>[^""]+)";
|
||||
var regex = new Regex(pattern, RegexOptions.Compiled);
|
||||
var match = regex.Match(input.FileDataBase64);
|
||||
|
||||
byte[] fileData = Convert.FromBase64String(match.Groups["data"].Value);
|
||||
var contentType = match.Groups["type"].Value;
|
||||
if (string.IsNullOrEmpty(input.FileName))
|
||||
input.FileName = $"{YitIdHelper.NextId()}.{contentType.AsSpan(contentType.LastIndexOf('/') + 1)}";
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
ms.Write(fileData);
|
||||
ms.Seek(0, SeekOrigin.Begin);
|
||||
IFormFile formFile = new FormFile(ms, 0, fileData.Length, "file", input.FileName)
|
||||
{
|
||||
Headers = new HeaderDictionary(),
|
||||
ContentType = contentType
|
||||
};
|
||||
var uploadFileInput = input.Adapt<UploadFileInput>();
|
||||
uploadFileInput.File = formFile;
|
||||
return await UploadFile(uploadFileInput);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传多文件 🔖
|
||||
/// </summary>
|
||||
/// <param name="files"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("上传多文件")]
|
||||
public async Task<List<SysFile>> UploadFiles([Required] List<IFormFile> files)
|
||||
{
|
||||
var fileList = new List<SysFile>();
|
||||
foreach (var file in files)
|
||||
{
|
||||
var uploadedFile = await UploadFile(new UploadFileInput { File = file });
|
||||
fileList.Add(uploadedFile);
|
||||
}
|
||||
return fileList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据文件Id或Url下载 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("根据文件Id或Url下载")]
|
||||
public async Task<IActionResult> DownloadFile(SysFile input)
|
||||
{
|
||||
var file = input.Id > 0 ? await GetFile(input.Id) : await _sysFileRep.CopyNew().GetFirstAsync(u => u.Url == input.Url);
|
||||
var fileName = HttpUtility.UrlEncode(file.FileName, Encoding.GetEncoding("UTF-8"));
|
||||
return await GetFileStreamResult(file, fileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 文件预览 🔖
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("文件预览")]
|
||||
public async Task<IActionResult> GetPreview([FromRoute] long id)
|
||||
{
|
||||
var file = await GetFile(id);
|
||||
//var fileName = HttpUtility.UrlEncode(file.FileName, Encoding.GetEncoding("UTF-8"));
|
||||
return await GetFileStreamResult(file, file.Id + "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件流
|
||||
/// </summary>
|
||||
/// <param name="file"></param>
|
||||
/// <param name="fileName"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<IActionResult> GetFileStreamResult(SysFile file, string fileName)
|
||||
{
|
||||
return await _customFileProvider.GetFileStreamResultAsync(file, fileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件流
|
||||
/// </summary>
|
||||
[NonAction]
|
||||
public async Task<Stream> GetFileStream(SysFile file)
|
||||
{
|
||||
var fileName = HttpUtility.UrlEncode(file.FileName, Encoding.GetEncoding("UTF-8"));
|
||||
var result = await _customFileProvider.GetFileStreamResultAsync(file, fileName);
|
||||
return result.FileStream;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载指定文件Base64格式 🔖
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("下载指定文件Base64格式")]
|
||||
public async Task<string> DownloadFileBase64([FromBody] string url)
|
||||
{
|
||||
var sysFile = await _sysFileRep.CopyNew().GetFirstAsync(u => u.Url == url) ?? throw Oops.Oh($"文件不存在");
|
||||
return await _customFileProvider.DownloadFileBase64Async(sysFile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除文件 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
|
||||
[DisplayName("删除文件")]
|
||||
public async Task DeleteFile(BaseIdInput input)
|
||||
{
|
||||
var file = await _sysFileRep.GetByIdAsync(input.Id) ?? throw Oops.Oh($"文件不存在");
|
||||
await _sysFileRep.DeleteAsync(file);
|
||||
await _customFileProvider.DeleteFileAsync(file);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新文件 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Update"), HttpPost]
|
||||
[DisplayName("更新文件")]
|
||||
public async Task UpdateFile(SysFile input)
|
||||
{
|
||||
var isExist = await _sysFileRep.IsAnyAsync(u => u.Id == input.Id);
|
||||
if (!isExist) throw Oops.Oh(ErrorCodeEnum.D8000);
|
||||
|
||||
await _sysFileRep.UpdateAsync(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件 🔖
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取文件")]
|
||||
public async Task<SysFile> GetFile([FromQuery] long id)
|
||||
{
|
||||
var file = await _sysFileRep.CopyNew().GetByIdAsync(id);
|
||||
return file ?? throw Oops.Oh(ErrorCodeEnum.D8000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据文件Id集合获取文件 🔖
|
||||
/// </summary>
|
||||
/// <param name="ids"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("根据文件Id集合获取文件")]
|
||||
public async Task<List<SysFile>> GetFileByIds([FromQuery][FlexibleArray<long>] List<long> ids)
|
||||
{
|
||||
return await _sysFileRep.AsQueryable().Where(u => ids.Contains(u.Id)).ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件路径 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取文件路径")]
|
||||
public async Task<List<TreeNode>> GetFolder()
|
||||
{
|
||||
// 优化:直接在数据库层面获取不重复的文件路径
|
||||
var folders = await _sysFileRep.AsQueryable()
|
||||
.Select(u => u.FilePath)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
var pathTreeBuilder = new PathTreeBuilder();
|
||||
var tree = pathTreeBuilder.BuildTree(folders);
|
||||
return tree.Children;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="targetPath">存储目标路径</param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("上传文件")]
|
||||
public async Task<SysFile> UploadFile([FromForm] UploadFileInput input, [BindNever] string targetPath = "")
|
||||
{
|
||||
if (input.File == null || input.File.Length <= 0) throw Oops.Oh(ErrorCodeEnum.D8000);
|
||||
|
||||
if (input.File.FileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) throw Oops.Oh(ErrorCodeEnum.D8005);
|
||||
|
||||
// 判断是否重复上传的文件
|
||||
var sizeKb = input.File.Length / 1024; // 大小KB
|
||||
var fileMd5 = string.Empty;
|
||||
if (_uploadOptions.EnableMd5)
|
||||
{
|
||||
await using (var fileStream = input.File.OpenReadStream())
|
||||
{
|
||||
fileMd5 = OssUtils.ComputeContentMd5(fileStream, fileStream.Length);
|
||||
}
|
||||
// Mysql8 中如果使用了 utf8mb4_general_ci 之外的编码会出错,尽量避免在条件里使用.ToString()
|
||||
// 因为 Squsugar 并不是把变量转换为字符串来构造SQL语句,而是构造了CAST(123 AS CHAR)这样的语句,这样这个返回值是utf8mb4_general_ci,所以容易出错。
|
||||
var sysFile = await _sysFileRep.GetFirstAsync(u => u.FileMd5 == fileMd5 && u.SizeKb == sizeKb);
|
||||
if (sysFile != null) return sysFile;
|
||||
}
|
||||
|
||||
// 验证文件类型
|
||||
if (!_uploadOptions.ContentType.Contains(input.File.ContentType)) throw Oops.Oh($"{ErrorCodeEnum.D8001}:{input.File.ContentType}");
|
||||
|
||||
// 验证文件大小
|
||||
if (sizeKb > _uploadOptions.MaxSize) throw Oops.Oh($"{ErrorCodeEnum.D8002},允许最大:{_uploadOptions.MaxSize}KB");
|
||||
|
||||
// 获取文件后缀
|
||||
var suffix = Path.GetExtension(input.File.FileName).ToLower(); // 后缀
|
||||
if (string.IsNullOrWhiteSpace(suffix))
|
||||
suffix = string.Concat(".", input.File.ContentType.AsSpan(input.File.ContentType.LastIndexOf('/') + 1));
|
||||
if (!string.IsNullOrWhiteSpace(suffix))
|
||||
{
|
||||
//var contentTypeProvider = FS.GetFileExtensionContentTypeProvider();
|
||||
//suffix = contentTypeProvider.Mappings.FirstOrDefault(u => u.Value == file.ContentType).Key;
|
||||
// 修改 image/jpeg 类型返回的 .jpeg、jpe 后缀
|
||||
if (suffix == ".jpeg" || suffix == ".jpe")
|
||||
suffix = ".jpg";
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(suffix)) throw Oops.Oh(ErrorCodeEnum.D8003);
|
||||
|
||||
// 防止客户端伪造文件类型
|
||||
if (!string.IsNullOrWhiteSpace(input.AllowSuffix) && !input.AllowSuffix.Contains(suffix)) throw Oops.Oh(ErrorCodeEnum.D8003);
|
||||
//if (!VerifyFileExtensionName.IsSameType(file.OpenReadStream(), suffix)) throw Oops.Oh(ErrorCodeEnum.D8001);
|
||||
|
||||
// 文件存储位置
|
||||
var path = string.IsNullOrWhiteSpace(targetPath) ? _uploadOptions.Path : targetPath;
|
||||
path = path.ParseToDateTimeForRep();
|
||||
|
||||
var newFile = input.Adapt<SysFile>();
|
||||
newFile.Id = YitIdHelper.NextId();
|
||||
|
||||
// 优先使用用户指定的存储桶名称,如果没有指定则使用默认配置
|
||||
if (!string.IsNullOrEmpty(input.BucketName))
|
||||
{
|
||||
newFile.BucketName = input.BucketName;
|
||||
}
|
||||
else
|
||||
{
|
||||
// MultiOSSFileProvider会自动使用默认配置
|
||||
newFile.BucketName = _OSSProviderOptions.Enabled ? _OSSProviderOptions.Bucket : "Local";
|
||||
}
|
||||
|
||||
newFile.FileName = Path.GetFileNameWithoutExtension(input.File.FileName);
|
||||
newFile.Suffix = suffix;
|
||||
newFile.SizeKb = sizeKb;
|
||||
newFile.FilePath = path;
|
||||
newFile.FileMd5 = fileMd5;
|
||||
newFile.DataId = input.DataId;
|
||||
|
||||
var finalName = newFile.Id + suffix; // 文件最终名称
|
||||
|
||||
newFile = await _customFileProvider.UploadFileAsync(input.File, newFile, path, finalName);
|
||||
await _sysFileRep.AsInsertable(newFile).ExecuteCommandAsync();
|
||||
return newFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传头像 🔖
|
||||
/// </summary>
|
||||
/// <param name="file"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("上传头像")]
|
||||
public async Task<SysFile> UploadAvatar([Required] IFormFile file)
|
||||
{
|
||||
var sysFile = await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType }, "upload/avatar");
|
||||
|
||||
var sysUserRep = _sysFileRep.ChangeRepository<SqlSugarRepository<SysUser>>();
|
||||
var user = await sysUserRep.GetByIdAsync(_userManager.UserId);
|
||||
await sysUserRep.UpdateAsync(u => new SysUser() { Avatar = sysFile.Url }, u => u.Id == user.Id);
|
||||
// 删除已有头像文件
|
||||
if (!string.IsNullOrWhiteSpace(user.Avatar))
|
||||
{
|
||||
var fileId = Path.GetFileNameWithoutExtension(user.Avatar);
|
||||
if (long.TryParse(fileId, out var id))
|
||||
{
|
||||
try
|
||||
{
|
||||
await DeleteFile(new BaseIdInput { Id = id });
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略删除旧头像文件的错误,不影响新头像上传
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sysFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传电子签名 🔖
|
||||
/// </summary>
|
||||
/// <param name="file"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("上传电子签名")]
|
||||
public async Task<SysFile> UploadSignature([Required] IFormFile file)
|
||||
{
|
||||
var sysFile = await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType }, "upload/signature");
|
||||
|
||||
var sysUserRep = _sysFileRep.ChangeRepository<SqlSugarRepository<SysUser>>();
|
||||
var user = await sysUserRep.GetByIdAsync(_userManager.UserId);
|
||||
// 删除已有电子签名文件
|
||||
if (!string.IsNullOrWhiteSpace(user.Signature) && user.Signature.EndsWith(".png"))
|
||||
{
|
||||
var fileId = Path.GetFileNameWithoutExtension(user.Signature);
|
||||
if (long.TryParse(fileId, out var id))
|
||||
{
|
||||
try
|
||||
{
|
||||
await DeleteFile(new BaseIdInput { Id = id });
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略删除旧签名文件的错误,不影响新签名上传
|
||||
}
|
||||
}
|
||||
}
|
||||
await sysUserRep.UpdateAsync(u => new SysUser() { Signature = sysFile.Url }, u => u.Id == user.Id);
|
||||
return sysFile;
|
||||
}
|
||||
|
||||
#region 统一实体与文件关联时,业务应用实体只需要定义一个SysFile集合导航属性,业务增加和更新、删除分别调用即可
|
||||
|
||||
/// <summary>
|
||||
/// 更新文件的业务数据Id
|
||||
/// </summary>
|
||||
/// <param name="dataId"></param>
|
||||
/// <param name="sysFiles"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task UpdateFileByDataId(long dataId, List<SysFile> sysFiles)
|
||||
{
|
||||
var newFileIds = sysFiles.Select(u => u.Id).ToList();
|
||||
|
||||
// 求文件Id差集并删除(无效文件)
|
||||
var tmpFiles = await _sysFileRep.GetListAsync(u => u.DataId == dataId);
|
||||
var tmpFileIds = tmpFiles.Select(u => u.Id).ToList();
|
||||
var deleteFileIds = tmpFileIds.Except(newFileIds);
|
||||
foreach (var fileId in deleteFileIds)
|
||||
await DeleteFile(new BaseIdInput() { Id = fileId });
|
||||
|
||||
await _sysFileRep.UpdateAsync(u => new SysFile() { DataId = dataId }, u => newFileIds.Contains(u.Id));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除业务数据对应的文件
|
||||
/// </summary>
|
||||
/// <param name="dataId"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task DeleteFileByDataId(long dataId)
|
||||
{
|
||||
// 删除冗余无效的物理文件
|
||||
var tmpFiles = await _sysFileRep.GetListAsync(u => u.DataId == dataId);
|
||||
foreach (var file in tmpFiles)
|
||||
await _customFileProvider.DeleteFileAsync(file);
|
||||
await _sysFileRep.AsDeleteable().Where(u => u.DataId == dataId).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
#endregion 统一实体与文件关联时,业务应用实体只需要定义一个SysFile集合导航属性,业务增加和更新、删除分别调用即可
|
||||
}
|
||||
Reference in New Issue
Block a user