修复实验排课
This commit is contained in:
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user