Compare commits
12
Commits
v2.4.0
..
d7d2560587
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 57 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user