using Jiaowu.Api.Domain.Academic; namespace Jiaowu.Api.Infrastructure.Graduation; public sealed record AcademicPlanningCourseSnapshot( Guid CourseId, string CourseName, decimal Credits, int RecommendedSemester, CurriculumCourseType Type, IReadOnlyCollection PrerequisiteCourseIds); public sealed record AcademicPlanningPrerequisiteIssue( Guid CourseId, Guid PrerequisiteCourseId, int PlannedSemester, int? PrerequisitePlannedSemester); public static class AcademicPlanningRules { public const decimal RecommendedSemesterCreditLimit = 24; public const decimal HeavySemesterCreditLimit = 30; public static IReadOnlyList FindPrerequisiteIssues( IEnumerable courses, IReadOnlySet completedCourseIds, IReadOnlySet inProgressCourseIds, IReadOnlyDictionary plannedSemesters) { var issues = new List(); foreach (var course in courses.Where(x => plannedSemesters.ContainsKey(x.CourseId))) { var plannedSemester = plannedSemesters[course.CourseId]; foreach (var prerequisiteId in course.PrerequisiteCourseIds) { if (completedCourseIds.Contains(prerequisiteId) || inProgressCourseIds.Contains(prerequisiteId)) continue; if (!plannedSemesters.TryGetValue( prerequisiteId, out var prerequisiteSemester) || prerequisiteSemester >= plannedSemester) { issues.Add(new AcademicPlanningPrerequisiteIssue( course.CourseId, prerequisiteId, plannedSemester, plannedSemesters.GetValueOrDefault(prerequisiteId))); } } } return issues; } public static IReadOnlyList SuggestNextSemester( IEnumerable courses, IReadOnlySet completedCourseIds, IReadOnlySet inProgressCourseIds, int nextSemester, decimal targetCredits = RecommendedSemesterCreditLimit) { var available = courses .Where(x => !completedCourseIds.Contains(x.CourseId) && !inProgressCourseIds.Contains(x.CourseId) && x.RecommendedSemester <= nextSemester && x.PrerequisiteCourseIds.All(prerequisiteId => completedCourseIds.Contains(prerequisiteId) || inProgressCourseIds.Contains(prerequisiteId))) .OrderBy(x => x.Type == CurriculumCourseType.Required ? 0 : 1) .ThenBy(x => x.RecommendedSemester > nextSemester ? 1 : 0) .ThenBy(x => x.RecommendedSemester) .ThenBy(x => x.CourseName) .ToList(); var selected = new List(); decimal credits = 0; foreach (var course in available) { var isOverdueRequired = course.Type == CurriculumCourseType.Required && course.RecommendedSemester <= nextSemester; if (!isOverdueRequired && selected.Count > 0 && credits + course.Credits > targetCredits) continue; selected.Add(course.CourseId); credits += course.Credits; if (credits >= targetCredits) break; } return selected; } public static int EstimateCompletionSemester( int nextSemester, int latestPlannedSemester, decimal remainingCredits, int remainingRequirementCount) { if (remainingCredits <= 0 && remainingRequirementCount <= 0) return Math.Max(nextSemester - 1, latestPlannedSemester); var byCredits = (int)Math.Ceiling( Math.Max(remainingCredits, 0) / RecommendedSemesterCreditLimit); var byRequirements = (int)Math.Ceiling( Math.Max(remainingRequirementCount, 0) / 6m); var additionalSemesters = Math.Max(1, Math.Max(byCredits, byRequirements)); return Math.Max(nextSemester - 1, latestPlannedSemester) + additionalSemesters; } }