实验成绩计算
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -58,6 +58,8 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
Set<ExperimentGradeItem>();
|
||||
public DbSet<ExperimentGradeRecord> ExperimentGradeRecords =>
|
||||
Set<ExperimentGradeRecord>();
|
||||
public DbSet<ExperimentCourseGrade> ExperimentCourseGrades =>
|
||||
Set<ExperimentCourseGrade>();
|
||||
public DbSet<ExperimentGradeItemScore> ExperimentGradeItemScores =>
|
||||
Set<ExperimentGradeItemScore>();
|
||||
public DbSet<CourseSelectionRound> CourseSelectionRounds =>
|
||||
@@ -795,6 +797,21 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
builder.Entity<ExperimentCourseGrade>(entity =>
|
||||
{
|
||||
entity.Property(x => x.WeightedAverageScore).HasPrecision(5, 1);
|
||||
entity.Property(x => x.TotalWeight).HasPrecision(8, 1);
|
||||
entity.HasIndex(x => new { x.TeachingTaskId, x.StudentId })
|
||||
.IsUnique();
|
||||
entity.HasIndex(x => x.StudentId);
|
||||
entity.HasOne(x => x.TeachingTask).WithMany()
|
||||
.HasForeignKey(x => x.TeachingTaskId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
entity.HasOne(x => x.Student).WithMany()
|
||||
.HasForeignKey(x => x.StudentId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
builder.Entity<ExperimentGradeItemScore>(entity =>
|
||||
{
|
||||
entity.HasKey(x => new
|
||||
|
||||
@@ -100,6 +100,8 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"20260809_52_reusable_course_groups";
|
||||
private const string CourseGradeStatisticsRefreshSettingsMigration =
|
||||
"20260809_53_course_grade_statistics_refresh_settings";
|
||||
private const string ExperimentCourseGradesMigration =
|
||||
"20260809_54_experiment_course_grades";
|
||||
|
||||
public async Task MigrateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -721,6 +723,14 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
ReusableCourseGroupsMigration,
|
||||
courseGroupsExist ? [] : ReusableCourseGroupsStatements,
|
||||
cancellationToken);
|
||||
var experimentCourseGradesExist = await db.Database
|
||||
.SqlQueryRaw<int>(
|
||||
"SELECT COUNT(*) AS \"Value\" FROM sqlite_master WHERE type = 'table' AND name = 'ExperimentCourseGrades'")
|
||||
.AnyAsync(value => value > 0, cancellationToken);
|
||||
await ApplyMigrationAsync(
|
||||
ExperimentCourseGradesMigration,
|
||||
experimentCourseGradesExist ? [] : ExperimentCourseGradesStatements,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ApplyMigrationAsync(
|
||||
@@ -2989,6 +2999,80 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"""CREATE UNIQUE INDEX IF NOT EXISTS "IX_CourseGradeStatisticsRefreshSettings_Key" ON "CourseGradeStatisticsRefreshSettings" ("Key");"""
|
||||
];
|
||||
|
||||
private static readonly string[] ExperimentCourseGradesStatements =
|
||||
[
|
||||
"""
|
||||
CREATE TABLE "ExperimentCourseGrades" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_ExperimentCourseGrades" PRIMARY KEY,
|
||||
"TeachingTaskId" TEXT NOT NULL,
|
||||
"StudentId" TEXT NOT NULL,
|
||||
"WeightedAverageScore" TEXT NULL,
|
||||
"TotalWeight" TEXT NOT NULL,
|
||||
"PublishedProjectCount" INTEGER NOT NULL,
|
||||
"RefreshedAt" TEXT NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_ExperimentCourseGrades_TeachingTasks_TeachingTaskId"
|
||||
FOREIGN KEY ("TeachingTaskId") REFERENCES "TeachingTasks" ("Id") ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_ExperimentCourseGrades_Students_StudentId"
|
||||
FOREIGN KEY ("StudentId") REFERENCES "Students" ("Id") ON DELETE RESTRICT
|
||||
);
|
||||
""",
|
||||
"""CREATE INDEX "IX_ExperimentCourseGrades_StudentId" ON "ExperimentCourseGrades" ("StudentId");""",
|
||||
"""CREATE UNIQUE INDEX "IX_ExperimentCourseGrades_TeachingTaskId_StudentId" ON "ExperimentCourseGrades" ("TeachingTaskId", "StudentId");""",
|
||||
"""
|
||||
INSERT INTO "ExperimentCourseGrades"
|
||||
("Id", "TeachingTaskId", "StudentId", "WeightedAverageScore",
|
||||
"TotalWeight", "PublishedProjectCount", "RefreshedAt",
|
||||
"CreatedAt", "UpdatedAt")
|
||||
SELECT
|
||||
lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-' ||
|
||||
'4' || substr(lower(hex(randomblob(2))), 2) || '-' ||
|
||||
substr('89ab', abs(random()) % 4 + 1, 1) ||
|
||||
substr(lower(hex(randomblob(2))), 2) || '-' || lower(hex(randomblob(6))),
|
||||
project."TeachingTaskId",
|
||||
record."StudentId",
|
||||
CASE
|
||||
WHEN COUNT(*) = (
|
||||
SELECT COUNT(*)
|
||||
FROM "ExperimentGradeSheets" AS all_sheet
|
||||
INNER JOIN "ExperimentProjects" AS all_project
|
||||
ON all_project."Id" = all_sheet."ExperimentProjectId"
|
||||
WHERE all_sheet."Status" = 4
|
||||
AND all_project."TeachingTaskId" = project."TeachingTaskId")
|
||||
AND SUM(CASE WHEN record."TotalScore" IS NULL THEN 1 ELSE 0 END) = 0
|
||||
THEN ROUND(
|
||||
SUM(record."TotalScore" * sheet."ContributionWeight") /
|
||||
SUM(sheet."ContributionWeight"), 1)
|
||||
ELSE NULL
|
||||
END,
|
||||
(
|
||||
SELECT COALESCE(SUM(all_sheet."ContributionWeight"), 0)
|
||||
FROM "ExperimentGradeSheets" AS all_sheet
|
||||
INNER JOIN "ExperimentProjects" AS all_project
|
||||
ON all_project."Id" = all_sheet."ExperimentProjectId"
|
||||
WHERE all_sheet."Status" = 4
|
||||
AND all_project."TeachingTaskId" = project."TeachingTaskId"),
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM "ExperimentGradeSheets" AS all_sheet
|
||||
INNER JOIN "ExperimentProjects" AS all_project
|
||||
ON all_project."Id" = all_sheet."ExperimentProjectId"
|
||||
WHERE all_sheet."Status" = 4
|
||||
AND all_project."TeachingTaskId" = project."TeachingTaskId"),
|
||||
strftime('%Y-%m-%d %H:%M:%f', 'now'),
|
||||
strftime('%Y-%m-%d %H:%M:%f', 'now'),
|
||||
strftime('%Y-%m-%d %H:%M:%f', 'now')
|
||||
FROM "ExperimentGradeRecords" AS record
|
||||
INNER JOIN "ExperimentGradeSheets" AS sheet
|
||||
ON sheet."Id" = record."ExperimentGradeSheetId"
|
||||
INNER JOIN "ExperimentProjects" AS project
|
||||
ON project."Id" = sheet."ExperimentProjectId"
|
||||
WHERE sheet."Status" = 4
|
||||
GROUP BY project."TeachingTaskId", record."StudentId";
|
||||
"""
|
||||
];
|
||||
|
||||
private static readonly string[] ExperimentClassroomConstraintStatements =
|
||||
[
|
||||
"""
|
||||
|
||||
+6934
File diff suppressed because it is too large
Load Diff
+113
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class PersistExperimentCourseGrades : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ExperimentCourseGrades",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
TeachingTaskId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
StudentId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
WeightedAverageScore = table.Column<decimal>(type: "decimal(5,1)", precision: 5, scale: 1, nullable: true),
|
||||
TotalWeight = table.Column<decimal>(type: "decimal(8,1)", precision: 8, scale: 1, nullable: false),
|
||||
PublishedProjectCount = table.Column<int>(type: "int", nullable: false),
|
||||
RefreshedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ExperimentCourseGrades", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ExperimentCourseGrades_Students_StudentId",
|
||||
column: x => x.StudentId,
|
||||
principalTable: "Students",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_ExperimentCourseGrades_TeachingTasks_TeachingTaskId",
|
||||
column: x => x.TeachingTaskId,
|
||||
principalTable: "TeachingTasks",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySQL:Charset", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExperimentCourseGrades_StudentId",
|
||||
table: "ExperimentCourseGrades",
|
||||
column: "StudentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExperimentCourseGrades_TeachingTaskId_StudentId",
|
||||
table: "ExperimentCourseGrades",
|
||||
columns: new[] { "TeachingTaskId", "StudentId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
INSERT INTO `ExperimentCourseGrades`
|
||||
(`Id`, `TeachingTaskId`, `StudentId`, `WeightedAverageScore`,
|
||||
`TotalWeight`, `PublishedProjectCount`, `RefreshedAt`,
|
||||
`CreatedAt`, `UpdatedAt`)
|
||||
SELECT
|
||||
UUID(),
|
||||
project.`TeachingTaskId`,
|
||||
record.`StudentId`,
|
||||
CASE
|
||||
WHEN COUNT(*) = (
|
||||
SELECT COUNT(*)
|
||||
FROM `ExperimentGradeSheets` AS all_sheet
|
||||
INNER JOIN `ExperimentProjects` AS all_project
|
||||
ON all_project.`Id` = all_sheet.`ExperimentProjectId`
|
||||
WHERE all_sheet.`Status` = 4
|
||||
AND all_project.`TeachingTaskId` = project.`TeachingTaskId`)
|
||||
AND SUM(CASE WHEN record.`TotalScore` IS NULL THEN 1 ELSE 0 END) = 0
|
||||
THEN ROUND(
|
||||
SUM(record.`TotalScore` * sheet.`ContributionWeight`) /
|
||||
SUM(sheet.`ContributionWeight`), 1)
|
||||
ELSE NULL
|
||||
END,
|
||||
(
|
||||
SELECT COALESCE(SUM(all_sheet.`ContributionWeight`), 0)
|
||||
FROM `ExperimentGradeSheets` AS all_sheet
|
||||
INNER JOIN `ExperimentProjects` AS all_project
|
||||
ON all_project.`Id` = all_sheet.`ExperimentProjectId`
|
||||
WHERE all_sheet.`Status` = 4
|
||||
AND all_project.`TeachingTaskId` = project.`TeachingTaskId`),
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM `ExperimentGradeSheets` AS all_sheet
|
||||
INNER JOIN `ExperimentProjects` AS all_project
|
||||
ON all_project.`Id` = all_sheet.`ExperimentProjectId`
|
||||
WHERE all_sheet.`Status` = 4
|
||||
AND all_project.`TeachingTaskId` = project.`TeachingTaskId`),
|
||||
UTC_TIMESTAMP(6), UTC_TIMESTAMP(6), UTC_TIMESTAMP(6)
|
||||
FROM `ExperimentGradeRecords` AS record
|
||||
INNER JOIN `ExperimentGradeSheets` AS sheet
|
||||
ON sheet.`Id` = record.`ExperimentGradeSheetId`
|
||||
INNER JOIN `ExperimentProjects` AS project
|
||||
ON project.`Id` = sheet.`ExperimentProjectId`
|
||||
WHERE sheet.`Status` = 4
|
||||
GROUP BY project.`TeachingTaskId`, record.`StudentId`;
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ExperimentCourseGrades");
|
||||
}
|
||||
}
|
||||
}
|
||||
+63
-2
@@ -2263,6 +2263,48 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.ToTable("ExperimentBookings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentCourseGrade", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("PublishedProjectCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("RefreshedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<Guid>("StudentId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("TeachingTaskId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<decimal>("TotalWeight")
|
||||
.HasPrecision(8, 1)
|
||||
.HasColumnType("decimal(8,1)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal?>("WeightedAverageScore")
|
||||
.HasPrecision(5, 1)
|
||||
.HasColumnType("decimal(5,1)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StudentId");
|
||||
|
||||
b.HasIndex("TeachingTaskId", "StudentId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ExperimentCourseGrades");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -2494,11 +2536,11 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
|
||||
b.HasIndex("ScheduleEntryId");
|
||||
|
||||
b.HasIndex("Status", "StartDate", "EndDate");
|
||||
|
||||
b.HasIndex("TeachingTaskId", "Code", "ScheduleEntryId", "ScheduleWeek")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Status", "StartDate", "EndDate");
|
||||
|
||||
b.ToTable("ExperimentProjects");
|
||||
});
|
||||
|
||||
@@ -5803,6 +5845,25 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.Navigation("Student");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentCourseGrade", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student")
|
||||
.WithMany()
|
||||
.HasForeignKey("StudentId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask")
|
||||
.WithMany()
|
||||
.HasForeignKey("TeachingTaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Student");
|
||||
|
||||
b.Navigation("TeachingTask");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItem", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", "ExperimentGradeSheet")
|
||||
|
||||
Reference in New Issue
Block a user