已按“实验成绩独立管理”完成修改。

每个实验项目独立建成绩单,支持评分项、参与状态、补做次数、安全违规、报告地址和教师评语。
流程为:录入 → 学院审核 → 教务发布;学生只能查看已发布成绩。
自主预约实验支持同步有效预约名单,已评分记录不会被误删。
课程成绩可将已发布实验成绩汇总为快照导入指定分项;导入后锁定,不能手工篡改。
实验项目成绩不完整时,对应课程分项保持空白,不按 0 分处理。
一级“实验管理”菜单下增加“项目与场次”“实验成绩”;学生端增加“实验安排”“实验成绩”。
已生成 MySQL 正式迁移及 SQLite 开发迁移,尚未操作生产数据库。
界面采用“实验项目 → 独立评分 → 审核发布”工作台设计。
This commit is contained in:
2026-07-28 22:21:18 +08:00 Unverified
parent 51c3b81e27
commit 8f56548b54
17 changed files with 9239 additions and 14 deletions
+82 -5
View File
@@ -101,7 +101,9 @@ public sealed class GradesController(
{
item.Id,
item.Name,
item.Weight
item.Weight,
item.SourceType,
item.SourceSnapshotAt
}),
StudentCount = sheet.Records.Count,
CompletedCount = sheet.Records.Count(record =>
@@ -220,7 +222,9 @@ public sealed class GradesController(
{
item.Id,
item.Name,
item.Weight
item.Weight,
item.SourceType,
item.SourceSnapshotAt
}),
x.Status,
x.ReviewComment,
@@ -408,7 +412,7 @@ public sealed class GradesController(
request.Records.Any(x => !records.ContainsKey(x.Id)))
return ValidationProblem("包含无效或重复的成绩记录。");
var itemIds = sheet.Items.Select(item => item.Id).ToHashSet();
var itemsById = sheet.Items.ToDictionary(item => item.Id);
foreach (var item in request.Records)
{
if (!ValidScore(item.RegularScore) ||
@@ -428,8 +432,19 @@ public sealed class GradesController(
{
if (!ValidScore(scoreEntry.Score))
return ValidationProblem("分项成绩必须在 0—100 分之间。");
if (scoreMap.TryGetValue(scoreEntry.GradeItemId, out var existing))
existing.Score = scoreEntry.Score;
if (!itemsById.TryGetValue(
scoreEntry.GradeItemId,
out var gradeItem) ||
!scoreMap.TryGetValue(
scoreEntry.GradeItemId,
out var existing))
continue;
if (gradeItem.SourceType ==
GradeItemSourceType.ExperimentSummary &&
existing.Score != scoreEntry.Score)
return ConflictProblem(
$"“{gradeItem.Name}”来自实验成绩快照,不能手工修改;请重新导入实验成绩。");
existing.Score = scoreEntry.Score;
}
}
@@ -438,6 +453,66 @@ public sealed class GradesController(
return await SaveAsync(id, false, cancellationToken);
}
[HttpPost("sheets/{id:guid}/import-experiment-scores")]
[Authorize(Roles = SheetUsers)]
public async Task<ActionResult> ImportExperimentScores(
Guid id,
ImportExperimentScoresRequest request,
CancellationToken cancellationToken)
{
var sheet = await AccessibleSheets()
.Include(x => x.Items)
.Include(x => x.Records)
.ThenInclude(x => x.ItemScores)
.Include(x => x.TeachingTask)
.ThenInclude(x => x!.Teachers)
.ThenInclude(x => x.Teacher)
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
if (sheet is null) return NotFound();
if (!CanEditScores(sheet.TeachingTask!)) return Forbid();
if (sheet.Status is not (
GradeSheetStatus.Draft or
GradeSheetStatus.Returned))
return ConflictProblem("成绩单提交后不能导入实验成绩。");
var targetItem = sheet.Items.FirstOrDefault(x =>
x.Id == request.GradeItemId);
if (targetItem is null)
return ValidationProblem("请选择当前成绩单中的有效分项。");
var aggregate = await ExperimentGradeAggregationService.CalculateAsync(
db,
sheet.TeachingTaskId,
sheet.Records.Select(x => x.StudentId).ToArray(),
cancellationToken);
if (aggregate.PublishedProjectCount == 0)
return ConflictProblem(
"该教学任务尚无已发布的实验项目成绩,无法导入。");
var imported = 0;
var missing = 0;
foreach (var record in sheet.Records)
{
var targetScore = record.ItemScores.First(x =>
x.GradeItemId == targetItem.Id);
targetScore.Score = aggregate.Scores[record.StudentId];
if (targetScore.Score.HasValue) imported++;
else missing++;
Recalculate(sheet, record);
}
targetItem.SourceType = GradeItemSourceType.ExperimentSummary;
targetItem.SourceSnapshotAt = DateTime.UtcNow;
await db.SaveChangesAsync(cancellationToken);
return Ok(new
{
aggregate.PublishedProjectCount,
aggregate.Projects,
ImportedCount = imported,
MissingCount = missing,
targetItem.SourceSnapshotAt
});
}
[HttpPost("sheets/{id:guid}/submit")]
[Authorize(Roles = SheetUsers)]
public async Task<ActionResult> Submit(Guid id, CancellationToken cancellationToken)
@@ -998,5 +1073,7 @@ public sealed record GradeItemScoreRequest(
Guid GradeItemId,
decimal? Score);
public sealed record ImportExperimentScoresRequest(Guid GradeItemId);
public sealed record GradeReviewRequest(
[MaxLength(500)] string? Comment);