实验成绩计算

This commit is contained in:
biss committed 2026-08-09 22:40:21 +08:00
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