考生范围统一为“行政班关联学生 + 已选课学生”,并自动去重;考场分配、人数显示、冲突检测、学生考试安排均使用同一口径。[TeachingTaskRosterQuery.cs (line 40)](E:/jiaowu/src/Jiaowu.Api/Infrastructure/Teaching/TeachingTaskRosterQuery.cs:40) 新增考场签名单 Excel 导出接口。[ExamsController.cs (line 568)](E:/jiaowu/src/Jiaowu.Api/Controllers/ExamsController.cs:568) 考试计划页面新增“导出考场签名单”按钮。[ExamsView.vue (line 255)](E:/jiaowu/web/src/views/ExamsView.vue:255) 工作簿包含“考场汇总”以及每个考试场次独立的 A4 签到表,字段包括座位号、学号、姓名、行政班、考生签名和备注。[ExamSignInWorkbookExporter.cs (line 12)](E:/jiaowu/src/Jiaowu.Api/Infrastructure/Exams/ExamSignInWorkbookExporter.cs:12) 未分配考场或监考教师时,也能导出并显示“待分配”。
123 lines
4.7 KiB
C#
123 lines
4.7 KiB
C#
using Jiaowu.Api.Domain.Academic;
|
|
using Jiaowu.Api.Infrastructure.Persistence;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Jiaowu.Api.Infrastructure.Teaching;
|
|
|
|
public static class TeachingTaskRosterQuery
|
|
{
|
|
public static IQueryable<Student> ForTask(AppDbContext db, Guid teachingTaskId) =>
|
|
db.Students.Where(student =>
|
|
(student.Status == StudentStatus.Active &&
|
|
db.TeachingTaskClasses.Any(assignment =>
|
|
assignment.TeachingTaskId == teachingTaskId &&
|
|
assignment.AdministrativeClassId == student.AdministrativeClassId)) ||
|
|
db.CourseEnrollments.Any(enrollment =>
|
|
enrollment.StudentId == student.Id &&
|
|
enrollment.Status == CourseEnrollmentStatus.Enrolled &&
|
|
enrollment.CourseSelectionOffering!.TeachingTaskId == teachingTaskId));
|
|
|
|
public static IQueryable<Guid> TaskIdsForStudent(AppDbContext db, Guid studentId)
|
|
{
|
|
var classTaskIds = db.Students
|
|
.Where(student =>
|
|
student.Id == studentId &&
|
|
student.Status == StudentStatus.Active)
|
|
.SelectMany(student => db.TeachingTaskClasses
|
|
.Where(assignment =>
|
|
assignment.AdministrativeClassId ==
|
|
student.AdministrativeClassId)
|
|
.Select(assignment => assignment.TeachingTaskId));
|
|
var enrolledTaskIds = db.CourseEnrollments
|
|
.Where(enrollment =>
|
|
enrollment.StudentId == studentId &&
|
|
enrollment.Status == CourseEnrollmentStatus.Enrolled)
|
|
.Select(enrollment =>
|
|
enrollment.CourseSelectionOffering!.TeachingTaskId);
|
|
return classTaskIds.Concat(enrolledTaskIds).Distinct();
|
|
}
|
|
|
|
public static async Task<IReadOnlyList<TeachingTaskRosterEntry>> LoadForTasksAsync(
|
|
AppDbContext db,
|
|
IEnumerable<Guid> teachingTaskIds,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var taskIds = teachingTaskIds.Distinct().ToArray();
|
|
if (taskIds.Length == 0) return [];
|
|
|
|
var assignments = await db.TeachingTaskClasses.AsNoTracking()
|
|
.WhereIn(taskIds, x => x.TeachingTaskId)
|
|
.Select(x => new
|
|
{
|
|
x.TeachingTaskId,
|
|
x.AdministrativeClassId
|
|
})
|
|
.ToListAsync(cancellationToken);
|
|
|
|
var classIds = assignments
|
|
.Select(x => x.AdministrativeClassId)
|
|
.Distinct()
|
|
.ToArray();
|
|
var classStudents = classIds.Length == 0
|
|
? []
|
|
: await db.Students.AsNoTracking()
|
|
.Where(x => x.Status == StudentStatus.Active)
|
|
.WhereIn(classIds, x => x.AdministrativeClassId)
|
|
.Select(x => new RosterStudent(
|
|
x.Id,
|
|
x.StudentNumber,
|
|
x.Name,
|
|
x.AdministrativeClassId,
|
|
x.AdministrativeClass!.Name))
|
|
.ToListAsync(cancellationToken);
|
|
|
|
var enrolledStudents = await db.CourseEnrollments.AsNoTracking()
|
|
.Where(x => x.Status == CourseEnrollmentStatus.Enrolled)
|
|
.WhereIn(taskIds, x => x.CourseSelectionOffering!.TeachingTaskId)
|
|
.Select(x => new TeachingTaskRosterEntry(
|
|
x.CourseSelectionOffering!.TeachingTaskId,
|
|
x.StudentId,
|
|
x.Student!.StudentNumber,
|
|
x.Student.Name,
|
|
x.Student.AdministrativeClass!.Name))
|
|
.ToListAsync(cancellationToken);
|
|
|
|
var result = enrolledStudents.ToDictionary(
|
|
x => (x.TeachingTaskId, x.StudentId));
|
|
var studentsByClass = classStudents.ToLookup(x => x.AdministrativeClassId);
|
|
foreach (var assignment in assignments)
|
|
{
|
|
foreach (var student in studentsByClass[assignment.AdministrativeClassId])
|
|
{
|
|
result.TryAdd(
|
|
(assignment.TeachingTaskId, student.StudentId),
|
|
new TeachingTaskRosterEntry(
|
|
assignment.TeachingTaskId,
|
|
student.StudentId,
|
|
student.StudentNumber,
|
|
student.Name,
|
|
student.ClassName));
|
|
}
|
|
}
|
|
|
|
return result.Values
|
|
.OrderBy(x => x.TeachingTaskId)
|
|
.ThenBy(x => x.StudentNumber)
|
|
.ToList();
|
|
}
|
|
|
|
private sealed record RosterStudent(
|
|
Guid StudentId,
|
|
string StudentNumber,
|
|
string Name,
|
|
Guid AdministrativeClassId,
|
|
string ClassName);
|
|
}
|
|
|
|
public sealed record TeachingTaskRosterEntry(
|
|
Guid TeachingTaskId,
|
|
Guid StudentId,
|
|
string StudentNumber,
|
|
string Name,
|
|
string ClassName);
|