课程/教学班归档

This commit is contained in:
biss committed 2026-09-12 10:32:06 +08:00
1 parent df87aa16d3
commit e165964a77
6 files changed
+111 -27

No files matched your search

@@ -44,7 +44,8 @@ public sealed class AttendanceController(
page = Math.Max(1, page);
pageSize = Math.Clamp(pageSize, 10, 50);
var tasks = AccessibleTasks().AsNoTracking()
.Where(x => x.Status == TeachingTaskStatus.Published);
.Where(x => x.Status == TeachingTaskStatus.Published ||
x.Status == TeachingTaskStatus.Closed);
if (academicTermId.HasValue)
tasks = tasks.Where(x => x.AcademicTermId == academicTermId);
if (!string.IsNullOrWhiteSpace(keyword))
@@ -73,6 +74,7 @@ public sealed class AttendanceController(
.Select(x => new
{
x.Id,
x.Status,
x.TaskNumber,
x.Name,
x.AcademicTermId,
@@ -162,6 +164,8 @@ public sealed class AttendanceController(
var task = await AccessibleTasks().AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == request.TeachingTaskId, cancellationToken);
if (task is null) return NotFound();
if (!IsTaskOperable(task))
return ConflictProblem("教学任务已归档,不能新建考勤表。");
var studentIds = await TeachingTaskRosterQuery
.ForTask(db, request.TeachingTaskId)
@@ -258,6 +262,7 @@ public sealed class AttendanceController(
x.LocationRadiusMeters,
x.Notes,
x.SubmittedAt,
TaskStatus = x.TeachingTask!.Status,
TaskNumber = x.TeachingTask!.TaskNumber,
TaskName = x.TeachingTask.Name,
CourseCode = x.TeachingTask.Course!.Code,
@@ -295,7 +300,8 @@ public sealed class AttendanceController(
var canManage = await CanManageTaskAsync(
sheet.TeachingTaskId,
cancellationToken);
var canEdit = sheet.Status == AttendanceSheetStatus.Draft && canManage;
var canEdit = sheet.Status == AttendanceSheetStatus.Draft &&
sheet.TaskStatus == TeachingTaskStatus.Published && canManage;
var now = DateTime.UtcNow;
var attempts = await db.AttendanceCheckInAttempts.AsNoTracking()
.Where(x => x.AttendanceSheetId == id)
@@ -758,6 +764,7 @@ public sealed class AttendanceController(
.Where(x =>
x.StudentId == studentId.Value &&
x.AttendanceSheet!.Status == AttendanceSheetStatus.Draft &&
x.AttendanceSheet.TeachingTask!.Status == TeachingTaskStatus.Published &&
x.AttendanceSheet.CheckInMethod == AttendanceCheckInMethod.Location &&
x.AttendanceSheet.CheckInStartsAt <= now &&
x.AttendanceSheet.CheckInEndsAt >= now)
@@ -791,6 +798,7 @@ public sealed class AttendanceController(
var source = db.AttendanceRecords
.Include(x => x.AttendanceSheet)
.ThenInclude(x => x!.TeachingTask)
.Where(x => x.StudentId == studentId.Value);
if (!string.IsNullOrWhiteSpace(request.Token))
{
@@ -813,6 +821,8 @@ public sealed class AttendanceController(
var record = await source.FirstOrDefaultAsync(cancellationToken);
if (record?.AttendanceSheet is null) return NotFound();
var sheet = record.AttendanceSheet;
if (!IsTaskOperable(sheet.TeachingTask!))
return ConflictProblem("教学任务已归档,签到活动已停止。");
var now = DateTime.UtcNow;
async Task<ActionResult> RejectAttemptAsync(
@@ -1011,11 +1021,15 @@ public sealed class AttendanceController(
return ConflictProblem("当前账号未关联学生档案。");
var record = await db.AttendanceRecords
.Include(r => r.AttendanceSheet)
.ThenInclude(s => s!.TeachingTask)
.FirstOrDefaultAsync(r =>
r.AttendanceSheetId == request.AttendanceSheetId &&
r.StudentId == studentId.Value,
cancellationToken);
if (record is null) return NotFound();
if (!IsTaskOperable(record.AttendanceSheet!.TeachingTask!))
return ConflictProblem("教学任务已归档,不能提交考勤申诉。");
if (record.AppealStatus == AttendanceAppealStatus.Pending)
return ConflictProblem("已有申诉正在处理中。");
if (record.AppealStatus == AttendanceAppealStatus.Approved)
@@ -1202,10 +1216,14 @@ public sealed class AttendanceController(
}
private bool CanManageSheet(AttendanceSheet sheet) =>
currentUserDataScope.Current.IsInRole(SystemRoles.SuperAdmin) ||
IsTaskOperable(sheet.TeachingTask!) &&
(currentUserDataScope.Current.IsInRole(SystemRoles.SuperAdmin) ||
currentUserDataScope.Current.IsInRole(SystemRoles.AcademicAdmin) ||
sheet.TeachingTask!.Teachers.Any(x =>
x.Teacher?.UserId == currentUserDataScope.Current.UserId);
x.Teacher?.UserId == currentUserDataScope.Current.UserId));
private static bool IsTaskOperable(TeachingTask task) =>
task.Status == TeachingTaskStatus.Published;
private async Task<bool> CanManageTaskAsync(
Guid teachingTaskId,
@@ -89,6 +89,7 @@ public sealed class ExperimentGradesController(
.GroupBy(x => new
{
x.TeachingTaskId,
TaskStatus = x.TeachingTask!.Status,
StartDate = x.TeachingTask!.AcademicTerm!.StartDate,
x.TeachingTask.TaskNumber
})
@@ -114,6 +115,7 @@ public sealed class ExperimentGradesController(
x.ScheduleWeek,
ProjectStatus = x.Status,
x.TeachingTaskId,
TaskStatus = x.TeachingTask!.Status,
x.TeachingTask!.TaskNumber,
TaskName = x.TeachingTask.Name,
AcademicTermId = x.TeachingTask.AcademicTermId,
@@ -567,7 +569,8 @@ public sealed class ExperimentGradesController(
.SingleAsync(
x => x.Id == sheet.TeachingTaskId,
cancellationToken);
var canReview = CanReviewCollege(sheet.CourseCollegeId) &&
var isTaskOperable = IsTaskOperable(task);
var canReview = isTaskOperable && CanReviewCollege(sheet.CourseCollegeId) &&
sheet.Status ==
ExperimentGradeSheetStatus.Submitted;
return Ok(new
@@ -603,12 +606,12 @@ public sealed class ExperimentGradesController(
sheet.CreatedAt,
sheet.UpdatedAt
},
CanEdit = (CanEdit(task) || isBoundInstructor) &&
CanEdit = isTaskOperable && (CanEdit(task) || isBoundInstructor) &&
sheet.Status is
ExperimentGradeSheetStatus.Draft or
ExperimentGradeSheetStatus.Returned,
CanReview = canReview,
CanPublish = IsPublisher() &&
CanPublish = isTaskOperable && IsPublisher() &&
sheet.Status == ExperimentGradeSheetStatus.Approved
});
}
@@ -1074,6 +1077,8 @@ public sealed class ExperimentGradesController(
{
var sheet = await LoadForWorkflowAsync(id, cancellationToken);
if (sheet is null) return NotFound();
if (!IsTaskOperable(sheet.ExperimentProject!.TeachingTask!))
return TaskArchivedProblem();
var collegeId = sheet.ExperimentProject!.TeachingTask!.Course!.CollegeId;
if (!CanReviewCollege(collegeId)) return Forbid();
if (sheet.Status != ExperimentGradeSheetStatus.Submitted)
@@ -1100,6 +1105,8 @@ public sealed class ExperimentGradesController(
{
var sheet = await LoadForWorkflowAsync(id, cancellationToken);
if (sheet is null) return NotFound();
if (!IsTaskOperable(sheet.ExperimentProject!.TeachingTask!))
return TaskArchivedProblem();
var collegeId = sheet.ExperimentProject!.TeachingTask!.Course!.CollegeId;
if (!CanReviewCollege(collegeId)) return Forbid();
if (sheet.Status != ExperimentGradeSheetStatus.Submitted)
@@ -1127,6 +1134,8 @@ public sealed class ExperimentGradesController(
{
var sheet = await LoadForWorkflowAsync(id, cancellationToken);
if (sheet is null) return NotFound();
if (!IsTaskOperable(sheet.ExperimentProject!.TeachingTask!))
return TaskArchivedProblem();
if (!IsPublisher()) return Forbid();
if (sheet.Status != ExperimentGradeSheetStatus.Approved)
return ConflictProblem("只有审核通过的实验成绩单可以发布。");
@@ -1291,21 +1300,23 @@ public sealed class ExperimentGradesController(
}
private bool CanInitialize(TeachingTask task) =>
currentUserDataScope.Current.IsInRole(SystemRoles.SuperAdmin) ||
IsTaskOperable(task) &&
(currentUserDataScope.Current.IsInRole(SystemRoles.SuperAdmin) ||
currentUserDataScope.Current.IsInRole(SystemRoles.AcademicAdmin) ||
currentUserDataScope.Current.IsInRole(SystemRoles.CollegeAdmin) ||
IsAssignedTeacher(task);
IsAssignedTeacher(task));
private bool CanEdit(TeachingTask task) =>
currentUserDataScope.Current.IsInRole(SystemRoles.SuperAdmin) ||
IsAssignedTeacher(task);
IsTaskOperable(task) && (currentUserDataScope.Current.IsInRole(SystemRoles.SuperAdmin) ||
IsAssignedTeacher(task));
private bool CanEditBoundSessionRecords(
ExperimentGradeSheet sheet,
IReadOnlyCollection<ExperimentGradeRecordRequest> requestedRecords)
{
var scope = currentUserDataScope.Current;
if (sheet.ExperimentProject!.ArrangementMode != ExperimentArrangementMode.SelfScheduled ||
if (!IsTaskOperable(sheet.ExperimentProject!.TeachingTask!) ||
sheet.ExperimentProject!.ArrangementMode != ExperimentArrangementMode.SelfScheduled ||
scope.Scope != DataScope.Self || !scope.IsInRole(SystemRoles.Teacher) ||
requestedRecords.Count == 0)
return false;
@@ -1320,6 +1331,12 @@ public sealed class ExperimentGradesController(
task.Teachers.Any(x =>
x.Teacher?.UserId == currentUserDataScope.Current.UserId);
private static bool IsTaskOperable(TeachingTask task) =>
task.Status == TeachingTaskStatus.Published;
private ActionResult TaskArchivedProblem() =>
ConflictProblem("教学任务已归档,实验成绩仅可查看或导出,不能再操作。");
private bool CanReviewCollege(Guid collegeId)
{
var scope = currentUserDataScope.Current;
@@ -272,6 +272,7 @@ public sealed class ExperimentsController(
x.Status,
x.PublishedAt,
x.ClosedAt,
TeachingTaskStatus = x.TeachingTask!.Status,
AcademicTermId = x.TeachingTask!.AcademicTermId,
TermName = x.TeachingTask.AcademicTerm!.Name,
TaskNumber = x.TeachingTask.TaskNumber,
@@ -368,6 +369,7 @@ public sealed class ExperimentsController(
x.SelectionStartsAt,
x.SelectionEndsAt,
x.Status,
TeachingTaskStatus = x.TeachingTask!.Status,
AcademicTermId = x.TeachingTask!.AcademicTermId,
TermName = x.TeachingTask.AcademicTerm!.Name,
TaskNumber = x.TeachingTask.TaskNumber,
@@ -687,6 +689,8 @@ public sealed class ExperimentsController(
.ThenInclude(x => x!.AcademicTerm)
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
if (project is null) return NotFound();
if (!IsTaskOperable(project.TeachingTask!))
return TaskArchivedProblem();
if (project.Status != ExperimentProjectStatus.Draft)
return ConflictProblem("只有草稿实验项目可以修改。");
if (project.TeachingTaskId != request.TeachingTaskId)
@@ -729,8 +733,11 @@ public sealed class ExperimentsController(
CancellationToken cancellationToken)
{
var project = await ScopedProjects()
.Include(x => x.TeachingTask)
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
if (project is null) return NotFound();
if (!IsTaskOperable(project.TeachingTask!))
return TaskArchivedProblem();
if (project.Status != ExperimentProjectStatus.Draft)
return ConflictProblem("只有草稿实验项目可以删除。");
@@ -754,6 +761,8 @@ public sealed class ExperimentsController(
.ThenInclude(x => x!.Course)
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
if (project is null) return NotFound();
if (!IsTaskOperable(project.TeachingTask!))
return TaskArchivedProblem();
if (project.Status != ExperimentProjectStatus.Published)
return ConflictProblem("只有已发布实验项目可以修正教学内容。");
if (!CanCorrectPublishedProject(project.TeachingTask!)) return Forbid();
@@ -786,11 +795,14 @@ public sealed class ExperimentsController(
return ValidationProblem("请为每个已选实验项目填写一个名称,每行一个且数量必须一致。");
var projects = await ScopedProjects()
.Include(x => x.TeachingTask)
.Where(x => ids.Contains(x.Id))
.ToListAsync(cancellationToken);
if (projects.Count != ids.Count) return NotFound();
if (projects.Any(x => x.Status != ExperimentProjectStatus.Draft))
return ConflictProblem("批量修改名称只能包含草稿实验项目。");
if (projects.Any(x => !IsTaskOperable(x.TeachingTask!)))
return TaskArchivedProblem();
var projectsById = projects.ToDictionary(x => x.Id);
for (var index = 0; index < ids.Count; index++)
@@ -809,11 +821,14 @@ public sealed class ExperimentsController(
var ids = ValidateBulkProjectIds(request.ProjectIds);
if (ids is null) return ValidationProblem("请选择 1 至 100 个实验项目。");
var projects = await ScopedProjects()
.Include(x => x.TeachingTask)
.Where(x => ids.Contains(x.Id))
.ToListAsync(cancellationToken);
if (projects.Count != ids.Count) return NotFound();
if (projects.Any(x => x.Status != ExperimentProjectStatus.Draft))
return ConflictProblem("批量删除只能包含草稿实验项目。");
if (projects.Any(x => !IsTaskOperable(x.TeachingTask!)))
return TaskArchivedProblem();
db.ExperimentProjects.RemoveRange(projects);
await db.SaveChangesAsync(cancellationToken);
@@ -833,6 +848,8 @@ public sealed class ExperimentsController(
.ThenInclude(x => x!.Course)
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
if (project is null) return NotFound();
if (!IsTaskOperable(project.TeachingTask!))
return TaskArchivedProblem();
if (project.Status != ExperimentProjectStatus.Draft)
return ConflictProblem("只有草稿实验项目可以发布。");
var hasSchedule = project.ArrangementMode == ExperimentArrangementMode.Centralized
@@ -872,8 +889,14 @@ public sealed class ExperimentsController(
{
var ids = ValidateBulkProjectIds(request.ProjectIds);
if (ids is null) return ValidationProblem("请选择 1 至 100 个实验项目。");
if (await ScopedProjects().CountAsync(x => ids.Contains(x.Id), cancellationToken) != ids.Count)
var projects = await ScopedProjects().AsNoTracking()
.Where(x => ids.Contains(x.Id))
.Select(x => new { x.Id, x.TeachingTask!.Status })
.ToListAsync(cancellationToken);
if (projects.Count != ids.Count)
return NotFound();
if (projects.Any(x => x.Status != TeachingTaskStatus.Published))
return TaskArchivedProblem();
var userId = currentUserDataScope.Current.UserId;
var job = new ExamPublishJob
{
@@ -908,8 +931,11 @@ public sealed class ExperimentsController(
CancellationToken cancellationToken)
{
var project = await ScopedProjects()
.Include(x => x.TeachingTask)
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
if (project is null) return NotFound();
if (!IsTaskOperable(project.TeachingTask!))
return TaskArchivedProblem();
if (project.Status != ExperimentProjectStatus.Published)
return ConflictProblem("只有已发布实验项目可以关闭。");
project.Status = ExperimentProjectStatus.Closed;
@@ -930,6 +956,8 @@ public sealed class ExperimentsController(
.ThenInclude(x => x!.AcademicTerm)
.FirstOrDefaultAsync(x => x.Id == projectId, cancellationToken);
if (project is null) return NotFound();
if (!IsTaskOperable(project.TeachingTask!))
return TaskArchivedProblem();
if (project.Status == ExperimentProjectStatus.Closed)
return ConflictProblem("已关闭实验项目不能再增加场次。");
if (project.ArrangementMode == ExperimentArrangementMode.Centralized)
@@ -1009,6 +1037,8 @@ public sealed class ExperimentsController(
if (projects.Count != projectIds.Count)
return ValidationProblem(
"部分实验项目不存在或不在当前管理范围内。");
if (projects.Values.Any(x => !IsTaskOperable(x.TeachingTask!)))
return TaskArchivedProblem();
var createdSessions = new List<(ExperimentProject Project, ExperimentSession Session)>(
request.Items.Count);
@@ -1100,6 +1130,8 @@ public sealed class ExperimentsController(
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
if (session is null) return NotFound();
if (!CanManageSession(session)) return Forbid();
if (!IsTaskOperable(session.ExperimentProject!.TeachingTask!))
return TaskArchivedProblem();
if (session.Status == ExperimentSessionStatus.Cancelled)
return NoContent();
@@ -1253,6 +1285,8 @@ public sealed class ExperimentsController(
var session = await LoadManagedSessionAsync(id, cancellationToken);
if (session is null) return NotFound();
if (!CanManageSession(session)) return Forbid();
if (!IsTaskOperable(session.ExperimentProject!.TeachingTask!))
return TaskArchivedProblem();
if (session.Status != ExperimentSessionStatus.Scheduled ||
session.ExperimentProject!.Status != ExperimentProjectStatus.Published ||
session.ExperimentProject.ArrangementMode != ExperimentArrangementMode.SelfScheduled)
@@ -1327,6 +1361,8 @@ public sealed class ExperimentsController(
var session = await LoadManagedSessionAsync(id, cancellationToken);
if (session is null) return NotFound();
if (!CanManageSession(session)) return Forbid();
if (!IsTaskOperable(session.ExperimentProject!.TeachingTask!))
return TaskArchivedProblem();
if (session.Status != ExperimentSessionStatus.Scheduled ||
session.ExperimentProject!.Status != ExperimentProjectStatus.Published ||
session.ExperimentProject.ArrangementMode != ExperimentArrangementMode.SelfScheduled)
@@ -1429,6 +1465,8 @@ public sealed class ExperimentsController(
session.ExperimentProject!.Status !=
ExperimentProjectStatus.Published)
return NotFound();
if (!IsTaskOperable(session.ExperimentProject.TeachingTask!))
return TaskArchivedProblem();
if (session.ExperimentProject.ArrangementMode !=
ExperimentArrangementMode.SelfScheduled)
return ConflictProblem("集中安排实验无需学生预约。");
@@ -1517,11 +1555,15 @@ public sealed class ExperimentsController(
var booking = await db.ExperimentBookings
.Include(x => x.ExperimentSession)
.ThenInclude(x => x!.ExperimentProject)
.ThenInclude(x => x!.TeachingTask)
.FirstOrDefaultAsync(x =>
x.Id == id &&
x.StudentId == student.Id,
cancellationToken);
if (booking is null) return NotFound();
if (!IsTaskOperable(booking.ExperimentSession!.ExperimentProject!.TeachingTask!))
return TaskArchivedProblem();
if (booking.Status == ExperimentBookingStatus.Cancelled)
return NoContent();
@@ -1847,6 +1889,12 @@ public sealed class ExperimentsController(
item.Teacher?.UserId == scope.UserId);
}
private static bool IsTaskOperable(TeachingTask task) =>
task.Status == TeachingTaskStatus.Published;
private ActionResult TaskArchivedProblem() =>
ConflictProblem("教学任务已归档,实验项目、场次和预约仅可查看,不能再操作。");
private Task<Student?> CurrentStudentAsync(
CancellationToken cancellationToken) =>
db.Students.FirstOrDefaultAsync(x =>