From 924574256af7d8b156d91c84b7f7c8aa80d7f384 Mon Sep 17 00:00:00 2001 From: biss Date: Sun, 9 Aug 2026 10:13:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=88=90=E7=BB=A9=E7=BB=9F=E8=AE=A1=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/ApprovalsController.cs | 8 + .../Controllers/GradesController.cs | 136 +- .../Domain/Academic/GradeEntities.cs | 51 + .../System/BackgroundJobOutboxMessage.cs | 3 +- .../BackgroundJobs/BackgroundJobOptions.cs | 3 + .../BackgroundJobOutboxPublisher.cs | 11 + .../BackgroundJobs/BackgroundJobRunner.cs | 19 + .../BackgroundJobs/RabbitMqBackgroundJobs.cs | 4 +- .../Infrastructure/Caching/AppCache.cs | 4 + .../Grades/CourseGradeStatisticsService.cs | 146 + .../Persistence/AppDbContext.cs | 30 + .../Persistence/DevelopmentSqliteMigrator.cs | 31 + ...08133529_CourseGradeStatistics.Designer.cs | 6381 +++++++++++++++++ .../20260808133529_CourseGradeStatistics.cs | 113 + .../MySql/AppDbContextModelSnapshot.cs | 136 + src/Jiaowu.Api/Program.cs | 3 + src/Jiaowu.Api/appsettings.json | 1 + web/src/router/index.ts | 8 + web/src/views/CourseGradeStatisticsView.vue | 271 + web/src/views/GradesView.vue | 24 +- 20 files changed, 7379 insertions(+), 4 deletions(-) create mode 100644 src/Jiaowu.Api/Infrastructure/Grades/CourseGradeStatisticsService.cs create mode 100644 src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260808133529_CourseGradeStatistics.Designer.cs create mode 100644 src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260808133529_CourseGradeStatistics.cs create mode 100644 web/src/views/CourseGradeStatisticsView.vue diff --git a/src/Jiaowu.Api/Controllers/ApprovalsController.cs b/src/Jiaowu.Api/Controllers/ApprovalsController.cs index d263558..cae910f 100644 --- a/src/Jiaowu.Api/Controllers/ApprovalsController.cs +++ b/src/Jiaowu.Api/Controllers/ApprovalsController.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Domain.System; using Jiaowu.Api.Infrastructure.Auth; using Jiaowu.Api.Infrastructure.Grades; using Jiaowu.Api.Infrastructure.Persistence; @@ -250,6 +251,13 @@ 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/GradesController.cs b/src/Jiaowu.Api/Controllers/GradesController.cs index 3fa7c35..dc79f69 100644 --- a/src/Jiaowu.Api/Controllers/GradesController.cs +++ b/src/Jiaowu.Api/Controllers/GradesController.cs @@ -3,7 +3,9 @@ using System.Globalization; using Jiaowu.Api.Contracts; using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Domain.System; using Jiaowu.Api.Infrastructure.Auth; +using Jiaowu.Api.Infrastructure.Caching; using Jiaowu.Api.Infrastructure.Excel; using Jiaowu.Api.Infrastructure.Grades; using Jiaowu.Api.Infrastructure.Persistence; @@ -19,7 +21,8 @@ namespace Jiaowu.Api.Controllers; [Route("api/grades")] public sealed class GradesController( AppDbContext db, - ICurrentUserDataScope currentUserDataScope) : ControllerBase + ICurrentUserDataScope currentUserDataScope, + IAppCache? cache = null) : ControllerBase { private const string SheetUsers = SystemRoles.SuperAdmin + "," + @@ -37,6 +40,9 @@ public sealed class GradesController( SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin; + private const string StatisticsUsers = + SheetUsers + "," + SystemRoles.Leader + "," + SystemRoles.Student; + [HttpGet("sheets")] [Authorize(Roles = SheetUsers)] public async Task GetSheets( @@ -502,6 +508,7 @@ public sealed class GradesController( } targetItem.SourceType = GradeItemSourceType.ExperimentSummary; targetItem.SourceSnapshotAt = DateTime.UtcNow; + QueueCourseStatisticsRefresh(sheet.Id); await db.SaveChangesAsync(cancellationToken); return Ok(new { @@ -661,6 +668,7 @@ 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. @@ -910,6 +918,7 @@ 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 }); } @@ -948,6 +957,7 @@ public sealed class GradesController( .Select(x => new { x.Id, + GradeSheetId = x.GradeSheetId, AcademicTermId = x.GradeSheet!.TeachingTask!.AcademicTermId, TermName = x.GradeSheet.TeachingTask.AcademicTerm!.Name, x.GradeSheet.TeachingTaskId, @@ -964,6 +974,121 @@ public sealed class GradesController( return Ok(new { Student = student, Records = records }); } + [HttpGet("sheets/{id:guid}/statistics")] + [Authorize(Roles = StatisticsUsers)] + public async Task GetCourseStatistics( + Guid id, + CancellationToken cancellationToken) + { + var scope = currentUserDataScope.Current; + var sheet = await db.GradeSheets.AsNoTracking() + .Where(x => x.Id == id) + .Select(x => new + { + x.Id, + x.Status, + x.TeachingTask!.CourseId, + x.TeachingTask.AcademicTermId, + CourseName = x.TeachingTask.Course!.Name, + CourseCode = x.TeachingTask.Course.Code, + TermName = x.TeachingTask.AcademicTerm!.Name + }) + .FirstOrDefaultAsync(cancellationToken); + if (sheet is null) return NotFound(); + + Guid? classId = null; + Guid? majorId = null; + Guid? collegeId = null; + if (scope.IsInRole(SystemRoles.Student)) + { + if (sheet.Status != GradeSheetStatus.Published) + return NotFound(); + var student = await db.GradeRecords.AsNoTracking() + .Where(x => x.GradeSheetId == id && x.Student!.UserId == scope.UserId) + .Select(x => new + { + x.Student!.AdministrativeClassId, + MajorId = x.Student.AdministrativeClass!.MajorId, + CollegeId = x.Student.AdministrativeClass.Major!.CollegeId + }) + .FirstOrDefaultAsync(cancellationToken); + if (student is null) return Forbid(); + classId = student.AdministrativeClassId; + majorId = student.MajorId; + collegeId = student.CollegeId; + } + else if (scope.Scope == DataScope.College) + { + collegeId = scope.RestrictedCollegeId; + if (collegeId == Guid.Empty || !scope.CanAccessCollege( + await db.Courses.Where(x => x.Id == sheet.CourseId) + .Select(x => x.CollegeId).FirstAsync(cancellationToken))) + return Forbid(); + } + else if (scope.IsInRole(SystemRoles.Counselor)) + { + var allowedClassIds = await db.AdministrativeClasses.AsNoTracking() + .Where(x => x.CounselorUserId == scope.UserId) + .Select(x => x.Id) + .ToListAsync(cancellationToken); + if (allowedClassIds.Count == 0) return Forbid(); + // A counselor sees their classes plus the matching major/college + // benchmarks, never an unrelated class-level statistic. + classId = allowedClassIds.First(); + majorId = await db.AdministrativeClasses.Where(x => x.Id == classId) + .Select(x => x.MajorId).FirstAsync(cancellationToken); + collegeId = await db.Majors.Where(x => x.Id == majorId) + .Select(x => x.CollegeId).FirstAsync(cancellationToken); + } + + var cacheKey = AppCacheKeys.CourseGradeStatistics(id); + var statistics = await (cache ?? NoOpAppCache.Instance).GetOrCreateAsync(cacheKey, async token => + { + var source = db.CourseGradeStatistics.AsNoTracking() + .Where(x => x.CourseId == sheet.CourseId && + x.AcademicTermId == sheet.AcademicTermId); + return await source.Select(x => new + { + x.Scope, x.ScopeEntityId, x.StudentCount, x.PassedCount, + x.Below60Count, x.From60To69Count, x.From70To79Count, + x.From80To89Count, x.From90To100Count, + x.HighestScore, x.AverageScore, x.LowestScore, x.PassRate, + x.CalculatedAt + }).ToListAsync(token); + }, AppCacheProfile.Analytics, + [AppCacheTags.CourseGradeStatistics], cancellationToken); + + object? Find(CourseGradeStatisticScope statisticScope, Guid? entityId) + { + var item = statistics.FirstOrDefault(x => x.Scope == statisticScope && + x.ScopeEntityId == entityId); + return item is null ? null : new + { + item.Scope, item.ScopeEntityId, item.StudentCount, item.PassedCount, + item.HighestScore, item.AverageScore, item.LowestScore, item.PassRate, + item.CalculatedAt, + Distribution = new[] + { + new { Range = "0–59", Count = item.Below60Count }, + new { Range = "60–69", Count = item.From60To69Count }, + new { Range = "70–79", Count = item.From70To79Count }, + new { Range = "80–89", Count = item.From80To89Count }, + new { Range = "90–100", Count = item.From90To100Count } + } + }; + } + return Ok(new + { + sheet.CourseName, sheet.CourseCode, sheet.TermName, + IsRefreshing = !statistics.Any(), + Class = classId.HasValue ? Find(CourseGradeStatisticScope.AdministrativeClass, classId) : null, + Major = majorId.HasValue ? Find(CourseGradeStatisticScope.Major, majorId) : null, + College = collegeId.HasValue ? Find(CourseGradeStatisticScope.College, collegeId) : null, + University = scope.Scope == DataScope.All || scope.IsInRole(SystemRoles.Student) + ? Find(CourseGradeStatisticScope.University, null) : null + }); + } + private IQueryable AccessibleTasks() { var source = db.TeachingTasks.AsQueryable(); @@ -1036,6 +1161,7 @@ public sealed class GradesController( { try { + QueueCourseStatisticsRefresh(id); await db.SaveChangesAsync(cancellationToken); return created ? Created(string.Empty, new { id }) : NoContent(); } @@ -1045,6 +1171,14 @@ 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/Domain/Academic/GradeEntities.cs b/src/Jiaowu.Api/Domain/Academic/GradeEntities.cs index 1429d2a..5175e43 100644 --- a/src/Jiaowu.Api/Domain/Academic/GradeEntities.cs +++ b/src/Jiaowu.Api/Domain/Academic/GradeEntities.cs @@ -54,6 +54,57 @@ public sealed class GradeItemScore public decimal? Score { get; set; } } +/// +/// Persisted course-result aggregate. One course/term is materialized at each +/// organizational level so the result-analysis page never aggregates raw +/// grade records on request. +/// +public sealed class CourseGradeStatistic : EntityBase +{ + public Guid CourseId { get; set; } + public Guid AcademicTermId { get; set; } + public CourseGradeStatisticScope Scope { get; set; } + public Guid? ScopeEntityId { get; set; } + public int StudentCount { get; set; } + public int PassedCount { get; set; } + public int Below60Count { get; set; } + public int From60To69Count { get; set; } + public int From70To79Count { get; set; } + public int From80To89Count { get; set; } + public int From90To100Count { get; set; } + public decimal HighestScore { get; set; } + public decimal AverageScore { get; set; } + public decimal LowestScore { get; set; } + public decimal PassRate { get; set; } + public DateTime CalculatedAt { get; set; } +} + +public enum CourseGradeStatisticScope +{ + AdministrativeClass = 1, + Major = 2, + College = 3, + University = 4 +} + +public sealed class CourseGradeStatisticsRefreshJob : EntityBase +{ + public Guid GradeSheetId { get; set; } + public CourseGradeStatisticsRefreshJobStatus Status { get; set; } = + CourseGradeStatisticsRefreshJobStatus.Queued; + public DateTime? StartedAt { get; set; } + public DateTime? CompletedAt { get; set; } + public string? ErrorMessage { get; set; } +} + +public enum CourseGradeStatisticsRefreshJobStatus +{ + Queued = 1, + Running = 2, + Succeeded = 3, + Failed = 4 +} + public enum GradeSheetStatus { Draft = 1, diff --git a/src/Jiaowu.Api/Domain/System/BackgroundJobOutboxMessage.cs b/src/Jiaowu.Api/Domain/System/BackgroundJobOutboxMessage.cs index 7ff2079..ce04eed 100644 --- a/src/Jiaowu.Api/Domain/System/BackgroundJobOutboxMessage.cs +++ b/src/Jiaowu.Api/Domain/System/BackgroundJobOutboxMessage.cs @@ -33,7 +33,8 @@ public enum BackgroundJobKind MakeupExamAuto = 3, ExamArrangement = 4, ExamSignInExport = 5, - ExamPublish = 6 + ExamPublish = 6, + CourseGradeStatisticsRefresh = 7 } public enum BackgroundJobOutboxState diff --git a/src/Jiaowu.Api/Infrastructure/BackgroundJobs/BackgroundJobOptions.cs b/src/Jiaowu.Api/Infrastructure/BackgroundJobs/BackgroundJobOptions.cs index a07105a..9ee657a 100644 --- a/src/Jiaowu.Api/Infrastructure/BackgroundJobs/BackgroundJobOptions.cs +++ b/src/Jiaowu.Api/Infrastructure/BackgroundJobs/BackgroundJobOptions.cs @@ -16,6 +16,7 @@ public sealed class BackgroundJobOptions public int ExamArrangementConcurrency { get; set; } = 1; public int ExamSignInExportConcurrency { get; set; } = 1; public int ExamPublishConcurrency { get; set; } = 1; + public int CourseGradeStatisticsRefreshConcurrency { get; set; } = 1; public string Exchange { get; set; } = "jiaowu.background-jobs"; public string QueuePrefix { get; set; } = "jiaowu.background-jobs"; public bool UseQuorumQueues { get; set; } = true; @@ -35,6 +36,8 @@ public sealed class BackgroundJobOptions BackgroundJobKind.ExamArrangement => ExamArrangementConcurrency, BackgroundJobKind.ExamSignInExport => ExamSignInExportConcurrency, BackgroundJobKind.ExamPublish => ExamPublishConcurrency, + BackgroundJobKind.CourseGradeStatisticsRefresh => + CourseGradeStatisticsRefreshConcurrency, _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null) }; } diff --git a/src/Jiaowu.Api/Infrastructure/BackgroundJobs/BackgroundJobOutboxPublisher.cs b/src/Jiaowu.Api/Infrastructure/BackgroundJobs/BackgroundJobOutboxPublisher.cs index 84ca8fc..9734e91 100644 --- a/src/Jiaowu.Api/Infrastructure/BackgroundJobs/BackgroundJobOutboxPublisher.cs +++ b/src/Jiaowu.Api/Infrastructure/BackgroundJobs/BackgroundJobOutboxPublisher.cs @@ -130,6 +130,15 @@ public sealed class BackgroundJobOutboxPublisher( message.JobId == x.Id)) .Select(x => x.Id) .ToListAsync(cancellationToken); + var gradeStatisticsJobs = await db.CourseGradeStatisticsRefreshJobs.AsNoTracking() + .Where(x => + (x.Status == CourseGradeStatisticsRefreshJobStatus.Queued || + x.Status == CourseGradeStatisticsRefreshJobStatus.Running) && + !db.BackgroundJobOutboxMessages.Any(message => + message.JobKind == BackgroundJobKind.CourseGradeStatisticsRefresh && + message.JobId == x.Id)) + .Select(x => x.Id) + .ToListAsync(cancellationToken); var missingKeys = automaticJobs .Select(id => (BackgroundJobKind.AutomaticSchedule, id)) @@ -143,6 +152,8 @@ public sealed class BackgroundJobOutboxPublisher( (BackgroundJobKind.ExamSignInExport, id))) .Concat(publishJobs2.Select(id => (BackgroundJobKind.ExamPublish, id))) + .Concat(gradeStatisticsJobs.Select(id => + (BackgroundJobKind.CourseGradeStatisticsRefresh, id))) .ToList(); foreach (var (kind, jobId) in missingKeys) { diff --git a/src/Jiaowu.Api/Infrastructure/BackgroundJobs/BackgroundJobRunner.cs b/src/Jiaowu.Api/Infrastructure/BackgroundJobs/BackgroundJobRunner.cs index 1f52b96..e144ed0 100644 --- a/src/Jiaowu.Api/Infrastructure/BackgroundJobs/BackgroundJobRunner.cs +++ b/src/Jiaowu.Api/Infrastructure/BackgroundJobs/BackgroundJobRunner.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Domain.System; using Jiaowu.Api.Infrastructure.Exams; +using Jiaowu.Api.Infrastructure.Grades; using Jiaowu.Api.Infrastructure.Persistence; using Jiaowu.Api.Infrastructure.Scheduling; using Microsoft.EntityFrameworkCore; @@ -100,6 +101,11 @@ public sealed class BackgroundJobRunner( .GetRequiredService() .ProcessAsync(message.JobId, cancellationToken); break; + case BackgroundJobKind.CourseGradeStatisticsRefresh: + await scope.ServiceProvider + .GetRequiredService() + .ProcessAsync(message.JobId, cancellationToken); + break; default: throw new InvalidOperationException( $"Unsupported background job kind '{message.JobKind}'."); @@ -308,6 +314,19 @@ public sealed class BackgroundJobRunner( .SetProperty(x => x.CompletedAt, completedAt), cancellationToken); break; + case BackgroundJobKind.CourseGradeStatisticsRefresh: + await db.CourseGradeStatisticsRefreshJobs + .Where(x => x.Id == message.JobId && + x.Status != CourseGradeStatisticsRefreshJobStatus.Succeeded && + x.Status != CourseGradeStatisticsRefreshJobStatus.Failed) + .ExecuteUpdateAsync( + setters => setters + .SetProperty(x => x.Status, + CourseGradeStatisticsRefreshJobStatus.Failed) + .SetProperty(x => x.ErrorMessage, error) + .SetProperty(x => x.CompletedAt, completedAt), + cancellationToken); + break; default: throw new ArgumentOutOfRangeException( nameof(message.JobKind), diff --git a/src/Jiaowu.Api/Infrastructure/BackgroundJobs/RabbitMqBackgroundJobs.cs b/src/Jiaowu.Api/Infrastructure/BackgroundJobs/RabbitMqBackgroundJobs.cs index c2a0754..5487fd5 100644 --- a/src/Jiaowu.Api/Infrastructure/BackgroundJobs/RabbitMqBackgroundJobs.cs +++ b/src/Jiaowu.Api/Infrastructure/BackgroundJobs/RabbitMqBackgroundJobs.cs @@ -319,7 +319,8 @@ internal static class RabbitMqBackgroundJobTopology BackgroundJobKind.MakeupExamAuto, BackgroundJobKind.ExamArrangement, BackgroundJobKind.ExamSignInExport, - BackgroundJobKind.ExamPublish + BackgroundJobKind.ExamPublish, + BackgroundJobKind.CourseGradeStatisticsRefresh ]; public static async Task CreateConnectionAsync( @@ -418,6 +419,7 @@ internal static class RabbitMqBackgroundJobTopology BackgroundJobKind.ExamArrangement => "exam.arrangement", BackgroundJobKind.ExamSignInExport => "exam.sign-in-export", BackgroundJobKind.ExamPublish => "exam.publish", + BackgroundJobKind.CourseGradeStatisticsRefresh => "grade-statistics.refresh", _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null) }; diff --git a/src/Jiaowu.Api/Infrastructure/Caching/AppCache.cs b/src/Jiaowu.Api/Infrastructure/Caching/AppCache.cs index 2c2286e..ca01410 100644 --- a/src/Jiaowu.Api/Infrastructure/Caching/AppCache.cs +++ b/src/Jiaowu.Api/Infrastructure/Caching/AppCache.cs @@ -182,12 +182,16 @@ public static class AppCacheKeys return $"statistics:v2:{Normalize(area)}:scope:{Normalize(dataScope)}:" + $"college:{effectiveCollegeId?.ToString("N") ?? "all"}:{filterPart}"; } + + public static string CourseGradeStatistics(Guid gradeSheetId) => + $"grade-statistics:sheet:{gradeSheetId:N}"; } public static class AppCacheTags { public const string BaseData = "base-data"; public const string Analytics = "analytics"; + public const string CourseGradeStatistics = "grade-statistics"; public const string Timetables = "timetables"; public const string TimetableOptions = "timetable:options"; diff --git a/src/Jiaowu.Api/Infrastructure/Grades/CourseGradeStatisticsService.cs b/src/Jiaowu.Api/Infrastructure/Grades/CourseGradeStatisticsService.cs new file mode 100644 index 0000000..230cb7c --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Grades/CourseGradeStatisticsService.cs @@ -0,0 +1,146 @@ +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Infrastructure.Caching; +using Jiaowu.Api.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace Jiaowu.Api.Infrastructure.Grades; + +/// +/// Rebuilds a course/term's denormalized result statistics. The operation is +/// intentionally idempotent: duplicate RabbitMQ deliveries are safe. +/// +public sealed class CourseGradeStatisticsRefreshJobProcessor( + AppDbContext db, + IAppCache cache, + ILogger logger) +{ + public async Task ProcessAsync(Guid jobId, CancellationToken cancellationToken) + { + var job = await db.CourseGradeStatisticsRefreshJobs + .FirstOrDefaultAsync(x => x.Id == jobId, cancellationToken); + if (job is null || job.Status == CourseGradeStatisticsRefreshJobStatus.Succeeded) + return; + + job.Status = CourseGradeStatisticsRefreshJobStatus.Running; + job.StartedAt = DateTime.UtcNow; + job.ErrorMessage = null; + await db.SaveChangesAsync(cancellationToken); + + var sheetData = await db.GradeSheets.AsNoTracking() + .Where(x => x.Id == job.GradeSheetId) + .Select(x => new { x.Id, x.TeachingTask!.CourseId, x.TeachingTask.AcademicTermId }) + .FirstOrDefaultAsync(cancellationToken); + if (sheetData is null) + { + job.Status = CourseGradeStatisticsRefreshJobStatus.Succeeded; + job.CompletedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + return; + } + var target = new StatisticsTarget(sheetData.CourseId, sheetData.AcademicTermId); + + try + { + // Statistics shown to students are based only on formally published + // scores. This prevents an unfinished class from exposing data. + var scores = await db.GradeRecords.AsNoTracking() + .Where(x => x.TotalScore != null && + x.GradeSheet!.Status == GradeSheetStatus.Published && + x.GradeSheet.TeachingTask!.CourseId == target.CourseId && + x.GradeSheet.TeachingTask.AcademicTermId == target.AcademicTermId) + .Select(x => new ScoreRow( + x.TotalScore!.Value, + x.Student!.AdministrativeClassId, + x.Student.AdministrativeClass!.MajorId, + x.Student.AdministrativeClass.Major!.CollegeId)) + .ToListAsync(cancellationToken); + + var now = DateTime.UtcNow; + var rebuilt = new List(); + AddStatistics(CourseGradeStatisticScope.AdministrativeClass, + scores.GroupBy(x => x.ClassId), rebuilt, target, now); + AddStatistics(CourseGradeStatisticScope.Major, + scores.GroupBy(x => x.MajorId), rebuilt, target, now); + AddStatistics(CourseGradeStatisticScope.College, + scores.GroupBy(x => x.CollegeId), rebuilt, target, now); + AddUniversityStatistic(scores, rebuilt, target, now); + + await db.CourseGradeStatistics + .Where(x => x.CourseId == target.CourseId && + x.AcademicTermId == target.AcademicTermId) + .ExecuteDeleteAsync(cancellationToken); + if (rebuilt.Count > 0) + db.CourseGradeStatistics.AddRange(rebuilt); + + job.Status = CourseGradeStatisticsRefreshJobStatus.Succeeded; + job.CompletedAt = now; + await db.SaveChangesAsync(cancellationToken); + await cache.RemoveByTagAsync(AppCacheTags.CourseGradeStatistics, + cancellationToken); + } + catch (Exception exception) + { + job.Status = CourseGradeStatisticsRefreshJobStatus.Failed; + job.ErrorMessage = exception.GetBaseException().Message[..Math.Min(2000, + exception.GetBaseException().Message.Length)]; + await db.SaveChangesAsync(CancellationToken.None); + logger.LogError(exception, "Course grade statistics refresh {JobId} failed.", jobId); + throw; + } + } + + private static void AddStatistics( + CourseGradeStatisticScope scope, + IEnumerable> groups, + ICollection target, + StatisticsTarget targetInfo, + DateTime calculatedAt) + { + foreach (var group in groups) + target.Add(Create(scope, group.Key, group.Select(x => x.Score), targetInfo, calculatedAt)); + } + + private static void AddUniversityStatistic( + IReadOnlyCollection scores, + ICollection target, + StatisticsTarget targetInfo, + DateTime calculatedAt) + { + if (scores.Count > 0) + target.Add(Create(CourseGradeStatisticScope.University, null, + scores.Select(x => x.Score), targetInfo, calculatedAt)); + } + + private static CourseGradeStatistic Create( + CourseGradeStatisticScope scope, + Guid? scopeEntityId, + IEnumerable source, + StatisticsTarget targetInfo, + DateTime calculatedAt) + { + var scores = source.ToArray(); + var passed = scores.Count(x => x >= 60m); + return new CourseGradeStatistic + { + CourseId = targetInfo.CourseId, + AcademicTermId = targetInfo.AcademicTermId, + Scope = scope, + ScopeEntityId = scopeEntityId, + StudentCount = scores.Length, + PassedCount = passed, + Below60Count = scores.Count(x => x < 60m), + From60To69Count = scores.Count(x => x >= 60m && x < 70m), + From70To79Count = scores.Count(x => x >= 70m && x < 80m), + From80To89Count = scores.Count(x => x >= 80m && x < 90m), + From90To100Count = scores.Count(x => x >= 90m), + HighestScore = scores.Max(), + AverageScore = Math.Round(scores.Average(), 1), + LowestScore = scores.Min(), + PassRate = Math.Round((decimal)passed / scores.Length * 100m, 2), + CalculatedAt = calculatedAt + }; + } + + private sealed record ScoreRow(decimal Score, Guid ClassId, Guid MajorId, Guid CollegeId); + private sealed record StatisticsTarget(Guid CourseId, Guid AcademicTermId); +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs index 1d7a33d..198dd9b 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs @@ -66,6 +66,10 @@ public sealed class AppDbContext(DbContextOptions options) public DbSet GradeRecords => Set(); public DbSet GradeItems => Set(); public DbSet GradeItemScores => Set(); + public DbSet CourseGradeStatistics => + Set(); + public DbSet CourseGradeStatisticsRefreshJobs => + Set(); public DbSet OtherExamBatches => Set(); public DbSet OtherExamResults => Set(); public DbSet AttendanceSheets => Set(); @@ -847,6 +851,32 @@ public sealed class AppDbContext(DbContextOptions options) .OnDelete(DeleteBehavior.Restrict); }); + builder.Entity(entity => + { + entity.Property(x => x.HighestScore).HasPrecision(5, 1); + entity.Property(x => x.AverageScore).HasPrecision(5, 1); + entity.Property(x => x.LowestScore).HasPrecision(5, 1); + entity.Property(x => x.PassRate).HasPrecision(5, 2); + entity.HasIndex(x => new + { + x.CourseId, x.AcademicTermId, x.Scope, x.ScopeEntityId + }).IsUnique().HasDatabaseName("UX_CourseGradeStatistics_Scope"); + entity.HasIndex(x => new { x.AcademicTermId, x.Scope, x.ScopeEntityId }); + entity.HasOne().WithMany().HasForeignKey(x => x.CourseId) + .OnDelete(DeleteBehavior.Restrict); + entity.HasOne().WithMany().HasForeignKey(x => x.AcademicTermId) + .OnDelete(DeleteBehavior.Restrict); + }); + + builder.Entity(entity => + { + entity.Property(x => x.ErrorMessage).HasMaxLength(2000); + entity.HasIndex(x => new { x.Status, x.CreatedAt }); + entity.HasIndex(x => x.GradeSheetId); + entity.HasOne().WithMany().HasForeignKey(x => x.GradeSheetId) + .OnDelete(DeleteBehavior.Cascade); + }); + builder.Entity(entity => { entity.Property(x => x.Name).HasMaxLength(120); diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs index 0c1074e..121b84b 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs @@ -84,6 +84,10 @@ public sealed class DevelopmentSqliteMigrator( "20260803_44_student_personal_profile"; private const string OtherExamResultsMigration = "20260808_45_other_exam_results"; + private const string CourseGradeStatisticsMigration = + "20260808_46_course_grade_statistics"; + private const string CourseGradeDistributionMigration = + "20260809_47_course_grade_distribution"; public async Task MigrateAsync(CancellationToken cancellationToken = default) { @@ -639,6 +643,14 @@ public sealed class DevelopmentSqliteMigrator( ? OtherExamResultsStatements.Skip(1) : OtherExamResultsStatements, cancellationToken); + await ApplyMigrationAsync( + CourseGradeStatisticsMigration, + CourseGradeStatisticsStatements, + cancellationToken); + await ApplyMigrationAsync( + CourseGradeDistributionMigration, + CourseGradeDistributionStatements, + cancellationToken); } private async Task ApplyMigrationAsync( @@ -2210,6 +2222,25 @@ public sealed class DevelopmentSqliteMigrator( """CREATE INDEX IF NOT EXISTS "IX_OtherExamResults_StudentId_OtherExamBatchId" ON "OtherExamResults" ("StudentId", "OtherExamBatchId");""" ]; + private static readonly string[] CourseGradeStatisticsStatements = + [ + """CREATE TABLE IF NOT EXISTS "CourseGradeStatistics" ("Id" TEXT NOT NULL CONSTRAINT "PK_CourseGradeStatistics" PRIMARY KEY, "CourseId" TEXT NOT NULL, "AcademicTermId" TEXT NOT NULL, "Scope" INTEGER NOT NULL, "ScopeEntityId" TEXT NULL, "StudentCount" INTEGER NOT NULL, "PassedCount" INTEGER NOT NULL, "HighestScore" TEXT NOT NULL, "AverageScore" TEXT NOT NULL, "LowestScore" TEXT NOT NULL, "PassRate" TEXT NOT NULL, "CalculatedAt" TEXT NOT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, CONSTRAINT "FK_CourseGradeStatistics_Courses_CourseId" FOREIGN KEY ("CourseId") REFERENCES "Courses" ("Id") ON DELETE RESTRICT, CONSTRAINT "FK_CourseGradeStatistics_AcademicTerms_AcademicTermId" FOREIGN KEY ("AcademicTermId") REFERENCES "AcademicTerms" ("Id") ON DELETE RESTRICT);""", + """CREATE UNIQUE INDEX IF NOT EXISTS "UX_CourseGradeStatistics_Scope" ON "CourseGradeStatistics" ("CourseId", "AcademicTermId", "Scope", "ScopeEntityId");""", + """CREATE INDEX IF NOT EXISTS "IX_CourseGradeStatistics_AcademicTermId_Scope_ScopeEntityId" ON "CourseGradeStatistics" ("AcademicTermId", "Scope", "ScopeEntityId");""", + """CREATE TABLE IF NOT EXISTS "CourseGradeStatisticsRefreshJobs" ("Id" TEXT NOT NULL CONSTRAINT "PK_CourseGradeStatisticsRefreshJobs" PRIMARY KEY, "GradeSheetId" TEXT NOT NULL, "Status" INTEGER NOT NULL, "StartedAt" TEXT NULL, "CompletedAt" TEXT NULL, "ErrorMessage" TEXT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, CONSTRAINT "FK_CourseGradeStatisticsRefreshJobs_GradeSheets_GradeSheetId" FOREIGN KEY ("GradeSheetId") REFERENCES "GradeSheets" ("Id") ON DELETE CASCADE);""", + """CREATE INDEX IF NOT EXISTS "IX_CourseGradeStatisticsRefreshJobs_GradeSheetId" ON "CourseGradeStatisticsRefreshJobs" ("GradeSheetId");""", + """CREATE INDEX IF NOT EXISTS "IX_CourseGradeStatisticsRefreshJobs_Status_CreatedAt" ON "CourseGradeStatisticsRefreshJobs" ("Status", "CreatedAt");""" + ]; + + private static readonly string[] CourseGradeDistributionStatements = + [ + """ALTER TABLE "CourseGradeStatistics" ADD COLUMN "Below60Count" INTEGER NOT NULL DEFAULT 0;""", + """ALTER TABLE "CourseGradeStatistics" ADD COLUMN "From60To69Count" INTEGER NOT NULL DEFAULT 0;""", + """ALTER TABLE "CourseGradeStatistics" ADD COLUMN "From70To79Count" INTEGER NOT NULL DEFAULT 0;""", + """ALTER TABLE "CourseGradeStatistics" ADD COLUMN "From80To89Count" INTEGER NOT NULL DEFAULT 0;""", + """ALTER TABLE "CourseGradeStatistics" ADD COLUMN "From90To100Count" INTEGER NOT NULL DEFAULT 0;""" + ]; + private static readonly string[] ApprovalTableStatements = [ """CREATE TABLE "CourseExemptions" ("Id" TEXT NOT NULL CONSTRAINT "PK_CourseExemptions" PRIMARY KEY, "StudentId" TEXT NOT NULL, "TeachingTaskId" TEXT NOT NULL, "Reason" TEXT NOT NULL, "Status" INTEGER NOT NULL, "ReviewComment" TEXT NULL, "SubmittedAt" TEXT NOT NULL, "ReviewedAt" TEXT NULL, "ReviewedByUserId" TEXT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, CONSTRAINT "FK_CourseExemptions_Students" FOREIGN KEY ("StudentId") REFERENCES "Students" ("Id") ON DELETE RESTRICT, CONSTRAINT "FK_CourseExemptions_TeachingTasks" FOREIGN KEY ("TeachingTaskId") REFERENCES "TeachingTasks" ("Id") ON DELETE RESTRICT);""", diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260808133529_CourseGradeStatistics.Designer.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260808133529_CourseGradeStatistics.Designer.cs new file mode 100644 index 0000000..0c715d9 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260808133529_CourseGradeStatistics.Designer.cs @@ -0,0 +1,6381 @@ +// +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("20260808133529_CourseGradeStatistics")] + partial class CourseGradeStatistics + { + /// + 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("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("CalculatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + 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("Below60Count") + .HasColumnType("int"); + + b.Property("From60To69Count") + .HasColumnType("int"); + + b.Property("From70To79Count") + .HasColumnType("int"); + + b.Property("From80To89Count") + .HasColumnType("int"); + + b.Property("From90To100Count") + .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.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("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("TeachingTaskId", "Code") + .IsUnique(); + + b.HasIndex("Status", "StartDate", "EndDate"); + + 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.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.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.TeachingTaskScheduleConstraint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllowedDayOfWeeks") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EarliestPeriod") + .HasColumnType("int"); + + 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("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("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.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.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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.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.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.TeachingTaskScheduleConstraint", b => + { + 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("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.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.TeachingTaskScheduleConstraint", b => + { + b.Navigation("AllowedClassrooms"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260808133529_CourseGradeStatistics.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260808133529_CourseGradeStatistics.cs new file mode 100644 index 0000000..ec0baa7 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260808133529_CourseGradeStatistics.cs @@ -0,0 +1,113 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql +{ + /// + public partial class CourseGradeStatistics : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CourseGradeStatistics", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + CourseId = table.Column(type: "char(36)", nullable: false), + AcademicTermId = table.Column(type: "char(36)", nullable: false), + Scope = table.Column(type: "int", nullable: false), + ScopeEntityId = table.Column(type: "char(36)", nullable: true), + StudentCount = table.Column(type: "int", nullable: false), + PassedCount = table.Column(type: "int", nullable: false), + Below60Count = table.Column(type: "int", nullable: false), + From60To69Count = table.Column(type: "int", nullable: false), + From70To79Count = table.Column(type: "int", nullable: false), + From80To89Count = table.Column(type: "int", nullable: false), + From90To100Count = table.Column(type: "int", nullable: false), + HighestScore = table.Column(type: "decimal(5,1)", precision: 5, scale: 1, nullable: false), + AverageScore = table.Column(type: "decimal(5,1)", precision: 5, scale: 1, nullable: false), + LowestScore = table.Column(type: "decimal(5,1)", precision: 5, scale: 1, nullable: false), + PassRate = table.Column(type: "decimal(5,2)", precision: 5, scale: 2, nullable: false), + CalculatedAt = table.Column(type: "datetime(6)", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CourseGradeStatistics", x => x.Id); + table.ForeignKey( + name: "FK_CourseGradeStatistics_AcademicTerms_AcademicTermId", + column: x => x.AcademicTermId, + principalTable: "AcademicTerms", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_CourseGradeStatistics_Courses_CourseId", + column: x => x.CourseId, + principalTable: "Courses", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "CourseGradeStatisticsRefreshJobs", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + GradeSheetId = table.Column(type: "char(36)", nullable: false), + Status = table.Column(type: "int", nullable: false), + StartedAt = table.Column(type: "datetime(6)", nullable: true), + CompletedAt = table.Column(type: "datetime(6)", nullable: true), + ErrorMessage = table.Column(type: "varchar(2000)", maxLength: 2000, nullable: true), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CourseGradeStatisticsRefreshJobs", x => x.Id); + table.ForeignKey( + name: "FK_CourseGradeStatisticsRefreshJobs_GradeSheets_GradeSheetId", + column: x => x.GradeSheetId, + principalTable: "GradeSheets", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_CourseGradeStatistics_AcademicTermId_Scope_ScopeEntityId", + table: "CourseGradeStatistics", + columns: new[] { "AcademicTermId", "Scope", "ScopeEntityId" }); + + migrationBuilder.CreateIndex( + name: "UX_CourseGradeStatistics_Scope", + table: "CourseGradeStatistics", + columns: new[] { "CourseId", "AcademicTermId", "Scope", "ScopeEntityId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_CourseGradeStatisticsRefreshJobs_GradeSheetId", + table: "CourseGradeStatisticsRefreshJobs", + column: "GradeSheetId"); + + migrationBuilder.CreateIndex( + name: "IX_CourseGradeStatisticsRefreshJobs_Status_CreatedAt", + table: "CourseGradeStatisticsRefreshJobs", + columns: new[] { "Status", "CreatedAt" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CourseGradeStatistics"); + + migrationBuilder.DropTable( + name: "CourseGradeStatisticsRefreshJobs"); + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs index 45d85c5..c6ab6f4 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs @@ -983,6 +983,118 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql 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("CalculatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + 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("Below60Count") + .HasColumnType("int"); + + b.Property("From60To69Count") + .HasColumnType("int"); + + b.Property("From70To79Count") + .HasColumnType("int"); + + b.Property("From80To89Count") + .HasColumnType("int"); + + b.Property("From90To100Count") + .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.CoursePrerequisite", b => { b.Property("Id") @@ -4871,6 +4983,30 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.Navigation("TeachingTask"); }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGradeStatistic", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", null) + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", null) + .WithMany() + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGradeStatisticsRefreshJob", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GradeSheet", null) + .WithMany() + .HasForeignKey("GradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CoursePrerequisite", b => { b.HasOne("Jiaowu.Api.Domain.Academic.Course", "Course") diff --git a/src/Jiaowu.Api/Program.cs b/src/Jiaowu.Api/Program.cs index caa742e..e214a1d 100644 --- a/src/Jiaowu.Api/Program.cs +++ b/src/Jiaowu.Api/Program.cs @@ -2,6 +2,7 @@ using System.Text; using System.Text.Json.Serialization; using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Infrastructure.BackgroundJobs; +using Jiaowu.Api.Infrastructure.Grades; using Jiaowu.Api.Infrastructure.Configuration; using Jiaowu.Api.Infrastructure.Auth; using Jiaowu.Api.Infrastructure.Caching; @@ -223,6 +224,7 @@ if (backgroundJobOptions.PollIntervalMilliseconds is < 100 or > 30000 || backgroundJobOptions.ExamArrangementConcurrency is < 1 or > 16 || backgroundJobOptions.ExamSignInExportConcurrency is < 1 or > 16 || backgroundJobOptions.ExamPublishConcurrency is < 1 or > 16 || + backgroundJobOptions.CourseGradeStatisticsRefreshConcurrency is < 1 or > 16 || backgroundJobOptions.ProcessingAttemptLimit is < 1 or > 100 || backgroundJobOptions.MaintenanceIntervalSeconds is < 10 or > 3600 || backgroundJobOptions.CompletedRetentionDays is < 1 or > 3650 || @@ -425,6 +427,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/src/Jiaowu.Api/appsettings.json b/src/Jiaowu.Api/appsettings.json index 9c0225d..cb21d94 100644 --- a/src/Jiaowu.Api/appsettings.json +++ b/src/Jiaowu.Api/appsettings.json @@ -56,6 +56,7 @@ "SchedulePublishConcurrency": 1, "MakeupExamAutoConcurrency": 1, "ExamArrangementConcurrency": 1, + "CourseGradeStatisticsRefreshConcurrency": 1, "Exchange": "jiaowu.background-jobs", "QueuePrefix": "jiaowu.background-jobs", "UseQuorumQueues": true, diff --git a/web/src/router/index.ts b/web/src/router/index.ts index d73900d..bd10c4e 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -247,6 +247,14 @@ const router = createRouter({ roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher', 'Student', 'Counselor'], }, }, + { + path: 'grades/:sheetId/statistics', + name: 'course-grade-statistics', + component: () => import('../views/CourseGradeStatisticsView.vue'), + meta: { + roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Leader', 'Counselor', 'Student'], + }, + }, { path: 'other-exams', name: 'other-exams', diff --git a/web/src/views/CourseGradeStatisticsView.vue b/web/src/views/CourseGradeStatisticsView.vue new file mode 100644 index 0000000..8aa7f46 --- /dev/null +++ b/web/src/views/CourseGradeStatisticsView.vue @@ -0,0 +1,271 @@ + + + + + diff --git a/web/src/views/GradesView.vue b/web/src/views/GradesView.vue index 60dd8cc..bf97a7f 100644 --- a/web/src/views/GradesView.vue +++ b/web/src/views/GradesView.vue @@ -2,6 +2,7 @@ import { computed, onMounted, reactive, ref } from 'vue' import { Check, + ArrowRight, Delete, DocumentChecked, Download, @@ -15,9 +16,11 @@ import { import http, { apiErrorMessage } from '../api/http' import { downloadApiFile, importExcel } from '../api/excel' import { useAuthStore } from '../stores/auth' +import { useRouter } from 'vue-router' import { academicTermLabel, academicTermOptionClass, defaultAcademicTermId } from '../utils/academicTerms' const auth = useAuthStore() +const router = useRouter() const isStudent = computed(() => auth.user?.roles.includes('Student') && !auth.user.roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher'].includes(role))) @@ -127,6 +130,10 @@ function removeItem(index: number) { createForm.items.splice(index, 1) } +function openStatistics(record: any) { + router.push({ name: 'course-grade-statistics', params: { sheetId: record.gradeSheetId } }) +} + async function load(resetPage = false) { loading.value = true try { @@ -463,7 +470,17 @@ onMounted(async () => {
-
+
{{ record.termName }}

{{ record.courseName }}

@@ -482,6 +499,7 @@ onMounted(async () => { {{ record.totalScore }} 绩点 {{ record.gradePoint ?? '—' }} + 查看成绩分析
@@ -835,6 +853,10 @@ onMounted(async () => { padding: 8px; } .transcript-result.is-failed b { color: #b42318; } +.transcript-course-card { cursor: pointer; transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease; } +.transcript-course-card:hover { transform: translateY(-2px); border-color: #91a8c8; box-shadow: 0 10px 24px rgb(31 66 112 / 9%); } +.transcript-course-card:focus-visible { outline: 2px solid #315a9b; outline-offset: 3px; } +.card-entry { display: inline-flex !important; align-items: center; justify-content: flex-end; gap: 4px; margin-top: 8px; color: #315a9b !important; font-size: 10px !important; font-weight: 700; } /* Preview total score */ .total-score.preview {