课程修读名单导出

This commit is contained in:
biss committed 2026-08-14 19:45:19 +08:00
1 parent 0ce758191e
commit 4575758812
5 files changed
+301 -46

No files matched your search

@@ -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,51 @@ 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(source)
.OrderByDescending(x => x.TermStartDate)
.ThenBy(x => x.CourseCode)
.ThenBy(x => x.StudentNumber)
.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(ApplyEnrollmentFilters(ScopedEnrollments(), query))
.OrderByDescending(x => x.TermStartDate)
.ThenBy(x => x.CourseCode)
.ThenBy(x => x.StudentNumber)
.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("rounds")]
[Authorize(Roles = SelectionUsers)]
public async Task<ActionResult> GetRounds(
@@ -2225,6 +2271,71 @@ 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 static 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.Status.HasValue)
source = source.Where(x => x.Status == query.Status.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 IQueryable<CourseEnrollmentListItem> ProjectEnrollments(
IQueryable<CourseEnrollment> source) => source.Select(x => new CourseEnrollmentListItem(
x.Id,
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.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 +2405,35 @@ public sealed record AdminEnrollmentRequest(
public sealed record ForceEnrollmentRequest(
[MinLength(1)] IReadOnlyCollection<Guid> StudentIds);
public sealed record CourseEnrollmentQuery(
Guid? AcademicTermId,
Guid? CourseId,
Guid? CollegeId,
CourseEnrollmentStatus? Status,
string? Keyword,
int Page = 1,
int PageSize = 20);
public sealed record CourseEnrollmentListItem(
Guid Id,
Guid CourseSelectionOfferingId,
string TermName,
DateOnly TermStartDate,
string CollegeName,
string CourseCode,
string CourseName,
string TaskNumber,
string StudentNumber,
string StudentName,
string MajorName,
string ClassName,
int Grade,
CourseEnrollmentStatus Status,
EnrollmentType EnrollmentType,
DateTime? EnrolledAt,
DateTime? WithdrawnAt,
ApprovalStatus? ExemptionStatus);
public sealed record StudentOfferingDto(
Guid Id,
Guid TeachingTaskId,
+4
View File
@@ -167,6 +167,10 @@ const navigationGroups = computed<NavigationGroup[]>(() => [
label: isTeachingAdmin.value ? '选课管理' : '学生选课',
},
),
...whenVisible(
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin']),
{ path: '/course-enrollments', label: '修读课程管理' },
),
...whenVisible(
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher']),
{
+6
View File
@@ -251,6 +251,12 @@ const router = createRouter({
roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student'],
},
},
{
path: 'course-enrollments',
name: 'course-enrollments',
component: () => import('../views/CourseEnrollmentManagementView.vue'),
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'] },
},
{
path: 'grades',
name: 'grades',
Loaded 3 of 5 files, more files were not shown because too many files have changed in this diff. Show more