学业预警

This commit is contained in:
2026-07-25 17:14:20 +08:00 Unverified
parent 5e4c76a9e4
commit db3a275601
8 changed files with 536 additions and 0 deletions
@@ -60,6 +60,8 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
public DbSet<DeferredExam> DeferredExams => Set<DeferredExam>();
public DbSet<GradeModification> GradeModifications => Set<GradeModification>();
public DbSet<CourseSubstitution> CourseSubstitutions => Set<CourseSubstitution>();
public DbSet<WarningRule> WarningRules => Set<WarningRule>();
public DbSet<WarningRecord> WarningRecords => Set<WarningRecord>();
public DbSet<Notification> Notifications => Set<Notification>();
public DbSet<StudentStatusChange> StudentStatusChanges =>
Set<StudentStatusChange>();
@@ -709,6 +711,26 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
entity.HasOne(x => x.GradeRecord).WithMany()
.HasForeignKey(x => x.GradeRecordId).OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<WarningRule>(entity =>
{
entity.Property(x => x.Name).HasMaxLength(100);
entity.Property(x => x.Threshold).HasPrecision(7, 2);
entity.Property(x => x.Description).HasMaxLength(300);
entity.HasIndex(x => new { x.AcademicTermId, x.Type }).IsUnique();
entity.HasOne(x => x.AcademicTerm).WithMany()
.HasForeignKey(x => x.AcademicTermId).OnDelete(DeleteBehavior.Cascade);
});
builder.Entity<WarningRecord>(entity =>
{
entity.Property(x => x.TriggerValue).HasPrecision(7, 2);
entity.Property(x => x.Detail).HasMaxLength(1000);
entity.Property(x => x.AcknowledgeComment).HasMaxLength(300);
entity.HasIndex(x => new { x.StudentId, x.AcademicTermId, x.Type }).IsUnique();
entity.HasIndex(x => x.Status);
entity.HasOne(x => x.Student).WithMany()
.HasForeignKey(x => x.StudentId).OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<CourseSubstitution>(entity =>
{
entity.Property(x => x.Reason).HasMaxLength(500);
@@ -42,6 +42,8 @@ public sealed class DevelopmentSqliteMigrator(
"20260725_23_attendance_appeal";
private const string ApprovalTablesMigration =
"20260725_24_approval_tables";
private const string AcademicWarningsMigration =
"20260725_25_academic_warnings";
public async Task MigrateAsync(CancellationToken cancellationToken = default)
{
@@ -280,6 +282,14 @@ public sealed class DevelopmentSqliteMigrator(
ApprovalTablesMigration,
approvalTablesExist ? [] : ApprovalTableStatements,
cancellationToken);
var warningRulesExist = await db.Database
.SqlQueryRaw<int>("SELECT COUNT(*) AS \"Value\" FROM sqlite_master WHERE type = 'table' AND name = 'WarningRules'")
.AnyAsync(value => value > 0, cancellationToken);
await ApplyMigrationAsync(
AcademicWarningsMigration,
warningRulesExist ? [] : AcademicWarningStatements,
cancellationToken);
}
private async Task ApplyMigrationAsync(
@@ -1606,4 +1616,13 @@ public sealed class DevelopmentSqliteMigrator(
"""CREATE UNIQUE INDEX "IX_CourseSubstitutions_StudentId_OriginalCourseId" ON "CourseSubstitutions" ("StudentId", "OriginalCourseId");""",
"""CREATE INDEX "IX_CourseSubstitutions_Status_CreatedAt" ON "CourseSubstitutions" ("Status", "CreatedAt");"""
];
private static readonly string[] AcademicWarningStatements =
[
"""CREATE TABLE "WarningRules" ("Id" TEXT NOT NULL CONSTRAINT "PK_WarningRules" PRIMARY KEY, "Type" INTEGER NOT NULL, "Name" TEXT NOT NULL, "Threshold" TEXT NOT NULL, "IsEnabled" INTEGER NOT NULL, "NotifyStudent" INTEGER NOT NULL, "NotifyCounselor" INTEGER NOT NULL, "AcademicTermId" TEXT NOT NULL, "Description" TEXT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, CONSTRAINT "FK_WarningRules_AcademicTerms" FOREIGN KEY ("AcademicTermId") REFERENCES "AcademicTerms" ("Id") ON DELETE CASCADE);""",
"""CREATE UNIQUE INDEX "IX_WarningRules_AcademicTermId_Type" ON "WarningRules" ("AcademicTermId", "Type");""",
"""CREATE TABLE "WarningRecords" ("Id" TEXT NOT NULL CONSTRAINT "PK_WarningRecords" PRIMARY KEY, "StudentId" TEXT NOT NULL, "Type" INTEGER NOT NULL, "Status" INTEGER NOT NULL, "TriggerValue" TEXT NOT NULL, "Detail" TEXT NOT NULL, "AcknowledgedAt" TEXT NULL, "AcknowledgeComment" TEXT NULL, "AcademicTermId" TEXT NOT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, CONSTRAINT "FK_WarningRecords_Students" FOREIGN KEY ("StudentId") REFERENCES "Students" ("Id") ON DELETE RESTRICT);""",
"""CREATE UNIQUE INDEX "IX_WarningRecords_StudentId_AcademicTermId_Type" ON "WarningRecords" ("StudentId", "AcademicTermId", "Type");""",
"""CREATE INDEX "IX_WarningRecords_Status" ON "WarningRecords" ("Status");"""
];
}
@@ -0,0 +1,46 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
{
public partial class AcademicWarnings : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable("WarningRules", table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
Type = table.Column<int>(type: "int", nullable: false),
Name = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false),
Threshold = table.Column<decimal>(type: "decimal(7,2)", precision: 7, scale: 2, nullable: false),
IsEnabled = table.Column<bool>(type: "tinyint(1)", nullable: false),
NotifyStudent = table.Column<bool>(type: "tinyint(1)", nullable: false),
NotifyCounselor = table.Column<bool>(type: "tinyint(1)", nullable: false),
AcademicTermId = table.Column<Guid>(type: "char(36)", nullable: false),
Description = table.Column<string>(type: "varchar(300)", maxLength: 300, nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
}, constraints: t => { t.PrimaryKey("PK_WarningRules", x => x.Id); t.ForeignKey("FK_WarningRules_AcademicTerms", x => x.AcademicTermId, "AcademicTerms", "Id", onDelete: ReferentialAction.Cascade); });
migrationBuilder.CreateIndex("IX_WarningRules_AcademicTermId_Type", "WarningRules", new[] { "AcademicTermId", "Type" }, unique: true);
migrationBuilder.CreateTable("WarningRecords", table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
StudentId = table.Column<Guid>(type: "char(36)", nullable: false),
Type = table.Column<int>(type: "int", nullable: false),
Status = table.Column<int>(type: "int", nullable: false),
TriggerValue = table.Column<decimal>(type: "decimal(7,2)", precision: 7, scale: 2, nullable: false),
Detail = table.Column<string>(type: "varchar(1000)", maxLength: 1000, nullable: false),
AcknowledgedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
AcknowledgeComment = table.Column<string>(type: "varchar(300)", maxLength: 300, nullable: true),
AcademicTermId = table.Column<Guid>(type: "char(36)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
}, constraints: t => { t.PrimaryKey("PK_WarningRecords", x => x.Id); t.ForeignKey("FK_WarningRecords_Students", x => x.StudentId, "Students", "Id", onDelete: ReferentialAction.Restrict); });
migrationBuilder.CreateIndex("IX_WarningRecords_StudentId_AcademicTermId_Type", "WarningRecords", new[] { "StudentId", "AcademicTermId", "Type" }, unique: true);
migrationBuilder.CreateIndex("IX_WarningRecords_Status", "WarningRecords", "Status");
}
protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable("WarningRecords"); migrationBuilder.DropTable("WarningRules"); }
}
}