已完成“考试安排与考场管理”模块:

考试计划草稿、场次配置与正式发布。
教学班、考试时间、考场、监考教师关联。
自动校验考场容量。
阻止考场、监考教师和学生考试时间冲突。
管理员查看考生名单。
教师查看个人监考安排。
学生查看已发布考试日程。
SQLite 增量升级和 MySQL 正式迁移。
This commit is contained in:
2026-07-24 15:43:55 +08:00 Unverified
parent af52e27a3a
commit 7f3e37de20
18 changed files with 3057 additions and 2 deletions
@@ -49,6 +49,8 @@ public sealed class DashboardController(AppDbContext db) : ControllerBase
x => x.Status == GradeSheetStatus.Published,
cancellationToken),
GradeRecords = await db.GradeRecords.CountAsync(cancellationToken),
ExamPlans = await db.ExamPlans.CountAsync(cancellationToken),
ExamSessions = await db.ExamSessions.CountAsync(cancellationToken),
Users = await db.Users.CountAsync(cancellationToken)
}
};
@@ -0,0 +1,307 @@
using System.ComponentModel.DataAnnotations;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Jiaowu.Api.Controllers;
[ApiController]
[Authorize]
[Route("api/exams")]
public sealed class ExamsController(
AppDbContext db,
ICurrentUserDataScope currentUserDataScope) : ControllerBase
{
private const string Managers =
SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin;
[HttpGet("plans")]
public async Task<ActionResult> GetPlans(
Guid? academicTermId,
CancellationToken cancellationToken)
{
var source = db.ExamPlans.AsNoTracking().AsQueryable();
if (academicTermId.HasValue)
source = source.Where(x => x.AcademicTermId == academicTermId);
if (!IsManager())
source = source.Where(x => x.Status == ExamPlanStatus.Published);
return Ok(await source.OrderByDescending(x => x.AcademicTerm!.StartDate)
.ThenByDescending(x => x.CreatedAt)
.Select(x => new
{
x.Id, x.Name, x.AcademicTermId, TermName = x.AcademicTerm!.Name,
x.Status, SessionCount = x.Sessions.Count, x.Notes, x.PublishedAt
}).ToListAsync(cancellationToken));
}
[HttpPost("plans")]
[Authorize(Roles = Managers)]
public async Task<ActionResult> CreatePlan(
ExamPlanRequest request,
CancellationToken cancellationToken)
{
if (!await db.AcademicTerms.AnyAsync(
x => x.Id == request.AcademicTermId && x.IsEnabled,
cancellationToken))
return ValidationProblem("所选学期不存在或已停用。");
var plan = new ExamPlan
{
AcademicTermId = request.AcademicTermId,
Name = request.Name.Trim(),
Notes = Normalize(request.Notes)
};
db.ExamPlans.Add(plan);
return await SaveAsync(plan.Id, true, cancellationToken);
}
[HttpGet("plans/{id:guid}")]
public async Task<ActionResult> GetPlan(Guid id, CancellationToken cancellationToken)
{
var manager = IsManager();
var plan = await db.ExamPlans.AsNoTracking()
.Where(x => x.Id == id && (manager || x.Status == ExamPlanStatus.Published))
.Select(x => new
{
x.Id, x.Name, x.AcademicTermId, TermName = x.AcademicTerm!.Name,
x.Status, x.Notes, x.PublishedAt,
Sessions = x.Sessions.OrderBy(item => item.StartsAt).Select(item => new
{
item.Id, item.TeachingTaskId, item.TeachingTask!.TaskNumber,
TaskName = item.TeachingTask.Name,
CourseCode = item.TeachingTask.Course!.Code,
CourseName = item.TeachingTask.Course.Name,
item.ClassroomId, ClassroomName = item.Classroom!.Name,
BuildingName = item.Classroom.Building!.Name,
ClassroomCapacity = item.Classroom.Capacity,
item.StartsAt, item.EndsAt, item.Notes,
InvigilatorIds = item.Invigilators.Select(i => i.TeacherId),
InvigilatorNames = item.Invigilators.Select(i => i.Teacher!.Name),
StudentCount = db.CourseEnrollments.Count(e =>
e.Status == CourseEnrollmentStatus.Enrolled &&
e.CourseSelectionOffering!.TeachingTaskId == item.TeachingTaskId)
})
}).FirstOrDefaultAsync(cancellationToken);
return plan is null ? NotFound() : Ok(plan);
}
[HttpPost("plans/{planId:guid}/sessions")]
[Authorize(Roles = Managers)]
public async Task<ActionResult> CreateSession(
Guid planId,
ExamSessionRequest request,
CancellationToken cancellationToken)
{
var plan = await db.ExamPlans.FindAsync([planId], cancellationToken);
if (plan is null) return NotFound();
if (plan.Status != ExamPlanStatus.Draft)
return ConflictProblem("已发布的考试计划不能调整场次。");
var validation = await ValidateSessionAsync(plan, null, request, cancellationToken);
if (validation is not null) return validation;
var session = new ExamSession
{
ExamPlanId = planId,
TeachingTaskId = request.TeachingTaskId,
ClassroomId = request.ClassroomId,
StartsAt = request.StartsAt.ToUniversalTime(),
EndsAt = request.EndsAt.ToUniversalTime(),
Notes = Normalize(request.Notes),
Invigilators = request.InvigilatorIds.Distinct().Select(id =>
new ExamSessionInvigilator { TeacherId = id }).ToList()
};
db.ExamSessions.Add(session);
return await SaveAsync(session.Id, true, cancellationToken);
}
[HttpDelete("plans/{planId:guid}/sessions/{id:guid}")]
[Authorize(Roles = Managers)]
public async Task<ActionResult> DeleteSession(
Guid planId,
Guid id,
CancellationToken cancellationToken)
{
var session = await db.ExamSessions.Include(x => x.ExamPlan)
.FirstOrDefaultAsync(x => x.Id == id && x.ExamPlanId == planId, cancellationToken);
if (session is null) return NotFound();
if (session.ExamPlan!.Status != ExamPlanStatus.Draft)
return ConflictProblem("已发布的考试计划不能调整场次。");
db.ExamSessions.Remove(session);
return await SaveAsync(id, false, cancellationToken);
}
[HttpPost("plans/{id:guid}/publish")]
[Authorize(Roles = Managers)]
public async Task<ActionResult> Publish(Guid id, CancellationToken cancellationToken)
{
var plan = await db.ExamPlans.Include(x => x.Sessions)
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
if (plan is null) return NotFound();
if (plan.Status != ExamPlanStatus.Draft)
return ConflictProblem("只有草稿考试计划可以发布。");
if (plan.Sessions.Count == 0)
return ConflictProblem("至少安排一个考试场次后才能发布。");
plan.Status = ExamPlanStatus.Published;
plan.PublishedAt = DateTime.UtcNow;
return await SaveAsync(id, false, cancellationToken);
}
[HttpGet("sessions/{id:guid}/roster")]
[Authorize(Roles = Managers)]
public async Task<ActionResult> GetRoster(Guid id, CancellationToken cancellationToken)
{
var session = await db.ExamSessions.AsNoTracking()
.Where(x => x.Id == id)
.Select(x => new
{
x.Id, x.TeachingTaskId, x.TeachingTask!.TaskNumber,
CourseName = x.TeachingTask.Course!.Name,
ClassroomName = x.Classroom!.Name, x.StartsAt,
Students = db.CourseEnrollments
.Where(e => e.Status == CourseEnrollmentStatus.Enrolled &&
e.CourseSelectionOffering!.TeachingTaskId == x.TeachingTaskId)
.OrderBy(e => e.Student!.StudentNumber)
.Select(e => new
{
e.StudentId, e.Student!.StudentNumber, e.Student.Name,
ClassName = e.Student.AdministrativeClass!.Name
}).ToList()
}).FirstOrDefaultAsync(cancellationToken);
return session is null ? NotFound() : Ok(session);
}
[HttpGet("my-schedule")]
public async Task<ActionResult> GetMySchedule(CancellationToken cancellationToken)
{
var scope = currentUserDataScope.Current;
if (scope.IsInRole(SystemRoles.Student))
{
var studentId = await db.Students.Where(x => x.UserId == scope.UserId)
.Select(x => (Guid?)x.Id).FirstOrDefaultAsync(cancellationToken);
if (!studentId.HasValue) return ConflictProblem("当前账号未关联学生档案。");
return Ok(await db.ExamSessions.AsNoTracking()
.Where(x => x.ExamPlan!.Status == ExamPlanStatus.Published &&
db.CourseEnrollments.Any(e => e.StudentId == studentId &&
e.Status == CourseEnrollmentStatus.Enrolled &&
e.CourseSelectionOffering!.TeachingTaskId == x.TeachingTaskId))
.OrderBy(x => x.StartsAt).Select(x => new
{
x.Id, PlanName = x.ExamPlan!.Name, x.StartsAt, x.EndsAt,
x.TeachingTask!.TaskNumber, CourseCode = x.TeachingTask.Course!.Code,
CourseName = x.TeachingTask.Course.Name,
ClassroomName = x.Classroom!.Name,
BuildingName = x.Classroom.Building!.Name,
InvigilatorNames = x.Invigilators.Select(i => i.Teacher!.Name)
})
.ToListAsync(cancellationToken));
}
if (scope.IsInRole(SystemRoles.Teacher))
{
return Ok(await db.ExamSessions.AsNoTracking()
.Where(x => x.ExamPlan!.Status == ExamPlanStatus.Published &&
x.Invigilators.Any(i => i.Teacher!.UserId == scope.UserId))
.OrderBy(x => x.StartsAt).Select(x => new
{
x.Id, PlanName = x.ExamPlan!.Name, x.StartsAt, x.EndsAt,
x.TeachingTask!.TaskNumber, CourseCode = x.TeachingTask.Course!.Code,
CourseName = x.TeachingTask.Course.Name,
ClassroomName = x.Classroom!.Name,
BuildingName = x.Classroom.Building!.Name,
InvigilatorNames = x.Invigilators.Select(i => i.Teacher!.Name),
StudentCount = db.CourseEnrollments.Count(e =>
e.Status == CourseEnrollmentStatus.Enrolled &&
e.CourseSelectionOffering!.TeachingTaskId == x.TeachingTaskId)
})
.ToListAsync(cancellationToken));
}
return Ok(Array.Empty<object>());
}
private async Task<ActionResult?> ValidateSessionAsync(
ExamPlan plan,
Guid? currentId,
ExamSessionRequest request,
CancellationToken cancellationToken)
{
var starts = request.StartsAt.ToUniversalTime();
var ends = request.EndsAt.ToUniversalTime();
if (starts >= ends) return ValidationProblem("考试开始时间必须早于结束时间。");
var task = await db.TeachingTasks.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == request.TeachingTaskId, cancellationToken);
if (task is null || task.AcademicTermId != plan.AcademicTermId)
return ValidationProblem("教学班与考试计划必须属于同一学期。");
var room = await db.Classrooms.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == request.ClassroomId && x.IsEnabled, cancellationToken);
if (room is null) return ValidationProblem("所选考场不存在或已停用。");
var studentCount = await db.CourseEnrollments.CountAsync(x =>
x.Status == CourseEnrollmentStatus.Enrolled &&
x.CourseSelectionOffering!.TeachingTaskId == task.Id, cancellationToken);
if (studentCount > room.Capacity)
return ConflictProblem($"考场容量不足:需容纳 {studentCount} 人,教室容量为 {room.Capacity}。");
var teacherIds = request.InvigilatorIds.Distinct().ToArray();
if (teacherIds.Length == 0) return ValidationProblem("至少安排一名监考教师。");
if (await db.Teachers.CountAsync(x => teacherIds.Contains(x.Id) &&
x.Status == TeacherStatus.Active, cancellationToken) != teacherIds.Length)
return ValidationProblem("存在无效监考教师。");
var overlaps = db.ExamSessions.Where(x => x.ExamPlanId == plan.Id &&
x.Id != currentId && x.StartsAt < ends && starts < x.EndsAt);
if (await overlaps.AnyAsync(x => x.ClassroomId == room.Id, cancellationToken))
return ConflictProblem("该时段考场已被占用。");
if (await overlaps.AnyAsync(x =>
x.Invigilators.Any(i => teacherIds.Contains(i.TeacherId)), cancellationToken))
return ConflictProblem("监考教师在该时段已有考试任务。");
var studentIds = db.CourseEnrollments.Where(x =>
x.Status == CourseEnrollmentStatus.Enrolled &&
x.CourseSelectionOffering!.TeachingTaskId == task.Id)
.Select(x => x.StudentId);
if (await overlaps.AnyAsync(other =>
db.CourseEnrollments.Any(e =>
studentIds.Contains(e.StudentId) &&
e.Status == CourseEnrollmentStatus.Enrolled &&
e.CourseSelectionOffering!.TeachingTaskId == other.TeachingTaskId),
cancellationToken))
return ConflictProblem("存在学生考试时间冲突。");
return null;
}
private bool IsManager() =>
currentUserDataScope.Current.IsInRole(SystemRoles.SuperAdmin) ||
currentUserDataScope.Current.IsInRole(SystemRoles.AcademicAdmin);
private async Task<ActionResult> SaveAsync(Guid id, bool created, CancellationToken token)
{
try
{
await db.SaveChangesAsync(token);
return created ? Created(string.Empty, new { id }) : NoContent();
}
catch (DbUpdateException)
{
return ConflictProblem("考试场次重复,或关联数据已发生变化。");
}
}
private ActionResult ConflictProblem(string detail) => Conflict(new ProblemDetails
{
Title = "无法完成考务操作", Detail = detail,
Status = StatusCodes.Status409Conflict
});
private static string? Normalize(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
public sealed record ExamPlanRequest(
Guid AcademicTermId,
[Required, MaxLength(120)] string Name,
[MaxLength(500)] string? Notes);
public sealed record ExamSessionRequest(
Guid TeachingTaskId,
Guid ClassroomId,
DateTime StartsAt,
DateTime EndsAt,
IReadOnlyCollection<Guid> InvigilatorIds,
[MaxLength(500)] string? Notes);
@@ -0,0 +1,43 @@
using Jiaowu.Api.Domain.Common;
namespace Jiaowu.Api.Domain.Academic;
public sealed class ExamPlan : EntityBase
{
public Guid AcademicTermId { get; set; }
public AcademicTerm? AcademicTerm { get; set; }
public required string Name { get; set; }
public ExamPlanStatus Status { get; set; } = ExamPlanStatus.Draft;
public string? Notes { get; set; }
public DateTime? PublishedAt { get; set; }
public ICollection<ExamSession> Sessions { get; set; } = [];
}
public sealed class ExamSession : EntityBase
{
public Guid ExamPlanId { get; set; }
public ExamPlan? ExamPlan { get; set; }
public Guid TeachingTaskId { get; set; }
public TeachingTask? TeachingTask { get; set; }
public Guid ClassroomId { get; set; }
public Classroom? Classroom { get; set; }
public DateTime StartsAt { get; set; }
public DateTime EndsAt { get; set; }
public string? Notes { get; set; }
public ICollection<ExamSessionInvigilator> Invigilators { get; set; } = [];
}
public sealed class ExamSessionInvigilator
{
public Guid ExamSessionId { get; set; }
public ExamSession? ExamSession { get; set; }
public Guid TeacherId { get; set; }
public Teacher? Teacher { get; set; }
}
public enum ExamPlanStatus
{
Draft = 1,
Published = 2,
Archived = 3
}
@@ -0,0 +1,11 @@
namespace Jiaowu.Api.Infrastructure.Exams;
public static class ExamConflictRules
{
public static bool TimeOverlaps(
DateTime firstStart,
DateTime firstEnd,
DateTime secondStart,
DateTime secondEnd) =>
firstStart < secondEnd && secondStart < firstEnd;
}
@@ -35,6 +35,10 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
public DbSet<CourseEnrollment> CourseEnrollments => Set<CourseEnrollment>();
public DbSet<GradeSheet> GradeSheets => Set<GradeSheet>();
public DbSet<GradeRecord> GradeRecords => Set<GradeRecord>();
public DbSet<ExamPlan> ExamPlans => Set<ExamPlan>();
public DbSet<ExamSession> ExamSessions => Set<ExamSession>();
public DbSet<ExamSessionInvigilator> ExamSessionInvigilators =>
Set<ExamSessionInvigilator>();
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
protected override void OnModelCreating(ModelBuilder builder)
@@ -353,6 +357,36 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
.OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<ExamPlan>(entity =>
{
entity.Property(x => x.Name).HasMaxLength(120);
entity.Property(x => x.Notes).HasMaxLength(500);
entity.HasIndex(x => new { x.AcademicTermId, x.Status });
entity.HasOne(x => x.AcademicTerm).WithMany()
.HasForeignKey(x => x.AcademicTermId).OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<ExamSession>(entity =>
{
entity.Property(x => x.Notes).HasMaxLength(500);
entity.HasIndex(x => new { x.ExamPlanId, x.StartsAt });
entity.HasIndex(x => x.TeachingTaskId);
entity.HasIndex(x => x.ClassroomId);
entity.HasOne(x => x.ExamPlan).WithMany(x => x.Sessions)
.HasForeignKey(x => x.ExamPlanId).OnDelete(DeleteBehavior.Cascade);
entity.HasOne(x => x.TeachingTask).WithMany()
.HasForeignKey(x => x.TeachingTaskId).OnDelete(DeleteBehavior.Restrict);
entity.HasOne(x => x.Classroom).WithMany()
.HasForeignKey(x => x.ClassroomId).OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<ExamSessionInvigilator>(entity =>
{
entity.HasKey(x => new { x.ExamSessionId, x.TeacherId });
entity.HasOne(x => x.ExamSession).WithMany(x => x.Invigilators)
.HasForeignKey(x => x.ExamSessionId).OnDelete(DeleteBehavior.Cascade);
entity.HasOne(x => x.Teacher).WithMany()
.HasForeignKey(x => x.TeacherId).OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<AuditLog>(entity =>
{
entity.Property(x => x.Method).HasMaxLength(10);
@@ -423,6 +423,7 @@ public sealed class DatabaseInitializer(
await SeedDevelopmentUsersAsync(computerCollege.Id);
await SeedDevelopmentCourseSelectionAsync();
await SeedDevelopmentGradesAsync();
await SeedDevelopmentExamsAsync();
}
private async Task SeedDevelopmentCourseSelectionAsync()
@@ -506,6 +507,38 @@ public sealed class DatabaseInitializer(
await db.SaveChangesAsync();
}
private async Task SeedDevelopmentExamsAsync()
{
if (await db.ExamPlans.AnyAsync()) return;
var term = await db.AcademicTerms.SingleAsync(x => x.IsCurrent);
var task = await db.TeachingTasks.SingleAsync(x => x.TaskNumber == "2026-1-CS101-01");
var room = await db.Classrooms.SingleAsync(x => x.Code == "J1-201");
var teacher = await db.Teachers.SingleAsync(x => x.TeacherNumber == "T2026001");
db.ExamPlans.Add(new ExamPlan
{
AcademicTermId = term.Id,
Name = "2026—2027 学年第一学期期末考试",
Status = ExamPlanStatus.Published,
PublishedAt = DateTime.UtcNow,
Notes = "本地开发演示考试计划。",
Sessions =
[
new ExamSession
{
TeachingTaskId = task.Id,
ClassroomId = room.Id,
StartsAt = new DateTime(2027, 1, 8, 9, 0, 0, DateTimeKind.Utc),
EndsAt = new DateTime(2027, 1, 8, 11, 0, 0, DateTimeKind.Utc),
Invigilators =
[
new ExamSessionInvigilator { TeacherId = teacher.Id }
]
}
]
});
await db.SaveChangesAsync();
}
private async Task SeedDevelopmentUsersAsync(Guid collegeId)
{
var definitions = new[]
@@ -13,6 +13,7 @@ public sealed class DevelopmentSqliteMigrator(
private const string ClassCounselorMigration = "20260724_05_class_counselor";
private const string CourseSelectionMigration = "20260724_06_course_selection";
private const string GradesMigration = "20260724_07_grades";
private const string ExamsMigration = "20260724_08_exams";
public async Task MigrateAsync(CancellationToken cancellationToken = default)
{
@@ -68,6 +69,7 @@ public sealed class DevelopmentSqliteMigrator(
GradesMigration,
GradesStatements,
cancellationToken);
await ApplyMigrationAsync(ExamsMigration, ExamsStatements, cancellationToken);
}
private async Task ApplyMigrationAsync(
@@ -569,4 +571,47 @@ public sealed class DevelopmentSqliteMigrator(
ON "GradeRecords" ("StudentId", "TotalScore");
"""
];
private static readonly string[] ExamsStatements =
[
"""
CREATE TABLE IF NOT EXISTS "ExamPlans" (
"Id" TEXT NOT NULL CONSTRAINT "PK_ExamPlans" PRIMARY KEY,
"AcademicTermId" TEXT NOT NULL, "Name" TEXT NOT NULL,
"Status" INTEGER NOT NULL, "Notes" TEXT NULL, "PublishedAt" TEXT NULL,
"CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL,
CONSTRAINT "FK_ExamPlans_AcademicTerms_AcademicTermId"
FOREIGN KEY ("AcademicTermId") REFERENCES "AcademicTerms" ("Id") ON DELETE RESTRICT
);
""",
"""CREATE INDEX IF NOT EXISTS "IX_ExamPlans_AcademicTermId_Status" ON "ExamPlans" ("AcademicTermId", "Status");""",
"""
CREATE TABLE IF NOT EXISTS "ExamSessions" (
"Id" TEXT NOT NULL CONSTRAINT "PK_ExamSessions" PRIMARY KEY,
"ExamPlanId" TEXT NOT NULL, "TeachingTaskId" TEXT NOT NULL,
"ClassroomId" TEXT NOT NULL, "StartsAt" TEXT NOT NULL, "EndsAt" TEXT NOT NULL,
"Notes" TEXT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL,
CONSTRAINT "FK_ExamSessions_ExamPlans_ExamPlanId" FOREIGN KEY ("ExamPlanId")
REFERENCES "ExamPlans" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_ExamSessions_TeachingTasks_TeachingTaskId" FOREIGN KEY ("TeachingTaskId")
REFERENCES "TeachingTasks" ("Id") ON DELETE RESTRICT,
CONSTRAINT "FK_ExamSessions_Classrooms_ClassroomId" FOREIGN KEY ("ClassroomId")
REFERENCES "Classrooms" ("Id") ON DELETE RESTRICT
);
""",
"""CREATE INDEX IF NOT EXISTS "IX_ExamSessions_ExamPlanId_StartsAt" ON "ExamSessions" ("ExamPlanId", "StartsAt");""",
"""CREATE INDEX IF NOT EXISTS "IX_ExamSessions_TeachingTaskId" ON "ExamSessions" ("TeachingTaskId");""",
"""CREATE INDEX IF NOT EXISTS "IX_ExamSessions_ClassroomId" ON "ExamSessions" ("ClassroomId");""",
"""
CREATE TABLE IF NOT EXISTS "ExamSessionInvigilators" (
"ExamSessionId" TEXT NOT NULL, "TeacherId" TEXT NOT NULL,
CONSTRAINT "PK_ExamSessionInvigilators" PRIMARY KEY ("ExamSessionId", "TeacherId"),
CONSTRAINT "FK_ExamSessionInvigilators_ExamSessions_ExamSessionId"
FOREIGN KEY ("ExamSessionId") REFERENCES "ExamSessions" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_ExamSessionInvigilators_Teachers_TeacherId"
FOREIGN KEY ("TeacherId") REFERENCES "Teachers" ("Id") ON DELETE RESTRICT
);
""",
"""CREATE INDEX IF NOT EXISTS "IX_ExamSessionInvigilators_TeacherId" ON "ExamSessionInvigilators" ("TeacherId");"""
];
}
@@ -0,0 +1,141 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
{
/// <inheritdoc />
public partial class ExamManagement : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ExamPlans",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
AcademicTermId = table.Column<Guid>(type: "char(36)", nullable: false),
Name = table.Column<string>(type: "varchar(120)", maxLength: 120, nullable: false),
Status = table.Column<int>(type: "int", nullable: false),
Notes = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true),
PublishedAt = table.Column<DateTime>(type: "datetime(6)", 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_ExamPlans", x => x.Id);
table.ForeignKey(
name: "FK_ExamPlans_AcademicTerms_AcademicTermId",
column: x => x.AcademicTermId,
principalTable: "AcademicTerms",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "ExamSessions",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
ExamPlanId = table.Column<Guid>(type: "char(36)", nullable: false),
TeachingTaskId = table.Column<Guid>(type: "char(36)", nullable: false),
ClassroomId = table.Column<Guid>(type: "char(36)", nullable: false),
StartsAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
EndsAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
Notes = table.Column<string>(type: "varchar(500)", maxLength: 500, 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_ExamSessions", x => x.Id);
table.ForeignKey(
name: "FK_ExamSessions_Classrooms_ClassroomId",
column: x => x.ClassroomId,
principalTable: "Classrooms",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_ExamSessions_ExamPlans_ExamPlanId",
column: x => x.ExamPlanId,
principalTable: "ExamPlans",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ExamSessions_TeachingTasks_TeachingTaskId",
column: x => x.TeachingTaskId,
principalTable: "TeachingTasks",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "ExamSessionInvigilators",
columns: table => new
{
ExamSessionId = table.Column<Guid>(type: "char(36)", nullable: false),
TeacherId = table.Column<Guid>(type: "char(36)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ExamSessionInvigilators", x => new { x.ExamSessionId, x.TeacherId });
table.ForeignKey(
name: "FK_ExamSessionInvigilators_ExamSessions_ExamSessionId",
column: x => x.ExamSessionId,
principalTable: "ExamSessions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ExamSessionInvigilators_Teachers_TeacherId",
column: x => x.TeacherId,
principalTable: "Teachers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_ExamPlans_AcademicTermId_Status",
table: "ExamPlans",
columns: new[] { "AcademicTermId", "Status" });
migrationBuilder.CreateIndex(
name: "IX_ExamSessionInvigilators_TeacherId",
table: "ExamSessionInvigilators",
column: "TeacherId");
migrationBuilder.CreateIndex(
name: "IX_ExamSessions_ClassroomId",
table: "ExamSessions",
column: "ClassroomId");
migrationBuilder.CreateIndex(
name: "IX_ExamSessions_ExamPlanId_StartsAt",
table: "ExamSessions",
columns: new[] { "ExamPlanId", "StartsAt" });
migrationBuilder.CreateIndex(
name: "IX_ExamSessions_TeachingTaskId",
table: "ExamSessions",
column: "TeachingTaskId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ExamSessionInvigilators");
migrationBuilder.DropTable(
name: "ExamSessions");
migrationBuilder.DropTable(
name: "ExamPlans");
}
}
}
@@ -634,6 +634,100 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.ToTable("CurriculumPlans");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamPlan", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<Guid>("AcademicTermId")
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("varchar(120)");
b.Property<string>("Notes")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<DateTime?>("PublishedAt")
.HasColumnType("datetime(6)");
b.Property<int>("Status")
.HasColumnType("int");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("AcademicTermId", "Status");
b.ToTable("ExamPlans");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSession", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<Guid>("ClassroomId")
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime>("EndsAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("ExamPlanId")
.HasColumnType("char(36)");
b.Property<string>("Notes")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<DateTime>("StartsAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("TeachingTaskId")
.HasColumnType("char(36)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("ClassroomId");
b.HasIndex("TeachingTaskId");
b.HasIndex("ExamPlanId", "StartsAt");
b.ToTable("ExamSessions");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSessionInvigilator", b =>
{
b.Property<Guid>("ExamSessionId")
.HasColumnType("char(36)");
b.Property<Guid>("TeacherId")
.HasColumnType("char(36)");
b.HasKey("ExamSessionId", "TeacherId");
b.HasIndex("TeacherId");
b.ToTable("ExamSessionInvigilators");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeRecord", b =>
{
b.Property<Guid>("Id")
@@ -1538,6 +1632,63 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Navigation("Major");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamPlan", b =>
{
b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm")
.WithMany()
.HasForeignKey("AcademicTermId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("AcademicTerm");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSession", b =>
{
b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom")
.WithMany()
.HasForeignKey("ClassroomId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Jiaowu.Api.Domain.Academic.ExamPlan", "ExamPlan")
.WithMany("Sessions")
.HasForeignKey("ExamPlanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask")
.WithMany()
.HasForeignKey("TeachingTaskId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Classroom");
b.Navigation("ExamPlan");
b.Navigation("TeachingTask");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSessionInvigilator", b =>
{
b.HasOne("Jiaowu.Api.Domain.Academic.ExamSession", "ExamSession")
.WithMany("Invigilators")
.HasForeignKey("ExamSessionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Jiaowu.Api.Domain.Academic.Teacher", "Teacher")
.WithMany()
.HasForeignKey("TeacherId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("ExamSession");
b.Navigation("Teacher");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeRecord", b =>
{
b.HasOne("Jiaowu.Api.Domain.Academic.GradeSheet", "GradeSheet")
@@ -1782,6 +1933,16 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Navigation("Modules");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamPlan", b =>
{
b.Navigation("Sessions");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSession", b =>
{
b.Navigation("Invigilators");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeSheet", b =>
{
b.Navigation("Records");