From 04456efd1fc88a13f2f14c1dc79a882ab441f10b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=A4=E4=BF=8A=E9=A3=9E?= Date: Thu, 28 Aug 2025 04:13:35 +0000 Subject: [PATCH] =?UTF-8?q?update=20Admin.NET/Admin.NET.Core/Service/File/?= =?UTF-8?q?SysFileService.cs.=20=E6=95=B0=E6=8D=AE=E6=9D=83=E9=99=90?= =?UTF-8?q?=E4=B8=BA=E6=9C=AC=E4=BA=BA=E7=9A=84=E6=83=85=E5=86=B5=E4=B8=8B?= =?UTF-8?q?=EF=BC=8Cvar=20user=20=3D=20await=20sysUserRep.GetByIdAsync(=5F?= =?UTF-8?q?userManager.UserId);=20=E8=8E=B7=E5=8F=96=E5=88=B0=E7=9A=84user?= =?UTF-8?q?=20=E4=B8=BAnull=20=EF=BC=8C=E6=98=AF=E5=9B=A0=E4=B8=BAEntityBa?= =?UTF-8?q?se=20=E4=B8=AD=E7=9A=84=20CreateUserId=E7=9A=84=E7=89=B9?= =?UTF-8?q?=E6=80=A7[OwnerUser]=20=E5=AF=BC=E8=87=B4=E7=94=9F=E6=88=90?= =?UTF-8?q?=E7=9A=84sql=20=E5=B8=A6=E4=B8=8A=E4=BA=86CreateUserId=3D?= =?UTF-8?q?=E5=BD=93=E5=89=8D=E7=94=A8=E6=88=B7Id.=20=E5=85=B6=E4=BB=96?= =?UTF-8?q?=E9=83=A8=E5=88=86=E4=B9=9F=E6=9C=89=E7=B1=BB=E4=BC=BC=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=8C=E6=88=91=E5=8F=AA=E4=BF=AE=E6=94=B9=E4=BA=86?= =?UTF-8?q?=E8=BF=99=E4=B8=80=E5=A4=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 汤俊飞 --- .../Service/File/SysFileService.cs | 775 +++++++++--------- 1 file changed, 388 insertions(+), 387 deletions(-) diff --git a/Admin.NET/Admin.NET.Core/Service/File/SysFileService.cs b/Admin.NET/Admin.NET.Core/Service/File/SysFileService.cs index 9b1bf230b..dbb8196b7 100644 --- a/Admin.NET/Admin.NET.Core/Service/File/SysFileService.cs +++ b/Admin.NET/Admin.NET.Core/Service/File/SysFileService.cs @@ -1,388 +1,389 @@ -// 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; - -/// -/// 系统文件服务 🧩 -/// -[ApiDescriptionSettings(Order = 410, Description = "系统文件")] -public class SysFileService : IDynamicApiController, ITransient -{ - private readonly UserManager _userManager; - private readonly SqlSugarRepository _sysFileRep; - private readonly OSSProviderOptions _OSSProviderOptions; - private readonly UploadOptions _uploadOptions; - private readonly string _imageType = ".jpeg.jpg.png.bmp.gif.tif"; - private readonly INamedServiceProvider _namedServiceProvider; - private readonly ICustomFileProvider _customFileProvider; - - public SysFileService(UserManager userManager, - SqlSugarRepository sysFileRep, - IOptions oSSProviderOptions, - IOptions uploadOptions, INamedServiceProvider namedServiceProvider) - { - _namedServiceProvider = namedServiceProvider; - _userManager = userManager; - _sysFileRep = sysFileRep; - _OSSProviderOptions = oSSProviderOptions.Value; - _uploadOptions = uploadOptions.Value; - if (_OSSProviderOptions.Enabled) - { - _customFileProvider = _namedServiceProvider.GetService(nameof(OSSFileProvider)); - } - else if (App.Configuration["SSHProvider:Enabled"].ToBoolean()) - { - _customFileProvider = _namedServiceProvider.GetService(nameof(SSHFileProvider)); - } - else - { - _customFileProvider = _namedServiceProvider.GetService(nameof(DefaultFileProvider)); - } - } - - /// - /// 获取文件分页列表 🔖 - /// - /// - /// - [DisplayName("获取文件分页列表")] - public async Task> 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); - } - - /// - /// 上传文件Base64 🔖 - /// - /// - /// - [DisplayName("上传文件Base64")] - public async Task UploadFileFromBase64(UploadFileFromBase64Input input) - { - var pattern = @"data:(?.+?);base64,(?[^""]+)"; - 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)}"; - - 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.File = formFile; - return await UploadFile(uploadFileInput); - } - - /// - /// 上传多文件 🔖 - /// - /// - /// - [DisplayName("上传多文件")] - public List UploadFiles([Required] List files) - { - var fileList = new List(); - files.ForEach(file => fileList.Add(UploadFile(new UploadFileInput { File = file }).Result)); - return fileList; - } - - /// - /// 根据文件Id或Url下载 🔖 - /// - /// - /// - [DisplayName("根据文件Id或Url下载")] - public async Task 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); - } - - /// - /// 文件预览 🔖 - /// - /// - /// - [DisplayName("文件预览")] - public async Task 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 + ""); - } - - /// - /// 获取文件流 - /// - /// - /// - /// - private async Task GetFileStreamResult(SysFile file, string fileName) - { - return await _customFileProvider.GetFileStreamResultAsync(file, fileName); - } - - /// - /// 下载指定文件Base64格式 🔖 - /// - /// - /// - [DisplayName("下载指定文件Base64格式")] - public async Task DownloadFileBase64([FromBody] string url) - { - var sysFile = await _sysFileRep.CopyNew().GetFirstAsync(u => u.Url == url) ?? throw Oops.Oh($"文件不存在"); - return await _customFileProvider.DownloadFileBase64Async(sysFile); - } - - /// - /// 删除文件 🔖 - /// - /// - /// - [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); - } - - /// - /// 更新文件 🔖 - /// - /// - /// - [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); - } - - /// - /// 获取文件 🔖 - /// - /// - /// - [DisplayName("获取文件")] - public async Task GetFile([FromQuery] long id) - { - var file = await _sysFileRep.CopyNew().GetByIdAsync(id); - return file ?? throw Oops.Oh(ErrorCodeEnum.D8000); - } - - /// - /// 根据文件Id集合获取文件 🔖 - /// - /// - /// - [DisplayName("根据文件Id集合获取文件")] - public async Task> GetFileByIds([FromQuery][FlexibleArray] List ids) - { - return await _sysFileRep.AsQueryable().Where(u => ids.Contains(u.Id)).ToListAsync(); - } - - /// - /// 获取文件路径 🔖 - /// - /// - [DisplayName("获取文件路径")] - public async Task> GetFolder() - { - var files = await _sysFileRep.AsQueryable().ToListAsync(); - var folders = files.GroupBy(u => u.FilePath).Select(u => u.First().FilePath).ToList(); - - var pathTreeBuilder = new PathTreeBuilder(); - var tree = pathTreeBuilder.BuildTree(folders); - return tree.Children; - } - - /// - /// 上传文件 🔖 - /// - /// - /// 存储目标路径 - /// - [DisplayName("上传文件")] - public async Task 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(); - newFile.Id = YitIdHelper.NextId(); - newFile.BucketName = _OSSProviderOptions.Enabled ? _OSSProviderOptions.Bucket : "Local"; // 阿里云对bucket名称有要求,1.只能包括小写字母,数字,短横线(-)2.必须以小写字母或者数字开头 3.长度必须在3-63字节之间 - newFile.FileName = Path.GetFileNameWithoutExtension(input.File.FileName); - newFile.Suffix = suffix; - newFile.SizeKb = sizeKb; - newFile.FilePath = path; - newFile.FileMd5 = fileMd5; - - var finalName = newFile.Id + suffix; // 文件最终名称 - - newFile = await _customFileProvider.UploadFileAsync(input.File, newFile, path, finalName); - await _sysFileRep.AsInsertable(newFile).ExecuteCommandAsync(); - return newFile; - } - - /// - /// 上传头像 🔖 - /// - /// - /// - [DisplayName("上传头像")] - public async Task UploadAvatar([Required] IFormFile file) - { - var sysFile = await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType }, "upload/avatar"); - - var sysUserRep = _sysFileRep.ChangeRepository>(); - 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); - await DeleteFile(new BaseIdInput { Id = long.Parse(fileId) }); - } - - return sysFile; - } - - /// - /// 上传电子签名 🔖 - /// - /// - /// - [DisplayName("上传电子签名")] - public async Task UploadSignature([Required] IFormFile file) - { - var sysFile = await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType }, "upload/signature"); - - var sysUserRep = _sysFileRep.ChangeRepository>(); - var user = await sysUserRep.GetByIdAsync(_userManager.UserId); - // 删除已有电子签名文件 - if (!string.IsNullOrWhiteSpace(user.Signature) && user.Signature.EndsWith(".png")) - { - var fileId = Path.GetFileNameWithoutExtension(user.Signature); - await DeleteFile(new BaseIdInput { Id = long.Parse(fileId) }); - } - await sysUserRep.UpdateAsync(u => new SysUser() { Signature = sysFile.Url }, u => u.Id == user.Id); - return sysFile; - } - - #region 统一实体与文件关联时,业务应用实体只需要定义一个SysFile集合导航属性,业务增加和更新、删除分别调用即可 - - /// - /// 更新文件的业务数据Id - /// - /// - /// - /// - [NonAction] - public async Task UpdateFileByDataId(long dataId, List 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)); - } - - /// - /// 删除业务数据对应的文件 - /// - /// - /// - [NonAction] - public async Task DeteleFileByDataId(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集合导航属性,业务增加和更新、删除分别调用即可 +// 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; + +/// +/// 系统文件服务 🧩 +/// +[ApiDescriptionSettings(Order = 410, Description = "系统文件")] +public class SysFileService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SqlSugarRepository _sysFileRep; + private readonly OSSProviderOptions _OSSProviderOptions; + private readonly UploadOptions _uploadOptions; + private readonly string _imageType = ".jpeg.jpg.png.bmp.gif.tif"; + private readonly INamedServiceProvider _namedServiceProvider; + private readonly ICustomFileProvider _customFileProvider; + + public SysFileService(UserManager userManager, + SqlSugarRepository sysFileRep, + IOptions oSSProviderOptions, + IOptions uploadOptions, INamedServiceProvider namedServiceProvider) + { + _namedServiceProvider = namedServiceProvider; + _userManager = userManager; + _sysFileRep = sysFileRep; + _OSSProviderOptions = oSSProviderOptions.Value; + _uploadOptions = uploadOptions.Value; + if (_OSSProviderOptions.Enabled) + { + _customFileProvider = _namedServiceProvider.GetService(nameof(OSSFileProvider)); + } + else if (App.Configuration["SSHProvider:Enabled"].ToBoolean()) + { + _customFileProvider = _namedServiceProvider.GetService(nameof(SSHFileProvider)); + } + else + { + _customFileProvider = _namedServiceProvider.GetService(nameof(DefaultFileProvider)); + } + } + + /// + /// 获取文件分页列表 🔖 + /// + /// + /// + [DisplayName("获取文件分页列表")] + public async Task> 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); + } + + /// + /// 上传文件Base64 🔖 + /// + /// + /// + [DisplayName("上传文件Base64")] + public async Task UploadFileFromBase64(UploadFileFromBase64Input input) + { + var pattern = @"data:(?.+?);base64,(?[^""]+)"; + 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)}"; + + 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.File = formFile; + return await UploadFile(uploadFileInput); + } + + /// + /// 上传多文件 🔖 + /// + /// + /// + [DisplayName("上传多文件")] + public List UploadFiles([Required] List files) + { + var fileList = new List(); + files.ForEach(file => fileList.Add(UploadFile(new UploadFileInput { File = file }).Result)); + return fileList; + } + + /// + /// 根据文件Id或Url下载 🔖 + /// + /// + /// + [DisplayName("根据文件Id或Url下载")] + public async Task 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); + } + + /// + /// 文件预览 🔖 + /// + /// + /// + [DisplayName("文件预览")] + public async Task 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 + ""); + } + + /// + /// 获取文件流 + /// + /// + /// + /// + private async Task GetFileStreamResult(SysFile file, string fileName) + { + return await _customFileProvider.GetFileStreamResultAsync(file, fileName); + } + + /// + /// 下载指定文件Base64格式 🔖 + /// + /// + /// + [DisplayName("下载指定文件Base64格式")] + public async Task DownloadFileBase64([FromBody] string url) + { + var sysFile = await _sysFileRep.CopyNew().GetFirstAsync(u => u.Url == url) ?? throw Oops.Oh($"文件不存在"); + return await _customFileProvider.DownloadFileBase64Async(sysFile); + } + + /// + /// 删除文件 🔖 + /// + /// + /// + [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); + } + + /// + /// 更新文件 🔖 + /// + /// + /// + [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); + } + + /// + /// 获取文件 🔖 + /// + /// + /// + [DisplayName("获取文件")] + public async Task GetFile([FromQuery] long id) + { + var file = await _sysFileRep.CopyNew().GetByIdAsync(id); + return file ?? throw Oops.Oh(ErrorCodeEnum.D8000); + } + + /// + /// 根据文件Id集合获取文件 🔖 + /// + /// + /// + [DisplayName("根据文件Id集合获取文件")] + public async Task> GetFileByIds([FromQuery][FlexibleArray] List ids) + { + return await _sysFileRep.AsQueryable().Where(u => ids.Contains(u.Id)).ToListAsync(); + } + + /// + /// 获取文件路径 🔖 + /// + /// + [DisplayName("获取文件路径")] + public async Task> GetFolder() + { + var files = await _sysFileRep.AsQueryable().ToListAsync(); + var folders = files.GroupBy(u => u.FilePath).Select(u => u.First().FilePath).ToList(); + + var pathTreeBuilder = new PathTreeBuilder(); + var tree = pathTreeBuilder.BuildTree(folders); + return tree.Children; + } + + /// + /// 上传文件 🔖 + /// + /// + /// 存储目标路径 + /// + [DisplayName("上传文件")] + public async Task 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(); + newFile.Id = YitIdHelper.NextId(); + newFile.BucketName = _OSSProviderOptions.Enabled ? _OSSProviderOptions.Bucket : "Local"; // 阿里云对bucket名称有要求,1.只能包括小写字母,数字,短横线(-)2.必须以小写字母或者数字开头 3.长度必须在3-63字节之间 + newFile.FileName = Path.GetFileNameWithoutExtension(input.File.FileName); + newFile.Suffix = suffix; + newFile.SizeKb = sizeKb; + newFile.FilePath = path; + newFile.FileMd5 = fileMd5; + + var finalName = newFile.Id + suffix; // 文件最终名称 + + newFile = await _customFileProvider.UploadFileAsync(input.File, newFile, path, finalName); + await _sysFileRep.AsInsertable(newFile).ExecuteCommandAsync(); + return newFile; + } + + /// + /// 上传头像 🔖 + /// + /// + /// + [DisplayName("上传头像")] + public async Task UploadAvatar([Required] IFormFile file) + { + var sysFile = await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType }, "upload/avatar"); + + var sysUserRep = _sysFileRep.ChangeRepository>(); + var user = await sysUserRep.Context.Ado.SqlQuerySingleAsync($"Select * from SysUser Where Id={_userManager.UserId}"); + + await sysUserRep.Context.Ado.ExecuteCommandAsync($"Update SysUser set Avatar='{sysFile.Url}' Where Id={_userManager.UserId}"); + // 删除已有头像文件 + if (!string.IsNullOrWhiteSpace(user.Avatar)) + { + var fileId = Path.GetFileNameWithoutExtension(user.Avatar); + await DeleteFile(new BaseIdInput { Id = long.Parse(fileId) }); + } + + return sysFile; + } + + /// + /// 上传电子签名 🔖 + /// + /// + /// + [DisplayName("上传电子签名")] + public async Task UploadSignature([Required] IFormFile file) + { + var sysFile = await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType }, "upload/signature"); + + var sysUserRep = _sysFileRep.ChangeRepository>(); + var user = await sysUserRep.GetByIdAsync(_userManager.UserId); + // 删除已有电子签名文件 + if (!string.IsNullOrWhiteSpace(user.Signature) && user.Signature.EndsWith(".png")) + { + var fileId = Path.GetFileNameWithoutExtension(user.Signature); + await DeleteFile(new BaseIdInput { Id = long.Parse(fileId) }); + } + await sysUserRep.UpdateAsync(u => new SysUser() { Signature = sysFile.Url }, u => u.Id == user.Id); + return sysFile; + } + + #region 统一实体与文件关联时,业务应用实体只需要定义一个SysFile集合导航属性,业务增加和更新、删除分别调用即可 + + /// + /// 更新文件的业务数据Id + /// + /// + /// + /// + [NonAction] + public async Task UpdateFileByDataId(long dataId, List 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)); + } + + /// + /// 删除业务数据对应的文件 + /// + /// + /// + [NonAction] + public async Task DeteleFileByDataId(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集合导航属性,业务增加和更新、删除分别调用即可 } \ No newline at end of file -- Gitee