预演
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
using System.Text.Json;
|
||||
using Jiaowu.Api.Controllers;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jiaowu.Api.Tests;
|
||||
|
||||
public sealed class AcademicPlanningControllerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Simulation_reuses_published_grade_and_checks_prerequisites()
|
||||
{
|
||||
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 user = new ApplicationUser
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserName = "student",
|
||||
NormalizedUserName = "STUDENT",
|
||||
DisplayName = "规划学生"
|
||||
};
|
||||
var college = new College { Code = "CS", Name = "计算机学院" };
|
||||
var major = new Major
|
||||
{
|
||||
Code = "SE",
|
||||
Name = "软件工程",
|
||||
CollegeId = college.Id,
|
||||
DegreeType = "工学",
|
||||
SchoolingYears = 4
|
||||
};
|
||||
var administrativeClass = new AdministrativeClass
|
||||
{
|
||||
Code = "SE2501",
|
||||
Name = "软件工程2501班",
|
||||
MajorId = major.Id,
|
||||
Grade = 2025
|
||||
};
|
||||
var student = new Student
|
||||
{
|
||||
StudentNumber = "20250001",
|
||||
Name = "规划学生",
|
||||
AdministrativeClassId = administrativeClass.Id,
|
||||
EnrollmentYear = 2025,
|
||||
EnrollmentDate = new DateOnly(2025, 9, 1),
|
||||
UserId = user.Id
|
||||
};
|
||||
var pastTerm = new AcademicTerm
|
||||
{
|
||||
Code = "2025-A",
|
||||
Name = "2025—2026 学年秋季学期",
|
||||
AcademicYear = "2025-2026",
|
||||
Season = TermSeason.Autumn,
|
||||
StartDate = new DateOnly(2025, 9, 1),
|
||||
EndDate = new DateOnly(2026, 1, 15)
|
||||
};
|
||||
var currentTerm = new AcademicTerm
|
||||
{
|
||||
Code = "2025-S",
|
||||
Name = "2025—2026 学年春季学期",
|
||||
AcademicYear = "2025-2026",
|
||||
Season = TermSeason.Spring,
|
||||
StartDate = new DateOnly(2026, 2, 20),
|
||||
EndDate = new DateOnly(2026, 7, 10),
|
||||
IsCurrent = true
|
||||
};
|
||||
var introduction = new Course
|
||||
{
|
||||
Code = "CS101",
|
||||
Name = "程序设计基础",
|
||||
CollegeId = college.Id,
|
||||
Credits = 4,
|
||||
TotalHours = 64,
|
||||
LectureHours = 48,
|
||||
PracticeHours = 16,
|
||||
Nature = CourseNature.MajorRequired,
|
||||
AssessmentMethod = AssessmentMethod.Examination
|
||||
};
|
||||
var dataStructures = new Course
|
||||
{
|
||||
Code = "CS201",
|
||||
Name = "数据结构",
|
||||
CollegeId = college.Id,
|
||||
Credits = 4,
|
||||
TotalHours = 64,
|
||||
LectureHours = 48,
|
||||
PracticeHours = 16,
|
||||
Nature = CourseNature.MajorRequired,
|
||||
AssessmentMethod = AssessmentMethod.Examination,
|
||||
Prerequisites =
|
||||
[
|
||||
new CoursePrerequisite
|
||||
{
|
||||
PrerequisiteCourseId = introduction.Id
|
||||
}
|
||||
]
|
||||
};
|
||||
var plan = new CurriculumPlan
|
||||
{
|
||||
MajorId = major.Id,
|
||||
Name = "软件工程本科培养方案",
|
||||
Version = "2025",
|
||||
EffectiveGrade = 2025,
|
||||
TotalCredits = 8,
|
||||
Status = CurriculumPlanStatus.Published,
|
||||
PublishedAt = DateTime.UtcNow,
|
||||
Modules =
|
||||
[
|
||||
new CurriculumModule
|
||||
{
|
||||
Code = "M01",
|
||||
Name = "专业基础",
|
||||
RequiredCredits = 0,
|
||||
Courses =
|
||||
[
|
||||
new CurriculumCourse
|
||||
{
|
||||
CourseId = introduction.Id,
|
||||
RecommendedSemester = 1,
|
||||
Type = CurriculumCourseType.Required
|
||||
},
|
||||
new CurriculumCourse
|
||||
{
|
||||
CourseId = dataStructures.Id,
|
||||
RecommendedSemester = 3,
|
||||
Type = CurriculumCourseType.Required
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
var teachingTask = new TeachingTask
|
||||
{
|
||||
TaskNumber = "2025-CS101-01",
|
||||
Name = "程序设计基础",
|
||||
AcademicTermId = pastTerm.Id,
|
||||
CourseId = introduction.Id,
|
||||
Capacity = 50,
|
||||
Status = TeachingTaskStatus.Closed
|
||||
};
|
||||
var gradeSheet = new GradeSheet
|
||||
{
|
||||
TeachingTaskId = teachingTask.Id,
|
||||
Status = GradeSheetStatus.Published,
|
||||
PublishedAt = DateTime.UtcNow,
|
||||
Records =
|
||||
[
|
||||
new GradeRecord
|
||||
{
|
||||
StudentId = student.Id,
|
||||
TotalScore = 82,
|
||||
GradePoint = 3.2m
|
||||
}
|
||||
]
|
||||
};
|
||||
db.AddRange(
|
||||
user,
|
||||
college,
|
||||
major,
|
||||
administrativeClass,
|
||||
student,
|
||||
pastTerm,
|
||||
currentTerm,
|
||||
introduction,
|
||||
dataStructures,
|
||||
plan,
|
||||
teachingTask,
|
||||
gradeSheet);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var controller = new AcademicPlanningController(
|
||||
db,
|
||||
new StudentDataScope(user.Id));
|
||||
var overview = Assert.IsType<OkObjectResult>(
|
||||
await controller.Get(default));
|
||||
using var overviewJson = ToJson(overview.Value);
|
||||
Assert.Equal(
|
||||
4,
|
||||
overviewJson.RootElement
|
||||
.GetProperty("baseline")
|
||||
.GetProperty("earnedCredits")
|
||||
.GetDecimal());
|
||||
Assert.Equal(
|
||||
dataStructures.Id,
|
||||
overviewJson.RootElement
|
||||
.GetProperty("nextSemesterSuggestion")[0]
|
||||
.GetProperty("courseId")
|
||||
.GetGuid());
|
||||
|
||||
var result = Assert.IsType<OkObjectResult>(
|
||||
await controller.Simulate(
|
||||
new AcademicPlanningRequest(
|
||||
[
|
||||
new AcademicPlanningTermRequest(
|
||||
3,
|
||||
[dataStructures.Id])
|
||||
]),
|
||||
default));
|
||||
using var resultJson = ToJson(result.Value);
|
||||
Assert.Empty(resultJson.RootElement.GetProperty("conflicts").EnumerateArray());
|
||||
var projected = resultJson.RootElement.GetProperty("projected");
|
||||
Assert.Equal(8, projected.GetProperty("earnedCredits").GetDecimal());
|
||||
Assert.Equal(
|
||||
(int)GraduationAuditConclusion.Eligible,
|
||||
projected.GetProperty("graduationConclusion").GetInt32());
|
||||
}
|
||||
|
||||
private static JsonDocument ToJson(object? value) => JsonDocument.Parse(
|
||||
JsonSerializer.Serialize(
|
||||
value,
|
||||
new JsonSerializerOptions(JsonSerializerDefaults.Web)));
|
||||
|
||||
private sealed class StudentDataScope(Guid userId) : ICurrentUserDataScope
|
||||
{
|
||||
public CurrentUserScope Current { get; } = new(
|
||||
userId,
|
||||
"规划学生",
|
||||
null,
|
||||
DataScope.Self,
|
||||
new HashSet<string>([SystemRoles.Student]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Infrastructure.Graduation;
|
||||
|
||||
namespace Jiaowu.Api.Tests;
|
||||
|
||||
public sealed class AcademicPlanningRulesTests
|
||||
{
|
||||
private static readonly Guid AdvancedCourseId = Guid.NewGuid();
|
||||
private static readonly Guid PrerequisiteCourseId = Guid.NewGuid();
|
||||
|
||||
private static readonly AcademicPlanningCourseSnapshot[] Courses =
|
||||
[
|
||||
new(
|
||||
PrerequisiteCourseId,
|
||||
"程序设计基础",
|
||||
4,
|
||||
1,
|
||||
CurriculumCourseType.Required,
|
||||
[]),
|
||||
new(
|
||||
AdvancedCourseId,
|
||||
"数据结构",
|
||||
4,
|
||||
2,
|
||||
CurriculumCourseType.Required,
|
||||
[PrerequisiteCourseId])
|
||||
];
|
||||
|
||||
[Fact]
|
||||
public void Prerequisite_must_be_completed_or_planned_in_an_earlier_term()
|
||||
{
|
||||
var sameTerm = AcademicPlanningRules.FindPrerequisiteIssues(
|
||||
Courses,
|
||||
new HashSet<Guid>(),
|
||||
new HashSet<Guid>(),
|
||||
new Dictionary<Guid, int>
|
||||
{
|
||||
[PrerequisiteCourseId] = 3,
|
||||
[AdvancedCourseId] = 3
|
||||
});
|
||||
|
||||
var correctOrder = AcademicPlanningRules.FindPrerequisiteIssues(
|
||||
Courses,
|
||||
new HashSet<Guid>(),
|
||||
new HashSet<Guid>(),
|
||||
new Dictionary<Guid, int>
|
||||
{
|
||||
[PrerequisiteCourseId] = 3,
|
||||
[AdvancedCourseId] = 4
|
||||
});
|
||||
|
||||
Assert.Single(sameTerm);
|
||||
Assert.Empty(correctOrder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void In_progress_prerequisite_unlocks_next_term_suggestion()
|
||||
{
|
||||
var suggestion = AcademicPlanningRules.SuggestNextSemester(
|
||||
Courses,
|
||||
new HashSet<Guid>(),
|
||||
new HashSet<Guid>([PrerequisiteCourseId]),
|
||||
2);
|
||||
|
||||
Assert.Equal([AdvancedCourseId], suggestion);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 0, 5, 4, 4)]
|
||||
[InlineData(25, 0, 5, 4, 6)]
|
||||
[InlineData(0, 7, 5, 4, 6)]
|
||||
public void Completion_estimate_uses_credit_and_requirement_capacity(
|
||||
decimal remainingCredits,
|
||||
int remainingRequirements,
|
||||
int nextSemester,
|
||||
int latestPlannedSemester,
|
||||
int expectedSemester)
|
||||
{
|
||||
Assert.Equal(
|
||||
expectedSemester,
|
||||
AcademicPlanningRules.EstimateCompletionSemester(
|
||||
nextSemester,
|
||||
latestPlannedSemester,
|
||||
remainingCredits,
|
||||
remainingRequirements));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user