diff --git a/.env.example b/.env.example index e4bec1f..678b859 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,7 @@ BackgroundJobs__AutomaticScheduleConcurrency=1 BackgroundJobs__SchedulePublishConcurrency=1 BackgroundJobs__MakeupExamAutoConcurrency=1 BackgroundJobs__ExamArrangementConcurrency=1 +BackgroundJobs__CourseGradeStatisticsRefreshConcurrency=1 # RabbitMq__HostName=rabbitmq.example.edu.cn # RabbitMq__Port=5671 # RabbitMq__UserName=jiaowu diff --git a/src/Jiaowu.Api/Controllers/ApprovalsController.cs b/src/Jiaowu.Api/Controllers/ApprovalsController.cs index cae910f..a8b80db 100644 --- a/src/Jiaowu.Api/Controllers/ApprovalsController.cs +++ b/src/Jiaowu.Api/Controllers/ApprovalsController.cs @@ -251,13 +251,6 @@ public sealed class ApprovalsController(AppDbContext db, ICurrentUserDataScope s // Auto-apply: update grade record gm.GradeRecord!.TotalScore = gm.RequestedScore; gm.GradeRecord.GradePoint = GradeCalculator.CalculateGradePoint(gm.RequestedScore); - var statisticsJob = new CourseGradeStatisticsRefreshJob - { - GradeSheetId = gm.GradeRecord.GradeSheetId - }; - db.CourseGradeStatisticsRefreshJobs.Add(statisticsJob); - db.BackgroundJobOutboxMessages.Add(BackgroundJobOutboxMessage.Create( - BackgroundJobKind.CourseGradeStatisticsRefresh, statisticsJob.Id)); await db.SaveChangesAsync(ct); await NotificationService.SendAsync( db, diff --git a/src/Jiaowu.Api/Controllers/GradeAnalyticsController.cs b/src/Jiaowu.Api/Controllers/GradeAnalyticsController.cs index 833b899..52b44ce 100644 --- a/src/Jiaowu.Api/Controllers/GradeAnalyticsController.cs +++ b/src/Jiaowu.Api/Controllers/GradeAnalyticsController.cs @@ -25,6 +25,69 @@ public sealed class GradeAnalyticsController( SystemRoles.CollegeAdmin + "," + SystemRoles.Leader + "," + SystemRoles.Teacher; + private const string ScheduleManagers = + SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin; + + [HttpGet("refresh-schedule")] + [Authorize(Roles = ScheduleManagers)] + public async Task GetRefreshSchedule(CancellationToken cancellationToken) + { + var setting = await db.CourseGradeStatisticsRefreshSettings.AsNoTracking() + .SingleOrDefaultAsync( + x => x.Key == CourseGradeStatisticsRefreshSetting.DefaultKey, + cancellationToken); + var defaults = new CourseGradeStatisticsRefreshSetting(); + var enabled = setting?.IsEnabled ?? defaults.IsEnabled; + var intervalSeconds = setting?.IntervalSeconds ?? defaults.IntervalSeconds; + var batchSize = setting?.BatchSize ?? defaults.BatchSize; + var lastRunAt = setting?.LastRunAt; + return Ok(new + { + IsEnabled = enabled, + IntervalSeconds = intervalSeconds, + BatchSize = batchSize, + LastRunAt = lastRunAt, + NextRunAt = enabled && lastRunAt.HasValue + ? lastRunAt.Value.AddSeconds(intervalSeconds) + : null as DateTime? + }); + } + + [HttpPut("refresh-schedule")] + [Authorize(Roles = ScheduleManagers)] + public async Task SaveRefreshSchedule( + SaveGradeStatisticsRefreshScheduleRequest request, + CancellationToken cancellationToken) + { + if (request.IntervalSeconds is < 10 or > 86400) + return BadRequest(new ProblemDetails + { + Title = "刷新间隔应在 10 秒到 24 小时之间。", + Status = StatusCodes.Status400BadRequest + }); + if (request.BatchSize is < 1 or > 5000) + return BadRequest(new ProblemDetails + { + Title = "单次刷新批量应在 1 到 5000 之间。", + Status = StatusCodes.Status400BadRequest + }); + + var setting = await db.CourseGradeStatisticsRefreshSettings + .SingleOrDefaultAsync( + x => x.Key == CourseGradeStatisticsRefreshSetting.DefaultKey, + cancellationToken); + if (setting is null) + { + setting = new CourseGradeStatisticsRefreshSetting(); + db.CourseGradeStatisticsRefreshSettings.Add(setting); + } + + setting.IsEnabled = request.IsEnabled; + setting.IntervalSeconds = request.IntervalSeconds; + setting.BatchSize = request.BatchSize; + await db.SaveChangesAsync(cancellationToken); + return NoContent(); + } [HttpGet("teaching-classes")] public async Task GetTeachingClasses( @@ -526,3 +589,8 @@ public sealed class GradeAnalyticsController( IReadOnlyList History, ComparisonDelta? UniversityDelta); } + +public sealed record SaveGradeStatisticsRefreshScheduleRequest( + bool IsEnabled, + int IntervalSeconds, + int BatchSize); diff --git a/src/Jiaowu.Api/Controllers/GradesController.cs b/src/Jiaowu.Api/Controllers/GradesController.cs index dc79f69..7be53db 100644 --- a/src/Jiaowu.Api/Controllers/GradesController.cs +++ b/src/Jiaowu.Api/Controllers/GradesController.cs @@ -508,7 +508,6 @@ public sealed class GradesController( } targetItem.SourceType = GradeItemSourceType.ExperimentSummary; targetItem.SourceSnapshotAt = DateTime.UtcNow; - QueueCourseStatisticsRefresh(sheet.Id); await db.SaveChangesAsync(cancellationToken); return Ok(new { @@ -668,7 +667,6 @@ public sealed class GradesController( return ConflictProblem("只有审核通过的成绩单可以发布。"); sheet.Status = GradeSheetStatus.Published; sheet.PublishedAt = DateTime.UtcNow; - QueueCourseStatisticsRefresh(sheet.Id); // The grade sheet roster is authoritative at publication time. This also // covers students added through approved roster corrections. @@ -918,7 +916,6 @@ public sealed class GradesController( if (errors.Count > 0) return ImportValidationProblem(errors); - QueueCourseStatisticsRefresh(sheet.Id); await db.SaveChangesAsync(cancellationToken); return Ok(new { updated, total = rows.Count }); } @@ -1161,7 +1158,6 @@ public sealed class GradesController( { try { - QueueCourseStatisticsRefresh(id); await db.SaveChangesAsync(cancellationToken); return created ? Created(string.Empty, new { id }) : NoContent(); } @@ -1171,14 +1167,6 @@ public sealed class GradesController( } } - private void QueueCourseStatisticsRefresh(Guid gradeSheetId) - { - var job = new CourseGradeStatisticsRefreshJob { GradeSheetId = gradeSheetId }; - db.CourseGradeStatisticsRefreshJobs.Add(job); - db.BackgroundJobOutboxMessages.Add(BackgroundJobOutboxMessage.Create( - BackgroundJobKind.CourseGradeStatisticsRefresh, job.Id)); - } - private ActionResult ConflictProblem(string detail) => Conflict(new ProblemDetails { diff --git a/src/Jiaowu.Api/Controllers/WarningsController.cs b/src/Jiaowu.Api/Controllers/WarningsController.cs index 0477410..e024512 100644 --- a/src/Jiaowu.Api/Controllers/WarningsController.cs +++ b/src/Jiaowu.Api/Controllers/WarningsController.cs @@ -21,32 +21,45 @@ public sealed class WarningsController(AppDbContext db, ICurrentUserDataScope sc [Authorize(Roles = Managers)] public async Task GetRules(Guid academicTermId, CancellationToken ct) => Ok(await db.WarningRules.AsNoTracking().Where(x => x.AcademicTermId == academicTermId) - .OrderBy(x => x.Type).Select(x => new { x.Id, x.Type, x.Name, x.Threshold, x.IsEnabled, x.NotifyStudent, x.NotifyCounselor, x.Description, x.AutoCheckEnabled, CheckDayOfWeek = x.CheckDayOfWeek ?? 0, x.CheckHour, x.CheckMinute, x.LastCheckAt }) + .OrderBy(x => x.Type).Select(x => new { x.Id, Type = (int)x.Type, x.Name, x.Threshold, x.IsEnabled, x.NotifyStudent, x.NotifyCounselor, x.Description, x.AutoCheckEnabled, CheckDayOfWeek = x.CheckDayOfWeek ?? 0, x.CheckHour, x.CheckMinute, x.LastCheckAt }) .ToListAsync(ct)); [HttpPut("rules")] [Authorize(Roles = Managers)] public async Task SaveRules(Guid academicTermId, List rules, CancellationToken ct) { + if (rules.GroupBy(x => x.Type).Any(group => group.Count() > 1)) + return BadRequest(new ProblemDetails { Title = "预警类型不能重复。", Status = StatusCodes.Status400BadRequest }); + if (rules.Any(x => x.CheckDayOfWeek is < 0 or > 7 || x.CheckHour is < 0 or > 23 || x.CheckMinute is < 0 or > 59)) + return BadRequest(new ProblemDetails { Title = "自动检测时间无效。", Status = StatusCodes.Status400BadRequest }); + var existing = await db.WarningRules.Where(x => x.AcademicTermId == academicTermId).ToListAsync(ct); - db.WarningRules.RemoveRange(existing); + var incomingTypes = rules.Select(x => x.Type).ToHashSet(); + db.WarningRules.RemoveRange(existing.Where(x => !incomingTypes.Contains(x.Type))); foreach (var r in rules) { - db.WarningRules.Add(new WarningRule + var entity = existing.FirstOrDefault(x => x.Type == r.Type); + if (entity is null) { - AcademicTermId = academicTermId, - Type = r.Type, - Name = r.Name.Trim(), - Threshold = r.Threshold, - IsEnabled = r.IsEnabled, - NotifyStudent = r.NotifyStudent, - NotifyCounselor = r.NotifyCounselor, - Description = r.Description?.Trim(), - AutoCheckEnabled = r.AutoCheckEnabled, - CheckDayOfWeek = r.CheckDayOfWeek == 0 ? null : r.CheckDayOfWeek, - CheckHour = r.CheckHour, - CheckMinute = r.CheckMinute - }); + entity = new WarningRule + { + AcademicTermId = academicTermId, + Type = r.Type, + Name = r.Name.Trim() + }; + db.WarningRules.Add(entity); + } + + entity.Name = r.Name.Trim(); + entity.Threshold = r.Threshold; + entity.IsEnabled = r.IsEnabled; + entity.NotifyStudent = r.NotifyStudent; + entity.NotifyCounselor = r.NotifyCounselor; + entity.Description = r.Description?.Trim(); + entity.AutoCheckEnabled = r.AutoCheckEnabled; + entity.CheckDayOfWeek = r.CheckDayOfWeek == 0 ? null : r.CheckDayOfWeek; + entity.CheckHour = r.CheckHour; + entity.CheckMinute = r.CheckMinute; } await db.SaveChangesAsync(ct); return NoContent(); @@ -120,7 +133,7 @@ public sealed class WarningsController(AppDbContext db, ICurrentUserDataScope sc } if (academicTermId.HasValue) q = q.Where(x => x.AcademicTermId == academicTermId); if (type.HasValue) q = q.Where(x => x.Type == type); - return Ok(await q.OrderByDescending(x => x.CreatedAt).Select(x => new { x.Id, x.StudentId, StudentName = x.Student!.Name, StudentNumber = x.Student.StudentNumber, ClassName = x.Student.AdministrativeClass!.Name, x.Type, x.Status, x.TriggerValue, x.Detail, x.AcknowledgedAt, x.AcknowledgeComment, x.CreatedAt }).ToListAsync(ct)); + return Ok(await q.OrderByDescending(x => x.CreatedAt).Select(x => new { x.Id, x.StudentId, StudentName = x.Student!.Name, StudentNumber = x.Student.StudentNumber, ClassName = x.Student.AdministrativeClass!.Name, Type = (int)x.Type, Status = (int)x.Status, x.TriggerValue, x.Detail, x.AcknowledgedAt, x.AcknowledgeComment, x.CreatedAt }).ToListAsync(ct)); } // ═══════════ Student ═══════════ @@ -131,7 +144,7 @@ public sealed class WarningsController(AppDbContext db, ICurrentUserDataScope sc var sid = await GetStudentIdAsync(ct); if (sid is null) return StudentNotFound(); return Ok(await db.WarningRecords.AsNoTracking().Where(x => x.StudentId == sid).OrderByDescending(x => x.CreatedAt) - .Select(x => new { x.Id, x.Type, x.Status, x.TriggerValue, x.Detail, x.AcknowledgedAt, x.AcknowledgeComment, x.CreatedAt, TermName = x.AcademicTermId }) + .Select(x => new { x.Id, Type = (int)x.Type, Status = (int)x.Status, x.TriggerValue, x.Detail, x.AcknowledgedAt, x.AcknowledgeComment, x.CreatedAt, TermName = x.AcademicTermId }) .ToListAsync(ct)); } diff --git a/src/Jiaowu.Api/Domain/Academic/CourseGradeStatisticsRefreshSetting.cs b/src/Jiaowu.Api/Domain/Academic/CourseGradeStatisticsRefreshSetting.cs new file mode 100644 index 0000000..2aa0bcb --- /dev/null +++ b/src/Jiaowu.Api/Domain/Academic/CourseGradeStatisticsRefreshSetting.cs @@ -0,0 +1,14 @@ +using Jiaowu.Api.Domain.Common; + +namespace Jiaowu.Api.Domain.Academic; + +public sealed class CourseGradeStatisticsRefreshSetting : EntityBase +{ + public const string DefaultKey = "default"; + + public string Key { get; set; } = DefaultKey; + public bool IsEnabled { get; set; } = true; + public int IntervalSeconds { get; set; } = 300; + public int BatchSize { get; set; } = 100; + public DateTime? LastRunAt { get; set; } +} diff --git a/src/Jiaowu.Api/Infrastructure/Grades/CourseGradeStatisticsRefreshWorker.cs b/src/Jiaowu.Api/Infrastructure/Grades/CourseGradeStatisticsRefreshWorker.cs new file mode 100644 index 0000000..9bc12c0 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Grades/CourseGradeStatisticsRefreshWorker.cs @@ -0,0 +1,228 @@ +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Domain.System; +using Jiaowu.Api.Infrastructure.BackgroundJobs; +using Jiaowu.Api.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace Jiaowu.Api.Infrastructure.Grades; + +/// +/// Repairs missing or stale materialized grade statistics on a database- +/// configured fixed interval. Grade writes do not enqueue refresh jobs; this +/// worker batches changes made during bulk imports. +/// +public sealed class CourseGradeStatisticsRefreshWorker( + IServiceScopeFactory scopeFactory, + TimeProvider timeProvider, + ILogger logger) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + logger.LogInformation("Database-configured course grade statistics scheduler started."); + using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10), timeProvider); + while (!stoppingToken.IsCancellationRequested) + { + try + { + await using var scope = scopeFactory.CreateAsyncScope(); + var scheduler = scope.ServiceProvider + .GetRequiredService(); + var queued = await scheduler.EnqueueDueAsync( + timeProvider.GetUtcNow().UtcDateTime, + stoppingToken); + if (queued > 0) + logger.LogInformation( + "Scheduled course grade statistics scan queued {Count} refresh jobs.", + queued); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + break; + } + catch (Exception exception) + { + logger.LogError(exception, "Scheduled course grade statistics scan failed."); + } + + if (!await timer.WaitForNextTickAsync(stoppingToken)) break; + } + } +} + +public sealed class CourseGradeStatisticsRefreshScheduler( + AppDbContext db, + ILogger logger) +{ + public async Task EnqueueDueAsync( + DateTime utcNow, + CancellationToken cancellationToken) => + await ExecuteWithLeaseAsync(async () => + { + var setting = await db.CourseGradeStatisticsRefreshSettings + .SingleOrDefaultAsync( + x => x.Key == CourseGradeStatisticsRefreshSetting.DefaultKey, + cancellationToken); + if (setting is null) + { + setting = new CourseGradeStatisticsRefreshSetting(); + db.CourseGradeStatisticsRefreshSettings.Add(setting); + } + + var interval = TimeSpan.FromSeconds( + Math.Clamp(setting.IntervalSeconds, 10, 86400)); + if (!setting.IsEnabled || + setting.LastRunAt.HasValue && utcNow < setting.LastRunAt.Value + interval) + { + if (db.Entry(setting).State == EntityState.Added) + await db.SaveChangesAsync(cancellationToken); + return 0; + } + + setting.LastRunAt = utcNow; + var queued = await EnqueueStaleCoreAsync(setting.BatchSize, cancellationToken); + await db.SaveChangesAsync(cancellationToken); + return queued; + }, cancellationToken); + + public async Task EnqueueStaleAsync( + int batchSize, + CancellationToken cancellationToken) => + await ExecuteWithLeaseAsync(async () => + { + var queued = await EnqueueStaleCoreAsync(batchSize, cancellationToken); + if (queued > 0) await db.SaveChangesAsync(cancellationToken); + return queued; + }, cancellationToken); + + private async Task ExecuteWithLeaseAsync( + Func> action, + CancellationToken cancellationToken) + { + var usesMySqlLease = db.Database.ProviderName?.Contains( + "MySql", + StringComparison.OrdinalIgnoreCase) == true; + if (usesMySqlLease && !await TryAcquireMySqlLeaseAsync(cancellationToken)) + { + await db.Database.CloseConnectionAsync(); + logger.LogDebug("Another instance owns the grade statistics refresh lease."); + return 0; + } + + try + { + return await action(); + } + finally + { + if (usesMySqlLease) + await ReleaseMySqlLeaseAsync(); + } + } + + private async Task EnqueueStaleCoreAsync( + int batchSize, + CancellationToken cancellationToken) + { + batchSize = Math.Clamp(batchSize, 1, 5000); + var activeTargets = await ( + from job in db.CourseGradeStatisticsRefreshJobs.AsNoTracking() + join sheet in db.GradeSheets.AsNoTracking() + on job.GradeSheetId equals sheet.Id + where job.Status == CourseGradeStatisticsRefreshJobStatus.Queued || + job.Status == CourseGradeStatisticsRefreshJobStatus.Running + select new CourseTermTarget( + sheet.TeachingTask!.CourseId, + sheet.TeachingTask.AcademicTermId)) + .Distinct() + .ToListAsync(cancellationToken); + var active = activeTargets.ToHashSet(); + + var rows = await db.GradeSheets.AsNoTracking() + .Where(sheet => + sheet.Status == GradeSheetStatus.Published && + sheet.Records.Any(record => record.TotalScore != null)) + .Select(sheet => new RefreshCandidate( + sheet.Id, + sheet.TeachingTask!.CourseId, + sheet.TeachingTask.AcademicTermId, + sheet.UpdatedAt, + sheet.Records + .Where(record => record.TotalScore != null) + .Max(record => record.UpdatedAt), + db.TeachingTaskGradeStatistics + .Where(statistic => statistic.GradeSheetId == sheet.Id) + .Select(statistic => (DateTime?)statistic.CalculatedAt) + .FirstOrDefault())) + .ToListAsync(cancellationToken); + + var stale = rows + .Where(row => + row.CalculatedAt is null || + row.SheetUpdatedAt > row.CalculatedAt || + row.RecordsUpdatedAt > row.CalculatedAt) + .GroupBy(row => new CourseTermTarget(row.CourseId, row.AcademicTermId)) + .Where(group => !active.Contains(group.Key)) + .Select(group => group + .OrderByDescending(row => row.RecordsUpdatedAt) + .ThenByDescending(row => row.SheetUpdatedAt) + .First()) + .Take(batchSize) + .ToArray(); + + foreach (var candidate in stale) + { + var job = new CourseGradeStatisticsRefreshJob + { + GradeSheetId = candidate.GradeSheetId + }; + db.CourseGradeStatisticsRefreshJobs.Add(job); + db.BackgroundJobOutboxMessages.Add(BackgroundJobOutboxMessage.Create( + BackgroundJobKind.CourseGradeStatisticsRefresh, + job.Id)); + } + + if (stale.Length == 0) return 0; + logger.LogDebug( + "Queued {Count} stale course grade statistics targets.", + stale.Length); + return stale.Length; + } + + private async Task TryAcquireMySqlLeaseAsync( + CancellationToken cancellationToken) + { + await db.Database.OpenConnectionAsync(cancellationToken); + await using var command = db.Database.GetDbConnection().CreateCommand(); + command.CommandText = "SELECT GET_LOCK('jiaowu:grade-statistics-refresh', 0);"; + var result = await command.ExecuteScalarAsync(cancellationToken); + return Convert.ToInt32(result) == 1; + } + + private async Task ReleaseMySqlLeaseAsync() + { + try + { + await using var command = db.Database.GetDbConnection().CreateCommand(); + command.CommandText = "SELECT RELEASE_LOCK('jiaowu:grade-statistics-refresh');"; + await command.ExecuteScalarAsync(CancellationToken.None); + } + catch (Exception exception) + { + logger.LogWarning(exception, "Failed to release grade statistics refresh lease."); + } + finally + { + await db.Database.CloseConnectionAsync(); + } + } + + private sealed record RefreshCandidate( + Guid GradeSheetId, + Guid CourseId, + Guid AcademicTermId, + DateTime SheetUpdatedAt, + DateTime RecordsUpdatedAt, + DateTime? CalculatedAt); + + private sealed record CourseTermTarget(Guid CourseId, Guid AcademicTermId); +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs index 5b6418b..3f43248 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs @@ -144,6 +144,8 @@ public sealed class AppDbContext(DbContextOptions options) Set(); public DbSet SystemFeatureSettings => Set(); + public DbSet CourseGradeStatisticsRefreshSettings => + Set(); public DbSet RefreshSessions => Set(); protected override void ConfigureConventions( @@ -1536,6 +1538,12 @@ public sealed class AppDbContext(DbContextOptions options) entity.HasIndex(x => x.Key).IsUnique(); }); + builder.Entity(entity => + { + entity.Property(x => x.Key).HasMaxLength(50); + entity.HasIndex(x => x.Key).IsUnique(); + }); + builder.Entity(entity => { entity.Property(x => x.DocumentNumber).HasMaxLength(50); diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs index 6529a61..d5bf3a2 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs @@ -98,6 +98,8 @@ public sealed class DevelopmentSqliteMigrator( "20260809_51_separate_experiment_classroom_scope"; private const string ReusableCourseGroupsMigration = "20260809_52_reusable_course_groups"; + private const string CourseGradeStatisticsRefreshSettingsMigration = + "20260809_53_course_grade_statistics_refresh_settings"; public async Task MigrateAsync(CancellationToken cancellationToken = default) { @@ -677,6 +679,14 @@ public sealed class DevelopmentSqliteMigrator( TeachingTaskGradeAnalyticsMigration, TeachingTaskGradeAnalyticsStatements, cancellationToken); + var gradeRefreshSettingsExist = await db.Database + .SqlQueryRaw( + "SELECT COUNT(*) AS \"Value\" FROM sqlite_master WHERE type = 'table' AND name = 'CourseGradeStatisticsRefreshSettings'") + .AnyAsync(value => value > 0, cancellationToken); + await ApplyMigrationAsync( + CourseGradeStatisticsRefreshSettingsMigration, + gradeRefreshSettingsExist ? [] : CourseGradeStatisticsRefreshSettingsStatements, + cancellationToken); var swaggerSettingsExist = await db.Database .SqlQueryRaw( "SELECT COUNT(*) AS \"Value\" FROM sqlite_master WHERE type = 'table' AND name = 'SystemFeatureSettings'") @@ -2973,6 +2983,12 @@ public sealed class DevelopmentSqliteMigrator( """ ]; + private static readonly string[] CourseGradeStatisticsRefreshSettingsStatements = + [ + """CREATE TABLE IF NOT EXISTS "CourseGradeStatisticsRefreshSettings" ("Id" TEXT NOT NULL CONSTRAINT "PK_CourseGradeStatisticsRefreshSettings" PRIMARY KEY, "Key" TEXT NOT NULL, "IsEnabled" INTEGER NOT NULL, "IntervalSeconds" INTEGER NOT NULL, "BatchSize" INTEGER NOT NULL, "LastRunAt" TEXT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL);""", + """CREATE UNIQUE INDEX IF NOT EXISTS "IX_CourseGradeStatisticsRefreshSettings_Key" ON "CourseGradeStatisticsRefreshSettings" ("Key");""" + ]; + private static readonly string[] ExperimentClassroomConstraintStatements = [ """ diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260809123700_CourseGradeStatisticsRefreshSettings.Designer.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260809123700_CourseGradeStatisticsRefreshSettings.Designer.cs new file mode 100644 index 0000000..1d64db6 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260809123700_CourseGradeStatisticsRefreshSettings.Designer.cs @@ -0,0 +1,6867 @@ +// +using System; +using Jiaowu.Api.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260809123700_CourseGradeStatisticsRefreshSettings")] + partial class CourseGradeStatisticsRefreshSettings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AcademicTerm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicYear") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ArchivedAt") + .HasColumnType("datetime(6)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("IsArchived") + .HasColumnType("tinyint(1)"); + + b.Property("IsCurrent") + .HasColumnType("tinyint(1)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Season") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsArchived"); + + b.HasIndex("IsCurrent"); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("AcademicTerms"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AdministrativeClass", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CounselorName") + .HasColumnType("longtext"); + + b.Property("CounselorUserId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Grade") + .HasColumnType("int"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("MajorId") + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("CounselorUserId"); + + b.HasIndex("MajorId"); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("AdministrativeClasses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AttendanceCheckInAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AccuracyMeters") + .HasColumnType("double"); + + b.Property("AttendanceSheetId") + .HasColumnType("char(36)"); + + b.Property("CheckInMethod") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DeviceIdentifierHash") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("DevicePlatform") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("DistanceMeters") + .HasColumnType("double"); + + b.Property("FailureCode") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("IsSuccessful") + .HasColumnType("tinyint(1)"); + + b.Property("Latitude") + .HasPrecision(10, 7) + .HasColumnType("decimal(10,7)"); + + b.Property("Longitude") + .HasPrecision(10, 7) + .HasColumnType("decimal(10,7)"); + + b.Property("RiskFlags") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("StudentId"); + + b.HasIndex("DeviceIdentifierHash", "CreatedAt"); + + b.HasIndex("IpAddress", "CreatedAt"); + + b.HasIndex("AttendanceSheetId", "StudentId", "CreatedAt"); + + b.ToTable("AttendanceCheckInAttempts"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AttendanceRecord", b => + { + b.Property("AttendanceSheetId") + .HasColumnType("char(36)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("AppealReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("AppealReviewComment") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("AppealReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("AppealStatus") + .HasColumnType("int"); + + b.Property("AppealSubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("CheckInAccuracyMeters") + .HasColumnType("double"); + + b.Property("CheckInAt") + .HasColumnType("datetime(6)"); + + b.Property("CheckInDistanceMeters") + .HasColumnType("double"); + + b.Property("CheckInLatitude") + .HasPrecision(10, 7) + .HasColumnType("decimal(10,7)"); + + b.Property("CheckInLongitude") + .HasPrecision(10, 7) + .HasColumnType("decimal(10,7)"); + + b.Property("CheckedInMethod") + .HasColumnType("int"); + + b.Property("Notes") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("AttendanceSheetId", "StudentId"); + + b.HasIndex("AppealStatus"); + + b.HasIndex("StudentId"); + + b.ToTable("AttendanceRecords"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AttendanceSheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AttendanceDate") + .HasColumnType("datetime(6)"); + + b.Property("CheckInEndsAt") + .HasColumnType("datetime(6)"); + + b.Property("CheckInMethod") + .HasColumnType("int"); + + b.Property("CheckInStartsAt") + .HasColumnType("datetime(6)"); + + b.Property("CheckInToken") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("LocationRadiusMeters") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TargetLatitude") + .HasPrecision(10, 7) + .HasColumnType("decimal(10,7)"); + + b.Property("TargetLongitude") + .HasPrecision(10, 7) + .HasColumnType("decimal(10,7)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CheckInToken") + .IsUnique(); + + b.HasIndex("TeachingTaskId", "AttendanceDate"); + + b.ToTable("AttendanceSheets"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AutomaticScheduleJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActiveSchedulePlanId") + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CompletedTasks") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedEntries") + .HasColumnType("int"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessagesJson") + .HasColumnType("longtext"); + + b.Property("ProcessedTasks") + .HasColumnType("int"); + + b.Property("RequestedByUserId") + .HasColumnType("char(36)"); + + b.Property("SchedulePlanId") + .HasColumnType("char(36)"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalTasks") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ActiveSchedulePlanId") + .IsUnique(); + + b.HasIndex("RequestedByUserId"); + + b.HasIndex("SchedulePlanId", "CreatedAt"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("AutomaticScheduleJobs"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Building", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CampusId") + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CampusId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Buildings"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Campus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Address") + .HasColumnType("longtext"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Campuses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Classroom", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BuildingId") + .HasColumnType("char(36)"); + + b.Property("Capacity") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Equipment") + .HasColumnType("longtext"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RoomType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("TeachingVenueNature") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("BuildingId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Classrooms"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ClassroomReservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("ApplicantCollegeId") + .HasColumnType("char(36)"); + + b.Property("ApplicantName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ApplicantUserId") + .HasColumnType("char(36)"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CancelledAt") + .HasColumnType("datetime(6)"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("ContactPhone") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("Purpose") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ReservationDate") + .HasColumnType("date"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewedByUserId") + .HasColumnType("char(36)"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId"); + + b.HasIndex("ReviewedByUserId"); + + b.HasIndex("ApplicantCollegeId", "Status", "ReservationDate"); + + b.HasIndex("ApplicantUserId", "Status", "CreatedAt"); + + b.HasIndex("ClassroomId", "ReservationDate", "Status", "StartPeriod"); + + b.ToTable("ClassroomReservations"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.College", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CampusId") + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ShortName") + .HasColumnType("longtext"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CampusId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Colleges"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Course", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AssessmentMethod") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CollegeId") + .HasColumnType("char(36)"); + + b.Property("CourseCategoryId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Credits") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("EnglishName") + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LectureHours") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Nature") + .HasColumnType("int"); + + b.Property("PracticeHours") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("TotalHours") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("CourseCategoryId"); + + b.HasIndex("CollegeId", "Nature"); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Courses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseAdjustment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApplicantUserId") + .HasColumnType("char(36)"); + + b.Property("CancelDate") + .HasColumnType("date"); + + b.Property("CancelWeek") + .HasColumnType("int"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DayOfWeek") + .HasColumnType("int"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewedByUserId") + .HasColumnType("char(36)"); + + b.Property("SourceClassroomId") + .HasColumnType("char(36)"); + + b.Property("SourceDate") + .HasColumnType("date"); + + b.Property("SourcePeriodCount") + .HasColumnType("int"); + + b.Property("SourceScheduleEntryId") + .HasColumnType("char(36)"); + + b.Property("SourceStartPeriod") + .HasColumnType("int"); + + b.Property("SourceWeek") + .HasColumnType("int"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("SubstituteTeacherId") + .HasColumnType("char(36)"); + + b.Property("TargetDate") + .HasColumnType("date"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantUserId"); + + b.HasIndex("ClassroomId"); + + b.HasIndex("SubstituteTeacherId"); + + b.HasIndex("SourceScheduleEntryId", "SourceWeek"); + + b.HasIndex("Status", "CreatedAt"); + + b.HasIndex("TeachingTaskId", "Status"); + + b.ToTable("CourseAdjustments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("CourseCategories"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CourseSelectionOfferingId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EnrolledAt") + .HasColumnType("datetime(6)"); + + b.Property("EnrollmentType") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WaitlistedAt") + .HasColumnType("datetime(6)"); + + b.Property("WithdrawnAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CourseSelectionOfferingId", "StudentId") + .IsUnique(); + + b.HasIndex("StudentId", "Status"); + + b.HasIndex("CourseSelectionOfferingId", "Status", "WaitlistedAt") + .HasDatabaseName("IX_CE_Offering_Status_WaitlistedAt"); + + b.ToTable("CourseEnrollments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseExemption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewedByUserId") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("Status", "CreatedAt"); + + b.HasIndex("StudentId", "TeachingTaskId") + .IsUnique(); + + b.ToTable("CourseExemptions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGradeStatistic", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("AverageScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("Below60Count") + .HasColumnType("int"); + + b.Property("CalculatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("From60To69Count") + .HasColumnType("int"); + + b.Property("From70To79Count") + .HasColumnType("int"); + + b.Property("From80To89Count") + .HasColumnType("int"); + + b.Property("From90To100Count") + .HasColumnType("int"); + + b.Property("HighestScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("LowestScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("PassRate") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("PassedCount") + .HasColumnType("int"); + + b.Property("Scope") + .HasColumnType("int"); + + b.Property("ScopeEntityId") + .HasColumnType("char(36)"); + + b.Property("StudentCount") + .HasColumnType("int"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("GradeSheetId") + .HasColumnType("char(36)"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GradeSheetId"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("CourseGradeStatisticsRefreshJobs"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGradeStatisticsRefreshSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BatchSize") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IntervalSeconds") + .HasColumnType("int"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LastRunAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("CourseGradeStatisticsRefreshSettings"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("CourseGroups"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGroupCourse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CourseGroupId") + .HasColumnType("char(36)"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.HasIndex("CourseGroupId", "CourseId") + .IsUnique(); + + b.ToTable("CourseGroupCourses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CoursePrerequisite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("PrerequisiteCourseId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PrerequisiteCourseId"); + + b.HasIndex("CourseId", "PrerequisiteCourseId") + .IsUnique(); + + b.ToTable("CoursePrerequisites"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionOffering", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Capacity") + .HasColumnType("int"); + + b.Property("CourseSelectionRoundId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsOpenToAll") + .HasColumnType("tinyint(1)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("CourseSelectionRoundId", "TeachingTaskId") + .IsUnique(); + + b.ToTable("CourseSelectionOfferings"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionRound", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndsAt") + .HasColumnType("datetime(6)"); + + b.Property("MaxCourseCount") + .HasColumnType("int"); + + b.Property("MaxCredits") + .HasPrecision(6, 1) + .HasColumnType("decimal(6,1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("StartsAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WithdrawalEndsAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId", "Status"); + + b.ToTable("CourseSelectionRounds"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionRoundGrade", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CourseSelectionRoundId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Grade") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Grade"); + + b.HasIndex("CourseSelectionRoundId", "Grade") + .IsUnique(); + + b.ToTable("CourseSelectionRoundGrades"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSubstitution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("OriginalCourseId") + .HasColumnType("char(36)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewedByUserId") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("SubstituteCourseId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OriginalCourseId"); + + b.HasIndex("SubstituteCourseId"); + + b.HasIndex("Status", "CreatedAt"); + + b.HasIndex("StudentId", "OriginalCourseId") + .IsUnique(); + + b.ToTable("CourseSubstitutions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumCourse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurriculumModuleId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RecommendedSemester") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.HasIndex("CurriculumModuleId", "CourseId") + .IsUnique(); + + b.ToTable("CurriculumCourses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumModule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurriculumPlanId") + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RequiredCredits") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CurriculumPlanId", "Code") + .IsUnique(); + + b.ToTable("CurriculumModules"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("EffectiveGrade") + .HasColumnType("int"); + + b.Property("MajorId") + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalCredits") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.HasKey("Id"); + + b.HasIndex("Status", "EffectiveGrade"); + + b.HasIndex("MajorId", "EffectiveGrade", "Version") + .IsUnique(); + + b.ToTable("CurriculumPlans"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.DeferredExam", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewedByUserId") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("Status", "CreatedAt"); + + b.HasIndex("StudentId", "TeachingTaskId") + .IsUnique(); + + b.ToTable("DeferredExams"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.DegreeAwardBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CalculatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DegreeName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("varchar(80)"); + + b.Property("GraduationYear") + .HasColumnType("int"); + + b.Property("MinimumGradePoint") + .HasPrecision(3, 2) + .HasColumnType("decimal(3,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GraduationYear", "Status"); + + b.ToTable("DegreeAwardBatches"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.DegreeAwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AverageGradePoint") + .HasPrecision(4, 2) + .HasColumnType("decimal(4,2)"); + + b.Property("CalculatedConclusion") + .HasColumnType("int"); + + b.Property("Conclusion") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DegreeAwardBatchId") + .HasColumnType("char(36)"); + + b.Property("ExceptionReason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("GraduationAuditResultId") + .HasColumnType("char(36)"); + + b.Property("IsOverridden") + .HasColumnType("tinyint(1)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GraduationAuditResultId"); + + b.HasIndex("StudentId"); + + b.HasIndex("Conclusion", "IsOverridden"); + + b.HasIndex("DegreeAwardBatchId", "StudentId") + .IsUnique(); + + b.ToTable("DegreeAwardResults"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationDimension", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EvaluationSetupId") + .HasColumnType("char(36)"); + + b.Property("MaxScore") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("varchar(60)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("EvaluationSetupId", "SortOrder"); + + b.ToTable("EvaluationDimensions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EvaluationSetupId") + .HasColumnType("char(36)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("StudentId"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("EvaluationSetupId", "StudentId", "TeachingTaskId") + .IsUnique(); + + b.ToTable("EvaluationRecords"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationScore", b => + { + b.Property("EvaluationRecordId") + .HasColumnType("char(36)"); + + b.Property("EvaluationDimensionId") + .HasColumnType("char(36)"); + + b.Property("Score") + .HasColumnType("int"); + + b.HasKey("EvaluationRecordId", "EvaluationDimensionId"); + + b.HasIndex("EvaluationDimensionId"); + + b.ToTable("EvaluationScores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationSetup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndsAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("StartsAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId", "Status"); + + b.ToTable("EvaluationSetups"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamArrangementJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActivePlanId") + .HasColumnType("char(36)"); + + b.Property("AssignClassrooms") + .HasColumnType("tinyint(1)"); + + b.Property("AssignInvigilators") + .HasColumnType("tinyint(1)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentStep") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("PlanId") + .HasColumnType("char(36)"); + + b.Property("ProcessedSessions") + .HasColumnType("int"); + + b.Property("RequestedByUserId") + .HasColumnType("char(36)"); + + b.Property("ResultMessage") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("SessionIdsJson") + .HasColumnType("longtext"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalSessions") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("RequestedByUserId"); + + b.HasIndex("Kind", "ActivePlanId") + .IsUnique() + .HasDatabaseName("UX_ExamArrangementJobs_Kind_ActivePlan"); + + b.HasIndex("Status", "CreatedAt"); + + b.HasIndex("Kind", "PlanId", "CreatedAt") + .HasDatabaseName("IX_ExamArrangementJobs_Kind_Plan_CreatedAt"); + + b.ToTable("ExamArrangementJobs"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId", "Status"); + + b.ToTable("ExamPlans"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamPublishJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActivePlanId") + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentStep") + .HasColumnType("longtext"); + + b.Property("ErrorMessage") + .HasColumnType("longtext"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("PlanId") + .HasColumnType("char(36)"); + + b.Property("RequestedByUserId") + .HasColumnType("char(36)"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("ExamPublishJobs"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndsAt") + .HasColumnType("datetime(6)"); + + b.Property("ExamDate") + .HasColumnType("date"); + + b.Property("ExamPlanId") + .HasColumnType("char(36)"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("RequiredInvigilatorCount") + .HasColumnType("int"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("StartsAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ClassroomId"); + + b.HasIndex("CourseId") + .HasDatabaseName("IX_ExamRooms_CourseId"); + + b.HasIndex("ExamPlanId", "StartsAt") + .HasDatabaseName("IX_ExamRooms_Plan_Time"); + + b.HasIndex("ExamPlanId", "ClassroomId", "StartsAt") + .HasDatabaseName("IX_ExamRooms_Plan_Room_Time"); + + b.ToTable("ExamRooms", (string)null); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomInvigilator", b => + { + b.Property("ExamRoomId") + .HasColumnType("char(36)"); + + b.Property("TeacherId") + .HasColumnType("char(36)"); + + b.HasKey("ExamRoomId", "TeacherId"); + + b.HasIndex("TeacherId") + .HasDatabaseName("IX_ExamRoomInvigilators_TeacherId"); + + b.ToTable("ExamRoomInvigilators", (string)null); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomSession", b => + { + b.Property("ExamRoomId") + .HasColumnType("char(36)"); + + b.Property("ExamSessionId") + .HasColumnType("char(36)"); + + b.HasKey("ExamRoomId", "ExamSessionId"); + + b.HasIndex("ExamSessionId") + .HasDatabaseName("IX_ExamRoomSessions_SessionId"); + + b.ToTable("ExamRoomSessions", (string)null); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSeatAssignment", b => + { + b.Property("ExamRoomId") + .HasColumnType("char(36)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("ExamSessionId") + .HasColumnType("char(36)"); + + b.Property("SeatNumber") + .HasColumnType("int"); + + b.HasKey("ExamRoomId", "StudentId"); + + b.HasIndex("StudentId") + .HasDatabaseName("IX_ExamSeats_StudentId"); + + b.HasIndex("ExamSessionId", "StudentId") + .IsUnique() + .HasDatabaseName("UX_ExamSeats_Session_Student"); + + b.ToTable("ExamSeats", (string)null); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndsAt") + .HasColumnType("datetime(6)"); + + b.Property("ExamDate") + .HasColumnType("date"); + + b.Property("ExamPlanId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("RequiredBuildingId") + .HasColumnType("char(36)"); + + b.Property("RequiredBuildingIds") + .HasColumnType("longtext"); + + b.Property("RequiredInvigilatorCount") + .HasColumnType("int"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("StartsAt") + .HasColumnType("datetime(6)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ClassroomId"); + + b.HasIndex("RequiredBuildingId"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("ExamPlanId", "ExamDate"); + + b.HasIndex("ExamPlanId", "StartsAt"); + + b.ToTable("ExamSessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSessionInvigilator", b => + { + b.Property("ExamSessionId") + .HasColumnType("char(36)"); + + b.Property("TeacherId") + .HasColumnType("char(36)"); + + b.HasKey("ExamSessionId", "TeacherId"); + + b.HasIndex("TeacherId"); + + b.ToTable("ExamSessionInvigilators"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSignInExportJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentStep") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("FileBytes") + .HasColumnType("longblob"); + + b.Property("FileName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("FileSize") + .HasColumnType("int"); + + b.Property("PlanId") + .HasColumnType("char(36)"); + + b.Property("RequestedByUserId") + .HasColumnType("char(36)"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("RequestedByUserId"); + + b.HasIndex("PlanId", "CreatedAt"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("ExamSignInExportJobs"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentBooking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BookedAt") + .HasColumnType("datetime(6)"); + + b.Property("CancelledAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentProjectId") + .HasColumnType("char(36)"); + + b.Property("ExperimentSessionId") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("StudentId"); + + b.HasIndex("ExperimentProjectId", "StudentId") + .IsUnique(); + + b.HasIndex("ExperimentSessionId", "Status", "BookedAt"); + + b.ToTable("ExperimentBookings"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentGradeSheetId") + .HasColumnType("char(36)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("varchar(60)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Weight") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.HasKey("Id"); + + b.HasIndex("ExperimentGradeSheetId", "SortOrder"); + + b.ToTable("ExperimentGradeItems"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItemScore", b => + { + b.Property("ExperimentGradeRecordId") + .HasColumnType("char(36)"); + + b.Property("ExperimentGradeItemId") + .HasColumnType("char(36)"); + + b.Property("Comment") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Score") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.HasKey("ExperimentGradeRecordId", "ExperimentGradeItemId"); + + b.HasIndex("ExperimentGradeItemId"); + + b.ToTable("ExperimentGradeItemScores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentGradeSheetId") + .HasColumnType("char(36)"); + + b.Property("ExperimentSessionId") + .HasColumnType("char(36)"); + + b.Property("IsLate") + .HasColumnType("tinyint(1)"); + + b.Property("IsPassed") + .HasColumnType("tinyint(1)"); + + b.Property("ParticipationStatus") + .HasColumnType("int"); + + b.Property("SafetyViolation") + .HasColumnType("tinyint(1)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("SubmissionReference") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TeacherComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("TotalScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ExperimentSessionId"); + + b.HasIndex("StudentId"); + + b.HasIndex("ExperimentGradeSheetId", "StudentId") + .IsUnique(); + + b.ToTable("ExperimentGradeRecords"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContributionWeight") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentProjectId") + .HasColumnType("char(36)"); + + b.Property("PassScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ExperimentProjectId") + .IsUnique(); + + b.ToTable("ExperimentGradeSheets"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ArrangementMode") + .HasColumnType("int"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Requirements") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("ScheduleEntryId") + .HasColumnType("char(36)"); + + b.Property("ScheduleWeek") + .HasColumnType("int"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ScheduleEntryId"); + + b.HasIndex("Status", "StartDate", "EndDate"); + + b.HasIndex("TeachingTaskId", "Code", "ScheduleEntryId", "ScheduleWeek") + .IsUnique(); + + b.ToTable("ExperimentProjects"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CancelledAt") + .HasColumnType("datetime(6)"); + + b.Property("Capacity") + .HasColumnType("int"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentProjectId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("ReservedCount") + .HasColumnType("int"); + + b.Property("SessionDate") + .HasColumnType("date"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ExperimentProjectId", "SessionDate", "StartPeriod"); + + b.HasIndex("ClassroomId", "SessionDate", "Status", "StartPeriod"); + + b.ToTable("ExperimentSessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("GradeSheetId") + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("varchar(60)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("SourceSnapshotAt") + .HasColumnType("datetime(6)"); + + b.Property("SourceType") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(1); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Weight") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.HasKey("Id"); + + b.HasIndex("GradeSheetId", "SortOrder"); + + b.ToTable("GradeItems"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItemScore", b => + { + b.Property("GradeRecordId") + .HasColumnType("char(36)"); + + b.Property("GradeItemId") + .HasColumnType("char(36)"); + + b.Property("Score") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.HasKey("GradeRecordId", "GradeItemId"); + + b.HasIndex("GradeItemId"); + + b.ToTable("GradeItemScores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeModification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApplicantUserId") + .HasColumnType("char(36)"); + + b.Property("CollegeReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("CollegeReviewedByUserId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("FinalReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("FinalReviewedByUserId") + .HasColumnType("char(36)"); + + b.Property("GradeRecordId") + .HasColumnType("char(36)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RequestedScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GradeRecordId"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("GradeModifications"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExamStatus") + .HasColumnType("int"); + + b.Property("FinalScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("GradePoint") + .HasPrecision(3, 1) + .HasColumnType("decimal(3,1)"); + + b.Property("GradeSheetId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RegularScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("TotalScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GradeSheetId", "StudentId") + .IsUnique(); + + b.HasIndex("StudentId", "TotalScore"); + + b.ToTable("GradeRecords"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeSheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FinalWeight") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("RegularWeight") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("TeachingTaskId") + .IsUnique(); + + b.ToTable("GradeSheets"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationAuditBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CalculatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EnrollmentYear") + .HasColumnType("int"); + + b.Property("GraduationYear") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("GraduationYear", "EnrollmentYear"); + + b.ToTable("GraduationAuditBatches"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationAuditResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CalculatedConclusion") + .HasColumnType("int"); + + b.Property("Conclusion") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurriculumPlanId") + .HasColumnType("char(36)"); + + b.Property("EarnedCredits") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("FailedCourseCount") + .HasColumnType("int"); + + b.Property("GraduationAuditBatchId") + .HasColumnType("char(36)"); + + b.Property("IsOverridden") + .HasColumnType("tinyint(1)"); + + b.Property("MissingCourseNames") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("PassedRequiredCourseCount") + .HasColumnType("int"); + + b.Property("RequiredCourseCount") + .HasColumnType("int"); + + b.Property("RequiredCredits") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("StudentStatusSnapshot") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CurriculumPlanId"); + + b.HasIndex("StudentId"); + + b.HasIndex("Conclusion", "IsOverridden"); + + b.HasIndex("GraduationAuditBatchId", "StudentId") + .IsUnique(); + + b.ToTable("GraduationAuditResults"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("GraduationYear") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GraduationYear", "Status"); + + b.ToTable("GraduationClearanceBatches"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("GraduationClearanceBatchId") + .HasColumnType("char(36)"); + + b.Property("IsRequired") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ResponsibleRole") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("ResponsibleUnit") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GraduationClearanceBatchId", "Code") + .IsUnique(); + + b.ToTable("GraduationClearanceItems"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CompletedByUserId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("GraduationClearanceItemId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GraduationClearanceItemId", "StudentId") + .IsUnique(); + + b.HasIndex("StudentId", "Status"); + + b.ToTable("GraduationClearanceRecords"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Major", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CollegeId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DegreeType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SchoolingYears") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("CollegeId"); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Majors"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamAutoJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedSessions") + .HasColumnType("int"); + + b.Property("EnrolledStudents") + .HasColumnType("int"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MakeupExamPlanId") + .HasColumnType("char(36)"); + + b.Property("MessagesJson") + .HasColumnType("longtext"); + + b.Property("ProcessedCourses") + .HasColumnType("int"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalCourses") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("MakeupExamPlanId", "CreatedAt"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("MakeupExamAutoJobs"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamEnrollment", b => + { + b.Property("MakeupExamSessionId") + .HasColumnType("char(36)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("MakeupScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("Reason") + .HasColumnType("int"); + + b.Property("SourceDeferredExamId") + .HasColumnType("char(36)"); + + b.Property("SourceGradeRecordId") + .HasColumnType("char(36)"); + + b.HasKey("MakeupExamSessionId", "StudentId"); + + b.HasIndex("SourceDeferredExamId"); + + b.HasIndex("SourceGradeRecordId"); + + b.HasIndex("StudentId", "MakeupExamSessionId"); + + b.ToTable("MakeupExamEnrollments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId", "Status"); + + b.ToTable("MakeupExamPlans"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndsAt") + .HasColumnType("datetime(6)"); + + b.Property("ExamDate") + .HasColumnType("date"); + + b.Property("MakeupExamPlanId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("RequiredBuildingId") + .HasColumnType("char(36)"); + + b.Property("RequiredBuildingIds") + .HasColumnType("longtext"); + + b.Property("RequiredInvigilatorCount") + .HasColumnType("int"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("StartsAt") + .HasColumnType("datetime(6)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ClassroomId"); + + b.HasIndex("RequiredBuildingId"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("MakeupExamPlanId", "ExamDate"); + + b.HasIndex("MakeupExamPlanId", "StartsAt"); + + b.ToTable("MakeupExamSessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamSessionInvigilator", b => + { + b.Property("MakeupExamSessionId") + .HasColumnType("char(36)"); + + b.Property("TeacherId") + .HasColumnType("char(36)"); + + b.HasKey("MakeupExamSessionId", "TeacherId"); + + b.HasIndex("TeacherId"); + + b.ToTable("MakeupExamSessionInvigilators"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MessageDispatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AudienceId") + .HasColumnType("char(36)"); + + b.Property("AudienceName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("AudienceType") + .HasColumnType("int"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("Content") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("LinkUrl") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RecipientCount") + .HasColumnType("int"); + + b.Property("SenderName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SenderUserId") + .HasColumnType("char(36)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SenderUserId", "CreatedAt"); + + b.ToTable("MessageDispatches"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("Content") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsRead") + .HasColumnType("tinyint(1)"); + + b.Property("LinkUrl") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MessageDispatchId") + .HasColumnType("char(36)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("MessageDispatchId"); + + b.HasIndex("UserId", "IsRead"); + + b.HasIndex("UserId", "Category", "CreatedAt"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DocumentNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvalidatedAt") + .HasColumnType("datetime(6)"); + + b.Property("InvalidatedByUserId") + .HasColumnType("char(36)"); + + b.Property("InvalidationReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IssuedAt") + .HasColumnType("datetime(6)"); + + b.Property("IssuedByUserId") + .HasColumnType("char(36)"); + + b.Property("PdfContent") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("PdfSha256") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Purpose") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ReissuedFromDocumentId") + .HasColumnType("char(36)"); + + b.Property("SnapshotJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("VerificationCodeHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentNumber") + .IsUnique(); + + b.HasIndex("InvalidatedByUserId"); + + b.HasIndex("IssuedByUserId"); + + b.HasIndex("ReissuedFromDocumentId") + .IsUnique(); + + b.HasIndex("VerificationCodeHash") + .IsUnique(); + + b.HasIndex("Status", "IssuedAt"); + + b.HasIndex("StudentId", "IssuedAt"); + + b.ToTable("OfficialDocuments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocumentDownload", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DownloadedByUserId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("OfficialDocumentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("DownloadedByUserId", "CreatedAt"); + + b.HasIndex("OfficialDocumentId", "CreatedAt"); + + b.ToTable("OfficialDocumentDownloads"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OtherExamBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExamCode") + .HasMaxLength(60) + .HasColumnType("varchar(60)"); + + b.Property("ExamDate") + .HasColumnType("date"); + + b.Property("LevelOptions") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("MaxScore") + .HasPrecision(8, 2) + .HasColumnType("decimal(8,2)"); + + b.Property("MetricKind") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("Organizer") + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("PublicationCount") + .HasColumnType("int"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Status", "ExamDate"); + + b.ToTable("OtherExamBatches"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OtherExamResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsPassed") + .HasColumnType("tinyint(1)"); + + b.Property("Level") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("OtherExamBatchId") + .HasColumnType("char(36)"); + + b.Property("Score") + .HasPrecision(8, 2) + .HasColumnType("decimal(8,2)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("StudentId", "OtherExamBatchId"); + + b.HasIndex("OtherExamBatchId", "StudentId", "AttemptNumber") + .IsUnique(); + + b.ToTable("OtherExamResults"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.PublishedScheduleOccurrence", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DayOfWeek") + .HasColumnType("int"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("ScheduleEntryId") + .HasColumnType("char(36)"); + + b.Property("SchedulePlanId") + .HasColumnType("char(36)"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Week") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ClassroomId"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("ScheduleEntryId", "Week") + .IsUnique(); + + b.HasIndex("AcademicTermId", "TeachingTaskId", "Week"); + + b.HasIndex("AcademicTermId", "Week", "DayOfWeek", "StartPeriod", "ClassroomId"); + + b.HasIndex("SchedulePlanId", "ClassroomId", "Week", "DayOfWeek", "StartPeriod"); + + b.ToTable("PublishedScheduleOccurrences"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ScheduleEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DayOfWeek") + .HasColumnType("int"); + + b.Property("EndWeek") + .HasColumnType("int"); + + b.Property("Kind") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(1); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("SchedulePlanId") + .HasColumnType("char(36)"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("StartWeek") + .HasColumnType("int"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WeekPattern") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ClassroomId"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("SchedulePlanId", "DayOfWeek", "StartPeriod"); + + b.ToTable("ScheduleEntries"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.SchedulePlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId", "Status"); + + b.HasIndex("AcademicTermId", "Version") + .IsUnique(); + + b.ToTable("SchedulePlans"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.SchedulePublishJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("ActiveAcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CompletedSteps") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentStep") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("RequestedByUserId") + .HasColumnType("char(36)"); + + b.Property("SchedulePlanId") + .HasColumnType("char(36)"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalSteps") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ActiveAcademicTermId") + .IsUnique(); + + b.HasIndex("RequestedByUserId"); + + b.HasIndex("SchedulePlanId", "CreatedAt"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("SchedulePublishJobs"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ScheduleTimeSlot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndsAt") + .HasColumnType("time"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("PeriodNumber") + .HasColumnType("int"); + + b.Property("StartsAt") + .HasColumnType("time"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId", "PeriodNumber") + .IsUnique(); + + b.ToTable("ScheduleTimeSlots"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Student", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AdministrativeClassId") + .HasColumnType("char(36)"); + + b.Property("Biography") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentAddress") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("Email") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("EmergencyContactName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("EmergencyContactPhone") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("EmergencyContactRelationship") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("EnglishName") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("EnrollmentDate") + .HasColumnType("date"); + + b.Property("EnrollmentYear") + .HasColumnType("int"); + + b.Property("Ethnicity") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Gender") + .HasColumnType("int"); + + b.Property("HouseholdAddress") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("IdCardNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Nationality") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("NativePlace") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("PoliticalStatus") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Qq") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("SpecialNeeds") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("SpecialTags") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentNumber") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("WeChat") + .HasMaxLength(60) + .HasColumnType("varchar(60)"); + + b.HasKey("Id"); + + b.HasIndex("EnrollmentYear"); + + b.HasIndex("StudentNumber") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("AdministrativeClassId", "Status"); + + b.ToTable("Students"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.StudentStatusChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApprovedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("OriginalStatus") + .HasColumnType("int"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TargetStatus") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("StudentId", "State"); + + b.ToTable("StudentStatusChanges"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Teacher", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CollegeId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gender") + .HasColumnType("int"); + + b.Property("HireDate") + .HasColumnType("date"); + + b.Property("IsExternal") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TeacherNumber") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("Title") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TeacherNumber") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("CollegeId", "Status"); + + b.ToTable("Teachers"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeacherCourseApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewedByUserId") + .HasColumnType("char(36)"); + + b.Property("Statement") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TeacherId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.HasIndex("ReviewedByUserId"); + + b.HasIndex("TeacherId"); + + b.HasIndex("Status", "AcademicTermId"); + + b.HasIndex("AcademicTermId", "TeacherId", "CourseId") + .IsUnique(); + + b.ToTable("TeacherCourseApplications"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("Capacity") + .HasColumnType("int"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndWeek") + .HasColumnType("int"); + + b.Property("GenerationBatchCode") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("SchedulingMode") + .HasColumnType("int"); + + b.Property("StartWeek") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TaskNumber") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WeeklyHours") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.HasIndex("TaskNumber") + .IsUnique(); + + b.HasIndex("AcademicTermId", "Status"); + + b.ToTable("TeachingTasks"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskAllowedClassroom", b => + { + b.Property("TeachingTaskScheduleConstraintId") + .HasColumnType("char(36)"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.HasKey("TeachingTaskScheduleConstraintId", "ClassroomId"); + + b.HasIndex("ClassroomId"); + + b.ToTable("TeachingTaskAllowedClassrooms"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskAllowedExperimentClassroom", b => + { + b.Property("TeachingTaskScheduleConstraintId") + .HasColumnType("char(36)"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.HasKey("TeachingTaskScheduleConstraintId", "ClassroomId"); + + b.HasIndex("ClassroomId"); + + b.ToTable("TeachingTaskAllowedExperimentClassrooms"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskClass", b => + { + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("AdministrativeClassId") + .HasColumnType("char(36)"); + + b.HasKey("TeachingTaskId", "AdministrativeClassId"); + + b.HasIndex("AdministrativeClassId"); + + b.ToTable("TeachingTaskClasses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskGradeScoreBand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("LowerBound") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("StudentCount") + .HasColumnType("int"); + + b.Property("TeachingTaskGradeStatisticId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpperBound") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.HasKey("Id"); + + b.HasIndex("TeachingTaskGradeStatisticId", "SortOrder") + .IsUnique(); + + b.ToTable("TeachingTaskGradeScoreBands"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskGradeStatistic", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("AverageScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("CalculatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExcellentCount") + .HasColumnType("int"); + + b.Property("ExcellentRate") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("GradeSheetId") + .HasColumnType("char(36)"); + + b.Property("HighestScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("LowestScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("MedianScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("PassRate") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("PassedCount") + .HasColumnType("int"); + + b.Property("StandardDeviation") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("StudentCount") + .HasColumnType("int"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId"); + + b.HasIndex("GradeSheetId") + .IsUnique(); + + b.HasIndex("TeachingTaskId") + .IsUnique(); + + b.HasIndex("CourseId", "AcademicTermId"); + + b.ToTable("TeachingTaskGradeStatistics"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllowedDayOfWeeks") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("AllowedExperimentVenueNatures") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EarliestPeriod") + .HasColumnType("int"); + + b.Property("ExperimentRequiredBuildingId") + .HasColumnType("char(36)"); + + b.Property("ExperimentRequiredCampusId") + .HasColumnType("char(36)"); + + b.Property("LatestPeriod") + .HasColumnType("int"); + + b.Property("RequiredBuildingId") + .HasColumnType("char(36)"); + + b.Property("RequiredCampusId") + .HasColumnType("char(36)"); + + b.Property("RequiresClassroom") + .HasColumnType("tinyint(1)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ExperimentRequiredBuildingId"); + + b.HasIndex("ExperimentRequiredCampusId"); + + b.HasIndex("RequiredBuildingId"); + + b.HasIndex("RequiredCampusId"); + + b.HasIndex("TeachingTaskId") + .IsUnique(); + + b.ToTable("TeachingTaskScheduleConstraints"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskTeacher", b => + { + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("TeacherId") + .HasColumnType("char(36)"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)"); + + b.HasKey("TeachingTaskId", "TeacherId"); + + b.HasIndex("TeacherId"); + + b.ToTable("TeachingTaskTeachers"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.WarningRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("AcknowledgeComment") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("AcknowledgedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Detail") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("TriggerValue") + .HasPrecision(7, 2) + .HasColumnType("decimal(7,2)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("StudentId", "AcademicTermId", "Type") + .IsUnique(); + + b.ToTable("WarningRecords"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.WarningRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("AutoCheckEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("CheckDayOfWeek") + .HasColumnType("int"); + + b.Property("CheckHour") + .HasColumnType("int"); + + b.Property("CheckMinute") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LastCheckAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("NotifyCounselor") + .HasColumnType("tinyint(1)"); + + b.Property("NotifyStudent") + .HasColumnType("tinyint(1)"); + + b.Property("Threshold") + .HasPrecision(7, 2) + .HasColumnType("decimal(7,2)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId", "Type") + .IsUnique(); + + b.ToTable("WarningRules"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Identity.ApplicationRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("DataScope") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Identity.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("CalendarSubscriptionCreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CalendarSubscriptionStamp") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CollegeId") + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LastLoginAt") + .HasColumnType("datetime(6)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("StaffNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("StaffNumber"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Identity.RefreshSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ClientType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("LastRefreshedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReplacedBySessionId") + .HasColumnType("char(36)"); + + b.Property("RevokedAt") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.HasIndex("UserId", "ExpiresAt"); + + b.ToTable("RefreshSessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.System.AppUpdateRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BundleContent") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("Channel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByUserName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("varchar(180)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("NativeVersion") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("PublishedByUserName") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ReleaseNotes") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Sha256") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Platform", "Channel", "NativeVersion", "Status"); + + b.HasIndex("Platform", "Channel", "NativeVersion", "Version") + .IsUnique(); + + b.ToTable("AppUpdateReleases"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.System.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Method") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("StatusCode") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("UserName") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.System.BackgroundJobOutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("JobId") + .HasColumnType("char(36)"); + + b.Property("JobKind") + .HasColumnType("int"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("LeaseExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("ProcessingAttempts") + .HasColumnType("int"); + + b.Property("ProcessingToken") + .HasColumnType("char(36)"); + + b.Property("PublishAttempts") + .HasColumnType("int"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("LeaseExpiresAt"); + + b.HasIndex("JobKind", "JobId") + .IsUnique(); + + b.HasIndex("State", "CompletedAt"); + + b.HasIndex("State", "CreatedAt"); + + b.ToTable("BackgroundJobOutboxMessages"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.System.SystemFeatureSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("SystemFeatureSettings"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AdministrativeClass", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "CounselorUser") + .WithMany() + .HasForeignKey("CounselorUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.Major", "Major") + .WithMany() + .HasForeignKey("MajorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CounselorUser"); + + b.Navigation("Major"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AttendanceCheckInAttempt", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AttendanceSheet", "AttendanceSheet") + .WithMany("CheckInAttempts") + .HasForeignKey("AttendanceSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AttendanceSheet"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AttendanceRecord", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AttendanceSheet", "AttendanceSheet") + .WithMany("Records") + .HasForeignKey("AttendanceSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AttendanceSheet"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AttendanceSheet", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AutomaticScheduleJob", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("RequestedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.SchedulePlan", "SchedulePlan") + .WithMany() + .HasForeignKey("SchedulePlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SchedulePlan"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Building", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Campus", "Campus") + .WithMany() + .HasForeignKey("CampusId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Campus"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Classroom", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Building", "Building") + .WithMany() + .HasForeignKey("BuildingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Building"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ClassroomReservation", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.College", "ApplicantCollege") + .WithMany() + .HasForeignKey("ApplicantCollegeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "ApplicantUser") + .WithMany() + .HasForeignKey("ApplicantUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "ReviewedByUser") + .WithMany() + .HasForeignKey("ReviewedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AcademicTerm"); + + b.Navigation("ApplicantCollege"); + + b.Navigation("ApplicantUser"); + + b.Navigation("Classroom"); + + b.Navigation("ReviewedByUser"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.College", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Campus", "Campus") + .WithMany() + .HasForeignKey("CampusId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Campus"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Course", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.College", "College") + .WithMany() + .HasForeignKey("CollegeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.CourseCategory", "CourseCategory") + .WithMany() + .HasForeignKey("CourseCategoryId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("College"); + + b.Navigation("CourseCategory"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseAdjustment", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.Teacher", "SubstituteTeacher") + .WithMany() + .HasForeignKey("SubstituteTeacherId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("SubstituteTeacher"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseEnrollment", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.CourseSelectionOffering", "CourseSelectionOffering") + .WithMany("Enrollments") + .HasForeignKey("CourseSelectionOfferingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CourseSelectionOffering"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseExemption", 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.Restrict) + .IsRequired(); + + b.Navigation("Student"); + + 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.CourseGroupCourse", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.CourseGroup", "CourseGroup") + .WithMany("Courses") + .HasForeignKey("CourseGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "Course") + .WithMany() + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Course"); + + b.Navigation("CourseGroup"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CoursePrerequisite", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "Course") + .WithMany("Prerequisites") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "PrerequisiteCourse") + .WithMany("RequiredByCourses") + .HasForeignKey("PrerequisiteCourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Course"); + + b.Navigation("PrerequisiteCourse"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionOffering", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.CourseSelectionRound", "CourseSelectionRound") + .WithMany("Offerings") + .HasForeignKey("CourseSelectionRoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CourseSelectionRound"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionRound", 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.CourseSelectionRoundGrade", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.CourseSelectionRound", "CourseSelectionRound") + .WithMany("EligibleGrades") + .HasForeignKey("CourseSelectionRoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CourseSelectionRound"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSubstitution", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "OriginalCourse") + .WithMany() + .HasForeignKey("OriginalCourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "SubstituteCourse") + .WithMany() + .HasForeignKey("SubstituteCourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("OriginalCourse"); + + b.Navigation("Student"); + + b.Navigation("SubstituteCourse"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumCourse", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "Course") + .WithMany() + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.CurriculumModule", "CurriculumModule") + .WithMany("Courses") + .HasForeignKey("CurriculumModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Course"); + + b.Navigation("CurriculumModule"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumModule", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.CurriculumPlan", "CurriculumPlan") + .WithMany("Modules") + .HasForeignKey("CurriculumPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurriculumPlan"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumPlan", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Major", "Major") + .WithMany() + .HasForeignKey("MajorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Major"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.DeferredExam", 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.Restrict) + .IsRequired(); + + b.Navigation("Student"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.DegreeAwardResult", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.DegreeAwardBatch", "DegreeAwardBatch") + .WithMany("Results") + .HasForeignKey("DegreeAwardBatchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.GraduationAuditResult", "GraduationAuditResult") + .WithMany() + .HasForeignKey("GraduationAuditResultId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DegreeAwardBatch"); + + b.Navigation("GraduationAuditResult"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationDimension", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.EvaluationSetup", "EvaluationSetup") + .WithMany("Dimensions") + .HasForeignKey("EvaluationSetupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EvaluationSetup"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationRecord", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.EvaluationSetup", "EvaluationSetup") + .WithMany("Records") + .HasForeignKey("EvaluationSetupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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.Restrict) + .IsRequired(); + + b.Navigation("EvaluationSetup"); + + b.Navigation("Student"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationScore", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.EvaluationDimension", "EvaluationDimension") + .WithMany("Scores") + .HasForeignKey("EvaluationDimensionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.EvaluationRecord", "EvaluationRecord") + .WithMany("Scores") + .HasForeignKey("EvaluationRecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EvaluationDimension"); + + b.Navigation("EvaluationRecord"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationSetup", 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.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.ExamRoomAssignment", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "Course") + .WithMany() + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExamPlan", "ExamPlan") + .WithMany("Rooms") + .HasForeignKey("ExamPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("Course"); + + b.Navigation("ExamPlan"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomInvigilator", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExamRoomAssignment", "ExamRoom") + .WithMany("Invigilators") + .HasForeignKey("ExamRoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Teacher", "Teacher") + .WithMany() + .HasForeignKey("TeacherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ExamRoom"); + + b.Navigation("Teacher"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomSession", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExamRoomAssignment", "ExamRoom") + .WithMany("SessionLinks") + .HasForeignKey("ExamRoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExamSession", "ExamSession") + .WithMany("RoomLinks") + .HasForeignKey("ExamSessionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ExamRoom"); + + b.Navigation("ExamSession"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSeatAssignment", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExamRoomAssignment", "ExamRoom") + .WithMany("Seats") + .HasForeignKey("ExamRoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExamSession", "ExamSession") + .WithMany("SeatAssignments") + .HasForeignKey("ExamSessionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ExamRoom"); + + b.Navigation("ExamSession"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSession", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExamPlan", "ExamPlan") + .WithMany("Sessions") + .HasForeignKey("ExamPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Building", "RequiredBuilding") + .WithMany() + .HasForeignKey("RequiredBuildingId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("ExamPlan"); + + b.Navigation("RequiredBuilding"); + + 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.ExperimentBooking", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentProject", "ExperimentProject") + .WithMany("Bookings") + .HasForeignKey("ExperimentProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentSession", "ExperimentSession") + .WithMany("Bookings") + .HasForeignKey("ExperimentSessionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ExperimentProject"); + + b.Navigation("ExperimentSession"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItem", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", "ExperimentGradeSheet") + .WithMany("Items") + .HasForeignKey("ExperimentGradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExperimentGradeSheet"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItemScore", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeItem", "ExperimentGradeItem") + .WithMany("Scores") + .HasForeignKey("ExperimentGradeItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", "ExperimentGradeRecord") + .WithMany("ItemScores") + .HasForeignKey("ExperimentGradeRecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExperimentGradeItem"); + + b.Navigation("ExperimentGradeRecord"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", "ExperimentGradeSheet") + .WithMany("Records") + .HasForeignKey("ExperimentGradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentSession", "ExperimentSession") + .WithMany() + .HasForeignKey("ExperimentSessionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ExperimentGradeSheet"); + + b.Navigation("ExperimentSession"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentProject", "ExperimentProject") + .WithOne("GradeSheet") + .HasForeignKey("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", "ExperimentProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExperimentProject"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ScheduleEntry", "ScheduleEntry") + .WithMany() + .HasForeignKey("ScheduleEntryId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ScheduleEntry"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentSession", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentProject", "ExperimentProject") + .WithMany("Sessions") + .HasForeignKey("ExperimentProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("ExperimentProject"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItem", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GradeSheet", "GradeSheet") + .WithMany("Items") + .HasForeignKey("GradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GradeSheet"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItemScore", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GradeItem", "GradeItem") + .WithMany("Scores") + .HasForeignKey("GradeItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.GradeRecord", "GradeRecord") + .WithMany("ItemScores") + .HasForeignKey("GradeRecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GradeItem"); + + b.Navigation("GradeRecord"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeModification", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GradeRecord", "GradeRecord") + .WithMany() + .HasForeignKey("GradeRecordId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GradeRecord"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeRecord", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GradeSheet", "GradeSheet") + .WithMany("Records") + .HasForeignKey("GradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GradeSheet"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeSheet", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationAuditResult", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.CurriculumPlan", "CurriculumPlan") + .WithMany() + .HasForeignKey("CurriculumPlanId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.GraduationAuditBatch", "GraduationAuditBatch") + .WithMany("Results") + .HasForeignKey("GraduationAuditBatchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CurriculumPlan"); + + b.Navigation("GraduationAuditBatch"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceItem", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GraduationClearanceBatch", "GraduationClearanceBatch") + .WithMany("Items") + .HasForeignKey("GraduationClearanceBatchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GraduationClearanceBatch"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceRecord", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GraduationClearanceItem", "GraduationClearanceItem") + .WithMany("Records") + .HasForeignKey("GraduationClearanceItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GraduationClearanceItem"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Major", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.College", "College") + .WithMany() + .HasForeignKey("CollegeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("College"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamAutoJob", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.MakeupExamPlan", "MakeupExamPlan") + .WithMany() + .HasForeignKey("MakeupExamPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MakeupExamPlan"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamEnrollment", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.MakeupExamSession", "MakeupExamSession") + .WithMany("Enrollments") + .HasForeignKey("MakeupExamSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.DeferredExam", "SourceDeferredExam") + .WithMany() + .HasForeignKey("SourceDeferredExamId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.GradeRecord", "SourceGradeRecord") + .WithMany() + .HasForeignKey("SourceGradeRecordId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("MakeupExamSession"); + + b.Navigation("SourceDeferredExam"); + + b.Navigation("SourceGradeRecord"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamPlan", 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.MakeupExamSession", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.MakeupExamPlan", "MakeupExamPlan") + .WithMany("Sessions") + .HasForeignKey("MakeupExamPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Building", "RequiredBuilding") + .WithMany() + .HasForeignKey("RequiredBuildingId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("MakeupExamPlan"); + + b.Navigation("RequiredBuilding"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamSessionInvigilator", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.MakeupExamSession", "MakeupExamSession") + .WithMany("Invigilators") + .HasForeignKey("MakeupExamSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Teacher", "Teacher") + .WithMany() + .HasForeignKey("TeacherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("MakeupExamSession"); + + b.Navigation("Teacher"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Notification", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.MessageDispatch", "MessageDispatch") + .WithMany("Notifications") + .HasForeignKey("MessageDispatchId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("MessageDispatch"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocument", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "InvalidatedByUser") + .WithMany() + .HasForeignKey("InvalidatedByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "IssuedByUser") + .WithMany() + .HasForeignKey("IssuedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.OfficialDocument", "ReissuedFromDocument") + .WithMany() + .HasForeignKey("ReissuedFromDocumentId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("InvalidatedByUser"); + + b.Navigation("IssuedByUser"); + + b.Navigation("ReissuedFromDocument"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocumentDownload", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "DownloadedByUser") + .WithMany() + .HasForeignKey("DownloadedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.OfficialDocument", "OfficialDocument") + .WithMany("Downloads") + .HasForeignKey("OfficialDocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DownloadedByUser"); + + b.Navigation("OfficialDocument"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OtherExamResult", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.OtherExamBatch", "OtherExamBatch") + .WithMany("Results") + .HasForeignKey("OtherExamBatchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("OtherExamBatch"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.PublishedScheduleOccurrence", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.ScheduleEntry", "ScheduleEntry") + .WithMany() + .HasForeignKey("ScheduleEntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("ScheduleEntry"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ScheduleEntry", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.SchedulePlan", "SchedulePlan") + .WithMany("Entries") + .HasForeignKey("SchedulePlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("SchedulePlan"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.SchedulePlan", 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.SchedulePublishJob", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("RequestedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.SchedulePlan", "SchedulePlan") + .WithMany() + .HasForeignKey("SchedulePlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SchedulePlan"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ScheduleTimeSlot", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AcademicTerm"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Student", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AdministrativeClass", "AdministrativeClass") + .WithMany("Students") + .HasForeignKey("AdministrativeClassId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AdministrativeClass"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.StudentStatusChange", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Teacher", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.College", "College") + .WithMany() + .HasForeignKey("CollegeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("College"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeacherCourseApplication", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "Course") + .WithMany() + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("ReviewedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.Teacher", "Teacher") + .WithMany() + .HasForeignKey("TeacherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AcademicTerm"); + + b.Navigation("Course"); + + b.Navigation("Teacher"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTask", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "Course") + .WithMany() + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AcademicTerm"); + + b.Navigation("Course"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskAllowedClassroom", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", "TeachingTaskScheduleConstraint") + .WithMany("AllowedClassrooms") + .HasForeignKey("TeachingTaskScheduleConstraintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("TeachingTaskScheduleConstraint"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskAllowedExperimentClassroom", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", "TeachingTaskScheduleConstraint") + .WithMany("AllowedExperimentClassrooms") + .HasForeignKey("TeachingTaskScheduleConstraintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("TeachingTaskScheduleConstraint"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskClass", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AdministrativeClass", "AdministrativeClass") + .WithMany() + .HasForeignKey("AdministrativeClassId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany("Classes") + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AdministrativeClass"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskGradeScoreBand", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTaskGradeStatistic", "TeachingTaskGradeStatistic") + .WithMany("ScoreBands") + .HasForeignKey("TeachingTaskGradeStatisticId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TeachingTaskGradeStatistic"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskGradeStatistic", 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(); + + b.HasOne("Jiaowu.Api.Domain.Academic.GradeSheet", "GradeSheet") + .WithOne() + .HasForeignKey("Jiaowu.Api.Domain.Academic.TeachingTaskGradeStatistic", "GradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithOne() + .HasForeignKey("Jiaowu.Api.Domain.Academic.TeachingTaskGradeStatistic", "TeachingTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GradeSheet"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Building", "ExperimentRequiredBuilding") + .WithMany() + .HasForeignKey("ExperimentRequiredBuildingId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.Campus", "ExperimentRequiredCampus") + .WithMany() + .HasForeignKey("ExperimentRequiredCampusId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.Building", "RequiredBuilding") + .WithMany() + .HasForeignKey("RequiredBuildingId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.Campus", "RequiredCampus") + .WithMany() + .HasForeignKey("RequiredCampusId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExperimentRequiredBuilding"); + + b.Navigation("ExperimentRequiredCampus"); + + b.Navigation("RequiredBuilding"); + + b.Navigation("RequiredCampus"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskTeacher", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Teacher", "Teacher") + .WithMany() + .HasForeignKey("TeacherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany("Teachers") + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Teacher"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.WarningRecord", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.WarningRule", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AcademicTerm"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Identity.RefreshSession", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AdministrativeClass", b => + { + b.Navigation("Students"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AttendanceSheet", b => + { + b.Navigation("CheckInAttempts"); + + b.Navigation("Records"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Course", b => + { + b.Navigation("Prerequisites"); + + b.Navigation("RequiredByCourses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGroup", b => + { + b.Navigation("Courses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionOffering", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionRound", b => + { + b.Navigation("EligibleGrades"); + + b.Navigation("Offerings"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumModule", b => + { + b.Navigation("Courses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumPlan", b => + { + b.Navigation("Modules"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.DegreeAwardBatch", b => + { + b.Navigation("Results"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationDimension", b => + { + b.Navigation("Scores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationRecord", b => + { + b.Navigation("Scores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationSetup", b => + { + b.Navigation("Dimensions"); + + b.Navigation("Records"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamPlan", b => + { + b.Navigation("Rooms"); + + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomAssignment", b => + { + b.Navigation("Invigilators"); + + b.Navigation("Seats"); + + b.Navigation("SessionLinks"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSession", b => + { + b.Navigation("Invigilators"); + + b.Navigation("RoomLinks"); + + b.Navigation("SeatAssignments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItem", b => + { + b.Navigation("Scores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", b => + { + b.Navigation("ItemScores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", b => + { + b.Navigation("Items"); + + b.Navigation("Records"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b => + { + b.Navigation("Bookings"); + + b.Navigation("GradeSheet"); + + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentSession", b => + { + b.Navigation("Bookings"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItem", b => + { + b.Navigation("Scores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeRecord", b => + { + b.Navigation("ItemScores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeSheet", b => + { + b.Navigation("Items"); + + b.Navigation("Records"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationAuditBatch", b => + { + b.Navigation("Results"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceBatch", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceItem", b => + { + b.Navigation("Records"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamPlan", b => + { + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamSession", b => + { + b.Navigation("Enrollments"); + + b.Navigation("Invigilators"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MessageDispatch", b => + { + b.Navigation("Notifications"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocument", b => + { + b.Navigation("Downloads"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OtherExamBatch", b => + { + b.Navigation("Results"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.SchedulePlan", b => + { + b.Navigation("Entries"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTask", b => + { + b.Navigation("Classes"); + + b.Navigation("Teachers"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskGradeStatistic", b => + { + b.Navigation("ScoreBands"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", b => + { + b.Navigation("AllowedClassrooms"); + + b.Navigation("AllowedExperimentClassrooms"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260809123700_CourseGradeStatisticsRefreshSettings.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260809123700_CourseGradeStatisticsRefreshSettings.cs new file mode 100644 index 0000000..251bdb8 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260809123700_CourseGradeStatisticsRefreshSettings.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql +{ + /// + public partial class CourseGradeStatisticsRefreshSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CourseGradeStatisticsRefreshSettings", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + Key = table.Column(type: "varchar(50)", maxLength: 50, nullable: false), + IsEnabled = table.Column(type: "tinyint(1)", nullable: false), + IntervalSeconds = table.Column(type: "int", nullable: false), + BatchSize = table.Column(type: "int", nullable: false), + LastRunAt = table.Column(type: "datetime(6)", nullable: true), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CourseGradeStatisticsRefreshSettings", x => x.Id); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_CourseGradeStatisticsRefreshSettings_Key", + table: "CourseGradeStatisticsRefreshSettings", + column: "Key", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CourseGradeStatisticsRefreshSettings"); + + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs index 213b7e0..7f8a7e4 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs @@ -1098,6 +1098,43 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.ToTable("CourseGradeStatisticsRefreshJobs"); }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGradeStatisticsRefreshSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BatchSize") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IntervalSeconds") + .HasColumnType("int"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LastRunAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("CourseGradeStatisticsRefreshSettings"); + }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGroup", b => { b.Property("Id") diff --git a/src/Jiaowu.Api/Program.cs b/src/Jiaowu.Api/Program.cs index ae0f4c5..3d49118 100644 --- a/src/Jiaowu.Api/Program.cs +++ b/src/Jiaowu.Api/Program.cs @@ -438,6 +438,9 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(TimeProvider.System); +builder.Services.AddHostedService(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/tests/Jiaowu.Api.Tests/CourseGradeStatisticsCronSchedulerTests.cs b/tests/Jiaowu.Api.Tests/CourseGradeStatisticsCronSchedulerTests.cs new file mode 100644 index 0000000..a63e6ee --- /dev/null +++ b/tests/Jiaowu.Api.Tests/CourseGradeStatisticsCronSchedulerTests.cs @@ -0,0 +1,176 @@ +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Domain.System; +using Jiaowu.Api.Infrastructure.Grades; +using Jiaowu.Api.Infrastructure.Persistence; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Jiaowu.Api.Tests; + +public sealed class CourseGradeStatisticsRefreshSchedulerTests +{ + [Fact] + public async Task EnqueueDueAsync_UsesPersistentScheduleAndHonorsInterval() + { + await using var connection = new SqliteConnection("Data Source=:memory:"); + await connection.OpenAsync(); + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + await using var db = new AppDbContext(options); + await db.Database.EnsureCreatedAsync(); + var scheduler = new CourseGradeStatisticsRefreshScheduler( + db, + NullLogger.Instance); + var now = new DateTime(2026, 8, 9, 12, 0, 0, DateTimeKind.Utc); + + await scheduler.EnqueueDueAsync(now, CancellationToken.None); + var setting = Assert.Single(await db.CourseGradeStatisticsRefreshSettings.ToListAsync()); + Assert.True(setting.IsEnabled); + Assert.Equal(now, setting.LastRunAt); + + await scheduler.EnqueueDueAsync(now.AddMinutes(1), CancellationToken.None); + Assert.Equal(now, setting.LastRunAt); + } + + [Fact] + public async Task EnqueueStaleAsync_GroupsByCourseTerm_AndSkipsActiveTarget() + { + await using var connection = new SqliteConnection("Data Source=:memory:"); + await connection.OpenAsync(); + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + await using var db = new AppDbContext(options); + await db.Database.EnsureCreatedAsync(); + + var college = new College { Code = "SCHEDULE", Name = "定时任务学院" }; + var major = new Major + { + Code = "SCHEDULE-M", + Name = "定时任务专业", + College = college, + DegreeType = "本科" + }; + var administrativeClass = new AdministrativeClass + { + Code = "SCHEDULE-C", + Name = "定时任务一班", + Major = major, + Grade = 2026 + }; + var student = new Student + { + StudentNumber = "SCHEDULE-001", + Name = "定时任务学生", + AdministrativeClass = administrativeClass, + EnrollmentYear = 2026, + EnrollmentDate = new DateOnly(2026, 9, 1) + }; + var course = new Course + { + Code = "SCHEDULE-COURSE", + Name = "定时任务课程", + College = college, + Credits = 2, + TotalHours = 32, + LectureHours = 32 + }; + var term = new AcademicTerm + { + Code = "2026-A", + Name = "2026-2027 学年第一学期", + AcademicYear = "2026-2027", + Season = TermSeason.Autumn, + StartDate = new DateOnly(2026, 9, 1), + EndDate = new DateOnly(2027, 1, 15) + }; + var firstTask = new TeachingTask + { + TaskNumber = "SCHEDULE-T1", + Name = "定时任务教学班一", + Course = course, + AcademicTerm = term, + Capacity = 30 + }; + var secondTask = new TeachingTask + { + TaskNumber = "SCHEDULE-T2", + Name = "定时任务教学班二", + Course = course, + AcademicTerm = term, + Capacity = 30 + }; + var firstSheet = PublishedSheet(firstTask, student, 80m); + var secondSheet = PublishedSheet(secondTask, student, 90m); + db.GradeSheets.AddRange(firstSheet, secondSheet); + await db.SaveChangesAsync(); + + var scheduler = new CourseGradeStatisticsRefreshScheduler( + db, + NullLogger.Instance); + + var queued = await scheduler.EnqueueStaleAsync(100, CancellationToken.None); + var queuedAgain = await scheduler.EnqueueStaleAsync(100, CancellationToken.None); + + Assert.Equal(1, queued); + Assert.Equal(0, queuedAgain); + var job = Assert.Single(await db.CourseGradeStatisticsRefreshJobs.ToListAsync()); + var outbox = Assert.Single(await db.BackgroundJobOutboxMessages.ToListAsync()); + Assert.Equal(BackgroundJobKind.CourseGradeStatisticsRefresh, outbox.JobKind); + + job.Status = CourseGradeStatisticsRefreshJobStatus.Succeeded; + db.TeachingTaskGradeStatistics.AddRange( + FreshStatistic(firstSheet, course, term), + FreshStatistic(secondSheet, course, term)); + await db.SaveChangesAsync(); + + var queuedAfterFreshStatistics = await scheduler.EnqueueStaleAsync( + 100, + CancellationToken.None); + + Assert.Equal(0, queuedAfterFreshStatistics); + Assert.Single(await db.CourseGradeStatisticsRefreshJobs.ToListAsync()); + } + + private static GradeSheet PublishedSheet( + TeachingTask task, + Student student, + decimal score) + { + var sheet = new GradeSheet + { + TeachingTask = task, + Status = GradeSheetStatus.Published, + PublishedAt = DateTime.UtcNow + }; + sheet.Records.Add(new GradeRecord + { + GradeSheet = sheet, + Student = student, + TotalScore = score, + GradePoint = 3m + }); + return sheet; + } + + private static TeachingTaskGradeStatistic FreshStatistic( + GradeSheet sheet, + Course course, + AcademicTerm term) => new() + { + GradeSheetId = sheet.Id, + TeachingTaskId = sheet.TeachingTaskId, + CourseId = course.Id, + AcademicTermId = term.Id, + StudentCount = 1, + PassedCount = 1, + HighestScore = 80m, + AverageScore = 80m, + MedianScore = 80m, + LowestScore = 80m, + PassRate = 100m, + CalculatedAt = DateTime.UtcNow.AddMinutes(1) + }; +} diff --git a/tests/Jiaowu.Api.Tests/WarningRulePersistenceTests.cs b/tests/Jiaowu.Api.Tests/WarningRulePersistenceTests.cs new file mode 100644 index 0000000..82f01fc --- /dev/null +++ b/tests/Jiaowu.Api.Tests/WarningRulePersistenceTests.cs @@ -0,0 +1,78 @@ +using System.Collections; +using Jiaowu.Api.Controllers; +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Infrastructure.Auth; +using Jiaowu.Api.Infrastructure.Persistence; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace Jiaowu.Api.Tests; + +public sealed class WarningRulePersistenceTests +{ + [Fact] + public async Task SaveRules_PersistsAutoCheck_AndReturnsNumericType() + { + await using var connection = new SqliteConnection("Data Source=:memory:"); + await connection.OpenAsync(); + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + await using var db = new AppDbContext(options); + await db.Database.EnsureCreatedAsync(); + var term = new AcademicTerm + { + Code = "WARN-TERM", + Name = "预警测试学期", + AcademicYear = "2026-2027", + Season = TermSeason.Autumn, + StartDate = new DateOnly(2026, 9, 1), + EndDate = new DateOnly(2027, 1, 15) + }; + db.AcademicTerms.Add(term); + await db.SaveChangesAsync(); + var controller = new WarningsController(db, new AllDataScope()); + + var saveResult = await controller.SaveRules(term.Id, + [ + new WarningRuleDto( + WarningType.FailedCredits, + "不及格学分", + 2m, + true, + true, + true, + null, + true, + 1, + 9, + 30) + ], CancellationToken.None); + + Assert.IsType(saveResult); + db.ChangeTracker.Clear(); + var persisted = Assert.Single(await db.WarningRules.AsNoTracking().ToListAsync()); + Assert.True(persisted.AutoCheckEnabled); + Assert.Equal(1, persisted.CheckDayOfWeek); + Assert.Equal(9, persisted.CheckHour); + Assert.Equal(30, persisted.CheckMinute); + + var getResult = await controller.GetRules(term.Id, CancellationToken.None); + var ok = Assert.IsType(getResult); + var row = Assert.Single(Assert.IsAssignableFrom(ok.Value).Cast()); + var type = row.GetType().GetProperty("Type")?.GetValue(row); + Assert.Equal(1, Assert.IsType(type)); + } + + private sealed class AllDataScope : ICurrentUserDataScope + { + public CurrentUserScope Current { get; } = new( + Guid.NewGuid(), + "测试管理员", + null, + DataScope.All, + new HashSet([SystemRoles.SuperAdmin])); + } +} diff --git a/web/src/views/GradeAnalyticsView.vue b/web/src/views/GradeAnalyticsView.vue index 34b65f9..b30e6d2 100644 --- a/web/src/views/GradeAnalyticsView.vue +++ b/web/src/views/GradeAnalyticsView.vue @@ -1,10 +1,11 @@