课程修读名单导出
This commit is contained in:
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.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,51 @@ 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(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")]
|
[HttpGet("rounds")]
|
||||||
[Authorize(Roles = SelectionUsers)]
|
[Authorize(Roles = SelectionUsers)]
|
||||||
public async Task<ActionResult> GetRounds(
|
public async Task<ActionResult> GetRounds(
|
||||||
@@ -2225,6 +2271,71 @@ 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 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() =>
|
private ActionResult ProfileNotFound() =>
|
||||||
ConflictProblem("当前账号未关联有效学生档案,请联系教务管理员。");
|
ConflictProblem("当前账号未关联有效学生档案,请联系教务管理员。");
|
||||||
|
|
||||||
@@ -2294,6 +2405,35 @@ 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,
|
||||||
|
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(
|
public sealed record StudentOfferingDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
Guid TeachingTaskId,
|
Guid TeachingTaskId,
|
||||||
|
|||||||
@@ -167,6 +167,10 @@ const navigationGroups = computed<NavigationGroup[]>(() => [
|
|||||||
label: isTeachingAdmin.value ? '选课管理' : '学生选课',
|
label: isTeachingAdmin.value ? '选课管理' : '学生选课',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
...whenVisible(
|
||||||
|
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin']),
|
||||||
|
{ path: '/course-enrollments', label: '修读课程管理' },
|
||||||
|
),
|
||||||
...whenVisible(
|
...whenVisible(
|
||||||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher']),
|
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher']),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -251,6 +251,12 @@ const router = createRouter({
|
|||||||
roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student'],
|
roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'course-enrollments',
|
||||||
|
name: 'course-enrollments',
|
||||||
|
component: () => import('../views/CourseEnrollmentManagementView.vue'),
|
||||||
|
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'] },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'grades',
|
path: 'grades',
|
||||||
name: 'grades',
|
name: 'grades',
|
||||||
|
|||||||
Loaded 3 of 5 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user