feat: initialize Evergreen Health backend

This commit is contained in:
fuenmao
2026-09-21 14:08:22 +08:00
commit 8553df5dd2
493 changed files with 75649 additions and 0 deletions
@@ -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集合导航属性,业务增加和更新、删除分别调用即可
}