实验成绩计算
This commit is contained in:
1 parent
d4abdd3db4
commit
c25f16ec20
12 files changed
+7653
-290
No files matched your search
@@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Data;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
@@ -138,7 +139,7 @@ public sealed class ExperimentGradesController(
|
||||
if (student is null)
|
||||
return ConflictProblem("当前账号未关联有效学生档案。");
|
||||
|
||||
return Ok(await db.ExperimentGradeRecords.AsNoTracking()
|
||||
var results = await db.ExperimentGradeRecords.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.StudentId == student.Id &&
|
||||
x.ExperimentGradeSheet!.Status ==
|
||||
@@ -157,12 +158,19 @@ public sealed class ExperimentGradesController(
|
||||
ProjectName =
|
||||
x.ExperimentGradeSheet.ExperimentProject.Name,
|
||||
x.ExperimentGradeSheet.ExperimentProject.ArrangementMode,
|
||||
TeachingTaskId = x.ExperimentGradeSheet.ExperimentProject
|
||||
.TeachingTaskId,
|
||||
AcademicTermId = x.ExperimentGradeSheet.ExperimentProject
|
||||
.TeachingTask!.AcademicTermId,
|
||||
TaskNumber = x.ExperimentGradeSheet.ExperimentProject
|
||||
.TeachingTask.TaskNumber,
|
||||
CourseCode = x.ExperimentGradeSheet.ExperimentProject
|
||||
.TeachingTask!.Course!.Code,
|
||||
CourseName = x.ExperimentGradeSheet.ExperimentProject
|
||||
.TeachingTask.Course.Name,
|
||||
TermName = x.ExperimentGradeSheet.ExperimentProject
|
||||
.TeachingTask.AcademicTerm!.Name,
|
||||
x.ExperimentGradeSheet.ContributionWeight,
|
||||
x.ExperimentGradeSheet.PassScore,
|
||||
x.ParticipationStatus,
|
||||
x.TotalScore,
|
||||
@@ -187,7 +195,40 @@ public sealed class ExperimentGradesController(
|
||||
}),
|
||||
x.ExperimentGradeSheet.PublishedAt
|
||||
})
|
||||
.ToListAsync(cancellationToken));
|
||||
.ToListAsync(cancellationToken);
|
||||
var taskIds = results.Select(x => x.TeachingTaskId).Distinct().ToArray();
|
||||
var aggregates = await db.ExperimentCourseGrades.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.StudentId == student.Id &&
|
||||
taskIds.Contains(x.TeachingTaskId))
|
||||
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
|
||||
var courses = results
|
||||
.GroupBy(x => x.TeachingTaskId)
|
||||
.Select(group =>
|
||||
{
|
||||
var first = group.First();
|
||||
aggregates.TryGetValue(group.Key, out var aggregate);
|
||||
return new
|
||||
{
|
||||
TeachingTaskId = group.Key,
|
||||
first.AcademicTermId,
|
||||
first.TermName,
|
||||
first.TaskNumber,
|
||||
first.CourseCode,
|
||||
first.CourseName,
|
||||
ExperimentCourseScore = aggregate?.WeightedAverageScore,
|
||||
PublishedProjectCount =
|
||||
aggregate?.PublishedProjectCount ?? group.Count(),
|
||||
TotalWeight = aggregate?.TotalWeight ??
|
||||
group.Sum(x => x.ContributionWeight),
|
||||
RefreshedAt = aggregate?.RefreshedAt,
|
||||
Projects = group.OrderBy(x => x.ProjectCode).ToList()
|
||||
};
|
||||
})
|
||||
.OrderByDescending(x => x.Projects.Max(project => project.PublishedAt))
|
||||
.ThenBy(x => x.CourseCode)
|
||||
.ToList();
|
||||
return Ok(courses);
|
||||
}
|
||||
|
||||
[HttpPost("sheets")]
|
||||
@@ -744,7 +785,15 @@ public sealed class ExperimentGradesController(
|
||||
|
||||
sheet.Status = ExperimentGradeSheetStatus.Published;
|
||||
sheet.PublishedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
await db.ExecuteInRetriableTransactionAsync(async transaction =>
|
||||
{
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
await ExperimentGradeAggregationService.RefreshTeachingTaskAsync(
|
||||
db,
|
||||
sheet.ExperimentProject!.TeachingTaskId,
|
||||
cancellationToken);
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}, cancellationToken, IsolationLevel.Serializable);
|
||||
var userIds = await db.ExperimentGradeRecords
|
||||
.Where(x => x.ExperimentGradeSheetId == sheet.Id)
|
||||
.Select(x => x.Student!.UserId)
|
||||
|
||||
@@ -51,6 +51,22 @@ public sealed class ExperimentGradeRecord : EntityBase
|
||||
public ICollection<ExperimentGradeItemScore> ItemScores { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A student's persisted experiment-part score for one teaching task.
|
||||
/// The score is the weighted average of every published experiment project.
|
||||
/// </summary>
|
||||
public sealed class ExperimentCourseGrade : EntityBase
|
||||
{
|
||||
public Guid TeachingTaskId { get; set; }
|
||||
public TeachingTask? TeachingTask { get; set; }
|
||||
public Guid StudentId { get; set; }
|
||||
public Student? Student { get; set; }
|
||||
public decimal? WeightedAverageScore { get; set; }
|
||||
public decimal TotalWeight { get; set; }
|
||||
public int PublishedProjectCount { get; set; }
|
||||
public DateTime RefreshedAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ExperimentGradeItemScore
|
||||
{
|
||||
public Guid ExperimentGradeRecordId { get; set; }
|
||||
|
||||
@@ -5,13 +5,85 @@ 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 db.ExperimentGradeSheets.AsNoTracking()
|
||||
var sheets = await LoadPublishedSheetsAsync(
|
||||
db,
|
||||
teachingTaskId,
|
||||
cancellationToken);
|
||||
var requestedStudentIds = studentIds.Distinct().ToArray();
|
||||
var persistedScores = await db.ExperimentCourseGrades.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.TeachingTaskId == teachingTaskId &&
|
||||
requestedStudentIds.Contains(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 &&
|
||||
@@ -28,40 +100,27 @@ public static class ExperimentGradeAggregationService
|
||||
.AsSplitQuery()
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var scores = new Dictionary<Guid, decimal?>();
|
||||
foreach (var studentId in studentIds.Distinct())
|
||||
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)
|
||||
{
|
||||
decimal weightedTotal = 0;
|
||||
decimal totalWeight = 0;
|
||||
var complete = sheets.Count > 0;
|
||||
foreach (var sheet in sheets)
|
||||
{
|
||||
var score = sheet.Scores.FirstOrDefault(x =>
|
||||
x.StudentId == studentId);
|
||||
if (score?.TotalScore is not decimal totalScore)
|
||||
{
|
||||
complete = false;
|
||||
break;
|
||||
}
|
||||
weightedTotal += totalScore * sheet.ContributionWeight;
|
||||
totalWeight += sheet.ContributionWeight;
|
||||
}
|
||||
scores[studentId] = complete && totalWeight > 0
|
||||
? Math.Round(
|
||||
weightedTotal / totalWeight,
|
||||
1,
|
||||
MidpointRounding.AwayFromZero)
|
||||
: null;
|
||||
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 new ExperimentGradeAggregateResult(
|
||||
sheets.Count,
|
||||
sheets.Select(x => new ExperimentGradeAggregateProject(
|
||||
x.Id,
|
||||
x.Code,
|
||||
x.Name,
|
||||
x.ContributionWeight)).ToList(),
|
||||
scores);
|
||||
return totalWeight > 0
|
||||
? Math.Round(
|
||||
weightedTotal / totalWeight,
|
||||
1,
|
||||
MidpointRounding.AwayFromZero)
|
||||
: null;
|
||||
}
|
||||
|
||||
private sealed record PublishedExperimentSheet(
|
||||
|
||||
Loaded 3 of 12 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user