feat: 统一资源管理

This commit is contained in:
alivender
2025-08-02 13:14:01 +08:00
parent e5f2be616c
commit 9904fecbee
15 changed files with 1178 additions and 838 deletions

View File

@@ -101,22 +101,6 @@ public class ExamController : ControllerBase
public bool IsVisibleToUsers { get; set; } = true;
}
/// <summary>
/// 资源信息类
/// </summary>
public class ResourceInfo
{
/// <summary>
/// 资源ID
/// </summary>
public int ID { get; set; }
/// <summary>
/// 资源名称
/// </summary>
public required string Name { get; set; }
}
/// <summary>
/// 创建实验请求类
/// </summary>
@@ -304,151 +288,4 @@ public class ExamController : ControllerBase
return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {ex.Message}");
}
}
/// <summary>
/// 添加实验资源
/// </summary>
/// <param name="examId">实验ID</param>
/// <param name="resourceType">资源类型</param>
/// <param name="file">资源文件</param>
/// <returns>添加结果</returns>
[Authorize("Admin")]
[HttpPost("{examId}/resources/{resourceType}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(ResourceInfo), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> AddExamResource(string examId, string resourceType, IFormFile file)
{
if (string.IsNullOrWhiteSpace(examId) || string.IsNullOrWhiteSpace(resourceType) || file == null)
return BadRequest("实验ID、资源类型和文件不能为空");
try
{
using var db = new Database.AppDataConnection();
// 读取文件数据
using var memoryStream = new MemoryStream();
await file.CopyToAsync(memoryStream);
var fileData = memoryStream.ToArray();
var result = db.AddExamResource(examId, resourceType, file.FileName, fileData);
if (!result.IsSuccessful)
{
if (result.Error.Message.Contains("不存在"))
return NotFound(result.Error.Message);
if (result.Error.Message.Contains("已存在"))
return Conflict(result.Error.Message);
logger.Error($"添加实验资源时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"添加实验资源失败: {result.Error.Message}");
}
var resource = result.Value;
var resourceInfo = new ResourceInfo
{
ID = resource.ID,
Name = resource.ResourceName
};
logger.Info($"成功添加实验资源: {examId}/{resourceType}/{file.FileName}");
return CreatedAtAction(nameof(GetExamResourceById), new { resourceId = resource.ID }, resourceInfo);
}
catch (Exception ex)
{
logger.Error($"添加实验资源 {examId}/{resourceType}/{file.FileName} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"添加实验资源失败: {ex.Message}");
}
}
/// <summary>
/// 获取指定实验ID的指定资源类型的所有资源的ID和名称
/// </summary>
/// <param name="examId">实验ID</param>
/// <param name="resourceType">资源类型</param>
/// <returns>资源列表</returns>
[Authorize]
[HttpGet("{examId}/resources/{resourceType}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(ResourceInfo[]), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetExamResourceList(string examId, string resourceType)
{
if (string.IsNullOrWhiteSpace(examId) || string.IsNullOrWhiteSpace(resourceType))
return BadRequest("实验ID和资源类型不能为空");
try
{
using var db = new Database.AppDataConnection();
var result = db.GetExamResourceList(examId, resourceType);
if (!result.IsSuccessful)
{
logger.Error($"获取实验资源列表时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验资源列表失败: {result.Error.Message}");
}
var resources = result.Value.Select(r => new ResourceInfo
{
ID = r.ID,
Name = r.Name
}).ToArray();
logger.Info($"成功获取实验资源列表: {examId}/{resourceType},共 {resources.Length} 个资源");
return Ok(resources);
}
catch (Exception ex)
{
logger.Error($"获取实验资源列表 {examId}/{resourceType} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验资源列表失败: {ex.Message}");
}
}
/// <summary>
/// 根据资源ID下载资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>资源文件</returns>
[HttpGet("resources/{resourceId}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetExamResourceById(int resourceId)
{
try
{
using var db = new Database.AppDataConnection();
var result = db.GetExamResourceById(resourceId);
if (!result.IsSuccessful)
{
logger.Error($"获取资源时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {result.Error.Message}");
}
if (!result.Value.HasValue)
{
logger.Warn($"资源不存在: {resourceId}");
return NotFound($"资源 {resourceId} 不存在");
}
var resource = result.Value.Value;
logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
return File(resource.Data, resource.MimeType, resource.ResourceName);
}
catch (Exception ex)
{
logger.Error($"获取资源 {resourceId} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {ex.Message}");
}
}
}

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Database;
namespace server.Controllers;
@@ -14,8 +15,6 @@ public class JtagController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private const string BITSTREAM_PATH = "bitstream/Jtag";
/// <summary>
/// 控制器首页信息
/// </summary>
@@ -112,64 +111,12 @@ public class JtagController : ControllerBase
}
}
/// <summary>
/// 上传比特流文件到服务器
/// </summary>
/// <param name="address">目标设备地址</param>
/// <param name="file">比特流文件</param>
/// <returns>上传结果</returns>
[HttpPost("UploadBitstream")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> UploadBitstream(string address, IFormFile file)
{
logger.Info($"User {User.Identity?.Name} uploading bitstream for device {address}");
if (file == null || file.Length == 0)
{
logger.Warn($"User {User.Identity?.Name} attempted to upload empty file for device {address}");
return TypedResults.BadRequest("未选择文件");
}
try
{
// 生成安全的文件名(避免路径遍历攻击)
var fileName = Path.GetRandomFileName();
var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
// 如果存在文件,则删除原文件再上传
if (Directory.Exists(uploadsFolder))
{
Directory.Delete(uploadsFolder, true);
logger.Info($"User {User.Identity?.Name} removed existing bitstream folder for device {address}");
}
Directory.CreateDirectory(uploadsFolder);
var filePath = Path.Combine(uploadsFolder, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
logger.Info($"User {User.Identity?.Name} successfully uploaded bitstream for device {address}, file size: {file.Length} bytes");
return TypedResults.Ok(true);
}
catch (Exception ex)
{
logger.Error(ex, $"User {User.Identity?.Name} failed to upload bitstream for device {address}");
return TypedResults.InternalServerError(ex);
}
}
/// <summary>
/// 通过 JTAG 下载比特流文件到 FPGA 设备
/// </summary>
/// <param name="address">JTAG 设备地址</param>
/// <param name="port">JTAG 设备端口</param>
/// <param name="bitstreamId">比特流ID</param>
/// <returns>下载结果</returns>
[HttpPost("DownloadBitstream")]
[EnableCors("Users")]
@@ -177,87 +124,111 @@ public class JtagController : ControllerBase
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async ValueTask<IResult> DownloadBitstream(string address, int port)
public async ValueTask<IResult> DownloadBitstream(string address, int port, int bitstreamId)
{
logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port}");
// 检查文件
var fileDir = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
if (!Directory.Exists(fileDir))
{
logger.Warn($"User {User.Identity?.Name} attempted to download non-existent bitstream for device {address}");
return TypedResults.BadRequest("Empty bitstream, Please upload it first");
}
logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port} using bitstream ID: {bitstreamId}");
try
{
// 读取文件
var filePath = Directory.GetFiles(fileDir)[0];
logger.Info($"User {User.Identity?.Name} reading bitstream file: {filePath}");
using (var fileStream = System.IO.File.Open(filePath, System.IO.FileMode.Open))
// 获取当前用户名
var username = User.Identity?.Name;
if (string.IsNullOrEmpty(username))
{
if (fileStream is null || fileStream.Length <= 0)
logger.Warn("Anonymous user attempted to download bitstream");
return TypedResults.Unauthorized();
}
// 从数据库获取用户信息
using var db = new Database.AppDataConnection();
var userResult = db.GetUserByName(username);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
{
logger.Error($"User {username} not found in database");
return TypedResults.BadRequest("用户不存在");
}
var user = userResult.Value.Value;
// 从数据库获取比特流
var bitstreamResult = db.GetResourceById(bitstreamId);
if (!bitstreamResult.IsSuccessful)
{
logger.Error($"User {username} failed to get bitstream from database: {bitstreamResult.Error}");
return TypedResults.InternalServerError($"数据库查询失败: {bitstreamResult.Error?.Message}");
}
if (!bitstreamResult.Value.HasValue)
{
logger.Warn($"User {username} attempted to download non-existent bitstream ID: {bitstreamId}");
return TypedResults.BadRequest("比特流不存在");
}
var bitstream = bitstreamResult.Value.Value;
// 处理比特流数据
var fileBytes = bitstream.Data;
if (fileBytes == null || fileBytes.Length == 0)
{
logger.Warn($"User {username} found empty bitstream data for ID: {bitstreamId}");
return TypedResults.BadRequest("比特流数据为空,请重新上传");
}
logger.Info($"User {username} processing bitstream file of size: {fileBytes.Length} bytes");
// 定义缓冲区大小: 32KB
byte[] buffer = new byte[32 * 1024];
byte[] revBuffer = new byte[32 * 1024];
long totalBytesProcessed = 0;
// 使用内存流处理文件
using (var inputStream = new MemoryStream(fileBytes))
using (var outputStream = new MemoryStream())
{
int bytesRead;
while ((bytesRead = await inputStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
logger.Warn($"User {User.Identity?.Name} found invalid bitstream file for device {address}");
return TypedResults.BadRequest("Wrong bitstream, Please upload it again");
// 反转 32bits
var retBuffer = Common.Number.ReverseBytes(buffer, 4);
if (!retBuffer.IsSuccessful)
{
logger.Error($"User {username} failed to reverse bytes: {retBuffer.Error}");
return TypedResults.InternalServerError(retBuffer.Error);
}
revBuffer = retBuffer.Value;
for (int i = 0; i < revBuffer.Length; i++)
{
revBuffer[i] = Common.Number.ReverseBits(revBuffer[i]);
}
await outputStream.WriteAsync(revBuffer, 0, bytesRead);
totalBytesProcessed += bytesRead;
}
logger.Info($"User {User.Identity?.Name} processing bitstream file of size: {fileStream.Length} bytes");
// 获取处理后的数据
var processedBytes = outputStream.ToArray();
logger.Info($"User {username} processed {totalBytesProcessed} bytes for device {address}");
// 定义缓冲区大小: 32KB
byte[] buffer = new byte[32 * 1024];
byte[] revBuffer = new byte[32 * 1024];
long totalBytesRead = 0;
// 下载比特流
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
var ret = await jtagCtrl.DownloadBitstream(processedBytes);
// 使用异步流读取文件
using (var memoryStream = new MemoryStream())
if (ret.IsSuccessful)
{
int bytesRead;
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
// 反转 32bits
var retBuffer = Common.Number.ReverseBytes(buffer, 4);
if (!retBuffer.IsSuccessful)
{
logger.Error($"User {User.Identity?.Name} failed to reverse bytes: {retBuffer.Error}");
return TypedResults.InternalServerError(retBuffer.Error);
}
revBuffer = retBuffer.Value;
for (int i = 0; i < revBuffer.Length; i++)
{
revBuffer[i] = Common.Number.ReverseBits(revBuffer[i]);
}
await memoryStream.WriteAsync(revBuffer, 0, bytesRead);
totalBytesRead += bytesRead;
}
// 将所有数据转换为字节数组(注意:如果文件非常大,可能不适合完全加载到内存)
var fileBytes = memoryStream.ToArray();
logger.Info($"User {User.Identity?.Name} processed {totalBytesRead} bytes for device {address}");
// 下载比特流
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
var ret = await jtagCtrl.DownloadBitstream(fileBytes);
if (ret.IsSuccessful)
{
logger.Info($"User {User.Identity?.Name} successfully downloaded bitstream to device {address}");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error($"User {User.Identity?.Name} failed to download bitstream to device {address}: {ret.Error}");
return TypedResults.InternalServerError(ret.Error);
}
logger.Info($"User {username} successfully downloaded bitstream '{bitstream.ResourceName}' to device {address}");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}");
return TypedResults.InternalServerError(ret.Error);
}
}
}
catch (Exception ex)
{
logger.Error(ex, $"User {User.Identity?.Name} encountered exception while downloading bitstream to device {address}");
logger.Error(ex, $"User encountered exception while downloading bitstream to device {address}");
return TypedResults.InternalServerError(ex);
}
}

View File

@@ -0,0 +1,377 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using DotNext;
using Database;
namespace server.Controllers;
/// <summary>
/// 资源控制器 - 提供统一的资源管理API
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class ResourceController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
/// <summary>
/// 资源信息类
/// </summary>
public class ResourceInfo
{
/// <summary>
/// 资源ID
/// </summary>
public int ID { get; set; }
/// <summary>
/// 资源名称
/// </summary>
public required string Name { get; set; }
/// <summary>
/// 资源类型
/// </summary>
public required string Type { get; set; }
/// <summary>
/// 资源用途template/user
/// </summary>
public required string Purpose { get; set; }
/// <summary>
/// 上传时间
/// </summary>
public DateTime UploadTime { get; set; }
/// <summary>
/// 所属实验ID可选
/// </summary>
public string? ExamID { get; set; }
/// <summary>
/// MIME类型
/// </summary>
public string? MimeType { get; set; }
}
/// <summary>
/// 添加资源请求类
/// </summary>
public class AddResourceRequest
{
/// <summary>
/// 资源类型
/// </summary>
public required string ResourceType { get; set; }
/// <summary>
/// 资源用途template/user
/// </summary>
public required string ResourcePurpose { get; set; }
/// <summary>
/// 所属实验ID可选
/// </summary>
public string? ExamID { get; set; }
}
/// <summary>
/// 添加资源(文件上传)
/// </summary>
/// <param name="request">添加资源请求</param>
/// <param name="file">资源文件</param>
/// <returns>添加结果</returns>
[Authorize]
[HttpPost]
[EnableCors("Users")]
[ProducesResponseType(typeof(ResourceInfo), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> AddResource([FromForm] AddResourceRequest request, IFormFile file)
{
if (string.IsNullOrWhiteSpace(request.ResourceType) || string.IsNullOrWhiteSpace(request.ResourcePurpose) || file == null)
return BadRequest("资源类型、资源用途和文件不能为空");
// 验证资源用途
if (request.ResourcePurpose != Resource.ResourcePurposes.Template && request.ResourcePurpose != Resource.ResourcePurposes.User)
return BadRequest($"无效的资源用途: {request.ResourcePurpose}");
// 模板资源需要管理员权限
if (request.ResourcePurpose == Resource.ResourcePurposes.Template && !User.IsInRole("Admin"))
return Forbid("只有管理员可以添加模板资源");
try
{
using var db = new Database.AppDataConnection();
// 获取当前用户ID
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = db.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
// 读取文件数据
using var memoryStream = new MemoryStream();
await file.CopyToAsync(memoryStream);
var fileData = memoryStream.ToArray();
var result = db.AddResource(user.ID, request.ResourceType, request.ResourcePurpose, file.FileName, fileData, request.ExamID);
if (!result.IsSuccessful)
{
if (result.Error.Message.Contains("不存在"))
return NotFound(result.Error.Message);
logger.Error($"添加资源时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"添加资源失败: {result.Error.Message}");
}
var resource = result.Value;
var resourceInfo = new ResourceInfo
{
ID = resource.ID,
Name = resource.ResourceName,
Type = resource.ResourceType,
Purpose = resource.ResourcePurpose,
UploadTime = resource.UploadTime,
ExamID = resource.ExamID,
MimeType = resource.MimeType
};
logger.Info($"成功添加资源: {request.ResourceType}/{request.ResourcePurpose}/{file.FileName}");
return CreatedAtAction(nameof(GetResourceById), new { resourceId = resource.ID }, resourceInfo);
}
catch (Exception ex)
{
logger.Error($"添加资源 {request.ResourceType}/{request.ResourcePurpose}/{file.FileName} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"添加资源失败: {ex.Message}");
}
}
/// <summary>
/// 获取资源列表
/// </summary>
/// <param name="examId">实验ID可选</param>
/// <param name="resourceType">资源类型(可选)</param>
/// <param name="resourcePurpose">资源用途(可选)</param>
/// <returns>资源列表</returns>
[Authorize]
[HttpGet]
[EnableCors("Users")]
[ProducesResponseType(typeof(ResourceInfo[]), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetResourceList([FromQuery] string? examId = null, [FromQuery] string? resourceType = null, [FromQuery] string? resourcePurpose = null)
{
try
{
using var db = new Database.AppDataConnection();
// 获取当前用户ID
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = db.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
// 普通用户只能查看自己的资源和模板资源
Guid? userId = null;
if (!User.IsInRole("Admin"))
{
// 如果指定了用户资源用途,则只查看自己的资源
if (resourcePurpose == Resource.ResourcePurposes.User)
{
userId = user.ID;
}
// 如果指定了模板资源用途则不限制用户ID
else if (resourcePurpose == Resource.ResourcePurposes.Template)
{
userId = null;
}
// 如果没有指定用途,则查看自己的用户资源和所有模板资源
else
{
// 这种情况下需要分别查询并合并结果
var userResourcesResult = db.GetFullResourceList(examId, resourceType, Resource.ResourcePurposes.User, user.ID);
var templateResourcesResult = db.GetFullResourceList(examId, resourceType, Resource.ResourcePurposes.Template, null);
if (!userResourcesResult.IsSuccessful || !templateResourcesResult.IsSuccessful)
{
logger.Error($"获取资源列表时出错");
return StatusCode(StatusCodes.Status500InternalServerError, "获取资源列表失败");
}
var allResources = userResourcesResult.Value.Concat(templateResourcesResult.Value)
.OrderByDescending(r => r.UploadTime);
var mergedResourceInfos = allResources.Select(r => new ResourceInfo
{
ID = r.ID,
Name = r.ResourceName,
Type = r.ResourceType,
Purpose = r.ResourcePurpose,
UploadTime = r.UploadTime,
ExamID = r.ExamID,
MimeType = r.MimeType
}).ToArray();
logger.Info($"成功获取资源列表,共 {mergedResourceInfos.Length} 个资源");
return Ok(mergedResourceInfos);
}
}
var result = db.GetFullResourceList(examId, resourceType, resourcePurpose, userId);
if (!result.IsSuccessful)
{
logger.Error($"获取资源列表时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源列表失败: {result.Error.Message}");
}
var resources = result.Value.Select(r => new ResourceInfo
{
ID = r.ID,
Name = r.ResourceName,
Type = r.ResourceType,
Purpose = r.ResourcePurpose,
UploadTime = r.UploadTime,
ExamID = r.ExamID,
MimeType = r.MimeType
}).ToArray();
logger.Info($"成功获取资源列表,共 {resources.Length} 个资源");
return Ok(resources);
}
catch (Exception ex)
{
logger.Error($"获取资源列表时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源列表失败: {ex.Message}");
}
}
/// <summary>
/// 根据资源ID下载资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>资源文件</returns>
[HttpGet("{resourceId}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetResourceById(int resourceId)
{
try
{
using var db = new Database.AppDataConnection();
var result = db.GetResourceById(resourceId);
if (!result.IsSuccessful)
{
logger.Error($"获取资源时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {result.Error.Message}");
}
if (!result.Value.HasValue)
{
logger.Warn($"资源不存在: {resourceId}");
return NotFound($"资源 {resourceId} 不存在");
}
var resource = result.Value.Value;
logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
return File(resource.Data, resource.MimeType ?? "application/octet-stream", resource.ResourceName);
}
catch (Exception ex)
{
logger.Error($"获取资源 {resourceId} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {ex.Message}");
}
}
/// <summary>
/// 删除资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>删除结果</returns>
[Authorize]
[HttpDelete("{resourceId}")]
[EnableCors("Users")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult DeleteResource(int resourceId)
{
try
{
using var db = new Database.AppDataConnection();
// 获取当前用户信息
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = db.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
// 先获取资源信息以验证权限
var resourceResult = db.GetResourceById(resourceId);
if (!resourceResult.IsSuccessful)
{
logger.Error($"获取资源时出错: {resourceResult.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {resourceResult.Error.Message}");
}
if (!resourceResult.Value.HasValue)
{
logger.Warn($"资源不存在: {resourceId}");
return NotFound($"资源 {resourceId} 不存在");
}
var resource = resourceResult.Value.Value;
// 权限检查:管理员可以删除所有资源,普通用户只能删除自己的用户资源
if (!User.IsInRole("Admin"))
{
if (resource.ResourcePurpose == Resource.ResourcePurposes.Template)
return Forbid("普通用户不能删除模板资源");
if (resource.UserID != user.ID)
return Forbid("只能删除自己的资源");
}
var deleteResult = db.DeleteResource(resourceId);
if (!deleteResult.IsSuccessful)
{
logger.Error($"删除资源时出错: {deleteResult.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"删除资源失败: {deleteResult.Error.Message}");
}
logger.Info($"成功删除资源: {resourceId} ({resource.ResourceName})");
return NoContent();
}
catch (Exception ex)
{
logger.Error($"删除资源 {resourceId} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"删除资源失败: {ex.Message}");
}
}
}

View File

@@ -229,9 +229,9 @@ public class Exam
}
/// <summary>
/// 实验资源表(图片等)
/// 资源类,统一管理实验资源、用户比特流等各类资源
/// </summary>
public class ExamResource
public class Resource
{
/// <summary>
/// 资源的唯一标识符
@@ -240,17 +240,29 @@ public class ExamResource
public int ID { get; set; }
/// <summary>
/// 所属实验ID
/// 上传资源的用户ID
/// </summary>
[NotNull]
public required string ExamID { get; set; }
public required Guid UserID { get; set; }
/// <summary>
/// 资源类型images, markdown, bitstream, diagram, project
/// 所属实验ID可选如果不属于特定实验则为空
/// </summary>
[Nullable]
public string? ExamID { get; set; }
/// <summary>
/// 资源类型images, markdown, bitstream, diagram, project等
/// </summary>
[NotNull]
public required string ResourceType { get; set; }
/// <summary>
/// 资源用途template模板或 user用户上传
/// </summary>
[NotNull]
public required string ResourcePurpose { get; set; }
/// <summary>
/// 资源名称(包含文件扩展名)
/// </summary>
@@ -264,10 +276,10 @@ public class ExamResource
public required byte[] Data { get; set; }
/// <summary>
/// 资源创建时间
/// 资源创建/上传时间
/// </summary>
[NotNull]
public DateTime CreatedTime { get; set; } = DateTime.Now;
public DateTime UploadTime { get; set; } = DateTime.Now;
/// <summary>
/// 资源的MIME类型
@@ -305,6 +317,22 @@ public class ExamResource
/// </summary>
public const string Project = "project";
}
/// <summary>
/// 资源用途枚举
/// </summary>
public static class ResourcePurposes
{
/// <summary>
/// 模板资源,通常由管理员上传,供用户参考
/// </summary>
public const string Template = "template";
/// <summary>
/// 用户上传的资源
/// </summary>
public const string User = "user";
}
}
/// <summary>
@@ -355,7 +383,7 @@ public class AppDataConnection : DataConnection
this.CreateTable<User>();
this.CreateTable<Board>();
this.CreateTable<Exam>();
this.CreateTable<ExamResource>();
this.CreateTable<Resource>();
logger.Info("数据库表创建完成");
}
@@ -368,7 +396,7 @@ public class AppDataConnection : DataConnection
this.DropTable<User>();
this.DropTable<Board>();
this.DropTable<Exam>();
this.DropTable<ExamResource>();
this.DropTable<Resource>();
logger.Warn("所有数据库表已删除");
}
@@ -828,9 +856,9 @@ public class AppDataConnection : DataConnection
public ITable<Exam> ExamTable => this.GetTable<Exam>();
/// <summary>
/// 实验资源表
/// 资源表(统一管理实验资源、用户比特流等)
/// </summary>
public ITable<ExamResource> ExamResourceTable => this.GetTable<ExamResource>();
public ITable<Resource> ResourceTable => this.GetTable<Resource>();
/// <summary>
/// 创建新实验
@@ -933,34 +961,44 @@ public class AppDataConnection : DataConnection
}
/// <summary>
/// 添加实验资源
/// 添加资源
/// </summary>
/// <param name="examId">所属实验ID</param>
/// <param name="userId">上传用户ID</param>
/// <param name="resourceType">资源类型</param>
/// <param name="resourcePurpose">资源用途template 或 user</param>
/// <param name="resourceName">资源名称</param>
/// <param name="data">资源二进制数据</param>
/// <param name="examId">所属实验ID可选</param>
/// <param name="mimeType">MIME类型可选将根据文件扩展名自动确定</param>
/// <returns>创建的资源</returns>
public Result<ExamResource> AddExamResource(string examId, string resourceType, string resourceName, byte[] data, string? mimeType = null)
public Result<Resource> AddResource(Guid userId, string resourceType, string resourcePurpose, string resourceName, byte[] data, string? examId = null, string? mimeType = null)
{
try
{
// 验证实验是否存在
var exam = this.ExamTable.Where(e => e.ID == examId).FirstOrDefault();
if (exam == null)
// 验证用户是否存在
var user = this.UserTable.Where(u => u.ID == userId).FirstOrDefault();
if (user == null)
{
logger.Error($"实验不存在: {examId}");
return new(new Exception($"实验不存在: {examId}"));
logger.Error($"用户不存在: {userId}");
return new(new Exception($"用户不存在: {userId}"));
}
// 检查资源是否存在
var existingResource = this.ExamResourceTable
.Where(r => r.ExamID == examId && r.ResourceType == resourceType && r.ResourceName == resourceName)
.FirstOrDefault();
if (existingResource != null)
// 如果指定了实验ID验证实验是否存在
if (!string.IsNullOrEmpty(examId))
{
logger.Error($"资源已存在: {examId}/{resourceType}/{resourceName}");
return new(new Exception($"资源已存在: {examId}/{resourceType}/{resourceName}"));
var exam = this.ExamTable.Where(e => e.ID == examId).FirstOrDefault();
if (exam == null)
{
logger.Error($"实验不存在: {examId}");
return new(new Exception($"实验不存在: {examId}"));
}
}
// 验证资源用途
if (resourcePurpose != Resource.ResourcePurposes.Template && resourcePurpose != Resource.ResourcePurposes.User)
{
logger.Error($"无效的资源用途: {resourcePurpose}");
return new(new Exception($"无效的资源用途: {resourcePurpose}"));
}
// 如果未指定MIME类型根据文件扩展名自动确定
@@ -970,49 +1008,126 @@ public class AppDataConnection : DataConnection
mimeType = GetMimeTypeFromExtension(extension, resourceName);
}
var resource = new ExamResource
var resource = new Resource
{
UserID = userId,
ExamID = examId,
ResourceType = resourceType,
ResourcePurpose = resourcePurpose,
ResourceName = resourceName,
Data = data,
MimeType = mimeType,
CreatedTime = DateTime.Now
UploadTime = DateTime.Now
};
this.Insert(resource);
logger.Info($"新资源已添加: {examId}/{resourceType}/{resourceName} ({data.Length} bytes)");
var insertedId = this.InsertWithIdentity(resource);
resource.ID = Convert.ToInt32(insertedId);
logger.Info($"新资源已添加: {userId}/{resourceType}/{resourcePurpose}/{resourceName} ({data.Length} bytes)" +
(examId != null ? $" [实验: {examId}]" : "") + $" [ID: {resource.ID}]");
return new(resource);
}
catch (Exception ex)
{
logger.Error($"添加实验资源时出错: {ex.Message}");
logger.Error($"添加资源时出错: {ex.Message}");
return new(ex);
}
}
/// <summary>
/// 获取指定实验ID的指定资源类型的所有资源的ID和名称
/// </summary>
/// <param name="examId">实验ID</param>
/// 获取资源信息列表(返回ID和名称
/// <param name="resourceType">资源类型</param>
/// <param name="examId">实验ID可选</param>
/// <param name="resourcePurpose">资源用途(可选)</param>
/// <param name="userId">用户ID可选</param>
/// </summary>
/// <returns>资源信息列表</returns>
public Result<(int ID, string Name)[]> GetExamResourceList(string examId, string resourceType)
public Result<(int ID, string Name)[]> GetResourceList(string resourceType, string? examId = null, string? resourcePurpose = null, Guid? userId = null)
{
try
{
var resources = this.ExamResourceTable
.Where(r => r.ExamID == examId && r.ResourceType == resourceType)
var query = this.ResourceTable.Where(r => r.ResourceType == resourceType);
if (examId != null)
{
query = query.Where(r => r.ExamID == examId);
}
if (resourcePurpose != null)
{
query = query.Where(r => r.ResourcePurpose == resourcePurpose);
}
if (userId != null)
{
query = query.Where(r => r.UserID == userId);
}
var resources = query
.Select(r => new { r.ID, r.ResourceName })
.ToArray();
var result = resources.Select(r => (r.ID, r.ResourceName)).ToArray();
logger.Info($"获取实验资源列表: {examId}/{resourceType},共 {result.Length} 个资源");
logger.Info($"获取资源列表: {resourceType}" +
(examId != null ? $"/{examId}" : "") +
(resourcePurpose != null ? $"/{resourcePurpose}" : "") +
(userId != null ? $"/{userId}" : "") +
$",共 {result.Length} 个资源");
return new(result);
}
catch (Exception ex)
{
logger.Error($"获取实验资源列表时出错: {ex.Message}");
logger.Error($"获取资源列表时出错: {ex.Message}");
return new(ex);
}
}
/// <summary>
/// 获取完整的资源列表
/// </summary>
/// <param name="examId">实验ID可选</param>
/// <param name="resourceType">资源类型(可选)</param>
/// <param name="resourcePurpose">资源用途(可选)</param>
/// <param name="userId">用户ID可选</param>
/// <returns>完整的资源对象列表</returns>
public Result<List<Resource>> GetFullResourceList(string? examId = null, string? resourceType = null, string? resourcePurpose = null, Guid? userId = null)
{
try
{
var query = this.ResourceTable.AsQueryable();
if (examId != null)
{
query = query.Where(r => r.ExamID == examId);
}
if (resourceType != null)
{
query = query.Where(r => r.ResourceType == resourceType);
}
if (resourcePurpose != null)
{
query = query.Where(r => r.ResourcePurpose == resourcePurpose);
}
if (userId != null)
{
query = query.Where(r => r.UserID == userId);
}
var resources = query.OrderByDescending(r => r.UploadTime).ToList();
logger.Info($"获取完整资源列表" +
(examId != null ? $" [实验: {examId}]" : "") +
(resourceType != null ? $" [类型: {resourceType}]" : "") +
(resourcePurpose != null ? $" [用途: {resourcePurpose}]" : "") +
(userId != null ? $" [用户: {userId}]" : "") +
$",共 {resources.Count} 个资源");
return new(resources);
}
catch (Exception ex)
{
logger.Error($"获取完整资源列表时出错: {ex.Message}");
return new(ex);
}
}
@@ -1022,16 +1137,16 @@ public class AppDataConnection : DataConnection
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>资源数据</returns>
public Result<Optional<ExamResource>> GetExamResourceById(int resourceId)
public Result<Optional<Resource>> GetResourceById(int resourceId)
{
try
{
var resource = this.ExamResourceTable.Where(r => r.ID == resourceId).FirstOrDefault();
var resource = this.ResourceTable.Where(r => r.ID == resourceId).FirstOrDefault();
if (resource == null)
{
logger.Info($"未找到资源: {resourceId}");
return new(Optional<ExamResource>.None);
return new(Optional<Resource>.None);
}
logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
@@ -1045,15 +1160,15 @@ public class AppDataConnection : DataConnection
}
/// <summary>
/// 删除实验资源
/// 删除资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>删除的记录数</returns>
public Result<int> DeleteExamResource(int resourceId)
public Result<int> DeleteResource(int resourceId)
{
try
{
var result = this.ExamResourceTable.Where(r => r.ID == resourceId).Delete();
var result = this.ResourceTable.Where(r => r.ID == resourceId).Delete();
logger.Info($"资源已删除: {resourceId},删除记录数: {result}");
return new(result);
}
@@ -1132,29 +1247,20 @@ public class AppDataConnection : DataConnection
}
/// <summary>
/// 删除所有实验
/// 根据文件扩展名获取比特流MIME类型
/// </summary>
/// <returns>删除的实验数量</returns>
public int DeleteAllExams()
/// <param name="extension">文件扩展名</param>
/// <returns>MIME类型</returns>
private string GetBitstreamMimeType(string extension)
{
// 先删除所有实验资源
var resourceDeleteCount = this.DeleteAllExamResources();
logger.Info($"已删除所有实验资源,共删除 {resourceDeleteCount} 个资源");
// 再删除所有实验
var examDeleteCount = this.ExamTable.Delete();
logger.Info($"已删除所有实验,共删除 {examDeleteCount} 个实验");
return examDeleteCount;
}
/// <summary>
/// 删除所有实验资源
/// </summary>
/// <returns>删除的资源数量</returns>
public int DeleteAllExamResources()
{
var deleteCount = this.ExamResourceTable.Delete();
logger.Info($"已删除所有实验资源,共删除 {deleteCount} 个资源");
return deleteCount;
return extension.ToLowerInvariant() switch
{
".bit" => "application/octet-stream",
".sbit" => "application/octet-stream",
".bin" => "application/octet-stream",
".mcs" => "application/octet-stream",
".hex" => "text/plain",
_ => "application/octet-stream"
};
}
}