using Jiaowu.Api.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; namespace Jiaowu.Api.Infrastructure.Grades; public static class ExperimentGradeAggregationService { public static async Task RefreshTeachingTaskAsync( AppDbContext db, Guid teachingTaskId, CancellationToken cancellationToken) { var sheets = await LoadPublishedSheetsAsync( db, teachingTaskId, cancellationToken); var existing = await db.ExperimentCourseGrades .Where(x => x.TeachingTaskId == teachingTaskId) .ToDictionaryAsync(x => x.StudentId, cancellationToken); var studentIds = sheets .SelectMany(x => x.Scores.Select(score => score.StudentId)) .Distinct() .ToArray(); var refreshedAt = DateTime.UtcNow; var totalWeight = sheets.Sum(x => x.ContributionWeight); foreach (var studentId in studentIds) { var score = CalculateStudentScore(sheets, studentId); if (!existing.Remove(studentId, out var aggregate)) { aggregate = new Domain.Academic.ExperimentCourseGrade { TeachingTaskId = teachingTaskId, StudentId = studentId }; db.ExperimentCourseGrades.Add(aggregate); } aggregate.WeightedAverageScore = score; aggregate.TotalWeight = totalWeight; aggregate.PublishedProjectCount = sheets.Count; aggregate.RefreshedAt = refreshedAt; } db.ExperimentCourseGrades.RemoveRange(existing.Values); await db.SaveChangesAsync(cancellationToken); } public static async Task CalculateAsync( AppDbContext db, Guid teachingTaskId, IReadOnlyCollection studentIds, CancellationToken cancellationToken) { var sheets = await LoadPublishedSheetsAsync( db, teachingTaskId, cancellationToken); var requestedStudentIds = studentIds.Distinct().ToArray(); var persistedScores = await db.ExperimentCourseGrades.AsNoTracking() .Where(x => x.TeachingTaskId == teachingTaskId) .WhereIn(requestedStudentIds, x => x.StudentId) .ToDictionaryAsync( x => x.StudentId, x => x.WeightedAverageScore, cancellationToken); var scores = requestedStudentIds.ToDictionary( studentId => studentId, studentId => persistedScores.GetValueOrDefault(studentId)); return new ExperimentGradeAggregateResult( sheets.Count, sheets.Select(x => new ExperimentGradeAggregateProject( x.Id, x.Code, x.Name, x.ContributionWeight)).ToList(), scores); } private static Task> LoadPublishedSheetsAsync( AppDbContext db, Guid teachingTaskId, CancellationToken cancellationToken) => db.ExperimentGradeSheets.AsNoTracking() .Where(x => x.Status == Domain.Academic.ExperimentGradeSheetStatus.Published && x.ExperimentProject!.TeachingTaskId == teachingTaskId) .OrderBy(x => x.ExperimentProject!.Code) .Select(x => new PublishedExperimentSheet( x.Id, x.ExperimentProject!.Code, x.ExperimentProject.Name, x.ContributionWeight, x.Records.Select(record => new PublishedExperimentScore( record.StudentId, record.TotalScore)).ToList())) .AsSplitQuery() .ToListAsync(cancellationToken); private static decimal? CalculateStudentScore( IReadOnlyCollection sheets, Guid studentId) { decimal weightedTotal = 0; decimal totalWeight = 0; if (sheets.Count == 0) return null; foreach (var sheet in sheets) { var score = sheet.Scores.FirstOrDefault(x => x.StudentId == studentId); if (score?.TotalScore is not decimal totalScore) return null; weightedTotal += totalScore * sheet.ContributionWeight; totalWeight += sheet.ContributionWeight; } return totalWeight > 0 ? Math.Round( weightedTotal / totalWeight, 1, MidpointRounding.AwayFromZero) : null; } private sealed record PublishedExperimentSheet( Guid Id, string Code, string Name, decimal ContributionWeight, List Scores); private sealed record PublishedExperimentScore( Guid StudentId, decimal? TotalScore); } public sealed record ExperimentGradeAggregateResult( int PublishedProjectCount, IReadOnlyList Projects, IReadOnlyDictionary Scores); public sealed record ExperimentGradeAggregateProject( Guid Id, string Code, string Name, decimal ContributionWeight);