add: 添加实验列表界面,实验增删完全依赖数据库实现

This commit is contained in:
alivender
2025-08-01 12:57:30 +08:00
parent d27b5d7737
commit c564844673
19 changed files with 3511 additions and 1688 deletions

View File

@@ -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}");
}
}
}