Files
Academic-Affairs-System/tests/Jiaowu.Api.Tests/ScheduleSettingsControllerTests.cs
T
biss 692b521453 已改成“普通排课与实验学时分离”:
普通课表学时 = 总学时 − 实践学时。
自动排课、手工排课、课表发布都会阻止把实践学时重复排入。
全部为实践学时的课程应设为“非排时课程”,再由实验管理安排。
教学任务和排课页面会显示“常规 / 实践学时”。
已有教学任务即使仍保存旧周学时,排课时也会按拆分后的课程学时计算。
无需数据库迁移。
2026-07-29 09:57:49 +08:00

331 lines
11 KiB
C#

using Jiaowu.Api.Controllers;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Infrastructure.Caching;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using System.Collections;
using System.Text.Encodings.Web;
using System.Text.Json;
namespace Jiaowu.Api.Tests;
public sealed class ScheduleSettingsControllerTests
{
[Fact]
public async Task Batch_constraints_assign_same_classroom_scope_to_all_tasks()
{
await using var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite(connection)
.Options;
await using var db = new AppDbContext(options);
await db.Database.EnsureCreatedAsync();
var campus = new Campus { Code = "MAIN", Name = "主校区" };
var building = new Building
{
Code = "J1",
Name = "第一教学楼",
Campus = campus
};
var classroom = new Classroom
{
Code = "J1-201",
Name = "J1-201",
Building = building,
Capacity = 80
};
var college = new College { Code = "CS", Name = "计算机学院" };
var course = new Course
{
Code = "CS101",
Name = "程序设计基础",
College = college,
Credits = 4,
TotalHours = 64,
LectureHours = 48,
PracticeHours = 16
};
var term = new AcademicTerm
{
Code = "2026-F",
Name = "2026 秋季",
AcademicYear = "2026-2027",
Season = TermSeason.Autumn,
StartDate = new DateOnly(2026, 9, 7),
EndDate = new DateOnly(2027, 1, 17)
};
var firstTask = NewTask("TASK-01", course, term);
var secondTask = NewTask("TASK-02", course, term);
db.AddRange(
campus,
building,
classroom,
college,
course,
term,
firstTask,
secondTask);
await db.SaveChangesAsync();
var controller = new ScheduleSettingsController(db, NoOpAppCache.Instance);
var result = await controller.SaveConstraintsBatch(
new TeachingTaskScheduleConstraintBatchRequest(
term.Id,
[firstTask.Id, secondTask.Id],
null,
null,
null,
true,
campus.Id,
building.Id,
[classroom.Id],
false,
null,
null),
CancellationToken.None);
Assert.IsType<OkObjectResult>(result);
db.ChangeTracker.Clear();
var constraints = await db.TeachingTaskScheduleConstraints
.Include(item => item.AllowedClassrooms)
.OrderBy(item => item.TeachingTaskId)
.ToListAsync();
Assert.Equal(2, constraints.Count);
Assert.All(constraints, constraint =>
{
Assert.True(constraint.RequiresClassroom);
Assert.Equal(campus.Id, constraint.RequiredCampusId);
Assert.Equal(building.Id, constraint.RequiredBuildingId);
Assert.Equal(
classroom.Id,
Assert.Single(constraint.AllowedClassrooms).ClassroomId);
});
}
[Fact]
public async Task Get_constraints_materializes_teachers_and_allowed_classrooms()
{
await using var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite(connection)
.Options;
await using var db = new AppDbContext(options);
await db.Database.EnsureCreatedAsync();
var college = new College { Code = "CS", Name = "计算机学院" };
var course = new Course
{
Code = "CS101",
Name = "程序设计基础",
College = college,
Credits = 4,
TotalHours = 64,
LectureHours = 48,
PracticeHours = 16
};
var term = new AcademicTerm
{
Code = "2026-F",
Name = "2026 秋季",
AcademicYear = "2026-2027",
Season = TermSeason.Autumn,
StartDate = new DateOnly(2026, 9, 7),
EndDate = new DateOnly(2027, 1, 17)
};
var teacher = new Teacher
{
TeacherNumber = "T001",
Name = "测试教师",
College = college
};
var classroom = new Classroom
{
Code = "J1-201",
Name = "J1-201",
Capacity = 80,
Building = new Building
{
Code = "J1",
Name = "第一教学楼",
Campus = new Campus { Code = "MAIN", Name = "主校区" }
}
};
var task = NewTask("TASK-01", course, term);
task.Teachers.Add(new TeachingTaskTeacher
{
Teacher = teacher,
IsPrimary = true
});
var constraint = new TeachingTaskScheduleConstraint
{
TeachingTask = task,
AllowedDayOfWeeks = "1,3,5",
EarliestPeriod = 1,
LatestPeriod = 8,
AllowedClassrooms =
[
new TeachingTaskAllowedClassroom { Classroom = classroom }
]
};
db.AddRange(college, course, term, teacher, classroom, task, constraint);
await db.SaveChangesAsync();
var result = await new ScheduleSettingsController(db, NoOpAppCache.Instance)
.GetConstraints(term.Id, CancellationToken.None);
var ok = Assert.IsType<OkObjectResult>(result);
var payload = Assert.IsAssignableFrom<IEnumerable>(ok.Value);
Assert.Single(payload.Cast<object>());
var json = JsonSerializer.Serialize(
ok.Value,
new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
Assert.Contains("测试教师", json);
Assert.Contains(classroom.Id.ToString(), json);
Assert.Contains("\"WeeklyHours\":3", json);
Assert.Contains("\"CoursePracticeHours\":16", json);
}
[Fact]
public async Task MySql_schedule_settings_materialize_time_slots_and_constraints()
{
var connectionString =
Environment.GetEnvironmentVariable("JIAOWU_TEST_MYSQL_CONNECTION");
if (string.IsNullOrWhiteSpace(connectionString))
return;
var previousConnectionString =
Environment.GetEnvironmentVariable("ConnectionStrings__MySql");
AppDbContext db;
try
{
Environment.SetEnvironmentVariable(
"ConnectionStrings__MySql",
connectionString);
db = new AppDbContextFactory().CreateDbContext([]);
}
finally
{
Environment.SetEnvironmentVariable(
"ConnectionStrings__MySql",
previousConnectionString);
}
await using var ownedDb = db;
var term = await db.AcademicTerms.FirstAsync();
var course = await db.Courses.FirstAsync();
var teacher = await db.Teachers.FirstAsync(
item => item.CollegeId == course.CollegeId);
var classroom = await db.Classrooms.FirstAsync();
await using var transaction = await db.Database.BeginTransactionAsync();
var periodNumber = await db.ScheduleTimeSlots
.Where(item => item.AcademicTermId == term.Id)
.Select(item => (int?)item.PeriodNumber)
.MaxAsync() ?? 0;
var task = new TeachingTask
{
TaskNumber = $"MYSQL-{Guid.NewGuid():N}"[..22],
Name = "MySQL 排课约束回归",
AcademicTermId = term.Id,
CourseId = course.Id,
Capacity = 60,
WeeklyHours = 2,
StartWeek = 1,
EndWeek = 16,
Status = TeachingTaskStatus.Published,
Teachers =
[
new TeachingTaskTeacher
{
TeacherId = teacher.Id,
IsPrimary = true
}
]
};
db.Add(task);
db.Add(new ScheduleTimeSlot
{
AcademicTermId = term.Id,
PeriodNumber = periodNumber + 1,
Name = "MySQL 回归节次",
StartsAt = new TimeOnly(8, 0),
EndsAt = new TimeOnly(8, 45)
});
db.Add(new TeachingTaskScheduleConstraint
{
TeachingTask = task,
AllowedDayOfWeeks = "1,3,5",
EarliestPeriod = 1,
LatestPeriod = 8,
AllowedClassrooms =
[
new TeachingTaskAllowedClassroom { ClassroomId = classroom.Id }
]
});
await db.SaveChangesAsync();
db.ChangeTracker.Clear();
var result = await new ScheduleSettingsController(db, NoOpAppCache.Instance)
.GetConstraints(term.Id, CancellationToken.None);
var ok = Assert.IsType<OkObjectResult>(result);
var payload = Assert.IsAssignableFrom<IEnumerable>(ok.Value);
Assert.Contains(payload.Cast<object>(), item =>
JsonSerializer.Serialize(item).Contains(task.TaskNumber));
var timeSlotsResult = await new ScheduleSettingsController(
db,
NoOpAppCache.Instance)
.GetTimeSlots(term.Id, CancellationToken.None);
var timeSlotsOk = Assert.IsType<OkObjectResult>(timeSlotsResult);
var timeSlotsJson = JsonSerializer.Serialize(timeSlotsOk.Value);
Assert.Contains("08:00", timeSlotsJson);
var examTimeSlotsResult = await new ExamsController(
db,
null!,
NoOpAppCache.Instance)
.GetTimeSlotsForTerm(term.Id, CancellationToken.None);
var examTimeSlotsOk =
Assert.IsType<OkObjectResult>(examTimeSlotsResult);
Assert.Contains(
"08:00",
JsonSerializer.Serialize(examTimeSlotsOk.Value));
var makeupTimeSlotsResult = await new MakeupExamsController(
db,
null!,
null!)
.GetTimeSlotsForTerm(term.Id, CancellationToken.None);
var makeupTimeSlotsOk =
Assert.IsType<OkObjectResult>(makeupTimeSlotsResult);
Assert.Contains(
"08:00",
JsonSerializer.Serialize(makeupTimeSlotsOk.Value));
await transaction.RollbackAsync();
}
private static TeachingTask NewTask(
string taskNumber,
Course course,
AcademicTerm term) =>
new()
{
TaskNumber = taskNumber,
Name = $"{course.Name}教学班",
Course = course,
AcademicTerm = term,
Capacity = 60,
WeeklyHours = 4,
StartWeek = 1,
EndWeek = 16,
Status = TeachingTaskStatus.Published
};
}