Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab
This commit is contained in:
		
							
								
								
									
										3
									
								
								server/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								server/.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,5 +1,6 @@
 | 
			
		||||
# Generate
 | 
			
		||||
obj
 | 
			
		||||
bin
 | 
			
		||||
bitstream
 | 
			
		||||
bsdl
 | 
			
		||||
 | 
			
		||||
data
 | 
			
		||||
 
 | 
			
		||||
@@ -62,8 +62,39 @@ 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;
 | 
			
		||||
 | 
			
		||||
            // We have to hook the OnMessageReceived event in order to
 | 
			
		||||
            // allow the JWT authentication handler to read the access
 | 
			
		||||
            // token from the query string when a WebSocket or
 | 
			
		||||
            // Server-Sent Events request comes in.
 | 
			
		||||
 | 
			
		||||
            // Sending the access token in the query string is required when using WebSockets or ServerSentEvents
 | 
			
		||||
            // due to a limitation in Browser APIs. We restrict it to only calls to the
 | 
			
		||||
            // SignalR hub in this code.
 | 
			
		||||
            // See https://docs.microsoft.com/aspnet/core/signalr/security#access-token-logging
 | 
			
		||||
            // for more information about security considerations when using
 | 
			
		||||
            // the query string to transmit the access token.
 | 
			
		||||
            options.Events = new JwtBearerEvents
 | 
			
		||||
            {
 | 
			
		||||
                OnMessageReceived = context =>
 | 
			
		||||
                {
 | 
			
		||||
                    var accessToken = context.Request.Query["access_token"];
 | 
			
		||||
 | 
			
		||||
                    // If the request is for our hub...
 | 
			
		||||
                    var path = context.HttpContext.Request.Path;
 | 
			
		||||
                    if (!string.IsNullOrEmpty(accessToken) && (
 | 
			
		||||
                            path.StartsWithSegments("/hubs/JtagHub") ||
 | 
			
		||||
                            path.StartsWithSegments("/hubs/ProgressHub")
 | 
			
		||||
                        ))
 | 
			
		||||
                    {
 | 
			
		||||
                        // Read the token out of the query string
 | 
			
		||||
                        context.Token = accessToken;
 | 
			
		||||
                    }
 | 
			
		||||
                    return Task.CompletedTask;
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
        });
 | 
			
		||||
    // Add JWT Token Authorization Policy
 | 
			
		||||
    builder.Services.AddAuthorization(options =>
 | 
			
		||||
@@ -71,7 +102,7 @@ try
 | 
			
		||||
        options.AddPolicy("Admin", policy =>
 | 
			
		||||
        {
 | 
			
		||||
            policy.RequireClaim(ClaimTypes.Role, new string[] {
 | 
			
		||||
                Database.User.UserPermission.Admin.ToString(),
 | 
			
		||||
                Database.UserPermission.Admin.ToString(),
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
@@ -141,6 +172,11 @@ try
 | 
			
		||||
        options.OperationProcessors.Add(new OperationSecurityScopeProcessor("Bearer"));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 添加数据库资源管理器服务
 | 
			
		||||
    builder.Services.AddScoped<Database.AppDataConnection>();
 | 
			
		||||
    builder.Services.AddScoped<Database.UserManager>();
 | 
			
		||||
    builder.Services.AddScoped<Database.ResourceManager>();
 | 
			
		||||
    builder.Services.AddScoped<Database.ExamManager>();
 | 
			
		||||
 | 
			
		||||
    // 添加 HTTP 视频流服务
 | 
			
		||||
    builder.Services.AddSingleton<HttpVideoStreamService>();
 | 
			
		||||
@@ -209,7 +245,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 +268,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
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -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";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// [TODO:description]
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class UserInfo
 | 
			
		||||
    public DataController(Database.UserManager userManager)
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 用户的唯一标识符
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public Guid ID { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 用户的名称
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 用户的电子邮箱
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string EMail { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 用户关联的板卡ID
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public Guid BoardID { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 用户绑定板子的过期时间
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public DateTime? BoardExpireTime { get; set; }
 | 
			
		||||
        _userManager = userManager;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -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)
 | 
			
		||||
@@ -473,14 +440,13 @@ public class DataController : ControllerBase
 | 
			
		||||
    [ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult UpdateBoardStatus(Guid boardId, Database.Board.BoardStatus newStatus)
 | 
			
		||||
    public IActionResult UpdateBoardStatus(Guid boardId, Database.BoardStatus newStatus)
 | 
			
		||||
    {
 | 
			
		||||
        if (boardId == Guid.Empty)
 | 
			
		||||
            return BadRequest("板子Guid不能为空");
 | 
			
		||||
        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,54 @@ public class DataController : ControllerBase
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, "更新失败,请稍后重试");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpPost("AddEmptyBoard")]
 | 
			
		||||
    [EnableCors("Development")]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult AddEmptyBoard()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var boardId = _userManager.AddBoard("Test");
 | 
			
		||||
            var result = _userManager.UpdateBoardStatus(boardId, Database.BoardStatus.Available);
 | 
			
		||||
            return Ok();
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "新增板子时发生异常");
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, "新增失败,请稍后重试");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// [TODO:description]
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class UserInfo
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 用户的唯一标识符
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public Guid ID { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 用户的名称
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 用户的电子邮箱
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string EMail { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 用户关联的板卡ID
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public Guid BoardID { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 用户绑定板子的过期时间
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public DateTime? BoardExpireTime { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,77 +15,11 @@ public class DebuggerController : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 表示单个信号通道的配置信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class ChannelConfig
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 通道名称
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public string name;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 通道显示颜色(如前端波形显示用)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public string color;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 通道信号线宽度(位数)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public UInt32 wireWidth;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 信号线在父端口中的起始索引(bit)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public UInt32 wireStartIndex;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 父端口编号
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public UInt32 parentPort;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 捕获模式(如上升沿、下降沿等)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public CaptureMode mode;
 | 
			
		||||
    }
 | 
			
		||||
    private readonly Database.UserManager _userManager;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 调试器整体配置信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class DebuggerConfig
 | 
			
		||||
    public DebuggerController(Database.UserManager userManager)
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 时钟频率
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public UInt32 clkFreq;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 总端口数量
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public UInt32 totalPortNum;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 捕获深度(采样点数)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public UInt32 captureDepth;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 触发器数量
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public UInt32 triggerNum;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 所有信号通道的配置信息
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public ChannelConfig[] channelConfigs;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 单个通道的捕获数据
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class ChannelCaptureData
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 通道名称
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public string name;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 通道捕获到的数据(Base64编码的UInt32数组)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public string data;
 | 
			
		||||
        this._userManager = userManager;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -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, "操作失败,请稍后重试");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 表示单个信号通道的配置信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class ChannelConfig
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 通道名称
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public string name;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 通道显示颜色(如前端波形显示用)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public string color;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 通道信号线宽度(位数)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public UInt32 wireWidth;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 信号线在父端口中的起始索引(bit)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public UInt32 wireStartIndex;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 父端口编号
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public UInt32 parentPort;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 捕获模式(如上升沿、下降沿等)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public CaptureMode mode;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 调试器整体配置信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class DebuggerConfig
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 时钟频率
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public UInt32 clkFreq;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 总端口数量
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public UInt32 totalPortNum;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 捕获深度(采样点数)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public UInt32 captureDepth;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 触发器数量
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public UInt32 triggerNum;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 所有信号通道的配置信息
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public ChannelConfig[] channelConfigs;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 单个通道的捕获数据
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class ChannelCaptureData
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 通道名称
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public string name;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 通道捕获到的数据(Base64编码的UInt32数组)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        required public string data;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Cors;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using DotNext;
 | 
			
		||||
using Database;
 | 
			
		||||
 | 
			
		||||
namespace server.Controllers;
 | 
			
		||||
 | 
			
		||||
@@ -14,127 +15,18 @@ public class ExamController : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验信息类
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class ExamInfo
 | 
			
		||||
    private readonly ExamManager _examManager;
 | 
			
		||||
    private readonly ResourceManager _resourceManager;
 | 
			
		||||
    private readonly UserManager _userManager;
 | 
			
		||||
 | 
			
		||||
    public ExamController(
 | 
			
		||||
        ExamManager examManager,
 | 
			
		||||
        ResourceManager resourceManager,
 | 
			
		||||
        UserManager userManager)
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验的唯一标识符
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string ID { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验名称
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验描述
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string Description { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验创建时间
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public DateTime CreatedTime { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验最后更新时间
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public DateTime UpdatedTime { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验标签
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string[] Tags { get; set; } = Array.Empty<string>();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验难度(1-5)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public int Difficulty { get; set; } = 1;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 普通用户是否可见
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool IsVisibleToUsers { get; set; } = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验简要信息类(用于列表显示)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class ExamSummary
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验的唯一标识符
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string ID { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验名称
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验创建时间
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public DateTime CreatedTime { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验最后更新时间
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public DateTime UpdatedTime { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验标签
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string[] Tags { get; set; } = Array.Empty<string>();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验难度(1-5)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public int Difficulty { get; set; } = 1;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 普通用户是否可见
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool IsVisibleToUsers { get; set; } = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 创建实验请求类
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class CreateExamRequest
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验ID
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string ID { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验名称
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验描述
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string Description { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验标签
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string[] Tags { get; set; } = Array.Empty<string>();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验难度(1-5)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public int Difficulty { get; set; } = 1;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 普通用户是否可见
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool IsVisibleToUsers { get; set; } = true;
 | 
			
		||||
        _examManager = examManager;
 | 
			
		||||
        _resourceManager = resourceManager;
 | 
			
		||||
        _userManager = userManager;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -144,29 +36,19 @@ public class ExamController : ControllerBase
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [HttpGet("list")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(ExamSummary[]), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(ExamInfo[]), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult GetExamList()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var db = new Database.AppDataConnection();
 | 
			
		||||
            var exams = db.GetAllExams();
 | 
			
		||||
            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();
 | 
			
		||||
            var examInfos = exams.Select(exam => new ExamInfo(exam)).ToArray();
 | 
			
		||||
 | 
			
		||||
            logger.Info($"成功获取实验列表,共 {examSummaries.Length} 个实验");
 | 
			
		||||
            return Ok(examSummaries);
 | 
			
		||||
            logger.Info($"成功获取实验列表,共 {examInfos.Length} 个实验");
 | 
			
		||||
            return Ok(examInfos);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
@@ -195,8 +77,7 @@ public class ExamController : ControllerBase
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var db = new Database.AppDataConnection();
 | 
			
		||||
            var result = db.GetExamByID(examId);
 | 
			
		||||
            var result = _examManager.GetExamByID(examId);
 | 
			
		||||
 | 
			
		||||
            if (!result.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
@@ -211,17 +92,7 @@ public class ExamController : ControllerBase
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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
 | 
			
		||||
            };
 | 
			
		||||
            var examInfo = new ExamInfo(exam);
 | 
			
		||||
 | 
			
		||||
            logger.Info($"成功获取实验信息: {examId}");
 | 
			
		||||
            return Ok(examInfo);
 | 
			
		||||
@@ -239,7 +110,7 @@ public class ExamController : ControllerBase
 | 
			
		||||
    /// <param name="request">创建实验请求</param>
 | 
			
		||||
    /// <returns>创建结果</returns>
 | 
			
		||||
    [Authorize("Admin")]
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    [HttpPost("create")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(ExamInfo), StatusCodes.Status201Created)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
@@ -247,37 +118,26 @@ public class ExamController : ControllerBase
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status409Conflict)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult CreateExam([FromBody] CreateExamRequest request)
 | 
			
		||||
    public IActionResult CreateExam([FromBody] ExamDto 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);
 | 
			
		||||
            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
 | 
			
		||||
            };
 | 
			
		||||
            var examInfo = new ExamInfo(exam);
 | 
			
		||||
 | 
			
		||||
            logger.Info($"成功创建实验: {request.ID}");
 | 
			
		||||
            return CreatedAtAction(nameof(GetExam), new { examId = request.ID }, examInfo);
 | 
			
		||||
@@ -288,4 +148,385 @@ public class ExamController : ControllerBase
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {ex.Message}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 更新实验信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="request">更新实验请求</param>
 | 
			
		||||
    /// <returns>更新结果</returns>
 | 
			
		||||
    [Authorize("Admin")]
 | 
			
		||||
    [HttpPost("update")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(ExamInfo), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult UpdateExam([FromBody] ExamDto request)
 | 
			
		||||
    {
 | 
			
		||||
        var examId = request.ID;
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            // 首先检查实验是否存在
 | 
			
		||||
            var existingExamResult = _examManager.GetExamByID(examId);
 | 
			
		||||
            if (!existingExamResult.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"检查实验是否存在时出错: {existingExamResult.Error.Message}");
 | 
			
		||||
                return StatusCode(StatusCodes.Status500InternalServerError, $"检查实验失败: {existingExamResult.Error.Message}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!existingExamResult.Value.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Warn($"要更新的实验不存在: {examId}");
 | 
			
		||||
                return NotFound($"实验 {examId} 不存在");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 执行更新
 | 
			
		||||
            var updateResult = _examManager.UpdateExam(
 | 
			
		||||
                examId,
 | 
			
		||||
                request.Name,
 | 
			
		||||
                request.Description,
 | 
			
		||||
                request.Tags,
 | 
			
		||||
                request.Difficulty,
 | 
			
		||||
                request.IsVisibleToUsers
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (!updateResult.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"更新实验时出错: {updateResult.Error.Message}");
 | 
			
		||||
                return StatusCode(StatusCodes.Status500InternalServerError, $"更新实验失败: {updateResult.Error.Message}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 获取更新后的实验信息并返回
 | 
			
		||||
            var updatedExamResult = _examManager.GetExamByID(examId);
 | 
			
		||||
            if (!updatedExamResult.IsSuccessful || !updatedExamResult.Value.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"获取更新后的实验信息失败: {examId}");
 | 
			
		||||
                return StatusCode(StatusCodes.Status500InternalServerError, "更新成功但获取更新后信息失败");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var updatedExam = updatedExamResult.Value.Value;
 | 
			
		||||
            var examInfo = new ExamInfo(updatedExam);
 | 
			
		||||
 | 
			
		||||
            logger.Info($"成功更新实验: {examId},更新记录数: {updateResult.Value}");
 | 
			
		||||
            return Ok(examInfo);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"更新实验 {examId} 时出错: {ex.Message}");
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, $"更新实验失败: {ex.Message}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 提交作业
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="examId">实验ID</param>
 | 
			
		||||
    /// <param name="file">提交的文件</param>
 | 
			
		||||
    /// <returns>提交结果</returns>
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [HttpPost("commit/{examId}")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(Resource), StatusCodes.Status201Created)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public async Task<IActionResult> SubmitHomework(string examId, IFormFile file)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(examId))
 | 
			
		||||
            return BadRequest("实验ID不能为空");
 | 
			
		||||
 | 
			
		||||
        if (file == null || file.Length == 0)
 | 
			
		||||
            return BadRequest("文件不能为空");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            // 获取当前用户信息
 | 
			
		||||
            var userName = User.Identity?.Name;
 | 
			
		||||
            if (string.IsNullOrEmpty(userName))
 | 
			
		||||
                return Unauthorized("无法获取用户信息");
 | 
			
		||||
 | 
			
		||||
            var userResult = _userManager.GetUserByName(userName);
 | 
			
		||||
            if (!userResult.IsSuccessful || !userResult.Value.HasValue)
 | 
			
		||||
                return Unauthorized("用户不存在");
 | 
			
		||||
 | 
			
		||||
            var user = userResult.Value.Value;
 | 
			
		||||
 | 
			
		||||
            // 检查实验是否存在
 | 
			
		||||
            var examResult = _examManager.GetExamByID(examId);
 | 
			
		||||
            if (!examResult.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"检查实验是否存在时出错: {examResult.Error.Message}");
 | 
			
		||||
                return StatusCode(StatusCodes.Status500InternalServerError, $"检查实验失败: {examResult.Error.Message}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!examResult.Value.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Warn($"实验不存在: {examId}");
 | 
			
		||||
                return NotFound($"实验 {examId} 不存在");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 读取文件内容
 | 
			
		||||
            byte[] fileData;
 | 
			
		||||
            using (var memoryStream = new MemoryStream())
 | 
			
		||||
            {
 | 
			
		||||
                await file.CopyToAsync(memoryStream);
 | 
			
		||||
                fileData = memoryStream.ToArray();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 提交作业
 | 
			
		||||
            var commitResult = _resourceManager.AddResource(
 | 
			
		||||
                user.ID, ResourceTypes.Compression, ResourcePurpose.Homework,
 | 
			
		||||
                file.FileName, fileData, examId);
 | 
			
		||||
            if (!commitResult.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"提交作业时出错: {commitResult.Error.Message}");
 | 
			
		||||
                return StatusCode(StatusCodes.Status500InternalServerError, $"提交作业失败: {commitResult.Error.Message}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var commit = commitResult.Value;
 | 
			
		||||
 | 
			
		||||
            logger.Info($"用户 {userName} 成功提交实验 {examId} 的作业,Commit ID: {commit.ID}");
 | 
			
		||||
            return CreatedAtAction(nameof(GetCommitsByExamId), new { examId = examId }, commit);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"提交实验 {examId} 作业时出错: {ex.Message}");
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, $"提交作业失败: {ex.Message}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取用户在指定实验中的提交记录
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="examId">实验ID</param>
 | 
			
		||||
    /// <returns>提交记录列表</returns>
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [HttpGet("commits/{examId}")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(Resource[]), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult GetCommitsByExamId(string examId)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(examId))
 | 
			
		||||
            return BadRequest("实验ID不能为空");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            // 获取当前用户信息
 | 
			
		||||
            var userName = User.Identity?.Name;
 | 
			
		||||
            if (string.IsNullOrEmpty(userName))
 | 
			
		||||
                return Unauthorized("无法获取用户信息");
 | 
			
		||||
 | 
			
		||||
            var userResult = _userManager.GetUserByName(userName);
 | 
			
		||||
            if (!userResult.IsSuccessful || !userResult.Value.HasValue)
 | 
			
		||||
                return Unauthorized("用户不存在");
 | 
			
		||||
 | 
			
		||||
            var user = userResult.Value.Value;
 | 
			
		||||
 | 
			
		||||
            // 检查实验是否存在
 | 
			
		||||
            var examResult = _examManager.GetExamByID(examId);
 | 
			
		||||
            if (!examResult.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"检查实验是否存在时出错: {examResult.Error.Message}");
 | 
			
		||||
                return StatusCode(StatusCodes.Status500InternalServerError, $"检查实验失败: {examResult.Error.Message}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!examResult.Value.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Warn($"实验不存在: {examId}");
 | 
			
		||||
                return NotFound($"实验 {examId} 不存在");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 获取用户的提交记录
 | 
			
		||||
            var commitsResult = _resourceManager.GetResourceListByType(
 | 
			
		||||
                ResourceTypes.Compression, ResourcePurpose.Homework, examId);
 | 
			
		||||
            if (!commitsResult.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"获取提交记录时出错: {commitsResult.Error.Message}");
 | 
			
		||||
                return StatusCode(StatusCodes.Status500InternalServerError, $"获取提交记录失败: {commitsResult.Error.Message}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var commits = commitsResult.Value;
 | 
			
		||||
 | 
			
		||||
            logger.Info($"成功获取用户 {userName} 在实验 {examId} 中的提交记录,共 {commits.Length} 条");
 | 
			
		||||
            return Ok(commits);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"获取实验 {examId} 提交记录时出错: {ex.Message}");
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, $"获取提交记录失败: {ex.Message}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 删除提交记录
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="commitId">提交记录ID</param>
 | 
			
		||||
    /// <returns>删除结果</returns>
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [HttpDelete("commit/{commitId}")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult DeleteCommit(string commitId)
 | 
			
		||||
    {
 | 
			
		||||
        if (!Guid.TryParse(commitId, out _))
 | 
			
		||||
            return BadRequest("提交记录ID格式不正确");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            // 获取当前用户信息
 | 
			
		||||
            var userName = User.Identity?.Name;
 | 
			
		||||
            if (string.IsNullOrEmpty(userName))
 | 
			
		||||
                return Unauthorized("无法获取用户信息");
 | 
			
		||||
 | 
			
		||||
            var userResult = _userManager.GetUserByName(userName);
 | 
			
		||||
            if (!userResult.IsSuccessful || !userResult.Value.HasValue)
 | 
			
		||||
                return Unauthorized("用户不存在");
 | 
			
		||||
 | 
			
		||||
            var user = userResult.Value.Value;
 | 
			
		||||
 | 
			
		||||
            // 检查是否是管理员
 | 
			
		||||
            var isAdmin = user.Permission == UserPermission.Admin;
 | 
			
		||||
 | 
			
		||||
            // 如果不是管理员,检查提交记录是否属于当前用户
 | 
			
		||||
            if (!isAdmin)
 | 
			
		||||
            {
 | 
			
		||||
                var commitResult = _resourceManager.GetResourceById(commitId);
 | 
			
		||||
                if (!commitResult.HasValue)
 | 
			
		||||
                {
 | 
			
		||||
                    logger.Warn($"提交记录不存在: {commitId}");
 | 
			
		||||
                    return NotFound($"提交记录 {commitId} 不存在");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var commit = commitResult.Value;
 | 
			
		||||
                if (commit.UserID != user.ID)
 | 
			
		||||
                {
 | 
			
		||||
                    logger.Warn($"用户 {userName} 尝试删除不属于自己的提交记录: {commitId}");
 | 
			
		||||
                    return Forbid("您只能删除自己的提交记录");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 执行删除
 | 
			
		||||
            var deleteResult = _resourceManager.DeleteResource(commitId);
 | 
			
		||||
            if (!deleteResult)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Warn($"提交记录不存在: {commitId}");
 | 
			
		||||
                return NotFound($"提交记录 {commitId} 不存在");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            logger.Info($"用户 {userName} 成功删除提交记录: {commitId}");
 | 
			
		||||
            return Ok($"提交记录 {commitId} 已成功删除");
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"删除提交记录 {commitId} 时出错: {ex.Message}");
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, $"删除提交记录失败: {ex.Message}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 实验信息
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class ExamInfo
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验的唯一标识符
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string ID { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验名称
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验描述
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string Description { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验创建时间
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public DateTime CreatedTime { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验最后更新时间
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public DateTime UpdatedTime { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验标签
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string[] Tags { get; set; } = Array.Empty<string>();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验难度(1-5)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public int Difficulty { get; set; } = 1;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 普通用户是否可见
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool IsVisibleToUsers { get; set; } = true;
 | 
			
		||||
 | 
			
		||||
    public ExamInfo(Exam exam)
 | 
			
		||||
    {
 | 
			
		||||
        ID = exam.ID;
 | 
			
		||||
        Name = exam.Name;
 | 
			
		||||
        Description = exam.Description;
 | 
			
		||||
        CreatedTime = exam.CreatedTime;
 | 
			
		||||
        UpdatedTime = exam.UpdatedTime;
 | 
			
		||||
        Tags = exam.GetTagsList();
 | 
			
		||||
        Difficulty = exam.Difficulty;
 | 
			
		||||
        IsVisibleToUsers = exam.IsVisibleToUsers;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 统一的实验数据传输对象
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class ExamDto
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验的唯一标识符
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public required string ID { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验名称
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public required string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验描述
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public required string Description { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验标签
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string[] Tags { get; set; } = Array.Empty<string>();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验难度(1-5)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public int Difficulty { get; set; } = 1;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 普通用户是否可见
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool IsVisibleToUsers { get; set; } = true;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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.");
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -127,6 +132,7 @@ public class JtagController : ControllerBase
 | 
			
		||||
    /// <param name="address">JTAG 设备地址</param>
 | 
			
		||||
    /// <param name="port">JTAG 设备端口</param>
 | 
			
		||||
    /// <param name="bitstreamId">比特流ID</param>
 | 
			
		||||
    /// <param name="cancelToken">取消令牌</param>
 | 
			
		||||
    /// <returns>进度跟踪TaskID</returns>
 | 
			
		||||
    [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<IResult> DownloadBitstream(string address, int port, int bitstreamId, CancellationToken cancelToken)
 | 
			
		||||
    public IResult DownloadBitstream(string address, int port, string bitstreamId, CancellationToken cancelToken)
 | 
			
		||||
    {
 | 
			
		||||
        logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port} using bitstream ID: {bitstreamId}");
 | 
			
		||||
 | 
			
		||||
@@ -149,35 +155,33 @@ 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)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"User {username} failed to get bitstream from database: {bitstreamResult.Error}");
 | 
			
		||||
                return TypedResults.InternalServerError($"数据库查询失败: {bitstreamResult.Error?.Message}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!bitstreamResult.Value.HasValue)
 | 
			
		||||
            if (!resourceRet.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;
 | 
			
		||||
            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 +239,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
 | 
			
		||||
 
 | 
			
		||||
@@ -15,56 +15,11 @@ public class LogicAnalyzerController : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 信号触发配置
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class SignalTriggerConfig
 | 
			
		||||
    private readonly Database.UserManager _userManager;
 | 
			
		||||
 | 
			
		||||
    public LogicAnalyzerController(Database.UserManager userManager)
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 信号索引 (0-7)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public int SignalIndex { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 操作符
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public SignalOperator Operator { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 信号值
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public SignalValue Value { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 捕获配置
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class CaptureConfig
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 全局触发模式
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public GlobalCaptureMode GlobalMode { get; set; }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 捕获深度
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public int CaptureLength { get; set; } = 2048 * 32;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 预采样深度
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public int PreCaptureLength { get; set; } = 2048;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 有效通道
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public AnalyzerChannelDiv ChannelDiv { get; set; } = AnalyzerChannelDiv.EIGHT;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 时钟分频系数
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public AnalyzerClockDiv ClockDiv { get; set; } = AnalyzerClockDiv.DIV1;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 信号触发配置列表
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public SignalTriggerConfig[] SignalConfigs { get; set; } = Array.Empty<SignalTriggerConfig>();
 | 
			
		||||
        _userManager = userManager;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -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, "操作失败,请稍后重试");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 信号触发配置
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class SignalTriggerConfig
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 信号索引 (0-7)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public int SignalIndex { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 操作符
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public SignalOperator Operator { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 信号值
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public SignalValue Value { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 捕获配置
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class CaptureConfig
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 全局触发模式
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public GlobalCaptureMode GlobalMode { get; set; }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 捕获深度
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public int CaptureLength { get; set; } = 2048 * 32;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 预采样深度
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public int PreCaptureLength { get; set; } = 2048;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 有效通道
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public AnalyzerChannelDiv ChannelDiv { get; set; } = AnalyzerChannelDiv.EIGHT;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 时钟分频系数
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public AnalyzerClockDiv ClockDiv { get; set; } = AnalyzerClockDiv.DIV1;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 信号触发配置列表
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public SignalTriggerConfig[] SignalConfigs { get; set; } = Array.Empty<SignalTriggerConfig>();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,71 +15,11 @@ public class OscilloscopeApiController : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 示波器完整配置
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class OscilloscopeFullConfig
 | 
			
		||||
    private readonly Database.UserManager _userManager;
 | 
			
		||||
 | 
			
		||||
    public OscilloscopeApiController(Database.UserManager userManager)
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 是否启动捕获
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool CaptureEnabled { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 触发电平(0-255)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public byte TriggerLevel { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 触发边沿(true为上升沿,false为下降沿)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool TriggerRisingEdge { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 水平偏移量(0-1023)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public ushort HorizontalShift { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 抽样率(0-1023)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public ushort DecimationRate { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 是否自动刷新RAM
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool AutoRefreshRAM { get; set; } = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 示波器状态和数据
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class OscilloscopeDataResponse
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// AD采样频率
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public uint ADFrequency { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// AD采样幅度
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public byte ADVpp { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// AD采样最大值
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public byte ADMax { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// AD采样最小值
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public byte ADMin { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 波形数据(Base64编码)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string WaveformData { get; set; } = string.Empty;
 | 
			
		||||
        _userManager = userManager;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -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, "操作失败,请稍后重试");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 示波器完整配置
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class OscilloscopeFullConfig
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 是否启动捕获
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool CaptureEnabled { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 触发电平(0-255)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public byte TriggerLevel { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 触发边沿(true为上升沿,false为下降沿)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool TriggerRisingEdge { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 水平偏移量(0-1023)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public ushort HorizontalShift { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 抽样率(0-1023)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public ushort DecimationRate { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 是否自动刷新RAM
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool AutoRefreshRAM { get; set; } = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 示波器状态和数据
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class OscilloscopeDataResponse
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// AD采样频率
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public uint ADFrequency { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// AD采样幅度
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public byte ADVpp { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// AD采样最大值
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public byte ADMax { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// AD采样最小值
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public byte ADMin { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 波形数据(Base64编码)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string WaveformData { get; set; } = string.Empty;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,309 @@ 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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 添加资源(文件上传)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="request">添加资源请求</param>
 | 
			
		||||
    /// <param name="file">资源文件</param>
 | 
			
		||||
    /// <returns>添加结果</returns>
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(ResourceInfo), StatusCodes.Status201Created)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public async Task<IActionResult> AddResource([FromForm] AddResourceRequest request, IFormFile file)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(request.ResourceType) || file == null)
 | 
			
		||||
            return BadRequest("资源类型、资源用途和文件不能为空");
 | 
			
		||||
 | 
			
		||||
        // 验证资源用途
 | 
			
		||||
        if (request.ResourcePurpose != ResourcePurpose.Template && request.ResourcePurpose != ResourcePurpose.User)
 | 
			
		||||
            return BadRequest($"无效的资源用途: {request.ResourcePurpose}");
 | 
			
		||||
 | 
			
		||||
        // 模板资源需要管理员权限
 | 
			
		||||
        if (request.ResourcePurpose == ResourcePurpose.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.ToString(),
 | 
			
		||||
                Name = resource.ResourceName,
 | 
			
		||||
                Type = resource.ResourceType,
 | 
			
		||||
                Purpose = resource.Purpose,
 | 
			
		||||
                UploadTime = resource.UploadTime,
 | 
			
		||||
                ExamID = resource.ExamID,
 | 
			
		||||
                MimeType = resource.MimeType
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            logger.Info($"成功添加资源: {request.ResourceType}/{request.ResourcePurpose}/{file.FileName}");
 | 
			
		||||
            return CreatedAtAction(nameof(GetResourceById), new { resourceId = resource.ID }, resourceInfo);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"添加资源 {request.ResourceType}/{request.ResourcePurpose}/{file.FileName} 时出错: {ex.Message}");
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, $"添加资源失败: {ex.Message}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取资源列表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="examId">实验ID(可选)</param>
 | 
			
		||||
    /// <param name="resourceType">资源类型(可选)</param>
 | 
			
		||||
    /// <param name="resourcePurpose">资源用途(可选)</param>
 | 
			
		||||
    /// <returns>资源列表</returns>
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [HttpGet]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(ResourceInfo[]), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult GetResourceList(
 | 
			
		||||
        [FromQuery] string? examId = null,
 | 
			
		||||
        [FromQuery] string? resourceType = null,
 | 
			
		||||
        [FromQuery] ResourcePurpose? 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;
 | 
			
		||||
 | 
			
		||||
            Result<List<Resource>> result;
 | 
			
		||||
            // 管理员
 | 
			
		||||
            if (user.Permission == UserPermission.Admin)
 | 
			
		||||
            {
 | 
			
		||||
                result = _resourceManager.GetFullResourceList(examId, resourceType, resourcePurpose);
 | 
			
		||||
            }
 | 
			
		||||
            // 用户
 | 
			
		||||
            else if (resourcePurpose == ResourcePurpose.User)
 | 
			
		||||
            {
 | 
			
		||||
                result = _resourceManager.GetFullResourceList(examId, resourceType, resourcePurpose, user.ID);
 | 
			
		||||
            }
 | 
			
		||||
            // 模板
 | 
			
		||||
            else if (resourcePurpose == ResourcePurpose.Template)
 | 
			
		||||
            {
 | 
			
		||||
                result = _resourceManager.GetFullResourceList(examId, resourceType, resourcePurpose);
 | 
			
		||||
            }
 | 
			
		||||
            // 其他
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                // 这种情况下需要分别查询并合并结果
 | 
			
		||||
                var userResourcesResult = _resourceManager.GetFullResourceList(
 | 
			
		||||
                    examId, resourceType, ResourcePurpose.User, user.ID);
 | 
			
		||||
                var templateResourcesResult = _resourceManager.GetFullResourceList(
 | 
			
		||||
                    examId, resourceType, ResourcePurpose.Template, null);
 | 
			
		||||
 | 
			
		||||
                if (!userResourcesResult.IsSuccessful || !templateResourcesResult.IsSuccessful)
 | 
			
		||||
                {
 | 
			
		||||
                    logger.Error($"获取资源列表时出错");
 | 
			
		||||
                    return StatusCode(StatusCodes.Status500InternalServerError, "获取资源列表失败");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var allResources = userResourcesResult.Value.Concat(templateResourcesResult.Value)
 | 
			
		||||
                    .OrderByDescending(r => r.UploadTime);
 | 
			
		||||
                var mergedResourceInfos = allResources.Select(r => new ResourceInfo
 | 
			
		||||
                {
 | 
			
		||||
                    ID = r.ID.ToString(),
 | 
			
		||||
                    Name = r.ResourceName,
 | 
			
		||||
                    Type = r.ResourceType,
 | 
			
		||||
                    Purpose = r.Purpose,
 | 
			
		||||
                    UploadTime = r.UploadTime,
 | 
			
		||||
                    ExamID = r.ExamID,
 | 
			
		||||
                    MimeType = r.MimeType
 | 
			
		||||
                }).ToArray();
 | 
			
		||||
 | 
			
		||||
                logger.Info($"成功获取资源列表,共 {mergedResourceInfos.Length} 个资源");
 | 
			
		||||
                return Ok(mergedResourceInfos);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!result.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"获取资源列表时出错: {result.Error.Message}");
 | 
			
		||||
                return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源列表失败: {result.Error.Message}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var resources = result.Value.Select(r => new ResourceInfo
 | 
			
		||||
            {
 | 
			
		||||
                ID = r.ID.ToString(),
 | 
			
		||||
                Name = r.ResourceName,
 | 
			
		||||
                Type = r.ResourceType,
 | 
			
		||||
                Purpose = r.Purpose,
 | 
			
		||||
                UploadTime = r.UploadTime,
 | 
			
		||||
                ExamID = r.ExamID,
 | 
			
		||||
                MimeType = r.MimeType
 | 
			
		||||
            }).ToArray();
 | 
			
		||||
 | 
			
		||||
            logger.Info($"成功获取资源列表,共 {resources.Length} 个资源");
 | 
			
		||||
            return Ok(resources);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"获取资源列表时出错: {ex.Message}");
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源列表失败: {ex.Message}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 根据资源ID下载资源
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="resourceId">资源ID</param>
 | 
			
		||||
    /// <returns>资源文件</returns>
 | 
			
		||||
    [HttpGet("{resourceId}")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult GetResourceById(string resourceId)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var result = _resourceManager.GetResourceById(resourceId);
 | 
			
		||||
 | 
			
		||||
            if (!result.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Warn($"资源不存在: {resourceId}");
 | 
			
		||||
                return NotFound($"资源 {resourceId} 不存在");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var resource = result.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}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 删除资源
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="resourceId">资源ID</param>
 | 
			
		||||
    /// <returns>删除结果</returns>
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [HttpDelete("{resourceId}")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult DeleteResource(string 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.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Warn($"资源不存在: {resourceId}");
 | 
			
		||||
                return NotFound($"资源 {resourceId} 不存在");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var resource = resourceResult.Value;
 | 
			
		||||
 | 
			
		||||
            // 权限检查:管理员可以删除所有资源,普通用户只能删除自己的用户资源
 | 
			
		||||
            if (!User.IsInRole("Admin"))
 | 
			
		||||
            {
 | 
			
		||||
                if (resource.Purpose == ResourcePurpose.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}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 资源信息类
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -23,7 +326,7 @@ public class ResourceController : ControllerBase
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 资源ID
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public int ID { get; set; }
 | 
			
		||||
        public required string ID { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 资源名称
 | 
			
		||||
@@ -38,7 +341,7 @@ public class ResourceController : ControllerBase
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 资源用途(template/user)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string Purpose { get; set; }
 | 
			
		||||
        public required ResourcePurpose Purpose { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 上传时间
 | 
			
		||||
@@ -69,7 +372,7 @@ public class ResourceController : ControllerBase
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 资源用途(template/user)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string ResourcePurpose { get; set; }
 | 
			
		||||
        public required ResourcePurpose ResourcePurpose { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 所属实验ID(可选)
 | 
			
		||||
@@ -77,301 +380,4 @@ public class ResourceController : ControllerBase
 | 
			
		||||
        public string? ExamID { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 添加资源(文件上传)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="request">添加资源请求</param>
 | 
			
		||||
    /// <param name="file">资源文件</param>
 | 
			
		||||
    /// <returns>添加结果</returns>
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(ResourceInfo), StatusCodes.Status201Created)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public async Task<IActionResult> AddResource([FromForm] AddResourceRequest request, IFormFile file)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(request.ResourceType) || string.IsNullOrWhiteSpace(request.ResourcePurpose) || file == null)
 | 
			
		||||
            return BadRequest("资源类型、资源用途和文件不能为空");
 | 
			
		||||
 | 
			
		||||
        // 验证资源用途
 | 
			
		||||
        if (request.ResourcePurpose != Resource.ResourcePurposes.Template && request.ResourcePurpose != Resource.ResourcePurposes.User)
 | 
			
		||||
            return BadRequest($"无效的资源用途: {request.ResourcePurpose}");
 | 
			
		||||
 | 
			
		||||
        // 模板资源需要管理员权限
 | 
			
		||||
        if (request.ResourcePurpose == Resource.ResourcePurposes.Template && !User.IsInRole("Admin"))
 | 
			
		||||
            return Forbid("只有管理员可以添加模板资源");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var db = new Database.AppDataConnection();
 | 
			
		||||
            
 | 
			
		||||
            // 获取当前用户ID
 | 
			
		||||
            var userName = User.Identity?.Name;
 | 
			
		||||
            if (string.IsNullOrEmpty(userName))
 | 
			
		||||
                return Unauthorized("无法获取用户信息");
 | 
			
		||||
 | 
			
		||||
            var userResult = db.GetUserByName(userName);
 | 
			
		||||
            if (!userResult.IsSuccessful || !userResult.Value.HasValue)
 | 
			
		||||
                return Unauthorized("用户不存在");
 | 
			
		||||
 | 
			
		||||
            var user = userResult.Value.Value;
 | 
			
		||||
 | 
			
		||||
            // 读取文件数据
 | 
			
		||||
            using var memoryStream = new MemoryStream();
 | 
			
		||||
            await file.CopyToAsync(memoryStream);
 | 
			
		||||
            var fileData = memoryStream.ToArray();
 | 
			
		||||
 | 
			
		||||
            var result = db.AddResource(user.ID, request.ResourceType, request.ResourcePurpose, file.FileName, fileData, request.ExamID);
 | 
			
		||||
 | 
			
		||||
            if (!result.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                if (result.Error.Message.Contains("不存在"))
 | 
			
		||||
                    return NotFound(result.Error.Message);
 | 
			
		||||
                
 | 
			
		||||
                logger.Error($"添加资源时出错: {result.Error.Message}");
 | 
			
		||||
                return StatusCode(StatusCodes.Status500InternalServerError, $"添加资源失败: {result.Error.Message}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var resource = result.Value;
 | 
			
		||||
            var resourceInfo = new ResourceInfo
 | 
			
		||||
            {
 | 
			
		||||
                ID = resource.ID,
 | 
			
		||||
                Name = resource.ResourceName,
 | 
			
		||||
                Type = resource.ResourceType,
 | 
			
		||||
                Purpose = resource.ResourcePurpose,
 | 
			
		||||
                UploadTime = resource.UploadTime,
 | 
			
		||||
                ExamID = resource.ExamID,
 | 
			
		||||
                MimeType = resource.MimeType
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            logger.Info($"成功添加资源: {request.ResourceType}/{request.ResourcePurpose}/{file.FileName}");
 | 
			
		||||
            return CreatedAtAction(nameof(GetResourceById), new { resourceId = resource.ID }, resourceInfo);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"添加资源 {request.ResourceType}/{request.ResourcePurpose}/{file.FileName} 时出错: {ex.Message}");
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, $"添加资源失败: {ex.Message}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取资源列表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="examId">实验ID(可选)</param>
 | 
			
		||||
    /// <param name="resourceType">资源类型(可选)</param>
 | 
			
		||||
    /// <param name="resourcePurpose">资源用途(可选)</param>
 | 
			
		||||
    /// <returns>资源列表</returns>
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [HttpGet]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(ResourceInfo[]), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult GetResourceList([FromQuery] string? examId = null, [FromQuery] string? resourceType = null, [FromQuery] string? resourcePurpose = null)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var db = new Database.AppDataConnection();
 | 
			
		||||
            
 | 
			
		||||
            // 获取当前用户ID
 | 
			
		||||
            var userName = User.Identity?.Name;
 | 
			
		||||
            if (string.IsNullOrEmpty(userName))
 | 
			
		||||
                return Unauthorized("无法获取用户信息");
 | 
			
		||||
 | 
			
		||||
            var userResult = db.GetUserByName(userName);
 | 
			
		||||
            if (!userResult.IsSuccessful || !userResult.Value.HasValue)
 | 
			
		||||
                return Unauthorized("用户不存在");
 | 
			
		||||
 | 
			
		||||
            var user = userResult.Value.Value;
 | 
			
		||||
 | 
			
		||||
            // 普通用户只能查看自己的资源和模板资源
 | 
			
		||||
            Guid? userId = null;
 | 
			
		||||
            if (!User.IsInRole("Admin"))
 | 
			
		||||
            {
 | 
			
		||||
                // 如果指定了用户资源用途,则只查看自己的资源
 | 
			
		||||
                if (resourcePurpose == Resource.ResourcePurposes.User)
 | 
			
		||||
                {
 | 
			
		||||
                    userId = user.ID;
 | 
			
		||||
                }
 | 
			
		||||
                // 如果指定了模板资源用途,则不限制用户ID
 | 
			
		||||
                else if (resourcePurpose == Resource.ResourcePurposes.Template)
 | 
			
		||||
                {
 | 
			
		||||
                    userId = null;
 | 
			
		||||
                }
 | 
			
		||||
                // 如果没有指定用途,则查看自己的用户资源和所有模板资源
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    // 这种情况下需要分别查询并合并结果
 | 
			
		||||
                    var userResourcesResult = db.GetFullResourceList(examId, resourceType, Resource.ResourcePurposes.User, user.ID);
 | 
			
		||||
                    var templateResourcesResult = db.GetFullResourceList(examId, resourceType, Resource.ResourcePurposes.Template, null);
 | 
			
		||||
                    
 | 
			
		||||
                    if (!userResourcesResult.IsSuccessful || !templateResourcesResult.IsSuccessful)
 | 
			
		||||
                    {
 | 
			
		||||
                        logger.Error($"获取资源列表时出错");
 | 
			
		||||
                        return StatusCode(StatusCodes.Status500InternalServerError, "获取资源列表失败");
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    var allResources = userResourcesResult.Value.Concat(templateResourcesResult.Value)
 | 
			
		||||
                        .OrderByDescending(r => r.UploadTime);
 | 
			
		||||
                    var mergedResourceInfos = allResources.Select(r => new ResourceInfo
 | 
			
		||||
                    {
 | 
			
		||||
                        ID = r.ID,
 | 
			
		||||
                        Name = r.ResourceName,
 | 
			
		||||
                        Type = r.ResourceType,
 | 
			
		||||
                        Purpose = r.ResourcePurpose,
 | 
			
		||||
                        UploadTime = r.UploadTime,
 | 
			
		||||
                        ExamID = r.ExamID,
 | 
			
		||||
                        MimeType = r.MimeType
 | 
			
		||||
                    }).ToArray();
 | 
			
		||||
 | 
			
		||||
                    logger.Info($"成功获取资源列表,共 {mergedResourceInfos.Length} 个资源");
 | 
			
		||||
                    return Ok(mergedResourceInfos);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var result = db.GetFullResourceList(examId, resourceType, resourcePurpose, userId);
 | 
			
		||||
 | 
			
		||||
            if (!result.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"获取资源列表时出错: {result.Error.Message}");
 | 
			
		||||
                return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源列表失败: {result.Error.Message}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var resources = result.Value.Select(r => new ResourceInfo
 | 
			
		||||
            {
 | 
			
		||||
                ID = r.ID,
 | 
			
		||||
                Name = r.ResourceName,
 | 
			
		||||
                Type = r.ResourceType,
 | 
			
		||||
                Purpose = r.ResourcePurpose,
 | 
			
		||||
                UploadTime = r.UploadTime,
 | 
			
		||||
                ExamID = r.ExamID,
 | 
			
		||||
                MimeType = r.MimeType
 | 
			
		||||
            }).ToArray();
 | 
			
		||||
 | 
			
		||||
            logger.Info($"成功获取资源列表,共 {resources.Length} 个资源");
 | 
			
		||||
            return Ok(resources);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"获取资源列表时出错: {ex.Message}");
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源列表失败: {ex.Message}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 根据资源ID下载资源
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="resourceId">资源ID</param>
 | 
			
		||||
    /// <returns>资源文件</returns>
 | 
			
		||||
    [HttpGet("{resourceId}")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult GetResourceById(int resourceId)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var db = new Database.AppDataConnection();
 | 
			
		||||
            var result = db.GetResourceById(resourceId);
 | 
			
		||||
 | 
			
		||||
            if (!result.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"获取资源时出错: {result.Error.Message}");
 | 
			
		||||
                return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {result.Error.Message}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!result.Value.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Warn($"资源不存在: {resourceId}");
 | 
			
		||||
                return NotFound($"资源 {resourceId} 不存在");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var resource = result.Value.Value;
 | 
			
		||||
            logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
 | 
			
		||||
            return File(resource.Data, resource.MimeType ?? "application/octet-stream", resource.ResourceName);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"获取资源 {resourceId} 时出错: {ex.Message}");
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {ex.Message}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 删除资源
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="resourceId">资源ID</param>
 | 
			
		||||
    /// <returns>删除结果</returns>
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [HttpDelete("{resourceId}")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult DeleteResource(int resourceId)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var db = new Database.AppDataConnection();
 | 
			
		||||
            
 | 
			
		||||
            // 获取当前用户信息
 | 
			
		||||
            var userName = User.Identity?.Name;
 | 
			
		||||
            if (string.IsNullOrEmpty(userName))
 | 
			
		||||
                return Unauthorized("无法获取用户信息");
 | 
			
		||||
 | 
			
		||||
            var userResult = db.GetUserByName(userName);
 | 
			
		||||
            if (!userResult.IsSuccessful || !userResult.Value.HasValue)
 | 
			
		||||
                return Unauthorized("用户不存在");
 | 
			
		||||
 | 
			
		||||
            var user = userResult.Value.Value;
 | 
			
		||||
 | 
			
		||||
            // 先获取资源信息以验证权限
 | 
			
		||||
            var resourceResult = db.GetResourceById(resourceId);
 | 
			
		||||
            if (!resourceResult.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"获取资源时出错: {resourceResult.Error.Message}");
 | 
			
		||||
                return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {resourceResult.Error.Message}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!resourceResult.Value.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Warn($"资源不存在: {resourceId}");
 | 
			
		||||
                return NotFound($"资源 {resourceId} 不存在");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var resource = resourceResult.Value.Value;
 | 
			
		||||
 | 
			
		||||
            // 权限检查:管理员可以删除所有资源,普通用户只能删除自己的用户资源
 | 
			
		||||
            if (!User.IsInRole("Admin"))
 | 
			
		||||
            {
 | 
			
		||||
                if (resource.ResourcePurpose == Resource.ResourcePurposes.Template)
 | 
			
		||||
                    return Forbid("普通用户不能删除模板资源");
 | 
			
		||||
                
 | 
			
		||||
                if (resource.UserID != user.ID)
 | 
			
		||||
                    return Forbid("只能删除自己的资源");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var deleteResult = db.DeleteResource(resourceId);
 | 
			
		||||
            if (!deleteResult.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"删除资源时出错: {deleteResult.Error.Message}");
 | 
			
		||||
                return StatusCode(StatusCodes.Status500InternalServerError, $"删除资源失败: {deleteResult.Error.Message}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            logger.Info($"成功删除资源: {resourceId} ({resource.ResourceName})");
 | 
			
		||||
            return NoContent();
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"删除资源 {resourceId} 时出错: {ex.Message}");
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, $"删除资源失败: {ex.Message}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,39 +3,22 @@ using Microsoft.AspNetCore.Cors;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
using Database;
 | 
			
		||||
using DotNext;
 | 
			
		||||
using server.Services;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 视频流控制器,支持动态配置摄像头连接
 | 
			
		||||
/// </summary>
 | 
			
		||||
[ApiController]
 | 
			
		||||
[Authorize]
 | 
			
		||||
[EnableCors("Users")]
 | 
			
		||||
[Route("api/[controller]")]
 | 
			
		||||
public class VideoStreamController : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
			
		||||
    private readonly server.Services.HttpVideoStreamService _videoStreamService;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 分辨率配置请求模型
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class ResolutionConfigRequest
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 宽度
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        [Required]
 | 
			
		||||
        [Range(640, 1920, ErrorMessage = "宽度必须在640-1920范围内")]
 | 
			
		||||
        public int Width { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 高度
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        [Required]
 | 
			
		||||
        [Range(480, 1080, ErrorMessage = "高度必须在480-1080范围内")]
 | 
			
		||||
        public int Height { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
    private readonly HttpVideoStreamService _videoStreamService;
 | 
			
		||||
    private readonly Database.UserManager _userManager;
 | 
			
		||||
 | 
			
		||||
    public class AvailableResolutionsResponse
 | 
			
		||||
    {
 | 
			
		||||
@@ -49,10 +32,40 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
    /// 初始化HTTP视频流控制器
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="videoStreamService">HTTP视频流服务</param>
 | 
			
		||||
    public VideoStreamController(server.Services.HttpVideoStreamService videoStreamService)
 | 
			
		||||
    /// <param name="userManager">用户管理服务</param>
 | 
			
		||||
    public VideoStreamController(
 | 
			
		||||
        HttpVideoStreamService videoStreamService, Database.UserManager userManager)
 | 
			
		||||
    {
 | 
			
		||||
        logger.Info("创建VideoStreamController,命名空间:{Namespace}", this.GetType().Namespace);
 | 
			
		||||
        _videoStreamService = videoStreamService;
 | 
			
		||||
        _userManager = userManager;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Optional<string> TryGetBoardId()
 | 
			
		||||
    {
 | 
			
		||||
        var userName = User.FindFirstValue(ClaimTypes.Name);
 | 
			
		||||
        if (string.IsNullOrEmpty(userName))
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error("User name not found in claims.");
 | 
			
		||||
            return Optional<string>.None;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var userRet = _userManager.GetUserByName(userName);
 | 
			
		||||
        if (!userRet.IsSuccessful || !userRet.Value.HasValue)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error("User not found.");
 | 
			
		||||
            return Optional<string>.None;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var user = userRet.Value.Value;
 | 
			
		||||
        var boardId = user.BoardID;
 | 
			
		||||
        if (boardId == Guid.Empty)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error("No board bound to this user.");
 | 
			
		||||
            return Optional<string>.None;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return boardId.ToString();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Optional<string> TryGetBoardId()
 | 
			
		||||
@@ -93,11 +106,10 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
    /// 获取 HTTP 视频流服务状态
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>服务状态信息</returns>
 | 
			
		||||
    [HttpGet("Status")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
 | 
			
		||||
    [HttpGet("ServiceStatus")]
 | 
			
		||||
    [ProducesResponseType(typeof(VideoStreamServiceStatus), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IResult GetStatus()
 | 
			
		||||
    public IResult GetServiceStatus()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
@@ -115,8 +127,7 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet("MyEndpoint")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(VideoStreamEndpoint), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IResult MyEndpoint()
 | 
			
		||||
    {
 | 
			
		||||
@@ -139,7 +150,6 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>连接测试结果</returns>
 | 
			
		||||
    [HttpPost("TestConnection")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public async Task<IResult> TestConnection()
 | 
			
		||||
@@ -172,14 +182,16 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpPost("DisableTransmission")]
 | 
			
		||||
    public async Task<IActionResult> DisableHdmiTransmission()
 | 
			
		||||
    [HttpPost("SetVideoStreamEnable")]
 | 
			
		||||
    [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public async Task<IActionResult> SetVideoStreamEnable(bool enable)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var boardId = TryGetBoardId().OrThrow(() => new ArgumentException("Board ID is required"));
 | 
			
		||||
 | 
			
		||||
            await _videoStreamService.DisableHdmiTransmissionAsync(boardId.ToString());
 | 
			
		||||
            await _videoStreamService.SetVideoStreamEnableAsync(boardId.ToString(), enable);
 | 
			
		||||
            return Ok($"HDMI transmission for board {boardId} disabled.");
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
@@ -241,7 +253,7 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
    /// <returns>支持的分辨率列表</returns>
 | 
			
		||||
    [HttpGet("SupportedResolutions")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(AvailableResolutionsResponse[]), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IResult GetSupportedResolutions()
 | 
			
		||||
    {
 | 
			
		||||
@@ -349,4 +361,65 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
            return TypedResults.InternalServerError($"执行自动对焦失败: {ex.Message}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 配置摄像头连接参数
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>配置结果</returns>
 | 
			
		||||
    [HttpPost("ConfigureCamera")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public async Task<IResult> ConfigureCamera()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var boardId = TryGetBoardId().OrThrow(() => new Exception("Board ID not found"));
 | 
			
		||||
 | 
			
		||||
            var ret = await _videoStreamService.ConfigureCameraAsync(boardId);
 | 
			
		||||
 | 
			
		||||
            if (ret)
 | 
			
		||||
            {
 | 
			
		||||
                return TypedResults.Ok(new { Message = "配置成功" });
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return TypedResults.BadRequest(new { Message = "配置失败" });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "配置摄像头连接失败");
 | 
			
		||||
            return TypedResults.InternalServerError(ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 分辨率配置请求模型
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class ResolutionConfigRequest
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 宽度
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        [Required]
 | 
			
		||||
        [Range(640, 1920, ErrorMessage = "宽度必须在640-1920范围内")]
 | 
			
		||||
        public int Width { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 高度
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        [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}";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										98
									
								
								server/src/Database/Connection.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								server/src/Database/Connection.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,98 @@
 | 
			
		||||
using DotNext;
 | 
			
		||||
using LinqToDB;
 | 
			
		||||
using LinqToDB.Data;
 | 
			
		||||
 | 
			
		||||
namespace Database;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 应用程序数据连接类,用于与数据库交互
 | 
			
		||||
/// </summary>
 | 
			
		||||
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}");
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public ITable<User> UserTable => this.GetTable<User>();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// FPGA 板子表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public ITable<Board> BoardTable => this.GetTable<Board>();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public ITable<Exam> ExamTable => this.GetTable<Exam>();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 资源表(统一管理实验资源、用户比特流等)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public ITable<Resource> ResourceTable => this.GetTable<Resource>();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 初始化应用程序数据连接
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    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.UserPermission.Admin,
 | 
			
		||||
            };
 | 
			
		||||
            this.Insert(user);
 | 
			
		||||
            logger.Info("默认管理员用户已创建");
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            logger.Info($"数据库连接已建立: {DATABASE_FILEPATH}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 创建所有数据库表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public void CreateAllTables()
 | 
			
		||||
    {
 | 
			
		||||
        logger.Info("正在创建数据库表...");
 | 
			
		||||
        this.CreateTable<User>();
 | 
			
		||||
        this.CreateTable<Board>();
 | 
			
		||||
        this.CreateTable<Exam>();
 | 
			
		||||
        this.CreateTable<Resource>();
 | 
			
		||||
        logger.Info("数据库表创建完成");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 删除所有数据库表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public void DropAllTables()
 | 
			
		||||
    {
 | 
			
		||||
        logger.Warn("正在删除所有数据库表...");
 | 
			
		||||
        this.DropTable<User>();
 | 
			
		||||
        this.DropTable<Board>();
 | 
			
		||||
        this.DropTable<Exam>();
 | 
			
		||||
        this.DropTable<Resource>();
 | 
			
		||||
        logger.Warn("所有数据库表已删除");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										154
									
								
								server/src/Database/ExamManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								server/src/Database/ExamManager.cs
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 创建新实验
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="id">实验ID</param>
 | 
			
		||||
    /// <param name="name">实验名称</param>
 | 
			
		||||
    /// <param name="description">实验描述</param>
 | 
			
		||||
    /// <param name="tags">实验标签</param>
 | 
			
		||||
    /// <param name="difficulty">实验难度</param>
 | 
			
		||||
    /// <param name="isVisibleToUsers">普通用户是否可见</param>
 | 
			
		||||
    /// <returns>创建的实验</returns>
 | 
			
		||||
    public Result<Exam> 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.ToString() == 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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 更新实验信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="id">实验ID</param>
 | 
			
		||||
    /// <param name="name">实验名称</param>
 | 
			
		||||
    /// <param name="description">实验描述</param>
 | 
			
		||||
    /// <param name="tags">实验标签</param>
 | 
			
		||||
    /// <param name="difficulty">实验难度</param>
 | 
			
		||||
    /// <param name="isVisibleToUsers">普通用户是否可见</param>
 | 
			
		||||
    /// <returns>更新的记录数</returns>
 | 
			
		||||
    public Result<int> 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.ToString() == id).Set(e => e.Name, name).Update();
 | 
			
		||||
            }
 | 
			
		||||
            if (description != null)
 | 
			
		||||
            {
 | 
			
		||||
                result += _db.ExamTable.Where(e => e.ID.ToString() == id).Set(e => e.Description, description).Update();
 | 
			
		||||
            }
 | 
			
		||||
            if (tags != null)
 | 
			
		||||
            {
 | 
			
		||||
                var tagsString = string.Join(",", tags.Where(tag => !string.IsNullOrWhiteSpace(tag)).Select(tag => tag.Trim()));
 | 
			
		||||
                result += _db.ExamTable.Where(e => e.ID.ToString() == id).Set(e => e.Tags, tagsString).Update();
 | 
			
		||||
            }
 | 
			
		||||
            if (difficulty.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                result += _db.ExamTable.Where(e => e.ID.ToString() == id).Set(e => e.Difficulty, Math.Max(1, Math.Min(5, difficulty.Value))).Update();
 | 
			
		||||
            }
 | 
			
		||||
            if (isVisibleToUsers.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                result += _db.ExamTable.Where(e => e.ID.ToString() == id).Set(e => e.IsVisibleToUsers, isVisibleToUsers.Value).Update();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 更新时间
 | 
			
		||||
            _db.ExamTable.Where(e => e.ID.ToString() == 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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取所有实验信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>所有实验的数组</returns>
 | 
			
		||||
    public Exam[] GetAllExams()
 | 
			
		||||
    {
 | 
			
		||||
        var exams = _db.ExamTable.OrderBy(e => e.ID).ToArray();
 | 
			
		||||
        logger.Debug($"获取所有实验,共 {exams.Length} 个");
 | 
			
		||||
        return exams;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 根据实验ID获取实验信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="examId">实验ID</param>
 | 
			
		||||
    /// <returns>包含实验信息的结果,如果未找到则返回空</returns>
 | 
			
		||||
    public Result<Optional<Exam>> GetExamByID(string examId)
 | 
			
		||||
    {
 | 
			
		||||
        var exams = _db.ExamTable.Where(exam => exam.ID.ToString() == 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<Exam>.None);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        logger.Debug($"成功获取实验信息: {examId}");
 | 
			
		||||
        return new(exams[0]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										356
									
								
								server/src/Database/ResourceManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										356
									
								
								server/src/Database/ResourceManager.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,356 @@
 | 
			
		||||
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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 根据文件扩展名获取MIME类型
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="extension">文件扩展名</param>
 | 
			
		||||
    /// <param name="fileName">文件名(可选,用于特殊文件判断)</param>
 | 
			
		||||
    /// <returns>MIME类型</returns>
 | 
			
		||||
    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"
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 将二进制数据写入指定路径
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="path">目标文件路径</param>
 | 
			
		||||
    /// <param name="data">要写入的二进制数据</param>
 | 
			
		||||
    /// <returns>写入是否成功</returns>
 | 
			
		||||
    public Result<bool> 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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 从指定路径读取二进制数据
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="path">要读取的文件路径</param>
 | 
			
		||||
    /// <returns>读取到的二进制数据</returns>
 | 
			
		||||
    public Result<byte[]> 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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 添加资源
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="userId">上传用户ID</param>
 | 
			
		||||
    /// <param name="resourceType">资源类型</param>
 | 
			
		||||
    /// <param name="resourcePurpose">资源用途(template 或 user)</param>
 | 
			
		||||
    /// <param name="resourceName">资源名称</param>
 | 
			
		||||
    /// <param name="data">资源二进制数据</param>
 | 
			
		||||
    /// <param name="examId">所属实验ID(可选)</param>
 | 
			
		||||
    /// <param name="mimeType">MIME类型(可选,将根据文件扩展名自动确定)</param>
 | 
			
		||||
    /// <returns>创建的资源</returns>
 | 
			
		||||
    public Result<Resource> AddResource(
 | 
			
		||||
        Guid userId, string resourceType, ResourcePurpose 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.ToString() == examId).FirstOrDefault();
 | 
			
		||||
                if (exam == null)
 | 
			
		||||
                {
 | 
			
		||||
                    logger.Error($"实验不存在: {examId}");
 | 
			
		||||
                    return new(new Exception($"实验不存在: {examId}"));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 验证资源用途
 | 
			
		||||
            if (resourcePurpose != ResourcePurpose.Template &&
 | 
			
		||||
                resourcePurpose != ResourcePurpose.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 duplicateResource;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var nowTime = DateTime.Now;
 | 
			
		||||
            var resource = new Resource
 | 
			
		||||
            {
 | 
			
		||||
                UserID = userId,
 | 
			
		||||
                ExamID = examId,
 | 
			
		||||
                ResourceType = resourceType,
 | 
			
		||||
                Purpose = resourcePurpose,
 | 
			
		||||
                ResourceName = resourceName,
 | 
			
		||||
                Path = duplicateResource == null ?
 | 
			
		||||
                    Path.Combine(resourceType, nowTime.ToString("yyyyMMddHH"), resourceName) :
 | 
			
		||||
                    duplicateResource.Path,
 | 
			
		||||
                SHA256 = sha256,
 | 
			
		||||
                MimeType = mimeType,
 | 
			
		||||
                UploadTime = nowTime
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            var insertedId = _db.Insert(resource);
 | 
			
		||||
 | 
			
		||||
            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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取资源信息列表(返回ID和名称)
 | 
			
		||||
    /// <param name="resourceType">资源类型</param>
 | 
			
		||||
    /// <param name="examId">实验ID(可选)</param>
 | 
			
		||||
    /// <param name="resourcePurpose">资源用途(可选)</param>
 | 
			
		||||
    /// <param name="userId">用户ID(可选)</param>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>资源信息列表</returns>
 | 
			
		||||
    public Result<(string ID, string Name)[]> GetResourceListByType(
 | 
			
		||||
        string resourceType,
 | 
			
		||||
        ResourcePurpose? resourcePurpose = null,
 | 
			
		||||
        string? examId = 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.Purpose == 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.ToString(), r.ResourceName)).ToArray();
 | 
			
		||||
            logger.Info($"获取资源列表: {resourceType}" +
 | 
			
		||||
                       (examId != null ? $"/{examId}" : "") +
 | 
			
		||||
                       ($"/{resourcePurpose.ToString()}") +
 | 
			
		||||
                       (userId != null ? $"/{userId}" : "") +
 | 
			
		||||
                       $",共 {result.Length} 个资源");
 | 
			
		||||
            return new(result);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"获取资源列表时出错: {ex.Message}");
 | 
			
		||||
            return new(ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取完整的资源列表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="examId">实验ID(可选)</param>
 | 
			
		||||
    /// <param name="resourceType">资源类型(可选)</param>
 | 
			
		||||
    /// <param name="resourcePurpose">资源用途(可选)</param>
 | 
			
		||||
    /// <param name="userId">用户ID(可选)</param>
 | 
			
		||||
    /// <returns>完整的资源对象列表</returns>
 | 
			
		||||
    public Result<List<Resource>> GetFullResourceList(
 | 
			
		||||
        string? examId = null,
 | 
			
		||||
        string? resourceType = null,
 | 
			
		||||
        ResourcePurpose? 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.Purpose == 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.ToString()}]") +
 | 
			
		||||
                       (userId != null ? $" [用户: {userId}]" : "") +
 | 
			
		||||
                       $",共 {resources.Count} 个资源");
 | 
			
		||||
            return new(resources);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"获取完整资源列表时出错: {ex.Message}");
 | 
			
		||||
            return new(ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 根据资源ID获取资源
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="resourceId">资源ID</param>
 | 
			
		||||
    /// <returns>资源数据</returns>
 | 
			
		||||
    public Optional<Resource> GetResourceById(string resourceId)
 | 
			
		||||
    {
 | 
			
		||||
        var resource = _db.ResourceTable.Where(r => r.ID.ToString() == resourceId).FirstOrDefault();
 | 
			
		||||
 | 
			
		||||
        if (resource == null)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Info($"未找到资源: {resourceId}");
 | 
			
		||||
            return new(null);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
 | 
			
		||||
        return new(resource);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 删除资源
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="resourceId">资源ID</param>
 | 
			
		||||
    /// <returns>删除的记录数</returns>
 | 
			
		||||
    public Result<int> DeleteResource(string resourceId)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var result = _db.ResourceTable.Where(r => r.ID.ToString() == resourceId).Delete();
 | 
			
		||||
            logger.Info($"资源已删除: {resourceId},删除记录数: {result}");
 | 
			
		||||
            return new(result);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"删除资源时出错: {ex.Message}");
 | 
			
		||||
            return new(ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										350
									
								
								server/src/Database/Type.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										350
									
								
								server/src/Database/Type.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,350 @@
 | 
			
		||||
using DotNext;
 | 
			
		||||
using LinqToDB;
 | 
			
		||||
using LinqToDB.Mapping;
 | 
			
		||||
 | 
			
		||||
namespace Database;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 用户权限枚举
 | 
			
		||||
/// </summary>
 | 
			
		||||
public enum UserPermission
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 管理员权限,可以管理用户和实验板
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    Admin,
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 普通用户权限,只能使用实验板
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    Normal,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 用户类,表示用户信息
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class User
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户的唯一标识符
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [PrimaryKey]
 | 
			
		||||
    public Guid ID { get; set; } = Guid.NewGuid();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户的名称
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public required string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户的电子邮箱
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public required string EMail { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户的密码(应该进行哈希处理)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public required string Password { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户权限等级
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public required UserPermission Permission { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 绑定的实验板ID,如果未绑定则为空
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Nullable]
 | 
			
		||||
    public Guid BoardID { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户绑定板子的过期时间
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Nullable]
 | 
			
		||||
    public DateTime? BoardExpireTime { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// FPGA 板子状态枚举
 | 
			
		||||
/// </summary>
 | 
			
		||||
public enum BoardStatus
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 未启用状态,无法被使用
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    Disabled,
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 繁忙状态,正在被用户使用
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    Busy,
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 可用状态,可以被分配给用户
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    Available,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// FPGA 板子类,表示板子信息
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class Board
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// FPGA 板子的唯一标识符
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [PrimaryKey]
 | 
			
		||||
    public Guid ID { get; set; } = Guid.NewGuid();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// FPGA 板子的名称
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public required string BoardName { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// FPGA 板子的IP地址
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public required string IpAddr { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// FPGA 板子的MAC地址
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public required string MacAddr { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// FPGA 板子的通信端口
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public int Port { get; set; } = 1234;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// FPGA 板子的当前状态
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public required BoardStatus Status { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 占用该板子的用户的唯一标识符
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Nullable]
 | 
			
		||||
    public Guid OccupiedUserID { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 占用该板子的用户的用户名
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Nullable]
 | 
			
		||||
    public string? OccupiedUserName { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// FPGA 板子的固件版本号
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public string FirmVersion { get; set; } = "1.0.0";
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 实验类,表示实验信息
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class Exam
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验的唯一标识符
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [PrimaryKey]
 | 
			
		||||
    public required string ID { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验名称
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public required string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验描述
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public required string Description { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验创建时间
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public DateTime CreatedTime { get; set; } = DateTime.Now;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验最后更新时间
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public DateTime UpdatedTime { get; set; } = DateTime.Now;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验标签(以逗号分隔的字符串)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public string Tags { get; set; } = "";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验难度(1-5,1为最简单)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public int Difficulty { get; set; } = 1;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 普通用户是否可见
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public bool IsVisibleToUsers { get; set; } = true;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取标签列表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>标签数组</returns>
 | 
			
		||||
    public string[] GetTagsList()
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(Tags))
 | 
			
		||||
            return Array.Empty<string>();
 | 
			
		||||
 | 
			
		||||
        return Tags.Split(',', StringSplitOptions.RemoveEmptyEntries)
 | 
			
		||||
                   .Select(tag => tag.Trim())
 | 
			
		||||
                   .Where(tag => !string.IsNullOrEmpty(tag))
 | 
			
		||||
                   .ToArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 设置标签列表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="tags">标签数组</param>
 | 
			
		||||
    public void SetTagsList(string[] tags)
 | 
			
		||||
    {
 | 
			
		||||
        Tags = string.Join(",", tags.Where(tag => !string.IsNullOrWhiteSpace(tag)).Select(tag => tag.Trim()));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 资源类型枚举
 | 
			
		||||
/// </summary>
 | 
			
		||||
public static class ResourceTypes
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 图片资源类型
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public const string Images = "images";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Markdown文档资源类型
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public const string Markdown = "markdown";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 比特流文件资源类型
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public const string Bitstream = "bitstream";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 原理图资源类型
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public const string Diagram = "diagram";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 项目文件资源类型
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public const string Project = "project";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 压缩文件资源类型
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public const string Compression = "compression";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public enum ResourcePurpose : int
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 模板资源,通常由管理员上传,供用户参考
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    Template,
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户上传的资源
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    User,
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户提交的作业
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    Homework
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 资源类,统一管理实验资源、用户比特流等各类资源
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class Resource
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 资源的唯一标识符
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [PrimaryKey]
 | 
			
		||||
    public Guid ID { get; set; } = Guid.NewGuid();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 上传资源的用户ID
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public required Guid UserID { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 所属实验ID(可选,如果不属于特定实验则为空)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Nullable]
 | 
			
		||||
    public string? ExamID { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 资源类型(images, markdown, bitstream, diagram, project等)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public required string ResourceType { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 资源用途:template(模板)或 user(用户上传)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public required ResourcePurpose Purpose { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 资源名称(包含文件扩展名)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public required string ResourceName { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 资源路径(包含文件名和扩展名)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public required string Path { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 资源SHA256哈希值
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public required string SHA256 { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 资源创建/上传时间
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public DateTime UploadTime { get; set; } = DateTime.Now;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 资源的MIME类型
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public string MimeType { get; set; } = "application/octet-stream";
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										458
									
								
								server/src/Database/UserManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										458
									
								
								server/src/Database/UserManager.cs
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 添加一个新的用户到数据库
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="name">用户的名称</param>
 | 
			
		||||
    /// <param name="email">用户的电子邮箱地址</param>
 | 
			
		||||
    /// <param name="password">用户的密码</param>
 | 
			
		||||
    /// <returns>插入的记录数</returns>
 | 
			
		||||
    public int AddUser(string name, string email, string password)
 | 
			
		||||
    {
 | 
			
		||||
        var user = new User()
 | 
			
		||||
        {
 | 
			
		||||
            Name = name,
 | 
			
		||||
            EMail = email,
 | 
			
		||||
            Password = password,
 | 
			
		||||
            Permission = UserPermission.Normal,
 | 
			
		||||
        };
 | 
			
		||||
        var result = _db.Insert(user);
 | 
			
		||||
        logger.Info($"新用户已添加: {name} ({email})");
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 根据用户名获取用户信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="name">用户名</param>
 | 
			
		||||
    /// <returns>包含用户信息的结果,如果未找到或出错则返回相应状态</returns>
 | 
			
		||||
    public Result<Optional<User>> 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<User>.None);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        logger.Debug($"成功获取用户信息: {name}");
 | 
			
		||||
        return new(user[0]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 根据电子邮箱获取用户信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="email">用户的电子邮箱地址</param>
 | 
			
		||||
    /// <returns>包含用户信息的结果,如果未找到或出错则返回相应状态</returns>
 | 
			
		||||
    public Result<Optional<User>> 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<User>.None);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        logger.Debug($"成功获取用户信息: {email}");
 | 
			
		||||
        return new(user[0]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 验证用户密码
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="name">用户名</param>
 | 
			
		||||
    /// <param name="password">用户密码</param>
 | 
			
		||||
    /// <returns>如果密码正确返回用户信息,否则返回空</returns>
 | 
			
		||||
    public Result<Optional<User>> CheckUserPassword(string name, string password)
 | 
			
		||||
    {
 | 
			
		||||
        var ret = GetUserByName(name);
 | 
			
		||||
        if (!ret.IsSuccessful)
 | 
			
		||||
            return new(ret.Error);
 | 
			
		||||
 | 
			
		||||
        if (!ret.Value.HasValue)
 | 
			
		||||
            return new(Optional<User>.None);
 | 
			
		||||
 | 
			
		||||
        var user = ret.Value.Value;
 | 
			
		||||
 | 
			
		||||
        if (user.Password == password)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Info($"用户 {name} 密码验证成功");
 | 
			
		||||
            return new(user);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            logger.Warn($"用户 {name} 密码验证失败");
 | 
			
		||||
            return new(Optional<User>.None);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 绑定用户与实验板
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="userId">用户的唯一标识符</param>
 | 
			
		||||
    /// <param name="boardId">实验板的唯一标识符</param>
 | 
			
		||||
    /// <param name="expireTime">绑定过期时间</param>
 | 
			
		||||
    /// <returns>更新的记录数</returns>
 | 
			
		||||
    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, BoardStatus.Busy)
 | 
			
		||||
            .Set(b => b.OccupiedUserID, userId)
 | 
			
		||||
            .Set(b => b.OccupiedUserName, user.Name)
 | 
			
		||||
            .Update();
 | 
			
		||||
 | 
			
		||||
        logger.Info($"用户 {userId} ({user.Name}) 已绑定到实验板 {boardId},过期时间: {expireTime}");
 | 
			
		||||
        return userResult + boardResult;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 解除用户与实验板的绑定
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="userId">用户的唯一标识符</param>
 | 
			
		||||
    /// <returns>更新的记录数</returns>
 | 
			
		||||
    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, BoardStatus.Available)
 | 
			
		||||
                .Set(b => b.OccupiedUserID, Guid.Empty)
 | 
			
		||||
                .Set(b => b.OccupiedUserName, (string?)null)
 | 
			
		||||
                .Update();
 | 
			
		||||
            logger.Info($"实验板 {boardId} 状态已设置为空闲,用户绑定信息已清空");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        logger.Info($"用户 {userId} 已解除实验板绑定");
 | 
			
		||||
        return userResult + boardResult;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自动分配一个未被占用的IP地址
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>分配的IP地址字符串</returns>
 | 
			
		||||
    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地址");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自动分配一个未被占用的MAC地址
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>分配的MAC地址字符串</returns>
 | 
			
		||||
    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地址");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 添加一块新的 FPGA 板子到数据库
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="name">FPGA 板子的名称</param>
 | 
			
		||||
    /// <returns>插入的记录数</returns>
 | 
			
		||||
    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 = BoardStatus.Disabled,
 | 
			
		||||
        };
 | 
			
		||||
        var result = _db.Insert(board);
 | 
			
		||||
        logger.Info($"新实验板已添加: {name} ({board.IpAddr}:{board.MacAddr})");
 | 
			
		||||
        return board.ID;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 根据名称删除实验板
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="name">实验板的名称</param>
 | 
			
		||||
    /// <returns>删除的记录数</returns>
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 根据ID删除实验板
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="id">实验板的唯一标识符</param>
 | 
			
		||||
    /// <returns>删除的记录数</returns>
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 根据实验板ID获取实验板信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="id">实验板的唯一标识符</param>
 | 
			
		||||
    /// <returns>包含实验板信息的结果,如果未找到则返回空</returns>
 | 
			
		||||
    public Result<Optional<Board>> 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<Board>.None);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        logger.Debug($"成功获取实验板信息: {id}");
 | 
			
		||||
        return new(boards[0]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 根据用户名获取实验板信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="userName">用户名</param>
 | 
			
		||||
    /// <returns>包含实验板信息的结果,如果未找到则返回空</returns>
 | 
			
		||||
    public Result<Optional<Board>> 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<Board>.None);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        logger.Debug($"成功获取实验板信息: {userName}");
 | 
			
		||||
        return new(boards[0]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取所有实验板信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>所有实验板的数组</returns>
 | 
			
		||||
    public Board[] GetAllBoard()
 | 
			
		||||
    {
 | 
			
		||||
        var boards = _db.BoardTable.ToArray();
 | 
			
		||||
        logger.Debug($"获取所有实验板,共 {boards.Length} 块");
 | 
			
		||||
        return boards;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取一块可用的实验板并将其状态设置为繁忙
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="userId">要分配板子的用户ID</param>
 | 
			
		||||
    /// <param name="expireTime">绑定过期时间</param>
 | 
			
		||||
    /// <returns>可用的实验板,如果没有可用的板子则返回空</returns>
 | 
			
		||||
    public Optional<Board> GetAvailableBoard(Guid userId, DateTime expireTime)
 | 
			
		||||
    {
 | 
			
		||||
        var boards = _db.BoardTable.Where(
 | 
			
		||||
                (board) => board.Status == 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, 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 = BoardStatus.Busy;
 | 
			
		||||
            board.OccupiedUserID = userId;
 | 
			
		||||
            board.OccupiedUserName = user.Name;
 | 
			
		||||
 | 
			
		||||
            logger.Info($"实验板 {board.BoardName} ({board.ID}) 已分配给用户 {user.Name} ({userId}),过期时间: {expireTime}");
 | 
			
		||||
            return new(board);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// [TODO:description]
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="boardId">[TODO:parameter]</param>
 | 
			
		||||
    /// <param name="newName">[TODO:parameter]</param>
 | 
			
		||||
    /// <returns>[TODO:return]</returns>
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// [TODO:description]
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="boardId">[TODO:parameter]</param>
 | 
			
		||||
    /// <param name="newStatus">[TODO:parameter]</param>
 | 
			
		||||
    /// <returns>[TODO:return]</returns>
 | 
			
		||||
    public int UpdateBoardStatus(Guid boardId, BoardStatus newStatus)
 | 
			
		||||
    {
 | 
			
		||||
        var result = _db.BoardTable
 | 
			
		||||
            .Where(b => b.ID == boardId)
 | 
			
		||||
            .Set(b => b.Status, newStatus)
 | 
			
		||||
            .Update();
 | 
			
		||||
        logger.Info($"TODO");
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -28,22 +28,24 @@ public interface IJtagReceiver
 | 
			
		||||
public class JtagHub : Hub<IJtagReceiver>, IJtagHub
 | 
			
		||||
{
 | 
			
		||||
    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
			
		||||
 | 
			
		||||
    private readonly IHubContext<JtagHub, IJtagReceiver> _hubContext;
 | 
			
		||||
    private readonly Database.UserManager _userManager;
 | 
			
		||||
 | 
			
		||||
    private static ConcurrentDictionary<string, int> FreqTable = new();
 | 
			
		||||
    private static ConcurrentDictionary<string, CancellationTokenSource> CancellationTokenSourceTable = new();
 | 
			
		||||
 | 
			
		||||
    private readonly IHubContext<JtagHub, IJtagReceiver> _hubContext;
 | 
			
		||||
 | 
			
		||||
    public JtagHub(IHubContext<JtagHub, IJtagReceiver> hubContext)
 | 
			
		||||
    public JtagHub(IHubContext<JtagHub, IJtagReceiver> hubContext, Database.UserManager userManager)
 | 
			
		||||
    {
 | 
			
		||||
        _hubContext = hubContext;
 | 
			
		||||
        _userManager = userManager;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Optional<Peripherals.JtagClient.Jtag> 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<IJtagReceiver>, IJtagHub
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await SetBoundaryScanFreq(freq);
 | 
			
		||||
            SetBoundaryScanFreq(freq);
 | 
			
		||||
            var cts = new CancellationTokenSource();
 | 
			
		||||
            CancellationTokenSourceTable.AddOrUpdate(userName, cts, (key, value) => cts);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ static class HdmiInAddr
 | 
			
		||||
    public const UInt32 HdmiIn_READFIFO = BASE + 0x1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class HdmiIn
 | 
			
		||||
public class HdmiIn
 | 
			
		||||
{
 | 
			
		||||
    private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,14 +7,28 @@ namespace Peripherals.JpegClient;
 | 
			
		||||
static class JpegAddr
 | 
			
		||||
{
 | 
			
		||||
    const UInt32 BASE = 0x0000_0000;
 | 
			
		||||
    public const UInt32 ENABLE = BASE + 0x0;
 | 
			
		||||
    public const UInt32 FRAME_NUM = BASE + 0x1;
 | 
			
		||||
    public const UInt32 FRAME_INFO = BASE + 0x2;
 | 
			
		||||
    public const UInt32 FRAME_SAMPLE_RATE = BASE + 0x3;
 | 
			
		||||
    public const UInt32 FRAME_DATA_MAX_POINTER = BASE + 0x4;
 | 
			
		||||
 | 
			
		||||
    public const UInt32 DDR_FRAME_DATA_ADDR = 0x0000_0000;
 | 
			
		||||
    public const UInt32 DDR_FRAME_DATA_MAX_ADDR = 0x8000_0000;
 | 
			
		||||
    public const UInt32 CAPTURE_RD_CTRL = BASE + 0x0;
 | 
			
		||||
    public const UInt32 CAPTURE_WR_CTRL = BASE + 0x1;
 | 
			
		||||
 | 
			
		||||
    public const UInt32 START_WR_ADDR0 = BASE + 0x2;
 | 
			
		||||
    public const UInt32 END_WR_ADDR0 = BASE + 0x3;
 | 
			
		||||
    public const UInt32 START_WR_ADDR1 = BASE + 0x4;
 | 
			
		||||
    public const UInt32 END_WR_ADDR1 = BASE + 0x5;
 | 
			
		||||
    public const UInt32 START_RD_ADDR0 = BASE + 0x6;
 | 
			
		||||
    public const UInt32 END_RD_ADDR0 = BASE + 0x7;
 | 
			
		||||
 | 
			
		||||
    public const UInt32 HDMI_NOT_READY = BASE + 0x8;
 | 
			
		||||
    public const UInt32 HDMI_HEIGHT_WIDTH = BASE + 0x9;
 | 
			
		||||
 | 
			
		||||
    public const UInt32 JPEG_HEIGHT_WIDTH = BASE + 0xA;
 | 
			
		||||
    public const UInt32 JPEG_ADD_NEED_FRAME_NUM = BASE + 0xB;
 | 
			
		||||
    public const UInt32 JPEG_FRAME_SAVE_NUM = BASE + 0xC;
 | 
			
		||||
    public const UInt32 JPEG_FIFO_FRAME_INFO = BASE + 0xD;
 | 
			
		||||
 | 
			
		||||
    public const UInt32 ADDR_HDMI_WD_START = 0x4000_0000;
 | 
			
		||||
    public const UInt32 ADDR_JPEG_START = 0x8000_0000;
 | 
			
		||||
    public const UInt32 ADDR_JPEG_END = 0xA000_0000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class JpegInfo
 | 
			
		||||
@@ -79,39 +93,248 @@ public class Jpeg
 | 
			
		||||
        this.timeout = timeout;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async ValueTask<Result<bool>> Init(bool enable = true)
 | 
			
		||||
    {
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await CheckHdmiIsReady();
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to check HDMI ready: {ret.Error}");
 | 
			
		||||
                return new(ret.Error);
 | 
			
		||||
            }
 | 
			
		||||
            if (!ret.Value)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error("HDMI not ready");
 | 
			
		||||
                return new(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        int width = -1, height = -1;
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await GetHdmiResolution();
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to get HDMI resolution: {ret.Error}");
 | 
			
		||||
                return new(ret.Error);
 | 
			
		||||
            }
 | 
			
		||||
            (width, height) = ret.Value;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await ConnectJpeg2Hdmi(width, height);
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to connect JPEG to HDMI: {ret.Error}");
 | 
			
		||||
                return new(ret.Error);
 | 
			
		||||
            }
 | 
			
		||||
            if (!ret.Value)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error("Failed to connect JPEG to HDMI");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (enable)
 | 
			
		||||
            return await SetEnable(true);
 | 
			
		||||
        else return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async ValueTask<bool> SetEnable(bool enable)
 | 
			
		||||
    {
 | 
			
		||||
        var ret = await UDPClientPool.WriteAddr(
 | 
			
		||||
            this.ep, this.taskID, JpegAddr.ENABLE, Convert.ToUInt32(enable), this.timeout);
 | 
			
		||||
        if (enable)
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await UDPClientPool.WriteAddrSeq(
 | 
			
		||||
                this.ep,
 | 
			
		||||
                this.taskID,
 | 
			
		||||
                [JpegAddr.CAPTURE_RD_CTRL, JpegAddr.CAPTURE_WR_CTRL],
 | 
			
		||||
                [0b11, 0b01],
 | 
			
		||||
                this.timeout
 | 
			
		||||
            );
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to set JPEG enable: {ret.Error}");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            return ret.Value;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await UDPClientPool.WriteAddrSeq(
 | 
			
		||||
                this.ep,
 | 
			
		||||
                this.taskID,
 | 
			
		||||
                [JpegAddr.CAPTURE_RD_CTRL, JpegAddr.CAPTURE_WR_CTRL],
 | 
			
		||||
                [0b00, 0b00],
 | 
			
		||||
                this.timeout
 | 
			
		||||
            );
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to set JPEG disable: {ret.Error}");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            return ret.Value;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async ValueTask<Result<bool>> CheckHdmiIsReady()
 | 
			
		||||
    {
 | 
			
		||||
        var ret = await UDPClientPool.ReadAddrWithWait(
 | 
			
		||||
            this.ep, this.taskID, JpegAddr.HDMI_NOT_READY, 0b01, 0b01, 100, this.timeout);
 | 
			
		||||
        if (!ret.IsSuccessful)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"Failed to set JPEG enable: {ret.Error}");
 | 
			
		||||
            return false;
 | 
			
		||||
            logger.Error($"Failed to check HDMI status: {ret.Error}");
 | 
			
		||||
            return new(ret.Error);
 | 
			
		||||
        }
 | 
			
		||||
        return ret.Value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async ValueTask<bool> SetSampleRate(uint rate)
 | 
			
		||||
    public async ValueTask<Result<(int, int)>> GetHdmiResolution()
 | 
			
		||||
    {
 | 
			
		||||
        var ret = await UDPClientPool.WriteAddr(
 | 
			
		||||
            this.ep, this.taskID, JpegAddr.FRAME_SAMPLE_RATE, rate, this.timeout);
 | 
			
		||||
        var ret = await UDPClientPool.ReadAddr(
 | 
			
		||||
            this.ep, this.taskID, JpegAddr.HDMI_HEIGHT_WIDTH, 0, this.timeout);
 | 
			
		||||
        if (!ret.IsSuccessful)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"Failed to set JPEG sample rate: {ret.Error}");
 | 
			
		||||
            return false;
 | 
			
		||||
            logger.Error($"Failed to get HDMI resolution: {ret.Error}");
 | 
			
		||||
            return new(ret.Error);
 | 
			
		||||
        }
 | 
			
		||||
        return ret.Value;
 | 
			
		||||
 | 
			
		||||
        var data = ret.Value.Options.Data;
 | 
			
		||||
        if (data == null || data.Length != 4)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"Invalid HDMI resolution data length: {data?.Length ?? 0}");
 | 
			
		||||
            return new(new Exception("Invalid HDMI resolution data length"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var width = data[0] | (data[1] << 8);
 | 
			
		||||
        var height = data[2] | (data[3] << 8);
 | 
			
		||||
        return new((width, height));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async ValueTask<bool> SetSampleRate(JpegSampleRate rate)
 | 
			
		||||
    public async ValueTask<Result<bool>> ConnectJpeg2Hdmi(int width, int height)
 | 
			
		||||
    {
 | 
			
		||||
        return await SetSampleRate((uint)rate);
 | 
			
		||||
        if (width <= 0 || height <= 0)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"Invalid HDMI resolution: {width}x{height}");
 | 
			
		||||
            return new(new ArgumentException("Invalid HDMI resolution"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var frameSize = (UInt32)(width * height / 4);
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await UDPClientPool.WriteAddr(
 | 
			
		||||
                this.ep, this.taskID, JpegAddr.START_WR_ADDR0, JpegAddr.ADDR_HDMI_WD_START, this.timeout);
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to set HDMI output start address: {ret.Error}");
 | 
			
		||||
                return new(ret.Error);
 | 
			
		||||
            }
 | 
			
		||||
            if (!ret.Value)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to set HDMI output start address");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await UDPClientPool.WriteAddr(
 | 
			
		||||
                this.ep, this.taskID, JpegAddr.END_WR_ADDR0,
 | 
			
		||||
                JpegAddr.ADDR_HDMI_WD_START + frameSize, this.timeout);
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to set HDMI output end address: {ret.Error}");
 | 
			
		||||
                return new(ret.Error);
 | 
			
		||||
            }
 | 
			
		||||
            if (!ret.Value)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to set HDMI output address");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await UDPClientPool.WriteAddr(
 | 
			
		||||
                this.ep, this.taskID, JpegAddr.START_RD_ADDR0, JpegAddr.ADDR_HDMI_WD_START, this.timeout);
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to set jpeg input start address: {ret.Error}");
 | 
			
		||||
                return new(ret.Error);
 | 
			
		||||
            }
 | 
			
		||||
            if (!ret.Value)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to set jpeg input address");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await UDPClientPool.WriteAddr(
 | 
			
		||||
                this.ep, this.taskID, JpegAddr.END_RD_ADDR0,
 | 
			
		||||
                JpegAddr.ADDR_HDMI_WD_START + frameSize, this.timeout);
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to set jpeg input end address: {ret.Error}");
 | 
			
		||||
                return new(ret.Error);
 | 
			
		||||
            }
 | 
			
		||||
            if (!ret.Value)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to set jpeg input end address");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await UDPClientPool.WriteAddr(
 | 
			
		||||
                this.ep, this.taskID, JpegAddr.START_WR_ADDR1, JpegAddr.ADDR_JPEG_START, this.timeout);
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to set jpeg output start address: {ret.Error}");
 | 
			
		||||
                return new(ret.Error);
 | 
			
		||||
            }
 | 
			
		||||
            if (!ret.Value)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to set jpeg output start address");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await UDPClientPool.WriteAddr(
 | 
			
		||||
                this.ep, this.taskID, JpegAddr.END_WR_ADDR1, JpegAddr.ADDR_JPEG_END, this.timeout);
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to set jpeg output end address: {ret.Error}");
 | 
			
		||||
                return new(ret.Error);
 | 
			
		||||
            }
 | 
			
		||||
            if (!ret.Value)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to set jpeg output end address");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // public async ValueTask<bool> SetSampleRate(uint rate)
 | 
			
		||||
    // {
 | 
			
		||||
    //     var ret = await UDPClientPool.WriteAddr(
 | 
			
		||||
    //         this.ep, this.taskID, JpegAddr.FRAME_SAMPLE_RATE, rate, this.timeout);
 | 
			
		||||
    //     if (!ret.IsSuccessful)
 | 
			
		||||
    //     {
 | 
			
		||||
    //         logger.Error($"Failed to set JPEG sample rate: {ret.Error}");
 | 
			
		||||
    //         return false;
 | 
			
		||||
    //     }
 | 
			
		||||
    //     return ret.Value;
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    // public async ValueTask<bool> SetSampleRate(JpegSampleRate rate)
 | 
			
		||||
    // {
 | 
			
		||||
    //     return await SetSampleRate((uint)rate);
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    public async ValueTask<uint> GetFrameNumber()
 | 
			
		||||
    {
 | 
			
		||||
        var ret = await UDPClientPool.ReadAddrByte(
 | 
			
		||||
            this.ep, this.taskID, JpegAddr.FRAME_NUM, this.timeout);
 | 
			
		||||
            this.ep, this.taskID, JpegAddr.JPEG_FRAME_SAVE_NUM, this.timeout);
 | 
			
		||||
        if (!ret.IsSuccessful)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"Failed to get JPEG frame number: {ret.Error}");
 | 
			
		||||
@@ -122,7 +345,7 @@ public class Jpeg
 | 
			
		||||
 | 
			
		||||
    public async ValueTask<Optional<List<JpegInfo>>> GetFrameInfo(int num)
 | 
			
		||||
    {
 | 
			
		||||
        var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, JpegAddr.FRAME_INFO, num, this.timeout);
 | 
			
		||||
        var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, JpegAddr.JPEG_FIFO_FRAME_INFO, num, this.timeout);
 | 
			
		||||
        if (!ret.IsSuccessful)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"Failed to get JPEG frame info: {ret.Error}");
 | 
			
		||||
@@ -150,10 +373,10 @@ public class Jpeg
 | 
			
		||||
        return new(infos);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async ValueTask<bool> UpdatePointer(uint cnt)
 | 
			
		||||
    public async ValueTask<bool> AddFrameNum2Process(uint cnt)
 | 
			
		||||
    {
 | 
			
		||||
        var ret = await UDPClientPool.WriteAddr(
 | 
			
		||||
            this.ep, this.taskID, JpegAddr.FRAME_DATA_MAX_POINTER, cnt, this.timeout);
 | 
			
		||||
            this.ep, this.taskID, JpegAddr.JPEG_ADD_NEED_FRAME_NUM, cnt, this.timeout);
 | 
			
		||||
        if (!ret.IsSuccessful)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"Failed to update pointer: {ret.Error}");
 | 
			
		||||
@@ -171,13 +394,16 @@ public class Jpeg
 | 
			
		||||
        }
 | 
			
		||||
        MsgBus.UDPServer.ClearUDPData(this.ep.Address.ToString(), this.ep.Port);
 | 
			
		||||
 | 
			
		||||
        var firstReadLength = (int)(Math.Min(length, JpegAddr.DDR_FRAME_DATA_MAX_ADDR - offset));
 | 
			
		||||
        var firstReadLength = (int)(Math.Min(
 | 
			
		||||
            length,
 | 
			
		||||
            JpegAddr.ADDR_JPEG_END - JpegAddr.ADDR_JPEG_START - offset
 | 
			
		||||
        ));
 | 
			
		||||
        var secondReadLength = (int)(length - firstReadLength);
 | 
			
		||||
        var dataBytes = new byte[length];
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await UDPClientPool.ReadAddr4Bytes(
 | 
			
		||||
                this.ep, this.taskID, JpegAddr.DDR_FRAME_DATA_ADDR + offset, firstReadLength, this.timeout);
 | 
			
		||||
                this.ep, this.taskID, JpegAddr.ADDR_JPEG_START + offset, firstReadLength, this.timeout);
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to get JPEG frame data: {ret.Error}");
 | 
			
		||||
@@ -194,7 +420,7 @@ public class Jpeg
 | 
			
		||||
        if (secondReadLength > 0)
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await UDPClientPool.ReadAddr4Bytes(
 | 
			
		||||
                this.ep, this.taskID, JpegAddr.DDR_FRAME_DATA_ADDR, secondReadLength, this.timeout);
 | 
			
		||||
                this.ep, this.taskID, JpegAddr.ADDR_JPEG_START, secondReadLength, this.timeout);
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to get JPEG frame data: {ret.Error}");
 | 
			
		||||
@@ -239,7 +465,7 @@ public class Jpeg
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await UpdatePointer((uint)sizes.Length);
 | 
			
		||||
            var ret = await AddFrameNum2Process((uint)sizes.Length);
 | 
			
		||||
            if (!ret) logger.Error($"Failed to update pointer");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using Peripherals.HdmiInClient;
 | 
			
		||||
using Peripherals.JpegClient;
 | 
			
		||||
 | 
			
		||||
namespace server.Services;
 | 
			
		||||
 | 
			
		||||
@@ -12,18 +13,34 @@ public class HdmiVideoStreamEndpoint
 | 
			
		||||
    public string SnapshotUrl { get; set; } = "";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class HdmiVideoStreamClient
 | 
			
		||||
{
 | 
			
		||||
    public required HdmiIn HdmiInClient { get; set; }
 | 
			
		||||
 | 
			
		||||
    public required Jpeg JpegClient { get; set; }
 | 
			
		||||
 | 
			
		||||
    public required CancellationTokenSource CTS { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
{
 | 
			
		||||
    private readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
			
		||||
 | 
			
		||||
    private readonly IServiceProvider _serviceProvider;
 | 
			
		||||
 | 
			
		||||
    private HttpListener? _httpListener;
 | 
			
		||||
    private readonly int _serverPort = 4322;
 | 
			
		||||
    private readonly ConcurrentDictionary<string, HdmiIn> _hdmiInDict = new();
 | 
			
		||||
    private readonly ConcurrentDictionary<string, CancellationTokenSource> _hdmiInCtsDict = new();
 | 
			
		||||
    private readonly ConcurrentDictionary<string, HdmiVideoStreamClient> _clientDict = new();
 | 
			
		||||
 | 
			
		||||
    public HttpHdmiVideoStreamService(IServiceProvider serviceProvider)
 | 
			
		||||
    {
 | 
			
		||||
        _serviceProvider = serviceProvider;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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}");
 | 
			
		||||
 | 
			
		||||
@@ -67,7 +84,7 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
 | 
			
		||||
        // 禁用所有活跃的HDMI传输
 | 
			
		||||
        var disableTasks = new List<Task>();
 | 
			
		||||
        foreach (var hdmiKey in _hdmiInDict.Keys)
 | 
			
		||||
        foreach (var hdmiKey in _clientDict.Keys)
 | 
			
		||||
        {
 | 
			
		||||
            disableTasks.Add(DisableHdmiTransmissionAsync(hdmiKey));
 | 
			
		||||
        }
 | 
			
		||||
@@ -76,8 +93,7 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
        await Task.WhenAll(disableTasks);
 | 
			
		||||
 | 
			
		||||
        // 清空字典
 | 
			
		||||
        _hdmiInDict.Clear();
 | 
			
		||||
        _hdmiInCtsDict.Clear();
 | 
			
		||||
        _clientDict.Clear();
 | 
			
		||||
 | 
			
		||||
        _httpListener?.Close(); // 立即关闭监听器,唤醒阻塞
 | 
			
		||||
        await base.StopAsync(cancellationToken);
 | 
			
		||||
@@ -87,11 +103,10 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var cts = _hdmiInCtsDict[key];
 | 
			
		||||
            cts.Cancel();
 | 
			
		||||
            var client = _clientDict[key];
 | 
			
		||||
            client.CTS.Cancel();
 | 
			
		||||
 | 
			
		||||
            var hdmiIn = _hdmiInDict[key];
 | 
			
		||||
            var disableResult = await hdmiIn.EnableTrans(false);
 | 
			
		||||
            var disableResult = await client.HdmiInClient.EnableTrans(false);
 | 
			
		||||
            if (disableResult.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Info("Successfully disabled HDMI transmission");
 | 
			
		||||
@@ -107,40 +122,14 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 获取/创建 HdmiIn 实例
 | 
			
		||||
    private async Task<HdmiIn?> GetOrCreateHdmiInAsync(string boardId)
 | 
			
		||||
    private async Task<HdmiVideoStreamClient?> GetOrCreateClientAsync(string boardId)
 | 
			
		||||
    {
 | 
			
		||||
        if (_hdmiInDict.TryGetValue(boardId, out var hdmiIn))
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var enableResult = await hdmiIn.EnableTrans(true);
 | 
			
		||||
                if (!enableResult.IsSuccessful)
 | 
			
		||||
                {
 | 
			
		||||
                    logger.Error($"Failed to enable HDMI transmission for board {boardId}: {enableResult.Error}");
 | 
			
		||||
                    return null;
 | 
			
		||||
                }
 | 
			
		||||
                logger.Info($"Successfully enabled HDMI transmission for board {boardId}");
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error(ex, $"Exception occurred while enabling HDMI transmission for board {boardId}");
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
        if (_clientDict.TryGetValue(boardId, out var client)) return client;
 | 
			
		||||
 | 
			
		||||
            _hdmiInDict[boardId] = hdmiIn;
 | 
			
		||||
            _hdmiInCtsDict[boardId] = new CancellationTokenSource();
 | 
			
		||||
            return hdmiIn;
 | 
			
		||||
        }
 | 
			
		||||
        using var scope = _serviceProvider.CreateScope();
 | 
			
		||||
        var userManager = scope.ServiceProvider.GetRequiredService<Database.UserManager>();
 | 
			
		||||
 | 
			
		||||
        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}");
 | 
			
		||||
@@ -149,18 +138,31 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
 | 
			
		||||
        var board = boardRet.Value.Value;
 | 
			
		||||
 | 
			
		||||
        hdmiIn = new HdmiIn(board.IpAddr, board.Port, 0); // taskID 可根据实际需求调整
 | 
			
		||||
        client = new HdmiVideoStreamClient()
 | 
			
		||||
        {
 | 
			
		||||
            HdmiInClient = new HdmiIn(board.IpAddr, board.Port, 1),
 | 
			
		||||
            JpegClient = new Jpeg(board.IpAddr, board.Port, 1),
 | 
			
		||||
            CTS = new CancellationTokenSource()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // 启用HDMI传输
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var enableResult = await hdmiIn.EnableTrans(true);
 | 
			
		||||
            if (!enableResult.IsSuccessful)
 | 
			
		||||
            var hdmiEnableRet = await client.HdmiInClient.EnableTrans(true);
 | 
			
		||||
            if (!hdmiEnableRet.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to enable HDMI transmission for board {boardId}: {enableResult.Error}");
 | 
			
		||||
                logger.Error($"Failed to enable HDMI transmission for board {boardId}: {hdmiEnableRet.Error}");
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
            logger.Info($"Successfully enabled HDMI transmission for board {boardId}");
 | 
			
		||||
 | 
			
		||||
            var jpegEnableRet = await client.JpegClient.Init(true);
 | 
			
		||||
            if (!jpegEnableRet.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to enable JPEG transmission for board {boardId}: {jpegEnableRet.Error}");
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
            logger.Info($"Successfully enabled JPEG transmission for board {boardId}");
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
@@ -168,9 +170,8 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _hdmiInDict[boardId] = hdmiIn;
 | 
			
		||||
        _hdmiInCtsDict[boardId] = new CancellationTokenSource();
 | 
			
		||||
        return hdmiIn;
 | 
			
		||||
        _clientDict[boardId] = client;
 | 
			
		||||
        return client;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task HandleRequestAsync(HttpListenerContext context, CancellationToken cancellationToken)
 | 
			
		||||
@@ -183,14 +184,14 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var hdmiIn = await GetOrCreateHdmiInAsync(boardId);
 | 
			
		||||
        if (hdmiIn == null)
 | 
			
		||||
        var client = await GetOrCreateClientAsync(boardId);
 | 
			
		||||
        if (client == null)
 | 
			
		||||
        {
 | 
			
		||||
            await SendErrorAsync(context.Response, "Invalid boardId or board not available");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var hdmiInToken = _hdmiInCtsDict[boardId].Token;
 | 
			
		||||
        var hdmiInToken = _clientDict[boardId].CTS.Token;
 | 
			
		||||
        if (hdmiInToken == null)
 | 
			
		||||
        {
 | 
			
		||||
            await SendErrorAsync(context.Response, "HDMI input is not available");
 | 
			
		||||
@@ -199,11 +200,11 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
 | 
			
		||||
        if (path == "/snapshot")
 | 
			
		||||
        {
 | 
			
		||||
            await HandleSnapshotRequestAsync(context.Response, hdmiIn, hdmiInToken);
 | 
			
		||||
            await HandleSnapshotRequestAsync(context.Response, client, hdmiInToken);
 | 
			
		||||
        }
 | 
			
		||||
        else if (path == "/mjpeg")
 | 
			
		||||
        {
 | 
			
		||||
            await HandleMjpegStreamAsync(context.Response, hdmiIn, hdmiInToken);
 | 
			
		||||
            await HandleMjpegStreamAsync(context.Response, client, hdmiInToken);
 | 
			
		||||
        }
 | 
			
		||||
        else if (path == "/video")
 | 
			
		||||
        {
 | 
			
		||||
@@ -215,14 +216,15 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task HandleSnapshotRequestAsync(HttpListenerResponse response, HdmiIn hdmiIn, CancellationToken cancellationToken)
 | 
			
		||||
    private async Task HandleSnapshotRequestAsync(
 | 
			
		||||
        HttpListenerResponse response, HdmiVideoStreamClient client, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            logger.Debug("处理HDMI快照请求");
 | 
			
		||||
 | 
			
		||||
            // 从HDMI读取RGB565数据
 | 
			
		||||
            var frameResult = await hdmiIn.ReadFrame();
 | 
			
		||||
            var frameResult = await client.HdmiInClient.ReadFrame();
 | 
			
		||||
            if (!frameResult.IsSuccessful || frameResult.Value == null)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error("HDMI快照获取失败");
 | 
			
		||||
@@ -256,7 +258,8 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task HandleMjpegStreamAsync(HttpListenerResponse response, HdmiIn hdmiIn, CancellationToken cancellationToken)
 | 
			
		||||
    private async Task HandleMjpegStreamAsync(
 | 
			
		||||
        HttpListenerResponse response, HdmiVideoStreamClient client, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
@@ -276,7 +279,7 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
                {
 | 
			
		||||
                    var frameStartTime = DateTime.UtcNow;
 | 
			
		||||
 | 
			
		||||
                    var ret = await hdmiIn.GetMJpegFrame();
 | 
			
		||||
                    var ret = await client.HdmiInClient.GetMJpegFrame();
 | 
			
		||||
                    if (ret == null) continue;
 | 
			
		||||
                    var frame = ret.Value;
 | 
			
		||||
 | 
			
		||||
@@ -311,7 +314,7 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                // 停止传输时禁用HDMI传输
 | 
			
		||||
                await hdmiIn.EnableTrans(false);
 | 
			
		||||
                await client.HdmiInClient.EnableTrans(false);
 | 
			
		||||
                logger.Info("已禁用HDMI传输");
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
@@ -366,8 +369,10 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
    /// <returns>返回所有可用的HDMI视频流终端点列表</returns>
 | 
			
		||||
    public List<HdmiVideoStreamEndpoint>? GetAllVideoEndpoints()
 | 
			
		||||
    {
 | 
			
		||||
        var db = new Database.AppDataConnection();
 | 
			
		||||
        var boards = db?.GetAllBoard();
 | 
			
		||||
        using var scope = _serviceProvider.CreateScope();
 | 
			
		||||
        var userManager = scope.ServiceProvider.GetRequiredService<Database.UserManager>();
 | 
			
		||||
 | 
			
		||||
        var boards = userManager.GetAllBoard();
 | 
			
		||||
        if (boards == null)
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
@@ -377,9 +382,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 +400,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}"
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ namespace server.Services;
 | 
			
		||||
public class VideoStreamClient
 | 
			
		||||
{
 | 
			
		||||
    public string? ClientId { get; set; } = string.Empty;
 | 
			
		||||
    public bool IsEnabled { get; set; } = true;
 | 
			
		||||
    public int FrameWidth { get; set; }
 | 
			
		||||
    public int FrameHeight { get; set; }
 | 
			
		||||
    public int FrameRate { get; set; }
 | 
			
		||||
@@ -35,28 +36,35 @@ public class VideoStreamClient
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 表示摄像头连接状态信息
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class VideoEndpoint
 | 
			
		||||
public class VideoStreamEndpoint
 | 
			
		||||
{
 | 
			
		||||
    public string BoardId { get; set; } = "";
 | 
			
		||||
    public string MjpegUrl { get; set; } = "";
 | 
			
		||||
    public string VideoUrl { get; set; } = "";
 | 
			
		||||
    public string SnapshotUrl { get; set; } = "";
 | 
			
		||||
    public required string BoardId { get; set; } = "";
 | 
			
		||||
    public required string MjpegUrl { get; set; } = "";
 | 
			
		||||
    public required string VideoUrl { get; set; } = "";
 | 
			
		||||
    public required string SnapshotUrl { get; set; } = "";
 | 
			
		||||
    public required string HtmlUrl { get; set; } = "";
 | 
			
		||||
    public required string UsbCameraUrl { get; set; } = "";
 | 
			
		||||
 | 
			
		||||
    public required bool IsEnabled { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 视频流的帧率(FPS)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public int FrameRate { get; set; }
 | 
			
		||||
    public required int FrameRate { get; set; }
 | 
			
		||||
 | 
			
		||||
    public int FrameWidth { get; set; }
 | 
			
		||||
    public int FrameHeight { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 视频分辨率(如 640x480)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string Resolution { get; set; } = string.Empty;
 | 
			
		||||
    public string Resolution => $"{FrameWidth}x{FrameHeight}";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 表示视频流服务的运行状态
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class ServiceStatus
 | 
			
		||||
public class VideoStreamServiceStatus
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 服务是否正在运行
 | 
			
		||||
@@ -71,7 +79,7 @@ public class ServiceStatus
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当前连接的客户端端点列表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public List<VideoEndpoint> ClientEndpoints { get; set; } = new();
 | 
			
		||||
    public List<VideoStreamEndpoint> ClientEndpoints { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当前连接的客户端数量
 | 
			
		||||
@@ -87,6 +95,8 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
{
 | 
			
		||||
    private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
			
		||||
 | 
			
		||||
    private readonly IServiceProvider _serviceProvider;
 | 
			
		||||
 | 
			
		||||
    private HttpListener? _httpListener;
 | 
			
		||||
    private readonly int _serverPort = 4321;
 | 
			
		||||
 | 
			
		||||
@@ -99,13 +109,60 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
    private readonly object _usbCameraLock = new object();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    public HttpVideoStreamService(IServiceProvider serviceProvider)
 | 
			
		||||
    {
 | 
			
		||||
        _serviceProvider = serviceProvider;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Optional<VideoStreamClient> TryGetClient(string boardId)
 | 
			
		||||
    {
 | 
			
		||||
        if (_clientDict.TryGetValue(boardId, out var client))
 | 
			
		||||
        {
 | 
			
		||||
            return client;
 | 
			
		||||
        }
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<VideoStreamClient?> GetOrCreateClientAsync(string boardId, int initWidth, int initHeight)
 | 
			
		||||
    {
 | 
			
		||||
        if (_clientDict.TryGetValue(boardId, out var client))
 | 
			
		||||
        {
 | 
			
		||||
            // 可在此处做分辨率/Camera等配置更新
 | 
			
		||||
            return client;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        using var scope = _serviceProvider.CreateScope();
 | 
			
		||||
        var userManager = scope.ServiceProvider.GetRequiredService<Database.UserManager>();
 | 
			
		||||
 | 
			
		||||
        var boardRet = userManager.GetBoardByID(Guid.Parse(boardId));
 | 
			
		||||
        if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"Failed to get board with ID {boardId}");
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var board = boardRet.Value.Value;
 | 
			
		||||
 | 
			
		||||
        var camera = new Peripherals.CameraClient.Camera(board.IpAddr, board.Port);
 | 
			
		||||
        var ret = await camera.Init();
 | 
			
		||||
        if (!ret.IsSuccessful || !ret.Value)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error("Camera Init Failed!");
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        client = new VideoStreamClient(boardId, initWidth, initHeight, camera);
 | 
			
		||||
        _clientDict[boardId] = client;
 | 
			
		||||
        return client;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 初始化 HttpVideoStreamService
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    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}");
 | 
			
		||||
 | 
			
		||||
@@ -130,53 +187,6 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
        await base.StopAsync(cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Optional<VideoStreamClient> TryGetClient(string boardId)
 | 
			
		||||
    {
 | 
			
		||||
        if (_clientDict.TryGetValue(boardId, out var client))
 | 
			
		||||
        {
 | 
			
		||||
            return client;
 | 
			
		||||
        }
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<VideoStreamClient?> GetOrCreateClientAsync(string boardId, int initWidth, int initHeight)
 | 
			
		||||
    {
 | 
			
		||||
        if (_clientDict.TryGetValue(boardId, out var client))
 | 
			
		||||
        {
 | 
			
		||||
            // 可在此处做分辨率/Camera等配置更新
 | 
			
		||||
            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));
 | 
			
		||||
        if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"Failed to get board with ID {boardId}");
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var board = boardRet.Value.Value;
 | 
			
		||||
 | 
			
		||||
        var camera = new Peripherals.CameraClient.Camera(board.IpAddr, board.Port);
 | 
			
		||||
        var ret = await camera.Init();
 | 
			
		||||
        if (!ret.IsSuccessful || !ret.Value)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error("Camera Init Failed!");
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        client = new VideoStreamClient(boardId, initWidth, initHeight, camera);
 | 
			
		||||
        _clientDict[boardId] = client;
 | 
			
		||||
        return client;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 执行 HTTP 视频流服务
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -254,6 +264,11 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
                // 单帧图像请求
 | 
			
		||||
                await HandleSnapshotRequestAsync(context.Response, client, cancellationToken);
 | 
			
		||||
            }
 | 
			
		||||
            else if (path == "/html")
 | 
			
		||||
            {
 | 
			
		||||
                // HTML页面请求
 | 
			
		||||
                await SendIndexHtmlPageAsync(context.Response);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                // 默认返回简单的HTML页面,提供链接到视频页面
 | 
			
		||||
@@ -668,42 +683,12 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public VideoEndpoint GetVideoEndpoint(string boardId)
 | 
			
		||||
    {
 | 
			
		||||
        var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
 | 
			
		||||
 | 
			
		||||
        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}",
 | 
			
		||||
            Resolution = $"{client.FrameWidth}x{client.FrameHeight}",
 | 
			
		||||
            FrameRate = client.FrameRate
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public List<VideoEndpoint> GetAllVideoEndpoints()
 | 
			
		||||
    {
 | 
			
		||||
        var endpoints = new List<VideoEndpoint>();
 | 
			
		||||
 | 
			
		||||
        foreach (var boardId in _clientDict.Keys)
 | 
			
		||||
            endpoints.Add(GetVideoEndpoint(boardId));
 | 
			
		||||
 | 
			
		||||
        return endpoints;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ServiceStatus GetServiceStatus()
 | 
			
		||||
    {
 | 
			
		||||
        return new ServiceStatus
 | 
			
		||||
        {
 | 
			
		||||
            IsRunning = true,
 | 
			
		||||
            ServerPort = _serverPort,
 | 
			
		||||
            ClientEndpoints = GetAllVideoEndpoints()
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task DisableHdmiTransmissionAsync(string boardId)
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 配置摄像头连接参数
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="boardId">板卡ID</param>
 | 
			
		||||
    /// <returns>配置是否成功</returns>
 | 
			
		||||
    public async Task<bool> ConfigureCameraAsync(string boardId)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
@@ -711,8 +696,67 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
 | 
			
		||||
            using (await client.Lock.AcquireWriteLockAsync())
 | 
			
		||||
            {
 | 
			
		||||
                var ret = await client.Camera.Init();
 | 
			
		||||
                if (!ret.IsSuccessful)
 | 
			
		||||
                {
 | 
			
		||||
                    logger.Error(ret.Error);
 | 
			
		||||
                    throw ret.Error;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!ret.Value)
 | 
			
		||||
                {
 | 
			
		||||
                    logger.Error($"Camera Init Failed!");
 | 
			
		||||
                    throw new Exception($"Camera Init Failed!");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            using (await client.Lock.AcquireWriteLockAsync())
 | 
			
		||||
            {
 | 
			
		||||
                var ret = await client.Camera.ChangeResolution(client.FrameWidth, client.FrameHeight);
 | 
			
		||||
                if (!ret.IsSuccessful)
 | 
			
		||||
                {
 | 
			
		||||
                    logger.Error(ret.Error);
 | 
			
		||||
                    throw ret.Error;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!ret.Value)
 | 
			
		||||
                {
 | 
			
		||||
                    logger.Error($"Camera Resolution Change Failed!");
 | 
			
		||||
                    throw new Exception($"Camera Resolution Change Failed!");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "配置摄像头连接时发生错误");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task SetVideoStreamEnableAsync(string boardId, bool enable)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
 | 
			
		||||
 | 
			
		||||
            if (client.IsEnabled == enable)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            using (await client.Lock.AcquireWriteLockAsync())
 | 
			
		||||
            {
 | 
			
		||||
                if (enable)
 | 
			
		||||
                {
 | 
			
		||||
                    client.CTS = new CancellationTokenSource();
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    client.CTS.Cancel();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var camera = client.Camera;
 | 
			
		||||
                var disableResult = await camera.EnableHardwareTrans(false);
 | 
			
		||||
                var disableResult = await camera.EnableHardwareTrans(enable);
 | 
			
		||||
                if (disableResult.IsSuccessful && disableResult.Value)
 | 
			
		||||
                    logger.Info($"Successfully disabled camera {boardId} hardware transmission");
 | 
			
		||||
                else
 | 
			
		||||
@@ -743,4 +787,41 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public VideoStreamEndpoint GetVideoEndpoint(string boardId)
 | 
			
		||||
    {
 | 
			
		||||
        var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
 | 
			
		||||
 | 
			
		||||
        return new VideoStreamEndpoint
 | 
			
		||||
        {
 | 
			
		||||
            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}",
 | 
			
		||||
            UsbCameraUrl = $"http://{Global.LocalHost}:{_serverPort}/usbCamera?boardId={boardId}",
 | 
			
		||||
            HtmlUrl = $"http://{Global.LocalHost}:{_serverPort}/html?boardId={boardId}",
 | 
			
		||||
            IsEnabled = client.IsEnabled,
 | 
			
		||||
            FrameRate = client.FrameRate
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public List<VideoStreamEndpoint> GetAllVideoEndpoints()
 | 
			
		||||
    {
 | 
			
		||||
        var endpoints = new List<VideoStreamEndpoint>();
 | 
			
		||||
 | 
			
		||||
        foreach (var boardId in _clientDict.Keys)
 | 
			
		||||
            endpoints.Add(GetVideoEndpoint(boardId));
 | 
			
		||||
 | 
			
		||||
        return endpoints;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public VideoStreamServiceStatus GetServiceStatus()
 | 
			
		||||
    {
 | 
			
		||||
        return new VideoStreamServiceStatus
 | 
			
		||||
        {
 | 
			
		||||
            IsRunning = true,
 | 
			
		||||
            ServerPort = _serverPort,
 | 
			
		||||
            ClientEndpoints = GetAllVideoEndpoints()
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -331,7 +331,9 @@ public class UDPClientPool
 | 
			
		||||
    /// <param name="timeout">超时时间(毫秒)</param>
 | 
			
		||||
    /// <returns>校验结果,true表示在超时前数据匹配期望值</returns>
 | 
			
		||||
    public static async ValueTask<Result<bool>> ReadAddrWithWait(
 | 
			
		||||
            IPEndPoint endPoint, int taskID, uint devAddr, UInt32 result, UInt32 resultMask, int waittime = 100, int timeout = 1000)
 | 
			
		||||
            IPEndPoint endPoint, int taskID, uint devAddr,
 | 
			
		||||
            UInt32 result, UInt32 resultMask,
 | 
			
		||||
            int waittime = 100, int timeout = 1000)
 | 
			
		||||
    {
 | 
			
		||||
        var address = endPoint.Address.ToString();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user