From b95a61c532bda2b5415e9d4a6eb0b7c8954843c2 Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Sun, 10 Aug 2025 20:13:44 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E7=9B=B8=E5=85=B3=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/Program.cs | 11 +- server/src/Common/Global.cs | 3 +- server/src/Controllers/DataController.cs | 102 +- server/src/Controllers/DebuggerController.cs | 150 +- server/src/Controllers/ExamController.cs | 309 ++-- .../Controllers/HdmiVideoStreamController.cs | 22 +- server/src/Controllers/JtagController.cs | 40 +- .../Controllers/LogicAnalyzerController.cs | 111 +- .../src/Controllers/OscilloscopeController.cs | 143 +- server/src/Controllers/ResourceController.cs | 607 ++++---- .../src/Controllers/VideoStreamController.cs | 73 +- server/src/Database.cs | 1266 ----------------- server/src/Database/Connection.cs | 98 ++ server/src/Database/ExamManager.cs | 154 ++ server/src/Database/ResourceManager.cs | 357 +++++ server/src/Database/Type.cs | 341 +++++ server/src/Database/UserManager.cs | 458 ++++++ server/src/Hubs/JtagHub.cs | 14 +- .../Services/HttpHdmiVideoStreamService.cs | 34 +- server/src/Services/HttpVideoStreamService.cs | 24 +- 20 files changed, 2252 insertions(+), 2065 deletions(-) delete mode 100644 server/src/Database.cs create mode 100644 server/src/Database/Connection.cs create mode 100644 server/src/Database/ExamManager.cs create mode 100644 server/src/Database/ResourceManager.cs create mode 100644 server/src/Database/Type.cs create mode 100644 server/src/Database/UserManager.cs diff --git a/server/Program.cs b/server/Program.cs index a0f9858..3116ce5 100644 --- a/server/Program.cs +++ b/server/Program.cs @@ -62,7 +62,7 @@ try IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes("my secret key 1234567890my secret key 1234567890")), }; - options.Authority = $"http://{Global.localhost}:5000"; + options.Authority = $"http://{Global.LocalHost}:5000"; options.RequireHttpsMetadata = false; }); // Add JWT Token Authorization Policy @@ -152,6 +152,11 @@ try builder.Services.AddSingleton(); builder.Services.AddHostedService(provider => provider.GetRequiredService()); + // 添加数据库资源管理器服务 + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + // Application Settings var app = builder.Build(); // Configure the HTTP request pipeline. @@ -209,7 +214,7 @@ try settings.PostProcess = (document, httpRequest) => { document.Servers.Clear(); - document.Servers.Add(new NSwag.OpenApiServer { Url = $"http://{Global.localhost}:5000" }); + document.Servers.Add(new NSwag.OpenApiServer { Url = $"http://{Global.LocalHost}:5000" }); }; }); app.UseSwaggerUi(); @@ -232,7 +237,7 @@ try { try { - var document = await OpenApiDocument.FromUrlAsync($"http://{Global.localhost}:5000/swagger/v1/swagger.json"); + var document = await OpenApiDocument.FromUrlAsync($"http://{Global.LocalHost}:5000/swagger/v1/swagger.json"); var settings = new TypeScriptClientGeneratorSettings { diff --git a/server/src/Common/Global.cs b/server/src/Common/Global.cs index 2d8cb0e..d48215a 100644 --- a/server/src/Common/Global.cs +++ b/server/src/Common/Global.cs @@ -4,7 +4,8 @@ using System.Net.Sockets; public static class Global { - public static readonly string localhost = "127.0.0.1"; + public static readonly string LocalHost = "127.0.0.1"; + public static readonly string DataPath = Path.Combine(Environment.CurrentDirectory, "data"); public static string GetLocalIPAddress() { diff --git a/server/src/Controllers/DataController.cs b/server/src/Controllers/DataController.cs index 0df9499..27cdb85 100644 --- a/server/src/Controllers/DataController.cs +++ b/server/src/Controllers/DataController.cs @@ -17,38 +17,15 @@ namespace server.Controllers; public class DataController : ControllerBase { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + + private readonly Database.UserManager _userManager; + // 固定的实验板IP,端口,MAC地址 private const string BOARD_IP = "169.254.109.0"; - /// - /// [TODO:description] - /// - public class UserInfo + public DataController(Database.UserManager userManager) { - /// - /// 用户的唯一标识符 - /// - public Guid ID { get; set; } - - /// - /// 用户的名称 - /// - public required string Name { get; set; } - - /// - /// 用户的电子邮箱 - /// - public required string EMail { get; set; } - - /// - /// 用户关联的板卡ID - /// - public Guid BoardID { get; set; } - - /// - /// 用户绑定板子的过期时间 - /// - public DateTime? BoardExpireTime { get; set; } + _userManager = userManager; } /// @@ -112,8 +89,7 @@ public class DataController : ControllerBase public IActionResult Login(string name, string password) { // 验证用户密码 - using var db = new Database.AppDataConnection(); - var ret = db.CheckUserPassword(name, password); + var ret = _userManager.CheckUserPassword(name, password); if (!ret.IsSuccessful) return StatusCode(StatusCodes.Status500InternalServerError, "数据库操作失败"); if (!ret.Value.HasValue) return BadRequest("用户名或密码错误"); var user = ret.Value.Value; @@ -188,8 +164,7 @@ public class DataController : ControllerBase return Unauthorized("未找到用户名信息"); // Get User Info - using var db = new Database.AppDataConnection(); - var ret = db.GetUserByName(userName); + var ret = _userManager.GetUserByName(userName); if (!ret.IsSuccessful) return StatusCode(StatusCodes.Status500InternalServerError, "数据库操作失败"); @@ -236,8 +211,7 @@ public class DataController : ControllerBase try { - using var db = new Database.AppDataConnection(); - var ret = db.AddUser(name, email, password); + var ret = _userManager.AddUser(name, email, password); return Ok(ret); } catch (Exception ex) @@ -265,15 +239,14 @@ public class DataController : ControllerBase if (string.IsNullOrEmpty(userName)) return Unauthorized("未找到用户名信息"); - using var db = new Database.AppDataConnection(); - var userRet = db.GetUserByName(userName); + var userRet = _userManager.GetUserByName(userName); if (!userRet.IsSuccessful || !userRet.Value.HasValue) return BadRequest("用户不存在"); var user = userRet.Value.Value; var expireTime = DateTime.UtcNow.AddHours(durationHours); - var boardOpt = db.GetAvailableBoard(user.ID, expireTime); + var boardOpt = _userManager.GetAvailableBoard(user.ID, expireTime); if (!boardOpt.HasValue) return NotFound("没有可用的实验板"); @@ -309,13 +282,12 @@ public class DataController : ControllerBase if (string.IsNullOrEmpty(userName)) return Unauthorized("未找到用户名信息"); - using var db = new Database.AppDataConnection(); - var userRet = db.GetUserByName(userName); + var userRet = _userManager.GetUserByName(userName); if (!userRet.IsSuccessful || !userRet.Value.HasValue) return BadRequest("用户不存在"); var user = userRet.Value.Value; - var result = db.UnbindUserFromBoard(user.ID); + var result = _userManager.UnbindUserFromBoard(user.ID); return Ok(result > 0); } catch (Exception ex) @@ -338,8 +310,7 @@ public class DataController : ControllerBase { try { - using var db = new Database.AppDataConnection(); - var ret = db.GetBoardByID(id); + var ret = _userManager.GetBoardByID(id); if (!ret.IsSuccessful) return StatusCode(StatusCodes.Status500InternalServerError, "数据库操作失败"); if (!ret.Value.HasValue) @@ -375,8 +346,7 @@ public class DataController : ControllerBase return BadRequest("板子名称不能为空"); try { - using var db = new Database.AppDataConnection(); - var ret = db.AddBoard(name); + var ret = _userManager.AddBoard(name); return Ok(ret); } catch (Exception ex) @@ -402,8 +372,7 @@ public class DataController : ControllerBase try { - using var db = new Database.AppDataConnection(); - var ret = db.DeleteBoardByID(id); + var ret = _userManager.DeleteBoardByID(id); return Ok(ret); } catch (Exception ex) @@ -425,8 +394,7 @@ public class DataController : ControllerBase { try { - using var db = new Database.AppDataConnection(); - var boards = db.GetAllBoard(); + var boards = _userManager.GetAllBoard(); return Ok(boards); } catch (Exception ex) @@ -453,8 +421,7 @@ public class DataController : ControllerBase return BadRequest("新名称不能为空"); try { - using var db = new Database.AppDataConnection(); - var result = db.UpdateBoardName(boardId, newName); + var result = _userManager.UpdateBoardName(boardId, newName); return Ok(result); } catch (Exception ex) @@ -479,8 +446,7 @@ public class DataController : ControllerBase return BadRequest("板子Guid不能为空"); try { - using var db = new Database.AppDataConnection(); - var result = db.UpdateBoardStatus(boardId, newStatus); + var result = _userManager.UpdateBoardStatus(boardId, newStatus); return Ok(result); } catch (Exception ex) @@ -489,4 +455,36 @@ public class DataController : ControllerBase return StatusCode(StatusCodes.Status500InternalServerError, "更新失败,请稍后重试"); } } + + + /// + /// [TODO:description] + /// + public class UserInfo + { + /// + /// 用户的唯一标识符 + /// + public Guid ID { get; set; } + + /// + /// 用户的名称 + /// + public required string Name { get; set; } + + /// + /// 用户的电子邮箱 + /// + public required string EMail { get; set; } + + /// + /// 用户关联的板卡ID + /// + public Guid BoardID { get; set; } + + /// + /// 用户绑定板子的过期时间 + /// + public DateTime? BoardExpireTime { get; set; } + } } diff --git a/server/src/Controllers/DebuggerController.cs b/server/src/Controllers/DebuggerController.cs index 44a3876..a47e5a6 100644 --- a/server/src/Controllers/DebuggerController.cs +++ b/server/src/Controllers/DebuggerController.cs @@ -15,77 +15,11 @@ public class DebuggerController : ControllerBase { private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); - /// - /// 表示单个信号通道的配置信息 - /// - public class ChannelConfig - { - /// - /// 通道名称 - /// - required public string name; - /// - /// 通道显示颜色(如前端波形显示用) - /// - required public string color; - /// - /// 通道信号线宽度(位数) - /// - required public UInt32 wireWidth; - /// - /// 信号线在父端口中的起始索引(bit) - /// - required public UInt32 wireStartIndex; - /// - /// 父端口编号 - /// - required public UInt32 parentPort; - /// - /// 捕获模式(如上升沿、下降沿等) - /// - required public CaptureMode mode; - } + private readonly Database.UserManager _userManager; - /// - /// 调试器整体配置信息 - /// - public class DebuggerConfig + public DebuggerController(Database.UserManager userManager) { - /// - /// 时钟频率 - /// - required public UInt32 clkFreq; - /// - /// 总端口数量 - /// - required public UInt32 totalPortNum; - /// - /// 捕获深度(采样点数) - /// - required public UInt32 captureDepth; - /// - /// 触发器数量 - /// - required public UInt32 triggerNum; - /// - /// 所有信号通道的配置信息 - /// - required public ChannelConfig[] channelConfigs; - } - - /// - /// 单个通道的捕获数据 - /// - public class ChannelCaptureData - { - /// - /// 通道名称 - /// - required public string name; - /// - /// 通道捕获到的数据(Base64编码的UInt32数组) - /// - required public string data; + this._userManager = userManager; } /// @@ -99,8 +33,7 @@ public class DebuggerController : ControllerBase if (string.IsNullOrEmpty(userName)) return null; - using var db = new Database.AppDataConnection(); - var userRet = db.GetUserByName(userName); + var userRet = _userManager.GetUserByName(userName); if (!userRet.IsSuccessful || !userRet.Value.HasValue) return null; @@ -108,7 +41,7 @@ public class DebuggerController : ControllerBase if (user.BoardID == Guid.Empty) return null; - var boardRet = db.GetBoardByID(user.BoardID); + var boardRet = _userManager.GetBoardByID(user.BoardID); if (!boardRet.IsSuccessful || !boardRet.Value.HasValue) return null; @@ -464,4 +397,77 @@ public class DebuggerController : ControllerBase return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试"); } } + + /// + /// 表示单个信号通道的配置信息 + /// + public class ChannelConfig + { + /// + /// 通道名称 + /// + required public string name; + /// + /// 通道显示颜色(如前端波形显示用) + /// + required public string color; + /// + /// 通道信号线宽度(位数) + /// + required public UInt32 wireWidth; + /// + /// 信号线在父端口中的起始索引(bit) + /// + required public UInt32 wireStartIndex; + /// + /// 父端口编号 + /// + required public UInt32 parentPort; + /// + /// 捕获模式(如上升沿、下降沿等) + /// + required public CaptureMode mode; + } + + /// + /// 调试器整体配置信息 + /// + public class DebuggerConfig + { + /// + /// 时钟频率 + /// + required public UInt32 clkFreq; + /// + /// 总端口数量 + /// + required public UInt32 totalPortNum; + /// + /// 捕获深度(采样点数) + /// + required public UInt32 captureDepth; + /// + /// 触发器数量 + /// + required public UInt32 triggerNum; + /// + /// 所有信号通道的配置信息 + /// + required public ChannelConfig[] channelConfigs; + } + + /// + /// 单个通道的捕获数据 + /// + public class ChannelCaptureData + { + /// + /// 通道名称 + /// + required public string name; + /// + /// 通道捕获到的数据(Base64编码的UInt32数组) + /// + required public string data; + } } diff --git a/server/src/Controllers/ExamController.cs b/server/src/Controllers/ExamController.cs index 178907f..03fba6a 100644 --- a/server/src/Controllers/ExamController.cs +++ b/server/src/Controllers/ExamController.cs @@ -14,6 +14,163 @@ public class ExamController : ControllerBase { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + private readonly Database.ExamManager _examManager; + + public ExamController(Database.ExamManager examManager) + { + _examManager = examManager; + } + + /// + /// 获取所有实验列表 + /// + /// 实验列表 + [Authorize] + [HttpGet("list")] + [EnableCors("Users")] + [ProducesResponseType(typeof(ExamSummary[]), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult GetExamList() + { + try + { + var exams = _examManager.GetAllExams(); + + var examSummaries = exams.Select(exam => new ExamSummary + { + ID = exam.ID, + Name = exam.Name, + CreatedTime = exam.CreatedTime, + UpdatedTime = exam.UpdatedTime, + Tags = exam.GetTagsList(), + Difficulty = exam.Difficulty, + IsVisibleToUsers = exam.IsVisibleToUsers + }).ToArray(); + + logger.Info($"成功获取实验列表,共 {examSummaries.Length} 个实验"); + return Ok(examSummaries); + } + catch (Exception ex) + { + logger.Error($"获取实验列表时出错: {ex.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验列表失败: {ex.Message}"); + } + } + + /// + /// 根据实验ID获取实验详细信息 + /// + /// 实验ID + /// 实验详细信息 + [Authorize] + [HttpGet("{examId}")] + [EnableCors("Users")] + [ProducesResponseType(typeof(ExamInfo), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult GetExam(string examId) + { + if (string.IsNullOrWhiteSpace(examId)) + return BadRequest("实验ID不能为空"); + + try + { + var result = _examManager.GetExamByID(examId); + + if (!result.IsSuccessful) + { + logger.Error($"获取实验时出错: {result.Error.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验失败: {result.Error.Message}"); + } + + if (!result.Value.HasValue) + { + logger.Warn($"实验不存在: {examId}"); + return NotFound($"实验 {examId} 不存在"); + } + + var exam = result.Value.Value; + var examInfo = new ExamInfo + { + ID = exam.ID, + Name = exam.Name, + Description = exam.Description, + CreatedTime = exam.CreatedTime, + UpdatedTime = exam.UpdatedTime, + Tags = exam.GetTagsList(), + Difficulty = exam.Difficulty, + IsVisibleToUsers = exam.IsVisibleToUsers + }; + + logger.Info($"成功获取实验信息: {examId}"); + return Ok(examInfo); + } + catch (Exception ex) + { + logger.Error($"获取实验 {examId} 时出错: {ex.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验失败: {ex.Message}"); + } + } + + /// + /// 创建新实验 + /// + /// 创建实验请求 + /// 创建结果 + [Authorize("Admin")] + [HttpPost] + [EnableCors("Users")] + [ProducesResponseType(typeof(ExamInfo), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult CreateExam([FromBody] CreateExamRequest request) + { + if (string.IsNullOrWhiteSpace(request.ID) || string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Description)) + return BadRequest("实验ID、名称和描述不能为空"); + + try + { + var result = _examManager.CreateExam(request.ID, request.Name, request.Description, request.Tags, request.Difficulty, request.IsVisibleToUsers); + + if (!result.IsSuccessful) + { + if (result.Error.Message.Contains("已存在")) + return Conflict(result.Error.Message); + + logger.Error($"创建实验时出错: {result.Error.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {result.Error.Message}"); + } + + var exam = result.Value; + var examInfo = new ExamInfo + { + ID = exam.ID, + Name = exam.Name, + Description = exam.Description, + CreatedTime = exam.CreatedTime, + UpdatedTime = exam.UpdatedTime, + Tags = exam.GetTagsList(), + Difficulty = exam.Difficulty, + IsVisibleToUsers = exam.IsVisibleToUsers + }; + + logger.Info($"成功创建实验: {request.ID}"); + return CreatedAtAction(nameof(GetExam), new { examId = request.ID }, examInfo); + } + catch (Exception ex) + { + logger.Error($"创建实验 {request.ID} 时出错: {ex.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {ex.Message}"); + } + } + + /// /// 实验信息类 /// @@ -136,156 +293,4 @@ public class ExamController : ControllerBase /// public bool IsVisibleToUsers { get; set; } = true; } - - /// - /// 获取所有实验列表 - /// - /// 实验列表 - [Authorize] - [HttpGet("list")] - [EnableCors("Users")] - [ProducesResponseType(typeof(ExamSummary[]), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public IActionResult GetExamList() - { - try - { - using var db = new Database.AppDataConnection(); - var exams = db.GetAllExams(); - - var examSummaries = exams.Select(exam => new ExamSummary - { - ID = exam.ID, - Name = exam.Name, - CreatedTime = exam.CreatedTime, - UpdatedTime = exam.UpdatedTime, - Tags = exam.GetTagsList(), - Difficulty = exam.Difficulty, - IsVisibleToUsers = exam.IsVisibleToUsers - }).ToArray(); - - logger.Info($"成功获取实验列表,共 {examSummaries.Length} 个实验"); - return Ok(examSummaries); - } - catch (Exception ex) - { - logger.Error($"获取实验列表时出错: {ex.Message}"); - return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验列表失败: {ex.Message}"); - } - } - - /// - /// 根据实验ID获取实验详细信息 - /// - /// 实验ID - /// 实验详细信息 - [Authorize] - [HttpGet("{examId}")] - [EnableCors("Users")] - [ProducesResponseType(typeof(ExamInfo), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public IActionResult GetExam(string examId) - { - if (string.IsNullOrWhiteSpace(examId)) - return BadRequest("实验ID不能为空"); - - try - { - using var db = new Database.AppDataConnection(); - var result = db.GetExamByID(examId); - - if (!result.IsSuccessful) - { - logger.Error($"获取实验时出错: {result.Error.Message}"); - return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验失败: {result.Error.Message}"); - } - - if (!result.Value.HasValue) - { - logger.Warn($"实验不存在: {examId}"); - return NotFound($"实验 {examId} 不存在"); - } - - var exam = result.Value.Value; - var examInfo = new ExamInfo - { - ID = exam.ID, - Name = exam.Name, - Description = exam.Description, - CreatedTime = exam.CreatedTime, - UpdatedTime = exam.UpdatedTime, - Tags = exam.GetTagsList(), - Difficulty = exam.Difficulty, - IsVisibleToUsers = exam.IsVisibleToUsers - }; - - logger.Info($"成功获取实验信息: {examId}"); - return Ok(examInfo); - } - catch (Exception ex) - { - logger.Error($"获取实验 {examId} 时出错: {ex.Message}"); - return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验失败: {ex.Message}"); - } - } - - /// - /// 创建新实验 - /// - /// 创建实验请求 - /// 创建结果 - [Authorize("Admin")] - [HttpPost] - [EnableCors("Users")] - [ProducesResponseType(typeof(ExamInfo), StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public IActionResult CreateExam([FromBody] CreateExamRequest request) - { - if (string.IsNullOrWhiteSpace(request.ID) || string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Description)) - return BadRequest("实验ID、名称和描述不能为空"); - - try - { - using var db = new Database.AppDataConnection(); - var result = db.CreateExam(request.ID, request.Name, request.Description, request.Tags, request.Difficulty, request.IsVisibleToUsers); - - if (!result.IsSuccessful) - { - if (result.Error.Message.Contains("已存在")) - return Conflict(result.Error.Message); - - logger.Error($"创建实验时出错: {result.Error.Message}"); - return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {result.Error.Message}"); - } - - var exam = result.Value; - var examInfo = new ExamInfo - { - ID = exam.ID, - Name = exam.Name, - Description = exam.Description, - CreatedTime = exam.CreatedTime, - UpdatedTime = exam.UpdatedTime, - Tags = exam.GetTagsList(), - Difficulty = exam.Difficulty, - IsVisibleToUsers = exam.IsVisibleToUsers - }; - - logger.Info($"成功创建实验: {request.ID}"); - return CreatedAtAction(nameof(GetExam), new { examId = request.ID }, examInfo); - } - catch (Exception ex) - { - logger.Error($"创建实验 {request.ID} 时出错: {ex.Message}"); - return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {ex.Message}"); - } - } } diff --git a/server/src/Controllers/HdmiVideoStreamController.cs b/server/src/Controllers/HdmiVideoStreamController.cs index 396af2d..cdcbb92 100644 --- a/server/src/Controllers/HdmiVideoStreamController.cs +++ b/server/src/Controllers/HdmiVideoStreamController.cs @@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Authorization; using System.Security.Claims; using server.Services; -using Database; namespace server.Controllers; @@ -12,12 +11,15 @@ namespace server.Controllers; [EnableCors("Users")] public class HdmiVideoStreamController : ControllerBase { - private readonly HttpHdmiVideoStreamService _videoStreamService; private readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); - public HdmiVideoStreamController(HttpHdmiVideoStreamService videoStreamService) + private readonly HttpHdmiVideoStreamService _videoStreamService; + private readonly Database.UserManager _userManager; + + public HdmiVideoStreamController(HttpHdmiVideoStreamService videoStreamService, Database.UserManager userManager) { _videoStreamService = videoStreamService; + _userManager = userManager; } // 管理员获取所有板子的 endpoints @@ -40,11 +42,7 @@ public class HdmiVideoStreamController : ControllerBase if (string.IsNullOrEmpty(userName)) return Unauthorized("User name not found in claims."); - var db = new AppDataConnection(); - if (db == null) - return NotFound("Database connection failed."); - - var userRet = db.GetUserByName(userName); + var userRet = _userManager.GetUserByName(userName); if (!userRet.IsSuccessful || !userRet.Value.HasValue) return NotFound("User not found."); @@ -53,7 +51,7 @@ public class HdmiVideoStreamController : ControllerBase if (boardId == Guid.Empty) return NotFound("No board bound to this user."); - var boardRet = db.GetBoardByID(boardId); + var boardRet = _userManager.GetBoardByID(boardId); if (!boardRet.IsSuccessful || !boardRet.Value.HasValue) return NotFound("Board not found."); @@ -70,11 +68,7 @@ public class HdmiVideoStreamController : ControllerBase if (string.IsNullOrEmpty(userName)) return Unauthorized("User name not found in claims."); - var db = new AppDataConnection(); - if (db == null) - return NotFound("Database connection failed."); - - var userRet = db.GetUserByName(userName); + var userRet = _userManager.GetUserByName(userName); if (!userRet.IsSuccessful || !userRet.Value.HasValue) return NotFound("User not found."); diff --git a/server/src/Controllers/JtagController.cs b/server/src/Controllers/JtagController.cs index 349442e..863b53b 100644 --- a/server/src/Controllers/JtagController.cs +++ b/server/src/Controllers/JtagController.cs @@ -17,12 +17,17 @@ public class JtagController : ControllerBase private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private readonly ProgressTrackerService _tracker; + private readonly UserManager _userManager; + private readonly ResourceManager _resourceManager; private const string BITSTREAM_PATH = "bitstream/Jtag"; - public JtagController(ProgressTrackerService tracker) + public JtagController( + ProgressTrackerService tracker, UserManager userManager, ResourceManager resourceManager) { _tracker = tracker; + _userManager = userManager; + _resourceManager = resourceManager; } /// @@ -127,6 +132,7 @@ public class JtagController : ControllerBase /// JTAG 设备地址 /// JTAG 设备端口 /// 比特流ID + /// 取消令牌 /// 进度跟踪TaskID [HttpPost("DownloadBitstream")] [EnableCors("Users")] @@ -134,7 +140,7 @@ public class JtagController : ControllerBase [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async ValueTask DownloadBitstream(string address, int port, int bitstreamId, CancellationToken cancelToken) + public IResult DownloadBitstream(string address, int port, int bitstreamId, CancellationToken cancelToken) { logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port} using bitstream ID: {bitstreamId}"); @@ -149,35 +155,39 @@ public class JtagController : ControllerBase } // 从数据库获取用户信息 - using var db = new Database.AppDataConnection(); - var userResult = db.GetUserByName(username); + var userResult = _userManager.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); + var user = userResult.Value.Value; + var resourceRet = _resourceManager.GetResourceById(bitstreamId); - if (!bitstreamResult.IsSuccessful) + if (!resourceRet.IsSuccessful) { - logger.Error($"User {username} failed to get bitstream from database: {bitstreamResult.Error}"); - return TypedResults.InternalServerError($"数据库查询失败: {bitstreamResult.Error?.Message}"); + logger.Error($"User {username} failed to get bitstream from database: {resourceRet.Error}"); + return TypedResults.InternalServerError($"数据库查询失败: {resourceRet.Error?.Message}"); } - if (!bitstreamResult.Value.HasValue) + if (!resourceRet.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; + var resource = resourceRet.Value.Value; + var bitstreamRet = _resourceManager.ReadBytesFromPath(resource.Path); + if (!bitstreamRet.IsSuccessful) + { + logger.Error($"User {username} failed to read bitstream file: {bitstreamRet.Error}"); + return TypedResults.InternalServerError($"比特流读取失败: {bitstreamRet.Error?.Message}"); + } + + var fileBytes = bitstreamRet.Value; if (fileBytes == null || fileBytes.Length == 0) { logger.Warn($"User {username} found empty bitstream data for ID: {bitstreamId}"); @@ -235,7 +245,7 @@ public class JtagController : ControllerBase if (ret.IsSuccessful) { - logger.Info($"User {username} successfully downloaded bitstream '{bitstream.ResourceName}' to device {address}"); + logger.Info($"User {username} successfully downloaded bitstream '{resource.ResourceName}' to device {address}"); progress.Finish(); } else diff --git a/server/src/Controllers/LogicAnalyzerController.cs b/server/src/Controllers/LogicAnalyzerController.cs index f54eeeb..ee6ded3 100644 --- a/server/src/Controllers/LogicAnalyzerController.cs +++ b/server/src/Controllers/LogicAnalyzerController.cs @@ -15,56 +15,11 @@ public class LogicAnalyzerController : ControllerBase { private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); - /// - /// 信号触发配置 - /// - public class SignalTriggerConfig + private readonly Database.UserManager _userManager; + + public LogicAnalyzerController(Database.UserManager userManager) { - /// - /// 信号索引 (0-7) - /// - public int SignalIndex { get; set; } - - /// - /// 操作符 - /// - public SignalOperator Operator { get; set; } - - /// - /// 信号值 - /// - public SignalValue Value { get; set; } - } - - /// - /// 捕获配置 - /// - public class CaptureConfig - { - /// - /// 全局触发模式 - /// - public GlobalCaptureMode GlobalMode { get; set; } - /// - /// 捕获深度 - /// - public int CaptureLength { get; set; } = 2048 * 32; - /// - /// 预采样深度 - /// - public int PreCaptureLength { get; set; } = 2048; - /// - /// 有效通道 - /// - public AnalyzerChannelDiv ChannelDiv { get; set; } = AnalyzerChannelDiv.EIGHT; - /// - /// 时钟分频系数 - /// - public AnalyzerClockDiv ClockDiv { get; set; } = AnalyzerClockDiv.DIV1; - /// - /// 信号触发配置列表 - /// - public SignalTriggerConfig[] SignalConfigs { get; set; } = Array.Empty(); + _userManager = userManager; } /// @@ -78,8 +33,7 @@ public class LogicAnalyzerController : ControllerBase if (string.IsNullOrEmpty(userName)) return null; - using var db = new Database.AppDataConnection(); - var userRet = db.GetUserByName(userName); + var userRet = _userManager.GetUserByName(userName); if (!userRet.IsSuccessful || !userRet.Value.HasValue) return null; @@ -87,7 +41,7 @@ public class LogicAnalyzerController : ControllerBase if (user.BoardID == Guid.Empty) return null; - var boardRet = db.GetBoardByID(user.BoardID); + var boardRet = _userManager.GetBoardByID(user.BoardID); if (!boardRet.IsSuccessful || !boardRet.Value.HasValue) return null; @@ -422,4 +376,57 @@ public class LogicAnalyzerController : ControllerBase return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试"); } } + + /// + /// 信号触发配置 + /// + public class SignalTriggerConfig + { + /// + /// 信号索引 (0-7) + /// + public int SignalIndex { get; set; } + + /// + /// 操作符 + /// + public SignalOperator Operator { get; set; } + + /// + /// 信号值 + /// + public SignalValue Value { get; set; } + } + + /// + /// 捕获配置 + /// + public class CaptureConfig + { + /// + /// 全局触发模式 + /// + public GlobalCaptureMode GlobalMode { get; set; } + /// + /// 捕获深度 + /// + public int CaptureLength { get; set; } = 2048 * 32; + /// + /// 预采样深度 + /// + public int PreCaptureLength { get; set; } = 2048; + /// + /// 有效通道 + /// + public AnalyzerChannelDiv ChannelDiv { get; set; } = AnalyzerChannelDiv.EIGHT; + /// + /// 时钟分频系数 + /// + public AnalyzerClockDiv ClockDiv { get; set; } = AnalyzerClockDiv.DIV1; + /// + /// 信号触发配置列表 + /// + public SignalTriggerConfig[] SignalConfigs { get; set; } = Array.Empty(); + } + } diff --git a/server/src/Controllers/OscilloscopeController.cs b/server/src/Controllers/OscilloscopeController.cs index a8c279f..ef8f629 100644 --- a/server/src/Controllers/OscilloscopeController.cs +++ b/server/src/Controllers/OscilloscopeController.cs @@ -15,71 +15,11 @@ public class OscilloscopeApiController : ControllerBase { private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); - /// - /// 示波器完整配置 - /// - public class OscilloscopeFullConfig + private readonly Database.UserManager _userManager; + + public OscilloscopeApiController(Database.UserManager userManager) { - /// - /// 是否启动捕获 - /// - public bool CaptureEnabled { get; set; } - - /// - /// 触发电平(0-255) - /// - public byte TriggerLevel { get; set; } - - /// - /// 触发边沿(true为上升沿,false为下降沿) - /// - public bool TriggerRisingEdge { get; set; } - - /// - /// 水平偏移量(0-1023) - /// - public ushort HorizontalShift { get; set; } - - /// - /// 抽样率(0-1023) - /// - public ushort DecimationRate { get; set; } - - /// - /// 是否自动刷新RAM - /// - public bool AutoRefreshRAM { get; set; } = true; - } - - /// - /// 示波器状态和数据 - /// - public class OscilloscopeDataResponse - { - /// - /// AD采样频率 - /// - public uint ADFrequency { get; set; } - - /// - /// AD采样幅度 - /// - public byte ADVpp { get; set; } - - /// - /// AD采样最大值 - /// - public byte ADMax { get; set; } - - /// - /// AD采样最小值 - /// - public byte ADMin { get; set; } - - /// - /// 波形数据(Base64编码) - /// - public string WaveformData { get; set; } = string.Empty; + _userManager = userManager; } /// @@ -93,8 +33,7 @@ public class OscilloscopeApiController : ControllerBase if (string.IsNullOrEmpty(userName)) return null; - using var db = new Database.AppDataConnection(); - var userRet = db.GetUserByName(userName); + var userRet = _userManager.GetUserByName(userName); if (!userRet.IsSuccessful || !userRet.Value.HasValue) return null; @@ -102,7 +41,7 @@ public class OscilloscopeApiController : ControllerBase if (user.BoardID == Guid.Empty) return null; - var boardRet = db.GetBoardByID(user.BoardID); + var boardRet = _userManager.GetBoardByID(user.BoardID); if (!boardRet.IsSuccessful || !boardRet.Value.HasValue) return null; @@ -481,4 +420,72 @@ public class OscilloscopeApiController : ControllerBase return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试"); } } -} \ No newline at end of file + + /// + /// 示波器完整配置 + /// + public class OscilloscopeFullConfig + { + /// + /// 是否启动捕获 + /// + public bool CaptureEnabled { get; set; } + + /// + /// 触发电平(0-255) + /// + public byte TriggerLevel { get; set; } + + /// + /// 触发边沿(true为上升沿,false为下降沿) + /// + public bool TriggerRisingEdge { get; set; } + + /// + /// 水平偏移量(0-1023) + /// + public ushort HorizontalShift { get; set; } + + /// + /// 抽样率(0-1023) + /// + public ushort DecimationRate { get; set; } + + /// + /// 是否自动刷新RAM + /// + public bool AutoRefreshRAM { get; set; } = true; + } + + /// + /// 示波器状态和数据 + /// + public class OscilloscopeDataResponse + { + /// + /// AD采样频率 + /// + public uint ADFrequency { get; set; } + + /// + /// AD采样幅度 + /// + public byte ADVpp { get; set; } + + /// + /// AD采样最大值 + /// + public byte ADMax { get; set; } + + /// + /// AD采样最小值 + /// + public byte ADMin { get; set; } + + /// + /// 波形数据(Base64编码) + /// + public string WaveformData { get; set; } = string.Empty; + } + +} diff --git a/server/src/Controllers/ResourceController.cs b/server/src/Controllers/ResourceController.cs index af8c9db..2e70482 100644 --- a/server/src/Controllers/ResourceController.cs +++ b/server/src/Controllers/ResourceController.cs @@ -15,6 +15,316 @@ public class ResourceController : ControllerBase { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + private readonly UserManager _userManager; + private readonly ResourceManager _resourceManager; + + public ResourceController(UserManager userManager, ResourceManager resourceManager) + { + _userManager = userManager; + _resourceManager = resourceManager; + } + + /// + /// 添加资源(文件上传) + /// + /// 添加资源请求 + /// 资源文件 + /// 添加结果 + [Authorize] + [HttpPost] + [EnableCors("Users")] + [ProducesResponseType(typeof(ResourceInfo), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task 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 + { + // 获取当前用户ID + 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; + + // 读取文件数据 + using var memoryStream = new MemoryStream(); + await file.CopyToAsync(memoryStream); + var fileData = memoryStream.ToArray(); + + var result = _resourceManager.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}"); + } + } + + /// + /// 获取资源列表 + /// + /// 实验ID(可选) + /// 资源类型(可选) + /// 资源用途(可选) + /// 资源列表 + [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 + { + // 获取当前用户ID + 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; + + // 普通用户只能查看自己的资源和模板资源 + 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 = _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); + } + } + + var result = _resourceManager.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}"); + } + } + + /// + /// 根据资源ID下载资源 + /// + /// 资源ID + /// 资源文件 + [HttpGet("{resourceId}")] + [EnableCors("Users")] + [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult GetResourceById(int resourceId) + { + try + { + var result = _resourceManager.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})"); + + var dataRet = _resourceManager.ReadBytesFromPath(resource.Path); + if (!dataRet.IsSuccessful) + { + logger.Error($"读取资源数据时出错: {dataRet.Error.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"读取资源数据失败: {dataRet.Error.Message}"); + } + + return File(dataRet.Value, resource.MimeType ?? "application/octet-stream", resource.ResourceName); + } + catch (Exception ex) + { + logger.Error($"获取资源 {resourceId} 时出错: {ex.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {ex.Message}"); + } + } + + /// + /// 删除资源 + /// + /// 资源ID + /// 删除结果 + [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 + { + // 获取当前用户信息 + 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 resourceResult = _resourceManager.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 = _resourceManager.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}"); + } + } + /// /// 资源信息类 /// @@ -77,301 +387,4 @@ public class ResourceController : ControllerBase public string? ExamID { get; set; } } - /// - /// 添加资源(文件上传) - /// - /// 添加资源请求 - /// 资源文件 - /// 添加结果 - [Authorize] - [HttpPost] - [EnableCors("Users")] - [ProducesResponseType(typeof(ResourceInfo), StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task 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}"); - } - } - - /// - /// 获取资源列表 - /// - /// 实验ID(可选) - /// 资源类型(可选) - /// 资源用途(可选) - /// 资源列表 - [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}"); - } - } - - /// - /// 根据资源ID下载资源 - /// - /// 资源ID - /// 资源文件 - [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}"); - } - } - - /// - /// 删除资源 - /// - /// 资源ID - /// 删除结果 - [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}"); - } - } } diff --git a/server/src/Controllers/VideoStreamController.cs b/server/src/Controllers/VideoStreamController.cs index fe786b9..b5a09bd 100644 --- a/server/src/Controllers/VideoStreamController.cs +++ b/server/src/Controllers/VideoStreamController.cs @@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; using System.Security.Claims; -using Database; using DotNext; /// @@ -15,44 +14,21 @@ using DotNext; public class VideoStreamController : ControllerBase { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + private readonly server.Services.HttpVideoStreamService _videoStreamService; - - /// - /// 分辨率配置请求模型 - /// - public class ResolutionConfigRequest - { - /// - /// 宽度 - /// - [Required] - [Range(640, 1920, ErrorMessage = "宽度必须在640-1920范围内")] - public int Width { get; set; } - - /// - /// 高度 - /// - [Required] - [Range(480, 1080, ErrorMessage = "高度必须在480-1080范围内")] - public int Height { get; set; } - } - - public class AvailableResolutionsResponse - { - public int Width { get; set; } - public int Height { get; set; } - public string Name { get; set; } = string.Empty; - public string Value => $"{Width}x{Height}"; - } + private readonly Database.UserManager _userManager; /// /// 初始化HTTP视频流控制器 /// /// HTTP视频流服务 - public VideoStreamController(server.Services.HttpVideoStreamService videoStreamService) + /// 用户管理服务 + public VideoStreamController( + server.Services.HttpVideoStreamService videoStreamService, Database.UserManager userManager) { logger.Info("创建VideoStreamController,命名空间:{Namespace}", this.GetType().Namespace); _videoStreamService = videoStreamService; + _userManager = userManager; } private Optional TryGetBoardId() @@ -64,14 +40,7 @@ public class VideoStreamController : ControllerBase return Optional.None; } - var db = new AppDataConnection(); - if (db == null) - { - logger.Error("Database connection failed."); - return Optional.None; - } - - var userRet = db.GetUserByName(userName); + var userRet = _userManager.GetUserByName(userName); if (!userRet.IsSuccessful || !userRet.Value.HasValue) { logger.Error("User not found."); @@ -349,4 +318,32 @@ public class VideoStreamController : ControllerBase return TypedResults.InternalServerError($"执行自动对焦失败: {ex.Message}"); } } + + /// + /// 分辨率配置请求模型 + /// + public class ResolutionConfigRequest + { + /// + /// 宽度 + /// + [Required] + [Range(640, 1920, ErrorMessage = "宽度必须在640-1920范围内")] + public int Width { get; set; } + + /// + /// 高度 + /// + [Required] + [Range(480, 1080, ErrorMessage = "高度必须在480-1080范围内")] + public int Height { get; set; } + } + + public class AvailableResolutionsResponse + { + public int Width { get; set; } + public int Height { get; set; } + public string Name { get; set; } = string.Empty; + public string Value => $"{Width}x{Height}"; + } } diff --git a/server/src/Database.cs b/server/src/Database.cs deleted file mode 100644 index 9f50902..0000000 --- a/server/src/Database.cs +++ /dev/null @@ -1,1266 +0,0 @@ -using DotNext; -using LinqToDB; -using LinqToDB.Data; -using LinqToDB.Mapping; - -namespace Database; - -/// -/// 用户类,表示用户信息 -/// -public class User -{ - /// - /// 用户的唯一标识符 - /// - [PrimaryKey] - public Guid ID { get; set; } = Guid.NewGuid(); - - /// - /// 用户的名称 - /// - [NotNull] - public required string Name { get; set; } - - /// - /// 用户的电子邮箱 - /// - [NotNull] - public required string EMail { get; set; } - - /// - /// 用户的密码(应该进行哈希处理) - /// - [NotNull] - public required string Password { get; set; } - - /// - /// 用户权限等级 - /// - [NotNull] - public required UserPermission Permission { get; set; } - - /// - /// 绑定的实验板ID,如果未绑定则为空 - /// - [Nullable] - public Guid BoardID { get; set; } - - /// - /// 用户绑定板子的过期时间 - /// - [Nullable] - public DateTime? BoardExpireTime { get; set; } - - /// - /// 用户权限枚举 - /// - public enum UserPermission - { - /// - /// 管理员权限,可以管理用户和实验板 - /// - Admin, - - /// - /// 普通用户权限,只能使用实验板 - /// - Normal, - } -} - -/// -/// FPGA 板子类,表示板子信息 -/// -public class Board -{ - /// - /// FPGA 板子的唯一标识符 - /// - [PrimaryKey] - public Guid ID { get; set; } = Guid.NewGuid(); - - /// - /// FPGA 板子的名称 - /// - [NotNull] - public required string BoardName { get; set; } - - /// - /// FPGA 板子的IP地址 - /// - [NotNull] - public required string IpAddr { get; set; } - - /// - /// FPGA 板子的MAC地址 - /// - [NotNull] - public required string MacAddr { get; set; } - - /// - /// FPGA 板子的通信端口 - /// - [NotNull] - public int Port { get; set; } = 1234; - - /// - /// FPGA 板子的当前状态 - /// - [NotNull] - public required BoardStatus Status { get; set; } - - /// - /// 占用该板子的用户的唯一标识符 - /// - [Nullable] - public Guid OccupiedUserID { get; set; } - - /// - /// 占用该板子的用户的用户名 - /// - [Nullable] - public string? OccupiedUserName { get; set; } - - /// - /// FPGA 板子的固件版本号 - /// - [NotNull] - public string FirmVersion { get; set; } = "1.0.0"; - - /// - /// FPGA 板子状态枚举 - /// - public enum BoardStatus - { - /// - /// 未启用状态,无法被使用 - /// - Disabled, - - /// - /// 繁忙状态,正在被用户使用 - /// - Busy, - - /// - /// 可用状态,可以被分配给用户 - /// - Available, - } -} - -/// -/// 实验类,表示实验信息 -/// -public class Exam -{ - /// - /// 实验的唯一标识符 - /// - [PrimaryKey] - public required string ID { get; set; } - - /// - /// 实验名称 - /// - [NotNull] - public required string Name { get; set; } - - /// - /// 实验描述 - /// - [NotNull] - public required string Description { get; set; } - - /// - /// 实验创建时间 - /// - [NotNull] - public DateTime CreatedTime { get; set; } = DateTime.Now; - - /// - /// 实验最后更新时间 - /// - [NotNull] - public DateTime UpdatedTime { get; set; } = DateTime.Now; - - /// - /// 实验标签(以逗号分隔的字符串) - /// - [NotNull] - public string Tags { get; set; } = ""; - - /// - /// 实验难度(1-5,1为最简单) - /// - [NotNull] - public int Difficulty { get; set; } = 1; - - /// - /// 普通用户是否可见 - /// - [NotNull] - public bool IsVisibleToUsers { get; set; } = true; - - /// - /// 获取标签列表 - /// - /// 标签数组 - public string[] GetTagsList() - { - if (string.IsNullOrWhiteSpace(Tags)) - return Array.Empty(); - - return Tags.Split(',', StringSplitOptions.RemoveEmptyEntries) - .Select(tag => tag.Trim()) - .Where(tag => !string.IsNullOrEmpty(tag)) - .ToArray(); - } - - /// - /// 设置标签列表 - /// - /// 标签数组 - public void SetTagsList(string[] tags) - { - Tags = string.Join(",", tags.Where(tag => !string.IsNullOrWhiteSpace(tag)).Select(tag => tag.Trim())); - } -} - -/// -/// 资源类,统一管理实验资源、用户比特流等各类资源 -/// -public class Resource -{ - /// - /// 资源的唯一标识符 - /// - [PrimaryKey, Identity] - public int ID { get; set; } - - /// - /// 上传资源的用户ID - /// - [NotNull] - public required Guid UserID { get; set; } - - /// - /// 所属实验ID(可选,如果不属于特定实验则为空) - /// - [Nullable] - public string? ExamID { get; set; } - - /// - /// 资源类型(images, markdown, bitstream, diagram, project等) - /// - [NotNull] - public required string ResourceType { get; set; } - - /// - /// 资源用途:template(模板)或 user(用户上传) - /// - [NotNull] - public required string ResourcePurpose { get; set; } - - /// - /// 资源名称(包含文件扩展名) - /// - [NotNull] - public required string ResourceName { get; set; } - - /// - /// 资源的二进制数据 - /// - [NotNull] - public required byte[] Data { get; set; } - - /// - /// 资源创建/上传时间 - /// - [NotNull] - public DateTime UploadTime { get; set; } = DateTime.Now; - - /// - /// 资源的MIME类型 - /// - [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"; - } -} - -/// -/// 应用程序数据连接类,用于与数据库交互 -/// -public class AppDataConnection : DataConnection -{ - private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); - - static readonly string DATABASE_FILEPATH = $"{Environment.CurrentDirectory}/Database.sqlite"; - - static readonly LinqToDB.DataOptions options = - new LinqToDB.DataOptions().UseSQLite($"Data Source={DATABASE_FILEPATH}"); - - /// - /// 初始化应用程序数据连接 - /// - public AppDataConnection() : base(options) - { - if (!Path.Exists(DATABASE_FILEPATH)) - { - logger.Info($"数据库文件不存在,正在创建新数据库: {DATABASE_FILEPATH}"); - LinqToDB.DataProvider.SQLite.SQLiteTools.CreateDatabase(DATABASE_FILEPATH); - this.CreateAllTables(); - var user = new User() - { - Name = "Admin", - EMail = "selfconfusion@gmail.com", - Password = "12345678", - Permission = Database.User.UserPermission.Admin, - }; - this.Insert(user); - logger.Info("默认管理员用户已创建"); - } - else - { - logger.Info($"数据库连接已建立: {DATABASE_FILEPATH}"); - } - } - - - /// - /// 创建所有数据库表 - /// - public void CreateAllTables() - { - logger.Info("正在创建数据库表..."); - this.CreateTable(); - this.CreateTable(); - this.CreateTable(); - this.CreateTable(); - logger.Info("数据库表创建完成"); - } - - /// - /// 删除所有数据库表 - /// - public void DropAllTables() - { - logger.Warn("正在删除所有数据库表..."); - this.DropTable(); - this.DropTable(); - this.DropTable(); - this.DropTable(); - logger.Warn("所有数据库表已删除"); - } - - /// - /// 添加一个新的用户到数据库 - /// - /// 用户的名称 - /// 用户的电子邮箱地址 - /// 用户的密码 - /// 插入的记录数 - public int AddUser(string name, string email, string password) - { - var user = new User() - { - Name = name, - EMail = email, - Password = password, - Permission = Database.User.UserPermission.Normal, - }; - var result = this.Insert(user); - logger.Info($"新用户已添加: {name} ({email})"); - return result; - } - - /// - /// 根据用户名获取用户信息 - /// - /// 用户名 - /// 包含用户信息的结果,如果未找到或出错则返回相应状态 - public Result> GetUserByName(string name) - { - var user = this.UserTable.Where((user) => user.Name == name).ToArray(); - - if (user.Length > 1) - { - logger.Error($"数据库中存在多个同名用户: {name}"); - return new(new Exception($"数据库中存在多个同名用户: {name}")); - } - - if (user.Length == 0) - { - logger.Info($"未找到用户: {name}"); - return new(Optional.None); - } - - logger.Debug($"成功获取用户信息: {name}"); - return new(user[0]); - } - - /// - /// 根据电子邮箱获取用户信息 - /// - /// 用户的电子邮箱地址 - /// 包含用户信息的结果,如果未找到或出错则返回相应状态 - public Result> GetUserByEMail(string email) - { - var user = this.UserTable.Where((user) => user.EMail == email).ToArray(); - - if (user.Length > 1) - { - logger.Error($"数据库中存在多个相同邮箱的用户: {email}"); - return new(new Exception($"数据库中存在多个相同邮箱的用户: {email}")); - } - - if (user.Length == 0) - { - logger.Info($"未找到邮箱对应的用户: {email}"); - return new(Optional.None); - } - - logger.Debug($"成功获取用户信息: {email}"); - return new(user[0]); - } - - /// - /// 验证用户密码 - /// - /// 用户名 - /// 用户密码 - /// 如果密码正确返回用户信息,否则返回空 - public Result> CheckUserPassword(string name, string password) - { - var ret = this.GetUserByName(name); - if (!ret.IsSuccessful) - return new(ret.Error); - - if (!ret.Value.HasValue) - return new(Optional.None); - - var user = ret.Value.Value; - - if (user.Password == password) - { - logger.Info($"用户 {name} 密码验证成功"); - return new(user); - } - else - { - logger.Warn($"用户 {name} 密码验证失败"); - return new(Optional.None); - } - } - - /// - /// 绑定用户与实验板 - /// - /// 用户的唯一标识符 - /// 实验板的唯一标识符 - /// 绑定过期时间 - /// 更新的记录数 - public int BindUserToBoard(Guid userId, Guid boardId, DateTime expireTime) - { - // 获取用户信息 - var user = this.UserTable.Where(u => u.ID == userId).FirstOrDefault(); - if (user == null) - { - logger.Error($"未找到用户: {userId}"); - return 0; - } - - // 更新用户的板子绑定信息 - var userResult = this.UserTable - .Where(u => u.ID == userId) - .Set(u => u.BoardID, boardId) - .Set(u => u.BoardExpireTime, expireTime) - .Update(); - - // 更新板子的用户绑定信息 - var boardResult = this.BoardTable - .Where(b => b.ID == boardId) - .Set(b => b.Status, Board.BoardStatus.Busy) - .Set(b => b.OccupiedUserID, userId) - .Set(b => b.OccupiedUserName, user.Name) - .Update(); - - logger.Info($"用户 {userId} ({user.Name}) 已绑定到实验板 {boardId},过期时间: {expireTime}"); - return userResult + boardResult; - } - - /// - /// 解除用户与实验板的绑定 - /// - /// 用户的唯一标识符 - /// 更新的记录数 - public int UnbindUserFromBoard(Guid userId) - { - // 获取用户当前绑定的板子ID - var user = this.UserTable.Where(u => u.ID == userId).FirstOrDefault(); - Guid boardId = user?.BoardID ?? Guid.Empty; - - // 清空用户的板子绑定信息 - var userResult = this.UserTable - .Where(u => u.ID == userId) - .Set(u => u.BoardID, Guid.Empty) - .Set(u => u.BoardExpireTime, (DateTime?)null) - .Update(); - - // 如果用户原本绑定了板子,则清空板子的用户绑定信息 - int boardResult = 0; - if (boardId != Guid.Empty) - { - boardResult = this.BoardTable - .Where(b => b.ID == boardId) - .Set(b => b.Status, Board.BoardStatus.Available) - .Set(b => b.OccupiedUserID, Guid.Empty) - .Set(b => b.OccupiedUserName, (string?)null) - .Update(); - logger.Info($"实验板 {boardId} 状态已设置为空闲,用户绑定信息已清空"); - } - - logger.Info($"用户 {userId} 已解除实验板绑定"); - return userResult + boardResult; - } - - /// - /// 自动分配一个未被占用的IP地址 - /// - /// 分配的IP地址字符串 - public string AllocateIpAddr() - { - var usedIps = this.BoardTable.Select(b => b.IpAddr).ToArray(); - for (int i = 1; i <= 254; i++) - { - string ip = $"169.254.109.{i}"; - if (!usedIps.Contains(ip)) - return ip; - } - throw new Exception("没有可用的IP地址"); - } - - /// - /// 自动分配一个未被占用的MAC地址 - /// - /// 分配的MAC地址字符串 - public string AllocateMacAddr() - { - var usedMacs = this.BoardTable.Select(b => b.MacAddr).ToArray(); - // 以 02-00-00-xx-xx-xx 格式分配,02 表示本地管理地址 - for (int i = 1; i <= 0xFFFFFF; i++) - { - string mac = $"02-00-00-{(i >> 16) & 0xFF:X2}-{(i >> 8) & 0xFF:X2}-{i & 0xFF:X2}"; - if (!usedMacs.Contains(mac)) - return mac; - } - throw new Exception("没有可用的MAC地址"); - } - - /// - /// 添加一块新的 FPGA 板子到数据库 - /// - /// FPGA 板子的名称 - /// 插入的记录数 - public Guid AddBoard(string name) - { - if (string.IsNullOrWhiteSpace(name) || name.Contains('\'') || name.Contains(';')) - { - logger.Error("实验板名称非法,包含不允许的字符"); - throw new ArgumentException("实验板名称非法"); - } - var board = new Board() - { - BoardName = name, - IpAddr = AllocateIpAddr(), - MacAddr = AllocateMacAddr(), - Status = Database.Board.BoardStatus.Disabled, - }; - var result = this.Insert(board); - logger.Info($"新实验板已添加: {name} ({board.IpAddr}:{board.MacAddr})"); - return board.ID; - } - - /// - /// 根据名称删除实验板 - /// - /// 实验板的名称 - /// 删除的记录数 - public int DeleteBoardByName(string name) - { - // 先获取要删除的板子信息 - var board = this.BoardTable.Where(b => b.BoardName == name).FirstOrDefault(); - if (board == null) - { - logger.Warn($"未找到名称为 {name} 的实验板"); - return 0; - } - - // 如果板子被占用,先解除绑定 - if (board.OccupiedUserID != Guid.Empty) - { - this.UserTable - .Where(u => u.ID == board.OccupiedUserID) - .Set(u => u.BoardID, Guid.Empty) - .Set(u => u.BoardExpireTime, (DateTime?)null) - .Update(); - logger.Info($"已解除用户 {board.OccupiedUserID} 与实验板 {name} 的绑定"); - } - - var result = this.BoardTable.Where(b => b.BoardName == name).Delete(); - logger.Info($"实验板已删除: {name},删除记录数: {result}"); - return result; - } - - /// - /// 根据ID删除实验板 - /// - /// 实验板的唯一标识符 - /// 删除的记录数 - public int DeleteBoardByID(Guid id) - { - // 先获取要删除的板子信息 - var board = this.BoardTable.Where(b => b.ID == id).FirstOrDefault(); - if (board == null) - { - logger.Warn($"未找到ID为 {id} 的实验板"); - return 0; - } - - // 如果板子被占用,先解除绑定 - if (board.OccupiedUserID != Guid.Empty) - { - this.UserTable - .Where(u => u.ID == board.OccupiedUserID) - .Set(u => u.BoardID, Guid.Empty) - .Set(u => u.BoardExpireTime, (DateTime?)null) - .Update(); - logger.Info($"已解除用户 {board.OccupiedUserID} 与实验板 {id} 的绑定"); - } - - var result = this.BoardTable.Where(b => b.ID == id).Delete(); - logger.Info($"实验板已删除: {id},删除记录数: {result}"); - return result; - } - - /// - /// 根据实验板ID获取实验板信息 - /// - /// 实验板的唯一标识符 - /// 包含实验板信息的结果,如果未找到则返回空 - public Result> GetBoardByID(Guid id) - { - var boards = this.BoardTable.Where(board => board.ID == id).ToArray(); - - if (boards.Length > 1) - { - logger.Error($"数据库中存在多个相同ID的实验板: {id}"); - return new(new Exception($"数据库中存在多个相同ID的实验板: {id}")); - } - - if (boards.Length == 0) - { - logger.Info($"未找到ID对应的实验板: {id}"); - return new(Optional.None); - } - - logger.Debug($"成功获取实验板信息: {id}"); - return new(boards[0]); - } - - /// - /// 根据用户名获取实验板信息 - /// - /// 用户名 - /// 包含实验板信息的结果,如果未找到则返回空 - public Result> GetBoardByUserName(string userName) - { - var boards = this.BoardTable.Where(board => board.OccupiedUserName == userName).ToArray(); - - if (boards.Length > 1) - { - logger.Error($"数据库中存在多个相同用户名的实验板: {userName}"); - return new(new Exception($"数据库中存在多个相同用户名的实验板: {userName}")); - } - - if (boards.Length == 0) - { - logger.Info($"未找到用户名对应的实验板: {userName}"); - return new(Optional.None); - } - - logger.Debug($"成功获取实验板信息: {userName}"); - return new(boards[0]); - } - - /// - /// 获取所有实验板信息 - /// - /// 所有实验板的数组 - public Board[] GetAllBoard() - { - var boards = this.BoardTable.ToArray(); - logger.Debug($"获取所有实验板,共 {boards.Length} 块"); - return boards; - } - - /// - /// 获取一块可用的实验板并将其状态设置为繁忙 - /// - /// 要分配板子的用户ID - /// 绑定过期时间 - /// 可用的实验板,如果没有可用的板子则返回空 - public Optional GetAvailableBoard(Guid userId, DateTime expireTime) - { - var boards = this.BoardTable.Where( - (board) => board.Status == Database.Board.BoardStatus.Available - ).ToArray(); - - if (boards.Length == 0) - { - logger.Warn("没有可用的实验板"); - return new(null); - } - else - { - var board = boards[0]; - var user = this.UserTable.Where(u => u.ID == userId).FirstOrDefault(); - - if (user == null) - { - logger.Error($"未找到用户: {userId}"); - return new(null); - } - - // 更新板子状态和用户绑定信息 - this.BoardTable - .Where(target => target.ID == board.ID) - .Set(target => target.Status, Board.BoardStatus.Busy) - .Set(target => target.OccupiedUserID, userId) - .Set(target => target.OccupiedUserName, user.Name) - .Update(); - - // 更新用户的板子绑定信息 - this.UserTable - .Where(u => u.ID == userId) - .Set(u => u.BoardID, board.ID) - .Set(u => u.BoardExpireTime, expireTime) - .Update(); - - board.Status = Database.Board.BoardStatus.Busy; - board.OccupiedUserID = userId; - board.OccupiedUserName = user.Name; - - logger.Info($"实验板 {board.BoardName} ({board.ID}) 已分配给用户 {user.Name} ({userId}),过期时间: {expireTime}"); - return new(board); - } - } - - /// - /// [TODO:description] - /// - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:return] - public int UpdateBoardName(Guid boardId, string newName) - { - if (string.IsNullOrWhiteSpace(newName) || newName.Contains('\'') || newName.Contains(';')) - { - logger.Error("实验板名称非法,包含不允许的字符"); - return 0; - } - var result = this.BoardTable - .Where(b => b.ID == boardId) - .Set(b => b.BoardName, newName) - .Update(); - logger.Info($"实验板名称已更新: {boardId} -> {newName}"); - return result; - } - - /// - /// [TODO:description] - /// - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:return] - public int UpdateBoardStatus(Guid boardId, Board.BoardStatus newStatus) - { - var result = this.BoardTable - .Where(b => b.ID == boardId) - .Set(b => b.Status, newStatus) - .Update(); - logger.Info($"TODO"); - return result; - } - - /// - /// 用户表 - /// - public ITable UserTable => this.GetTable(); - - /// - /// FPGA 板子表 - /// - public ITable BoardTable => this.GetTable(); - - /// - /// 实验表 - /// - public ITable ExamTable => this.GetTable(); - - /// - /// 资源表(统一管理实验资源、用户比特流等) - /// - public ITable ResourceTable => this.GetTable(); - - /// - /// 创建新实验 - /// - /// 实验ID - /// 实验名称 - /// 实验描述 - /// 实验标签 - /// 实验难度 - /// 普通用户是否可见 - /// 创建的实验 - public Result CreateExam(string id, string name, string description, string[]? tags = null, int difficulty = 1, bool isVisibleToUsers = true) - { - try - { - // 检查实验ID是否已存在 - var existingExam = this.ExamTable.Where(e => e.ID == id).FirstOrDefault(); - if (existingExam != null) - { - logger.Error($"实验ID已存在: {id}"); - return new(new Exception($"实验ID已存在: {id}")); - } - - var exam = new Exam - { - ID = id, - Name = name, - Description = description, - Difficulty = Math.Max(1, Math.Min(5, difficulty)), - IsVisibleToUsers = isVisibleToUsers, - CreatedTime = DateTime.Now, - UpdatedTime = DateTime.Now - }; - - if (tags != null) - { - exam.SetTagsList(tags); - } - - this.Insert(exam); - logger.Info($"新实验已创建: {id} ({name})"); - return new(exam); - } - catch (Exception ex) - { - logger.Error($"创建实验时出错: {ex.Message}"); - return new(ex); - } - } - - /// - /// 更新实验信息 - /// - /// 实验ID - /// 实验名称 - /// 实验描述 - /// 实验标签 - /// 实验难度 - /// 普通用户是否可见 - /// 更新的记录数 - public Result UpdateExam(string id, string? name = null, string? description = null, string[]? tags = null, int? difficulty = null, bool? isVisibleToUsers = null) - { - try - { - int result = 0; - - if (name != null) - { - result += this.ExamTable.Where(e => e.ID == id).Set(e => e.Name, name).Update(); - } - if (description != null) - { - result += this.ExamTable.Where(e => e.ID == 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 += this.ExamTable.Where(e => e.ID == id).Set(e => e.Tags, tagsString).Update(); - } - if (difficulty.HasValue) - { - result += this.ExamTable.Where(e => e.ID == id).Set(e => e.Difficulty, Math.Max(1, Math.Min(5, difficulty.Value))).Update(); - } - if (isVisibleToUsers.HasValue) - { - result += this.ExamTable.Where(e => e.ID == id).Set(e => e.IsVisibleToUsers, isVisibleToUsers.Value).Update(); - } - - // 更新时间 - this.ExamTable.Where(e => e.ID == id).Set(e => e.UpdatedTime, DateTime.Now).Update(); - - logger.Info($"实验已更新: {id},更新记录数: {result}"); - return new(result); - } - catch (Exception ex) - { - logger.Error($"更新实验时出错: {ex.Message}"); - return new(ex); - } - } - - /// - /// 添加资源 - /// - /// 上传用户ID - /// 资源类型 - /// 资源用途(template 或 user) - /// 资源名称 - /// 资源二进制数据 - /// 所属实验ID(可选) - /// MIME类型(可选,将根据文件扩展名自动确定) - /// 创建的资源 - public Result AddResource(Guid userId, string resourceType, string resourcePurpose, string resourceName, byte[] data, string? examId = null, string? mimeType = null) - { - try - { - // 验证用户是否存在 - var user = this.UserTable.Where(u => u.ID == userId).FirstOrDefault(); - if (user == null) - { - logger.Error($"用户不存在: {userId}"); - return new(new Exception($"用户不存在: {userId}")); - } - - // 如果指定了实验ID,验证实验是否存在 - if (!string.IsNullOrEmpty(examId)) - { - 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类型,根据文件扩展名自动确定 - if (string.IsNullOrEmpty(mimeType)) - { - var extension = Path.GetExtension(resourceName).ToLowerInvariant(); - mimeType = GetMimeTypeFromExtension(extension, resourceName); - } - - var resource = new Resource - { - UserID = userId, - ExamID = examId, - ResourceType = resourceType, - ResourcePurpose = resourcePurpose, - ResourceName = resourceName, - Data = data, - MimeType = mimeType, - UploadTime = DateTime.Now - }; - - 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}"); - return new(ex); - } - } - - /// - /// 获取资源信息列表(返回ID和名称) - /// 资源类型 - /// 实验ID(可选) - /// 资源用途(可选) - /// 用户ID(可选) - /// - /// 资源信息列表 - public Result<(int ID, string Name)[]> GetResourceList(string resourceType, string? examId = null, string? resourcePurpose = null, Guid? userId = null) - { - try - { - 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($"获取资源列表: {resourceType}" + - (examId != null ? $"/{examId}" : "") + - (resourcePurpose != null ? $"/{resourcePurpose}" : "") + - (userId != null ? $"/{userId}" : "") + - $",共 {result.Length} 个资源"); - return new(result); - } - catch (Exception ex) - { - logger.Error($"获取资源列表时出错: {ex.Message}"); - return new(ex); - } - } - - /// - /// 获取完整的资源列表 - /// - /// 实验ID(可选) - /// 资源类型(可选) - /// 资源用途(可选) - /// 用户ID(可选) - /// 完整的资源对象列表 - public Result> 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); - } - } - - /// - /// 根据资源ID获取资源 - /// - /// 资源ID - /// 资源数据 - public Result> GetResourceById(int resourceId) - { - try - { - var resource = this.ResourceTable.Where(r => r.ID == resourceId).FirstOrDefault(); - - if (resource == null) - { - logger.Info($"未找到资源: {resourceId}"); - return new(Optional.None); - } - - logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})"); - return new(resource); - } - catch (Exception ex) - { - logger.Error($"获取资源时出错: {ex.Message}"); - return new(ex); - } - } - - /// - /// 删除资源 - /// - /// 资源ID - /// 删除的记录数 - public Result DeleteResource(int resourceId) - { - try - { - var result = this.ResourceTable.Where(r => r.ID == resourceId).Delete(); - logger.Info($"资源已删除: {resourceId},删除记录数: {result}"); - return new(result); - } - catch (Exception ex) - { - logger.Error($"删除资源时出错: {ex.Message}"); - return new(ex); - } - } - - /// - /// 根据文件扩展名获取MIME类型 - /// - /// 文件扩展名 - /// 文件名(可选,用于特殊文件判断) - /// MIME类型 - private string GetMimeTypeFromExtension(string extension, string fileName = "") - { - // 特殊文件名处理 - if (!string.IsNullOrEmpty(fileName) && fileName.Equals("diagram.json", StringComparison.OrdinalIgnoreCase)) - { - return "application/json"; - } - - return extension.ToLowerInvariant() switch - { - ".png" => "image/png", - ".jpg" or ".jpeg" => "image/jpeg", - ".gif" => "image/gif", - ".bmp" => "image/bmp", - ".svg" => "image/svg+xml", - ".sbit" => "application/octet-stream", - ".bit" => "application/octet-stream", - ".bin" => "application/octet-stream", - ".json" => "application/json", - ".zip" => "application/zip", - ".md" => "text/markdown", - _ => "application/octet-stream" - }; - } - - /// - /// 获取所有实验信息 - /// - /// 所有实验的数组 - public Exam[] GetAllExams() - { - var exams = this.ExamTable.OrderBy(e => e.ID).ToArray(); - logger.Debug($"获取所有实验,共 {exams.Length} 个"); - return exams; - } - - /// - /// 根据实验ID获取实验信息 - /// - /// 实验ID - /// 包含实验信息的结果,如果未找到则返回空 - public Result> GetExamByID(string examId) - { - var exams = this.ExamTable.Where(exam => exam.ID == examId).ToArray(); - - if (exams.Length > 1) - { - logger.Error($"数据库中存在多个相同ID的实验: {examId}"); - return new(new Exception($"数据库中存在多个相同ID的实验: {examId}")); - } - - if (exams.Length == 0) - { - logger.Info($"未找到ID对应的实验: {examId}"); - return new(Optional.None); - } - - logger.Debug($"成功获取实验信息: {examId}"); - return new(exams[0]); - } - - /// - /// 根据文件扩展名获取比特流MIME类型 - /// - /// 文件扩展名 - /// MIME类型 - private string GetBitstreamMimeType(string extension) - { - 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" - }; - } -} diff --git a/server/src/Database/Connection.cs b/server/src/Database/Connection.cs new file mode 100644 index 0000000..1abbe00 --- /dev/null +++ b/server/src/Database/Connection.cs @@ -0,0 +1,98 @@ +using DotNext; +using LinqToDB; +using LinqToDB.Data; + +namespace Database; + +/// +/// 应用程序数据连接类,用于与数据库交互 +/// +public class AppDataConnection : DataConnection +{ + private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + + static readonly string DATABASE_FILEPATH = $"{Global.DataPath}/Database.sqlite"; + + static readonly LinqToDB.DataOptions options = + new LinqToDB.DataOptions().UseSQLite($"Data Source={DATABASE_FILEPATH}"); + + /// + /// 用户表 + /// + public ITable UserTable => this.GetTable(); + + /// + /// FPGA 板子表 + /// + public ITable BoardTable => this.GetTable(); + + /// + /// 实验表 + /// + public ITable ExamTable => this.GetTable(); + + /// + /// 资源表(统一管理实验资源、用户比特流等) + /// + public ITable ResourceTable => this.GetTable(); + + /// + /// 初始化应用程序数据连接 + /// + public AppDataConnection() : base(options) + { + var filePath = Path.GetDirectoryName(DATABASE_FILEPATH); + if (!string.IsNullOrEmpty(filePath) && !Directory.Exists(filePath)) + { + Directory.CreateDirectory(filePath); + } + + if (!Path.Exists(DATABASE_FILEPATH)) + { + logger.Info($"数据库文件不存在,正在创建新数据库: {DATABASE_FILEPATH}"); + LinqToDB.DataProvider.SQLite.SQLiteTools.CreateDatabase(DATABASE_FILEPATH); + this.CreateAllTables(); + var user = new User() + { + Name = "Admin", + EMail = "selfconfusion@gmail.com", + Password = "12345678", + Permission = Database.User.UserPermission.Admin, + }; + this.Insert(user); + logger.Info("默认管理员用户已创建"); + } + else + { + logger.Info($"数据库连接已建立: {DATABASE_FILEPATH}"); + } + } + + /// + /// 创建所有数据库表 + /// + public void CreateAllTables() + { + logger.Info("正在创建数据库表..."); + this.CreateTable(); + this.CreateTable(); + this.CreateTable(); + this.CreateTable(); + logger.Info("数据库表创建完成"); + } + + /// + /// 删除所有数据库表 + /// + public void DropAllTables() + { + logger.Warn("正在删除所有数据库表..."); + this.DropTable(); + this.DropTable(); + this.DropTable(); + this.DropTable(); + logger.Warn("所有数据库表已删除"); + } + + +} diff --git a/server/src/Database/ExamManager.cs b/server/src/Database/ExamManager.cs new file mode 100644 index 0000000..170f385 --- /dev/null +++ b/server/src/Database/ExamManager.cs @@ -0,0 +1,154 @@ +using DotNext; +using LinqToDB; +using LinqToDB.Data; + +namespace Database; + +public class ExamManager +{ + private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + + private readonly AppDataConnection _db; + + public ExamManager(AppDataConnection db) + { + this._db = db; + } + + /// + /// 创建新实验 + /// + /// 实验ID + /// 实验名称 + /// 实验描述 + /// 实验标签 + /// 实验难度 + /// 普通用户是否可见 + /// 创建的实验 + public Result CreateExam(string id, string name, string description, string[]? tags = null, int difficulty = 1, bool isVisibleToUsers = true) + { + try + { + // 检查实验ID是否已存在 + var existingExam = _db.ExamTable.Where(e => e.ID == id).FirstOrDefault(); + if (existingExam != null) + { + logger.Error($"实验ID已存在: {id}"); + return new(new Exception($"实验ID已存在: {id}")); + } + + var exam = new Exam + { + ID = id, + Name = name, + Description = description, + Difficulty = Math.Max(1, Math.Min(5, difficulty)), + IsVisibleToUsers = isVisibleToUsers, + CreatedTime = DateTime.Now, + UpdatedTime = DateTime.Now + }; + + if (tags != null) + { + exam.SetTagsList(tags); + } + + _db.Insert(exam); + logger.Info($"新实验已创建: {id} ({name})"); + return new(exam); + } + catch (Exception ex) + { + logger.Error($"创建实验时出错: {ex.Message}"); + return new(ex); + } + } + + /// + /// 更新实验信息 + /// + /// 实验ID + /// 实验名称 + /// 实验描述 + /// 实验标签 + /// 实验难度 + /// 普通用户是否可见 + /// 更新的记录数 + public Result UpdateExam(string id, string? name = null, string? description = null, string[]? tags = null, int? difficulty = null, bool? isVisibleToUsers = null) + { + try + { + int result = 0; + + if (name != null) + { + result += _db.ExamTable.Where(e => e.ID == id).Set(e => e.Name, name).Update(); + } + if (description != null) + { + result += _db.ExamTable.Where(e => e.ID == 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(); + } + if (difficulty.HasValue) + { + result += _db.ExamTable.Where(e => e.ID == 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(); + } + + // 更新时间 + _db.ExamTable.Where(e => e.ID == id).Set(e => e.UpdatedTime, DateTime.Now).Update(); + + logger.Info($"实验已更新: {id},更新记录数: {result}"); + return new(result); + } + catch (Exception ex) + { + logger.Error($"更新实验时出错: {ex.Message}"); + return new(ex); + } + } + + /// + /// 获取所有实验信息 + /// + /// 所有实验的数组 + public Exam[] GetAllExams() + { + var exams = _db.ExamTable.OrderBy(e => e.ID).ToArray(); + logger.Debug($"获取所有实验,共 {exams.Length} 个"); + return exams; + } + + /// + /// 根据实验ID获取实验信息 + /// + /// 实验ID + /// 包含实验信息的结果,如果未找到则返回空 + public Result> GetExamByID(string examId) + { + var exams = _db.ExamTable.Where(exam => exam.ID == examId).ToArray(); + + if (exams.Length > 1) + { + logger.Error($"数据库中存在多个相同ID的实验: {examId}"); + return new(new Exception($"数据库中存在多个相同ID的实验: {examId}")); + } + + if (exams.Length == 0) + { + logger.Info($"未找到ID对应的实验: {examId}"); + return new(Optional.None); + } + + logger.Debug($"成功获取实验信息: {examId}"); + return new(exams[0]); + } + +} diff --git a/server/src/Database/ResourceManager.cs b/server/src/Database/ResourceManager.cs new file mode 100644 index 0000000..0680c99 --- /dev/null +++ b/server/src/Database/ResourceManager.cs @@ -0,0 +1,357 @@ +using DotNext; +using LinqToDB; +using LinqToDB.Data; +using System.Security.Cryptography; + +namespace Database; + +public class ResourceManager +{ + private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + + private readonly AppDataConnection _db; + + public ResourceManager(AppDataConnection db) + { + this._db = db; + } + + /// + /// 根据文件扩展名获取MIME类型 + /// + /// 文件扩展名 + /// 文件名(可选,用于特殊文件判断) + /// MIME类型 + private string GetMimeTypeFromExtension(string extension, string fileName = "") + { + // 特殊文件名处理 + if (!string.IsNullOrEmpty(fileName) && fileName.Equals("diagram.json", StringComparison.OrdinalIgnoreCase)) + { + return "application/json"; + } + + return extension.ToLowerInvariant() switch + { + ".png" => "image/png", + ".jpg" or ".jpeg" => "image/jpeg", + ".gif" => "image/gif", + ".bmp" => "image/bmp", + ".svg" => "image/svg+xml", + ".bit" => "application/octet-stream", + ".sbit" => "application/octet-stream", + ".bin" => "application/octet-stream", + ".mcs" => "application/octet-stream", + ".hex" => "text/plain", + ".json" => "application/json", + ".zip" => "application/zip", + ".md" => "text/markdown", + _ => "application/octet-stream" + }; + } + + /// + /// 将二进制数据写入指定路径 + /// + /// 目标文件路径 + /// 要写入的二进制数据 + /// 写入是否成功 + public Result WriteBytesToPath(string path, byte[] data) + { + try + { + var filePath = Path.Combine(Global.DataPath, path); + var directory = Path.GetDirectoryName(filePath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + File.WriteAllBytes(filePath, data); + logger.Info($"成功写入文件: {filePath},大小: {data.Length} bytes"); + return new(true); + } + catch (Exception ex) + { + logger.Error($"写入文件时出错: {ex.Message}"); + return new(ex); + } + } + + /// + /// 从指定路径读取二进制数据 + /// + /// 要读取的文件路径 + /// 读取到的二进制数据 + public Result ReadBytesFromPath(string path) + { + try + { + var filePath = Path.Combine(Global.DataPath, path); + if (!File.Exists(filePath)) + { + logger.Error($"文件不存在: {filePath}"); + return new(new Exception($"文件不存在: {filePath}")); + } + var data = File.ReadAllBytes(filePath); + logger.Info($"成功读取文件: {filePath},大小: {data.Length} bytes"); + return new(data); + } + catch (Exception ex) + { + logger.Error($"读取文件时出错: {ex.Message}"); + return new(ex); + } + } + + /// + /// 添加资源 + /// + /// 上传用户ID + /// 资源类型 + /// 资源用途(template 或 user) + /// 资源名称 + /// 资源二进制数据 + /// 所属实验ID(可选) + /// MIME类型(可选,将根据文件扩展名自动确定) + /// 创建的资源 + public Result AddResource( + Guid userId, string resourceType, string resourcePurpose, + string resourceName, byte[] data, string? examId = null, string? mimeType = null) + { + try + { + // 验证用户是否存在 + var user = _db.UserTable.Where(u => u.ID == userId).FirstOrDefault(); + if (user == null) + { + logger.Error($"用户不存在: {userId}"); + return new(new Exception($"用户不存在: {userId}")); + } + + // 如果指定了实验ID,验证实验是否存在 + if (!string.IsNullOrEmpty(examId)) + { + var exam = _db.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类型,根据文件扩展名自动确定 + if (string.IsNullOrEmpty(mimeType)) + { + var extension = Path.GetExtension(resourceName).ToLowerInvariant(); + mimeType = GetMimeTypeFromExtension(extension, resourceName); + } + + // 计算数据的SHA256 + var sha256 = SHA256.HashData(data).ToString(); + if (string.IsNullOrEmpty(sha256)) + { + logger.Error($"SHA256计算失败"); + return new(new Exception("SHA256计算失败")); + } + + var duplicateResource = _db.ResourceTable.Where(r => r.SHA256 == sha256).FirstOrDefault(); + if (duplicateResource != null && duplicateResource.ResourceName == resourceName) + { + logger.Info($"资源已存在: {resourceName}"); + return new(new Exception($"资源已存在: {resourceName}")); + } + + var nowTime = DateTime.Now; + var resource = new Resource + { + UserID = userId, + ExamID = examId, + ResourceType = resourceType, + ResourcePurpose = resourcePurpose, + ResourceName = resourceName, + Path = duplicateResource == null ? + Path.Combine(resourceType, nowTime.ToString("yyyyMMddHH"), resourceName) : + duplicateResource.Path, + SHA256 = sha256, + MimeType = mimeType, + UploadTime = nowTime + }; + + var insertedId = _db.InsertWithIdentity(resource); + resource.ID = Convert.ToInt32(insertedId); + + var writeRet = WriteBytesToPath(resource.Path, data); + if (writeRet.IsSuccessful && writeRet.Value) + { + logger.Info($"新资源已添加: {userId}/{resourceType}/{resourcePurpose}/{resourceName} ({data.Length} bytes)" + + (examId != null ? $" [实验: {examId}]" : "") + $" [ID: {resource.ID}]"); + return new(resource); + } + else + { + _db.ResourceTable.Where(r => r.ID == resource.ID).Delete(); + + logger.Error($"写入资源文件时出错: {writeRet.Error}"); + return new(new Exception(writeRet.Error?.ToString() ?? $"写入失败")); + } + } + catch (Exception ex) + { + logger.Error($"添加资源时出错: {ex.Message}"); + return new(ex); + } + } + + /// + /// 获取资源信息列表(返回ID和名称) + /// 资源类型 + /// 实验ID(可选) + /// 资源用途(可选) + /// 用户ID(可选) + /// + /// 资源信息列表 + public Result<(int ID, string Name)[]> GetResourceList(string resourceType, string? examId = null, string? resourcePurpose = null, Guid? userId = null) + { + try + { + var query = _db.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($"获取资源列表: {resourceType}" + + (examId != null ? $"/{examId}" : "") + + (resourcePurpose != null ? $"/{resourcePurpose}" : "") + + (userId != null ? $"/{userId}" : "") + + $",共 {result.Length} 个资源"); + return new(result); + } + catch (Exception ex) + { + logger.Error($"获取资源列表时出错: {ex.Message}"); + return new(ex); + } + } + + /// + /// 获取完整的资源列表 + /// + /// 实验ID(可选) + /// 资源类型(可选) + /// 资源用途(可选) + /// 用户ID(可选) + /// 完整的资源对象列表 + public Result> GetFullResourceList(string? examId = null, string? resourceType = null, string? resourcePurpose = null, Guid? userId = null) + { + try + { + var query = _db.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); + } + } + + /// + /// 根据资源ID获取资源 + /// + /// 资源ID + /// 资源数据 + public Result> GetResourceById(int resourceId) + { + try + { + var resource = _db.ResourceTable.Where(r => r.ID == resourceId).FirstOrDefault(); + + if (resource == null) + { + logger.Info($"未找到资源: {resourceId}"); + return new(Optional.None); + } + + logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})"); + return new(resource); + } + catch (Exception ex) + { + logger.Error($"获取资源时出错: {ex.Message}"); + return new(ex); + } + } + + /// + /// 删除资源 + /// + /// 资源ID + /// 删除的记录数 + public Result DeleteResource(int resourceId) + { + try + { + var result = _db.ResourceTable.Where(r => r.ID == resourceId).Delete(); + logger.Info($"资源已删除: {resourceId},删除记录数: {result}"); + return new(result); + } + catch (Exception ex) + { + logger.Error($"删除资源时出错: {ex.Message}"); + return new(ex); + } + } + +} diff --git a/server/src/Database/Type.cs b/server/src/Database/Type.cs new file mode 100644 index 0000000..2263fc5 --- /dev/null +++ b/server/src/Database/Type.cs @@ -0,0 +1,341 @@ +using DotNext; +using LinqToDB; +using LinqToDB.Mapping; + +namespace Database; + +/// +/// 用户类,表示用户信息 +/// +public class User +{ + /// + /// 用户的唯一标识符 + /// + [PrimaryKey] + public Guid ID { get; set; } = Guid.NewGuid(); + + /// + /// 用户的名称 + /// + [NotNull] + public required string Name { get; set; } + + /// + /// 用户的电子邮箱 + /// + [NotNull] + public required string EMail { get; set; } + + /// + /// 用户的密码(应该进行哈希处理) + /// + [NotNull] + public required string Password { get; set; } + + /// + /// 用户权限等级 + /// + [NotNull] + public required UserPermission Permission { get; set; } + + /// + /// 绑定的实验板ID,如果未绑定则为空 + /// + [Nullable] + public Guid BoardID { get; set; } + + /// + /// 用户绑定板子的过期时间 + /// + [Nullable] + public DateTime? BoardExpireTime { get; set; } + + /// + /// 用户权限枚举 + /// + public enum UserPermission + { + /// + /// 管理员权限,可以管理用户和实验板 + /// + Admin, + + /// + /// 普通用户权限,只能使用实验板 + /// + Normal, + } +} + +/// +/// FPGA 板子类,表示板子信息 +/// +public class Board +{ + /// + /// FPGA 板子的唯一标识符 + /// + [PrimaryKey] + public Guid ID { get; set; } = Guid.NewGuid(); + + /// + /// FPGA 板子的名称 + /// + [NotNull] + public required string BoardName { get; set; } + + /// + /// FPGA 板子的IP地址 + /// + [NotNull] + public required string IpAddr { get; set; } + + /// + /// FPGA 板子的MAC地址 + /// + [NotNull] + public required string MacAddr { get; set; } + + /// + /// FPGA 板子的通信端口 + /// + [NotNull] + public int Port { get; set; } = 1234; + + /// + /// FPGA 板子的当前状态 + /// + [NotNull] + public required BoardStatus Status { get; set; } + + /// + /// 占用该板子的用户的唯一标识符 + /// + [Nullable] + public Guid OccupiedUserID { get; set; } + + /// + /// 占用该板子的用户的用户名 + /// + [Nullable] + public string? OccupiedUserName { get; set; } + + /// + /// FPGA 板子的固件版本号 + /// + [NotNull] + public string FirmVersion { get; set; } = "1.0.0"; + + /// + /// FPGA 板子状态枚举 + /// + public enum BoardStatus + { + /// + /// 未启用状态,无法被使用 + /// + Disabled, + + /// + /// 繁忙状态,正在被用户使用 + /// + Busy, + + /// + /// 可用状态,可以被分配给用户 + /// + Available, + } +} + +/// +/// 实验类,表示实验信息 +/// +public class Exam +{ + /// + /// 实验的唯一标识符 + /// + [PrimaryKey] + public required string ID { get; set; } + + /// + /// 实验名称 + /// + [NotNull] + public required string Name { get; set; } + + /// + /// 实验描述 + /// + [NotNull] + public required string Description { get; set; } + + /// + /// 实验创建时间 + /// + [NotNull] + public DateTime CreatedTime { get; set; } = DateTime.Now; + + /// + /// 实验最后更新时间 + /// + [NotNull] + public DateTime UpdatedTime { get; set; } = DateTime.Now; + + /// + /// 实验标签(以逗号分隔的字符串) + /// + [NotNull] + public string Tags { get; set; } = ""; + + /// + /// 实验难度(1-5,1为最简单) + /// + [NotNull] + public int Difficulty { get; set; } = 1; + + /// + /// 普通用户是否可见 + /// + [NotNull] + public bool IsVisibleToUsers { get; set; } = true; + + /// + /// 获取标签列表 + /// + /// 标签数组 + public string[] GetTagsList() + { + if (string.IsNullOrWhiteSpace(Tags)) + return Array.Empty(); + + return Tags.Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(tag => tag.Trim()) + .Where(tag => !string.IsNullOrEmpty(tag)) + .ToArray(); + } + + /// + /// 设置标签列表 + /// + /// 标签数组 + public void SetTagsList(string[] tags) + { + Tags = string.Join(",", tags.Where(tag => !string.IsNullOrWhiteSpace(tag)).Select(tag => tag.Trim())); + } +} + +/// +/// 资源类,统一管理实验资源、用户比特流等各类资源 +/// +public class Resource +{ + /// + /// 资源的唯一标识符 + /// + [PrimaryKey, Identity] + public int ID { get; set; } + + /// + /// 上传资源的用户ID + /// + [NotNull] + public required Guid UserID { get; set; } + + /// + /// 所属实验ID(可选,如果不属于特定实验则为空) + /// + [Nullable] + public string? ExamID { get; set; } + + /// + /// 资源类型(images, markdown, bitstream, diagram, project等) + /// + [NotNull] + public required string ResourceType { get; set; } + + /// + /// 资源用途:template(模板)或 user(用户上传) + /// + [NotNull] + public required string ResourcePurpose { get; set; } + + /// + /// 资源名称(包含文件扩展名) + /// + [NotNull] + public required string ResourceName { get; set; } + + /// + /// 资源路径(包含文件名和扩展名) + /// + [NotNull] + public required string Path { get; set; } + + /// + /// 资源SHA256哈希值 + /// + [NotNull] + public required string SHA256 { get; set; } + + /// + /// 资源创建/上传时间 + /// + [NotNull] + public DateTime UploadTime { get; set; } = DateTime.Now; + + /// + /// 资源的MIME类型 + /// + [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 new file mode 100644 index 0000000..72fb290 --- /dev/null +++ b/server/src/Database/UserManager.cs @@ -0,0 +1,458 @@ +using DotNext; +using LinqToDB; +using LinqToDB.Data; + +namespace Database; + +public class UserManager +{ + private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + + private readonly AppDataConnection _db; + + public UserManager(AppDataConnection db) + { + this._db = db; + } + + /// + /// 添加一个新的用户到数据库 + /// + /// 用户的名称 + /// 用户的电子邮箱地址 + /// 用户的密码 + /// 插入的记录数 + public int AddUser(string name, string email, string password) + { + var user = new User() + { + Name = name, + EMail = email, + Password = password, + Permission = Database.User.UserPermission.Normal, + }; + var result = _db.Insert(user); + logger.Info($"新用户已添加: {name} ({email})"); + return result; + } + + /// + /// 根据用户名获取用户信息 + /// + /// 用户名 + /// 包含用户信息的结果,如果未找到或出错则返回相应状态 + public Result> GetUserByName(string name) + { + var user = _db.UserTable.Where((user) => user.Name == name).ToArray(); + + if (user.Length > 1) + { + logger.Error($"数据库中存在多个同名用户: {name}"); + return new(new Exception($"数据库中存在多个同名用户: {name}")); + } + + if (user.Length == 0) + { + logger.Info($"未找到用户: {name}"); + return new(Optional.None); + } + + logger.Debug($"成功获取用户信息: {name}"); + return new(user[0]); + } + + /// + /// 根据电子邮箱获取用户信息 + /// + /// 用户的电子邮箱地址 + /// 包含用户信息的结果,如果未找到或出错则返回相应状态 + public Result> GetUserByEMail(string email) + { + var user = _db.UserTable.Where((user) => user.EMail == email).ToArray(); + + if (user.Length > 1) + { + logger.Error($"数据库中存在多个相同邮箱的用户: {email}"); + return new(new Exception($"数据库中存在多个相同邮箱的用户: {email}")); + } + + if (user.Length == 0) + { + logger.Info($"未找到邮箱对应的用户: {email}"); + return new(Optional.None); + } + + logger.Debug($"成功获取用户信息: {email}"); + return new(user[0]); + } + + /// + /// 验证用户密码 + /// + /// 用户名 + /// 用户密码 + /// 如果密码正确返回用户信息,否则返回空 + public Result> CheckUserPassword(string name, string password) + { + var ret = GetUserByName(name); + if (!ret.IsSuccessful) + return new(ret.Error); + + if (!ret.Value.HasValue) + return new(Optional.None); + + var user = ret.Value.Value; + + if (user.Password == password) + { + logger.Info($"用户 {name} 密码验证成功"); + return new(user); + } + else + { + logger.Warn($"用户 {name} 密码验证失败"); + return new(Optional.None); + } + } + + /// + /// 绑定用户与实验板 + /// + /// 用户的唯一标识符 + /// 实验板的唯一标识符 + /// 绑定过期时间 + /// 更新的记录数 + public int BindUserToBoard(Guid userId, Guid boardId, DateTime expireTime) + { + // 获取用户信息 + var user = _db.UserTable.Where(u => u.ID == userId).FirstOrDefault(); + if (user == null) + { + logger.Error($"未找到用户: {userId}"); + return 0; + } + + // 更新用户的板子绑定信息 + var userResult = _db.UserTable + .Where(u => u.ID == userId) + .Set(u => u.BoardID, boardId) + .Set(u => u.BoardExpireTime, expireTime) + .Update(); + + // 更新板子的用户绑定信息 + var boardResult = _db.BoardTable + .Where(b => b.ID == boardId) + .Set(b => b.Status, Board.BoardStatus.Busy) + .Set(b => b.OccupiedUserID, userId) + .Set(b => b.OccupiedUserName, user.Name) + .Update(); + + logger.Info($"用户 {userId} ({user.Name}) 已绑定到实验板 {boardId},过期时间: {expireTime}"); + return userResult + boardResult; + } + + /// + /// 解除用户与实验板的绑定 + /// + /// 用户的唯一标识符 + /// 更新的记录数 + public int UnbindUserFromBoard(Guid userId) + { + // 获取用户当前绑定的板子ID + var user = _db.UserTable.Where(u => u.ID == userId).FirstOrDefault(); + Guid boardId = user?.BoardID ?? Guid.Empty; + + // 清空用户的板子绑定信息 + var userResult = _db.UserTable + .Where(u => u.ID == userId) + .Set(u => u.BoardID, Guid.Empty) + .Set(u => u.BoardExpireTime, (DateTime?)null) + .Update(); + + // 如果用户原本绑定了板子,则清空板子的用户绑定信息 + int boardResult = 0; + if (boardId != Guid.Empty) + { + boardResult = _db.BoardTable + .Where(b => b.ID == boardId) + .Set(b => b.Status, Board.BoardStatus.Available) + .Set(b => b.OccupiedUserID, Guid.Empty) + .Set(b => b.OccupiedUserName, (string?)null) + .Update(); + logger.Info($"实验板 {boardId} 状态已设置为空闲,用户绑定信息已清空"); + } + + logger.Info($"用户 {userId} 已解除实验板绑定"); + return userResult + boardResult; + } + + /// + /// 自动分配一个未被占用的IP地址 + /// + /// 分配的IP地址字符串 + public string AllocateIpAddr() + { + var usedIps = _db.BoardTable.Select(b => b.IpAddr).ToArray(); + for (int i = 1; i <= 254; i++) + { + string ip = $"169.254.109.{i}"; + if (!usedIps.Contains(ip)) + return ip; + } + throw new Exception("没有可用的IP地址"); + } + + /// + /// 自动分配一个未被占用的MAC地址 + /// + /// 分配的MAC地址字符串 + public string AllocateMacAddr() + { + var usedMacs = _db.BoardTable.Select(b => b.MacAddr).ToArray(); + // 以 02-00-00-xx-xx-xx 格式分配,02 表示本地管理地址 + for (int i = 1; i <= 0xFFFFFF; i++) + { + string mac = $"02-00-00-{(i >> 16) & 0xFF:X2}-{(i >> 8) & 0xFF:X2}-{i & 0xFF:X2}"; + if (!usedMacs.Contains(mac)) + return mac; + } + throw new Exception("没有可用的MAC地址"); + } + + /// + /// 添加一块新的 FPGA 板子到数据库 + /// + /// FPGA 板子的名称 + /// 插入的记录数 + public Guid AddBoard(string name) + { + if (string.IsNullOrWhiteSpace(name) || name.Contains('\'') || name.Contains(';')) + { + logger.Error("实验板名称非法,包含不允许的字符"); + throw new ArgumentException("实验板名称非法"); + } + var board = new Board() + { + BoardName = name, + IpAddr = AllocateIpAddr(), + MacAddr = AllocateMacAddr(), + Status = Database.Board.BoardStatus.Disabled, + }; + var result = _db.Insert(board); + logger.Info($"新实验板已添加: {name} ({board.IpAddr}:{board.MacAddr})"); + return board.ID; + } + + /// + /// 根据名称删除实验板 + /// + /// 实验板的名称 + /// 删除的记录数 + public int DeleteBoardByName(string name) + { + // 先获取要删除的板子信息 + var board = _db.BoardTable.Where(b => b.BoardName == name).FirstOrDefault(); + if (board == null) + { + logger.Warn($"未找到名称为 {name} 的实验板"); + return 0; + } + + // 如果板子被占用,先解除绑定 + if (board.OccupiedUserID != Guid.Empty) + { + _db.UserTable + .Where(u => u.ID == board.OccupiedUserID) + .Set(u => u.BoardID, Guid.Empty) + .Set(u => u.BoardExpireTime, (DateTime?)null) + .Update(); + logger.Info($"已解除用户 {board.OccupiedUserID} 与实验板 {name} 的绑定"); + } + + var result = _db.BoardTable.Where(b => b.BoardName == name).Delete(); + logger.Info($"实验板已删除: {name},删除记录数: {result}"); + return result; + } + + /// + /// 根据ID删除实验板 + /// + /// 实验板的唯一标识符 + /// 删除的记录数 + public int DeleteBoardByID(Guid id) + { + // 先获取要删除的板子信息 + var board = _db.BoardTable.Where(b => b.ID == id).FirstOrDefault(); + if (board == null) + { + logger.Warn($"未找到ID为 {id} 的实验板"); + return 0; + } + + // 如果板子被占用,先解除绑定 + if (board.OccupiedUserID != Guid.Empty) + { + _db.UserTable + .Where(u => u.ID == board.OccupiedUserID) + .Set(u => u.BoardID, Guid.Empty) + .Set(u => u.BoardExpireTime, (DateTime?)null) + .Update(); + logger.Info($"已解除用户 {board.OccupiedUserID} 与实验板 {id} 的绑定"); + } + + var result = _db.BoardTable.Where(b => b.ID == id).Delete(); + logger.Info($"实验板已删除: {id},删除记录数: {result}"); + return result; + } + + /// + /// 根据实验板ID获取实验板信息 + /// + /// 实验板的唯一标识符 + /// 包含实验板信息的结果,如果未找到则返回空 + public Result> GetBoardByID(Guid id) + { + var boards = _db.BoardTable.Where(board => board.ID == id).ToArray(); + + if (boards.Length > 1) + { + logger.Error($"数据库中存在多个相同ID的实验板: {id}"); + return new(new Exception($"数据库中存在多个相同ID的实验板: {id}")); + } + + if (boards.Length == 0) + { + logger.Info($"未找到ID对应的实验板: {id}"); + return new(Optional.None); + } + + logger.Debug($"成功获取实验板信息: {id}"); + return new(boards[0]); + } + + /// + /// 根据用户名获取实验板信息 + /// + /// 用户名 + /// 包含实验板信息的结果,如果未找到则返回空 + public Result> GetBoardByUserName(string userName) + { + var boards = _db.BoardTable.Where(board => board.OccupiedUserName == userName).ToArray(); + + if (boards.Length > 1) + { + logger.Error($"数据库中存在多个相同用户名的实验板: {userName}"); + return new(new Exception($"数据库中存在多个相同用户名的实验板: {userName}")); + } + + if (boards.Length == 0) + { + logger.Info($"未找到用户名对应的实验板: {userName}"); + return new(Optional.None); + } + + logger.Debug($"成功获取实验板信息: {userName}"); + return new(boards[0]); + } + + /// + /// 获取所有实验板信息 + /// + /// 所有实验板的数组 + public Board[] GetAllBoard() + { + var boards = _db.BoardTable.ToArray(); + logger.Debug($"获取所有实验板,共 {boards.Length} 块"); + return boards; + } + + /// + /// 获取一块可用的实验板并将其状态设置为繁忙 + /// + /// 要分配板子的用户ID + /// 绑定过期时间 + /// 可用的实验板,如果没有可用的板子则返回空 + public Optional GetAvailableBoard(Guid userId, DateTime expireTime) + { + var boards = _db.BoardTable.Where( + (board) => board.Status == Database.Board.BoardStatus.Available + ).ToArray(); + + if (boards.Length == 0) + { + logger.Warn("没有可用的实验板"); + return new(null); + } + else + { + var board = boards[0]; + var user = _db.UserTable.Where(u => u.ID == userId).FirstOrDefault(); + + if (user == null) + { + logger.Error($"未找到用户: {userId}"); + return new(null); + } + + // 更新板子状态和用户绑定信息 + _db.BoardTable + .Where(target => target.ID == board.ID) + .Set(target => target.Status, Board.BoardStatus.Busy) + .Set(target => target.OccupiedUserID, userId) + .Set(target => target.OccupiedUserName, user.Name) + .Update(); + + // 更新用户的板子绑定信息 + _db.UserTable + .Where(u => u.ID == userId) + .Set(u => u.BoardID, board.ID) + .Set(u => u.BoardExpireTime, expireTime) + .Update(); + + board.Status = Database.Board.BoardStatus.Busy; + board.OccupiedUserID = userId; + board.OccupiedUserName = user.Name; + + logger.Info($"实验板 {board.BoardName} ({board.ID}) 已分配给用户 {user.Name} ({userId}),过期时间: {expireTime}"); + return new(board); + } + } + + /// + /// [TODO:description] + /// + /// [TODO:parameter] + /// [TODO:parameter] + /// [TODO:return] + public int UpdateBoardName(Guid boardId, string newName) + { + if (string.IsNullOrWhiteSpace(newName) || newName.Contains('\'') || newName.Contains(';')) + { + logger.Error("实验板名称非法,包含不允许的字符"); + return 0; + } + var result = _db.BoardTable + .Where(b => b.ID == boardId) + .Set(b => b.BoardName, newName) + .Update(); + logger.Info($"实验板名称已更新: {boardId} -> {newName}"); + return result; + } + + /// + /// [TODO:description] + /// + /// [TODO:parameter] + /// [TODO:parameter] + /// [TODO:return] + public int UpdateBoardStatus(Guid boardId, Board.BoardStatus newStatus) + { + var result = _db.BoardTable + .Where(b => b.ID == boardId) + .Set(b => b.Status, newStatus) + .Update(); + logger.Info($"TODO"); + return result; + } + +} diff --git a/server/src/Hubs/JtagHub.cs b/server/src/Hubs/JtagHub.cs index f768f87..51cf78a 100644 --- a/server/src/Hubs/JtagHub.cs +++ b/server/src/Hubs/JtagHub.cs @@ -28,22 +28,24 @@ public interface IJtagReceiver public class JtagHub : Hub, IJtagHub { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + + private readonly IHubContext _hubContext; + private readonly Database.UserManager _userManager; + private static ConcurrentDictionary FreqTable = new(); private static ConcurrentDictionary CancellationTokenSourceTable = new(); - private readonly IHubContext _hubContext; - - public JtagHub(IHubContext hubContext) + public JtagHub(IHubContext hubContext, Database.UserManager userManager) { _hubContext = hubContext; + _userManager = userManager; } private Optional GetJtagClient(string userName) { try { - using var db = new Database.AppDataConnection(); - var board = db.GetBoardByUserName(userName); + var board = _userManager.GetBoardByUserName(userName); if (!board.IsSuccessful) { logger.Error($"Find Board {board.Value.Value.ID} failed because {board.Error}"); @@ -97,7 +99,7 @@ public class JtagHub : Hub, IJtagHub return false; } - await SetBoundaryScanFreq(freq); + SetBoundaryScanFreq(freq); var cts = new CancellationTokenSource(); CancellationTokenSourceTable.AddOrUpdate(userName, cts, (key, value) => cts); diff --git a/server/src/Services/HttpHdmiVideoStreamService.cs b/server/src/Services/HttpHdmiVideoStreamService.cs index d6475cf..1e762ee 100644 --- a/server/src/Services/HttpHdmiVideoStreamService.cs +++ b/server/src/Services/HttpHdmiVideoStreamService.cs @@ -15,15 +15,23 @@ public class HdmiVideoStreamEndpoint public class HttpHdmiVideoStreamService : BackgroundService { private readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + + private readonly Database.UserManager _userManager; + private HttpListener? _httpListener; private readonly int _serverPort = 4322; private readonly ConcurrentDictionary _hdmiInDict = new(); private readonly ConcurrentDictionary _hdmiInCtsDict = new(); + public HttpHdmiVideoStreamService(Database.UserManager userManager) + { + _userManager = userManager; + } + public override async Task StartAsync(CancellationToken cancellationToken) { _httpListener = new HttpListener(); - _httpListener.Prefixes.Add($"http://{Global.localhost}:{_serverPort}/"); + _httpListener.Prefixes.Add($"http://{Global.LocalHost}:{_serverPort}/"); _httpListener.Start(); logger.Info($"HDMI Video Stream Service started on port {_serverPort}"); @@ -133,14 +141,7 @@ public class HttpHdmiVideoStreamService : BackgroundService return hdmiIn; } - var db = new Database.AppDataConnection(); - if (db == null) - { - logger.Error("Failed to create HdmiIn instance"); - return null; - } - - var boardRet = db.GetBoardByID(Guid.Parse(boardId)); + var boardRet = _userManager.GetBoardByID(Guid.Parse(boardId)); if (!boardRet.IsSuccessful || !boardRet.Value.HasValue) { logger.Error($"Failed to get board with ID {boardId}"); @@ -366,8 +367,7 @@ public class HttpHdmiVideoStreamService : BackgroundService /// 返回所有可用的HDMI视频流终端点列表 public List? GetAllVideoEndpoints() { - var db = new Database.AppDataConnection(); - var boards = db?.GetAllBoard(); + var boards = _userManager.GetAllBoard(); if (boards == null) return null; @@ -377,9 +377,9 @@ public class HttpHdmiVideoStreamService : BackgroundService endpoints.Add(new HdmiVideoStreamEndpoint { BoardId = board.ID.ToString(), - MjpegUrl = $"http://{Global.localhost}:{_serverPort}/mjpeg?boardId={board.ID}", - VideoUrl = $"http://{Global.localhost}:{_serverPort}/video?boardId={board.ID}", - SnapshotUrl = $"http://{Global.localhost}:{_serverPort}/snapshot?boardId={board.ID}" + MjpegUrl = $"http://{Global.LocalHost}:{_serverPort}/mjpeg?boardId={board.ID}", + VideoUrl = $"http://{Global.LocalHost}:{_serverPort}/video?boardId={board.ID}", + SnapshotUrl = $"http://{Global.LocalHost}:{_serverPort}/snapshot?boardId={board.ID}" }); } return endpoints; @@ -395,9 +395,9 @@ public class HttpHdmiVideoStreamService : BackgroundService return new HdmiVideoStreamEndpoint { BoardId = boardId, - MjpegUrl = $"http://{Global.localhost}:{_serverPort}/mjpeg?boardId={boardId}", - VideoUrl = $"http://{Global.localhost}:{_serverPort}/video?boardId={boardId}", - SnapshotUrl = $"http://{Global.localhost}:{_serverPort}/snapshot?boardId={boardId}" + MjpegUrl = $"http://{Global.LocalHost}:{_serverPort}/mjpeg?boardId={boardId}", + VideoUrl = $"http://{Global.LocalHost}:{_serverPort}/video?boardId={boardId}", + SnapshotUrl = $"http://{Global.LocalHost}:{_serverPort}/snapshot?boardId={boardId}" }; } } diff --git a/server/src/Services/HttpVideoStreamService.cs b/server/src/Services/HttpVideoStreamService.cs index f06967e..8bcbcce 100644 --- a/server/src/Services/HttpVideoStreamService.cs +++ b/server/src/Services/HttpVideoStreamService.cs @@ -87,6 +87,8 @@ public class HttpVideoStreamService : BackgroundService { private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + private readonly Database.UserManager _userManager; + private HttpListener? _httpListener; private readonly int _serverPort = 4321; @@ -99,13 +101,18 @@ public class HttpVideoStreamService : BackgroundService private readonly object _usbCameraLock = new object(); #endif + public HttpVideoStreamService(Database.UserManager userManager) + { + _userManager = userManager; + } + /// /// 初始化 HttpVideoStreamService /// public override async Task StartAsync(CancellationToken cancellationToken) { _httpListener = new HttpListener(); - _httpListener.Prefixes.Add($"http://{Global.localhost}:{_serverPort}/"); + _httpListener.Prefixes.Add($"http://{Global.LocalHost}:{_serverPort}/"); _httpListener.Start(); logger.Info($"Video Stream Service started on port {_serverPort}"); @@ -147,14 +154,7 @@ public class HttpVideoStreamService : BackgroundService return client; } - var db = new Database.AppDataConnection(); - if (db == null) - { - logger.Error("Failed to create HdmiIn instance"); - return null; - } - - var boardRet = db.GetBoardByID(Guid.Parse(boardId)); + var boardRet = _userManager.GetBoardByID(Guid.Parse(boardId)); if (!boardRet.IsSuccessful || !boardRet.Value.HasValue) { logger.Error($"Failed to get board with ID {boardId}"); @@ -675,9 +675,9 @@ public class HttpVideoStreamService : BackgroundService return new VideoEndpoint { BoardId = boardId, - MjpegUrl = $"http://{Global.localhost}:{_serverPort}/mjpeg?boardId={boardId}", - VideoUrl = $"http://{Global.localhost}:{_serverPort}/video?boardId={boardId}", - SnapshotUrl = $"http://{Global.localhost}:{_serverPort}/snapshot?boardId={boardId}", + MjpegUrl = $"http://{Global.LocalHost}:{_serverPort}/mjpeg?boardId={boardId}", + VideoUrl = $"http://{Global.LocalHost}:{_serverPort}/video?boardId={boardId}", + SnapshotUrl = $"http://{Global.LocalHost}:{_serverPort}/snapshot?boardId={boardId}", Resolution = $"{client.FrameWidth}x{client.FrameHeight}", FrameRate = client.FrameRate };