选课名单/课程修读情况
This commit is contained in:
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
@@ -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>
|
||||||
@@ -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
Reference in new issue
Block a user