实验成绩计算

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.ComponentModel.DataAnnotations;
using System.Data;
using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth; using Jiaowu.Api.Infrastructure.Auth;
@@ -138,7 +139,7 @@ public sealed class ExperimentGradesController(
if (student is null) if (student is null)
return ConflictProblem("当前账号未关联有效学生档案。"); return ConflictProblem("当前账号未关联有效学生档案。");
return Ok(await db.ExperimentGradeRecords.AsNoTracking() var results = await db.ExperimentGradeRecords.AsNoTracking()
.Where(x => .Where(x =>
x.StudentId == student.Id && x.StudentId == student.Id &&
x.ExperimentGradeSheet!.Status == x.ExperimentGradeSheet!.Status ==
@@ -157,12 +158,19 @@ public sealed class ExperimentGradesController(
ProjectName = ProjectName =
x.ExperimentGradeSheet.ExperimentProject.Name, x.ExperimentGradeSheet.ExperimentProject.Name,
x.ExperimentGradeSheet.ExperimentProject.ArrangementMode, 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 CourseCode = x.ExperimentGradeSheet.ExperimentProject
.TeachingTask!.Course!.Code, .TeachingTask!.Course!.Code,
CourseName = x.ExperimentGradeSheet.ExperimentProject CourseName = x.ExperimentGradeSheet.ExperimentProject
.TeachingTask.Course.Name, .TeachingTask.Course.Name,
TermName = x.ExperimentGradeSheet.ExperimentProject TermName = x.ExperimentGradeSheet.ExperimentProject
.TeachingTask.AcademicTerm!.Name, .TeachingTask.AcademicTerm!.Name,
x.ExperimentGradeSheet.ContributionWeight,
x.ExperimentGradeSheet.PassScore, x.ExperimentGradeSheet.PassScore,
x.ParticipationStatus, x.ParticipationStatus,
x.TotalScore, x.TotalScore,
@@ -187,7 +195,40 @@ public sealed class ExperimentGradesController(
}), }),
x.ExperimentGradeSheet.PublishedAt 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")] [HttpPost("sheets")]
@@ -744,7 +785,15 @@ public sealed class ExperimentGradesController(
sheet.Status = ExperimentGradeSheetStatus.Published; sheet.Status = ExperimentGradeSheetStatus.Published;
sheet.PublishedAt = DateTime.UtcNow; 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 var userIds = await db.ExperimentGradeRecords
.Where(x => x.ExperimentGradeSheetId == sheet.Id) .Where(x => x.ExperimentGradeSheetId == sheet.Id)
.Select(x => x.Student!.UserId) .Select(x => x.Student!.UserId)
@@ -51,6 +51,22 @@ public sealed class ExperimentGradeRecord : EntityBase
public ICollection<ExperimentGradeItemScore> ItemScores { get; set; } = []; 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 sealed class ExperimentGradeItemScore
{ {
public Guid ExperimentGradeRecordId { get; set; } public Guid ExperimentGradeRecordId { get; set; }
@@ -5,13 +5,85 @@ namespace Jiaowu.Api.Infrastructure.Grades;
public static class ExperimentGradeAggregationService 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( public static async Task<ExperimentGradeAggregateResult> CalculateAsync(
AppDbContext db, AppDbContext db,
Guid teachingTaskId, Guid teachingTaskId,
IReadOnlyCollection<Guid> studentIds, IReadOnlyCollection<Guid> studentIds,
CancellationToken cancellationToken) 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 => .Where(x =>
x.Status == x.Status ==
Domain.Academic.ExperimentGradeSheetStatus.Published && Domain.Academic.ExperimentGradeSheetStatus.Published &&
@@ -28,40 +100,27 @@ public static class ExperimentGradeAggregationService
.AsSplitQuery() .AsSplitQuery()
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var scores = new Dictionary<Guid, decimal?>(); private static decimal? CalculateStudentScore(
foreach (var studentId in studentIds.Distinct()) 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; var score = sheet.Scores.FirstOrDefault(x =>
decimal totalWeight = 0; x.StudentId == studentId);
var complete = sheets.Count > 0; if (score?.TotalScore is not decimal totalScore) return null;
foreach (var sheet in sheets) weightedTotal += totalScore * sheet.ContributionWeight;
{ totalWeight += sheet.ContributionWeight;
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;
} }
return totalWeight > 0
return new ExperimentGradeAggregateResult( ? Math.Round(
sheets.Count, weightedTotal / totalWeight,
sheets.Select(x => new ExperimentGradeAggregateProject( 1,
x.Id, MidpointRounding.AwayFromZero)
x.Code, : null;
x.Name,
x.ContributionWeight)).ToList(),
scores);
} }
private sealed record PublishedExperimentSheet( 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