diff --git a/server/Program.cs b/server/Program.cs index c5fbe28..35d028a 100644 --- a/server/Program.cs +++ b/server/Program.cs @@ -102,7 +102,7 @@ try options.AddPolicy("Admin", policy => { policy.RequireClaim(ClaimTypes.Role, new string[] { - Database.User.UserPermission.Admin.ToString(), + Database.UserPermission.Admin.ToString(), }); }); }); diff --git a/server/src/Controllers/DataController.cs b/server/src/Controllers/DataController.cs index 27cdb85..71e38ce 100644 --- a/server/src/Controllers/DataController.cs +++ b/server/src/Controllers/DataController.cs @@ -440,7 +440,7 @@ public class DataController : ControllerBase [ProducesResponseType(typeof(int), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public IActionResult UpdateBoardStatus(Guid boardId, Database.Board.BoardStatus newStatus) + public IActionResult UpdateBoardStatus(Guid boardId, Database.BoardStatus newStatus) { if (boardId == Guid.Empty) return BadRequest("板子Guid不能为空"); @@ -456,6 +456,24 @@ public class DataController : ControllerBase } } + [HttpPost("AddEmptyBoard")] + [EnableCors("Development")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult AddEmptyBoard() + { + try + { + var boardId = _userManager.AddBoard("Test"); + var result = _userManager.UpdateBoardStatus(boardId, Database.BoardStatus.Available); + return Ok(); + } + catch (Exception ex) + { + logger.Error(ex, "新增板子时发生异常"); + return StatusCode(StatusCodes.Status500InternalServerError, "新增失败,请稍后重试"); + } + } /// /// [TODO:description] diff --git a/server/src/Controllers/ExamController.cs b/server/src/Controllers/ExamController.cs index 49cecf8..071f7e2 100644 --- a/server/src/Controllers/ExamController.cs +++ b/server/src/Controllers/ExamController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; using DotNext; +using Database; namespace server.Controllers; @@ -14,11 +15,18 @@ public class ExamController : ControllerBase { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); - private readonly Database.ExamManager _examManager; + private readonly ExamManager _examManager; + private readonly ResourceManager _resourceManager; + private readonly UserManager _userManager; - public ExamController(Database.ExamManager examManager) + public ExamController( + ExamManager examManager, + ResourceManager resourceManager, + UserManager userManager) { _examManager = examManager; + _resourceManager = resourceManager; + _userManager = userManager; } /// @@ -211,6 +219,222 @@ public class ExamController : ControllerBase return StatusCode(StatusCodes.Status500InternalServerError, $"更新实验失败: {ex.Message}"); } } + + /// + /// 提交作业 + /// + /// 实验ID + /// 提交的文件 + /// 提交结果 + [Authorize] + [HttpPost("commit/{examId}")] + [EnableCors("Users")] + [ProducesResponseType(typeof(Resource), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task SubmitHomework(string examId, IFormFile file) + { + if (string.IsNullOrWhiteSpace(examId)) + return BadRequest("实验ID不能为空"); + + if (file == null || file.Length == 0) + return BadRequest("文件不能为空"); + + try + { + // 获取当前用户信息 + var userName = User.Identity?.Name; + if (string.IsNullOrEmpty(userName)) + return Unauthorized("无法获取用户信息"); + + var userResult = _userManager.GetUserByName(userName); + if (!userResult.IsSuccessful || !userResult.Value.HasValue) + return Unauthorized("用户不存在"); + + var user = userResult.Value.Value; + + // 检查实验是否存在 + var examResult = _examManager.GetExamByID(examId); + if (!examResult.IsSuccessful) + { + logger.Error($"检查实验是否存在时出错: {examResult.Error.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"检查实验失败: {examResult.Error.Message}"); + } + + if (!examResult.Value.HasValue) + { + logger.Warn($"实验不存在: {examId}"); + return NotFound($"实验 {examId} 不存在"); + } + + // 读取文件内容 + byte[] fileData; + using (var memoryStream = new MemoryStream()) + { + await file.CopyToAsync(memoryStream); + fileData = memoryStream.ToArray(); + } + + // 提交作业 + var commitResult = _resourceManager.AddResource( + user.ID, ResourceTypes.Compression, ResourcePurpose.Homework, + file.FileName, fileData, examId); + if (!commitResult.IsSuccessful) + { + logger.Error($"提交作业时出错: {commitResult.Error.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"提交作业失败: {commitResult.Error.Message}"); + } + + var commit = commitResult.Value; + + logger.Info($"用户 {userName} 成功提交实验 {examId} 的作业,Commit ID: {commit.ID}"); + return CreatedAtAction(nameof(GetCommitsByExamId), new { examId = examId }, commit); + } + catch (Exception ex) + { + logger.Error($"提交实验 {examId} 作业时出错: {ex.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"提交作业失败: {ex.Message}"); + } + } + + /// + /// 获取用户在指定实验中的提交记录 + /// + /// 实验ID + /// 提交记录列表 + [Authorize] + [HttpGet("commits/{examId}")] + [EnableCors("Users")] + [ProducesResponseType(typeof(Resource[]), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult GetCommitsByExamId(string examId) + { + if (string.IsNullOrWhiteSpace(examId)) + return BadRequest("实验ID不能为空"); + + try + { + // 获取当前用户信息 + var userName = User.Identity?.Name; + if (string.IsNullOrEmpty(userName)) + return Unauthorized("无法获取用户信息"); + + var userResult = _userManager.GetUserByName(userName); + if (!userResult.IsSuccessful || !userResult.Value.HasValue) + return Unauthorized("用户不存在"); + + var user = userResult.Value.Value; + + // 检查实验是否存在 + var examResult = _examManager.GetExamByID(examId); + if (!examResult.IsSuccessful) + { + logger.Error($"检查实验是否存在时出错: {examResult.Error.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"检查实验失败: {examResult.Error.Message}"); + } + + if (!examResult.Value.HasValue) + { + logger.Warn($"实验不存在: {examId}"); + return NotFound($"实验 {examId} 不存在"); + } + + // 获取用户的提交记录 + var commitsResult = _resourceManager.GetResourceListByType( + ResourceTypes.Compression, ResourcePurpose.Homework, examId); + if (!commitsResult.IsSuccessful) + { + logger.Error($"获取提交记录时出错: {commitsResult.Error.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"获取提交记录失败: {commitsResult.Error.Message}"); + } + + var commits = commitsResult.Value; + + logger.Info($"成功获取用户 {userName} 在实验 {examId} 中的提交记录,共 {commits.Length} 条"); + return Ok(commits); + } + catch (Exception ex) + { + logger.Error($"获取实验 {examId} 提交记录时出错: {ex.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"获取提交记录失败: {ex.Message}"); + } + } + + /// + /// 删除提交记录 + /// + /// 提交记录ID + /// 删除结果 + [Authorize] + [HttpDelete("commit/{commitId}")] + [EnableCors("Users")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult DeleteCommit(string commitId) + { + if (!Guid.TryParse(commitId, out _)) + return BadRequest("提交记录ID格式不正确"); + + try + { + // 获取当前用户信息 + var userName = User.Identity?.Name; + if (string.IsNullOrEmpty(userName)) + return Unauthorized("无法获取用户信息"); + + var userResult = _userManager.GetUserByName(userName); + if (!userResult.IsSuccessful || !userResult.Value.HasValue) + return Unauthorized("用户不存在"); + + var user = userResult.Value.Value; + + // 检查是否是管理员 + var isAdmin = user.Permission == UserPermission.Admin; + + // 如果不是管理员,检查提交记录是否属于当前用户 + if (!isAdmin) + { + var commitResult = _resourceManager.GetResourceById(commitId); + if (!commitResult.HasValue) + { + logger.Warn($"提交记录不存在: {commitId}"); + return NotFound($"提交记录 {commitId} 不存在"); + } + + var commit = commitResult.Value; + if (commit.UserID != user.ID) + { + logger.Warn($"用户 {userName} 尝试删除不属于自己的提交记录: {commitId}"); + return Forbid("您只能删除自己的提交记录"); + } + } + + // 执行删除 + var deleteResult = _resourceManager.DeleteResource(commitId); + if (!deleteResult) + { + logger.Warn($"提交记录不存在: {commitId}"); + return NotFound($"提交记录 {commitId} 不存在"); + } + + logger.Info($"用户 {userName} 成功删除提交记录: {commitId}"); + return Ok($"提交记录 {commitId} 已成功删除"); + } + catch (Exception ex) + { + logger.Error($"删除提交记录 {commitId} 时出错: {ex.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"删除提交记录失败: {ex.Message}"); + } + } } /// @@ -258,7 +482,7 @@ public class ExamInfo /// public bool IsVisibleToUsers { get; set; } = true; - public ExamInfo(Database.Exam exam) + public ExamInfo(Exam exam) { ID = exam.ID; Name = exam.Name; diff --git a/server/src/Controllers/JtagController.cs b/server/src/Controllers/JtagController.cs index 863b53b..177992f 100644 --- a/server/src/Controllers/JtagController.cs +++ b/server/src/Controllers/JtagController.cs @@ -140,7 +140,7 @@ public class JtagController : ControllerBase [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public IResult DownloadBitstream(string address, int port, int bitstreamId, CancellationToken cancelToken) + public IResult DownloadBitstream(string address, int port, string bitstreamId, CancellationToken cancelToken) { logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port} using bitstream ID: {bitstreamId}"); diff --git a/server/src/Controllers/ResourceController.cs b/server/src/Controllers/ResourceController.cs index 2e70482..0a501e9 100644 --- a/server/src/Controllers/ResourceController.cs +++ b/server/src/Controllers/ResourceController.cs @@ -40,15 +40,15 @@ public class ResourceController : ControllerBase [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task AddResource([FromForm] AddResourceRequest request, IFormFile file) { - if (string.IsNullOrWhiteSpace(request.ResourceType) || string.IsNullOrWhiteSpace(request.ResourcePurpose) || file == null) + if (string.IsNullOrWhiteSpace(request.ResourceType) || file == null) return BadRequest("资源类型、资源用途和文件不能为空"); // 验证资源用途 - if (request.ResourcePurpose != Resource.ResourcePurposes.Template && request.ResourcePurpose != Resource.ResourcePurposes.User) + if (request.ResourcePurpose != ResourcePurpose.Template && request.ResourcePurpose != ResourcePurpose.User) return BadRequest($"无效的资源用途: {request.ResourcePurpose}"); // 模板资源需要管理员权限 - if (request.ResourcePurpose == Resource.ResourcePurposes.Template && !User.IsInRole("Admin")) + if (request.ResourcePurpose == ResourcePurpose.Template && !User.IsInRole("Admin")) return Forbid("只有管理员可以添加模板资源"); try @@ -85,10 +85,10 @@ public class ResourceController : ControllerBase var resource = result.Value; var resourceInfo = new ResourceInfo { - ID = resource.ID, + ID = resource.ID.ToString(), Name = resource.ResourceName, Type = resource.ResourceType, - Purpose = resource.ResourcePurpose, + Purpose = resource.Purpose, UploadTime = resource.UploadTime, ExamID = resource.ExamID, MimeType = resource.MimeType @@ -117,7 +117,10 @@ public class ResourceController : ControllerBase [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) + public IActionResult GetResourceList( + [FromQuery] string? examId = null, + [FromQuery] string? resourceType = null, + [FromQuery] ResourcePurpose? resourcePurpose = null) { try { @@ -132,52 +135,53 @@ public class ResourceController : ControllerBase var user = userResult.Value.Value; - // 普通用户只能查看自己的资源和模板资源 - Guid? userId = null; - if (!User.IsInRole("Admin")) + Result> result; + // 管理员 + if (user.Permission == UserPermission.Admin) { - // 如果指定了用户资源用途,则只查看自己的资源 - if (resourcePurpose == Resource.ResourcePurposes.User) - { - userId = user.ID; - } - // 如果指定了模板资源用途,则不限制用户ID - else if (resourcePurpose == Resource.ResourcePurposes.Template) - { - userId = null; - } - // 如果没有指定用途,则查看自己的用户资源和所有模板资源 - else - { - // 这种情况下需要分别查询并合并结果 - var userResourcesResult = _resourceManager.GetFullResourceList(examId, resourceType, Resource.ResourcePurposes.User, user.ID); - var templateResourcesResult = _resourceManager.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); - } + result = _resourceManager.GetFullResourceList(examId, resourceType, resourcePurpose); } + // 用户 + else if (resourcePurpose == ResourcePurpose.User) + { + result = _resourceManager.GetFullResourceList(examId, resourceType, resourcePurpose, user.ID); + } + // 模板 + else if (resourcePurpose == ResourcePurpose.Template) + { + result = _resourceManager.GetFullResourceList(examId, resourceType, resourcePurpose); + } + // 其他 + else + { + // 这种情况下需要分别查询并合并结果 + var userResourcesResult = _resourceManager.GetFullResourceList( + examId, resourceType, ResourcePurpose.User, user.ID); + var templateResourcesResult = _resourceManager.GetFullResourceList( + examId, resourceType, ResourcePurpose.Template, null); - var result = _resourceManager.GetFullResourceList(examId, resourceType, resourcePurpose, userId); + 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.ToString(), + Name = r.ResourceName, + Type = r.ResourceType, + Purpose = r.Purpose, + UploadTime = r.UploadTime, + ExamID = r.ExamID, + MimeType = r.MimeType + }).ToArray(); + + logger.Info($"成功获取资源列表,共 {mergedResourceInfos.Length} 个资源"); + return Ok(mergedResourceInfos); + } if (!result.IsSuccessful) { @@ -187,10 +191,10 @@ public class ResourceController : ControllerBase var resources = result.Value.Select(r => new ResourceInfo { - ID = r.ID, + ID = r.ID.ToString(), Name = r.ResourceName, Type = r.ResourceType, - Purpose = r.ResourcePurpose, + Purpose = r.Purpose, UploadTime = r.UploadTime, ExamID = r.ExamID, MimeType = r.MimeType @@ -217,7 +221,7 @@ public class ResourceController : ControllerBase [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public IActionResult GetResourceById(int resourceId) + public IActionResult GetResourceById(string resourceId) { try { @@ -267,7 +271,7 @@ public class ResourceController : ControllerBase [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public IActionResult DeleteResource(int resourceId) + public IActionResult DeleteResource(string resourceId) { try { @@ -301,7 +305,7 @@ public class ResourceController : ControllerBase // 权限检查:管理员可以删除所有资源,普通用户只能删除自己的用户资源 if (!User.IsInRole("Admin")) { - if (resource.ResourcePurpose == Resource.ResourcePurposes.Template) + if (resource.Purpose == ResourcePurpose.Template) return Forbid("普通用户不能删除模板资源"); if (resource.UserID != user.ID) @@ -333,7 +337,7 @@ public class ResourceController : ControllerBase /// /// 资源ID /// - public int ID { get; set; } + public required string ID { get; set; } /// /// 资源名称 @@ -348,7 +352,7 @@ public class ResourceController : ControllerBase /// /// 资源用途(template/user) /// - public required string Purpose { get; set; } + public required ResourcePurpose Purpose { get; set; } /// /// 上传时间 @@ -379,7 +383,7 @@ public class ResourceController : ControllerBase /// /// 资源用途(template/user) /// - public required string ResourcePurpose { get; set; } + public required ResourcePurpose ResourcePurpose { get; set; } /// /// 所属实验ID(可选) diff --git a/server/src/Database/Connection.cs b/server/src/Database/Connection.cs index 1abbe00..125b84f 100644 --- a/server/src/Database/Connection.cs +++ b/server/src/Database/Connection.cs @@ -57,7 +57,7 @@ public class AppDataConnection : DataConnection Name = "Admin", EMail = "selfconfusion@gmail.com", Password = "12345678", - Permission = Database.User.UserPermission.Admin, + Permission = Database.UserPermission.Admin, }; this.Insert(user); logger.Info("默认管理员用户已创建"); diff --git a/server/src/Database/ExamManager.cs b/server/src/Database/ExamManager.cs index 170f385..477280c 100644 --- a/server/src/Database/ExamManager.cs +++ b/server/src/Database/ExamManager.cs @@ -30,7 +30,7 @@ public class ExamManager try { // 检查实验ID是否已存在 - var existingExam = _db.ExamTable.Where(e => e.ID == id).FirstOrDefault(); + var existingExam = _db.ExamTable.Where(e => e.ID.ToString() == id).FirstOrDefault(); if (existingExam != null) { logger.Error($"实验ID已存在: {id}"); @@ -82,28 +82,28 @@ public class ExamManager if (name != null) { - result += _db.ExamTable.Where(e => e.ID == id).Set(e => e.Name, name).Update(); + result += _db.ExamTable.Where(e => e.ID.ToString() == id).Set(e => e.Name, name).Update(); } if (description != null) { - result += _db.ExamTable.Where(e => e.ID == id).Set(e => e.Description, description).Update(); + result += _db.ExamTable.Where(e => e.ID.ToString() == id).Set(e => e.Description, description).Update(); } if (tags != null) { var tagsString = string.Join(",", tags.Where(tag => !string.IsNullOrWhiteSpace(tag)).Select(tag => tag.Trim())); - result += _db.ExamTable.Where(e => e.ID == id).Set(e => e.Tags, tagsString).Update(); + result += _db.ExamTable.Where(e => e.ID.ToString() == id).Set(e => e.Tags, tagsString).Update(); } if (difficulty.HasValue) { - result += _db.ExamTable.Where(e => e.ID == id).Set(e => e.Difficulty, Math.Max(1, Math.Min(5, difficulty.Value))).Update(); + result += _db.ExamTable.Where(e => e.ID.ToString() == id).Set(e => e.Difficulty, Math.Max(1, Math.Min(5, difficulty.Value))).Update(); } if (isVisibleToUsers.HasValue) { - result += _db.ExamTable.Where(e => e.ID == id).Set(e => e.IsVisibleToUsers, isVisibleToUsers.Value).Update(); + result += _db.ExamTable.Where(e => e.ID.ToString() == id).Set(e => e.IsVisibleToUsers, isVisibleToUsers.Value).Update(); } // 更新时间 - _db.ExamTable.Where(e => e.ID == id).Set(e => e.UpdatedTime, DateTime.Now).Update(); + _db.ExamTable.Where(e => e.ID.ToString() == id).Set(e => e.UpdatedTime, DateTime.Now).Update(); logger.Info($"实验已更新: {id},更新记录数: {result}"); return new(result); @@ -133,7 +133,7 @@ public class ExamManager /// 包含实验信息的结果,如果未找到则返回空 public Result> GetExamByID(string examId) { - var exams = _db.ExamTable.Where(exam => exam.ID == examId).ToArray(); + var exams = _db.ExamTable.Where(exam => exam.ID.ToString() == examId).ToArray(); if (exams.Length > 1) { diff --git a/server/src/Database/ResourceManager.cs b/server/src/Database/ResourceManager.cs index f540678..5df3691 100644 --- a/server/src/Database/ResourceManager.cs +++ b/server/src/Database/ResourceManager.cs @@ -114,7 +114,7 @@ public class ResourceManager /// MIME类型(可选,将根据文件扩展名自动确定) /// 创建的资源 public Result AddResource( - Guid userId, string resourceType, string resourcePurpose, + Guid userId, string resourceType, ResourcePurpose resourcePurpose, string resourceName, byte[] data, string? examId = null, string? mimeType = null) { try @@ -130,7 +130,7 @@ public class ResourceManager // 如果指定了实验ID,验证实验是否存在 if (!string.IsNullOrEmpty(examId)) { - var exam = _db.ExamTable.Where(e => e.ID == examId).FirstOrDefault(); + var exam = _db.ExamTable.Where(e => e.ID.ToString() == examId).FirstOrDefault(); if (exam == null) { logger.Error($"实验不存在: {examId}"); @@ -139,8 +139,8 @@ public class ResourceManager } // 验证资源用途 - if (resourcePurpose != Resource.ResourcePurposes.Template && - resourcePurpose != Resource.ResourcePurposes.User) + if (resourcePurpose != ResourcePurpose.Template && + resourcePurpose != ResourcePurpose.User) { logger.Error($"无效的资源用途: {resourcePurpose}"); return new(new Exception($"无效的资源用途: {resourcePurpose}")); @@ -174,7 +174,7 @@ public class ResourceManager UserID = userId, ExamID = examId, ResourceType = resourceType, - ResourcePurpose = resourcePurpose, + Purpose = resourcePurpose, ResourceName = resourceName, Path = duplicateResource == null ? Path.Combine(resourceType, nowTime.ToString("yyyyMMddHH"), resourceName) : @@ -184,8 +184,7 @@ public class ResourceManager UploadTime = nowTime }; - var insertedId = _db.InsertWithIdentity(resource); - resource.ID = Convert.ToInt32(insertedId); + var insertedId = _db.Insert(resource); var writeRet = WriteBytesToPath(resource.Path, data); if (writeRet.IsSuccessful && writeRet.Value) @@ -217,7 +216,11 @@ public class ResourceManager /// 用户ID(可选) /// /// 资源信息列表 - public Result<(int ID, string Name)[]> GetResourceList(string resourceType, string? examId = null, string? resourcePurpose = null, Guid? userId = null) + public Result<(string ID, string Name)[]> GetResourceListByType( + string resourceType, + ResourcePurpose? resourcePurpose = null, + string? examId = null, + Guid? userId = null) { try { @@ -230,7 +233,7 @@ public class ResourceManager if (resourcePurpose != null) { - query = query.Where(r => r.ResourcePurpose == resourcePurpose); + query = query.Where(r => r.Purpose == resourcePurpose); } if (userId != null) @@ -242,10 +245,10 @@ public class ResourceManager .Select(r => new { r.ID, r.ResourceName }) .ToArray(); - var result = resources.Select(r => (r.ID, r.ResourceName)).ToArray(); + var result = resources.Select(r => (r.ID.ToString(), r.ResourceName)).ToArray(); logger.Info($"获取资源列表: {resourceType}" + (examId != null ? $"/{examId}" : "") + - (resourcePurpose != null ? $"/{resourcePurpose}" : "") + + ($"/{resourcePurpose.ToString()}") + (userId != null ? $"/{userId}" : "") + $",共 {result.Length} 个资源"); return new(result); @@ -265,7 +268,11 @@ public class ResourceManager /// 资源用途(可选) /// 用户ID(可选) /// 完整的资源对象列表 - public Result> GetFullResourceList(string? examId = null, string? resourceType = null, string? resourcePurpose = null, Guid? userId = null) + public Result> GetFullResourceList( + string? examId = null, + string? resourceType = null, + ResourcePurpose? resourcePurpose = null, + Guid? userId = null) { try { @@ -283,7 +290,7 @@ public class ResourceManager if (resourcePurpose != null) { - query = query.Where(r => r.ResourcePurpose == resourcePurpose); + query = query.Where(r => r.Purpose == resourcePurpose); } if (userId != null) @@ -295,7 +302,7 @@ public class ResourceManager logger.Info($"获取完整资源列表" + (examId != null ? $" [实验: {examId}]" : "") + (resourceType != null ? $" [类型: {resourceType}]" : "") + - (resourcePurpose != null ? $" [用途: {resourcePurpose}]" : "") + + ($" [用途: {resourcePurpose.ToString()}]") + (userId != null ? $" [用户: {userId}]" : "") + $",共 {resources.Count} 个资源"); return new(resources); @@ -312,26 +319,18 @@ public class ResourceManager /// /// 资源ID /// 资源数据 - public Result> GetResourceById(int resourceId) + public Optional GetResourceById(string resourceId) { - try - { - var resource = _db.ResourceTable.Where(r => r.ID == resourceId).FirstOrDefault(); + var resource = _db.ResourceTable.Where(r => r.ID.ToString() == resourceId).FirstOrDefault(); - if (resource == null) - { - logger.Info($"未找到资源: {resourceId}"); - return new(Optional.None); - } - - logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})"); - return new(resource); - } - catch (Exception ex) + if (resource == null) { - logger.Error($"获取资源时出错: {ex.Message}"); - return new(ex); + logger.Info($"未找到资源: {resourceId}"); + return new(null); } + + logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})"); + return new(resource); } /// @@ -339,11 +338,11 @@ public class ResourceManager /// /// 资源ID /// 删除的记录数 - public Result DeleteResource(int resourceId) + public Result DeleteResource(string resourceId) { try { - var result = _db.ResourceTable.Where(r => r.ID == resourceId).Delete(); + var result = _db.ResourceTable.Where(r => r.ID.ToString() == resourceId).Delete(); logger.Info($"资源已删除: {resourceId},删除记录数: {result}"); return new(result); } diff --git a/server/src/Database/Type.cs b/server/src/Database/Type.cs index 2263fc5..8989bf7 100644 --- a/server/src/Database/Type.cs +++ b/server/src/Database/Type.cs @@ -4,6 +4,22 @@ using LinqToDB.Mapping; namespace Database; +/// +/// 用户权限枚举 +/// +public enum UserPermission +{ + /// + /// 管理员权限,可以管理用户和实验板 + /// + Admin, + + /// + /// 普通用户权限,只能使用实验板 + /// + Normal, +} + /// /// 用户类,表示用户信息 /// @@ -50,22 +66,27 @@ public class User /// [Nullable] public DateTime? BoardExpireTime { get; set; } +} + +/// +/// FPGA 板子状态枚举 +/// +public enum BoardStatus +{ + /// + /// 未启用状态,无法被使用 + /// + Disabled, /// - /// 用户权限枚举 + /// 繁忙状态,正在被用户使用 /// - public enum UserPermission - { - /// - /// 管理员权限,可以管理用户和实验板 - /// - Admin, + Busy, - /// - /// 普通用户权限,只能使用实验板 - /// - Normal, - } + /// + /// 可用状态,可以被分配给用户 + /// + Available, } /// @@ -127,26 +148,6 @@ public class Board [NotNull] public string FirmVersion { get; set; } = "1.0.0"; - /// - /// FPGA 板子状态枚举 - /// - public enum BoardStatus - { - /// - /// 未启用状态,无法被使用 - /// - Disabled, - - /// - /// 繁忙状态,正在被用户使用 - /// - Busy, - - /// - /// 可用状态,可以被分配给用户 - /// - Available, - } } /// @@ -227,6 +228,60 @@ public class Exam } } +/// +/// 资源类型枚举 +/// +public static class ResourceTypes +{ + /// + /// 图片资源类型 + /// + public const string Images = "images"; + + /// + /// Markdown文档资源类型 + /// + public const string Markdown = "markdown"; + + /// + /// 比特流文件资源类型 + /// + public const string Bitstream = "bitstream"; + + /// + /// 原理图资源类型 + /// + public const string Diagram = "diagram"; + + /// + /// 项目文件资源类型 + /// + public const string Project = "project"; + + /// + /// 压缩文件资源类型 + /// + public const string Compression = "compression"; +} + +public enum ResourcePurpose : int +{ + /// + /// 模板资源,通常由管理员上传,供用户参考 + /// + Template, + + /// + /// 用户上传的资源 + /// + User, + + /// + /// 用户提交的作业 + /// + Homework +} + /// /// 资源类,统一管理实验资源、用户比特流等各类资源 /// @@ -235,8 +290,8 @@ public class Resource /// /// 资源的唯一标识符 /// - [PrimaryKey, Identity] - public int ID { get; set; } + [PrimaryKey] + public Guid ID { get; set; } = Guid.NewGuid(); /// /// 上传资源的用户ID @@ -260,7 +315,7 @@ public class Resource /// 资源用途:template(模板)或 user(用户上传) /// [NotNull] - public required string ResourcePurpose { get; set; } + public required ResourcePurpose Purpose { get; set; } /// /// 资源名称(包含文件扩展名) @@ -292,50 +347,4 @@ public class Resource [NotNull] public string MimeType { get; set; } = "application/octet-stream"; - /// - /// 资源类型枚举 - /// - public static class ResourceTypes - { - /// - /// 图片资源类型 - /// - public const string Images = "images"; - - /// - /// Markdown文档资源类型 - /// - public const string Markdown = "markdown"; - - /// - /// 比特流文件资源类型 - /// - public const string Bitstream = "bitstream"; - - /// - /// 原理图资源类型 - /// - public const string Diagram = "diagram"; - - /// - /// 项目文件资源类型 - /// - public const string Project = "project"; - } - - /// - /// 资源用途枚举 - /// - public static class ResourcePurposes - { - /// - /// 模板资源,通常由管理员上传,供用户参考 - /// - public const string Template = "template"; - - /// - /// 用户上传的资源 - /// - public const string User = "user"; - } } diff --git a/server/src/Database/UserManager.cs b/server/src/Database/UserManager.cs index 72fb290..72c68ae 100644 --- a/server/src/Database/UserManager.cs +++ b/server/src/Database/UserManager.cs @@ -29,7 +29,7 @@ public class UserManager Name = name, EMail = email, Password = password, - Permission = Database.User.UserPermission.Normal, + Permission = UserPermission.Normal, }; var result = _db.Insert(user); logger.Info($"新用户已添加: {name} ({email})"); @@ -142,7 +142,7 @@ public class UserManager // 更新板子的用户绑定信息 var boardResult = _db.BoardTable .Where(b => b.ID == boardId) - .Set(b => b.Status, Board.BoardStatus.Busy) + .Set(b => b.Status, BoardStatus.Busy) .Set(b => b.OccupiedUserID, userId) .Set(b => b.OccupiedUserName, user.Name) .Update(); @@ -175,7 +175,7 @@ public class UserManager { boardResult = _db.BoardTable .Where(b => b.ID == boardId) - .Set(b => b.Status, Board.BoardStatus.Available) + .Set(b => b.Status, BoardStatus.Available) .Set(b => b.OccupiedUserID, Guid.Empty) .Set(b => b.OccupiedUserName, (string?)null) .Update(); @@ -236,7 +236,7 @@ public class UserManager BoardName = name, IpAddr = AllocateIpAddr(), MacAddr = AllocateMacAddr(), - Status = Database.Board.BoardStatus.Disabled, + Status = BoardStatus.Disabled, }; var result = _db.Insert(board); logger.Info($"新实验板已添加: {name} ({board.IpAddr}:{board.MacAddr})"); @@ -375,7 +375,7 @@ public class UserManager public Optional GetAvailableBoard(Guid userId, DateTime expireTime) { var boards = _db.BoardTable.Where( - (board) => board.Status == Database.Board.BoardStatus.Available + (board) => board.Status == BoardStatus.Available ).ToArray(); if (boards.Length == 0) @@ -397,7 +397,7 @@ public class UserManager // 更新板子状态和用户绑定信息 _db.BoardTable .Where(target => target.ID == board.ID) - .Set(target => target.Status, Board.BoardStatus.Busy) + .Set(target => target.Status, BoardStatus.Busy) .Set(target => target.OccupiedUserID, userId) .Set(target => target.OccupiedUserName, user.Name) .Update(); @@ -409,7 +409,7 @@ public class UserManager .Set(u => u.BoardExpireTime, expireTime) .Update(); - board.Status = Database.Board.BoardStatus.Busy; + board.Status = BoardStatus.Busy; board.OccupiedUserID = userId; board.OccupiedUserName = user.Name; @@ -445,7 +445,7 @@ public class UserManager /// [TODO:parameter] /// [TODO:parameter] /// [TODO:return] - public int UpdateBoardStatus(Guid boardId, Board.BoardStatus newStatus) + public int UpdateBoardStatus(Guid boardId, BoardStatus newStatus) { var result = _db.BoardTable .Where(b => b.ID == boardId) diff --git a/src/APIClient.ts b/src/APIClient.ts index a61fe6b..38edd7d 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -2833,6 +2833,267 @@ export class ExamClient { } return Promise.resolve(null as any); } + + /** + * 提交作业 + * @param examId 实验ID + * @param file (optional) 提交的文件 + * @return 提交结果 + */ + submitCommit(examId: string, file: FileParameter | undefined, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/Exam/commit/{examId}"; + if (examId === undefined || examId === null) + throw new Error("The parameter 'examId' must be defined."); + url_ = url_.replace("{examId}", encodeURIComponent("" + examId)); + url_ = url_.replace(/[?&]$/, ""); + + const content_ = new FormData(); + if (file === null || file === undefined) + throw new Error("The parameter 'file' cannot be null."); + else + content_.append("file", file.data, file.fileName ? file.fileName : "file"); + + let options_: AxiosRequestConfig = { + data: content_, + method: "POST", + url: url_, + headers: { + "Accept": "application/json" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processSubmitCommit(_response); + }); + } + + protected processSubmitCommit(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 201) { + const _responseText = response.data; + let result201: any = null; + let resultData201 = _responseText; + result201 = Commit.fromJS(resultData201); + return Promise.resolve(result201); + + } else if (status === 400) { + const _responseText = response.data; + let result400: any = null; + let resultData400 = _responseText; + result400 = ProblemDetails.fromJS(resultData400); + return throwException("A server side error occurred.", status, _responseText, _headers, result400); + + } else if (status === 401) { + const _responseText = response.data; + let result401: any = null; + let resultData401 = _responseText; + result401 = ProblemDetails.fromJS(resultData401); + return throwException("A server side error occurred.", status, _responseText, _headers, result401); + + } else if (status === 404) { + const _responseText = response.data; + let result404: any = null; + let resultData404 = _responseText; + result404 = ProblemDetails.fromJS(resultData404); + return throwException("A server side error occurred.", status, _responseText, _headers, result404); + + } else if (status === 500) { + const _responseText = response.data; + return throwException("A server side error occurred.", status, _responseText, _headers); + + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + /** + * 获取用户在指定实验中的提交记录 + * @param examId 实验ID + * @return 提交记录列表 + */ + getCommitsByExamId(examId: string, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/Exam/commits/{examId}"; + if (examId === undefined || examId === null) + throw new Error("The parameter 'examId' must be defined."); + url_ = url_.replace("{examId}", encodeURIComponent("" + examId)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + method: "GET", + url: url_, + headers: { + "Accept": "application/json" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processGetCommitsByExamId(_response); + }); + } + + protected processGetCommitsByExamId(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200) { + const _responseText = response.data; + let result200: any = null; + let resultData200 = _responseText; + if (Array.isArray(resultData200)) { + result200 = [] as any; + for (let item of resultData200) + result200!.push(Commit.fromJS(item)); + } + else { + result200 = null; + } + return Promise.resolve(result200); + + } else if (status === 400) { + const _responseText = response.data; + let result400: any = null; + let resultData400 = _responseText; + result400 = ProblemDetails.fromJS(resultData400); + return throwException("A server side error occurred.", status, _responseText, _headers, result400); + + } else if (status === 401) { + const _responseText = response.data; + let result401: any = null; + let resultData401 = _responseText; + result401 = ProblemDetails.fromJS(resultData401); + return throwException("A server side error occurred.", status, _responseText, _headers, result401); + + } else if (status === 404) { + const _responseText = response.data; + let result404: any = null; + let resultData404 = _responseText; + result404 = ProblemDetails.fromJS(resultData404); + return throwException("A server side error occurred.", status, _responseText, _headers, result404); + + } else if (status === 500) { + const _responseText = response.data; + return throwException("A server side error occurred.", status, _responseText, _headers); + + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + /** + * 删除提交记录 + * @param commitId 提交记录ID + * @return 删除结果 + */ + deleteCommit(commitId: string, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/Exam/commit/{commitId}"; + if (commitId === undefined || commitId === null) + throw new Error("The parameter 'commitId' must be defined."); + url_ = url_.replace("{commitId}", encodeURIComponent("" + commitId)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + method: "DELETE", + url: url_, + headers: { + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processDeleteCommit(_response); + }); + } + + protected processDeleteCommit(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200) { + const _responseText = response.data; + return Promise.resolve(null as any); + + } else if (status === 400) { + const _responseText = response.data; + let result400: any = null; + let resultData400 = _responseText; + result400 = ProblemDetails.fromJS(resultData400); + return throwException("A server side error occurred.", status, _responseText, _headers, result400); + + } else if (status === 401) { + const _responseText = response.data; + let result401: any = null; + let resultData401 = _responseText; + result401 = ProblemDetails.fromJS(resultData401); + return throwException("A server side error occurred.", status, _responseText, _headers, result401); + + } else if (status === 403) { + const _responseText = response.data; + let result403: any = null; + let resultData403 = _responseText; + result403 = ProblemDetails.fromJS(resultData403); + return throwException("A server side error occurred.", status, _responseText, _headers, result403); + + } else if (status === 404) { + const _responseText = response.data; + let result404: any = null; + let resultData404 = _responseText; + result404 = ProblemDetails.fromJS(resultData404); + return throwException("A server side error occurred.", status, _responseText, _headers, result404); + + } else if (status === 500) { + const _responseText = response.data; + return throwException("A server side error occurred.", status, _responseText, _headers); + + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } } export class HdmiVideoStreamClient { @@ -3224,7 +3485,7 @@ export class JtagClient { * @param bitstreamId (optional) 比特流ID * @return 进度跟踪TaskID */ - downloadBitstream(address: string | undefined, port: number | undefined, bitstreamId: number | undefined, cancelToken?: CancelToken): Promise { + downloadBitstream(address: string | undefined, port: number | undefined, bitstreamId: string | undefined, cancelToken?: CancelToken): Promise { let url_ = this.baseUrl + "/api/Jtag/DownloadBitstream?"; if (address === null) throw new Error("The parameter 'address' cannot be null."); @@ -6475,7 +6736,7 @@ export class ResourceClient { * @param resourceId 资源ID * @return 资源文件 */ - getResourceById(resourceId: number, cancelToken?: CancelToken): Promise { + getResourceById(resourceId: string, cancelToken?: CancelToken): Promise { let url_ = this.baseUrl + "/api/Resource/{resourceId}"; if (resourceId === undefined || resourceId === null) throw new Error("The parameter 'resourceId' must be defined."); @@ -6554,7 +6815,7 @@ export class ResourceClient { * @param resourceId 资源ID * @return 删除结果 */ - deleteResource(resourceId: number, cancelToken?: CancelToken): Promise { + deleteResource(resourceId: string, cancelToken?: CancelToken): Promise { let url_ = this.baseUrl + "/api/Resource/{resourceId}"; if (resourceId === undefined || resourceId === null) throw new Error("The parameter 'resourceId' must be defined."); @@ -8069,6 +8330,74 @@ export interface IExamDto { isVisibleToUsers: boolean; } +export class Commit implements ICommit { + /** 资源的唯一标识符 */ + id!: string; + /** 上传资源的用户ID */ + userID!: string; + /** 所属实验ID */ + examID?: string | undefined; + type!: CommitType; + resourceID!: string; + createdAt!: Date; + + constructor(data?: ICommit) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data?: any) { + if (_data) { + this.id = _data["id"]; + this.userID = _data["userID"]; + this.examID = _data["examID"]; + this.type = _data["type"]; + this.resourceID = _data["resourceID"]; + this.createdAt = _data["createdAt"] ? new Date(_data["createdAt"].toString()) : undefined; + } + } + + static fromJS(data: any): Commit { + data = typeof data === 'object' ? data : {}; + let result = new Commit(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["userID"] = this.userID; + data["examID"] = this.examID; + data["type"] = this.type; + data["resourceID"] = this.resourceID; + data["createdAt"] = this.createdAt ? this.createdAt.toISOString() : undefined; + return data; + } +} + +export interface ICommit { + /** 资源的唯一标识符 */ + id: string; + /** 上传资源的用户ID */ + userID: string; + /** 所属实验ID */ + examID?: string | undefined; + type: CommitType; + resourceID: string; + createdAt: Date; +} + +export enum CommitType { + Homework = 0, + Project = 1, + Markdown = 2, +} + export class HdmiVideoStreamEndpoint implements IHdmiVideoStreamEndpoint { boardId!: string; mjpegUrl!: string; @@ -8587,7 +8916,7 @@ export interface IOscilloscopeDataResponse { /** 资源信息类 */ export class ResourceInfo implements IResourceInfo { /** 资源ID */ - id!: number; + id!: string; /** 资源名称 */ name!: string; /** 资源类型 */ @@ -8645,7 +8974,7 @@ export class ResourceInfo implements IResourceInfo { /** 资源信息类 */ export interface IResourceInfo { /** 资源ID */ - id: number; + id: string; /** 资源名称 */ name: string; /** 资源类型 */ diff --git a/src/views/Exam/ExamEditModal.vue b/src/views/Exam/ExamEditModal.vue index 0d50765..2501c79 100644 --- a/src/views/Exam/ExamEditModal.vue +++ b/src/views/Exam/ExamEditModal.vue @@ -473,7 +473,7 @@ const canCreateExam = computed(() => { editExamInfo.value.id.trim() !== "" && editExamInfo.value.name.trim() !== "" && editExamInfo.value.description.trim() !== "" && - uploadFiles.value.mdFile !== null + (uploadFiles.value.mdFile !== null || mode.value === "edit") ); }); diff --git a/src/views/Exam/ExamInfoModal.vue b/src/views/Exam/ExamInfoModal.vue index cd8f8db..77569eb 100644 --- a/src/views/Exam/ExamInfoModal.vue +++ b/src/views/Exam/ExamInfoModal.vue @@ -166,9 +166,20 @@

提交历史

-
+
暂无提交记录
+
+
    +
  • Register
  • +
  • Choose plan
  • +
  • Purchase
  • +
  • Receive Product
  • +
+
@@ -239,13 +250,19 @@