Compare commits
7
Commits
90877ae428
...
v2.4.2
No files matched your search
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Set up .NET
|
||||
uses: https://github.com/actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: "10.0.302"
|
||||
dotnet-version: "10.0.400"
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: https://github.com/actions/setup-node@v4
|
||||
@@ -46,6 +46,11 @@ jobs:
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Install Chinese PDF font for backend tests
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes fonts-noto-cjk
|
||||
|
||||
- name: Run backend tests
|
||||
run: dotnet test Jiaowu.slnx --configuration Release
|
||||
|
||||
|
||||
@@ -1208,6 +1208,106 @@ public sealed class ExperimentsController(
|
||||
.ToListAsync(cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("sessions/{id:guid}/eligible-students")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> GetProxyBookingStudents(
|
||||
Guid id,
|
||||
string? keyword,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var session = await LoadManagedSessionAsync(id, cancellationToken);
|
||||
if (session is null) return NotFound();
|
||||
if (!CanManageSession(session)) return Forbid();
|
||||
if (session.ExperimentProject!.ArrangementMode != ExperimentArrangementMode.SelfScheduled)
|
||||
return ConflictProblem("集中安排实验无需代选场次。");
|
||||
|
||||
var bookedStudentIds = await db.ExperimentBookings.AsNoTracking()
|
||||
.Where(x => x.ExperimentProjectId == session.ExperimentProjectId &&
|
||||
x.Status == ExperimentBookingStatus.Booked)
|
||||
.Select(x => x.StudentId)
|
||||
.ToListAsync(cancellationToken);
|
||||
var normalizedKeyword = string.IsNullOrWhiteSpace(keyword) ? null : keyword.Trim();
|
||||
var roster = await TeachingTaskRosterQuery.LoadForTasksAsync(
|
||||
db, [session.ExperimentProject.TeachingTaskId], cancellationToken);
|
||||
return Ok(roster
|
||||
.Where(x => !bookedStudentIds.Contains(x.StudentId))
|
||||
.Where(x => normalizedKeyword is null ||
|
||||
x.StudentNumber.Contains(normalizedKeyword) ||
|
||||
x.Name.Contains(normalizedKeyword) ||
|
||||
x.ClassName.Contains(normalizedKeyword))
|
||||
.OrderBy(x => x.StudentNumber)
|
||||
.Take(100)
|
||||
.Select(x => new { x.StudentId, x.StudentNumber, x.Name, x.ClassName }));
|
||||
}
|
||||
|
||||
[HttpPost("sessions/{id:guid}/proxy-bookings")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> CreateProxyBooking(
|
||||
Guid id,
|
||||
ProxyExperimentBookingRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
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 student = await db.Students.FirstOrDefaultAsync(x =>
|
||||
x.Id == request.StudentId && x.Status == StudentStatus.Active,
|
||||
cancellationToken);
|
||||
if (student is null) return ValidationProblem("学生不存在或不在学。");
|
||||
if (!await TeachingTaskRosterQuery.TaskIdsForStudent(db, student.Id)
|
||||
.ContainsAsync(session.ExperimentProject.TeachingTaskId, cancellationToken))
|
||||
return ValidationProblem("该学生不在本实验项目的教学任务名单内。");
|
||||
|
||||
var existing = await db.ExperimentBookings.FirstOrDefaultAsync(x =>
|
||||
x.ExperimentProjectId == session.ExperimentProjectId && x.StudentId == student.Id,
|
||||
cancellationToken);
|
||||
if (existing?.Status == ExperimentBookingStatus.Booked)
|
||||
return existing.ExperimentSessionId == session.Id
|
||||
? NoContent()
|
||||
: ConflictProblem("该学生已预约本实验项目的其他场次。");
|
||||
|
||||
var scheduleProblem = await StudentConflictAsync(student.Id, session, cancellationToken);
|
||||
if (scheduleProblem is not null) return ConflictProblem(scheduleProblem);
|
||||
var reservedCount = await db.ExperimentBookings.CountAsync(x =>
|
||||
x.ExperimentSessionId == session.Id && x.Status == ExperimentBookingStatus.Booked,
|
||||
cancellationToken);
|
||||
if (reservedCount >= session.Capacity)
|
||||
return ConflictProblem("该实验场次名额已满,请选择其他时间。");
|
||||
|
||||
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 + 1;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
return NoContent();
|
||||
},
|
||||
cancellationToken,
|
||||
IsolationLevel.Serializable);
|
||||
}
|
||||
|
||||
[HttpPost("sessions/{id:guid}/book")]
|
||||
[Authorize(Roles = SystemRoles.Student)]
|
||||
public async Task<ActionResult> Book(
|
||||
@@ -1604,6 +1704,24 @@ public sealed class ExperimentsController(
|
||||
taskIds.Contains(x.TeachingTaskId));
|
||||
}
|
||||
|
||||
private Task<ExperimentSession?> LoadManagedSessionAsync(
|
||||
Guid id,
|
||||
CancellationToken cancellationToken) =>
|
||||
db.ExperimentSessions
|
||||
.Include(x => x.ExperimentProject)
|
||||
.ThenInclude(x => x!.TeachingTask)
|
||||
.ThenInclude(x => x!.AcademicTerm)
|
||||
.Include(x => x.ExperimentProject)
|
||||
.ThenInclude(x => x!.TeachingTask)
|
||||
.ThenInclude(x => x!.Course)
|
||||
.Include(x => x.ExperimentProject)
|
||||
.ThenInclude(x => x!.TeachingTask)
|
||||
.ThenInclude(x => x!.Teachers)
|
||||
.ThenInclude(x => x.Teacher)
|
||||
.Include(x => x.Instructors)
|
||||
.ThenInclude(x => x.Teacher)
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
|
||||
private bool CanManageSession(ExperimentSession session)
|
||||
{
|
||||
var scope = currentUserDataScope.Current;
|
||||
@@ -1847,6 +1965,8 @@ public sealed record ExperimentSessionRequest(
|
||||
[MaxLength(500)] string? Notes,
|
||||
[Required, MinLength(1)] IReadOnlyList<Guid>? InstructorTeacherIds = null);
|
||||
|
||||
public sealed record ProxyExperimentBookingRequest(Guid StudentId);
|
||||
|
||||
public sealed record ExperimentSessionBatchRequest(
|
||||
[Required] IReadOnlyList<ExperimentSessionBatchItem> Items);
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ public static class PdfTypefaceResolver
|
||||
{
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "simhei.ttf"),
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "msyh.ttc"),
|
||||
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/noto/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc"
|
||||
};
|
||||
|
||||
Loaded 3 of 11 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user