10 Commits
56 changed files with 1909 additions and 170 deletions

No files matched your search

@@ -37,7 +37,8 @@ public sealed class AttendanceController(
string? keyword = null,
int page = 1,
int pageSize = 20,
CancellationToken cancellationToken = default)
CancellationToken cancellationToken = default,
string? classKeyword = null)
{
page = Math.Max(1, page);
pageSize = Math.Clamp(pageSize, 10, 50);
@@ -54,6 +55,14 @@ public sealed class AttendanceController(
x.Course!.Code.Contains(keyword) ||
x.Course.Name.Contains(keyword));
}
if (!string.IsNullOrWhiteSpace(classKeyword))
{
classKeyword = classKeyword.Trim();
tasks = tasks.Where(x => x.Classes.Any(item =>
item.AdministrativeClass!.Code.Contains(classKeyword) ||
item.AdministrativeClass.Name.Contains(classKeyword) ||
item.AdministrativeClass.Major!.Name.Contains(classKeyword)));
}
var total = await tasks.CountAsync(cancellationToken);
var result = await tasks
.OrderBy(x => x.Course!.Code)
@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.Data;
using Jiaowu.Api.Contracts;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
@@ -37,6 +38,107 @@ public sealed class CourseSelectionsController(
OfferingManagers + "," +
SystemRoles.Teacher;
[HttpGet("enrollments")]
[Authorize(Roles = OfferingManagers)]
public async Task<ActionResult> GetEnrollments(
[FromQuery] CourseEnrollmentQuery query,
CancellationToken cancellationToken)
{
var source = ApplyEnrollmentFilters(ScopedEnrollments(), query);
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(OrderEnrollments(source))
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return Ok(new PagedResult<object>(items.Cast<object>().ToList(), total, page, pageSize));
}
[HttpGet("enrollments/export.xlsx")]
[Authorize(Roles = OfferingManagers)]
public async Task<ActionResult> ExportEnrollments(
[FromQuery] CourseEnrollmentQuery query,
CancellationToken cancellationToken)
{
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,
x.StudentNumber, x.StudentName, x.MajorName, x.ClassName, x.Grade,
x.Status.ToString(), x.EnrollmentType.ToString(), x.EnrolledAt,
x.WithdrawnAt, x.ExemptionStatus?.ToString() ?? "—"
]).ToList();
var bytes = ExcelWorkbookHelper.Create("修读课程名单", [
"学期", "开课院系", "课程代码", "课程名称", "教学班", "学号", "姓名",
"专业", "行政班", "年级", "修读状态", "修读类型", "选入时间", "退选时间", "免修审批"
], rows);
return File(bytes, ExcelWorkbookHelper.ContentType,
$"修读课程名单-{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 matchingStudentIds = source.Select(x => x.StudentId).Distinct();
var total = await matchingStudentIds.CountAsync(cancellationToken);
var page = Math.Max(1, query.Page);
var pageSize = Math.Clamp(query.PageSize, 1, 100);
var pageStudents = await db.Students.AsNoTracking()
.Where(x => matchingStudentIds.Contains(x.Id))
.OrderBy(x => x.StudentNumber).Skip((page - 1) * pageSize).Take(pageSize)
.Select(x => new EnrollmentStudentIdentity(
x.Id, x.StudentNumber, x.Name, x.AdministrativeClass!.Major!.Name,
x.AdministrativeClass.Name, x.AdministrativeClass.Grade))
.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(
@@ -2225,6 +2327,99 @@ public sealed class CourseSelectionsController(
: result;
}
private IQueryable<CourseEnrollment> ScopedEnrollments()
{
var source = db.CourseEnrollments.AsNoTracking();
var scope = currentUserDataScope.Current;
return scope.Scope == DataScope.All
? source
: source.Where(x =>
x.CourseSelectionOffering!.TeachingTask!.Course!.CollegeId ==
scope.RestrictedCollegeId);
}
private IQueryable<CourseEnrollment> ApplyEnrollmentFilters(
IQueryable<CourseEnrollment> source,
CourseEnrollmentQuery query)
{
if (query.AcademicTermId.HasValue)
source = source.Where(x => x.CourseSelectionOffering!.CourseSelectionRound!
.AcademicTermId == query.AcademicTermId.Value);
if (query.CourseId.HasValue)
source = source.Where(x => x.CourseSelectionOffering!.TeachingTask!.CourseId ==
query.CourseId.Value);
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();
source = source.Where(x =>
x.Student!.StudentNumber.Contains(keyword) ||
x.Student.Name.Contains(keyword) ||
x.CourseSelectionOffering!.TeachingTask!.Course!.Code.Contains(keyword) ||
x.CourseSelectionOffering.TeachingTask.Course.Name.Contains(keyword) ||
x.CourseSelectionOffering.TeachingTask.TaskNumber.Contains(keyword));
}
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,
x.CourseSelectionOffering.TeachingTask!.Course!.College!.Name,
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,
x.Student.AdministrativeClass.Name,
x.Student.AdministrativeClass.Grade,
x.Status,
x.EnrollmentType,
x.EnrolledAt,
x.WithdrawnAt,
db.CourseExemptions
.Where(exemption => exemption.StudentId == x.StudentId &&
exemption.TeachingTaskId == x.CourseSelectionOffering.TeachingTaskId)
.OrderByDescending(exemption => exemption.CreatedAt)
.Select(exemption => (ApprovalStatus?)exemption.Status)
.FirstOrDefault()));
private ActionResult ProfileNotFound() =>
ConflictProblem("当前账号未关联有效学生档案,请联系教务管理员。");
@@ -2294,6 +2489,62 @@ public sealed record AdminEnrollmentRequest(
public sealed record ForceEnrollmentRequest(
[MinLength(1)] IReadOnlyCollection<Guid> StudentIds);
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,
string CollegeName,
string CourseCode,
string CourseName,
string TaskNumber,
decimal Credits,
string StudentNumber,
string StudentName,
string MajorName,
string ClassName,
int Grade,
CourseEnrollmentStatus Status,
EnrollmentType EnrollmentType,
DateTime? EnrolledAt,
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,
@@ -323,10 +323,7 @@ public sealed class CoursesController(
if (scope.Scope == DataScope.All) return source;
if (scope.Scope == DataScope.College)
{
return source.Where(x =>
x.Nature == CourseNature.GeneralRequired ||
x.Nature == CourseNature.GeneralElective ||
x.CollegeId == scope.RestrictedCollegeId);
return source.Where(x => x.CollegeId == scope.RestrictedCollegeId);
}
if (scope.Scope == DataScope.Class)
{
Loaded 3 of 56 files, more files were not shown because too many files have changed in this diff. Show more