This commit is contained in:
2026-07-27 12:20:29 +08:00 Unverified
parent 727a429abf
commit 33ae991e86
16 changed files with 7117 additions and 8 deletions
@@ -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));
}
}