成绩统计图
This commit is contained in:
@@ -16,6 +16,7 @@ public sealed class BackgroundJobOptions
|
||||
public int ExamArrangementConcurrency { get; set; } = 1;
|
||||
public int ExamSignInExportConcurrency { get; set; } = 1;
|
||||
public int ExamPublishConcurrency { get; set; } = 1;
|
||||
public int CourseGradeStatisticsRefreshConcurrency { get; set; } = 1;
|
||||
public string Exchange { get; set; } = "jiaowu.background-jobs";
|
||||
public string QueuePrefix { get; set; } = "jiaowu.background-jobs";
|
||||
public bool UseQuorumQueues { get; set; } = true;
|
||||
@@ -35,6 +36,8 @@ public sealed class BackgroundJobOptions
|
||||
BackgroundJobKind.ExamArrangement => ExamArrangementConcurrency,
|
||||
BackgroundJobKind.ExamSignInExport => ExamSignInExportConcurrency,
|
||||
BackgroundJobKind.ExamPublish => ExamPublishConcurrency,
|
||||
BackgroundJobKind.CourseGradeStatisticsRefresh =>
|
||||
CourseGradeStatisticsRefreshConcurrency,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -130,6 +130,15 @@ public sealed class BackgroundJobOutboxPublisher(
|
||||
message.JobId == x.Id))
|
||||
.Select(x => x.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
var gradeStatisticsJobs = await db.CourseGradeStatisticsRefreshJobs.AsNoTracking()
|
||||
.Where(x =>
|
||||
(x.Status == CourseGradeStatisticsRefreshJobStatus.Queued ||
|
||||
x.Status == CourseGradeStatisticsRefreshJobStatus.Running) &&
|
||||
!db.BackgroundJobOutboxMessages.Any(message =>
|
||||
message.JobKind == BackgroundJobKind.CourseGradeStatisticsRefresh &&
|
||||
message.JobId == x.Id))
|
||||
.Select(x => x.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var missingKeys = automaticJobs
|
||||
.Select(id => (BackgroundJobKind.AutomaticSchedule, id))
|
||||
@@ -143,6 +152,8 @@ public sealed class BackgroundJobOutboxPublisher(
|
||||
(BackgroundJobKind.ExamSignInExport, id)))
|
||||
.Concat(publishJobs2.Select(id =>
|
||||
(BackgroundJobKind.ExamPublish, id)))
|
||||
.Concat(gradeStatisticsJobs.Select(id =>
|
||||
(BackgroundJobKind.CourseGradeStatisticsRefresh, id)))
|
||||
.ToList();
|
||||
foreach (var (kind, jobId) in missingKeys)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Diagnostics;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.System;
|
||||
using Jiaowu.Api.Infrastructure.Exams;
|
||||
using Jiaowu.Api.Infrastructure.Grades;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Jiaowu.Api.Infrastructure.Scheduling;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -100,6 +101,11 @@ public sealed class BackgroundJobRunner(
|
||||
.GetRequiredService<ExamPublishJobProcessor>()
|
||||
.ProcessAsync(message.JobId, cancellationToken);
|
||||
break;
|
||||
case BackgroundJobKind.CourseGradeStatisticsRefresh:
|
||||
await scope.ServiceProvider
|
||||
.GetRequiredService<CourseGradeStatisticsRefreshJobProcessor>()
|
||||
.ProcessAsync(message.JobId, cancellationToken);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"Unsupported background job kind '{message.JobKind}'.");
|
||||
@@ -308,6 +314,19 @@ public sealed class BackgroundJobRunner(
|
||||
.SetProperty(x => x.CompletedAt, completedAt),
|
||||
cancellationToken);
|
||||
break;
|
||||
case BackgroundJobKind.CourseGradeStatisticsRefresh:
|
||||
await db.CourseGradeStatisticsRefreshJobs
|
||||
.Where(x => x.Id == message.JobId &&
|
||||
x.Status != CourseGradeStatisticsRefreshJobStatus.Succeeded &&
|
||||
x.Status != CourseGradeStatisticsRefreshJobStatus.Failed)
|
||||
.ExecuteUpdateAsync(
|
||||
setters => setters
|
||||
.SetProperty(x => x.Status,
|
||||
CourseGradeStatisticsRefreshJobStatus.Failed)
|
||||
.SetProperty(x => x.ErrorMessage, error)
|
||||
.SetProperty(x => x.CompletedAt, completedAt),
|
||||
cancellationToken);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(message.JobKind),
|
||||
|
||||
@@ -319,7 +319,8 @@ internal static class RabbitMqBackgroundJobTopology
|
||||
BackgroundJobKind.MakeupExamAuto,
|
||||
BackgroundJobKind.ExamArrangement,
|
||||
BackgroundJobKind.ExamSignInExport,
|
||||
BackgroundJobKind.ExamPublish
|
||||
BackgroundJobKind.ExamPublish,
|
||||
BackgroundJobKind.CourseGradeStatisticsRefresh
|
||||
];
|
||||
|
||||
public static async Task<IConnection> CreateConnectionAsync(
|
||||
@@ -418,6 +419,7 @@ internal static class RabbitMqBackgroundJobTopology
|
||||
BackgroundJobKind.ExamArrangement => "exam.arrangement",
|
||||
BackgroundJobKind.ExamSignInExport => "exam.sign-in-export",
|
||||
BackgroundJobKind.ExamPublish => "exam.publish",
|
||||
BackgroundJobKind.CourseGradeStatisticsRefresh => "grade-statistics.refresh",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null)
|
||||
};
|
||||
|
||||
|
||||
@@ -182,12 +182,16 @@ public static class AppCacheKeys
|
||||
return $"statistics:v2:{Normalize(area)}:scope:{Normalize(dataScope)}:" +
|
||||
$"college:{effectiveCollegeId?.ToString("N") ?? "all"}:{filterPart}";
|
||||
}
|
||||
|
||||
public static string CourseGradeStatistics(Guid gradeSheetId) =>
|
||||
$"grade-statistics:sheet:{gradeSheetId:N}";
|
||||
}
|
||||
|
||||
public static class AppCacheTags
|
||||
{
|
||||
public const string BaseData = "base-data";
|
||||
public const string Analytics = "analytics";
|
||||
public const string CourseGradeStatistics = "grade-statistics";
|
||||
public const string Timetables = "timetables";
|
||||
public const string TimetableOptions = "timetable:options";
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Infrastructure.Caching;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Grades;
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds a course/term's denormalized result statistics. The operation is
|
||||
/// intentionally idempotent: duplicate RabbitMQ deliveries are safe.
|
||||
/// </summary>
|
||||
public sealed class CourseGradeStatisticsRefreshJobProcessor(
|
||||
AppDbContext db,
|
||||
IAppCache cache,
|
||||
ILogger<CourseGradeStatisticsRefreshJobProcessor> logger)
|
||||
{
|
||||
public async Task ProcessAsync(Guid jobId, CancellationToken cancellationToken)
|
||||
{
|
||||
var job = await db.CourseGradeStatisticsRefreshJobs
|
||||
.FirstOrDefaultAsync(x => x.Id == jobId, cancellationToken);
|
||||
if (job is null || job.Status == CourseGradeStatisticsRefreshJobStatus.Succeeded)
|
||||
return;
|
||||
|
||||
job.Status = CourseGradeStatisticsRefreshJobStatus.Running;
|
||||
job.StartedAt = DateTime.UtcNow;
|
||||
job.ErrorMessage = null;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var sheetData = await db.GradeSheets.AsNoTracking()
|
||||
.Where(x => x.Id == job.GradeSheetId)
|
||||
.Select(x => new { x.Id, x.TeachingTask!.CourseId, x.TeachingTask.AcademicTermId })
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
if (sheetData is null)
|
||||
{
|
||||
job.Status = CourseGradeStatisticsRefreshJobStatus.Succeeded;
|
||||
job.CompletedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
var target = new StatisticsTarget(sheetData.CourseId, sheetData.AcademicTermId);
|
||||
|
||||
try
|
||||
{
|
||||
// Statistics shown to students are based only on formally published
|
||||
// scores. This prevents an unfinished class from exposing data.
|
||||
var scores = await db.GradeRecords.AsNoTracking()
|
||||
.Where(x => x.TotalScore != null &&
|
||||
x.GradeSheet!.Status == GradeSheetStatus.Published &&
|
||||
x.GradeSheet.TeachingTask!.CourseId == target.CourseId &&
|
||||
x.GradeSheet.TeachingTask.AcademicTermId == target.AcademicTermId)
|
||||
.Select(x => new ScoreRow(
|
||||
x.TotalScore!.Value,
|
||||
x.Student!.AdministrativeClassId,
|
||||
x.Student.AdministrativeClass!.MajorId,
|
||||
x.Student.AdministrativeClass.Major!.CollegeId))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var rebuilt = new List<CourseGradeStatistic>();
|
||||
AddStatistics(CourseGradeStatisticScope.AdministrativeClass,
|
||||
scores.GroupBy(x => x.ClassId), rebuilt, target, now);
|
||||
AddStatistics(CourseGradeStatisticScope.Major,
|
||||
scores.GroupBy(x => x.MajorId), rebuilt, target, now);
|
||||
AddStatistics(CourseGradeStatisticScope.College,
|
||||
scores.GroupBy(x => x.CollegeId), rebuilt, target, now);
|
||||
AddUniversityStatistic(scores, rebuilt, target, now);
|
||||
|
||||
await db.CourseGradeStatistics
|
||||
.Where(x => x.CourseId == target.CourseId &&
|
||||
x.AcademicTermId == target.AcademicTermId)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
if (rebuilt.Count > 0)
|
||||
db.CourseGradeStatistics.AddRange(rebuilt);
|
||||
|
||||
job.Status = CourseGradeStatisticsRefreshJobStatus.Succeeded;
|
||||
job.CompletedAt = now;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
await cache.RemoveByTagAsync(AppCacheTags.CourseGradeStatistics,
|
||||
cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
job.Status = CourseGradeStatisticsRefreshJobStatus.Failed;
|
||||
job.ErrorMessage = exception.GetBaseException().Message[..Math.Min(2000,
|
||||
exception.GetBaseException().Message.Length)];
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
logger.LogError(exception, "Course grade statistics refresh {JobId} failed.", jobId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddStatistics(
|
||||
CourseGradeStatisticScope scope,
|
||||
IEnumerable<IGrouping<Guid, ScoreRow>> groups,
|
||||
ICollection<CourseGradeStatistic> target,
|
||||
StatisticsTarget targetInfo,
|
||||
DateTime calculatedAt)
|
||||
{
|
||||
foreach (var group in groups)
|
||||
target.Add(Create(scope, group.Key, group.Select(x => x.Score), targetInfo, calculatedAt));
|
||||
}
|
||||
|
||||
private static void AddUniversityStatistic(
|
||||
IReadOnlyCollection<ScoreRow> scores,
|
||||
ICollection<CourseGradeStatistic> target,
|
||||
StatisticsTarget targetInfo,
|
||||
DateTime calculatedAt)
|
||||
{
|
||||
if (scores.Count > 0)
|
||||
target.Add(Create(CourseGradeStatisticScope.University, null,
|
||||
scores.Select(x => x.Score), targetInfo, calculatedAt));
|
||||
}
|
||||
|
||||
private static CourseGradeStatistic Create(
|
||||
CourseGradeStatisticScope scope,
|
||||
Guid? scopeEntityId,
|
||||
IEnumerable<decimal> source,
|
||||
StatisticsTarget targetInfo,
|
||||
DateTime calculatedAt)
|
||||
{
|
||||
var scores = source.ToArray();
|
||||
var passed = scores.Count(x => x >= 60m);
|
||||
return new CourseGradeStatistic
|
||||
{
|
||||
CourseId = targetInfo.CourseId,
|
||||
AcademicTermId = targetInfo.AcademicTermId,
|
||||
Scope = scope,
|
||||
ScopeEntityId = scopeEntityId,
|
||||
StudentCount = scores.Length,
|
||||
PassedCount = passed,
|
||||
Below60Count = scores.Count(x => x < 60m),
|
||||
From60To69Count = scores.Count(x => x >= 60m && x < 70m),
|
||||
From70To79Count = scores.Count(x => x >= 70m && x < 80m),
|
||||
From80To89Count = scores.Count(x => x >= 80m && x < 90m),
|
||||
From90To100Count = scores.Count(x => x >= 90m),
|
||||
HighestScore = scores.Max(),
|
||||
AverageScore = Math.Round(scores.Average(), 1),
|
||||
LowestScore = scores.Min(),
|
||||
PassRate = Math.Round((decimal)passed / scores.Length * 100m, 2),
|
||||
CalculatedAt = calculatedAt
|
||||
};
|
||||
}
|
||||
|
||||
private sealed record ScoreRow(decimal Score, Guid ClassId, Guid MajorId, Guid CollegeId);
|
||||
private sealed record StatisticsTarget(Guid CourseId, Guid AcademicTermId);
|
||||
}
|
||||
@@ -66,6 +66,10 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
public DbSet<GradeRecord> GradeRecords => Set<GradeRecord>();
|
||||
public DbSet<GradeItem> GradeItems => Set<GradeItem>();
|
||||
public DbSet<GradeItemScore> GradeItemScores => Set<GradeItemScore>();
|
||||
public DbSet<CourseGradeStatistic> CourseGradeStatistics =>
|
||||
Set<CourseGradeStatistic>();
|
||||
public DbSet<CourseGradeStatisticsRefreshJob> CourseGradeStatisticsRefreshJobs =>
|
||||
Set<CourseGradeStatisticsRefreshJob>();
|
||||
public DbSet<OtherExamBatch> OtherExamBatches => Set<OtherExamBatch>();
|
||||
public DbSet<OtherExamResult> OtherExamResults => Set<OtherExamResult>();
|
||||
public DbSet<AttendanceSheet> AttendanceSheets => Set<AttendanceSheet>();
|
||||
@@ -847,6 +851,32 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
builder.Entity<CourseGradeStatistic>(entity =>
|
||||
{
|
||||
entity.Property(x => x.HighestScore).HasPrecision(5, 1);
|
||||
entity.Property(x => x.AverageScore).HasPrecision(5, 1);
|
||||
entity.Property(x => x.LowestScore).HasPrecision(5, 1);
|
||||
entity.Property(x => x.PassRate).HasPrecision(5, 2);
|
||||
entity.HasIndex(x => new
|
||||
{
|
||||
x.CourseId, x.AcademicTermId, x.Scope, x.ScopeEntityId
|
||||
}).IsUnique().HasDatabaseName("UX_CourseGradeStatistics_Scope");
|
||||
entity.HasIndex(x => new { x.AcademicTermId, x.Scope, x.ScopeEntityId });
|
||||
entity.HasOne<Course>().WithMany().HasForeignKey(x => x.CourseId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
entity.HasOne<AcademicTerm>().WithMany().HasForeignKey(x => x.AcademicTermId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
builder.Entity<CourseGradeStatisticsRefreshJob>(entity =>
|
||||
{
|
||||
entity.Property(x => x.ErrorMessage).HasMaxLength(2000);
|
||||
entity.HasIndex(x => new { x.Status, x.CreatedAt });
|
||||
entity.HasIndex(x => x.GradeSheetId);
|
||||
entity.HasOne<GradeSheet>().WithMany().HasForeignKey(x => x.GradeSheetId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
builder.Entity<AttendanceSheet>(entity =>
|
||||
{
|
||||
entity.Property(x => x.Name).HasMaxLength(120);
|
||||
|
||||
@@ -84,6 +84,10 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"20260803_44_student_personal_profile";
|
||||
private const string OtherExamResultsMigration =
|
||||
"20260808_45_other_exam_results";
|
||||
private const string CourseGradeStatisticsMigration =
|
||||
"20260808_46_course_grade_statistics";
|
||||
private const string CourseGradeDistributionMigration =
|
||||
"20260809_47_course_grade_distribution";
|
||||
|
||||
public async Task MigrateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -639,6 +643,14 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
? OtherExamResultsStatements.Skip(1)
|
||||
: OtherExamResultsStatements,
|
||||
cancellationToken);
|
||||
await ApplyMigrationAsync(
|
||||
CourseGradeStatisticsMigration,
|
||||
CourseGradeStatisticsStatements,
|
||||
cancellationToken);
|
||||
await ApplyMigrationAsync(
|
||||
CourseGradeDistributionMigration,
|
||||
CourseGradeDistributionStatements,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ApplyMigrationAsync(
|
||||
@@ -2210,6 +2222,25 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"""CREATE INDEX IF NOT EXISTS "IX_OtherExamResults_StudentId_OtherExamBatchId" ON "OtherExamResults" ("StudentId", "OtherExamBatchId");"""
|
||||
];
|
||||
|
||||
private static readonly string[] CourseGradeStatisticsStatements =
|
||||
[
|
||||
"""CREATE TABLE IF NOT EXISTS "CourseGradeStatistics" ("Id" TEXT NOT NULL CONSTRAINT "PK_CourseGradeStatistics" PRIMARY KEY, "CourseId" TEXT NOT NULL, "AcademicTermId" TEXT NOT NULL, "Scope" INTEGER NOT NULL, "ScopeEntityId" TEXT NULL, "StudentCount" INTEGER NOT NULL, "PassedCount" INTEGER NOT NULL, "HighestScore" TEXT NOT NULL, "AverageScore" TEXT NOT NULL, "LowestScore" TEXT NOT NULL, "PassRate" TEXT NOT NULL, "CalculatedAt" TEXT NOT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, CONSTRAINT "FK_CourseGradeStatistics_Courses_CourseId" FOREIGN KEY ("CourseId") REFERENCES "Courses" ("Id") ON DELETE RESTRICT, CONSTRAINT "FK_CourseGradeStatistics_AcademicTerms_AcademicTermId" FOREIGN KEY ("AcademicTermId") REFERENCES "AcademicTerms" ("Id") ON DELETE RESTRICT);""",
|
||||
"""CREATE UNIQUE INDEX IF NOT EXISTS "UX_CourseGradeStatistics_Scope" ON "CourseGradeStatistics" ("CourseId", "AcademicTermId", "Scope", "ScopeEntityId");""",
|
||||
"""CREATE INDEX IF NOT EXISTS "IX_CourseGradeStatistics_AcademicTermId_Scope_ScopeEntityId" ON "CourseGradeStatistics" ("AcademicTermId", "Scope", "ScopeEntityId");""",
|
||||
"""CREATE TABLE IF NOT EXISTS "CourseGradeStatisticsRefreshJobs" ("Id" TEXT NOT NULL CONSTRAINT "PK_CourseGradeStatisticsRefreshJobs" PRIMARY KEY, "GradeSheetId" TEXT NOT NULL, "Status" INTEGER NOT NULL, "StartedAt" TEXT NULL, "CompletedAt" TEXT NULL, "ErrorMessage" TEXT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, CONSTRAINT "FK_CourseGradeStatisticsRefreshJobs_GradeSheets_GradeSheetId" FOREIGN KEY ("GradeSheetId") REFERENCES "GradeSheets" ("Id") ON DELETE CASCADE);""",
|
||||
"""CREATE INDEX IF NOT EXISTS "IX_CourseGradeStatisticsRefreshJobs_GradeSheetId" ON "CourseGradeStatisticsRefreshJobs" ("GradeSheetId");""",
|
||||
"""CREATE INDEX IF NOT EXISTS "IX_CourseGradeStatisticsRefreshJobs_Status_CreatedAt" ON "CourseGradeStatisticsRefreshJobs" ("Status", "CreatedAt");"""
|
||||
];
|
||||
|
||||
private static readonly string[] CourseGradeDistributionStatements =
|
||||
[
|
||||
"""ALTER TABLE "CourseGradeStatistics" ADD COLUMN "Below60Count" INTEGER NOT NULL DEFAULT 0;""",
|
||||
"""ALTER TABLE "CourseGradeStatistics" ADD COLUMN "From60To69Count" INTEGER NOT NULL DEFAULT 0;""",
|
||||
"""ALTER TABLE "CourseGradeStatistics" ADD COLUMN "From70To79Count" INTEGER NOT NULL DEFAULT 0;""",
|
||||
"""ALTER TABLE "CourseGradeStatistics" ADD COLUMN "From80To89Count" INTEGER NOT NULL DEFAULT 0;""",
|
||||
"""ALTER TABLE "CourseGradeStatistics" ADD COLUMN "From90To100Count" INTEGER NOT NULL DEFAULT 0;"""
|
||||
];
|
||||
|
||||
private static readonly string[] ApprovalTableStatements =
|
||||
[
|
||||
"""CREATE TABLE "CourseExemptions" ("Id" TEXT NOT NULL CONSTRAINT "PK_CourseExemptions" PRIMARY KEY, "StudentId" TEXT NOT NULL, "TeachingTaskId" TEXT NOT NULL, "Reason" TEXT NOT NULL, "Status" INTEGER NOT NULL, "ReviewComment" TEXT NULL, "SubmittedAt" TEXT NOT NULL, "ReviewedAt" TEXT NULL, "ReviewedByUserId" TEXT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, CONSTRAINT "FK_CourseExemptions_Students" FOREIGN KEY ("StudentId") REFERENCES "Students" ("Id") ON DELETE RESTRICT, CONSTRAINT "FK_CourseExemptions_TeachingTasks" FOREIGN KEY ("TeachingTaskId") REFERENCES "TeachingTasks" ("Id") ON DELETE RESTRICT);""",
|
||||
|
||||
+6381
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 CourseGradeStatistics : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CourseGradeStatistics",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
CourseId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
AcademicTermId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
Scope = table.Column<int>(type: "int", nullable: false),
|
||||
ScopeEntityId = table.Column<Guid>(type: "char(36)", nullable: true),
|
||||
StudentCount = table.Column<int>(type: "int", nullable: false),
|
||||
PassedCount = table.Column<int>(type: "int", nullable: false),
|
||||
Below60Count = table.Column<int>(type: "int", nullable: false),
|
||||
From60To69Count = table.Column<int>(type: "int", nullable: false),
|
||||
From70To79Count = table.Column<int>(type: "int", nullable: false),
|
||||
From80To89Count = table.Column<int>(type: "int", nullable: false),
|
||||
From90To100Count = table.Column<int>(type: "int", nullable: false),
|
||||
HighestScore = table.Column<decimal>(type: "decimal(5,1)", precision: 5, scale: 1, nullable: false),
|
||||
AverageScore = table.Column<decimal>(type: "decimal(5,1)", precision: 5, scale: 1, nullable: false),
|
||||
LowestScore = table.Column<decimal>(type: "decimal(5,1)", precision: 5, scale: 1, nullable: false),
|
||||
PassRate = table.Column<decimal>(type: "decimal(5,2)", precision: 5, scale: 2, nullable: false),
|
||||
CalculatedAt = 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_CourseGradeStatistics", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CourseGradeStatistics_AcademicTerms_AcademicTermId",
|
||||
column: x => x.AcademicTermId,
|
||||
principalTable: "AcademicTerms",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_CourseGradeStatistics_Courses_CourseId",
|
||||
column: x => x.CourseId,
|
||||
principalTable: "Courses",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
})
|
||||
.Annotation("MySQL:Charset", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CourseGradeStatisticsRefreshJobs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
GradeSheetId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
Status = table.Column<int>(type: "int", nullable: false),
|
||||
StartedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
CompletedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
ErrorMessage = table.Column<string>(type: "varchar(2000)", maxLength: 2000, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CourseGradeStatisticsRefreshJobs", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CourseGradeStatisticsRefreshJobs_GradeSheets_GradeSheetId",
|
||||
column: x => x.GradeSheetId,
|
||||
principalTable: "GradeSheets",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySQL:Charset", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CourseGradeStatistics_AcademicTermId_Scope_ScopeEntityId",
|
||||
table: "CourseGradeStatistics",
|
||||
columns: new[] { "AcademicTermId", "Scope", "ScopeEntityId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_CourseGradeStatistics_Scope",
|
||||
table: "CourseGradeStatistics",
|
||||
columns: new[] { "CourseId", "AcademicTermId", "Scope", "ScopeEntityId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CourseGradeStatisticsRefreshJobs_GradeSheetId",
|
||||
table: "CourseGradeStatisticsRefreshJobs",
|
||||
column: "GradeSheetId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CourseGradeStatisticsRefreshJobs_Status_CreatedAt",
|
||||
table: "CourseGradeStatisticsRefreshJobs",
|
||||
columns: new[] { "Status", "CreatedAt" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "CourseGradeStatistics");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "CourseGradeStatisticsRefreshJobs");
|
||||
}
|
||||
}
|
||||
}
|
||||
+136
@@ -983,6 +983,118 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.ToTable("CourseExemptions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGradeStatistic", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("AcademicTermId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<decimal>("AverageScore")
|
||||
.HasPrecision(5, 1)
|
||||
.HasColumnType("decimal(5,1)");
|
||||
|
||||
b.Property<DateTime>("CalculatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<Guid>("CourseId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("HighestScore")
|
||||
.HasPrecision(5, 1)
|
||||
.HasColumnType("decimal(5,1)");
|
||||
|
||||
b.Property<decimal>("LowestScore")
|
||||
.HasPrecision(5, 1)
|
||||
.HasColumnType("decimal(5,1)");
|
||||
|
||||
b.Property<decimal>("PassRate")
|
||||
.HasPrecision(5, 2)
|
||||
.HasColumnType("decimal(5,2)");
|
||||
|
||||
b.Property<int>("PassedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Below60Count")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("From60To69Count")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("From70To79Count")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("From80To89Count")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("From90To100Count")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Scope")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid?>("ScopeEntityId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<int>("StudentCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AcademicTermId", "Scope", "ScopeEntityId");
|
||||
|
||||
b.HasIndex("CourseId", "AcademicTermId", "Scope", "ScopeEntityId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_CourseGradeStatistics_Scope");
|
||||
|
||||
b.ToTable("CourseGradeStatistics");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGradeStatisticsRefreshJob", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("varchar(2000)");
|
||||
|
||||
b.Property<Guid>("GradeSheetId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GradeSheetId");
|
||||
|
||||
b.HasIndex("Status", "CreatedAt");
|
||||
|
||||
b.ToTable("CourseGradeStatisticsRefreshJobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CoursePrerequisite", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -4871,6 +4983,30 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.Navigation("TeachingTask");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGradeStatistic", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AcademicTermId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.Course", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("CourseId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGradeStatisticsRefreshJob", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.GradeSheet", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("GradeSheetId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CoursePrerequisite", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.Course", "Course")
|
||||
|
||||
Reference in New Issue
Block a user