v 1.1.4 新增文件上传中转 七牛云

This commit is contained in:
2025-06-18 17:19:29 +08:00
parent c12e1b5d65
commit 7f269c8b04
15 changed files with 921 additions and 19 deletions
@@ -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<MachineBasicDao>();
services.AddScoped<PermissionBasicDao>();
services.AddScoped<PermissionTypeDao>();
services.AddScoped<OptionGlobalDAO>();
// 注入 Extensions
@@ -63,6 +66,7 @@ namespace Lai_server.Configuration
services.AddScoped<ITokenService, TokenService>();
services.AddScoped<ITaskConcurrencyManager, TaskConcurrencyManager>();
services.AddScoped<ITaskService, TaskService>();
services.AddScoped<IQiniuUploadService, QiniuUploadService>();
// 注册后台服务
@@ -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;
/// <summary>
/// 通过字节数组上传文件
/// </summary>
/// <param name="request">字节上传请求</param>
/// <returns></returns>
[HttpPost("{machineId}")]
public async Task<ActionResult<APIResponseModel<UploadResult>>> FileUpload(string machineId, [FromBody] ByteUploadRequest request)
{
return await _qiniuUploadService.UploadBase64Async(request, machineId);
}
/// <summary>
/// 获取用户文件列表,通过MachineId
/// </summary>
/// <param name="page">页码</param>
/// <param name="pageSize">每页数量</param>
/// <returns></returns>
[HttpGet("{machineId}")]
public async Task<ActionResult<APIResponseModel<CollectionResponse<FileMachineRequestReturn>>>> GetFilesByMachineId(string machineId, int page = 1, int pageSize = 10)
{
return await _qiniuUploadService.GetFilesByMachineId(machineId, page, pageSize);
}
/// <summary>
/// 获取指定ID的用户的文件列表
/// </summary>
/// <param name="userId"></param>
/// <param name="page"></param>
/// <param name="pageSize"></param>
/// <returns></returns>
[HttpGet("{userId}")]
[Authorize]
public async Task<ActionResult<APIResponseModel<CollectionResponse<FileUserRequestReturn>>>> 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);
}
///// <summary>
///// 删除文件
///// </summary>
///// <param name="fileId">文件ID</param>
///// <returns></returns>
//[HttpDelete("files/{fileId}")]
//public async Task<IActionResult> 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}" });
// }
//}
}
}
+1
View File
@@ -29,6 +29,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Qiniu.Shared" Version="7.2.15" />
<PackageReference Include="Quartz" Version="3.14.0" />
<PackageReference Include="Quartz.AspNetCore" Version="3.14.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
+11
View File
@@ -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<IpRateLimitOptions>(builder.Configuration.GetSection("IpRateLimiting"));
builder.Services.Configure<FileUploadSettings>(
builder.Configuration.GetSection("FileUploadSettings"));
// 配置模型验证
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = false; // 确保模型验证生效
});
// 注入计数器和规则存储
builder.Services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>();
builder.Services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();
@@ -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<APIResponseModel<UploadResult>> UploadBase64Async(ByteUploadRequest request, string machineId);
//Task<List<FileUploads>> GetUserFilesAsync(string userId, int page = 1, int pageSize = 20);
//Task<int> GetUserFilesCountAsync(string userId);
//Task<bool> DeleteFileAsync(long fileId, string userId);
Task<ActionResult<APIResponseModel<CollectionResponse<FileMachineRequestReturn>>>> GetFilesByMachineId(string machineId, int page, int pageSize);
Task<ActionResult<APIResponseModel<CollectionResponse<FileUserRequestReturn>>>> GetFilesByUser(long requestUserId, long userId, int page, int pageSize);
}
}
@@ -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<QiniuUploadService> _logger;
private readonly UserBasicDao _userBasicDao;
private readonly OptionGlobalDAO _optionGlobalDAO;
public QiniuUploadService(
IOptions<FileUploadSettings> uploadSettings,
ILogger<QiniuUploadService> logger,
UserBasicDao userBasicDao,
OptionGlobalDAO optionGlobalDAO,
ApplicationDbContext dbContext)
{
_uploadSettings = uploadSettings.Value;
_logger = logger;
_dbContext = dbContext;
_optionGlobalDAO = optionGlobalDAO;
_userBasicDao = userBasicDao; ;
_uploadManager = new UploadManager();
}
/// <summary>
/// 通过字节数组上传文件到七牛云
/// </summary>
/// <param name="request"></param>
/// <param name="machineId"></param>
/// <returns></returns>
public async Task<APIResponseModel<UploadResult>> UploadBase64Async(ByteUploadRequest request, string machineId)
{
try
{
// 将 文件的base64 字符串转换为字节数组
var fileBytes = ConvertBase64ToBytes(request.File);
if (fileBytes == null || fileBytes.Length == 0)
{
return APIResponseModel<UploadResult>.CreateErrorResponseModel(ResponseCode.ParameterError, "无效的文件数据");
}
// 1. 验证数据
var validationResult = ValidateUploadRequest(request, fileBytes);
if (!validationResult.Success)
{
return APIResponseModel<UploadResult>.CreateErrorResponseModel(ResponseCode.ParameterError, validationResult.Message);
}
// 2. 获取用户
long? userId = await GetUserIdFromMachine(machineId);
if (userId == null)
{
return APIResponseModel<UploadResult>.CreateErrorResponseModel(ResponseCode.ParameterError, "无效的机器ID或未找到关联用户");
}
// 3. 校验当前用户是不是超出了上传限制
var userFilesCount = await GetUserUploadToday(userId.Value);
if (userFilesCount >= 5)
{
return APIResponseModel<UploadResult>.CreateErrorResponseModel(ResponseCode.ParameterError, "今日上传文件数量已达上限,请明天再试");
}
QiniuSettings? qiniuSettings = await _optionGlobalDAO.FindAndReturnOption<QiniuSettings>("SYS_QiniuSetting");
if (qiniuSettings == null || string.IsNullOrEmpty(qiniuSettings.AccessKey) || string.IsNullOrEmpty(qiniuSettings.SecretKey) || string.IsNullOrEmpty(qiniuSettings.BucketName) || string.IsNullOrEmpty(qiniuSettings.Domain))
{
return APIResponseModel<UploadResult>.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<UploadResult>.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<UploadResult>.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<UploadResult>.CreateErrorResponseModel(ResponseCode.SystemError, $"上传失败: {ex.Message}");
}
}
/// <summary>
/// 获取对应机器码上传的图片信息
/// </summary>
/// <param name="machineId"></param>
/// <param name="page"></param>
/// <param name="pageSize"></param>
/// <returns></returns>
public async Task<ActionResult<APIResponseModel<CollectionResponse<FileMachineRequestReturn>>>> GetFilesByMachineId(string machineId, int page, int pageSize)
{
try
{
// 1. 判断机器码 是不是 存在 并且获取对应的ID
long? userId = await GetUserIdFromMachine(machineId);
if (userId == null)
{
return APIResponseModel<CollectionResponse<FileMachineRequestReturn>>.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<FileMachineRequestReturn>
{
Current = page,
Total = filesList.totlaCount,
Collection = fileList
};
return APIResponseModel<CollectionResponse<FileMachineRequestReturn>>.CreateSuccessResponseModel(response);
}
catch (Exception ex)
{
// 这里可以记录日志或处理异常
_logger.LogError(ex, $"获取文件列表失败, 机器码: {machineId}");
return APIResponseModel<CollectionResponse<FileMachineRequestReturn>>.CreateErrorResponseModel(ResponseCode.SystemError, "获取文件列表失败");
}
}
/// <summary>
/// 获取指定的用户的文件列表,如果是超级管理员则获取所有用户的文件列表
/// </summary>
/// <param name="requestUserId"></param>
/// <param name="page"></param>
/// <param name="pageSize"></param>
/// <returns></returns>
public async Task<ActionResult<APIResponseModel<CollectionResponse<FileUserRequestReturn>>>> GetFilesByUser(long requestUserId, long userId, int page, int pageSize)
{
try
{
// 1. 判断用户是不是超级管理员,不是超级管理员只能获取自己的
bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(requestUserId);
var fileMessage = (0, new List<FileUploads>());
if (isSuperAdmin)
{
// 超级管理员可以获取所有用户的文件
fileMessage = await GetAllUserFilesAsync(page, pageSize);
}
else
{
if (requestUserId != userId)
{
return APIResponseModel<CollectionResponse<FileUserRequestReturn>>.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<FileUserRequestReturn>
{
Current = page,
Total = fileMessage.Item1,
Collection = fileList
};
return APIResponseModel<CollectionResponse<FileUserRequestReturn>>.CreateSuccessResponseModel(response);
}
catch (Exception ex)
{
_logger.LogError(ex, $"获取用户文件列表失败, 用户ID: {requestUserId}");
return APIResponseModel<CollectionResponse<FileUserRequestReturn>>.CreateErrorResponseModel(ResponseCode.SystemError, "获取用户文件列表失败");
}
}
public async Task<(int totlaCount, List<FileUploads> 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<FileUploads>());
}
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<FileUploads> 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<FileUploads>());
}
// 获取所有用户的文件列表
List<FileUploads> 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<int> GetUserFilesCountAsync(long userId)
{
return await _dbContext.FileUploads
.CountAsync(f => f.UserId == userId && f.Status == "active");
}
private async Task<int> 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<long?> 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}";
}
}
}
+11 -1
View File
@@ -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": "*"
}