Compare commits
17
Commits
v2.4.2
...
e70d71cb67
No files matched your search
@@ -557,7 +557,7 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
|
||||
.Select(x => new ClassroomListItem(
|
||||
x.Id, x.Code, x.Name, x.BuildingId, x.Building!.CampusId,
|
||||
x.Building.Name, x.Building.Campus!.Name, x.Capacity,
|
||||
x.RoomType, x.TeachingVenueNature, x.Equipment,
|
||||
x.Floor, x.RoomType, x.TeachingVenueNature, x.Equipment,
|
||||
x.IsEnabled, x.SortOrder))
|
||||
.ToListAsync(cancellationToken);
|
||||
return Ok(new PagedResult<ClassroomListItem>(
|
||||
@@ -578,6 +578,7 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
|
||||
x.Building.Name,
|
||||
x.Building.Campus!.Name,
|
||||
x.Capacity,
|
||||
x.Floor,
|
||||
x.RoomType,
|
||||
x.TeachingVenueNature,
|
||||
x.Equipment,
|
||||
@@ -645,6 +646,7 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
|
||||
Name = request.Name.Trim(),
|
||||
BuildingId = request.BuildingId,
|
||||
Capacity = request.Capacity,
|
||||
Floor = request.Floor,
|
||||
RoomType = request.RoomType.Trim(),
|
||||
TeachingVenueNature = request.TeachingVenueNature == 0
|
||||
? TeachingVenueNature.GeneralClassroom
|
||||
@@ -668,6 +670,7 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
|
||||
ApplyCatalog(entity, request);
|
||||
entity.BuildingId = request.BuildingId;
|
||||
entity.Capacity = request.Capacity;
|
||||
entity.Floor = request.Floor;
|
||||
entity.RoomType = request.RoomType.Trim();
|
||||
entity.TeachingVenueNature = request.TeachingVenueNature == 0
|
||||
? TeachingVenueNature.GeneralClassroom
|
||||
@@ -822,6 +825,7 @@ public sealed record ClassroomRequest(
|
||||
string Code, string Name, int SortOrder, bool IsEnabled,
|
||||
Guid BuildingId,
|
||||
[Range(1, 1000)] int Capacity,
|
||||
[Range(1, 99)] int Floor,
|
||||
[Required, MaxLength(40)] string RoomType,
|
||||
TeachingVenueNature TeachingVenueNature,
|
||||
[MaxLength(300)] string? Equipment)
|
||||
@@ -879,6 +883,7 @@ public sealed record ClassroomListItem(
|
||||
string BuildingName,
|
||||
string CampusName,
|
||||
int Capacity,
|
||||
int Floor,
|
||||
string RoomType,
|
||||
TeachingVenueNature TeachingVenueNature,
|
||||
string? Equipment,
|
||||
|
||||
@@ -340,11 +340,17 @@ public sealed class CoursesController(
|
||||
isTeacher && db.TeachingTasks.Any(task =>
|
||||
task.CourseId == course.Id &&
|
||||
task.Teachers.Any(item => item.Teacher!.UserId == userId)) ||
|
||||
isStudent && db.TeachingTasks.Any(task =>
|
||||
task.CourseId == course.Id &&
|
||||
task.Classes.Any(item =>
|
||||
item.AdministrativeClass!.Students.Any(student =>
|
||||
student.UserId == userId))));
|
||||
isStudent && (
|
||||
db.TeachingTasks.Any(task =>
|
||||
task.CourseId == course.Id &&
|
||||
task.Classes.Any(item =>
|
||||
item.AdministrativeClass!.Students.Any(student =>
|
||||
student.UserId == userId))) ||
|
||||
db.CourseEnrollments.Any(enrollment =>
|
||||
enrollment.Status == CourseEnrollmentStatus.Enrolled &&
|
||||
enrollment.Student!.UserId == userId &&
|
||||
enrollment.CourseSelectionOffering!.TeachingTask!.CourseId ==
|
||||
course.Id)));
|
||||
}
|
||||
|
||||
private bool CanManage(Guid collegeId, CourseNature nature)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Loaded 3 of 42 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user