247 lines
10 KiB
C#
247 lines
10 KiB
C#
using Jiaowu.Api.Domain.Academic;
|
|
using Jiaowu.Api.Domain.Identity;
|
|
using Jiaowu.Api.Infrastructure.Auth;
|
|
using Jiaowu.Api.Infrastructure.Graduation;
|
|
using Jiaowu.Api.Infrastructure.Persistence;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Jiaowu.Api.Controllers;
|
|
|
|
[ApiController]
|
|
[Authorize(Roles = SystemRoles.Student)]
|
|
[Route("api/student/curriculum-plan")]
|
|
public sealed class StudentCurriculumController(
|
|
AppDbContext db,
|
|
ICurrentUserDataScope currentUserDataScope) : ControllerBase
|
|
{
|
|
[HttpGet]
|
|
public async Task<ActionResult> GetMine(CancellationToken cancellationToken)
|
|
{
|
|
var userId = currentUserDataScope.Current.UserId;
|
|
var student = await db.Students.AsNoTracking()
|
|
.Include(x => x.AdministrativeClass)
|
|
.ThenInclude(x => x!.Major)
|
|
.ThenInclude(x => x!.College)
|
|
.FirstOrDefaultAsync(x => x.UserId == userId, cancellationToken);
|
|
if (student is null)
|
|
return ConflictProblem("当前账号尚未关联学生档案。");
|
|
|
|
var studentProfile = new
|
|
{
|
|
student.Id,
|
|
student.StudentNumber,
|
|
student.Name,
|
|
student.EnrollmentYear,
|
|
student.Status,
|
|
ClassName = student.AdministrativeClass!.Name,
|
|
MajorName = student.AdministrativeClass.Major!.Name,
|
|
CollegeName = student.AdministrativeClass.Major.College!.Name
|
|
};
|
|
|
|
var plan = await db.CurriculumPlans.AsNoTracking()
|
|
.AsSplitQuery()
|
|
.Include(x => x.Major)
|
|
.ThenInclude(x => x!.College)
|
|
.Include(x => x.Modules)
|
|
.ThenInclude(x => x.Courses)
|
|
.ThenInclude(x => x.Course)
|
|
.ThenInclude(x => x!.CourseCategory)
|
|
.Where(x =>
|
|
x.MajorId == student.AdministrativeClass.MajorId &&
|
|
x.EffectiveGrade == student.EnrollmentYear &&
|
|
x.Status == CurriculumPlanStatus.Published)
|
|
.OrderByDescending(x => x.PublishedAt)
|
|
.FirstOrDefaultAsync(cancellationToken);
|
|
|
|
if (plan is null)
|
|
{
|
|
return Ok(new
|
|
{
|
|
Student = studentProfile,
|
|
Plan = (object?)null,
|
|
Message =
|
|
$"{student.EnrollmentYear} 级{student.AdministrativeClass.Major.Name}尚未发布培养方案。"
|
|
});
|
|
}
|
|
|
|
var planCourseIds = plan.Modules
|
|
.SelectMany(x => x.Courses)
|
|
.Select(x => x.CourseId)
|
|
.Distinct()
|
|
.ToArray();
|
|
var gradeAttempts = await db.GradeRecords.AsNoTracking()
|
|
.Where(x =>
|
|
x.StudentId == student.Id &&
|
|
x.GradeSheet!.Status == GradeSheetStatus.Published)
|
|
.WhereIn(planCourseIds, x => x.GradeSheet!.TeachingTask!.CourseId)
|
|
.Select(x => new StudentGradeAttempt(
|
|
x.GradeSheet!.TeachingTask!.CourseId,
|
|
x.TotalScore,
|
|
x.GradePoint,
|
|
x.ExamStatus,
|
|
x.GradeSheet.TeachingTask.AcademicTerm!.Name,
|
|
x.GradeSheet.TeachingTask.TaskNumber,
|
|
x.GradeSheet.PublishedAt ?? x.GradeSheet.UpdatedAt))
|
|
.ToListAsync(cancellationToken);
|
|
|
|
var currentCourseIds = await db.TeachingTasks.AsNoTracking()
|
|
.Where(task =>
|
|
task.Status == TeachingTaskStatus.Published &&
|
|
task.AcademicTerm!.IsCurrent &&
|
|
(task.Classes.Any(item =>
|
|
item.AdministrativeClassId == student.AdministrativeClassId) ||
|
|
db.CourseEnrollments.Any(enrollment =>
|
|
enrollment.StudentId == student.Id &&
|
|
enrollment.Status == CourseEnrollmentStatus.Enrolled &&
|
|
enrollment.CourseSelectionOffering!.TeachingTaskId == task.Id)))
|
|
.WhereIn(planCourseIds, task => task.CourseId)
|
|
.Select(x => x.CourseId)
|
|
.Distinct()
|
|
.ToListAsync(cancellationToken);
|
|
var currentCourseIdSet = currentCourseIds.ToHashSet();
|
|
var attemptsByCourse = gradeAttempts
|
|
.GroupBy(x => x.CourseId)
|
|
.ToDictionary(x => x.Key, x => x.OrderByDescending(
|
|
attempt => attempt.PublishedAt).ToList());
|
|
|
|
var progressByCourse = new Dictionary<Guid, CourseProgress>();
|
|
foreach (var courseId in planCourseIds)
|
|
{
|
|
var attempts = attemptsByCourse.GetValueOrDefault(courseId) ?? [];
|
|
var evaluation = StudentCourseProgressRules.Evaluate(
|
|
attempts.Select(x => new StudentCourseAttemptSnapshot(
|
|
x.TotalScore,
|
|
x.ExamStatus)),
|
|
currentCourseIdSet.Contains(courseId));
|
|
var displayedAttempt = evaluation.HasPassedAttempt
|
|
? attempts.FirstOrDefault(x => StudentCourseProgressRules.IsPassed(
|
|
new StudentCourseAttemptSnapshot(x.TotalScore, x.ExamStatus)))
|
|
: attempts.FirstOrDefault();
|
|
progressByCourse[courseId] = new CourseProgress(
|
|
evaluation,
|
|
displayedAttempt);
|
|
}
|
|
|
|
var distinctCourses = plan.Modules
|
|
.SelectMany(x => x.Courses)
|
|
.GroupBy(x => x.CourseId)
|
|
.Select(x => x.First())
|
|
.ToList();
|
|
var completedCredits = distinctCourses
|
|
.Where(x =>
|
|
progressByCourse[x.CourseId].Evaluation.Status ==
|
|
StudentCourseProgressStatus.Completed)
|
|
.Sum(x => x.Course?.Credits ?? 0);
|
|
var completedCourseCount = distinctCourses.Count(x =>
|
|
progressByCourse[x.CourseId].Evaluation.Status ==
|
|
StudentCourseProgressStatus.Completed);
|
|
var inProgressCourseCount = distinctCourses.Count(x =>
|
|
progressByCourse[x.CourseId].Evaluation.Status is
|
|
StudentCourseProgressStatus.InProgress or
|
|
StudentCourseProgressStatus.Retaking);
|
|
var failedCourseCount = distinctCourses.Count(x =>
|
|
progressByCourse[x.CourseId].Evaluation.HasFailedAttempt &&
|
|
!progressByCourse[x.CourseId].Evaluation.HasPassedAttempt);
|
|
|
|
return Ok(new
|
|
{
|
|
Student = studentProfile,
|
|
Plan = new
|
|
{
|
|
plan.Id,
|
|
plan.Name,
|
|
plan.Version,
|
|
plan.EffectiveGrade,
|
|
plan.TotalCredits,
|
|
plan.Description,
|
|
plan.PublishedAt,
|
|
MajorName = plan.Major!.Name,
|
|
CollegeName = plan.Major.College!.Name,
|
|
SchoolingYears = plan.Major.SchoolingYears,
|
|
Summary = new
|
|
{
|
|
CourseCount = distinctCourses.Count,
|
|
CompletedCourseCount = completedCourseCount,
|
|
InProgressCourseCount = inProgressCourseCount,
|
|
FailedCourseCount = failedCourseCount,
|
|
CompletedCredits = completedCredits,
|
|
CompletionRate = plan.TotalCredits <= 0
|
|
? 0
|
|
: Math.Min(100, Math.Round(
|
|
completedCredits / plan.TotalCredits * 100,
|
|
1))
|
|
},
|
|
Modules = plan.Modules
|
|
.OrderBy(x => x.SortOrder)
|
|
.ThenBy(x => x.Code)
|
|
.Select(module => new
|
|
{
|
|
module.Id,
|
|
module.Code,
|
|
module.Name,
|
|
module.RequiredCredits,
|
|
module.SortOrder,
|
|
Courses = module.Courses
|
|
.OrderBy(x => x.RecommendedSemester)
|
|
.ThenBy(x => x.Course!.Code)
|
|
.Select(item =>
|
|
{
|
|
var progress = progressByCourse[item.CourseId];
|
|
return new
|
|
{
|
|
item.Id,
|
|
item.CourseId,
|
|
CourseCode = item.Course!.Code,
|
|
CourseName = item.Course.Name,
|
|
item.Course.Credits,
|
|
item.Course.TotalHours,
|
|
item.Course.Nature,
|
|
CategoryName = item.Course.CourseCategory?.Name,
|
|
item.RecommendedSemester,
|
|
item.Type,
|
|
item.Notes,
|
|
progress.Evaluation.Status,
|
|
progress.Evaluation.HasFailedAttempt,
|
|
progress.Evaluation.HasPassedAttempt,
|
|
LatestResult = progress.DisplayedAttempt is null
|
|
? null
|
|
: new
|
|
{
|
|
progress.DisplayedAttempt.TotalScore,
|
|
progress.DisplayedAttempt.GradePoint,
|
|
progress.DisplayedAttempt.ExamStatus,
|
|
progress.DisplayedAttempt.TermName,
|
|
progress.DisplayedAttempt.TaskNumber,
|
|
progress.DisplayedAttempt.PublishedAt
|
|
}
|
|
};
|
|
})
|
|
})
|
|
}
|
|
});
|
|
}
|
|
|
|
private ActionResult ConflictProblem(string detail) =>
|
|
Conflict(new ProblemDetails
|
|
{
|
|
Title = "无法查看培养方案",
|
|
Detail = detail,
|
|
Status = StatusCodes.Status409Conflict
|
|
});
|
|
|
|
private sealed record StudentGradeAttempt(
|
|
Guid CourseId,
|
|
decimal? TotalScore,
|
|
decimal? GradePoint,
|
|
GradeExamStatus ExamStatus,
|
|
string TermName,
|
|
string TaskNumber,
|
|
DateTime PublishedAt);
|
|
|
|
private sealed record CourseProgress(
|
|
StudentCourseProgressEvaluation Evaluation,
|
|
StudentGradeAttempt? DisplayedAttempt);
|
|
}
|