选课名单/课程修读情况

This commit is contained in:
biss committed 2026-08-14 21:50:14 +08:00
1 parent 4575758812
commit ef3fbf92f0
7 files changed
+241 -139

No files matched your search

@@ -48,10 +48,7 @@ public sealed class CourseSelectionsController(
var total = await source.CountAsync(cancellationToken);
var page = Math.Max(1, query.Page);
var pageSize = Math.Clamp(query.PageSize, 1, 100);
var items = await ProjectEnrollments(source)
.OrderByDescending(x => x.TermStartDate)
.ThenBy(x => x.CourseCode)
.ThenBy(x => x.StudentNumber)
var items = await ProjectEnrollments(OrderEnrollments(source))
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
@@ -64,10 +61,8 @@ public sealed class CourseSelectionsController(
[FromQuery] CourseEnrollmentQuery query,
CancellationToken cancellationToken)
{
var items = await ProjectEnrollments(ApplyEnrollmentFilters(ScopedEnrollments(), query))
.OrderByDescending(x => x.TermStartDate)
.ThenBy(x => x.CourseCode)
.ThenBy(x => x.StudentNumber)
var items = await ProjectEnrollments(OrderEnrollments(
ApplyEnrollmentFilters(ScopedEnrollments(), query)))
.ToListAsync(cancellationToken);
var rows = items.Select(x => (IReadOnlyList<object?>)[
x.TermName, x.CollegeName, x.CourseCode, x.CourseName, x.TaskNumber,
@@ -83,6 +78,66 @@ public sealed class CourseSelectionsController(
$"修读课程名单-{DateTime.Now:yyyyMMdd-HHmm}.xlsx");
}
[HttpGet("enrollments/students")]
[Authorize(Roles = OfferingManagers)]
public async Task<ActionResult> GetStudentEnrollmentSummaries(
[FromQuery] CourseEnrollmentQuery query,
CancellationToken cancellationToken)
{
var source = ApplyEnrollmentFilters(ScopedEnrollments(), query);
var students = source.Select(x => new EnrollmentStudentIdentity(
x.StudentId, x.Student!.StudentNumber, x.Student.Name,
x.Student.AdministrativeClass!.Major!.Name,
x.Student.AdministrativeClass.Name, x.Student.AdministrativeClass.Grade));
var total = await students.Distinct().CountAsync(cancellationToken);
var page = Math.Max(1, query.Page);
var pageSize = Math.Clamp(query.PageSize, 1, 100);
var pageStudents = await students.Distinct()
.OrderBy(x => x.StudentNumber).Skip((page - 1) * pageSize).Take(pageSize)
.ToListAsync(cancellationToken);
var studentIds = pageStudents.Select(x => x.StudentId).ToArray();
var enrollments = await ProjectEnrollments(OrderEnrollments(source.Where(x =>
studentIds.Contains(x.StudentId))))
.ToListAsync(cancellationToken);
var summaries = pageStudents.Select(student =>
{
var courses = enrollments.Where(x => x.StudentId == student.StudentId).ToList();
return new StudentEnrollmentSummary(
student.StudentId, student.StudentNumber, student.StudentName,
student.MajorName, student.ClassName, student.Grade, courses,
courses.Count, courses.Where(x => x.Status == CourseEnrollmentStatus.Enrolled)
.Sum(x => x.Credits));
}).ToList();
return Ok(new PagedResult<StudentEnrollmentSummary>(summaries, total, page, pageSize));
}
[HttpGet("enrollments/students/export.xlsx")]
[Authorize(Roles = OfferingManagers)]
public async Task<ActionResult> ExportStudentEnrollmentSummaries(
[FromQuery] CourseEnrollmentQuery query,
CancellationToken cancellationToken)
{
var items = await ProjectEnrollments(OrderEnrollments(
ApplyEnrollmentFilters(ScopedEnrollments(), query)))
.ToListAsync(cancellationToken);
var rows = items.GroupBy(x => new { x.StudentId, x.StudentNumber, x.StudentName,
x.MajorName, x.ClassName, x.Grade })
.OrderBy(x => x.Key.StudentNumber)
.Select(group => (IReadOnlyList<object?>)[
group.Key.StudentNumber, group.Key.StudentName, group.Key.MajorName,
group.Key.ClassName, group.Key.Grade,
string.Join(";", group.Select(x =>
$"{x.TermName} {x.CourseCode} {x.CourseName}({x.Status})")),
group.Where(x => x.Status == CourseEnrollmentStatus.Enrolled)
.Sum(x => x.Credits)
]).ToList();
var bytes = ExcelWorkbookHelper.Create("学生修读汇总", [
"学号", "姓名", "专业", "行政班", "年级", "修读课程", "在修学分"
], rows);
return File(bytes, ExcelWorkbookHelper.ContentType,
$"学生修读汇总-{DateTime.Now:yyyyMMdd-HHmm}.xlsx");
}
[HttpGet("rounds")]
[Authorize(Roles = SelectionUsers)]
public async Task<ActionResult> GetRounds(
@@ -2282,7 +2337,7 @@ public sealed class CourseSelectionsController(
scope.RestrictedCollegeId);
}
private static IQueryable<CourseEnrollment> ApplyEnrollmentFilters(
private IQueryable<CourseEnrollment> ApplyEnrollmentFilters(
IQueryable<CourseEnrollment> source,
CourseEnrollmentQuery query)
{
@@ -2295,8 +2350,27 @@ public sealed class CourseSelectionsController(
if (query.CollegeId.HasValue)
source = source.Where(x => x.CourseSelectionOffering!.TeachingTask!.Course!.CollegeId ==
query.CollegeId.Value);
if (query.MajorId.HasValue)
source = source.Where(x => x.Student!.AdministrativeClass!.MajorId ==
query.MajorId.Value);
if (query.AdministrativeClassId.HasValue)
source = source.Where(x => x.Student!.AdministrativeClassId ==
query.AdministrativeClassId.Value);
if (query.Grade.HasValue)
source = source.Where(x => x.Student!.AdministrativeClass!.Grade == query.Grade.Value);
if (query.EnrollmentYear.HasValue)
source = source.Where(x => x.Student!.EnrollmentYear == query.EnrollmentYear.Value);
if (query.Status.HasValue)
source = source.Where(x => x.Status == query.Status.Value);
if (query.EnrollmentType.HasValue)
source = source.Where(x => x.EnrollmentType == query.EnrollmentType.Value);
if (query.ExemptionStatus.HasValue)
source = source.Where(x => db.CourseExemptions
.Where(exemption => exemption.StudentId == x.StudentId &&
exemption.TeachingTaskId == x.CourseSelectionOffering!.TeachingTaskId)
.OrderByDescending(exemption => exemption.CreatedAt)
.Select(exemption => (ApprovalStatus?)exemption.Status)
.FirstOrDefault() == query.ExemptionStatus.Value);
if (!string.IsNullOrWhiteSpace(query.Keyword))
{
var keyword = query.Keyword.Trim();
@@ -2310,9 +2384,17 @@ public sealed class CourseSelectionsController(
return source;
}
private static IOrderedQueryable<CourseEnrollment> OrderEnrollments(
IQueryable<CourseEnrollment> source) => source
.OrderByDescending(x => x.CourseSelectionOffering!.CourseSelectionRound!
.AcademicTerm!.StartDate)
.ThenBy(x => x.CourseSelectionOffering!.TeachingTask!.Course!.Code)
.ThenBy(x => x.Student!.StudentNumber);
private IQueryable<CourseEnrollmentListItem> ProjectEnrollments(
IQueryable<CourseEnrollment> source) => source.Select(x => new CourseEnrollmentListItem(
x.Id,
x.StudentId,
x.CourseSelectionOfferingId,
x.CourseSelectionOffering!.CourseSelectionRound!.AcademicTerm!.Name,
x.CourseSelectionOffering.CourseSelectionRound.AcademicTerm.StartDate,
@@ -2320,6 +2402,7 @@ public sealed class CourseSelectionsController(
x.CourseSelectionOffering.TeachingTask.Course.Code,
x.CourseSelectionOffering.TeachingTask.Course.Name,
x.CourseSelectionOffering.TeachingTask.TaskNumber,
x.CourseSelectionOffering.TeachingTask.Course.Credits,
x.Student!.StudentNumber,
x.Student.Name,
x.Student.AdministrativeClass!.Major!.Name,
@@ -2409,13 +2492,20 @@ public sealed record CourseEnrollmentQuery(
Guid? AcademicTermId,
Guid? CourseId,
Guid? CollegeId,
Guid? MajorId,
Guid? AdministrativeClassId,
int? Grade,
int? EnrollmentYear,
CourseEnrollmentStatus? Status,
EnrollmentType? EnrollmentType,
ApprovalStatus? ExemptionStatus,
string? Keyword,
int Page = 1,
int PageSize = 20);
public sealed record CourseEnrollmentListItem(
Guid Id,
Guid StudentId,
Guid CourseSelectionOfferingId,
string TermName,
DateOnly TermStartDate,
@@ -2423,6 +2513,7 @@ public sealed record CourseEnrollmentListItem(
string CourseCode,
string CourseName,
string TaskNumber,
decimal Credits,
string StudentNumber,
string StudentName,
string MajorName,
@@ -2434,6 +2525,25 @@ public sealed record CourseEnrollmentListItem(
DateTime? WithdrawnAt,
ApprovalStatus? ExemptionStatus);
public sealed record EnrollmentStudentIdentity(
Guid StudentId,
string StudentNumber,
string StudentName,
string MajorName,
string ClassName,
int Grade);
public sealed record StudentEnrollmentSummary(
Guid StudentId,
string StudentNumber,
string StudentName,
string MajorName,
string ClassName,
int Grade,
IReadOnlyList<CourseEnrollmentListItem> Courses,
int CourseCount,
decimal ActiveCredits);
public sealed record StudentOfferingDto(
Guid Id,
Guid TeachingTaskId,