成绩统计图

This commit is contained in:
2026-08-09 10:13:12 +08:00 Unverified
parent 5f30dd6766
commit 924574256a
20 changed files with 7379 additions and 4 deletions
@@ -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);""",
@@ -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");
}
}
}
@@ -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")