已按“实验成绩独立管理”完成修改。
每个实验项目独立建成绩单,支持评分项、参与状态、补做次数、安全违规、报告地址和教师评语。 流程为:录入 → 学院审核 → 教务发布;学生只能查看已发布成绩。 自主预约实验支持同步有效预约名单,已评分记录不会被误删。 课程成绩可将已发布实验成绩汇总为快照导入指定分项;导入后锁定,不能手工篡改。 实验项目成绩不完整时,对应课程分项保持空白,不按 0 分处理。 一级“实验管理”菜单下增加“项目与场次”“实验成绩”;学生端增加“实验安排”“实验成绩”。 已生成 MySQL 正式迁移及 SQLite 开发迁移,尚未操作生产数据库。 界面采用“实验项目 → 独立评分 → 审核发布”工作台设计。
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user