add: 添加实验列表界面,实验增删完全依赖数据库实现
This commit is contained in:
@@ -168,6 +168,17 @@ try
|
||||
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "log")),
|
||||
RequestPath = "/log"
|
||||
});
|
||||
|
||||
// Exam Files (实验静态资源)
|
||||
if (Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), "exam")))
|
||||
{
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "exam")),
|
||||
RequestPath = "/exam"
|
||||
});
|
||||
}
|
||||
|
||||
app.MapFallbackToFile("index.html");
|
||||
}
|
||||
|
||||
@@ -194,19 +205,6 @@ try
|
||||
// Setup Program
|
||||
MsgBus.Init();
|
||||
|
||||
// 扫描并更新实验数据库
|
||||
try
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
var examFolderPath = Path.Combine(Directory.GetCurrentDirectory(), "exam");
|
||||
var updateCount = db.ScanAndUpdateExams(examFolderPath);
|
||||
logger.Info($"实验数据库扫描完成,更新了 {updateCount} 个实验");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"扫描实验文件夹时出错: {ex.Message}");
|
||||
}
|
||||
|
||||
// Generate API Client
|
||||
app.MapGet("GetAPIClientCode", async (HttpContext context) =>
|
||||
{
|
||||
|
@@ -1,37 +0,0 @@
|
||||
# 实验001:基础逻辑门电路
|
||||
|
||||
## 实验目的
|
||||
|
||||
本实验旨在帮助学生理解基础逻辑门的工作原理,包括与门、或门、非门等基本逻辑运算。
|
||||
|
||||
## 实验内容
|
||||
|
||||
### 1. 与门(AND Gate)
|
||||
与门是一个基本的逻辑门,当所有输入都为高电平(1)时,输出才为高电平(1)。
|
||||
|
||||
### 2. 或门(OR Gate)
|
||||
或门是另一个基本的逻辑门,当任意一个输入为高电平(1)时,输出就为高电平(1)。
|
||||
|
||||
### 3. 非门(NOT Gate)
|
||||
非门是一个反相器,输入为高电平时输出为低电平,反之亦然。
|
||||
|
||||
## 实验步骤
|
||||
|
||||
1. 打开 FPGA 开发环境
|
||||
2. 创建新的项目文件
|
||||
3. 编写 Verilog 代码实现各种逻辑门
|
||||
4. 进行仿真验证
|
||||
5. 下载到 FPGA 板进行硬件验证
|
||||
|
||||
## 预期结果
|
||||
|
||||
通过本实验,学生应该能够:
|
||||
- 理解基本逻辑门的真值表
|
||||
- 掌握 Verilog 代码的基本语法
|
||||
- 学会使用 FPGA 开发工具进行仿真
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 确保输入信号的电平正确
|
||||
- 注意时序的约束
|
||||
- 验证结果时要仔细对比真值表
|
@@ -1,35 +0,0 @@
|
||||
# 实验002:组合逻辑电路设计
|
||||
|
||||
## 实验目的
|
||||
|
||||
本实验旨在让学生学习如何设计和实现复杂的组合逻辑电路,掌握多个逻辑门的组合使用。
|
||||
|
||||
## 实验内容
|
||||
|
||||
### 1. 半加器设计
|
||||
设计一个半加器电路,实现两个一位二进制数的加法运算。
|
||||
|
||||
### 2. 全加器设计
|
||||
在半加器的基础上,设计全加器电路,考虑进位输入。
|
||||
|
||||
### 3. 编码器和译码器
|
||||
实现简单的编码器和译码器电路。
|
||||
|
||||
## 实验要求
|
||||
|
||||
1. 使用 Verilog HDL 编写代码
|
||||
2. 绘制逻辑电路图
|
||||
3. 编写测试用例验证功能
|
||||
4. 分析电路的延时特性
|
||||
|
||||
## 评估标准
|
||||
|
||||
- 电路功能正确性 (40%)
|
||||
- 代码质量和规范性 (30%)
|
||||
- 测试覆盖率 (20%)
|
||||
- 实验报告 (10%)
|
||||
|
||||
## 参考资料
|
||||
|
||||
- 数字逻辑设计教材第3-4章
|
||||
- Verilog HDL 语法参考手册
|
@@ -25,9 +25,14 @@ public class ExamController : ControllerBase
|
||||
public required string ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验文档内容(Markdown格式)
|
||||
/// 实验名称
|
||||
/// </summary>
|
||||
public required string DocContent { get; set; }
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验描述
|
||||
/// </summary>
|
||||
public required string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验创建时间
|
||||
@@ -38,6 +43,21 @@ public class ExamController : ControllerBase
|
||||
/// 实验最后更新时间
|
||||
/// </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>
|
||||
@@ -50,6 +70,11 @@ public class ExamController : ControllerBase
|
||||
/// </summary>
|
||||
public required string ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验名称
|
||||
/// </summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验创建时间
|
||||
/// </summary>
|
||||
@@ -61,25 +86,71 @@ public class ExamController : ControllerBase
|
||||
public DateTime UpdatedTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验标题(从文档内容中提取)
|
||||
/// 实验标签
|
||||
/// </summary>
|
||||
public string Title { get; set; } = "";
|
||||
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 ScanResult
|
||||
public class ResourceInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 结果消息
|
||||
/// 资源ID
|
||||
/// </summary>
|
||||
public required string Message { get; set; }
|
||||
public int ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新的实验数量
|
||||
/// 资源名称
|
||||
/// </summary>
|
||||
public int UpdateCount { get; set; }
|
||||
public required string Name { get; set; }
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -102,9 +173,12 @@ public class ExamController : ControllerBase
|
||||
var examSummaries = exams.Select(exam => new ExamSummary
|
||||
{
|
||||
ID = exam.ID,
|
||||
Name = exam.Name,
|
||||
CreatedTime = exam.CreatedTime,
|
||||
UpdatedTime = exam.UpdatedTime,
|
||||
Title = ExtractTitleFromMarkdown(exam.DocContent)
|
||||
Tags = exam.GetTagsList(),
|
||||
Difficulty = exam.Difficulty,
|
||||
IsVisibleToUsers = exam.IsVisibleToUsers
|
||||
}).ToArray();
|
||||
|
||||
logger.Info($"成功获取实验列表,共 {examSummaries.Length} 个实验");
|
||||
@@ -156,9 +230,13 @@ public class ExamController : ControllerBase
|
||||
var examInfo = new ExamInfo
|
||||
{
|
||||
ID = exam.ID,
|
||||
DocContent = exam.DocContent,
|
||||
Name = exam.Name,
|
||||
Description = exam.Description,
|
||||
CreatedTime = exam.CreatedTime,
|
||||
UpdatedTime = exam.UpdatedTime
|
||||
UpdatedTime = exam.UpdatedTime,
|
||||
Tags = exam.GetTagsList(),
|
||||
Difficulty = exam.Difficulty,
|
||||
IsVisibleToUsers = exam.IsVisibleToUsers
|
||||
};
|
||||
|
||||
logger.Info($"成功获取实验信息: {examId}");
|
||||
@@ -172,60 +250,205 @@ public class ExamController : ControllerBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重新扫描实验文件夹并更新数据库
|
||||
/// 创建新实验
|
||||
/// </summary>
|
||||
/// <returns>更新结果</returns>
|
||||
/// <param name="request">创建实验请求</param>
|
||||
/// <returns>创建结果</returns>
|
||||
[Authorize("Admin")]
|
||||
[HttpPost("scan")]
|
||||
[HttpPost]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(ScanResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ExamInfo), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult ScanExams()
|
||||
public IActionResult CreateExam([FromBody] CreateExamRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.ID) || string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Description))
|
||||
return BadRequest("实验ID、名称和描述不能为空");
|
||||
|
||||
try
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
var examFolderPath = Path.Combine(Directory.GetCurrentDirectory(), "exam");
|
||||
var updateCount = db.ScanAndUpdateExams(examFolderPath);
|
||||
var result = db.CreateExam(request.ID, request.Name, request.Description, request.Tags, request.Difficulty, request.IsVisibleToUsers);
|
||||
|
||||
var result = new ScanResult
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
Message = $"扫描完成,更新了 {updateCount} 个实验",
|
||||
UpdateCount = updateCount
|
||||
if (result.Error.Message.Contains("已存在"))
|
||||
return Conflict(result.Error.Message);
|
||||
|
||||
logger.Error($"创建实验时出错: {result.Error.Message}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {result.Error.Message}");
|
||||
}
|
||||
|
||||
var exam = result.Value;
|
||||
var examInfo = new ExamInfo
|
||||
{
|
||||
ID = exam.ID,
|
||||
Name = exam.Name,
|
||||
Description = exam.Description,
|
||||
CreatedTime = exam.CreatedTime,
|
||||
UpdatedTime = exam.UpdatedTime,
|
||||
Tags = exam.GetTagsList(),
|
||||
Difficulty = exam.Difficulty,
|
||||
IsVisibleToUsers = exam.IsVisibleToUsers
|
||||
};
|
||||
|
||||
logger.Info($"手动扫描实验完成,更新了 {updateCount} 个实验");
|
||||
return Ok(result);
|
||||
logger.Info($"成功创建实验: {request.ID}");
|
||||
return CreatedAtAction(nameof(GetExam), new { examId = request.ID }, examInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"扫描实验时出错: {ex.Message}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"扫描实验失败: {ex.Message}");
|
||||
logger.Error($"创建实验 {request.ID} 时出错: {ex.Message}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 Markdown 内容中提取标题
|
||||
/// 添加实验资源
|
||||
/// </summary>
|
||||
/// <param name="markdownContent">Markdown 内容</param>
|
||||
/// <returns>提取的标题</returns>
|
||||
private static string ExtractTitleFromMarkdown(string markdownContent)
|
||||
/// <param name="examId">实验ID</param>
|
||||
/// <param name="resourceType">资源类型</param>
|
||||
/// <param name="file">资源文件</param>
|
||||
/// <returns>添加结果</returns>
|
||||
[Authorize("Admin")]
|
||||
[HttpPost("{examId}/resources/{resourceType}")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(ResourceInfo), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> AddExamResource(string examId, string resourceType, IFormFile file)
|
||||
{
|
||||
if (string.IsNullOrEmpty(markdownContent))
|
||||
return "";
|
||||
if (string.IsNullOrWhiteSpace(examId) || string.IsNullOrWhiteSpace(resourceType) || file == null)
|
||||
return BadRequest("实验ID、资源类型和文件不能为空");
|
||||
|
||||
var lines = markdownContent.Split('\n');
|
||||
foreach (var line in lines)
|
||||
try
|
||||
{
|
||||
var trimmedLine = line.Trim();
|
||||
if (trimmedLine.StartsWith("# "))
|
||||
{
|
||||
return trimmedLine.Substring(2).Trim();
|
||||
}
|
||||
}
|
||||
using var db = new Database.AppDataConnection();
|
||||
|
||||
// 读取文件数据
|
||||
using var memoryStream = new MemoryStream();
|
||||
await file.CopyToAsync(memoryStream);
|
||||
var fileData = memoryStream.ToArray();
|
||||
|
||||
return "";
|
||||
var result = db.AddExamResource(examId, resourceType, file.FileName, fileData);
|
||||
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
if (result.Error.Message.Contains("不存在"))
|
||||
return NotFound(result.Error.Message);
|
||||
if (result.Error.Message.Contains("已存在"))
|
||||
return Conflict(result.Error.Message);
|
||||
|
||||
logger.Error($"添加实验资源时出错: {result.Error.Message}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"添加实验资源失败: {result.Error.Message}");
|
||||
}
|
||||
|
||||
var resource = result.Value;
|
||||
var resourceInfo = new ResourceInfo
|
||||
{
|
||||
ID = resource.ID,
|
||||
Name = resource.ResourceName
|
||||
};
|
||||
|
||||
logger.Info($"成功添加实验资源: {examId}/{resourceType}/{file.FileName}");
|
||||
return CreatedAtAction(nameof(GetExamResourceById), new { resourceId = resource.ID }, resourceInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"添加实验资源 {examId}/{resourceType}/{file.FileName} 时出错: {ex.Message}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"添加实验资源失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定实验ID的指定资源类型的所有资源的ID和名称
|
||||
/// </summary>
|
||||
/// <param name="examId">实验ID</param>
|
||||
/// <param name="resourceType">资源类型</param>
|
||||
/// <returns>资源列表</returns>
|
||||
[Authorize]
|
||||
[HttpGet("{examId}/resources/{resourceType}")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(ResourceInfo[]), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult GetExamResourceList(string examId, string resourceType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(examId) || string.IsNullOrWhiteSpace(resourceType))
|
||||
return BadRequest("实验ID和资源类型不能为空");
|
||||
|
||||
try
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
var result = db.GetExamResourceList(examId, resourceType);
|
||||
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"获取实验资源列表时出错: {result.Error.Message}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验资源列表失败: {result.Error.Message}");
|
||||
}
|
||||
|
||||
var resources = result.Value.Select(r => new ResourceInfo
|
||||
{
|
||||
ID = r.ID,
|
||||
Name = r.Name
|
||||
}).ToArray();
|
||||
|
||||
logger.Info($"成功获取实验资源列表: {examId}/{resourceType},共 {resources.Length} 个资源");
|
||||
return Ok(resources);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"获取实验资源列表 {examId}/{resourceType} 时出错: {ex.Message}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验资源列表失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据资源ID下载资源
|
||||
/// </summary>
|
||||
/// <param name="resourceId">资源ID</param>
|
||||
/// <returns>资源文件</returns>
|
||||
[HttpGet("resources/{resourceId}")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult GetExamResourceById(int resourceId)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
var result = db.GetExamResourceById(resourceId);
|
||||
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"获取资源时出错: {result.Error.Message}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {result.Error.Message}");
|
||||
}
|
||||
|
||||
if (!result.Value.HasValue)
|
||||
{
|
||||
logger.Warn($"资源不存在: {resourceId}");
|
||||
return NotFound($"资源 {resourceId} 不存在");
|
||||
}
|
||||
|
||||
var resource = result.Value.Value;
|
||||
logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
|
||||
return File(resource.Data, resource.MimeType, resource.ResourceName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"获取资源 {resourceId} 时出错: {ex.Message}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -162,10 +162,16 @@ public class Exam
|
||||
public required string ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验文档内容(Markdown格式)
|
||||
/// 实验名称
|
||||
/// </summary>
|
||||
[NotNull]
|
||||
public required string DocContent { get; set; }
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验描述
|
||||
/// </summary>
|
||||
[NotNull]
|
||||
public required string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验创建时间
|
||||
@@ -178,6 +184,127 @@ public class Exam
|
||||
/// </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 class ExamResource
|
||||
{
|
||||
/// <summary>
|
||||
/// 资源的唯一标识符
|
||||
/// </summary>
|
||||
[PrimaryKey, Identity]
|
||||
public int ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属实验ID
|
||||
/// </summary>
|
||||
[NotNull]
|
||||
public required string ExamID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 资源类型(images, markdown, bitstream, diagram, project)
|
||||
/// </summary>
|
||||
[NotNull]
|
||||
public required string ResourceType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 资源名称(包含文件扩展名)
|
||||
/// </summary>
|
||||
[NotNull]
|
||||
public required string ResourceName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 资源的二进制数据
|
||||
/// </summary>
|
||||
[NotNull]
|
||||
public required byte[] Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 资源创建时间
|
||||
/// </summary>
|
||||
[NotNull]
|
||||
public DateTime CreatedTime { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 资源的MIME类型
|
||||
/// </summary>
|
||||
[NotNull]
|
||||
public string MimeType { get; set; } = "application/octet-stream";
|
||||
|
||||
/// <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>
|
||||
@@ -228,6 +355,7 @@ public class AppDataConnection : DataConnection
|
||||
this.CreateTable<User>();
|
||||
this.CreateTable<Board>();
|
||||
this.CreateTable<Exam>();
|
||||
this.CreateTable<ExamResource>();
|
||||
logger.Info("数据库表创建完成");
|
||||
}
|
||||
|
||||
@@ -240,6 +368,7 @@ public class AppDataConnection : DataConnection
|
||||
this.DropTable<User>();
|
||||
this.DropTable<Board>();
|
||||
this.DropTable<Exam>();
|
||||
this.DropTable<ExamResource>();
|
||||
logger.Warn("所有数据库表已删除");
|
||||
}
|
||||
|
||||
@@ -674,75 +803,271 @@ public class AppDataConnection : DataConnection
|
||||
public ITable<Exam> ExamTable => this.GetTable<Exam>();
|
||||
|
||||
/// <summary>
|
||||
/// 扫描 exam 文件夹并更新实验数据库
|
||||
/// 实验资源表
|
||||
/// </summary>
|
||||
/// <param name="examFolderPath">exam 文件夹的路径</param>
|
||||
/// <returns>更新的实验数量</returns>
|
||||
public int ScanAndUpdateExams(string examFolderPath)
|
||||
public ITable<ExamResource> ExamResourceTable => this.GetTable<ExamResource>();
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (!Directory.Exists(examFolderPath))
|
||||
try
|
||||
{
|
||||
logger.Warn($"实验文件夹不存在: {examFolderPath}");
|
||||
return 0;
|
||||
// 检查实验ID是否已存在
|
||||
var existingExam = this.ExamTable.Where(e => e.ID == id).FirstOrDefault();
|
||||
if (existingExam != null)
|
||||
{
|
||||
logger.Error($"实验ID已存在: {id}");
|
||||
return new(new Exception($"实验ID已存在: {id}"));
|
||||
}
|
||||
|
||||
var exam = new Exam
|
||||
{
|
||||
ID = id,
|
||||
Name = name,
|
||||
Description = description,
|
||||
Difficulty = Math.Max(1, Math.Min(5, difficulty)),
|
||||
IsVisibleToUsers = isVisibleToUsers,
|
||||
CreatedTime = DateTime.Now,
|
||||
UpdatedTime = DateTime.Now
|
||||
};
|
||||
|
||||
if (tags != null)
|
||||
{
|
||||
exam.SetTagsList(tags);
|
||||
}
|
||||
|
||||
this.Insert(exam);
|
||||
logger.Info($"新实验已创建: {id} ({name})");
|
||||
return new(exam);
|
||||
}
|
||||
|
||||
int updateCount = 0;
|
||||
var subdirectories = Directory.GetDirectories(examFolderPath);
|
||||
|
||||
foreach (var examDir in subdirectories)
|
||||
catch (Exception ex)
|
||||
{
|
||||
var examId = Path.GetFileName(examDir);
|
||||
var docPath = Path.Combine(examDir, "doc.md");
|
||||
|
||||
if (!File.Exists(docPath))
|
||||
{
|
||||
logger.Warn($"实验 {examId} 缺少 doc.md 文件");
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var docContent = File.ReadAllText(docPath);
|
||||
var existingExam = this.ExamTable.Where(e => e.ID == examId).FirstOrDefault();
|
||||
|
||||
if (existingExam == null)
|
||||
{
|
||||
// 创建新实验
|
||||
var newExam = new Exam
|
||||
{
|
||||
ID = examId,
|
||||
DocContent = docContent,
|
||||
CreatedTime = DateTime.Now,
|
||||
UpdatedTime = DateTime.Now
|
||||
};
|
||||
this.Insert(newExam);
|
||||
logger.Info($"新实验已添加: {examId}");
|
||||
updateCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 更新现有实验
|
||||
var fileLastWrite = File.GetLastWriteTime(docPath);
|
||||
if (fileLastWrite > existingExam.UpdatedTime)
|
||||
{
|
||||
this.ExamTable
|
||||
.Where(e => e.ID == examId)
|
||||
.Set(e => e.DocContent, docContent)
|
||||
.Set(e => e.UpdatedTime, DateTime.Now)
|
||||
.Update();
|
||||
logger.Info($"实验已更新: {examId}");
|
||||
updateCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"处理实验 {examId} 时出错: {ex.Message}");
|
||||
}
|
||||
logger.Error($"创建实验时出错: {ex.Message}");
|
||||
return new(ex);
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info($"实验扫描完成,共更新 {updateCount} 个实验");
|
||||
return updateCount;
|
||||
/// <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 += this.ExamTable.Where(e => e.ID == id).Set(e => e.Name, name).Update();
|
||||
}
|
||||
if (description != null)
|
||||
{
|
||||
result += this.ExamTable.Where(e => e.ID == id).Set(e => e.Description, description).Update();
|
||||
}
|
||||
if (tags != null)
|
||||
{
|
||||
var tagsString = string.Join(",", tags.Where(tag => !string.IsNullOrWhiteSpace(tag)).Select(tag => tag.Trim()));
|
||||
result += this.ExamTable.Where(e => e.ID == id).Set(e => e.Tags, tagsString).Update();
|
||||
}
|
||||
if (difficulty.HasValue)
|
||||
{
|
||||
result += this.ExamTable.Where(e => e.ID == id).Set(e => e.Difficulty, Math.Max(1, Math.Min(5, difficulty.Value))).Update();
|
||||
}
|
||||
if (isVisibleToUsers.HasValue)
|
||||
{
|
||||
result += this.ExamTable.Where(e => e.ID == id).Set(e => e.IsVisibleToUsers, isVisibleToUsers.Value).Update();
|
||||
}
|
||||
|
||||
// 更新时间
|
||||
this.ExamTable.Where(e => e.ID == id).Set(e => e.UpdatedTime, DateTime.Now).Update();
|
||||
|
||||
logger.Info($"实验已更新: {id},更新记录数: {result}");
|
||||
return new(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"更新实验时出错: {ex.Message}");
|
||||
return new(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加实验资源
|
||||
/// </summary>
|
||||
/// <param name="examId">所属实验ID</param>
|
||||
/// <param name="resourceType">资源类型</param>
|
||||
/// <param name="resourceName">资源名称</param>
|
||||
/// <param name="data">资源二进制数据</param>
|
||||
/// <param name="mimeType">MIME类型(可选,将根据文件扩展名自动确定)</param>
|
||||
/// <returns>创建的资源</returns>
|
||||
public Result<ExamResource> AddExamResource(string examId, string resourceType, string resourceName, byte[] data, string? mimeType = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 验证实验是否存在
|
||||
var exam = this.ExamTable.Where(e => e.ID == examId).FirstOrDefault();
|
||||
if (exam == null)
|
||||
{
|
||||
logger.Error($"实验不存在: {examId}");
|
||||
return new(new Exception($"实验不存在: {examId}"));
|
||||
}
|
||||
|
||||
// 检查资源是否已存在
|
||||
var existingResource = this.ExamResourceTable
|
||||
.Where(r => r.ExamID == examId && r.ResourceType == resourceType && r.ResourceName == resourceName)
|
||||
.FirstOrDefault();
|
||||
if (existingResource != null)
|
||||
{
|
||||
logger.Error($"资源已存在: {examId}/{resourceType}/{resourceName}");
|
||||
return new(new Exception($"资源已存在: {examId}/{resourceType}/{resourceName}"));
|
||||
}
|
||||
|
||||
// 如果未指定MIME类型,根据文件扩展名自动确定
|
||||
if (string.IsNullOrEmpty(mimeType))
|
||||
{
|
||||
var extension = Path.GetExtension(resourceName).ToLowerInvariant();
|
||||
mimeType = GetMimeTypeFromExtension(extension, resourceName);
|
||||
}
|
||||
|
||||
var resource = new ExamResource
|
||||
{
|
||||
ExamID = examId,
|
||||
ResourceType = resourceType,
|
||||
ResourceName = resourceName,
|
||||
Data = data,
|
||||
MimeType = mimeType,
|
||||
CreatedTime = DateTime.Now
|
||||
};
|
||||
|
||||
this.Insert(resource);
|
||||
logger.Info($"新资源已添加: {examId}/{resourceType}/{resourceName} ({data.Length} bytes)");
|
||||
return new(resource);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"添加实验资源时出错: {ex.Message}");
|
||||
return new(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定实验ID的指定资源类型的所有资源的ID和名称
|
||||
/// </summary>
|
||||
/// <param name="examId">实验ID</param>
|
||||
/// <param name="resourceType">资源类型</param>
|
||||
/// <returns>资源信息列表</returns>
|
||||
public Result<(int ID, string Name)[]> GetExamResourceList(string examId, string resourceType)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resources = this.ExamResourceTable
|
||||
.Where(r => r.ExamID == examId && r.ResourceType == resourceType)
|
||||
.Select(r => new { r.ID, r.ResourceName })
|
||||
.ToArray();
|
||||
|
||||
var result = resources.Select(r => (r.ID, r.ResourceName)).ToArray();
|
||||
logger.Info($"获取实验资源列表: {examId}/{resourceType},共 {result.Length} 个资源");
|
||||
return new(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"获取实验资源列表时出错: {ex.Message}");
|
||||
return new(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据资源ID获取资源
|
||||
/// </summary>
|
||||
/// <param name="resourceId">资源ID</param>
|
||||
/// <returns>资源数据</returns>
|
||||
public Result<Optional<ExamResource>> GetExamResourceById(int resourceId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resource = this.ExamResourceTable.Where(r => r.ID == resourceId).FirstOrDefault();
|
||||
|
||||
if (resource == null)
|
||||
{
|
||||
logger.Info($"未找到资源: {resourceId}");
|
||||
return new(Optional<ExamResource>.None);
|
||||
}
|
||||
|
||||
logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
|
||||
return new(resource);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"获取资源时出错: {ex.Message}");
|
||||
return new(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除实验资源
|
||||
/// </summary>
|
||||
/// <param name="resourceId">资源ID</param>
|
||||
/// <returns>删除的记录数</returns>
|
||||
public Result<int> DeleteExamResource(int resourceId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = this.ExamResourceTable.Where(r => r.ID == resourceId).Delete();
|
||||
logger.Info($"资源已删除: {resourceId},删除记录数: {result}");
|
||||
return new(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"删除资源时出错: {ex.Message}");
|
||||
return new(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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",
|
||||
".sbit" => "application/octet-stream",
|
||||
".bit" => "application/octet-stream",
|
||||
".bin" => "application/octet-stream",
|
||||
".json" => "application/json",
|
||||
".zip" => "application/zip",
|
||||
".md" => "text/markdown",
|
||||
_ => "application/octet-stream"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -780,4 +1105,31 @@ public class AppDataConnection : DataConnection
|
||||
logger.Debug($"成功获取实验信息: {examId}");
|
||||
return new(exams[0]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除所有实验
|
||||
/// </summary>
|
||||
/// <returns>删除的实验数量</returns>
|
||||
public int DeleteAllExams()
|
||||
{
|
||||
// 先删除所有实验资源
|
||||
var resourceDeleteCount = this.DeleteAllExamResources();
|
||||
logger.Info($"已删除所有实验资源,共删除 {resourceDeleteCount} 个资源");
|
||||
|
||||
// 再删除所有实验
|
||||
var examDeleteCount = this.ExamTable.Delete();
|
||||
logger.Info($"已删除所有实验,共删除 {examDeleteCount} 个实验");
|
||||
return examDeleteCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除所有实验资源
|
||||
/// </summary>
|
||||
/// <returns>删除的资源数量</returns>
|
||||
public int DeleteAllExamResources()
|
||||
{
|
||||
var deleteCount = this.ExamResourceTable.Delete();
|
||||
logger.Info($"已删除所有实验资源,共删除 {deleteCount} 个资源");
|
||||
return deleteCount;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user