实验成绩导出

This commit is contained in:
biss committed 2026-08-21 17:51:33 +08:00
1 parent bcdb00ee2e
commit 4446cfbd30
3 files changed
+194

No files matched your search

@@ -190,6 +190,84 @@ public sealed class ExperimentGradesController(
});
}
[HttpGet("teaching-tasks/{teachingTaskId:guid}/export.xlsx")]
[Authorize(Roles = Managers)]
public async Task<IActionResult> ExportTeachingTaskScores(
Guid teachingTaskId,
CancellationToken cancellationToken)
{
var sheets = await ScopedSheets()
.Where(sheet =>
sheet.ExperimentProject!.TeachingTaskId == teachingTaskId)
.Include(sheet => sheet.Records)
.ThenInclude(record => record.Student)
.ThenInclude(student => student!.AdministrativeClass)
.Include(sheet => sheet.ExperimentProject)
.ThenInclude(project => project!.TeachingTask)
.ThenInclude(task => task!.AcademicTerm)
.Include(sheet => sheet.ExperimentProject)
.ThenInclude(project => project!.TeachingTask)
.ThenInclude(task => task!.Course)
.ThenInclude(course => course!.College)
.AsSplitQuery()
.AsNoTracking()
.OrderBy(sheet => sheet.ExperimentProject!.Code)
.ToListAsync(cancellationToken);
if (sheets.Count == 0)
return NotFound();
var task = sheets[0].ExperimentProject!.TeachingTask!;
var course = task.Course!;
var students = sheets
.SelectMany(sheet => sheet.Records)
.Select(record => record.Student!)
.GroupBy(student => student.Id)
.Select(group => group.First())
.OrderBy(student => student.StudentNumber)
.ToList();
var aggregate = await ExperimentGradeAggregationService.CalculateAsync(
db,
teachingTaskId,
students.Select(student => student.Id).ToArray(),
cancellationToken);
var headers = new List<string>
{
"学期", "开课学院", "课程代码", "课程名称", "教学班",
"学号", "姓名", "行政班"
};
headers.AddRange(sheets.Select(sheet =>
$"{sheet.ExperimentProject!.Code}·{sheet.ExperimentProject.Name}" +
$"({SheetStatusLabel(sheet.Status)})"));
headers.Add("实验合成总分(仅已发布项目)");
var rows = students.Select(student =>
{
var values = new List<object?>
{
task.AcademicTerm!.Name,
course.College!.Name,
course.Code,
course.Name,
task.TaskNumber,
student.StudentNumber,
student.Name,
student.AdministrativeClass!.Name
};
values.AddRange(sheets.Select(sheet => (object?)sheet.Records
.FirstOrDefault(record => record.StudentId == student.Id)
?.TotalScore));
values.Add(aggregate.Scores.GetValueOrDefault(student.Id));
return (IReadOnlyList<object?>)values;
}).ToList();
var bytes = ExcelWorkbookHelper.Create("课程实验成绩", headers, rows);
return File(bytes, ExcelWorkbookHelper.ContentType,
DownloadFileName.Export(
"课程实验成绩",
".xlsx",
course.Code,
task.TaskNumber));
}
[HttpGet("mine")]
[Authorize(Roles = SystemRoles.Student)]
public async Task<ActionResult> GetMine(CancellationToken cancellationToken)
@@ -1300,6 +1378,17 @@ public sealed class ExperimentGradesController(
_ => "待登记"
};
private static string SheetStatusLabel(ExperimentGradeSheetStatus status) =>
status switch
{
ExperimentGradeSheetStatus.Draft => "录入中",
ExperimentGradeSheetStatus.Submitted => "待审核",
ExperimentGradeSheetStatus.Approved => "审核通过",
ExperimentGradeSheetStatus.Published => "已发布",
ExperimentGradeSheetStatus.Returned => "已退回",
_ => status.ToString()
};
private static bool TryParseParticipationStatus(
ExcelRow row,
out ExperimentParticipationStatus status,
@@ -14,6 +14,92 @@ namespace Jiaowu.Api.Tests;
public sealed class ExperimentGradesControllerTests
{
[Fact]
public async Task ExportTeachingTaskScores_IncludesEveryProjectAndPublishedTotal()
{
await using var fixture = await ExperimentGradeFixture.CreateAsync();
var firstProject = new ExperimentProject
{
TeachingTaskId = fixture.Task.Id,
Code = "LAB-EXPORT-1",
Name = "实验一",
ArrangementMode = ExperimentArrangementMode.Centralized,
StartDate = fixture.Term.StartDate,
EndDate = fixture.Term.EndDate,
Status = ExperimentProjectStatus.Published,
PublishedAt = DateTime.UtcNow
};
var secondProject = new ExperimentProject
{
TeachingTaskId = fixture.Task.Id,
Code = "LAB-EXPORT-2",
Name = "实验二",
ArrangementMode = ExperimentArrangementMode.Centralized,
StartDate = fixture.Term.StartDate,
EndDate = fixture.Term.EndDate,
Status = ExperimentProjectStatus.Published,
PublishedAt = DateTime.UtcNow
};
fixture.Db.ExperimentProjects.AddRange(firstProject, secondProject);
await fixture.Db.SaveChangesAsync();
fixture.Db.ExperimentGradeSheets.AddRange(
new ExperimentGradeSheet
{
ExperimentProjectId = firstProject.Id,
ContributionWeight = 1,
PassScore = 60,
Status = ExperimentGradeSheetStatus.Published,
PublishedAt = DateTime.UtcNow,
Records =
[
new ExperimentGradeRecord
{
StudentId = fixture.Student.Id,
ParticipationStatus = ExperimentParticipationStatus.Completed,
TotalScore = 80,
IsPassed = true
}
]
},
new ExperimentGradeSheet
{
ExperimentProjectId = secondProject.Id,
ContributionWeight = 3,
PassScore = 60,
Status = ExperimentGradeSheetStatus.Published,
PublishedAt = DateTime.UtcNow,
Records =
[
new ExperimentGradeRecord
{
StudentId = fixture.Student.Id,
ParticipationStatus = ExperimentParticipationStatus.Completed,
TotalScore = 90,
IsPassed = true
}
]
});
await fixture.Db.SaveChangesAsync();
await ExperimentGradeAggregationService.RefreshTeachingTaskAsync(
fixture.Db,
fixture.Task.Id,
CancellationToken.None);
fixture.Db.ChangeTracker.Clear();
var file = Assert.IsType<FileContentResult>(await fixture
.ExperimentGrades(fixture.AdminScope)
.ExportTeachingTaskScores(fixture.Task.Id, CancellationToken.None));
await using var stream = new MemoryStream(file.FileContents);
using var workbook = new XLWorkbook(stream);
var worksheet = workbook.Worksheet("课程实验成绩");
Assert.Equal("LAB-EXPORT-1·实验一(已发布)", worksheet.Cell(1, 9).GetString());
Assert.Equal("LAB-EXPORT-2·实验二(已发布)", worksheet.Cell(1, 10).GetString());
Assert.Equal("实验合成总分(仅已发布项目)", worksheet.Cell(1, 11).GetString());
Assert.Equal("80", worksheet.Cell(2, 9).GetFormattedString());
Assert.Equal("90", worksheet.Cell(2, 10).GetFormattedString());
Assert.Equal("87.5", worksheet.Cell(2, 11).GetFormattedString());
}
[Fact]
public async Task UpdateSettings_ReplacesItemsAndRecreatesScores()
{
+19
View File
@@ -376,6 +376,18 @@ function downloadTemplate() {
)
}
async function exportCourseScores(course: any) {
try {
await downloadApiFile(
`/experiment-grades/teaching-tasks/${course.teachingTaskId}/export.xlsx`,
`${course.courseCode}-${course.taskNumber}-课程实验成绩.xlsx`,
)
ElMessage.success('课程实验成绩已导出')
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
}
function chooseImportFile() {
importFileInput.value?.click()
}
@@ -650,6 +662,13 @@ onMounted(async () => {
<b>{{ course.projectCount }}</b><span>个实验项目</span>
<small v-if="course.sheetCount">{{ course.sheetCount }} 份成绩单 · {{ course.scoredCount }}/{{ course.studentCount }} 已计分</small>
<small v-else>尚未建立成绩单</small>
<el-button
v-if="course.sheetCount"
type="primary"
text
:icon="Download"
@click="exportCourseScores(course)"
>导出实验成绩</el-button>
</div>
</header>
<el-table :data="course.projects" size="small" class="course-project-table">