点名、成绩册统一读取“行政班在籍学生 ∪ 已选课学生”,不再错误显示 0 人。
教师选课名单改为展示全部本人教学班及完整学生名单,支持 Excel 导出。 调停课改用教师专用教学班接口,并在服务端限制只能操作本人教学班。
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -27,6 +27,37 @@ public sealed class CourseAdjustmentsController(
|
||||
SystemRoles.AcademicAdmin + "," +
|
||||
SystemRoles.CollegeAdmin;
|
||||
|
||||
[HttpGet("task-options")]
|
||||
[Authorize(Roles = Applicants)]
|
||||
public async Task<ActionResult> 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<TeachingTask> 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<CourseAdjustment, object>> AdjustmentProjection() => x => new
|
||||
{
|
||||
|
||||
@@ -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<ActionResult> 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<ActionResult> 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<ActionResult> 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<object?>
|
||||
{
|
||||
student.StudentNumber,
|
||||
student.Name,
|
||||
student.ClassName,
|
||||
student.MajorName,
|
||||
student.EnrolledAt.HasValue ? "选课" : "行政班关联",
|
||||
student.EnrolledAt?.ToString("yyyy-MM-dd HH:mm") ?? "-"
|
||||
}).ToList<IReadOnlyList<object?>>());
|
||||
return File(
|
||||
bytes,
|
||||
ExcelWorkbookHelper.ContentType,
|
||||
$"教学班名单-{task.TaskNumber}.xlsx");
|
||||
}
|
||||
|
||||
[HttpGet("offerings/{id:guid}/roster/export.xlsx")]
|
||||
[Authorize(Roles = RosterReaders)]
|
||||
public async Task<ActionResult> ExportRoster(
|
||||
@@ -1627,6 +1721,50 @@ public sealed class CourseSelectionsController(
|
||||
x.TeachingTask!.Course!.CollegeId == scope.RestrictedCollegeId);
|
||||
}
|
||||
|
||||
private Task<TeacherTeachingTaskInfo?> 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<List<TeachingTaskRosterStudent>> 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<TeachingTask?> 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(
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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<Student> 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));
|
||||
}
|
||||
Reference in New Issue
Block a user