From 7f269c8b04168af4371945aa593dcaadb34d47e9 Mon Sep 17 00:00:00 2001 From: lq1405 <2769838458@qq.com> Date: Wed, 18 Jun 2025 17:19:29 +0800 Subject: [PATCH] =?UTF-8?q?v=201.1.4=20=E6=96=B0=E5=A2=9E=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=B8=8A=E4=BC=A0=E4=B8=AD=E8=BD=AC=20=E4=B8=83?= =?UTF-8?q?=E7=89=9B=E4=BA=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LMS.DAO/ApplicationDbContext.cs | 12 + LMS.DAO/OptionDAO/OptionGlobalDAO.cs | 67 +++ LMS.Repository/DB/FileUploads.cs | 47 ++ LMS.Repository/DTO/FileUploadDto.cs | 39 ++ .../FileUpload/FileRequestReturn.cs | 42 ++ LMS.Repository/FileUpload/QiniuSettings.cs | 19 + LMS.Tools/ImageTool/ImageTypeDetector.cs | 66 +++ .../Configuration/ServiceConfiguration.cs | 4 + .../Controllers/FileUploadController.cs | 87 ++++ LMS.service/LMS.service.csproj | 1 + LMS.service/Program.cs | 11 + .../FileUploadService/IQiniuUploadService.cs | 19 + .../FileUploadService/QiniuUploadService.cs | 454 ++++++++++++++++++ LMS.service/appsettings.json | 12 +- SQL/v1.1.3/FileUploads.sql | 60 ++- 15 files changed, 921 insertions(+), 19 deletions(-) create mode 100644 LMS.DAO/OptionDAO/OptionGlobalDAO.cs create mode 100644 LMS.Repository/DB/FileUploads.cs create mode 100644 LMS.Repository/DTO/FileUploadDto.cs create mode 100644 LMS.Repository/FileUpload/FileRequestReturn.cs create mode 100644 LMS.Repository/FileUpload/QiniuSettings.cs create mode 100644 LMS.Tools/ImageTool/ImageTypeDetector.cs create mode 100644 LMS.service/Controllers/FileUploadController.cs create mode 100644 LMS.service/Service/FileUploadService/IQiniuUploadService.cs create mode 100644 LMS.service/Service/FileUploadService/QiniuUploadService.cs diff --git a/LMS.DAO/ApplicationDbContext.cs b/LMS.DAO/ApplicationDbContext.cs index 7698f77..d9efb04 100644 --- a/LMS.DAO/ApplicationDbContext.cs +++ b/LMS.DAO/ApplicationDbContext.cs @@ -53,6 +53,8 @@ namespace LMS.DAO public DbSet MJApiTasks { get; set; } + public DbSet FileUploads { get; set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -112,6 +114,16 @@ namespace LMS.DAO .HasForeignKey(e => e.TokenId) .OnDelete(DeleteBehavior.Cascade); }); + + modelBuilder.Entity(entity => + { + entity.HasIndex(e => e.UserId); + entity.HasIndex(e => e.FileKey); + entity.HasIndex(e => e.UploadTime); + + entity.Property(e => e.CreatedAt).HasDefaultValueSql("GETDATE()"); + entity.Property(e => e.UploadTime).HasDefaultValueSql("GETDATE()"); + }); } } } diff --git a/LMS.DAO/OptionDAO/OptionGlobalDAO.cs b/LMS.DAO/OptionDAO/OptionGlobalDAO.cs new file mode 100644 index 0000000..17a3b3c --- /dev/null +++ b/LMS.DAO/OptionDAO/OptionGlobalDAO.cs @@ -0,0 +1,67 @@ +using LMS.Repository.DB; +using Microsoft.EntityFrameworkCore; + +namespace LMS.DAO.OptionDAO +{ + public class OptionGlobalDAO(ApplicationDbContext dbContext) + { + private readonly ApplicationDbContext _dbContext = dbContext; + /// + /// 根据配置键查找并返回指定类型的配置值 + /// + /// 返回值类型 + /// 配置键 + /// 配置值,如果不存在或转换失败则返回默认值 + public async Task FindAndReturnOption(string optionKey) + { + // 参数验证 + if (string.IsNullOrWhiteSpace(optionKey)) + { + return default(T); + } + + var options = await _dbContext.Options + .Where(x => x.Key == optionKey) + .FirstOrDefaultAsync(); + + if (options == null) return default; + // 直接返回转换结果,GetValueObject内部应该处理null情况 + return options.GetValueObject() ?? default; + } + + /// + /// 检查配置是否存在 + /// + /// 配置键 + /// 是否存在 + public async Task OptionExists(string optionKey) + { + if (string.IsNullOrWhiteSpace(optionKey)) + { + return false; + } + + return await _dbContext.Options + .AnyAsync(x => x.Key == optionKey); + } + + /// + /// 获取多个配置 + /// + /// 配置键列表 + /// 配置字典 + public async Task> GetMultipleOptions(params string[] optionKeys) + { + if (optionKeys == null || optionKeys.Length == 0) + { + return new Dictionary(); + } + + var options = await _dbContext.Options + .Where(x => optionKeys.Contains(x.Key)) + .ToListAsync(); + + return options.ToDictionary(x => x.Key, x => x); + } + } +} diff --git a/LMS.Repository/DB/FileUploads.cs b/LMS.Repository/DB/FileUploads.cs new file mode 100644 index 0000000..081e785 --- /dev/null +++ b/LMS.Repository/DB/FileUploads.cs @@ -0,0 +1,47 @@ +using System.ComponentModel.DataAnnotations; + +namespace LMS.Repository.DB; + +public class FileUploads +{ + [Key] + public long Id { get; set; } + + [Required] + [StringLength(50)] + public long UserId { get; set; } + + [Required] + [StringLength(255)] + public string FileName { get; set; } + + [Required] + [StringLength(500)] + public string FileKey { get; set; } + + public long FileSize { get; set; } + + [Required] + [StringLength(100)] + public string ContentType { get; set; } + + [Required] + [StringLength(100)] + public string Hash { get; set; } + + [Required] + [StringLength(1000)] + public string QiniuUrl { get; set; } + + public DateTime UploadTime { get; set; } + + [StringLength(20)] + public string Status { get; set; } = "active"; + + public DateTime CreatedAt { get; set; } + + /// + /// 删除时间 ,默认为最大值,表示未删除 + /// + public DateTime DeleteTime { get; set; } = DateTime.MaxValue; +} diff --git a/LMS.Repository/DTO/FileUploadDto.cs b/LMS.Repository/DTO/FileUploadDto.cs new file mode 100644 index 0000000..c6b3350 --- /dev/null +++ b/LMS.Repository/DTO/FileUploadDto.cs @@ -0,0 +1,39 @@ +using LMS.Repository.DB; +using System.ComponentModel.DataAnnotations; + +namespace LMS.Repository.DTO +{ + public class FileUploadDto + { + public class ByteUploadRequest + { + //public required string FileBytes { get; set; } + /// + /// 文件的base64 + /// + public required string File { get; set; } + public required string FileName { get; set; } + public required string ContentType { get; set; } + public Dictionary Metadata { get; set; } = new(); + } + + public class UploadResult + { + public bool Success { get; set; } + public string Message { get; set; } + public string Url { get; set; } + public string FileKey { get; set; } + public string Hash { get; set; } + public long FileId { get; set; } + public long FileSize { get; set; } + } + + public class FileListResponse + { + public List Files { get; set; } + public int TotalCount { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + } + } +} diff --git a/LMS.Repository/FileUpload/FileRequestReturn.cs b/LMS.Repository/FileUpload/FileRequestReturn.cs new file mode 100644 index 0000000..8bd1fb4 --- /dev/null +++ b/LMS.Repository/FileUpload/FileRequestReturn.cs @@ -0,0 +1,42 @@ +namespace LMS.Repository.FileUpload +{ + public class FileRequestReturn + { + public class FileMachineRequestReturn + { + public string MachineId { get; set; } + public string FileName { get; set; } + public long FileSize { get; set; } + public string ContentType { get; set; } + + public string Hash { get; set; } + + public string Url { get; set; } + + public DateTime UploadTime { get; set; } + + public DateTime CreatedAt { get; set; } + /// + /// 删除时间 ,默认为最大值,表示未不删除 + /// + public DateTime DeleteTime { get; set; } = DateTime.MaxValue; + } + + public class FileUserRequestReturn + { + public long Id { get; set; } + public long UserId { get; set; } + public string FileName { get; set; } + public long FileSize { get; set; } + public string ContentType { get; set; } + public string Hash { get; set; } + public string Url { get; set; } + public DateTime UploadTime { get; set; } + public DateTime CreatedAt { get; set; } + /// + /// 删除时间 ,默认为最大值,表示未不删除 + /// + public DateTime DeleteTime { get; set; } = DateTime.MaxValue; + } + } +} diff --git a/LMS.Repository/FileUpload/QiniuSettings.cs b/LMS.Repository/FileUpload/QiniuSettings.cs new file mode 100644 index 0000000..43a6211 --- /dev/null +++ b/LMS.Repository/FileUpload/QiniuSettings.cs @@ -0,0 +1,19 @@ +namespace LMS.Repository.FileUpload; +public class QiniuSettings +{ + public string AccessKey { get; set; } + public string SecretKey { get; set; } + public string BucketName { get; set; } + public string Domain { get; set; } + + /// + /// 删除时间 天数 没有值 则不删除 + /// + public int? DeleteDay { get; set; } +} + +public class FileUploadSettings +{ + public long MaxFileSize { get; set; } = 3 * 1024 * 1024; // 5MB + public List AllowedContentTypes { get; set; } = new(); +} diff --git a/LMS.Tools/ImageTool/ImageTypeDetector.cs b/LMS.Tools/ImageTool/ImageTypeDetector.cs new file mode 100644 index 0000000..674eaf7 --- /dev/null +++ b/LMS.Tools/ImageTool/ImageTypeDetector.cs @@ -0,0 +1,66 @@ +namespace LMS.Tools.ImageTool +{ + public static class ImageTypeDetector + { + private static readonly Dictionary ImageSignatures = new() + { + { "image/jpeg", new byte[] { 0xFF, 0xD8, 0xFF } }, + { "image/png", new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } }, + { "image/gif", new byte[] { 0x47, 0x49, 0x46, 0x38 } }, // GIF8 + { "image/bmp", new byte[] { 0x42, 0x4D } }, // BM + { "image/webp", new byte[] { 0x52, 0x49, 0x46, 0x46 } } // RIFF (需要额外检查) + }; + + /// + /// 检查是否为支持的图片格式 + /// + /// 文件字节数组 + /// 是否为图片 + public static bool IsValidImage(byte[] fileBytes) + { + if (fileBytes == null || fileBytes.Length < 8) + return false; + + // 检查 JPEG + if (StartsWithBytes(fileBytes, ImageSignatures["image/jpeg"])) + return true; + + // 检查 PNG + if (StartsWithBytes(fileBytes, ImageSignatures["image/png"])) + return true; + + // 检查 GIF + if (StartsWithBytes(fileBytes, ImageSignatures["image/gif"])) + return true; + + // 检查 BMP + if (StartsWithBytes(fileBytes, ImageSignatures["image/bmp"])) + return true; + + // 检查 WEBP (RIFF + WEBP标识) + if (StartsWithBytes(fileBytes, ImageSignatures["image/webp"]) && + fileBytes.Length >= 12 && + fileBytes[8] == 0x57 && fileBytes[9] == 0x45 && + fileBytes[10] == 0x42 && fileBytes[11] == 0x50) // "WEBP" + { + return true; + } + + return false; + } + + private static bool StartsWithBytes(byte[] fileBytes, byte[] signature) + { + if (fileBytes.Length < signature.Length) + return false; + + for (int i = 0; i < signature.Length; i++) + { + if (fileBytes[i] != signature[i]) + return false; + } + + return true; + } + } +} diff --git a/LMS.service/Configuration/ServiceConfiguration.cs b/LMS.service/Configuration/ServiceConfiguration.cs index 44249a4..b72eb60 100644 --- a/LMS.service/Configuration/ServiceConfiguration.cs +++ b/LMS.service/Configuration/ServiceConfiguration.cs @@ -1,10 +1,12 @@ using LMS.DAO.MachineDAO; +using LMS.DAO.OptionDAO; using LMS.DAO.PermissionDAO; using LMS.DAO.RoleDAO; using LMS.DAO.UserDAO; using LMS.service.Configuration.InitConfiguration; using LMS.service.Extensions.Mail; using LMS.service.Service; +using LMS.service.Service.FileUploadService; using LMS.service.Service.MJPackage; using LMS.service.Service.Other; using LMS.service.Service.PermissionService; @@ -49,6 +51,7 @@ namespace Lai_server.Configuration services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // 注入 Extensions @@ -63,6 +66,7 @@ namespace Lai_server.Configuration services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // 注册后台服务 diff --git a/LMS.service/Controllers/FileUploadController.cs b/LMS.service/Controllers/FileUploadController.cs new file mode 100644 index 0000000..371b37b --- /dev/null +++ b/LMS.service/Controllers/FileUploadController.cs @@ -0,0 +1,87 @@ +using LMS.Common.Extensions; +using LMS.Repository.DTO; +using LMS.service.Service.FileUploadService; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; +using static LMS.Repository.DTO.FileUploadDto; +using static LMS.Repository.FileUpload.FileRequestReturn; + + +namespace LMS.service.Controllers +{ + [ApiController] + [Route("lms/[controller]/[action]")] + public class FileUploadController(IQiniuUploadService qiniuUploadService) : ControllerBase + { + private readonly IQiniuUploadService _qiniuUploadService = qiniuUploadService; + + /// + /// 通过字节数组上传文件 + /// + /// 字节上传请求 + /// + [HttpPost("{machineId}")] + public async Task>> FileUpload(string machineId, [FromBody] ByteUploadRequest request) + { + return await _qiniuUploadService.UploadBase64Async(request, machineId); + } + + /// + /// 获取用户文件列表,通过MachineId + /// + /// 页码 + /// 每页数量 + /// + [HttpGet("{machineId}")] + public async Task>>> GetFilesByMachineId(string machineId, int page = 1, int pageSize = 10) + { + return await _qiniuUploadService.GetFilesByMachineId(machineId, page, pageSize); + } + + /// + /// 获取指定ID的用户的文件列表 + /// + /// + /// + /// + /// + [HttpGet("{userId}")] + [Authorize] + public async Task>>> GetFilesByUser(long userId, [FromQuery] int page = 1, [FromQuery] int pageSize = 10) + { + long requestUserId = ConvertExtension.ObjectToLong(HttpContext.Items["UserId"] ?? 0); + return await _qiniuUploadService.GetFilesByUser(requestUserId, userId, page, pageSize); + } + + + + ///// + ///// 删除文件 + ///// + ///// 文件ID + ///// + //[HttpDelete("files/{fileId}")] + //public async Task DeleteFile(long fileId) + //{ + // try + // { + // var userId = GetCurrentUserId(); + // var success = await _qiniuUploadService.DeleteFileAsync(fileId, userId); + + // if (success) + // { + // return Ok(new { message = "删除成功" }); + // } + // else + // { + // return NotFound(new { message = "文件不存在或无权限删除" }); + // } + // } + // catch (Exception ex) + // { + // return BadRequest(new { message = $"删除失败: {ex.Message}" }); + // } + //} + } +} diff --git a/LMS.service/LMS.service.csproj b/LMS.service/LMS.service.csproj index 081d19b..71443e7 100644 --- a/LMS.service/LMS.service.csproj +++ b/LMS.service/LMS.service.csproj @@ -29,6 +29,7 @@ + diff --git a/LMS.service/Program.cs b/LMS.service/Program.cs index 16b7090..0a9b528 100644 --- a/LMS.service/Program.cs +++ b/LMS.service/Program.cs @@ -1,12 +1,14 @@ using AspNetCoreRateLimit; using Lai_server.Configuration; using LMS.DAO; +using LMS.Repository.FileUpload; using LMS.Repository.Models.DB; using LMS.service.Configuration; using LMS.service.Configuration.InitConfiguration; using LMS.service.Extensions.Middleware; using LMS.Tools.MJPackage; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Serilog; @@ -52,6 +54,15 @@ builder.Services.AddMemoryCache(); // ͨãappsettings.json builder.Services.Configure(builder.Configuration.GetSection("IpRateLimiting")); +builder.Services.Configure( + builder.Configuration.GetSection("FileUploadSettings")); + +// ģ֤ +builder.Services.Configure(options => +{ + options.SuppressModelStateInvalidFilter = false; // ȷģ֤Ч +}); + // ע͹洢 builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/LMS.service/Service/FileUploadService/IQiniuUploadService.cs b/LMS.service/Service/FileUploadService/IQiniuUploadService.cs new file mode 100644 index 0000000..13dc062 --- /dev/null +++ b/LMS.service/Service/FileUploadService/IQiniuUploadService.cs @@ -0,0 +1,19 @@ +using LMS.Repository.DB; +using LMS.Repository.DTO; +using LMS.Repository.FileUpload; +using Microsoft.AspNetCore.Mvc; +using static LMS.Repository.DTO.FileUploadDto; +using static LMS.Repository.FileUpload.FileRequestReturn; + +namespace LMS.service.Service.FileUploadService +{ + public interface IQiniuUploadService + { + Task> UploadBase64Async(ByteUploadRequest request, string machineId); + //Task> GetUserFilesAsync(string userId, int page = 1, int pageSize = 20); + //Task GetUserFilesCountAsync(string userId); + //Task DeleteFileAsync(long fileId, string userId); + Task>>> GetFilesByMachineId(string machineId, int page, int pageSize); + Task>>> GetFilesByUser(long requestUserId, long userId, int page, int pageSize); + } +} diff --git a/LMS.service/Service/FileUploadService/QiniuUploadService.cs b/LMS.service/Service/FileUploadService/QiniuUploadService.cs new file mode 100644 index 0000000..f992533 --- /dev/null +++ b/LMS.service/Service/FileUploadService/QiniuUploadService.cs @@ -0,0 +1,454 @@ +using LMS.Common.Extensions; +using LMS.DAO; +using LMS.DAO.OptionDAO; +using LMS.DAO.UserDAO; +using LMS.Repository.DB; +using LMS.Repository.DTO; +using LMS.Repository.FileUpload; +using LMS.Tools.ImageTool; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Qiniu.Http; +using Qiniu.IO; +using Qiniu.IO.Model; +using Qiniu.RS; +using Qiniu.Util; +using System.Security.Cryptography; +using static LMS.Common.Enums.ResponseCodeEnum; +using static LMS.Repository.DTO.FileUploadDto; +using static LMS.Repository.FileUpload.FileRequestReturn; +using Options = LMS.Repository.DB.Options; + +namespace LMS.service.Service.FileUploadService +{ + public class QiniuUploadService : IQiniuUploadService + { + private readonly FileUploadSettings _uploadSettings; + private readonly ApplicationDbContext _dbContext; + private readonly UploadManager _uploadManager; + private readonly ILogger _logger; + private readonly UserBasicDao _userBasicDao; + private readonly OptionGlobalDAO _optionGlobalDAO; + + public QiniuUploadService( + IOptions uploadSettings, + ILogger logger, + UserBasicDao userBasicDao, + OptionGlobalDAO optionGlobalDAO, + ApplicationDbContext dbContext) + { + _uploadSettings = uploadSettings.Value; + _logger = logger; + _dbContext = dbContext; + _optionGlobalDAO = optionGlobalDAO; + _userBasicDao = userBasicDao; ; + _uploadManager = new UploadManager(); + } + + /// + /// 通过字节数组上传文件到七牛云 + /// + /// + /// + /// + public async Task> UploadBase64Async(ByteUploadRequest request, string machineId) + { + try + { + // 将 文件的base64 字符串转换为字节数组 + var fileBytes = ConvertBase64ToBytes(request.File); + if (fileBytes == null || fileBytes.Length == 0) + { + return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError, "无效的文件数据"); + } + + // 1. 验证数据 + var validationResult = ValidateUploadRequest(request, fileBytes); + if (!validationResult.Success) + { + return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError, validationResult.Message); + } + + // 2. 获取用户 + long? userId = await GetUserIdFromMachine(machineId); + if (userId == null) + { + return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError, "无效的机器ID或未找到关联用户"); + } + + // 3. 校验当前用户是不是超出了上传限制 + var userFilesCount = await GetUserUploadToday(userId.Value); + if (userFilesCount >= 5) + { + return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError, "今日上传文件数量已达上限,请明天再试"); + } + + QiniuSettings? qiniuSettings = await _optionGlobalDAO.FindAndReturnOption("SYS_QiniuSetting"); + if (qiniuSettings == null || string.IsNullOrEmpty(qiniuSettings.AccessKey) || string.IsNullOrEmpty(qiniuSettings.SecretKey) || string.IsNullOrEmpty(qiniuSettings.BucketName) || string.IsNullOrEmpty(qiniuSettings.Domain)) + { + return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError, "配置不完整,请检查配置,请联系管理员"); + } + + Mac mac = new(qiniuSettings.AccessKey, qiniuSettings.SecretKey); + + + // 4. 生成文件key + var fileKey = GenerateFileKey(userId.Value, request.FileName); + + + // 5. 生成上传凭证 + var putPolicy = new PutPolicy + { + Scope = qiniuSettings.BucketName + }; + if (qiniuSettings.DeleteDay != null) + { + putPolicy.DeleteAfterDays = qiniuSettings.DeleteDay.Value; // 设置过期时间 + } + putPolicy.SetExpires(3600); + var token = Auth.CreateUploadToken(mac, putPolicy.ToJsonString()); + + // 6. 计算文件哈希 + var hash = ComputeSHA1Hash(fileBytes); + + // 7. 上传到七牛云 + HttpResult uploadResult; + using (var stream = new MemoryStream(fileBytes)) + { + uploadResult = await _uploadManager.UploadStreamAsync(stream, fileKey, token); + } + + // 8. 检查上传结果 + if (uploadResult.Code != 200) + { + _logger.LogError($"文件上传失败, 上传用户ID: {userId}, 错误信息: {uploadResult.Text}"); + return APIResponseModel.CreateErrorResponseModel(ResponseCode.SystemError, $"文件上传失败: {uploadResult.Text}"); + } + + // 9. 构建访问URL + var qiniuUrl = BuildFileUrl(qiniuSettings.Domain, fileKey); + + // 10. 保存到数据库 + var fileUpload = new FileUploads + { + UserId = userId.Value, + FileName = request.FileName, + FileKey = fileKey, + FileSize = fileBytes.Length, + ContentType = request.ContentType ?? "application/octet-stream", + Hash = hash, + QiniuUrl = qiniuUrl, + UploadTime = DateTime.Now, + Status = "active", + CreatedAt = DateTime.Now, + DeleteTime = qiniuSettings.DeleteDay != null ? BeijingTimeExtension.GetBeijingTime().AddDays((double)qiniuSettings.DeleteDay) : DateTime.MaxValue // 默认未删除 + }; + + _dbContext.FileUploads.Add(fileUpload); + await _dbContext.SaveChangesAsync(); + + return APIResponseModel.CreateSuccessResponseModel(new UploadResult + { + Success = true, + Message = "上传成功", + Url = qiniuUrl, + FileKey = fileKey, + Hash = hash, + FileId = fileUpload.Id, + FileSize = fileBytes.Length + }); + } + catch (Exception ex) + { + // 这里可以记录日志或处理异常 + _logger.LogError(ex, $"文件上传失败, 上传机器码: {machineId}"); + return APIResponseModel.CreateErrorResponseModel(ResponseCode.SystemError, $"上传失败: {ex.Message}"); + } + } + + /// + /// 获取对应机器码上传的图片信息 + /// + /// + /// + /// + /// + public async Task>>> GetFilesByMachineId(string machineId, int page, int pageSize) + { + try + { + // 1. 判断机器码 是不是 存在 并且获取对应的ID + long? userId = await GetUserIdFromMachine(machineId); + if (userId == null) + { + return APIResponseModel>.CreateErrorResponseModel(ResponseCode.ParameterError, "无效的机器ID或未找到关联用户"); + } + // 2. 获取用户的文件列表 + var filesList = await GetUserFilesAsync(userId.Value, page, pageSize); + + // 4. 构建返回结果 + var fileList = filesList.fileList.Select(f => new FileMachineRequestReturn + { + MachineId = machineId, + FileName = f.FileName, + FileSize = f.FileSize, + ContentType = f.ContentType, + Hash = f.Hash, + Url = f.QiniuUrl, + UploadTime = f.UploadTime, + CreatedAt = f.CreatedAt, + DeleteTime = f.DeleteTime + }).ToList(); + var response = new CollectionResponse + { + Current = page, + Total = filesList.totlaCount, + Collection = fileList + }; + return APIResponseModel>.CreateSuccessResponseModel(response); + } + catch (Exception ex) + { + // 这里可以记录日志或处理异常 + _logger.LogError(ex, $"获取文件列表失败, 机器码: {machineId}"); + return APIResponseModel>.CreateErrorResponseModel(ResponseCode.SystemError, "获取文件列表失败"); + } + } + + /// + /// 获取指定的用户的文件列表,如果是超级管理员则获取所有用户的文件列表 + /// + /// + /// + /// + /// + public async Task>>> GetFilesByUser(long requestUserId, long userId, int page, int pageSize) + { + try + { + // 1. 判断用户是不是超级管理员,不是超级管理员只能获取自己的 + bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(requestUserId); + + var fileMessage = (0, new List()); + if (isSuperAdmin) + { + // 超级管理员可以获取所有用户的文件 + fileMessage = await GetAllUserFilesAsync(page, pageSize); + } + else + { + if (requestUserId != userId) + { + return APIResponseModel>.CreateErrorResponseModel(ResponseCode.NotPermissionAction); + } + // 普通用户只能获取自己的文件 + fileMessage = await GetUserFilesAsync(requestUserId, page, pageSize); + } + // 2. 构建返回结果 + var fileList = fileMessage.Item2.Select(f => new FileUserRequestReturn + { + Id = f.Id, + UserId = requestUserId, + FileName = f.FileName, + FileSize = f.FileSize, + ContentType = f.ContentType, + Hash = f.Hash, + Url = f.QiniuUrl, + UploadTime = f.UploadTime, + CreatedAt = f.CreatedAt, + DeleteTime = f.DeleteTime + }).ToList(); + var response = new CollectionResponse + { + Current = page, + Total = fileMessage.Item1, + Collection = fileList + }; + return APIResponseModel>.CreateSuccessResponseModel(response); + } + catch (Exception ex) + { + _logger.LogError(ex, $"获取用户文件列表失败, 用户ID: {requestUserId}"); + return APIResponseModel>.CreateErrorResponseModel(ResponseCode.SystemError, "获取用户文件列表失败"); + } + } + + public async Task<(int totlaCount, List fileList)> GetUserFilesAsync(long userId, int page = 1, int pageSize = 10) + { + // 获取用户的文件总数 + int totalCount = await _dbContext.FileUploads + .Where(f => f.UserId == userId && f.Status == "active") + .CountAsync(); + // 获取用户的文件列表 + if (totalCount == 0) + { + return (0, new List()); + } + + var fileList = _dbContext.FileUploads + .Where(f => f.UserId == userId && f.Status == "active") + .OrderByDescending(f => f.UploadTime) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + return (totalCount, await fileList); + } + + public async Task<(int totleCount, List fileList)> GetAllUserFilesAsync(int page = 1, int pageSize = 10) + { + // 获取所有用户的文件总数 + int totalCount = await _dbContext.FileUploads + .Where(f => f.Status == "active") + .CountAsync(); + if (totalCount == 0) + { + return (0, new List()); + } + // 获取所有用户的文件列表 + List fileUploads = await _dbContext.FileUploads + .Where(f => f.Status == "active") + .OrderByDescending(f => f.UploadTime) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + return (totalCount, fileUploads); + } + + public async Task GetUserFilesCountAsync(long userId) + { + return await _dbContext.FileUploads + .CountAsync(f => f.UserId == userId && f.Status == "active"); + } + + private async Task GetUserUploadToday(long userId) + { + return await _dbContext.FileUploads + .CountAsync(f => f.UserId == userId && f.CreatedAt.Date == BeijingTimeExtension.GetBeijingTime().Date); + } + + private static byte[]? ConvertBase64ToBytes(string base64String) + { + if (string.IsNullOrEmpty(base64String)) + { + return null; + } + // 检查是否以 "data:" 开头并包含 base64 编码 + if (base64String.StartsWith("data:")) + { + // 提取 base64 编码部分 + var commaIndex = base64String.IndexOf(','); + if (commaIndex >= 0) + { + base64String = base64String.Substring(commaIndex + 1); + } + } + // 判断会不会出现异常 + try + { + // 尝试将 base64 字符串转换为字节数组 + return Convert.FromBase64String(base64String); + } + catch (FormatException) + { + // 如果格式不正确,返回 null + return null; + } + } + + private async Task GetUserIdFromMachine(string machineId) + { + if (string.IsNullOrWhiteSpace(machineId)) + { + return null; + } + Machine? machine = await _dbContext.Machine + .Where(m => m.MachineId == machineId && (m.DeactivationTime > BeijingTimeExtension.GetBeijingTime() || m.DeactivationTime == null)) + .FirstOrDefaultAsync(); // 改回原来的 FirstOrDefaultAsync + + if (machine != null) + { + return machine.UserID; + } + else + { + _logger.LogWarning($"未找到与机器ID {machineId} 关联的用户ID"); + return null; + } + } + + // 私有方法 + private UploadResult ValidateUploadRequest(ByteUploadRequest request, byte[] fileBytes) + { + if (fileBytes == null || fileBytes.Length == 0) + { + return new UploadResult + { + Success = false, + Message = "文件字节数据不能为空" + }; + } + + if (string.IsNullOrEmpty(request.FileName)) + { + return new UploadResult + { + Success = false, + Message = "文件名不能为空" + }; + } + + if (fileBytes.Length > _uploadSettings.MaxFileSize) + { + return new UploadResult + { + Success = false, + Message = $"文件大小不能超过 {_uploadSettings.MaxFileSize / (1024 * 1024)}MB" + }; + } + + if (_uploadSettings.AllowedContentTypes.Count != 0 && + !string.IsNullOrEmpty(request.ContentType) && + !_uploadSettings.AllowedContentTypes.Contains(request.ContentType.ToLower())) + { + return new UploadResult + { + Success = false, + Message = $"不支持的文件类型: {request.ContentType}" + }; + } + + // 检查实际的文件类型是否在允许的列表中 + // 只检查是否为图片,不是图片就拒绝 + if (!ImageTypeDetector.IsValidImage(fileBytes)) + { + return new UploadResult + { + Success = false, + Message = "只支持图片格式文件 (JPEG, PNG, GIF, BMP, WEBP)" + }; + } + + return new UploadResult { Success = true }; + } + + private static string GenerateFileKey(long userId, string fileName) + { + var date = DateTime.Now.ToString("yyyyMMdd"); + var guid = Guid.NewGuid().ToString("N"); + var extension = Path.GetExtension(fileName); + return $"diantu/user/{userId}/{date}/{guid}{extension}"; + } + + private static string ComputeSHA1Hash(byte[] data) + { + var hash = SHA1.HashData(data); + return Convert.ToHexString(hash).ToLower(); + } + + private static string BuildFileUrl(string domain, string fileKey) + { + return $"{domain}/{fileKey}"; + } + } +} diff --git a/LMS.service/appsettings.json b/LMS.service/appsettings.json index a50ea4f..0c89f3e 100644 --- a/LMS.service/appsettings.json +++ b/LMS.service/appsettings.json @@ -68,6 +68,16 @@ } ] }, - "Version": "1.1.3", + "FileUploadSettings": { + "MaxFileSize": 5242880, + "AllowedContentTypes": [ + "image/jpeg", + "image/jpg", + "image/png", + "image/gif", + "image/webp" + ] + }, + "Version": "1.1.4", "AllowedHosts": "*" } diff --git a/SQL/v1.1.3/FileUploads.sql b/SQL/v1.1.3/FileUploads.sql index edf65b3..6b75316 100644 --- a/SQL/v1.1.3/FileUploads.sql +++ b/SQL/v1.1.3/FileUploads.sql @@ -1,19 +1,43 @@ --- 文件上传记录表 -CREATE TABLE FileUploads ( - Id BIGINT AUTO_INCREMENT PRIMARY KEY, - UserId BIGINT NOT NULL, -- 用户ID - FileName VARCHAR(255) NOT NULL, -- 原始文件名 - FileKey VARCHAR(500) NOT NULL, -- 七牛云存储key - FileSize BIGINT NOT NULL, -- 文件大小 - ContentType VARCHAR(100) NOT NULL, -- 内容类型 - Hash VARCHAR(100) NOT NULL, -- 文件哈希值 - QiniuUrl VARCHAR(1000) NOT NULL, -- 七牛云访问URL - UploadTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - Status VARCHAR(20) NOT NULL DEFAULT 'active', - CreatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +/* + Navicat Premium Dump SQL --- 创建索引 -CREATE INDEX IX_FileUploads_UserId ON FileUploads(UserId); -CREATE INDEX IX_FileUploads_FileKey ON FileUploads(FileKey); -CREATE INDEX IX_FileUploads_UploadTime ON FileUploads(UploadTime); \ No newline at end of file + Source Server : 亿速云(国内) + Source Server Type : MySQL + Source Server Version : 80018 (8.0.18) + Source Host : yisurds-66dc0b453c05d4.rds.ysydb1.com:14080 + Source Schema : LMS_TEST + + Target Server Type : MySQL + Target Server Version : 80018 (8.0.18) + File Encoding : 65001 + + Date: 18/06/2025 16:31:33 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for FileUploads +-- ---------------------------- +DROP TABLE IF EXISTS `FileUploads`; +CREATE TABLE `FileUploads` ( + `Id` bigint(20) NOT NULL AUTO_INCREMENT, + `UserId` bigint(20) NOT NULL, + `FileName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `FileKey` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `FileSize` bigint(20) NOT NULL, + `ContentType` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `Hash` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `QiniuUrl` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `UploadTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `Status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active', + `CreatedAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `DeleteTime` datetime NOT NULL COMMENT '删除时间', + PRIMARY KEY (`Id`) USING BTREE, + INDEX `IX_FileUploads_UserId`(`UserId` ASC) USING BTREE, + INDEX `IX_FileUploads_FileKey`(`FileKey` ASC) USING BTREE, + INDEX `IX_FileUploads_UploadTime`(`UploadTime` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; + +SET FOREIGN_KEY_CHECKS = 1;