Compare commits
12
Commits
v2.4.0
..
d7d2560587
No files matched your search
@@ -37,7 +37,8 @@ public sealed class AttendanceController(
|
|||||||
string? keyword = null,
|
string? keyword = null,
|
||||||
int page = 1,
|
int page = 1,
|
||||||
int pageSize = 20,
|
int pageSize = 20,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default,
|
||||||
|
string? classKeyword = null)
|
||||||
{
|
{
|
||||||
page = Math.Max(1, page);
|
page = Math.Max(1, page);
|
||||||
pageSize = Math.Clamp(pageSize, 10, 50);
|
pageSize = Math.Clamp(pageSize, 10, 50);
|
||||||
@@ -54,6 +55,14 @@ public sealed class AttendanceController(
|
|||||||
x.Course!.Code.Contains(keyword) ||
|
x.Course!.Code.Contains(keyword) ||
|
||||||
x.Course.Name.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 total = await tasks.CountAsync(cancellationToken);
|
||||||
var result = await tasks
|
var result = await tasks
|
||||||
.OrderBy(x => x.Course!.Code)
|
.OrderBy(x => x.Course!.Code)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
using Jiaowu.Api.Contracts;
|
||||||
using Jiaowu.Api.Domain.Academic;
|
using Jiaowu.Api.Domain.Academic;
|
||||||
using Jiaowu.Api.Domain.Identity;
|
using Jiaowu.Api.Domain.Identity;
|
||||||
using Jiaowu.Api.Infrastructure.Auth;
|
using Jiaowu.Api.Infrastructure.Auth;
|
||||||
@@ -37,6 +38,107 @@ public sealed class CourseSelectionsController(
|
|||||||
OfferingManagers + "," +
|
OfferingManagers + "," +
|
||||||
SystemRoles.Teacher;
|
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")]
|
[HttpGet("rounds")]
|
||||||
[Authorize(Roles = SelectionUsers)]
|
[Authorize(Roles = SelectionUsers)]
|
||||||
public async Task<ActionResult> GetRounds(
|
public async Task<ActionResult> GetRounds(
|
||||||
@@ -2225,6 +2327,99 @@ public sealed class CourseSelectionsController(
|
|||||||
: result;
|
: 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() =>
|
private ActionResult ProfileNotFound() =>
|
||||||
ConflictProblem("当前账号未关联有效学生档案,请联系教务管理员。");
|
ConflictProblem("当前账号未关联有效学生档案,请联系教务管理员。");
|
||||||
|
|
||||||
@@ -2294,6 +2489,62 @@ public sealed record AdminEnrollmentRequest(
|
|||||||
public sealed record ForceEnrollmentRequest(
|
public sealed record ForceEnrollmentRequest(
|
||||||
[MinLength(1)] IReadOnlyCollection<Guid> StudentIds);
|
[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(
|
public sealed record StudentOfferingDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
Guid TeachingTaskId,
|
Guid TeachingTaskId,
|
||||||
|
|||||||
@@ -323,10 +323,7 @@ public sealed class CoursesController(
|
|||||||
if (scope.Scope == DataScope.All) return source;
|
if (scope.Scope == DataScope.All) return source;
|
||||||
if (scope.Scope == DataScope.College)
|
if (scope.Scope == DataScope.College)
|
||||||
{
|
{
|
||||||
return source.Where(x =>
|
return source.Where(x => x.CollegeId == scope.RestrictedCollegeId);
|
||||||
x.Nature == CourseNature.GeneralRequired ||
|
|
||||||
x.Nature == CourseNature.GeneralElective ||
|
|
||||||
x.CollegeId == scope.RestrictedCollegeId);
|
|
||||||
}
|
}
|
||||||
if (scope.Scope == DataScope.Class)
|
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