点名、成绩册统一读取“行政班在籍学生 ∪ 已选课学生”,不再错误显示 0 人。

教师选课名单改为展示全部本人教学班及完整学生名单,支持 Excel 导出。
调停课改用教师专用教学班接口,并在服务端限制只能操作本人教学班。
This commit is contained in:
2026-07-27 08:05:42 +08:00 Unverified
parent bc6ffa2ada
commit 22321abe23
9 changed files with 491 additions and 69 deletions
@@ -6,6 +6,7 @@ using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.CourseSelection;
using Jiaowu.Api.Infrastructure.Excel;
using Jiaowu.Api.Infrastructure.Persistence;
using Jiaowu.Api.Infrastructure.Teaching;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@@ -1561,6 +1562,99 @@ public sealed class CourseSelectionsController(
return Ok(offerings);
}
[HttpGet("my-teaching-tasks")]
[Authorize(Roles = SystemRoles.Teacher)]
public async Task<ActionResult> GetMyTeachingTasks(
Guid? academicTermId,
CancellationToken cancellationToken)
{
var userId = currentUserDataScope.Current.UserId;
var source = db.TeachingTasks.AsNoTracking()
.Where(x =>
(x.Status == TeachingTaskStatus.Published ||
x.Status == TeachingTaskStatus.Closed) &&
x.Teachers.Any(item => item.Teacher!.UserId == userId));
if (academicTermId.HasValue)
source = source.Where(x => x.AcademicTermId == academicTermId);
return Ok(await source
.OrderByDescending(x => x.AcademicTerm!.StartDate)
.ThenBy(x => x.Course!.Code)
.ThenBy(x => x.TaskNumber)
.Select(x => new
{
x.Id,
x.TaskNumber,
TaskName = x.Name,
x.AcademicTermId,
TermName = x.AcademicTerm!.Name,
CourseCode = x.Course!.Code,
CourseName = x.Course.Name,
x.Capacity,
StudentCount = db.Students.Count(student =>
(student.Status == StudentStatus.Active &&
x.Classes.Any(assignment =>
assignment.AdministrativeClassId ==
student.AdministrativeClassId)) ||
db.CourseEnrollments.Any(enrollment =>
enrollment.StudentId == student.Id &&
enrollment.Status == CourseEnrollmentStatus.Enrolled &&
enrollment.CourseSelectionOffering!.TeachingTaskId == x.Id))
})
.ToListAsync(cancellationToken));
}
[HttpGet("teaching-tasks/{id:guid}/roster")]
[Authorize(Roles = SystemRoles.Teacher)]
public async Task<ActionResult> GetTeachingTaskRoster(
Guid id,
CancellationToken cancellationToken)
{
var task = await GetAssignedTeachingTaskAsync(id, cancellationToken);
if (task is null) return NotFound();
var students = await LoadTeachingTaskRosterAsync(id, cancellationToken);
return Ok(new
{
task.Id,
task.TaskNumber,
task.TaskName,
task.CourseCode,
task.CourseName,
task.Capacity,
StudentCount = students.Count,
Students = students
});
}
[HttpGet("teaching-tasks/{id:guid}/roster/export.xlsx")]
[Authorize(Roles = SystemRoles.Teacher)]
public async Task<ActionResult> ExportTeachingTaskRoster(
Guid id,
CancellationToken cancellationToken)
{
var task = await GetAssignedTeachingTaskAsync(id, cancellationToken);
if (task is null) return NotFound();
var students = await LoadTeachingTaskRosterAsync(id, cancellationToken);
var bytes = ExcelWorkbookHelper.Create(
"教学班名单",
["学号", "姓名", "班级", "专业", "进入方式", "选课时间"],
students.Select(student => new List<object?>
{
student.StudentNumber,
student.Name,
student.ClassName,
student.MajorName,
student.EnrolledAt.HasValue ? "选课" : "行政班关联",
student.EnrolledAt?.ToString("yyyy-MM-dd HH:mm") ?? "-"
}).ToList<IReadOnlyList<object?>>());
return File(
bytes,
ExcelWorkbookHelper.ContentType,
$"教学班名单-{task.TaskNumber}.xlsx");
}
[HttpGet("offerings/{id:guid}/roster/export.xlsx")]
[Authorize(Roles = RosterReaders)]
public async Task<ActionResult> ExportRoster(
@@ -1627,6 +1721,50 @@ public sealed class CourseSelectionsController(
x.TeachingTask!.Course!.CollegeId == scope.RestrictedCollegeId);
}
private Task<TeacherTeachingTaskInfo?> GetAssignedTeachingTaskAsync(
Guid id,
CancellationToken cancellationToken)
{
var userId = currentUserDataScope.Current.UserId;
return db.TeachingTasks.AsNoTracking()
.Where(x =>
x.Id == id &&
(x.Status == TeachingTaskStatus.Published ||
x.Status == TeachingTaskStatus.Closed) &&
x.Teachers.Any(item => item.Teacher!.UserId == userId))
.Select(x => new TeacherTeachingTaskInfo(
x.Id,
x.TaskNumber,
x.Name,
x.Course!.Code,
x.Course.Name,
x.Capacity))
.FirstOrDefaultAsync(cancellationToken);
}
private Task<List<TeachingTaskRosterStudent>> LoadTeachingTaskRosterAsync(
Guid teachingTaskId,
CancellationToken cancellationToken) =>
TeachingTaskRosterQuery.ForTask(db, teachingTaskId)
.AsNoTracking()
.OrderBy(x => x.StudentNumber)
.Select(x => new TeachingTaskRosterStudent(
x.Id,
x.StudentNumber,
x.Name,
x.AdministrativeClass!.Name,
x.AdministrativeClass.Major!.Name,
db.CourseEnrollments
.Where(enrollment =>
enrollment.StudentId == x.Id &&
enrollment.Status == CourseEnrollmentStatus.Enrolled &&
enrollment.CourseSelectionOffering!.TeachingTaskId ==
teachingTaskId)
.OrderByDescending(enrollment => enrollment.EnrolledAt)
.Select(enrollment => (DateTime?)enrollment.EnrolledAt)
.FirstOrDefault()))
.ToListAsync(cancellationToken);
private async Task<TeachingTask?> FindAccessibleTaskAsync(
Guid id,
CancellationToken cancellationToken)
@@ -2001,6 +2139,22 @@ public sealed class CourseSelectionsController(
values?.Distinct().OrderBy(x => x).ToArray() ?? [];
private sealed record EnrollmentEligibility(bool IsRetake, string? Error);
private sealed record TeacherTeachingTaskInfo(
Guid Id,
string TaskNumber,
string TaskName,
string CourseCode,
string CourseName,
int Capacity);
private sealed record TeachingTaskRosterStudent(
Guid StudentId,
string StudentNumber,
string Name,
string ClassName,
string MajorName,
DateTime? EnrolledAt);
}
public sealed record CourseSelectionRoundRequest(