From 22321abe237b1ee567f6a2f3db39f581ff4c2d04 Mon Sep 17 00:00:00 2001 From: biss Date: Mon, 27 Jul 2026 08:05:42 +0800 Subject: [PATCH] =?UTF-8?q?=E7=82=B9=E5=90=8D=E3=80=81=E6=88=90=E7=BB=A9?= =?UTF-8?q?=E5=86=8C=E7=BB=9F=E4=B8=80=E8=AF=BB=E5=8F=96=E2=80=9C=E8=A1=8C?= =?UTF-8?q?=E6=94=BF=E7=8F=AD=E5=9C=A8=E7=B1=8D=E5=AD=A6=E7=94=9F=20?= =?UTF-8?q?=E2=88=AA=20=E5=B7=B2=E9=80=89=E8=AF=BE=E5=AD=A6=E7=94=9F?= =?UTF-8?q?=E2=80=9D=EF=BC=8C=E4=B8=8D=E5=86=8D=E9=94=99=E8=AF=AF=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=200=20=E4=BA=BA=E3=80=82=20=E6=95=99=E5=B8=88?= =?UTF-8?q?=E9=80=89=E8=AF=BE=E5=90=8D=E5=8D=95=E6=94=B9=E4=B8=BA=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E5=85=A8=E9=83=A8=E6=9C=AC=E4=BA=BA=E6=95=99=E5=AD=A6?= =?UTF-8?q?=E7=8F=AD=E5=8F=8A=E5=AE=8C=E6=95=B4=E5=AD=A6=E7=94=9F=E5=90=8D?= =?UTF-8?q?=E5=8D=95=EF=BC=8C=E6=94=AF=E6=8C=81=20Excel=20=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E3=80=82=20=E8=B0=83=E5=81=9C=E8=AF=BE=E6=94=B9?= =?UTF-8?q?=E7=94=A8=E6=95=99=E5=B8=88=E4=B8=93=E7=94=A8=E6=95=99=E5=AD=A6?= =?UTF-8?q?=E7=8F=AD=E6=8E=A5=E5=8F=A3=EF=BC=8C=E5=B9=B6=E5=9C=A8=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E7=AB=AF=E9=99=90=E5=88=B6=E5=8F=AA=E8=83=BD=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E6=9C=AC=E4=BA=BA=E6=95=99=E5=AD=A6=E7=8F=AD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/AttendanceController.cs | 41 ++-- .../CourseAdjustmentsController.cs | 47 ++++- .../Controllers/CourseSelectionsController.cs | 154 +++++++++++++++ .../Controllers/GradesController.cs | 13 +- .../Teaching/TeachingTaskRosterQuery.cs | 18 ++ tests/Jiaowu.Api.Tests/MySqlMigrationTests.cs | 16 ++ .../TeachingWorkflowRosterTests.cs | 178 ++++++++++++++++++ web/src/views/CourseAdjustmentsView.vue | 27 ++- web/src/views/TeacherRosterView.vue | 66 +++---- 9 files changed, 491 insertions(+), 69 deletions(-) create mode 100644 src/Jiaowu.Api/Infrastructure/Teaching/TeachingTaskRosterQuery.cs create mode 100644 tests/Jiaowu.Api.Tests/TeachingWorkflowRosterTests.cs diff --git a/src/Jiaowu.Api/Controllers/AttendanceController.cs b/src/Jiaowu.Api/Controllers/AttendanceController.cs index 9ee60de..188c75b 100644 --- a/src/Jiaowu.Api/Controllers/AttendanceController.cs +++ b/src/Jiaowu.Api/Controllers/AttendanceController.cs @@ -7,6 +7,7 @@ using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Infrastructure.Auth; using Jiaowu.Api.Infrastructure.Excel; using Jiaowu.Api.Infrastructure.Persistence; +using Jiaowu.Api.Infrastructure.Teaching; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -48,9 +49,15 @@ public sealed class AttendanceController( TermName = x.AcademicTerm!.Name, CourseCode = x.Course!.Code, CourseName = x.Course!.Name, - StudentCount = db.CourseEnrollments.Count(enrollment => - enrollment.Status == CourseEnrollmentStatus.Enrolled && - enrollment.CourseSelectionOffering!.TeachingTaskId == x.Id), + StudentCount = db.Students.Count(student => + (student.Status == StudentStatus.Active && + x.Classes.Any(assignment => + assignment.AdministrativeClassId == + student.AdministrativeClassId)) || + db.CourseEnrollments.Any(enrollment => + enrollment.StudentId == student.Id && + enrollment.Status == CourseEnrollmentStatus.Enrolled && + enrollment.CourseSelectionOffering!.TeachingTaskId == x.Id)), SheetCount = db.AttendanceSheets.Count(sheet => sheet.TeachingTaskId == x.Id) }) @@ -104,15 +111,13 @@ public sealed class AttendanceController( .FirstOrDefaultAsync(x => x.Id == request.TeachingTaskId, cancellationToken); if (task is null) return NotFound(); - var studentIds = await db.CourseEnrollments.AsNoTracking() - .Where(x => - x.Status == CourseEnrollmentStatus.Enrolled && - x.CourseSelectionOffering!.TeachingTaskId == request.TeachingTaskId) - .Select(x => x.StudentId) - .Distinct() + var studentIds = await TeachingTaskRosterQuery + .ForTask(db, request.TeachingTaskId) + .AsNoTracking() + .Select(x => x.Id) .ToListAsync(cancellationToken); if (studentIds.Count == 0) - return ConflictProblem("该教学班没有有效选课学生。"); + return ConflictProblem("该教学班没有有效学生。"); var checkInMethod = request.CheckInMethod ?? AttendanceCheckInMethod.Manual; if (!Enum.IsDefined(checkInMethod)) @@ -1032,16 +1037,14 @@ public sealed class AttendanceController( .FirstOrDefaultAsync(cancellationToken); if (course is null) return null; - var enrolledStudents = await db.CourseEnrollments.AsNoTracking() - .Where(x => - x.Status == CourseEnrollmentStatus.Enrolled && - x.CourseSelectionOffering!.TeachingTaskId == teachingTaskId) + var enrolledStudents = await TeachingTaskRosterQuery + .ForTask(db, teachingTaskId) + .AsNoTracking() .Select(x => new AttendanceStatisticsStudentIdentity( - x.StudentId, - x.Student!.StudentNumber, - x.Student.Name, - x.Student.AdministrativeClass!.Name)) - .Distinct() + x.Id, + x.StudentNumber, + x.Name, + x.AdministrativeClass!.Name)) .ToListAsync(cancellationToken); var submittedSheets = await db.AttendanceSheets.AsNoTracking() diff --git a/src/Jiaowu.Api/Controllers/CourseAdjustmentsController.cs b/src/Jiaowu.Api/Controllers/CourseAdjustmentsController.cs index f8111be..d4ec400 100644 --- a/src/Jiaowu.Api/Controllers/CourseAdjustmentsController.cs +++ b/src/Jiaowu.Api/Controllers/CourseAdjustmentsController.cs @@ -27,6 +27,37 @@ public sealed class CourseAdjustmentsController( SystemRoles.AcademicAdmin + "," + SystemRoles.CollegeAdmin; + [HttpGet("task-options")] + [Authorize(Roles = Applicants)] + public async Task GetTaskOptions( + Guid? academicTermId, + CancellationToken cancellationToken) + { + var source = AccessibleTeachingTasks().AsNoTracking() + .Where(x => x.Status == TeachingTaskStatus.Published); + if (academicTermId.HasValue) + source = source.Where(x => x.AcademicTermId == academicTermId); + + return Ok(await source + .OrderByDescending(x => x.AcademicTerm!.StartDate) + .ThenBy(x => x.Course!.Code) + .ThenBy(x => x.TaskNumber) + .Select(x => new + { + x.Id, + x.TaskNumber, + x.Name, + x.AcademicTermId, + TermName = x.AcademicTerm!.Name, + CourseCode = x.Course!.Code, + CourseName = x.Course.Name, + ClassNames = x.Classes + .OrderBy(item => item.AdministrativeClass!.Code) + .Select(item => item.AdministrativeClass!.Name) + }) + .ToListAsync(cancellationToken)); + } + // ═══════════════ My adjustments ═══════════════ [HttpGet("mine")] @@ -399,7 +430,7 @@ public sealed class CourseAdjustmentsController( CourseAdjustmentRequest request, CancellationToken cancellationToken) { - var task = await db.TeachingTasks.AsNoTracking() + var task = await AccessibleTeachingTasks().AsNoTracking() .Include(x => x.Course) .FirstOrDefaultAsync(x => x.Id == request.TeachingTaskId && @@ -438,6 +469,20 @@ public sealed class CourseAdjustmentsController( return null; } + private IQueryable AccessibleTeachingTasks() + { + var source = db.TeachingTasks.AsQueryable(); + var scope = currentUserDataScope.Current; + if (scope.Scope == DataScope.All) return source; + if (scope.Scope == DataScope.College) + return source.Where(x => + x.Course!.CollegeId == scope.RestrictedCollegeId); + if (scope.IsInRole(SystemRoles.Teacher)) + return source.Where(x => + x.Teachers.Any(item => item.Teacher!.UserId == scope.UserId)); + return source.Where(_ => false); + } + private static System.Linq.Expressions.Expression< Func> AdjustmentProjection() => x => new { diff --git a/src/Jiaowu.Api/Controllers/CourseSelectionsController.cs b/src/Jiaowu.Api/Controllers/CourseSelectionsController.cs index 056ab72..8b5cd39 100644 --- a/src/Jiaowu.Api/Controllers/CourseSelectionsController.cs +++ b/src/Jiaowu.Api/Controllers/CourseSelectionsController.cs @@ -6,6 +6,7 @@ using Jiaowu.Api.Infrastructure.Auth; using Jiaowu.Api.Infrastructure.CourseSelection; using Jiaowu.Api.Infrastructure.Excel; using Jiaowu.Api.Infrastructure.Persistence; +using Jiaowu.Api.Infrastructure.Teaching; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -1561,6 +1562,99 @@ public sealed class CourseSelectionsController( return Ok(offerings); } + [HttpGet("my-teaching-tasks")] + [Authorize(Roles = SystemRoles.Teacher)] + public async Task GetMyTeachingTasks( + Guid? academicTermId, + CancellationToken cancellationToken) + { + var userId = currentUserDataScope.Current.UserId; + var source = db.TeachingTasks.AsNoTracking() + .Where(x => + (x.Status == TeachingTaskStatus.Published || + x.Status == TeachingTaskStatus.Closed) && + x.Teachers.Any(item => item.Teacher!.UserId == userId)); + if (academicTermId.HasValue) + source = source.Where(x => x.AcademicTermId == academicTermId); + + return Ok(await source + .OrderByDescending(x => x.AcademicTerm!.StartDate) + .ThenBy(x => x.Course!.Code) + .ThenBy(x => x.TaskNumber) + .Select(x => new + { + x.Id, + x.TaskNumber, + TaskName = x.Name, + x.AcademicTermId, + TermName = x.AcademicTerm!.Name, + CourseCode = x.Course!.Code, + CourseName = x.Course.Name, + x.Capacity, + StudentCount = db.Students.Count(student => + (student.Status == StudentStatus.Active && + x.Classes.Any(assignment => + assignment.AdministrativeClassId == + student.AdministrativeClassId)) || + db.CourseEnrollments.Any(enrollment => + enrollment.StudentId == student.Id && + enrollment.Status == CourseEnrollmentStatus.Enrolled && + enrollment.CourseSelectionOffering!.TeachingTaskId == x.Id)) + }) + .ToListAsync(cancellationToken)); + } + + [HttpGet("teaching-tasks/{id:guid}/roster")] + [Authorize(Roles = SystemRoles.Teacher)] + public async Task GetTeachingTaskRoster( + Guid id, + CancellationToken cancellationToken) + { + var task = await GetAssignedTeachingTaskAsync(id, cancellationToken); + if (task is null) return NotFound(); + + var students = await LoadTeachingTaskRosterAsync(id, cancellationToken); + return Ok(new + { + task.Id, + task.TaskNumber, + task.TaskName, + task.CourseCode, + task.CourseName, + task.Capacity, + StudentCount = students.Count, + Students = students + }); + } + + [HttpGet("teaching-tasks/{id:guid}/roster/export.xlsx")] + [Authorize(Roles = SystemRoles.Teacher)] + public async Task ExportTeachingTaskRoster( + Guid id, + CancellationToken cancellationToken) + { + var task = await GetAssignedTeachingTaskAsync(id, cancellationToken); + if (task is null) return NotFound(); + + var students = await LoadTeachingTaskRosterAsync(id, cancellationToken); + var bytes = ExcelWorkbookHelper.Create( + "教学班名单", + ["学号", "姓名", "班级", "专业", "进入方式", "选课时间"], + students.Select(student => new List + { + student.StudentNumber, + student.Name, + student.ClassName, + student.MajorName, + student.EnrolledAt.HasValue ? "选课" : "行政班关联", + student.EnrolledAt?.ToString("yyyy-MM-dd HH:mm") ?? "-" + }).ToList>()); + return File( + bytes, + ExcelWorkbookHelper.ContentType, + $"教学班名单-{task.TaskNumber}.xlsx"); + } + [HttpGet("offerings/{id:guid}/roster/export.xlsx")] [Authorize(Roles = RosterReaders)] public async Task ExportRoster( @@ -1627,6 +1721,50 @@ public sealed class CourseSelectionsController( x.TeachingTask!.Course!.CollegeId == scope.RestrictedCollegeId); } + private Task GetAssignedTeachingTaskAsync( + Guid id, + CancellationToken cancellationToken) + { + var userId = currentUserDataScope.Current.UserId; + return db.TeachingTasks.AsNoTracking() + .Where(x => + x.Id == id && + (x.Status == TeachingTaskStatus.Published || + x.Status == TeachingTaskStatus.Closed) && + x.Teachers.Any(item => item.Teacher!.UserId == userId)) + .Select(x => new TeacherTeachingTaskInfo( + x.Id, + x.TaskNumber, + x.Name, + x.Course!.Code, + x.Course.Name, + x.Capacity)) + .FirstOrDefaultAsync(cancellationToken); + } + + private Task> LoadTeachingTaskRosterAsync( + Guid teachingTaskId, + CancellationToken cancellationToken) => + TeachingTaskRosterQuery.ForTask(db, teachingTaskId) + .AsNoTracking() + .OrderBy(x => x.StudentNumber) + .Select(x => new TeachingTaskRosterStudent( + x.Id, + x.StudentNumber, + x.Name, + x.AdministrativeClass!.Name, + x.AdministrativeClass.Major!.Name, + db.CourseEnrollments + .Where(enrollment => + enrollment.StudentId == x.Id && + enrollment.Status == CourseEnrollmentStatus.Enrolled && + enrollment.CourseSelectionOffering!.TeachingTaskId == + teachingTaskId) + .OrderByDescending(enrollment => enrollment.EnrolledAt) + .Select(enrollment => (DateTime?)enrollment.EnrolledAt) + .FirstOrDefault())) + .ToListAsync(cancellationToken); + private async Task FindAccessibleTaskAsync( Guid id, CancellationToken cancellationToken) @@ -2001,6 +2139,22 @@ public sealed class CourseSelectionsController( values?.Distinct().OrderBy(x => x).ToArray() ?? []; private sealed record EnrollmentEligibility(bool IsRetake, string? Error); + + private sealed record TeacherTeachingTaskInfo( + Guid Id, + string TaskNumber, + string TaskName, + string CourseCode, + string CourseName, + int Capacity); + + private sealed record TeachingTaskRosterStudent( + Guid StudentId, + string StudentNumber, + string Name, + string ClassName, + string MajorName, + DateTime? EnrolledAt); } public sealed record CourseSelectionRoundRequest( diff --git a/src/Jiaowu.Api/Controllers/GradesController.cs b/src/Jiaowu.Api/Controllers/GradesController.cs index 249d5d5..7122f85 100644 --- a/src/Jiaowu.Api/Controllers/GradesController.cs +++ b/src/Jiaowu.Api/Controllers/GradesController.cs @@ -6,6 +6,7 @@ using Jiaowu.Api.Infrastructure.Auth; using Jiaowu.Api.Infrastructure.Excel; using Jiaowu.Api.Infrastructure.Grades; using Jiaowu.Api.Infrastructure.Persistence; +using Jiaowu.Api.Infrastructure.Teaching; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -134,15 +135,13 @@ public sealed class GradesController( cancellationToken)) return ConflictProblem("该教学班已经建立成绩单。"); - var studentIds = await db.CourseEnrollments.AsNoTracking() - .Where(x => - x.Status == CourseEnrollmentStatus.Enrolled && - x.CourseSelectionOffering!.TeachingTaskId == task.Id) - .Select(x => x.StudentId) - .Distinct() + var studentIds = await TeachingTaskRosterQuery + .ForTask(db, task.Id) + .AsNoTracking() + .Select(x => x.Id) .ToListAsync(cancellationToken); if (studentIds.Count == 0) - return ConflictProblem("教学班当前没有有效选课学生,无法建立成绩单。"); + return ConflictProblem("教学班当前没有有效学生,无法建立成绩单。"); var sheet = new GradeSheet { diff --git a/src/Jiaowu.Api/Infrastructure/Teaching/TeachingTaskRosterQuery.cs b/src/Jiaowu.Api/Infrastructure/Teaching/TeachingTaskRosterQuery.cs new file mode 100644 index 0000000..f2c6f77 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Teaching/TeachingTaskRosterQuery.cs @@ -0,0 +1,18 @@ +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Infrastructure.Persistence; + +namespace Jiaowu.Api.Infrastructure.Teaching; + +public static class TeachingTaskRosterQuery +{ + public static IQueryable ForTask(AppDbContext db, Guid teachingTaskId) => + db.Students.Where(student => + (student.Status == StudentStatus.Active && + db.TeachingTaskClasses.Any(assignment => + assignment.TeachingTaskId == teachingTaskId && + assignment.AdministrativeClassId == student.AdministrativeClassId)) || + db.CourseEnrollments.Any(enrollment => + enrollment.StudentId == student.Id && + enrollment.Status == CourseEnrollmentStatus.Enrolled && + enrollment.CourseSelectionOffering!.TeachingTaskId == teachingTaskId)); +} diff --git a/tests/Jiaowu.Api.Tests/MySqlMigrationTests.cs b/tests/Jiaowu.Api.Tests/MySqlMigrationTests.cs index f2a864b..526be8e 100644 --- a/tests/Jiaowu.Api.Tests/MySqlMigrationTests.cs +++ b/tests/Jiaowu.Api.Tests/MySqlMigrationTests.cs @@ -1,4 +1,5 @@ using Jiaowu.Api.Infrastructure.Persistence; +using Jiaowu.Api.Infrastructure.Teaching; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -98,6 +99,21 @@ public sealed class MySqlMigrationTests Assert.Contains(ids[1].ToString(), sql, StringComparison.OrdinalIgnoreCase); } + [Fact] + public void MySql_teaching_task_roster_query_is_translatable() + { + using var db = new AppDbContext(CreateMySqlOptions()); + + var sql = TeachingTaskRosterQuery + .ForTask( + db, + Guid.Parse("11111111-1111-1111-1111-111111111111")) + .ToQueryString(); + + Assert.Contains("TeachingTaskClasses", sql, StringComparison.OrdinalIgnoreCase); + Assert.Contains("CourseEnrollments", sql, StringComparison.OrdinalIgnoreCase); + } + [Fact] public void MySql_index_names_fit_the_server_identifier_limit() { diff --git a/tests/Jiaowu.Api.Tests/TeachingWorkflowRosterTests.cs b/tests/Jiaowu.Api.Tests/TeachingWorkflowRosterTests.cs new file mode 100644 index 0000000..9ea1c02 --- /dev/null +++ b/tests/Jiaowu.Api.Tests/TeachingWorkflowRosterTests.cs @@ -0,0 +1,178 @@ +using Jiaowu.Api.Controllers; +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Infrastructure.Auth; +using Jiaowu.Api.Infrastructure.Persistence; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace Jiaowu.Api.Tests; + +public sealed class TeachingWorkflowRosterTests +{ + [Fact] + public async Task ClassAssignedStudents_AppearAcrossTeacherWorkflows() + { + await using var connection = new SqliteConnection("Data Source=:memory:"); + await connection.OpenAsync(); + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + await using var db = new AppDbContext(options); + await db.Database.EnsureCreatedAsync(); + + var teacherUserId = Guid.NewGuid(); + var teacherUser = new ApplicationUser + { + Id = teacherUserId, + UserName = "T001", + NormalizedUserName = "T001", + DisplayName = "张老师" + }; + var college = new College { Code = "CS", Name = "计算机学院" }; + var major = new Major + { + Code = "080901", + Name = "计算机科学与技术", + CollegeId = college.Id, + DegreeType = "工学学士" + }; + var administrativeClass = new AdministrativeClass + { + Code = "CS2026-01", + Name = "计科 2026-1 班", + MajorId = major.Id, + Grade = 2026 + }; + var student = new Student + { + StudentNumber = "202601001", + Name = "周同学", + AdministrativeClassId = administrativeClass.Id, + EnrollmentYear = 2026, + EnrollmentDate = new DateOnly(2026, 9, 1) + }; + var teacher = new Teacher + { + TeacherNumber = "T001", + Name = "张老师", + CollegeId = college.Id, + UserId = teacherUserId + }; + var course = new Course + { + Code = "CS101", + Name = "程序设计基础", + CollegeId = college.Id, + Credits = 4, + TotalHours = 64, + LectureHours = 48, + PracticeHours = 16, + Nature = CourseNature.MajorRequired, + AssessmentMethod = AssessmentMethod.Examination + }; + var term = new AcademicTerm + { + Code = "2026-1", + Name = "2026—2027 学年第一学期", + AcademicYear = "2026-2027", + Season = TermSeason.Autumn, + StartDate = new DateOnly(2026, 9, 1), + EndDate = new DateOnly(2027, 1, 20) + }; + var task = new TeachingTask + { + TaskNumber = "2026-1-CS101-01", + Name = "程序设计基础教学班", + AcademicTermId = term.Id, + CourseId = course.Id, + Capacity = 60, + Status = TeachingTaskStatus.Published, + Teachers = + [ + new TeachingTaskTeacher + { + TeacherId = teacher.Id, + IsPrimary = true + } + ], + Classes = + [ + new TeachingTaskClass + { + AdministrativeClassId = administrativeClass.Id + } + ] + }; + db.AddRange( + teacherUser, + college, + major, + administrativeClass, + student, + teacher, + course, + term, + task); + await db.SaveChangesAsync(); + + var scope = new TeacherDataScope(teacherUserId); + var attendance = new AttendanceController(db, scope); + var attendanceResult = await attendance.CreateSheet( + new AttendanceSheetRequest( + task.Id, + "第1周点名", + new DateTime(2026, 9, 3), + null, + AttendanceCheckInMethod.Manual, + null, + null, + null, + null), + CancellationToken.None); + Assert.IsType(attendanceResult); + Assert.Equal(1, await db.AttendanceRecords.CountAsync()); + + var grades = new GradesController(db, scope); + var gradeResult = await grades.CreateSheet( + new GradeSheetRequest(task.Id, 40, 60, []), + CancellationToken.None); + Assert.IsType(gradeResult); + Assert.Equal(1, await db.GradeRecords.CountAsync()); + + var selections = new CourseSelectionsController(db, scope); + Assert.Single(ReadItems(await selections.GetMyTeachingTasks( + term.Id, + CancellationToken.None))); + var roster = Assert.IsType( + await selections.GetTeachingTaskRoster(task.Id, CancellationToken.None)); + Assert.Equal(1, ReadIntProperty(roster.Value!, "StudentCount")); + + var adjustments = new CourseAdjustmentsController(db, scope); + Assert.Single(ReadItems(await adjustments.GetTaskOptions( + term.Id, + CancellationToken.None))); + } + + private static List ReadItems(ActionResult result) + { + var ok = Assert.IsType(result); + return Assert.IsAssignableFrom(ok.Value) + .Cast() + .ToList(); + } + + private static int ReadIntProperty(object value, string name) => + (int)value.GetType().GetProperty(name)!.GetValue(value)!; + + private sealed class TeacherDataScope(Guid userId) : ICurrentUserDataScope + { + public CurrentUserScope Current { get; } = new( + userId, + "张老师", + null, + DataScope.Self, + new HashSet([SystemRoles.Teacher])); + } +} diff --git a/web/src/views/CourseAdjustmentsView.vue b/web/src/views/CourseAdjustmentsView.vue index 95e032e..28caa44 100644 --- a/web/src/views/CourseAdjustmentsView.vue +++ b/web/src/views/CourseAdjustmentsView.vue @@ -101,6 +101,18 @@ async function submit() { } catch (e) { ElMessage.error(apiErrorMessage(e)) } } +async function loadTaskOptions() { + if (!isTeacher.value && isManager.value) return + try { + tasks.value = (await http.get('/course-adjustments/task-options', { + params: { academicTermId: termId.value || undefined }, + })).data + } catch (error) { + tasks.value = [] + ElMessage.error(apiErrorMessage(error)) + } +} + async function doSubmit(adj: any) { try { await http.post(`/course-adjustments/${adj.id}/submit`) @@ -137,16 +149,13 @@ async function doReject() { onMounted(async () => { try { - const [termRes, taskRes] = await Promise.all([ - http.get('/base-data/terms'), - isTeacher.value || !isManager.value - ? http.get('/teaching-tasks', { params: { page: 1, pageSize: 100 } }) - : Promise.resolve({ data: { items: [] } }), - ]) + const termRes = await http.get('/base-data/terms') terms.value = termRes.data - tasks.value = taskRes.data.items termId.value = defaultAcademicTermId(terms.value) - } catch (_) {} + await loadTaskOptions() + } catch (error) { + ElMessage.error(apiErrorMessage(error)) + } await load() }) @@ -173,7 +182,7 @@ function showCancel(type: string) { return type === 'Cancel' }
- + ENROLLMENT ROSTER

选课名单

-

查看本人授课班级的选课学生名单,支持 Excel 导出。

+

查看本人授课教学班的完整学生名单,支持 Excel 导出。

- 刷新 + 刷新
- + - 共 {{ offerings.length }} 个教学班 + 共 {{ tasks.length }} 个教学班
- +