修复实验排课

This commit is contained in:
2026-07-29 09:40:11 +08:00 Unverified
parent 8f56548b54
commit 356c410788
4 changed files with 187 additions and 27 deletions
@@ -3,6 +3,7 @@ using System.Data;
using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth; using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Experiments;
using Jiaowu.Api.Infrastructure.Persistence; using Jiaowu.Api.Infrastructure.Persistence;
using Jiaowu.Api.Infrastructure.Teaching; using Jiaowu.Api.Infrastructure.Teaching;
using Jiaowu.Api.Infrastructure.Timetables; using Jiaowu.Api.Infrastructure.Timetables;
@@ -833,38 +834,38 @@ public sealed class ExperimentsController(
term, term,
date); date);
var scheduleConflicts = await db.ScheduleEntries.AsNoTracking() var relatedTaskIds = (await CentralizedExperimentConflictQuery
.Where(x => .TaskIdsForTeachers(db, teacherIds)
x.SchedulePlan!.Status == SchedulePlanStatus.Published && .ToListAsync(cancellationToken))
x.SchedulePlan.AcademicTermId == term.Id && .Concat(await CentralizedExperimentConflictQuery
x.DayOfWeek == dayOfWeek && .TaskIdsForClasses(db, classIds)
x.StartWeek <= week && .ToListAsync(cancellationToken))
x.EndWeek >= week && .Distinct()
x.StartPeriod < startPeriod + periodCount && .ToArray();
startPeriod < x.StartPeriod + x.PeriodCount &&
(x.TeachingTask!.Teachers.Any(item => var scheduleConflicts = await CentralizedExperimentConflictQuery
teacherIds.Contains(item.TeacherId)) || .ScheduleEntries(
x.TeachingTask.Classes.Any(item => db,
classIds.Contains(item.AdministrativeClassId)))) relatedTaskIds,
term.Id,
dayOfWeek,
week,
startPeriod,
periodCount)
.Select(x => new { x.TeachingTaskId, x.WeekPattern }) .Select(x => new { x.TeachingTaskId, x.WeekPattern })
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (scheduleConflicts.Any(x => if (scheduleConflicts.Any(x =>
FreeClassroomRules.MatchesWeek(x.WeekPattern, week))) FreeClassroomRules.MatchesWeek(x.WeekPattern, week)))
return "集中实验与相关教师或行政班的已发布课表冲突。"; return "集中实验与相关教师或行政班的已发布课表冲突。";
var experimentConflict = await db.ExperimentSessions.AsNoTracking() var experimentConflict = await CentralizedExperimentConflictQuery
.AnyAsync(x => .ExperimentSessions(
x.Status == ExperimentSessionStatus.Scheduled && db,
x.SessionDate == date && relatedTaskIds,
x.StartPeriod < startPeriod + periodCount && date,
startPeriod < x.StartPeriod + x.PeriodCount && startPeriod,
x.ExperimentProject!.ArrangementMode == periodCount)
ExperimentArrangementMode.Centralized && .AnyAsync(cancellationToken);
(x.ExperimentProject.TeachingTask!.Teachers.Any(item =>
teacherIds.Contains(item.TeacherId)) ||
x.ExperimentProject.TeachingTask.Classes.Any(item =>
classIds.Contains(item.AdministrativeClassId))),
cancellationToken);
return experimentConflict return experimentConflict
? "集中实验与相关教师或行政班的其他实验安排冲突。" ? "集中实验与相关教师或行政班的其他实验安排冲突。"
: null; : null;
@@ -0,0 +1,59 @@
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
namespace Jiaowu.Api.Infrastructure.Experiments;
internal static class CentralizedExperimentConflictQuery
{
public static IQueryable<Guid> TaskIdsForTeachers(
AppDbContext db,
IReadOnlyCollection<Guid> teacherIds) =>
db.TeachingTaskTeachers.AsNoTracking()
.WhereIn(teacherIds, x => x.TeacherId)
.Select(x => x.TeachingTaskId);
public static IQueryable<Guid> TaskIdsForClasses(
AppDbContext db,
IReadOnlyCollection<Guid> classIds) =>
db.TeachingTaskClasses.AsNoTracking()
.WhereIn(classIds, x => x.AdministrativeClassId)
.Select(x => x.TeachingTaskId);
public static IQueryable<ScheduleEntry> ScheduleEntries(
AppDbContext db,
IReadOnlyCollection<Guid> relatedTaskIds,
Guid academicTermId,
int dayOfWeek,
int week,
int startPeriod,
int periodCount) =>
db.ScheduleEntries.AsNoTracking()
.Where(x =>
x.SchedulePlan!.Status == SchedulePlanStatus.Published &&
x.SchedulePlan.AcademicTermId == academicTermId &&
x.DayOfWeek == dayOfWeek &&
x.StartWeek <= week &&
x.EndWeek >= week &&
x.StartPeriod < startPeriod + periodCount &&
startPeriod < x.StartPeriod + x.PeriodCount)
.WhereIn(relatedTaskIds, x => x.TeachingTaskId);
public static IQueryable<ExperimentSession> ExperimentSessions(
AppDbContext db,
IReadOnlyCollection<Guid> relatedTaskIds,
DateOnly date,
int startPeriod,
int periodCount) =>
db.ExperimentSessions.AsNoTracking()
.Where(x =>
x.Status == ExperimentSessionStatus.Scheduled &&
x.SessionDate == date &&
x.StartPeriod < startPeriod + periodCount &&
startPeriod < x.StartPeriod + x.PeriodCount &&
x.ExperimentProject!.ArrangementMode ==
ExperimentArrangementMode.Centralized)
.WhereIn(
relatedTaskIds,
x => x.ExperimentProject!.TeachingTaskId);
}
@@ -138,6 +138,47 @@ public sealed class ExperimentsControllerTests
Assert.Empty(fixture.Db.ExperimentBookings); Assert.Empty(fixture.Db.ExperimentBookings);
} }
[Fact]
public async Task CentralizedSession_RejectsRelatedPublishedTimetableConflict()
{
await using var fixture = await ExperimentFixture.CreateAsync();
var manager = fixture.Controller(fixture.ManagerScope);
await manager.CreateProject(
fixture.ProjectRequest(ExperimentArrangementMode.Centralized),
CancellationToken.None);
var project = await fixture.Db.ExperimentProjects.SingleAsync();
fixture.Db.SchedulePlans.Add(new SchedulePlan
{
AcademicTermId = fixture.Term.Id,
Name = "正式课表",
Version = "V1",
Status = SchedulePlanStatus.Published,
Entries =
[
new ScheduleEntry
{
TeachingTaskId = fixture.Task.Id,
ClassroomId = fixture.SecondClassroom.Id,
DayOfWeek = 1,
StartPeriod = 1,
PeriodCount = 2,
StartWeek = 1,
EndWeek = 18,
WeekPattern = WeekPattern.All
}
]
});
await fixture.Db.SaveChangesAsync();
var result = await manager.CreateSession(
project.Id,
fixture.SessionRequest(1, 2),
CancellationToken.None);
Assert.IsType<ConflictObjectResult>(result);
Assert.Empty(fixture.Db.ExperimentSessions);
}
[Fact] [Fact]
public async Task StudentTimetable_IncludesCentralizedAndBookedSelfScheduledSessions() public async Task StudentTimetable_IncludesCentralizedAndBookedSelfScheduledSessions()
{ {
+60 -1
View File
@@ -1,5 +1,6 @@
using Jiaowu.Api.Infrastructure.Persistence; using Jiaowu.Api.Infrastructure.Experiments;
using Jiaowu.Api.Infrastructure.Exams; using Jiaowu.Api.Infrastructure.Exams;
using Jiaowu.Api.Infrastructure.Persistence;
using Jiaowu.Api.Infrastructure.Teaching; using Jiaowu.Api.Infrastructure.Teaching;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -176,6 +177,64 @@ public sealed class MySqlMigrationTests
StringComparison.OrdinalIgnoreCase); StringComparison.OrdinalIgnoreCase);
} }
[Fact]
public void MySql_centralized_experiment_conflict_queries_are_translatable()
{
using var db = new AppDbContext(CreateMySqlOptions());
var firstId =
Guid.Parse("11111111-1111-1111-1111-111111111111");
var secondId =
Guid.Parse("22222222-2222-2222-2222-222222222222");
Guid[] relatedIds = [firstId, secondId];
var teacherTaskSql = CentralizedExperimentConflictQuery
.TaskIdsForTeachers(db, relatedIds)
.ToQueryString();
var classTaskSql = CentralizedExperimentConflictQuery
.TaskIdsForClasses(db, relatedIds)
.ToQueryString();
var scheduleSql = CentralizedExperimentConflictQuery
.ScheduleEntries(
db,
relatedIds,
firstId,
1,
2,
1,
2)
.ToQueryString();
var experimentSql = CentralizedExperimentConflictQuery
.ExperimentSessions(
db,
relatedIds,
new DateOnly(2026, 8, 1),
1,
2)
.ToQueryString();
Assert.Contains(
"TeachingTaskTeachers",
teacherTaskSql,
StringComparison.OrdinalIgnoreCase);
Assert.Contains(
"TeachingTaskClasses",
classTaskSql,
StringComparison.OrdinalIgnoreCase);
Assert.Contains(
"ScheduleEntries",
scheduleSql,
StringComparison.OrdinalIgnoreCase);
Assert.Contains(
"ExperimentSessions",
experimentSql,
StringComparison.OrdinalIgnoreCase);
Assert.Contains(" IN (", scheduleSql, StringComparison.OrdinalIgnoreCase);
Assert.Contains(
" IN (",
experimentSql,
StringComparison.OrdinalIgnoreCase);
}
[Fact] [Fact]
public void MySql_index_names_fit_the_server_identifier_limit() public void MySql_index_names_fit_the_server_identifier_limit()
{ {