选课名单/课程修读情况

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 total = await source.CountAsync(cancellationToken);
var page = Math.Max(1, query.Page); var page = Math.Max(1, query.Page);
var pageSize = Math.Clamp(query.PageSize, 1, 100); var pageSize = Math.Clamp(query.PageSize, 1, 100);
var items = await ProjectEnrollments(source) var items = await ProjectEnrollments(OrderEnrollments(source))
.OrderByDescending(x => x.TermStartDate)
.ThenBy(x => x.CourseCode)
.ThenBy(x => x.StudentNumber)
.Skip((page - 1) * pageSize) .Skip((page - 1) * pageSize)
.Take(pageSize) .Take(pageSize)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -64,10 +61,8 @@ public sealed class CourseSelectionsController(
[FromQuery] CourseEnrollmentQuery query, [FromQuery] CourseEnrollmentQuery query,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var items = await ProjectEnrollments(ApplyEnrollmentFilters(ScopedEnrollments(), query)) var items = await ProjectEnrollments(OrderEnrollments(
.OrderByDescending(x => x.TermStartDate) ApplyEnrollmentFilters(ScopedEnrollments(), query)))
.ThenBy(x => x.CourseCode)
.ThenBy(x => x.StudentNumber)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var rows = items.Select(x => (IReadOnlyList<object?>)[ var rows = items.Select(x => (IReadOnlyList<object?>)[
x.TermName, x.CollegeName, x.CourseCode, x.CourseName, x.TaskNumber, x.TermName, x.CollegeName, x.CourseCode, x.CourseName, x.TaskNumber,
@@ -83,6 +78,66 @@ public sealed class CourseSelectionsController(
$"修读课程名单-{DateTime.Now:yyyyMMdd-HHmm}.xlsx"); $"修读课程名单-{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")] [HttpGet("rounds")]
[Authorize(Roles = SelectionUsers)] [Authorize(Roles = SelectionUsers)]
public async Task<ActionResult> GetRounds( public async Task<ActionResult> GetRounds(
@@ -2282,7 +2337,7 @@ public sealed class CourseSelectionsController(
scope.RestrictedCollegeId); scope.RestrictedCollegeId);
} }
private static IQueryable<CourseEnrollment> ApplyEnrollmentFilters( private IQueryable<CourseEnrollment> ApplyEnrollmentFilters(
IQueryable<CourseEnrollment> source, IQueryable<CourseEnrollment> source,
CourseEnrollmentQuery query) CourseEnrollmentQuery query)
{ {
@@ -2295,8 +2350,27 @@ public sealed class CourseSelectionsController(
if (query.CollegeId.HasValue) if (query.CollegeId.HasValue)
source = source.Where(x => x.CourseSelectionOffering!.TeachingTask!.Course!.CollegeId == source = source.Where(x => x.CourseSelectionOffering!.TeachingTask!.Course!.CollegeId ==
query.CollegeId.Value); 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) if (query.Status.HasValue)
source = source.Where(x => x.Status == query.Status.Value); 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)) if (!string.IsNullOrWhiteSpace(query.Keyword))
{ {
var keyword = query.Keyword.Trim(); var keyword = query.Keyword.Trim();
@@ -2310,9 +2384,17 @@ public sealed class CourseSelectionsController(
return source; 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( private IQueryable<CourseEnrollmentListItem> ProjectEnrollments(
IQueryable<CourseEnrollment> source) => source.Select(x => new CourseEnrollmentListItem( IQueryable<CourseEnrollment> source) => source.Select(x => new CourseEnrollmentListItem(
x.Id, x.Id,
x.StudentId,
x.CourseSelectionOfferingId, x.CourseSelectionOfferingId,
x.CourseSelectionOffering!.CourseSelectionRound!.AcademicTerm!.Name, x.CourseSelectionOffering!.CourseSelectionRound!.AcademicTerm!.Name,
x.CourseSelectionOffering.CourseSelectionRound.AcademicTerm.StartDate, x.CourseSelectionOffering.CourseSelectionRound.AcademicTerm.StartDate,
@@ -2320,6 +2402,7 @@ public sealed class CourseSelectionsController(
x.CourseSelectionOffering.TeachingTask.Course.Code, x.CourseSelectionOffering.TeachingTask.Course.Code,
x.CourseSelectionOffering.TeachingTask.Course.Name, x.CourseSelectionOffering.TeachingTask.Course.Name,
x.CourseSelectionOffering.TeachingTask.TaskNumber, x.CourseSelectionOffering.TeachingTask.TaskNumber,
x.CourseSelectionOffering.TeachingTask.Course.Credits,
x.Student!.StudentNumber, x.Student!.StudentNumber,
x.Student.Name, x.Student.Name,
x.Student.AdministrativeClass!.Major!.Name, x.Student.AdministrativeClass!.Major!.Name,
@@ -2409,13 +2492,20 @@ public sealed record CourseEnrollmentQuery(
Guid? AcademicTermId, Guid? AcademicTermId,
Guid? CourseId, Guid? CourseId,
Guid? CollegeId, Guid? CollegeId,
Guid? MajorId,
Guid? AdministrativeClassId,
int? Grade,
int? EnrollmentYear,
CourseEnrollmentStatus? Status, CourseEnrollmentStatus? Status,
EnrollmentType? EnrollmentType,
ApprovalStatus? ExemptionStatus,
string? Keyword, string? Keyword,
int Page = 1, int Page = 1,
int PageSize = 20); int PageSize = 20);
public sealed record CourseEnrollmentListItem( public sealed record CourseEnrollmentListItem(
Guid Id, Guid Id,
Guid StudentId,
Guid CourseSelectionOfferingId, Guid CourseSelectionOfferingId,
string TermName, string TermName,
DateOnly TermStartDate, DateOnly TermStartDate,
@@ -2423,6 +2513,7 @@ public sealed record CourseEnrollmentListItem(
string CourseCode, string CourseCode,
string CourseName, string CourseName,
string TaskNumber, string TaskNumber,
decimal Credits,
string StudentNumber, string StudentNumber,
string StudentName, string StudentName,
string MajorName, string MajorName,
@@ -2434,6 +2525,25 @@ public sealed record CourseEnrollmentListItem(
DateTime? WithdrawnAt, DateTime? WithdrawnAt,
ApprovalStatus? ExemptionStatus); 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( public sealed record StudentOfferingDto(
Guid Id, Guid Id,
Guid TeachingTaskId, Guid TeachingTaskId,
+3 -3
View File
@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<JiaowuBackendVersion>2.4.1</JiaowuBackendVersion> <JiaowuBackendVersion>2.4.2-beta1</JiaowuBackendVersion>
<JiaowuFrontendVersion>2.4.1</JiaowuFrontendVersion> <JiaowuFrontendVersion>2.4.2-beta1</JiaowuFrontendVersion>
<JiaowuSwaggerVersion>2.4.1</JiaowuSwaggerVersion> <JiaowuSwaggerVersion>2.4.2-beta1</JiaowuSwaggerVersion>
</PropertyGroup> </PropertyGroup>
</Project> </Project>
+1 -8
View File
@@ -171,13 +171,6 @@ const navigationGroups = computed<NavigationGroup[]>(() => [
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin']), hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin']),
{ path: '/course-enrollments', label: '修读课程管理' }, { path: '/course-enrollments', label: '修读课程管理' },
), ),
...whenVisible(
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher']),
{
path: '/course-adjustments',
label: isTeacher.value ? '我的调停课' : '调停课审核',
},
),
], ],
}, },
{ {
@@ -247,7 +240,7 @@ const navigationGroups = computed<NavigationGroup[]>(() => [
), ),
...whenVisible( ...whenVisible(
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor', 'Teacher', 'Student']), hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor', 'Teacher', 'Student']),
{ path: '/approvals', label: '审批中心' }, { path: '/approvals', label: '审批工作台' },
), ),
...whenVisible( ...whenVisible(
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'Counselor', 'Student']), hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'Counselor', 'Student']),
Loaded 3 of 7 files, more files were not shown because too many files have changed in this diff. Show more