147 lines
5.2 KiB
C#
147 lines
5.2 KiB
C#
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<ExperimentGradeAggregateResult> CalculateAsync(
|
|
AppDbContext db,
|
|
Guid teachingTaskId,
|
|
IReadOnlyCollection<Guid> 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<List<PublishedExperimentSheet>> 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<PublishedExperimentSheet> 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<PublishedExperimentScore> Scores);
|
|
|
|
private sealed record PublishedExperimentScore(
|
|
Guid StudentId,
|
|
decimal? TotalScore);
|
|
}
|
|
|
|
public sealed record ExperimentGradeAggregateResult(
|
|
int PublishedProjectCount,
|
|
IReadOnlyList<ExperimentGradeAggregateProject> Projects,
|
|
IReadOnlyDictionary<Guid, decimal?> Scores);
|
|
|
|
public sealed record ExperimentGradeAggregateProject(
|
|
Guid Id,
|
|
string Code,
|
|
string Name,
|
|
decimal ContributionWeight);
|