diff --git a/server/src/Controllers/DataController.cs b/server/src/Controllers/DataController.cs index c2cb10a..91b2a3c 100644 --- a/server/src/Controllers/DataController.cs +++ b/server/src/Controllers/DataController.cs @@ -41,6 +41,11 @@ public class DataController : ControllerBase /// 用户关联的板卡ID /// public Guid BoardID { get; set; } + + /// + /// 用户绑定板子的过期时间 + /// + public DateTime? BoardExpireTime { get; set; } } /// @@ -148,6 +153,7 @@ public class DataController : ControllerBase Name = user.Name, EMail = user.EMail, BoardID = user.BoardID, + BoardExpireTime = user.BoardExpireTime, }); } @@ -194,32 +200,32 @@ public class DataController : ControllerBase /// /// 获取一个空闲的实验板(普通用户权限) /// + /// 绑定持续时间(小时),默认为1小时 [Authorize] [HttpGet("GetAvailableBoard")] [EnableCors("Users")] [ProducesResponseType(typeof(Database.Board), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public IActionResult GetAvailableBoard() + public IActionResult GetAvailableBoard(int durationHours = 1) { try { - using var db = new Database.AppDataConnection(); - var boardOpt = db.GetAvailableBoard(); - if (!boardOpt.HasValue) - return NotFound("没有可用的实验板"); - - // 绑定用户与实验板 var userName = User.Identity?.Name; if (string.IsNullOrEmpty(userName)) return Unauthorized("未找到用户名信息"); + using var db = new Database.AppDataConnection(); var userRet = db.GetUserByName(userName); if (!userRet.IsSuccessful || !userRet.Value.HasValue) return BadRequest("用户不存在"); var user = userRet.Value.Value; - db.BindUserToBoard(user.ID, boardOpt.Value.ID); + var expireTime = DateTime.UtcNow.AddHours(durationHours); + + var boardOpt = db.GetAvailableBoard(user.ID, expireTime); + if (!boardOpt.HasValue) + return NotFound("没有可用的实验板"); return Ok(boardOpt.Value); } @@ -230,6 +236,67 @@ public class DataController : ControllerBase } } + /// + /// 解除当前用户绑定的实验板(普通用户权限) + /// + [Authorize] + [HttpPost("UnbindBoard")] + [EnableCors("Users")] + [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult UnbindBoard() + { + try + { + var userName = User.Identity?.Name; + if (string.IsNullOrEmpty(userName)) + return Unauthorized("未找到用户名信息"); + + using var db = new Database.AppDataConnection(); + var userRet = db.GetUserByName(userName); + if (!userRet.IsSuccessful || !userRet.Value.HasValue) + return BadRequest("用户不存在"); + + var user = userRet.Value.Value; + var result = db.UnbindUserFromBoard(user.ID); + return Ok(result > 0); + } + catch (Exception ex) + { + logger.Error(ex, "解除实验板绑定时发生异常"); + return StatusCode(StatusCodes.Status500InternalServerError, "解除失败,请稍后重试"); + } + } + + /// + /// 用户根据实验板ID获取实验板信息(普通用户权限) + /// + [Authorize] + [HttpGet("GetBoardByID")] + [EnableCors("Users")] + [ProducesResponseType(typeof(Database.Board), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult GetBoardByID(Guid id) + { + try + { + using var db = new Database.AppDataConnection(); + var ret = db.GetBoardByID(id); + if (!ret.IsSuccessful) + return StatusCode(StatusCodes.Status500InternalServerError, "数据库操作失败"); + if (!ret.Value.HasValue) + return NotFound("未找到对应的实验板"); + return Ok(ret.Value.Value); + } + catch (Exception ex) + { + logger.Error(ex, "获取实验板信息时发生异常"); + return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试"); + } + } + /// /// 新增板子(管理员权限) /// diff --git a/server/src/Controllers/JtagController.cs b/server/src/Controllers/JtagController.cs index 8411454..74fedd5 100644 --- a/server/src/Controllers/JtagController.cs +++ b/server/src/Controllers/JtagController.cs @@ -1,13 +1,15 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; namespace server.Controllers; /// -/// Jtag API +/// JTAG 控制器 - 提供 JTAG 相关的 API 操作 /// [ApiController] [Route("api/[controller]")] +[Authorize] // 添加用户认证要求 public class JtagController : ControllerBase { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); @@ -15,134 +17,193 @@ public class JtagController : ControllerBase private const string BITSTREAM_PATH = "bitstream/Jtag"; /// - /// 页面 + /// 控制器首页信息 /// + /// 控制器描述信息 [HttpGet] + [EnableCors("Users")] + [ProducesResponseType(typeof(string), StatusCodes.Status200OK)] public string Index() { + logger.Info($"User {User.Identity?.Name} accessed Jtag controller index"); return "This is Jtag Controller"; } /// - /// 获取Jtag ID Code + /// 获取 JTAG 设备的 ID Code /// - /// 设备地址 - /// 设备端口 + /// JTAG 设备地址 + /// JTAG 设备端口 + /// 设备的 ID Code [HttpGet("GetDeviceIDCode")] [EnableCors("Users")] [ProducesResponseType(typeof(uint), StatusCodes.Status200OK)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async ValueTask GetDeviceIDCode(string address, int port) { - var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); - var ret = await jtagCtrl.ReadIDCode(); + logger.Info($"User {User.Identity?.Name} requesting device ID code from {address}:{port}"); - if (ret.IsSuccessful) + try { - logger.Info($"Get device {address} ID code: 0x{ret.Value:X4}"); - return TypedResults.Ok(ret.Value); + var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); + var ret = await jtagCtrl.ReadIDCode(); + + if (ret.IsSuccessful) + { + logger.Info($"User {User.Identity?.Name} successfully got device {address} ID code: 0x{ret.Value:X8}"); + return TypedResults.Ok(ret.Value); + } + else + { + logger.Error($"User {User.Identity?.Name} failed to get device {address} ID code: {ret.Error}"); + return TypedResults.InternalServerError(ret.Error); + } } - else + catch (Exception ex) { - logger.Error(ret.Error); - return TypedResults.InternalServerError(ret.Error); + logger.Error(ex, $"User {User.Identity?.Name} encountered exception while getting device {address} ID code"); + return TypedResults.InternalServerError(ex); } } /// - /// 获取状态寄存器 + /// 读取 JTAG 设备的状态寄存器 /// - /// 设备地址 - /// 设备端口 + /// JTAG 设备地址 + /// JTAG 设备端口 + /// 状态寄存器的原始值、二进制表示和解码值 [HttpGet("ReadStatusReg")] + [EnableCors("Users")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async ValueTask ReadStatusReg(string address, int port) { - var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); - var ret = await jtagCtrl.ReadStatusReg(); + logger.Info($"User {User.Identity?.Name} requesting status register from {address}:{port}"); - if (ret.IsSuccessful) + try { - var binaryValue = Common.String.Reverse(Convert.ToString(ret.Value, 2).PadLeft(32, '0')); - var decodeValue = new Peripherals.JtagClient.JtagStatusReg(ret.Value); - logger.Info($"Read device {address} Status Register: \n\t 0b{binaryValue} \n\t {decodeValue}"); - return TypedResults.Ok(new + var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); + var ret = await jtagCtrl.ReadStatusReg(); + + if (ret.IsSuccessful) { - original = ret.Value, - binaryValue, - decodeValue, - }); + var binaryValue = Common.String.Reverse(Convert.ToString(ret.Value, 2).PadLeft(32, '0')); + var decodeValue = new Peripherals.JtagClient.JtagStatusReg(ret.Value); + logger.Info($"User {User.Identity?.Name} successfully read device {address} Status Register: \n\t 0b{binaryValue} \n\t {decodeValue}"); + return TypedResults.Ok(new + { + original = ret.Value, + binaryValue, + decodeValue, + }); + } + else + { + logger.Error($"User {User.Identity?.Name} failed to read device {address} status register: {ret.Error}"); + return TypedResults.InternalServerError(ret.Error); + } } - else + catch (Exception ex) { - logger.Error(ret.Error); - return TypedResults.InternalServerError(ret.Error); + logger.Error(ex, $"User {User.Identity?.Name} encountered exception while reading device {address} status register"); + return TypedResults.InternalServerError(ex); } } /// - /// 上传比特流文件 + /// 上传比特流文件到服务器 /// - /// 设备地址 + /// 目标设备地址 /// 比特流文件 + /// 上传结果 [HttpPost("UploadBitstream")] [EnableCors("Users")] [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async ValueTask UploadBitstream(string address, IFormFile file) { + logger.Info($"User {User.Identity?.Name} uploading bitstream for device {address}"); + if (file == null || file.Length == 0) + { + logger.Warn($"User {User.Identity?.Name} attempted to upload empty file for device {address}"); return TypedResults.BadRequest("未选择文件"); - - // 生成安全的文件名(避免路径遍历攻击) - var fileName = Path.GetRandomFileName(); - var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}"); - - // 如果存在文件,则删除原文件再上传 - if (Directory.Exists(uploadsFolder)) - { - Directory.Delete(uploadsFolder, true); - } - Directory.CreateDirectory(uploadsFolder); - - var filePath = Path.Combine(uploadsFolder, fileName); - - using (var stream = new FileStream(filePath, FileMode.Create)) - { - await file.CopyToAsync(stream); } - logger.Info($"Device {address} Upload Bitstream Successfully"); - return TypedResults.Ok(true); + try + { + // 生成安全的文件名(避免路径遍历攻击) + var fileName = Path.GetRandomFileName(); + var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}"); + + // 如果存在文件,则删除原文件再上传 + if (Directory.Exists(uploadsFolder)) + { + Directory.Delete(uploadsFolder, true); + logger.Info($"User {User.Identity?.Name} removed existing bitstream folder for device {address}"); + } + Directory.CreateDirectory(uploadsFolder); + + var filePath = Path.Combine(uploadsFolder, fileName); + + using (var stream = new FileStream(filePath, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + + logger.Info($"User {User.Identity?.Name} successfully uploaded bitstream for device {address}, file size: {file.Length} bytes"); + return TypedResults.Ok(true); + } + catch (Exception ex) + { + logger.Error(ex, $"User {User.Identity?.Name} failed to upload bitstream for device {address}"); + return TypedResults.InternalServerError(ex); + } } /// - /// 通过Jtag下载比特流文件 + /// 通过 JTAG 下载比特流文件到 FPGA 设备 /// - /// 设备地址 - /// 设备端口 + /// JTAG 设备地址 + /// JTAG 设备端口 + /// 下载结果 [HttpPost("DownloadBitstream")] [EnableCors("Users")] [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async ValueTask DownloadBitstream(string address, int port) { + logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port}"); + // 检查文件 var fileDir = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}"); if (!Directory.Exists(fileDir)) + { + logger.Warn($"User {User.Identity?.Name} attempted to download non-existent bitstream for device {address}"); return TypedResults.BadRequest("Empty bitstream, Please upload it first"); + } try { // 读取文件 var filePath = Directory.GetFiles(fileDir)[0]; + logger.Info($"User {User.Identity?.Name} reading bitstream file: {filePath}"); using (var fileStream = System.IO.File.Open(filePath, System.IO.FileMode.Open)) { if (fileStream is null || fileStream.Length <= 0) + { + logger.Warn($"User {User.Identity?.Name} found invalid bitstream file for device {address}"); return TypedResults.BadRequest("Wrong bitstream, Please upload it again"); + } + + logger.Info($"User {User.Identity?.Name} processing bitstream file of size: {fileStream.Length} bytes"); // 定义缓冲区大小: 32KB byte[] buffer = new byte[32 * 1024]; @@ -158,7 +219,10 @@ public class JtagController : ControllerBase // 反转 32bits var retBuffer = Common.Number.ReverseBytes(buffer, 4); if (!retBuffer.IsSuccessful) + { + logger.Error($"User {User.Identity?.Name} failed to reverse bytes: {retBuffer.Error}"); return TypedResults.InternalServerError(retBuffer.Error); + } revBuffer = retBuffer.Value; for (int i = 0; i < revBuffer.Length; i++) @@ -172,6 +236,7 @@ public class JtagController : ControllerBase // 将所有数据转换为字节数组(注意:如果文件非常大,可能不适合完全加载到内存) var fileBytes = memoryStream.ToArray(); + logger.Info($"User {User.Identity?.Name} processed {totalBytesRead} bytes for device {address}"); // 下载比特流 var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); @@ -179,100 +244,140 @@ public class JtagController : ControllerBase if (ret.IsSuccessful) { - logger.Info($"Device {address} dowload bitstream successfully"); + logger.Info($"User {User.Identity?.Name} successfully downloaded bitstream to device {address}"); return TypedResults.Ok(ret.Value); } else { - logger.Error(ret.Error); + logger.Error($"User {User.Identity?.Name} failed to download bitstream to device {address}: {ret.Error}"); return TypedResults.InternalServerError(ret.Error); } } - } - } - catch (Exception error) + catch (Exception ex) { - return TypedResults.InternalServerError(error); - } - finally - { - + logger.Error(ex, $"User {User.Identity?.Name} encountered exception while downloading bitstream to device {address}"); + return TypedResults.InternalServerError(ex); } } /// - /// [TODO:description] + /// 执行边界扫描,获取所有端口状态 /// - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:return] + /// JTAG 设备地址 + /// JTAG 设备端口 + /// 边界扫描结果 [HttpPost("BoundaryScanAllPorts")] [EnableCors("Users")] [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async ValueTask BoundaryScanAllPorts(string address, int port) { - var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); - var ret = await jtagCtrl.BoundaryScan(); - if (!ret.IsSuccessful) - { - if (ret.Error is ArgumentException) - return TypedResults.BadRequest(ret.Error); - else return TypedResults.InternalServerError(ret.Error); - } + logger.Info($"User {User.Identity?.Name} initiating boundary scan for all ports on device {address}:{port}"); - return TypedResults.Ok(ret.Value); + try + { + var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); + var ret = await jtagCtrl.BoundaryScan(); + + if (!ret.IsSuccessful) + { + logger.Error($"User {User.Identity?.Name} boundary scan failed for device {address}: {ret.Error}"); + if (ret.Error is ArgumentException) + return TypedResults.BadRequest(ret.Error); + else + return TypedResults.InternalServerError(ret.Error); + } + + logger.Info($"User {User.Identity?.Name} successfully completed boundary scan for device {address}"); + return TypedResults.Ok(ret.Value); + } + catch (Exception ex) + { + logger.Error(ex, $"User {User.Identity?.Name} encountered exception during boundary scan for device {address}"); + return TypedResults.InternalServerError(ex); + } } /// - /// [TODO:description] + /// 执行逻辑端口边界扫描 /// - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:return] + /// JTAG 设备地址 + /// JTAG 设备端口 + /// 逻辑端口状态字典 [HttpPost("BoundaryScanLogicalPorts")] [EnableCors("Users")] [ProducesResponseType(typeof(Dictionary), StatusCodes.Status200OK)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async ValueTask BoundaryScanLogicalPorts(string address, int port) { - var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); - var ret = await jtagCtrl.BoundaryScanLogicalPorts(); - if (!ret.IsSuccessful) - { - if (ret.Error is ArgumentException) - return TypedResults.BadRequest(ret.Error); - else return TypedResults.InternalServerError(ret.Error); - } + logger.Info($"User {User.Identity?.Name} initiating logical ports boundary scan on device {address}:{port}"); - return TypedResults.Ok(ret.Value); + try + { + var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); + var ret = await jtagCtrl.BoundaryScanLogicalPorts(); + + if (!ret.IsSuccessful) + { + logger.Error($"User {User.Identity?.Name} logical ports boundary scan failed for device {address}: {ret.Error}"); + if (ret.Error is ArgumentException) + return TypedResults.BadRequest(ret.Error); + else + return TypedResults.InternalServerError(ret.Error); + } + + logger.Info($"User {User.Identity?.Name} successfully completed logical ports boundary scan for device {address}, found {ret.Value?.Count} ports"); + return TypedResults.Ok(ret.Value); + } + catch (Exception ex) + { + logger.Error(ex, $"User {User.Identity?.Name} encountered exception during logical ports boundary scan for device {address}"); + return TypedResults.InternalServerError(ex); + } } /// - /// [TODO:description] + /// 设置 JTAG 时钟速度 /// - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:return] + /// JTAG 设备地址 + /// JTAG 设备端口 + /// 时钟速度 (Hz) + /// 设置结果 [HttpPost("SetSpeed")] [EnableCors("Users")] [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async ValueTask SetSpeed(string address, int port, UInt32 speed) { - var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); - var ret = await jtagCtrl.SetSpeed(speed); - if (!ret.IsSuccessful) - { - if (ret.Error is ArgumentException) - return TypedResults.BadRequest(ret.Error); - else return TypedResults.InternalServerError(ret.Error); - } + logger.Info($"User {User.Identity?.Name} setting JTAG speed to {speed} Hz for device {address}:{port}"); - return TypedResults.Ok(ret.Value); + try + { + var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); + var ret = await jtagCtrl.SetSpeed(speed); + + if (!ret.IsSuccessful) + { + logger.Error($"User {User.Identity?.Name} failed to set speed for device {address}: {ret.Error}"); + if (ret.Error is ArgumentException) + return TypedResults.BadRequest(ret.Error); + else + return TypedResults.InternalServerError(ret.Error); + } + + logger.Info($"User {User.Identity?.Name} successfully set JTAG speed to {speed} Hz for device {address}"); + return TypedResults.Ok(ret.Value); + } + catch (Exception ex) + { + logger.Error(ex, $"User {User.Identity?.Name} encountered exception while setting speed for device {address}"); + return TypedResults.InternalServerError(ex); + } } } diff --git a/server/src/Database.cs b/server/src/Database.cs index 0d31cb3..26412fe 100644 --- a/server/src/Database.cs +++ b/server/src/Database.cs @@ -46,6 +46,12 @@ public class User [Nullable] public Guid BoardID { get; set; } + /// + /// 用户绑定板子的过期时间 + /// + [Nullable] + public DateTime? BoardExpireTime { get; set; } + /// /// 用户权限枚举 /// @@ -98,6 +104,18 @@ public class Board [NotNull] public required BoardStatus Status { get; set; } + /// + /// 占用该板子的用户的唯一标识符 + /// + [Nullable] + public Guid OccupiedUserID { get; set; } + + /// + /// 占用该板子的用户的用户名 + /// + [Nullable] + public string? OccupiedUserName { get; set; } + /// /// FPGA 板子的固件版本号 /// @@ -210,7 +228,7 @@ public class AppDataConnection : DataConnection /// 包含用户信息的结果,如果未找到或出错则返回相应状态 public Result> GetUserByName(string name) { - var user = this.User.Where((user) => user.Name == name).ToArray(); + var user = this.UserTable.Where((user) => user.Name == name).ToArray(); if (user.Length > 1) { @@ -235,7 +253,7 @@ public class AppDataConnection : DataConnection /// 包含用户信息的结果,如果未找到或出错则返回相应状态 public Result> GetUserByEMail(string email) { - var user = this.User.Where((user) => user.EMail == email).ToArray(); + var user = this.UserTable.Where((user) => user.EMail == email).ToArray(); if (user.Length > 1) { @@ -270,12 +288,12 @@ public class AppDataConnection : DataConnection var user = ret.Value.Value; - if (user.Password == password) + if (user.Password == password) { logger.Info($"用户 {name} 密码验证成功"); return new(user); } - else + else { logger.Warn($"用户 {name} 密码验证失败"); return new(Optional.None); @@ -287,15 +305,70 @@ public class AppDataConnection : DataConnection /// /// 用户的唯一标识符 /// 实验板的唯一标识符 + /// 绑定过期时间 /// 更新的记录数 - public int BindUserToBoard(Guid userId, Guid boardId) + public int BindUserToBoard(Guid userId, Guid boardId, DateTime expireTime) { - var result = this.User + // 获取用户信息 + 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(); - logger.Info($"用户 {userId} 已绑定到实验板 {boardId}"); - return result; + + // 更新板子的用户绑定信息 + 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; } /// @@ -326,7 +399,26 @@ public class AppDataConnection : DataConnection /// 删除的记录数 public int DeleteBoardByName(string name) { - var result = this.Board.Where(board => board.BoardName == name).Delete(); + // 先获取要删除的板子信息 + 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; } @@ -338,18 +430,62 @@ public class AppDataConnection : DataConnection /// 删除的记录数 public int DeleteBoardByID(Guid id) { - var result = this.Board.Where(board => board.ID == id).Delete(); + // 先获取要删除的板子信息 + 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 Board[] GetAllBoard() { - var boards = this.Board.ToArray(); + var boards = this.BoardTable.ToArray(); logger.Debug($"获取所有实验板,共 {boards.Length} 块"); return boards; } @@ -357,10 +493,12 @@ public class AppDataConnection : DataConnection /// /// 获取一块可用的实验板并将其状态设置为繁忙 /// + /// 要分配板子的用户ID + /// 绑定过期时间 /// 可用的实验板,如果没有可用的板子则返回空 - public Optional GetAvailableBoard() + public Optional GetAvailableBoard(Guid userId, DateTime expireTime) { - var boards = this.Board.Where( + var boards = this.BoardTable.Where( (board) => board.Status == Database.Board.BoardStatus.Available ).ToArray(); @@ -372,12 +510,34 @@ public class AppDataConnection : DataConnection else { var board = boards[0]; - board.Status = Database.Board.BoardStatus.Busy; - this.Board + 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.Status) + .Set(target => target.Status, Board.BoardStatus.Busy) + .Set(target => target.OccupiedUserID, userId) + .Set(target => target.OccupiedUserName, user.Name) .Update(); - logger.Info($"实验板 {board.BoardName} ({board.ID}) 已分配,状态更新为繁忙"); + + // 更新用户的板子绑定信息 + 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); } } @@ -385,10 +545,10 @@ public class AppDataConnection : DataConnection /// /// 用户表 /// - public ITable User => this.GetTable(); + public ITable UserTable => this.GetTable(); /// /// FPGA 板子表 /// - public ITable Board => this.GetTable(); + public ITable BoardTable => this.GetTable(); } diff --git a/src/APIClient.ts b/src/APIClient.ts index 00619e8..22851b1 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -651,9 +651,14 @@ export class DataClient { /** * 获取一个空闲的实验板(普通用户权限) + * @param durationHours (optional) 绑定持续时间(小时),默认为1小时 */ - getAvailableBoard(): Promise { - let url_ = this.baseUrl + "/api/Data/GetAvailableBoard"; + getAvailableBoard(durationHours: number | undefined): Promise { + let url_ = this.baseUrl + "/api/Data/GetAvailableBoard?"; + if (durationHours === null) + throw new Error("The parameter 'durationHours' cannot be null."); + else if (durationHours !== undefined) + url_ += "durationHours=" + encodeURIComponent("" + durationHours) + "&"; url_ = url_.replace(/[?&]$/, ""); let options_: RequestInit = { @@ -697,6 +702,108 @@ export class DataClient { return Promise.resolve(null as any); } + /** + * 解除当前用户绑定的实验板(普通用户权限) + */ + unbindBoard(): Promise { + let url_ = this.baseUrl + "/api/Data/UnbindBoard"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "POST", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processUnbindBoard(_response); + }); + } + + protected processUnbindBoard(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result200 = resultData200 !== undefined ? resultData200 : null; + + return result200; + }); + } else if (status === 400) { + return response.text().then((_responseText) => { + let result400: any = null; + let resultData400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result400 = ProblemDetails.fromJS(resultData400); + return throwException("A server side error occurred.", status, _responseText, _headers, result400); + }); + } else if (status === 500) { + return response.text().then((_responseText) => { + return throwException("A server side error occurred.", status, _responseText, _headers); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + /** + * 用户根据实验板ID获取实验板信息(普通用户权限) + * @param id (optional) + */ + getBoardByID(id: string | undefined): Promise { + let url_ = this.baseUrl + "/api/Data/GetBoardByID?"; + if (id === null) + throw new Error("The parameter 'id' cannot be null."); + else if (id !== undefined) + url_ += "id=" + encodeURIComponent("" + id) + "&"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processGetBoardByID(_response); + }); + } + + protected processGetBoardByID(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result200 = Board.fromJS(resultData200); + return result200; + }); + } else if (status === 404) { + return response.text().then((_responseText) => { + let result404: any = null; + let resultData404 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result404 = ProblemDetails.fromJS(resultData404); + return throwException("A server side error occurred.", status, _responseText, _headers, result404); + }); + } else if (status === 500) { + return response.text().then((_responseText) => { + return throwException("A server side error occurred.", status, _responseText, _headers); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + /** * 新增板子(管理员权限) * @param name (optional) @@ -1115,7 +1222,8 @@ export class JtagClient { } /** - * 页面 + * 控制器首页信息 + * @return 控制器描述信息 */ index(): Promise { let url_ = this.baseUrl + "/api/Jtag"; @@ -1153,9 +1261,10 @@ export class JtagClient { } /** - * 获取Jtag ID Code - * @param address (optional) 设备地址 - * @param port (optional) 设备端口 + * 获取 JTAG 设备的 ID Code + * @param address (optional) JTAG 设备地址 + * @param port (optional) JTAG 设备端口 + * @return 设备的 ID Code */ getDeviceIDCode(address: string | undefined, port: number | undefined): Promise { let url_ = this.baseUrl + "/api/Jtag/GetDeviceIDCode?"; @@ -1199,6 +1308,13 @@ export class JtagClient { result500 = Exception.fromJS(resultData500); return throwException("A server side error occurred.", status, _responseText, _headers, result500); }); + } else if (status === 401) { + return response.text().then((_responseText) => { + let result401: any = null; + let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result401 = ProblemDetails.fromJS(resultData401); + return throwException("A server side error occurred.", status, _responseText, _headers, result401); + }); } else if (status !== 200 && status !== 204) { return response.text().then((_responseText) => { return throwException("An unexpected server error occurred.", status, _responseText, _headers); @@ -1208,9 +1324,10 @@ export class JtagClient { } /** - * 获取状态寄存器 - * @param address (optional) 设备地址 - * @param port (optional) 设备端口 + * 读取 JTAG 设备的状态寄存器 + * @param address (optional) JTAG 设备地址 + * @param port (optional) JTAG 设备端口 + * @return 状态寄存器的原始值、二进制表示和解码值 */ readStatusReg(address: string | undefined, port: number | undefined): Promise { let url_ = this.baseUrl + "/api/Jtag/ReadStatusReg?"; @@ -1246,6 +1363,13 @@ export class JtagClient { return response.text().then((_responseText) => { return throwException("A server side error occurred.", status, _responseText, _headers); }); + } else if (status === 401) { + return response.text().then((_responseText) => { + let result401: any = null; + let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result401 = ProblemDetails.fromJS(resultData401); + return throwException("A server side error occurred.", status, _responseText, _headers, result401); + }); } else if (status !== 200 && status !== 204) { return response.text().then((_responseText) => { return throwException("An unexpected server error occurred.", status, _responseText, _headers); @@ -1255,9 +1379,10 @@ export class JtagClient { } /** - * 上传比特流文件 - * @param address (optional) 设备地址 + * 上传比特流文件到服务器 + * @param address (optional) 目标设备地址 * @param file (optional) 比特流文件 + * @return 上传结果 */ uploadBitstream(address: string | undefined, file: FileParameter | undefined): Promise { let url_ = this.baseUrl + "/api/Jtag/UploadBitstream?"; @@ -1305,6 +1430,17 @@ export class JtagClient { return throwException("A server side error occurred.", status, _responseText, _headers, result400); }); + } else if (status === 401) { + return response.text().then((_responseText) => { + let result401: any = null; + let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result401 = ProblemDetails.fromJS(resultData401); + return throwException("A server side error occurred.", status, _responseText, _headers, result401); + }); + } else if (status === 500) { + return response.text().then((_responseText) => { + return throwException("A server side error occurred.", status, _responseText, _headers); + }); } else if (status !== 200 && status !== 204) { return response.text().then((_responseText) => { return throwException("An unexpected server error occurred.", status, _responseText, _headers); @@ -1314,9 +1450,10 @@ export class JtagClient { } /** - * 通过Jtag下载比特流文件 - * @param address (optional) 设备地址 - * @param port (optional) 设备端口 + * 通过 JTAG 下载比特流文件到 FPGA 设备 + * @param address (optional) JTAG 设备地址 + * @param port (optional) JTAG 设备端口 + * @return 下载结果 */ downloadBitstream(address: string | undefined, port: number | undefined): Promise { let url_ = this.baseUrl + "/api/Jtag/DownloadBitstream?"; @@ -1368,6 +1505,13 @@ export class JtagClient { result500 = Exception.fromJS(resultData500); return throwException("A server side error occurred.", status, _responseText, _headers, result500); }); + } else if (status === 401) { + return response.text().then((_responseText) => { + let result401: any = null; + let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result401 = ProblemDetails.fromJS(resultData401); + return throwException("A server side error occurred.", status, _responseText, _headers, result401); + }); } else if (status !== 200 && status !== 204) { return response.text().then((_responseText) => { return throwException("An unexpected server error occurred.", status, _responseText, _headers); @@ -1377,10 +1521,10 @@ export class JtagClient { } /** - * [TODO:description] - * @param address (optional) [TODO:parameter] - * @param port (optional) [TODO:parameter] - * @return [TODO:return] + * 执行边界扫描,获取所有端口状态 + * @param address (optional) JTAG 设备地址 + * @param port (optional) JTAG 设备端口 + * @return 边界扫描结果 */ boundaryScanAllPorts(address: string | undefined, port: number | undefined): Promise { let url_ = this.baseUrl + "/api/Jtag/BoundaryScanAllPorts?"; @@ -1432,6 +1576,13 @@ export class JtagClient { result500 = Exception.fromJS(resultData500); return throwException("A server side error occurred.", status, _responseText, _headers, result500); }); + } else if (status === 401) { + return response.text().then((_responseText) => { + let result401: any = null; + let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result401 = ProblemDetails.fromJS(resultData401); + return throwException("A server side error occurred.", status, _responseText, _headers, result401); + }); } else if (status !== 200 && status !== 204) { return response.text().then((_responseText) => { return throwException("An unexpected server error occurred.", status, _responseText, _headers); @@ -1441,10 +1592,10 @@ export class JtagClient { } /** - * [TODO:description] - * @param address (optional) [TODO:parameter] - * @param port (optional) [TODO:parameter] - * @return [TODO:return] + * 执行逻辑端口边界扫描 + * @param address (optional) JTAG 设备地址 + * @param port (optional) JTAG 设备端口 + * @return 逻辑端口状态字典 */ boundaryScanLogicalPorts(address: string | undefined, port: number | undefined): Promise<{ [key: string]: boolean; }> { let url_ = this.baseUrl + "/api/Jtag/BoundaryScanLogicalPorts?"; @@ -1496,6 +1647,13 @@ export class JtagClient { result500 = Exception.fromJS(resultData500); return throwException("A server side error occurred.", status, _responseText, _headers, result500); }); + } else if (status === 401) { + return response.text().then((_responseText) => { + let result401: any = null; + let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result401 = ProblemDetails.fromJS(resultData401); + return throwException("A server side error occurred.", status, _responseText, _headers, result401); + }); } else if (status !== 200 && status !== 204) { return response.text().then((_responseText) => { return throwException("An unexpected server error occurred.", status, _responseText, _headers); @@ -1505,11 +1663,11 @@ export class JtagClient { } /** - * [TODO:description] - * @param address (optional) [TODO:parameter] - * @param port (optional) [TODO:parameter] - * @param speed (optional) [TODO:parameter] - * @return [TODO:return] + * 设置 JTAG 时钟速度 + * @param address (optional) JTAG 设备地址 + * @param port (optional) JTAG 设备端口 + * @param speed (optional) 时钟速度 (Hz) + * @return 设置结果 */ setSpeed(address: string | undefined, port: number | undefined, speed: number | undefined): Promise { let url_ = this.baseUrl + "/api/Jtag/SetSpeed?"; @@ -1557,6 +1715,13 @@ export class JtagClient { result500 = Exception.fromJS(resultData500); return throwException("A server side error occurred.", status, _responseText, _headers, result500); }); + } else if (status === 401) { + return response.text().then((_responseText) => { + let result401: any = null; + let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result401 = ProblemDetails.fromJS(resultData401); + return throwException("A server side error occurred.", status, _responseText, _headers, result401); + }); } else if (status !== 200 && status !== 204) { return response.text().then((_responseText) => { return throwException("An unexpected server error occurred.", status, _responseText, _headers); @@ -2715,6 +2880,8 @@ export class UserInfo implements IUserInfo { eMail!: string; /** 用户关联的板卡ID */ boardID!: string; + /** 用户绑定板子的过期时间 */ + boardExpireTime?: Date | undefined; constructor(data?: IUserInfo) { if (data) { @@ -2731,6 +2898,7 @@ export class UserInfo implements IUserInfo { this.name = _data["name"]; this.eMail = _data["eMail"]; this.boardID = _data["boardID"]; + this.boardExpireTime = _data["boardExpireTime"] ? new Date(_data["boardExpireTime"].toString()) : undefined; } } @@ -2747,6 +2915,7 @@ export class UserInfo implements IUserInfo { data["name"] = this.name; data["eMail"] = this.eMail; data["boardID"] = this.boardID; + data["boardExpireTime"] = this.boardExpireTime ? this.boardExpireTime.toISOString() : undefined; return data; } } @@ -2761,6 +2930,8 @@ export interface IUserInfo { eMail: string; /** 用户关联的板卡ID */ boardID: string; + /** 用户绑定板子的过期时间 */ + boardExpireTime?: Date | undefined; } /** FPGA 板子类,表示板子信息 */ @@ -2775,6 +2946,10 @@ export class Board implements IBoard { port!: number; /** FPGA 板子的当前状态 */ status!: BoardStatus; + /** 占用该板子的用户的唯一标识符 */ + occupiedUserID!: string; + /** 占用该板子的用户的用户名 */ + occupiedUserName?: string | undefined; /** FPGA 板子的固件版本号 */ firmVersion!: string; @@ -2794,6 +2969,8 @@ export class Board implements IBoard { this.ipAddr = _data["ipAddr"]; this.port = _data["port"]; this.status = _data["status"]; + this.occupiedUserID = _data["occupiedUserID"]; + this.occupiedUserName = _data["occupiedUserName"]; this.firmVersion = _data["firmVersion"]; } } @@ -2812,6 +2989,8 @@ export class Board implements IBoard { data["ipAddr"] = this.ipAddr; data["port"] = this.port; data["status"] = this.status; + data["occupiedUserID"] = this.occupiedUserID; + data["occupiedUserName"] = this.occupiedUserName; data["firmVersion"] = this.firmVersion; return data; } @@ -2829,6 +3008,10 @@ export interface IBoard { port: number; /** FPGA 板子的当前状态 */ status: BoardStatus; + /** 占用该板子的用户的唯一标识符 */ + occupiedUserID: string; + /** 占用该板子的用户的用户名 */ + occupiedUserName?: string | undefined; /** FPGA 板子的固件版本号 */ firmVersion: string; } diff --git a/src/components/Navbar.vue b/src/components/Navbar.vue index 440bafa..6f2a369 100644 --- a/src/components/Navbar.vue +++ b/src/components/Navbar.vue @@ -152,7 +152,7 @@ const loadUserInfo = async () => { try { const authenticated = await AuthManager.isAuthenticated(); if (authenticated) { - const client = AuthManager.createAuthenticatedClient(); + const client = AuthManager.createAuthenticatedDataClient(); const userInfo = await client.getUserInfo(); userName.value = userInfo.name; isLoggedIn.value = true; diff --git a/src/utils/AuthManager.ts b/src/utils/AuthManager.ts index fd2a531..2fc7326 100644 --- a/src/utils/AuthManager.ts +++ b/src/utils/AuthManager.ts @@ -1,4 +1,28 @@ -import { DataClient } from "@/APIClient"; +import { + DataClient, + VideoStreamClient, + BsdlParserClient, + DDSClient, + JtagClient, + MatrixKeyClient, + PowerClient, + RemoteUpdateClient, + TutorialClient, + UDPClient, +} from "@/APIClient"; + +// 支持的客户端类型联合类型 +type SupportedClient = + | DataClient + | VideoStreamClient + | BsdlParserClient + | DDSClient + | JtagClient + | MatrixKeyClient + | PowerClient + | RemoteUpdateClient + | TutorialClient + | UDPClient; export class AuthManager { // 存储token到localStorage @@ -21,30 +45,110 @@ export class AuthManager { return await AuthManager.verifyToken(); } - // 为HTTP请求添加Authorization header - public static addAuthHeader(client: any): void { + // 通用的为HTTP请求添加Authorization header的方法 + public static addAuthHeader(client: SupportedClient): void { const token = AuthManager.getToken(); - if (token && client.http) { - const originalFetch = client.http.fetch; - client.http.fetch = (url: RequestInfo, init?: RequestInit) => { + if (token) { + // 创建一个自定义的 http 对象,包装原有的 fetch 方法 + const customHttp = { + fetch: (url: RequestInfo, init?: RequestInit) => { + if (!init) init = {}; + if (!init.headers) init.headers = {}; + + // 添加Authorization header + if (typeof init.headers === "object" && init.headers !== null) { + (init.headers as any)["Authorization"] = `Bearer ${token}`; + } + + // 使用全局 fetch 或 window.fetch + return (window as any).fetch(url, init); + }, + }; + + // 重新构造客户端,传入自定义的 http 对象 + const ClientClass = client.constructor as new ( + baseUrl?: string, + http?: any, + ) => SupportedClient; + const newClient = new ClientClass(undefined, customHttp); + + // 将新客户端的属性复制到原客户端(这是一个 workaround) + // 更好的做法是返回新的客户端实例 + Object.setPrototypeOf(client, Object.getPrototypeOf(newClient)); + Object.assign(client, newClient); + } + } + + // 私有方法:创建带认证的HTTP客户端 + private static createAuthenticatedHttp() { + const token = AuthManager.getToken(); + if (!token) { + return null; + } + + return { + fetch: (url: RequestInfo, init?: RequestInit) => { if (!init) init = {}; if (!init.headers) init.headers = {}; - // 添加Authorization header if (typeof init.headers === "object" && init.headers !== null) { (init.headers as any)["Authorization"] = `Bearer ${token}`; } - return originalFetch(url, init); - }; - } + return (window as any).fetch(url, init); + }, + }; } - // 创建已配置认证的API客户端 - public static createAuthenticatedClient(): DataClient { - const client = new DataClient(); - AuthManager.addAuthHeader(client); - return client; + // 通用的创建已认证客户端的方法(使用泛型) + public static createAuthenticatedClient( + ClientClass: new (baseUrl?: string, http?: any) => T, + ): T { + const customHttp = AuthManager.createAuthenticatedHttp(); + return customHttp + ? new ClientClass(undefined, customHttp) + : new ClientClass(); + } + + // 便捷方法:创建已配置认证的各种客户端 + public static createAuthenticatedDataClient(): DataClient { + return AuthManager.createAuthenticatedClient(DataClient); + } + + public static createAuthenticatedVideoStreamClient(): VideoStreamClient { + return AuthManager.createAuthenticatedClient(VideoStreamClient); + } + + public static createAuthenticatedBsdlParserClient(): BsdlParserClient { + return AuthManager.createAuthenticatedClient(BsdlParserClient); + } + + public static createAuthenticatedDDSClient(): DDSClient { + return AuthManager.createAuthenticatedClient(DDSClient); + } + + public static createAuthenticatedJtagClient(): JtagClient { + return AuthManager.createAuthenticatedClient(JtagClient); + } + + public static createAuthenticatedMatrixKeyClient(): MatrixKeyClient { + return AuthManager.createAuthenticatedClient(MatrixKeyClient); + } + + public static createAuthenticatedPowerClient(): PowerClient { + return AuthManager.createAuthenticatedClient(PowerClient); + } + + public static createAuthenticatedRemoteUpdateClient(): RemoteUpdateClient { + return AuthManager.createAuthenticatedClient(RemoteUpdateClient); + } + + public static createAuthenticatedTutorialClient(): TutorialClient { + return AuthManager.createAuthenticatedClient(TutorialClient); + } + + public static createAuthenticatedUDPClient(): UDPClient { + return AuthManager.createAuthenticatedClient(UDPClient); } // 登录函数 @@ -60,7 +164,7 @@ export class AuthManager { AuthManager.setToken(token); // 验证token - const authClient = AuthManager.createAuthenticatedClient(); + const authClient = AuthManager.createAuthenticatedDataClient(); await authClient.testAuth(); return true; @@ -85,7 +189,7 @@ export class AuthManager { return false; } - const client = AuthManager.createAuthenticatedClient(); + const client = AuthManager.createAuthenticatedDataClient(); await client.testAuth(); return true; } catch (error) { @@ -102,7 +206,7 @@ export class AuthManager { return false; } - const client = AuthManager.createAuthenticatedClient(); + const client = AuthManager.createAuthenticatedDataClient(); await client.testAdminAuth(); return true; } catch (error) { @@ -119,4 +223,10 @@ export class AuthManager { return false; } } + + // 检查客户端是否已配置认证 + public static isClientAuthenticated(client: SupportedClient): boolean { + const token = AuthManager.getToken(); + return !!token; + } } diff --git a/src/utils/BoardManager.ts b/src/utils/BoardManager.ts index 1b55213..869b38c 100644 --- a/src/utils/BoardManager.ts +++ b/src/utils/BoardManager.ts @@ -41,7 +41,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => { return { success: false, error: "权限不足" }; } - const client = AuthManager.createAuthenticatedClient(); + const client = AuthManager.createAuthenticatedDataClient(); const result = await client.getAllBoards(); if (result) { @@ -91,7 +91,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => { return { success: false, error: "参数不完整" }; } - const client = AuthManager.createAuthenticatedClient(); + const client = AuthManager.createAuthenticatedDataClient(); const boardId = await client.addBoard(name, ipAddr, port); if (boardId) { @@ -130,7 +130,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => { return { success: false, error: "板卡ID不能为空" }; } - const client = AuthManager.createAuthenticatedClient(); + const client = AuthManager.createAuthenticatedDataClient(); const result = await client.deleteBoard(boardId); if (result > 0) { diff --git a/src/views/Project/Index.vue b/src/views/Project/Index.vue index 4475b98..7593874 100644 --- a/src/views/Project/Index.vue +++ b/src/views/Project/Index.vue @@ -84,6 +84,13 @@ @add-template="handleAddTemplate" @close="showComponentsMenu = false" /> + + + @@ -96,10 +103,13 @@ import ComponentSelector from "@/components/LabCanvas/ComponentSelector.vue"; import PropertyPanel from "@/components/PropertyPanel.vue"; import MarkdownRenderer from "@/components/MarkdownRenderer.vue"; import BottomBar from "@/views/Project/BottomBar.vue"; +import RequestBoardDialog from "@/views/Project/RequestBoardDialog.vue"; import { useProvideComponentManager } from "@/components/LabCanvas"; import type { DiagramData } from "@/components/LabCanvas"; import { useAlertStore } from "@/components/Alert"; import { AuthManager } from "@/utils/AuthManager"; +import { useEquipments } from "@/stores/equipments"; +import type { Board } from "@/APIClient"; import { useRoute } from "vue-router"; const route = useRoute(); @@ -108,8 +118,14 @@ const router = useRouter(); // 提供组件管理服务 const componentManager = useProvideComponentManager(); +// 设备管理store +const equipments = useEquipments(); + const alert = useAlertStore(); +// --- 实验板申请对话框 --- +const showRequestBoardDialog = ref(false); + // --- 文档面板控制 --- const showDocPanel = ref(false); const documentContent = ref(""); @@ -208,6 +224,62 @@ function updateComponentDirectProp( componentManager.updateComponentDirectProp(componentId, propName, value); } +// --- 实验板管理 --- +// 检查并初始化用户实验板 +async function checkAndInitializeBoard() { + try { + const client = AuthManager.createAuthenticatedDataClient(); + const userInfo = await client.getUserInfo(); + + if (userInfo.boardID && userInfo.boardID.trim() !== '') { + // 用户已绑定实验板,获取实验板信息并更新到equipment + try { + const board = await client.getBoardByID(userInfo.boardID); + updateEquipmentFromBoard(board); + alert?.show(`实验板 ${board.boardName} 已连接`, "success"); + } catch (boardError) { + console.error('获取实验板信息失败:', boardError); + alert?.show("获取实验板信息失败", "error"); + showRequestBoardDialog.value = true; + } + } else { + // 用户未绑定实验板,显示申请对话框 + showRequestBoardDialog.value = true; + } + } catch (error) { + console.error('检查用户实验板失败:', error); + alert?.show("检查用户信息失败", "error"); + showRequestBoardDialog.value = true; + } +} + +// 根据实验板信息更新equipment store +function updateEquipmentFromBoard(board: Board) { + equipments.setAddr(board.ipAddr); + equipments.setPort(board.port); + + console.log(`实验板信息已更新到equipment store:`, { + address: board.ipAddr, + port: board.port, + boardName: board.boardName, + boardId: board.id + }); +} + +// 处理申请实验板对话框关闭 +function handleRequestBoardClose() { + showRequestBoardDialog.value = false; + // 如果用户取消申请,可以选择返回上一页或显示警告 + router.push('/'); +} + +// 处理申请实验板成功 +function handleRequestBoardSuccess(board: Board) { + showRequestBoardDialog.value = false; + updateEquipmentFromBoard(board); + alert?.show(`实验板 ${board.boardName} 申请成功!`, "success"); +} + // --- 生命周期钩子 --- onMounted(async () => { // 验证用户身份 @@ -224,6 +296,9 @@ onMounted(async () => { return; } + // 检查并初始化用户实验板 + await checkAndInitializeBoard(); + // 检查是否有例程参数,如果有则自动打开文档面板 if (route.query.tutorial) { showDocPanel.value = true; diff --git a/src/views/Project/RequestBoardDialog.vue b/src/views/Project/RequestBoardDialog.vue new file mode 100644 index 0000000..9723921 --- /dev/null +++ b/src/views/Project/RequestBoardDialog.vue @@ -0,0 +1,179 @@ + + + diff --git a/src/views/User/Index.vue b/src/views/User/Index.vue index 2d3368b..e79d9cb 100644 --- a/src/views/User/Index.vue +++ b/src/views/User/Index.vue @@ -14,10 +14,9 @@
-
+
-

用户信息

-

这里是用户信息页面的内容。

+
@@ -31,6 +30,7 @@ import BoardTable from "./BoardTable.vue"; import { toNumber } from "lodash"; import { onMounted, ref } from "vue"; import { AuthManager } from "@/utils/AuthManager"; +import UserInfo from "./UserInfo.vue"; const activePage = ref(1); const isAdmin = ref(false); @@ -40,9 +40,9 @@ function setActivePage(event: Event) { activePage.value = toNumber(target.id); } -onMounted(async ()=>{ +onMounted(async () => { isAdmin.value = await AuthManager.verifyAdminAuth(); -}) +});