Add batch proxy booking for self-scheduled experiments

This commit is contained in:
biss committed 2026-08-17 19:33:07 +08:00
1 parent 1e43d910c0
commit e6d25791b3
3 files changed
+152 -11

No files matched your search

@@ -1308,6 +1308,104 @@ public sealed class ExperimentsController(
IsolationLevel.Serializable);
}
[HttpPost("sessions/{id:guid}/proxy-bookings/batch")]
[Authorize(Roles = Managers)]
public async Task<ActionResult> CreateProxyBookings(
Guid id,
ProxyExperimentBookingBatchRequest request,
CancellationToken cancellationToken)
{
var studentIds = request.StudentIds.Distinct().ToArray();
if (studentIds.Length == 0)
return ValidationProblem("请至少选择一名学生。");
if (studentIds.Length > 100)
return ValidationProblem("单次最多可代选 100 名学生。");
return await db.ExecuteInRetriableTransactionAsync<ActionResult>(
async transaction =>
{
db.ChangeTracker.Clear();
var session = await LoadManagedSessionAsync(id, cancellationToken);
if (session is null) return NotFound();
if (!CanManageSession(session)) return Forbid();
if (session.Status != ExperimentSessionStatus.Scheduled ||
session.ExperimentProject!.Status != ExperimentProjectStatus.Published ||
session.ExperimentProject.ArrangementMode != ExperimentArrangementMode.SelfScheduled)
return ConflictProblem("只能为已发布的自行安排实验场次代选学生。");
var students = await db.Students
.WhereIn(studentIds, x => x.Id)
.OrderBy(x => x.StudentNumber)
.ToListAsync(cancellationToken);
if (students.Count != studentIds.Length)
return ValidationProblem("存在无效的学生档案。");
var inactive = students.FirstOrDefault(x => x.Status != StudentStatus.Active);
if (inactive is not null)
return ConflictProblem($"学生 {inactive.StudentNumber} {inactive.Name} 当前不是在籍状态。");
var roster = await TeachingTaskRosterQuery.LoadForTasksAsync(
db, [session.ExperimentProject.TeachingTaskId], cancellationToken);
var eligibleStudentIds = roster.Select(x => x.StudentId).ToHashSet();
var outOfRoster = students.FirstOrDefault(x => !eligibleStudentIds.Contains(x.Id));
if (outOfRoster is not null)
return ConflictProblem($"学生 {outOfRoster.StudentNumber} {outOfRoster.Name} 不在本实验项目的教学任务名单内。");
var existingBookings = await db.ExperimentBookings
.Where(x => x.ExperimentProjectId == session.ExperimentProjectId)
.WhereIn(studentIds, x => x.StudentId)
.ToListAsync(cancellationToken);
var booked = existingBookings.FirstOrDefault(x =>
x.Status == ExperimentBookingStatus.Booked);
if (booked is not null)
{
var student = students.First(x => x.Id == booked.StudentId);
return ConflictProblem(booked.ExperimentSessionId == session.Id
? $"学生 {student.StudentNumber} {student.Name} 已预约本实验场次。"
: $"学生 {student.StudentNumber} {student.Name} 已预约本实验项目的其他场次。");
}
foreach (var student in students)
{
var scheduleProblem = await StudentConflictAsync(student.Id, session, cancellationToken);
if (scheduleProblem is not null)
return ConflictProblem($"学生 {student.StudentNumber} {student.Name}:{scheduleProblem}");
}
var reservedCount = await db.ExperimentBookings.CountAsync(x =>
x.ExperimentSessionId == session.Id && x.Status == ExperimentBookingStatus.Booked,
cancellationToken);
if (reservedCount + students.Count > session.Capacity)
return ConflictProblem($"该实验场次仅剩 {Math.Max(0, session.Capacity - reservedCount)} 个名额,无法完成本次代选。");
foreach (var student in students)
{
var existing = existingBookings.FirstOrDefault(x => x.StudentId == student.Id);
if (existing is null)
{
db.ExperimentBookings.Add(new ExperimentBooking
{
ExperimentProjectId = session.ExperimentProjectId,
ExperimentSessionId = session.Id,
StudentId = student.Id
});
}
else
{
existing.ExperimentSessionId = session.Id;
existing.Status = ExperimentBookingStatus.Booked;
existing.BookedAt = DateTime.UtcNow;
existing.CancelledAt = null;
}
}
session.ReservedCount = reservedCount + students.Count;
await db.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
return Ok(new { BookedCount = students.Count });
},
cancellationToken,
IsolationLevel.Serializable);
}
[HttpPost("sessions/{id:guid}/book")]
[Authorize(Roles = SystemRoles.Student)]
public async Task<ActionResult> Book(
@@ -1967,6 +2065,9 @@ public sealed record ExperimentSessionRequest(
public sealed record ProxyExperimentBookingRequest(Guid StudentId);
public sealed record ProxyExperimentBookingBatchRequest(
[Required, MinLength(1)] IReadOnlyCollection<Guid> StudentIds);
public sealed record ExperimentSessionBatchRequest(
[Required] IReadOnlyList<ExperimentSessionBatchItem> Items);