From ae075bda6d330b020f6849614e5a10b7882d42a6 Mon Sep 17 00:00:00 2001 From: biss Date: Mon, 7 Sep 2026 19:34:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=87=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 + .../Controllers/ArchivesController.cs | 69 + .../Controllers/BaseDataController.cs | 43 +- .../Controllers/BaseDataExcelController.cs | 16 +- .../Controllers/CoursesController.cs | 10 +- .../Controllers/CoursesExcelController.cs | 9 +- .../Controllers/GradesController.cs | 8 +- .../Controllers/PersonnelController.cs | 15 +- .../Controllers/PersonnelExcelController.cs | 12 + .../Controllers/StudentProfileController.cs | 7 + .../StudentStatusChangesController.cs | 2 + .../TeacherCourseApplicationsController.cs | 20 +- .../Controllers/TeachingTasksController.cs | 22 +- src/Jiaowu.Api/Domain/Academic/Archiving.cs | 23 + .../Domain/Academic/GradeEntities.cs | 8 +- .../Domain/Academic/OrganizationEntities.cs | 6 +- .../Academic/PeopleAndCourseEntities.cs | 24 +- .../Infrastructure/Academic/ArchiveService.cs | 144 + .../Persistence/AppDbContext.cs | 14 + .../Persistence/DevelopmentSqliteMigrator.cs | 15 + ...0260905093721_UnifiedArchiving.Designer.cs | 8805 +++++++++++++++++ .../MySql/20260905093721_UnifiedArchiving.cs | 272 + .../MySql/AppDbContextModelSnapshot.cs | 114 + .../ArchivesControllerTests.cs | 204 + tests/Jiaowu.Api.Tests/AuthControllerTests.cs | 1 + .../BaseDataControllerTests.cs | 4 +- web/src/components.d.ts | 1 + web/src/components/ArchiveDialog.vue | 118 + web/src/views/BaseDataView.vue | 54 +- web/src/views/CoursesView.vue | 31 +- web/src/views/GradesView.vue | 24 +- web/src/views/PersonnelView.vue | 31 +- 32 files changed, 10052 insertions(+), 90 deletions(-) create mode 100644 src/Jiaowu.Api/Controllers/ArchivesController.cs create mode 100644 src/Jiaowu.Api/Domain/Academic/Archiving.cs create mode 100644 src/Jiaowu.Api/Infrastructure/Academic/ArchiveService.cs create mode 100644 src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260905093721_UnifiedArchiving.Designer.cs create mode 100644 src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260905093721_UnifiedArchiving.cs create mode 100644 tests/Jiaowu.Api.Tests/ArchivesControllerTests.cs create mode 100644 web/src/components/ArchiveDialog.vue diff --git a/README.md b/README.md index 17ffc3b..decdbf7 100644 --- a/README.md +++ b/README.md @@ -692,3 +692,19 @@ Docker 镜像不包含 `.env`、数据库密码或 JWT 密钥。应用容器以 dotnet test Jiaowu.slnx npm --prefix web run build ``` + + +## 人员、课程、学期与成绩归档 + +人员和课程列表默认显示未归档记录,可切换“已归档”或清空筛选查看全部;成绩管理提供同样的归档筛选。学期管理保留当前和历史学期展示,并可按归档状态筛选。各页面均提供单条、批量归档及恢复,提交前展示阻断事项和处理入口,归档或恢复原因必填,每批最多 100 条。批量请求逐项重新校验和提交,显示每条记录的处理结果。 + +- 人员:学生毕业/退学、教师退休/离职后可归档;归档前检查相关未结课任务、未发布成绩、学生待审批事项和必办离校事项。恢复不会变更学籍、任职状态或登录账号状态。 +- 课程:未结束教学任务,以及在读学生适用的已发布培养方案会阻止归档。已归档课程退出新开课和课程申报选项;已有培养方案、教学任务和成绩引用保留。课程启停状态独立于归档状态。 +- 学期:当前学期不可归档;开放选课批次、未结束教学任务、未建册或未发布成绩需要先处理。恢复后仍为历史学期,不会自动设为当前。归档不会连带归档人员、课程,也不冻结补考和成绩更正。 +- 成绩:以教学班成绩单为单位,仅允许已发布且没有待审批成绩更正的成绩单归档。归档标记独立于发布状态,成绩单、绩点、毕业审核和历史统计继续使用原正式成绩;更正仍走既有审批流程。 + +已归档人员、课程和学期不能直接编辑或删除;人员、课程和学期 Excel 导入不能覆盖已归档记录,导出遵循列表归档筛选。教学任务发布和撤销结课会检查已归档的学期、课程与教师。归档操作按原有角色及学院范围授权,课程继续执行公共课校级维护规则,学期由校级管理员管理。每次状态变化保留操作者、时间和原因,恢复不删除历史日志。 + +统一接口为 `POST /api/archives/{kind}/preview`、`POST /api/archives/{kind}/apply` 和 `GET /api/archives/{kind}/{id}/history?page=1`,其中 `kind` 为 `teachers`、`students`、`courses`、`terms`、`grades`。原学期归档接口继续可用并复用同一校验流程。历史日志按每页 20 条读取,旧归档记录不会伪造操作日志。 + +数据库新增 `20260905093721_UnifiedArchiving` MySQL 迁移;SQLite 开发库由 `DevelopmentSqliteMigrator` 补齐字段和归档日志表,已有记录默认未归档,已有学期归档状态保留。部署时应按现有数据库升级流程应用迁移。 diff --git a/src/Jiaowu.Api/Controllers/ArchivesController.cs b/src/Jiaowu.Api/Controllers/ArchivesController.cs new file mode 100644 index 0000000..f68a13f --- /dev/null +++ b/src/Jiaowu.Api/Controllers/ArchivesController.cs @@ -0,0 +1,69 @@ +using System.ComponentModel.DataAnnotations; +using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Infrastructure.Academic; +using Jiaowu.Api.Infrastructure.Auth; +using Jiaowu.Api.Infrastructure.Caching; +using Jiaowu.Api.Infrastructure.Persistence; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Jiaowu.Api.Controllers; + +[ApiController] +[Authorize(Roles = SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin + "," + SystemRoles.CollegeAdmin)] +[Route("api/archives/{kind}")] +public sealed class ArchivesController(AppDbContext db, ICurrentUserDataScope scope, IAppCache cache) : ControllerBase +{ + [HttpPost("preview")] + public async Task Preview(string kind, ArchiveBatchRequest request, CancellationToken ct) + { + var service = new ArchiveService(db, scope); + var results = new List(); + foreach (var id in request.Ids.Distinct()) + { + var item = await service.InspectAsync(kind, id, !request.Archived, ct); + results.Add(item is null ? new { Id = id, Error = "记录不存在或无权操作。" } : (object)item.Value.Preview); + } + return Ok(results); + } + + [HttpGet("{id:guid}/history")] + public async Task History(string kind, Guid id, CancellationToken ct, int page = 1) + { + if (await new ArchiveService(db, scope).InspectAsync(kind, id, true, ct) is null) return NotFound(); + return Ok(await db.ArchiveEvents.AsNoTracking().Where(x => x.Kind == kind && x.EntityId == id) + .OrderByDescending(x => x.CreatedAt).ThenByDescending(x => x.Id) + .Skip((Math.Clamp(page, 1, 100000) - 1) * 20).Take(20).ToListAsync(ct)); + } + + [HttpPost("apply")] + public async Task Apply(string kind, ArchiveBatchRequest request, CancellationToken ct) + { + if (string.IsNullOrWhiteSpace(request.Reason)) return BadRequest("请填写归档或恢复原因。"); + var results = new List(); + foreach (var id in request.Ids.Distinct()) + { + try + { + var result = await new ArchiveService(db, scope).ChangeAsync(kind, id, request.Archived, request.Reason, ct); + results.Add(result); + } + catch (Exception ex) when (ex is DbUpdateException or System.Data.Common.DbException) + { + results.Add(new ArchiveOperationResult(id, false, "记录已发生变化或保存失败,请刷新后重试。")); + } + finally { db.ChangeTracker.Clear(); } + } + await cache.RemoveByTagAsync(AppCacheTags.Timetables, ct); + await cache.RemoveByTagAsync(AppCacheTags.Analytics, ct); + await cache.RemoveByTagAsync(AppCacheTags.BaseData, ct); + return Ok(results); + } +} + +public sealed record ArchiveBatchRequest( + [property: Required, MinLength(1), MaxLength(100)] Guid[] Ids, + bool Archived, + [property: MaxLength(500)] string? Reason = null); + diff --git a/src/Jiaowu.Api/Controllers/BaseDataController.cs b/src/Jiaowu.Api/Controllers/BaseDataController.cs index 56ced0b..53937d8 100644 --- a/src/Jiaowu.Api/Controllers/BaseDataController.cs +++ b/src/Jiaowu.Api/Controllers/BaseDataController.cs @@ -1,3 +1,5 @@ +using Jiaowu.Api.Infrastructure.Academic; +using Jiaowu.Api.Infrastructure.Auth; using System.ComponentModel.DataAnnotations; using Jiaowu.Api.Contracts; using Jiaowu.Api.Domain.Academic; @@ -14,7 +16,8 @@ namespace Jiaowu.Api.Controllers; [ApiController] [Authorize] [Route("api/base-data")] -public sealed class BaseDataController(AppDbContext db, IAppCache cache) : ControllerBase +public sealed class BaseDataController(AppDbContext db, IAppCache cache, + ICurrentUserDataScope? currentUserDataScope = null) : ControllerBase { private const string Administrators = $"{SystemRoles.SuperAdmin},{SystemRoles.AcademicAdmin}"; @@ -435,8 +438,8 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr return ValidationProblem("学期结束日期必须晚于开始日期。"); if (entity.IsCurrent && !request.IsCurrent) return ConflictProblem("不能直接取消当前学期,请将另一个学期设为当前。"); - if (request.IsCurrent && entity.IsArchived) - return ConflictProblem("已归档学期不能直接设为当前,请先撤销归档。"); + if (entity.IsArchived) + return ConflictProblem("已归档学期请先恢复后再编辑。"); if (request.IsCurrent && !request.IsEnabled) return ValidationProblem("当前学期必须保持启用。"); if (request.IsCurrent) @@ -493,26 +496,28 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr return ConflictProblem("当前学期不能归档,请先切换到新的当前学期。"); if (entity.IsArchived) return entity; - entity.IsArchived = true; - entity.ArchivedAt = DateTime.UtcNow; - await SaveAndInvalidateAsync(cancellationToken); - return entity; + return await ChangeTermArchiveAsync(id, true, cancellationToken); } [HttpPost("terms/{id:guid}/unarchive")] [Authorize(Roles = Administrators)] - public async Task> UnarchiveTerm( - Guid id, - CancellationToken cancellationToken) - { - var entity = await db.AcademicTerms.FindAsync([id], cancellationToken); - if (entity is null) return NotFound(); - if (!entity.IsArchived) return entity; + public Task> UnarchiveTerm(Guid id, CancellationToken cancellationToken) => + ChangeTermArchiveAsync(id, false, cancellationToken); - entity.IsArchived = false; - entity.ArchivedAt = null; - await SaveAndInvalidateAsync(cancellationToken); - return entity; + private async Task> ChangeTermArchiveAsync(Guid id, bool archived, CancellationToken ct) + { + if (!await db.AcademicTerms.AnyAsync(x => x.Id == id, ct)) return NotFound(); + var scope = currentUserDataScope ?? new CurrentUserDataScope(new HttpContextAccessor { HttpContext = HttpContext }); + var service = new ArchiveService(db, scope); + try + { + var result = await service.ChangeAsync("terms", id, archived, archived ? "通过学期管理归档" : "通过学期管理恢复", ct); + if (!result.Success) return ConflictProblem(result.Message); + await cache.RemoveByTagAsync(AppCacheTags.BaseData, ct); + await cache.RemoveByTagAsync(AppCacheTags.Timetables, ct); + return (await db.AcademicTerms.FindAsync([id], ct))!; + } + catch (DbUpdateException) { return ConflictProblem("学期数据已变化,请刷新后重试。"); } } [HttpGet("classrooms")] @@ -704,6 +709,8 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr if (entity is null) return NotFound(); if (entity is AcademicTerm { IsCurrent: true }) return ConflictProblem("当前学期不能删除,请先切换到其他学期。"); + if (entity is AcademicTerm { IsArchived: true }) + return ConflictProblem("已归档学期不能删除,请先恢复。"); db.Remove(entity); try { diff --git a/src/Jiaowu.Api/Controllers/BaseDataExcelController.cs b/src/Jiaowu.Api/Controllers/BaseDataExcelController.cs index 7c8bd04..dbf82d9 100644 --- a/src/Jiaowu.Api/Controllers/BaseDataExcelController.cs +++ b/src/Jiaowu.Api/Controllers/BaseDataExcelController.cs @@ -51,10 +51,10 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) : } [HttpGet("{kind}/export")] - public async Task Export(string kind, CancellationToken cancellationToken) + public async Task Export(string kind, CancellationToken cancellationToken, bool? isArchived = null, string? keyword = null) { if (!TryGetHeaders(kind, out var headers)) return NotFound(); - var rows = await GetExportRowsAsync(kind, cancellationToken); + var rows = await GetExportRowsAsync(kind, cancellationToken, isArchived, keyword?.Trim()); var bytes = ExcelWorkbookHelper.Create(KindName(kind), headers, rows); return File(bytes, ExcelWorkbookHelper.ContentType, DownloadFileName.Export(KindName(kind), ".xlsx")); @@ -140,7 +140,7 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) : private async Task>> GetExportRowsAsync( string kind, - CancellationToken cancellationToken) => + CancellationToken cancellationToken, bool? isArchived, string? keyword) => kind.ToLowerInvariant() switch { "campuses" => (await db.Campuses.AsNoTracking().OrderBy(x => x.Code) @@ -158,7 +158,10 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) : .Include(x => x.CounselorUser).OrderBy(x => x.Code).ToListAsync(cancellationToken)) .Select(x => Row(x.Code, x.Name, x.Major!.Code, x.Grade, x.CounselorUser?.StaffNumber, x.SortOrder, Status(x.IsEnabled))).ToList(), - "terms" => (await db.AcademicTerms.AsNoTracking().OrderByDescending(x => x.StartDate) + "terms" => (await db.AcademicTerms.AsNoTracking() + .Where(x => !isArchived.HasValue || x.IsArchived == isArchived.Value) + .Where(x => keyword == null || keyword == "" || x.Code.Contains(keyword) || x.Name.Contains(keyword)) + .OrderByDescending(x => x.StartDate) .ToListAsync(cancellationToken)) .Select(x => Row(x.Code, x.Name, x.AcademicYear, SeasonName(x.Season), x.StartDate, x.EndDate, YesNo(x.IsCurrent), Status(x.IsEnabled))).ToList(), @@ -384,6 +387,11 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) : continue; } var entity = existing.GetValueOrDefault(code); + if (entity?.IsArchived == true) + { + errors.Add($"第 {row.RowNumber} 行:学期已归档,请先恢复后再导入更新。"); + continue; + } if (entity is null) { entity = new AcademicTerm diff --git a/src/Jiaowu.Api/Controllers/CoursesController.cs b/src/Jiaowu.Api/Controllers/CoursesController.cs index 639c3d9..142a296 100644 --- a/src/Jiaowu.Api/Controllers/CoursesController.cs +++ b/src/Jiaowu.Api/Controllers/CoursesController.cs @@ -33,7 +33,8 @@ public sealed class CoursesController( Guid? categoryId = null, CourseNature? nature = null, bool? isEnabled = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default, + bool? isArchived = null) { pageSize = Math.Clamp(pageSize, 10, 100); if (!CursorToken.TryDecode(cursor, out var after)) @@ -61,6 +62,7 @@ public sealed class CoursesController( (x.EnglishName != null && x.EnglishName.Contains(keyword))); } + if (isArchived.HasValue) source = source.Where(x => x.IsArchived == isArchived.Value); var total = await source.CountAsync(cancellationToken); if (after is not null) { @@ -105,7 +107,7 @@ public sealed class CoursesController( }), x.IsEnabled, x.SortOrder, - x.CreatedAt, + x.IsArchived, x.ArchivedAt, x.ArchiveReason, x.CreatedAt, CanManage = canManageAll || canManageCollegeCourses && x.CollegeId == managedCollegeId && @@ -132,7 +134,7 @@ public sealed class CoursesController( string? keyword = null, CancellationToken cancellationToken = default) { - var source = ScopedCourses().AsNoTracking().Where(x => x.IsEnabled); + var source = ScopedCourses().AsNoTracking().Where(x => x.IsEnabled && !x.IsArchived); if (collegeId.HasValue) source = source.Where(x => x.CollegeId == collegeId.Value); if (nature.HasValue) @@ -220,6 +222,7 @@ public sealed class CoursesController( var validation = await ValidateAsync(id, request, cancellationToken); if (validation is not null) return validation; + if (entity.IsArchived) return Conflict(new ProblemDetails { Detail = "已归档课程请先恢复后再编辑。", Status = 409 }); entity.Code = request.Code.Trim(); entity.Name = request.Name.Trim(); entity.EnglishName = Normalize(request.EnglishName); @@ -257,6 +260,7 @@ public sealed class CoursesController( var entity = await db.Courses.FindAsync([id], cancellationToken); if (entity is null) return NotFound(); if (!CanManage(entity.CollegeId, entity.Nature)) return Forbid(); + if (entity.IsArchived) return Conflict(new ProblemDetails { Detail = "已归档课程不能删除。", Status = 409 }); db.Courses.Remove(entity); return await SaveAsync(id, false, cancellationToken); } diff --git a/src/Jiaowu.Api/Controllers/CoursesExcelController.cs b/src/Jiaowu.Api/Controllers/CoursesExcelController.cs index 1048714..b1eaf8a 100644 --- a/src/Jiaowu.Api/Controllers/CoursesExcelController.cs +++ b/src/Jiaowu.Api/Controllers/CoursesExcelController.cs @@ -62,7 +62,8 @@ public sealed class CoursesExcelController( Guid? categoryId = null, CourseNature? nature = null, bool? isEnabled = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default, + bool? isArchived = null) { var source = ApplyFilters( ScopedCourses().AsNoTracking(), @@ -71,6 +72,7 @@ public sealed class CoursesExcelController( categoryId, nature, isEnabled); + if (isArchived.HasValue) source = source.Where(x => x.IsArchived == isArchived.Value); var courses = await source .Include(x => x.College) .Include(x => x.CourseCategory) @@ -266,6 +268,11 @@ public sealed class CoursesExcelController( updated++; } + if (entity.IsArchived) + { + errors.Add($"第 {row.RowNumber} 行:记录已归档,请先恢复后再导入更新。"); + continue; + } entity.Name = name; entity.EnglishName = englishName; entity.CollegeId = college.Id; diff --git a/src/Jiaowu.Api/Controllers/GradesController.cs b/src/Jiaowu.Api/Controllers/GradesController.cs index 0db7050..236c8d4 100644 --- a/src/Jiaowu.Api/Controllers/GradesController.cs +++ b/src/Jiaowu.Api/Controllers/GradesController.cs @@ -56,7 +56,8 @@ public sealed class GradesController( Guid? collegeId = null, string? teacherKeyword = null, string? classKeyword = null, - Guid? sheetId = null) + Guid? sheetId = null, + bool? isArchived = null) { page = Math.Max(1, page); pageSize = Math.Clamp(pageSize, 10, 50); @@ -64,6 +65,10 @@ public sealed class GradesController( .Where(x => x.Status == TeachingTaskStatus.Published || x.Status == TeachingTaskStatus.Closed); + if (isArchived.HasValue) + source = isArchived.Value + ? source.Where(x => db.GradeSheets.Any(s => s.TeachingTaskId == x.Id && s.IsArchived)) + : source.Where(x => !db.GradeSheets.Any(s => s.TeachingTaskId == x.Id && s.IsArchived)); if (academicTermId.HasValue) source = source.Where(x => x.AcademicTermId == academicTermId); if (collegeId.HasValue) @@ -125,6 +130,7 @@ public sealed class GradesController( .Select(sheet => new { sheet.Id, + sheet.IsArchived, sheet.ArchivedAt, sheet.ArchiveReason, sheet.Status, sheet.RegularWeight, sheet.FinalWeight, diff --git a/src/Jiaowu.Api/Controllers/PersonnelController.cs b/src/Jiaowu.Api/Controllers/PersonnelController.cs index 7b3f153..b3d6bb1 100644 --- a/src/Jiaowu.Api/Controllers/PersonnelController.cs +++ b/src/Jiaowu.Api/Controllers/PersonnelController.cs @@ -56,6 +56,7 @@ public sealed class PersonnelController( if (query.TeacherStatus.HasValue) source = source.Where(x => x.Status == query.TeacherStatus); + if (query.IsArchived.HasValue) source = source.Where(x => x.IsArchived == query.IsArchived.Value); var total = await source.CountAsync(cancellationToken); var items = await source .OrderBy(x => x.TeacherNumber) @@ -77,7 +78,7 @@ public sealed class PersonnelController( x.Email, x.Notes, x.UserId, - x.CreatedAt + x.IsArchived, x.ArchivedAt, x.ArchiveReason, x.CreatedAt }) .ToListAsync(cancellationToken); @@ -124,6 +125,7 @@ public sealed class PersonnelController( var validation = await ValidateCollegeAsync(request.CollegeId, cancellationToken); if (validation is not null) return validation; + if (entity.IsArchived) return ConflictProblem("已归档人员请先恢复后再编辑。"); entity.TeacherNumber = request.TeacherNumber.Trim(); entity.Name = request.Name.Trim(); entity.Gender = request.Gender; @@ -150,6 +152,7 @@ public sealed class PersonnelController( return ConflictProblem("教师已关联登录账号,请先解除账号关联后再删除。"); } + if (entity.IsArchived) return ConflictProblem("已归档人员不能删除。"); db.Teachers.Remove(entity); return await SaveNoContentAsync(cancellationToken); } @@ -168,7 +171,7 @@ public sealed class PersonnelController( var teacher = await db.Teachers.FindAsync([id], cancellationToken); if (teacher is null) return NotFound(); if (!CanAccessCollege(teacher.CollegeId)) return Forbid(); - if (teacher.Status != TeacherStatus.Active) + if (teacher.IsArchived || teacher.Status != TeacherStatus.Active) { return ConflictProblem("仅在职教师可以激活登录账号。"); } @@ -254,6 +257,7 @@ public sealed class PersonnelController( x.StudentNumber.Contains(keyword) || x.Name.Contains(keyword)); } + if (query.IsArchived.HasValue) source = source.Where(x => x.IsArchived == query.IsArchived.Value); var total = await source.CountAsync(cancellationToken); var items = await source .OrderBy(x => x.StudentNumber) @@ -296,7 +300,7 @@ public sealed class PersonnelController( Biography = canReadFullProfile ? x.Biography : null, Notes = canReadFullProfile ? x.Notes : null, x.UserId, - x.CreatedAt + x.IsArchived, x.ArchivedAt, x.ArchiveReason, x.CreatedAt }) .ToListAsync(cancellationToken); @@ -401,6 +405,7 @@ public sealed class PersonnelController( if (!CanAccessCollege(targetClass.CollegeId)) return Forbid(); + if (entity.IsArchived) return ConflictProblem("已归档人员请先恢复后再编辑。"); entity.StudentNumber = request.StudentNumber.Trim(); entity.Name = request.Name.Trim(); entity.Gender = request.Gender; @@ -448,6 +453,7 @@ public sealed class PersonnelController( return ConflictProblem("学生已关联登录账号,请先解除账号关联后再删除。"); } + if (entity.IsArchived) return ConflictProblem("已归档人员不能删除。"); db.Students.Remove(entity); return await SaveNoContentAsync(cancellationToken); } @@ -582,7 +588,8 @@ public sealed record PersonnelQuery( Guid? ClassId = null, int? EnrollmentYear = null, TeacherStatus? TeacherStatus = null, - StudentStatus? StudentStatus = null); + StudentStatus? StudentStatus = null, + bool? IsArchived = null); public sealed record TeacherRequest( [Required, MaxLength(30)] string TeacherNumber, diff --git a/src/Jiaowu.Api/Controllers/PersonnelExcelController.cs b/src/Jiaowu.Api/Controllers/PersonnelExcelController.cs index ebcbee5..71c9115 100644 --- a/src/Jiaowu.Api/Controllers/PersonnelExcelController.cs +++ b/src/Jiaowu.Api/Controllers/PersonnelExcelController.cs @@ -290,6 +290,11 @@ public sealed class PersonnelExcelController( created++; } else updated++; + if (entity.IsArchived) + { + errors.Add($"第 {row.RowNumber} 行:记录已归档,请先恢复后再导入更新。"); + continue; + } entity.Name = name; entity.Gender = gender.Value; entity.CollegeId = college.Id; @@ -362,6 +367,11 @@ public sealed class PersonnelExcelController( created++; } else updated++; + if (entity.IsArchived) + { + errors.Add($"第 {row.RowNumber} 行:记录已归档,请先恢复后再导入更新。"); + continue; + } entity.Name = name; entity.Gender = gender.Value; entity.AdministrativeClassId = targetClass.Id; @@ -397,6 +407,7 @@ public sealed class PersonnelExcelController( IQueryable source, PersonnelQuery query) { + if (query.IsArchived.HasValue) source = source.Where(x => x.IsArchived == query.IsArchived.Value); if (query.CollegeId.HasValue) source = source.Where(x => x.CollegeId == query.CollegeId); if (query.TeacherStatus.HasValue) @@ -415,6 +426,7 @@ public sealed class PersonnelExcelController( IQueryable source, PersonnelQuery query) { + if (query.IsArchived.HasValue) source = source.Where(x => x.IsArchived == query.IsArchived.Value); if (query.CollegeId.HasValue) source = source.Where(x => x.AdministrativeClass!.Major!.CollegeId == query.CollegeId); diff --git a/src/Jiaowu.Api/Controllers/StudentProfileController.cs b/src/Jiaowu.Api/Controllers/StudentProfileController.cs index 10a7653..fa45d74 100644 --- a/src/Jiaowu.Api/Controllers/StudentProfileController.cs +++ b/src/Jiaowu.Api/Controllers/StudentProfileController.cs @@ -70,6 +70,13 @@ public sealed class StudentProfileController( x => x.UserId == userId, cancellationToken); if (student is null) return NotFound(); + if (student.IsArchived) + return Conflict(new ProblemDetails + { + Title = "无法保存档案", + Detail = "学生档案已归档,请联系教务管理员恢复后再修改。", + Status = StatusCodes.Status409Conflict + }); student.Gender = request.Gender; student.DateOfBirth = request.DateOfBirth; diff --git a/src/Jiaowu.Api/Controllers/StudentStatusChangesController.cs b/src/Jiaowu.Api/Controllers/StudentStatusChangesController.cs index c7de970..d2ecfa3 100644 --- a/src/Jiaowu.Api/Controllers/StudentStatusChangesController.cs +++ b/src/Jiaowu.Api/Controllers/StudentStatusChangesController.cs @@ -106,6 +106,8 @@ public sealed class StudentStatusChangesController( var userId = currentUserDataScope.Current.UserId; var student = await db.Students.FirstOrDefaultAsync(x => x.UserId == userId, token); if (student is null) return ConflictProblem("当前账号未关联学生档案。"); + if (student.IsArchived) + return ConflictProblem("学生档案已归档,不能发起新的学籍异动。"); var target = request.Type switch { StudentStatusChangeType.Suspension when student.Status == StudentStatus.Active => diff --git a/src/Jiaowu.Api/Controllers/TeacherCourseApplicationsController.cs b/src/Jiaowu.Api/Controllers/TeacherCourseApplicationsController.cs index ce96dbf..ac379a6 100644 --- a/src/Jiaowu.Api/Controllers/TeacherCourseApplicationsController.cs +++ b/src/Jiaowu.Api/Controllers/TeacherCourseApplicationsController.cs @@ -27,7 +27,7 @@ public sealed class TeacherCourseApplicationsController( public async Task GetCourseOptions(CancellationToken cancellationToken) { return Ok(await db.Courses.AsNoTracking() - .Where(x => x.IsEnabled) + .Where(x => x.IsEnabled && !x.IsArchived) .OrderBy(x => x.SortOrder) .ThenBy(x => x.Code) .Select(x => new @@ -95,11 +95,11 @@ public sealed class TeacherCourseApplicationsController( var teacher = await CurrentTeacherAsync(cancellationToken); if (teacher is null) return ConflictProblem("当前账号尚未关联在职教师档案。"); if (!await db.AcademicTerms.AnyAsync( - x => x.Id == request.AcademicTermId && x.IsEnabled, + x => x.Id == request.AcademicTermId && x.IsEnabled && !x.IsArchived, cancellationToken)) return ValidationProblem("所选学期不存在或已停用。"); if (!await db.Courses.AnyAsync( - x => x.Id == request.CourseId && x.IsEnabled, + x => x.Id == request.CourseId && x.IsEnabled && !x.IsArchived, cancellationToken)) return ValidationProblem("所选课程不存在或已停用。"); @@ -202,7 +202,7 @@ public sealed class TeacherCourseApplicationsController( CancellationToken cancellationToken) { var teachers = db.Teachers.AsNoTracking() - .Where(x => x.Status == TeacherStatus.Active); + .Where(x => x.Status == TeacherStatus.Active && !x.IsArchived); var collegeId = currentUserDataScope.Current.RestrictedCollegeId; if (collegeId.HasValue) teachers = teachers.Where(x => x.CollegeId == collegeId.Value); @@ -222,7 +222,7 @@ public sealed class TeacherCourseApplicationsController( }) .ToListAsync(cancellationToken), Courses = await db.Courses.AsNoTracking() - .Where(x => x.IsEnabled) + .Where(x => x.IsEnabled && !x.IsArchived) .OrderBy(x => x.SortOrder) .ThenBy(x => x.Code) .Select(x => new @@ -246,17 +246,17 @@ public sealed class TeacherCourseApplicationsController( CancellationToken cancellationToken) { if (!await db.AcademicTerms.AnyAsync( - x => x.Id == request.AcademicTermId && x.IsEnabled, + x => x.Id == request.AcademicTermId && x.IsEnabled && !x.IsArchived, cancellationToken)) return ValidationProblem("所选学期不存在或已停用。"); if (!await db.Courses.AnyAsync( - x => x.Id == request.CourseId && x.IsEnabled, + x => x.Id == request.CourseId && x.IsEnabled && !x.IsArchived, cancellationToken)) return ValidationProblem("所选课程不存在或已停用。"); var teachers = db.Teachers.Where(x => x.Id == request.TeacherId && - x.Status == TeacherStatus.Active); + x.Status == TeacherStatus.Active && !x.IsArchived); var collegeId = currentUserDataScope.Current.RestrictedCollegeId; if (collegeId.HasValue) teachers = teachers.Where(x => x.CollegeId == collegeId.Value); @@ -328,7 +328,7 @@ public sealed class TeacherCourseApplicationsController( x.AcademicTermId == academicTermId && x.CourseId == courseId && x.Status == TeacherCourseApplicationStatus.Approved && - x.Teacher!.Status == TeacherStatus.Active); + x.Teacher!.Status == TeacherStatus.Active && !x.Teacher.IsArchived); return Ok(await source .OrderBy(x => x.Teacher!.TeacherNumber) .Select(x => new @@ -355,7 +355,7 @@ public sealed class TeacherCourseApplicationsController( { var userId = currentUserDataScope.Current.UserId; return db.Teachers.FirstOrDefaultAsync( - x => x.UserId == userId && x.Status == TeacherStatus.Active, + x => x.UserId == userId && x.Status == TeacherStatus.Active && !x.IsArchived, cancellationToken); } diff --git a/src/Jiaowu.Api/Controllers/TeachingTasksController.cs b/src/Jiaowu.Api/Controllers/TeachingTasksController.cs index 9c16e52..cfb9879 100644 --- a/src/Jiaowu.Api/Controllers/TeachingTasksController.cs +++ b/src/Jiaowu.Api/Controllers/TeachingTasksController.cs @@ -369,6 +369,8 @@ public sealed class TeachingTasksController( if (!CanManage(task.Course!)) return Forbid(); if (task.Status != TeachingTaskStatus.Closed) return ConflictProblem("只有已归档的教学任务可以撤销归档。"); + if (await HasArchivedDependenciesAsync([task.Id], cancellationToken)) + return ConflictProblem("学期、课程或教师已归档,请先恢复后再启用教学任务。"); task.Status = TeachingTaskStatus.Published; return await SaveAsync(id, false, cancellationToken); } @@ -423,6 +425,8 @@ public sealed class TeachingTasksController( foreach (var task in tasks) task.Status = TeachingTaskStatus.Closed; break; case TeachingTaskBatchOperation.Unarchive: + if (await HasArchivedDependenciesAsync(ids, cancellationToken)) + return ConflictProblem("学期、课程或教师已归档,请先恢复后再启用教学任务。"); if (tasks.Any(x => x.Status != TeachingTaskStatus.Closed)) return ConflictProblem( $"教学任务“{tasks.First(x => x.Status != TeachingTaskStatus.Closed).Name}”不是已归档状态,整批未撤销归档。"); @@ -454,12 +458,12 @@ public sealed class TeachingTasksController( return ValidationProblem("开始周不能晚于结束周。"); var term = await db.AcademicTerms.AsNoTracking() .FirstOrDefaultAsync( - x => x.Id == request.AcademicTermId && x.IsEnabled, + x => x.Id == request.AcademicTermId && x.IsEnabled && !x.IsArchived, cancellationToken); if (term is null) return ValidationProblem("所选学期不存在或已停用。"); var course = await db.Courses.AsNoTracking() .FirstOrDefaultAsync( - x => x.Id == request.CourseId && x.IsEnabled, + x => x.Id == request.CourseId && x.IsEnabled && !x.IsArchived, cancellationToken); if (course is null) return ValidationProblem("所选课程不存在或已停用。"); if (!CanManage(course)) return Forbid(); @@ -518,7 +522,7 @@ public sealed class TeachingTasksController( x.AcademicTermId == request.AcademicTermId && x.CourseId == request.CourseId && x.Status == TeacherCourseApplicationStatus.Approved && - x.Teacher!.Status == TeacherStatus.Active) + x.Teacher!.Status == TeacherStatus.Active && !x.Teacher.IsArchived) .Where(x => !scopedCollegeId.HasValue || x.Teacher!.CollegeId == scopedCollegeId.Value) .Select(x => x.Teacher!) @@ -634,7 +638,7 @@ public sealed class TeachingTasksController( if (request.StartWeek > request.EndWeek) return ValidationProblem("开始周不能晚于结束周。"); var course = await db.Courses.AsNoTracking() - .FirstOrDefaultAsync(x => x.Id == request.CourseId && x.IsEnabled, cancellationToken); + .FirstOrDefaultAsync(x => x.Id == request.CourseId && x.IsEnabled && !x.IsArchived, cancellationToken); if (course is null) return ValidationProblem("所选课程不存在或已停用。"); if (!CanManage(course)) return Forbid(); if (!Enum.IsDefined(request.SchedulingMode)) @@ -648,7 +652,7 @@ public sealed class TeachingTasksController( if (hoursProblem is not null) return ValidationProblem(hoursProblem); var collegeId = ScopedCollegeId(); if (!await db.AcademicTerms.AnyAsync( - x => x.Id == request.AcademicTermId && x.IsEnabled, + x => x.Id == request.AcademicTermId && x.IsEnabled && !x.IsArchived, cancellationToken)) return ValidationProblem("所选学期不存在或已停用。"); @@ -657,7 +661,7 @@ public sealed class TeachingTasksController( !teacherIds.Contains(request.PrimaryTeacherId.Value)) return ValidationProblem("主讲教师必须包含在授课教师中。"); if (await db.Teachers - .Where(x => x.Status == TeacherStatus.Active) + .Where(x => x.Status == TeacherStatus.Active && !x.IsArchived) .WhereIn(teacherIds, x => x.Id) .CountAsync(cancellationToken) != teacherIds.Length) return ValidationProblem("存在无效或非在职授课教师。"); @@ -707,6 +711,8 @@ public sealed class TeachingTasksController( IReadOnlyCollection tasks, CancellationToken cancellationToken) { + if (await HasArchivedDependenciesAsync(tasks.Select(x => x.Id).ToArray(), cancellationToken)) + return "学期、课程或教师已归档,不能发布教学任务。"; var invalidStatus = tasks.FirstOrDefault(x => x.Status != TeachingTaskStatus.Draft); if (invalidStatus is not null) return PublishingProblem( @@ -780,6 +786,10 @@ public sealed class TeachingTasksController( course.CollegeId, course.Nature); + private Task HasArchivedDependenciesAsync(Guid[] ids, CancellationToken ct) => + db.TeachingTasks.WhereIn(ids, x => x.Id).AnyAsync(x => + x.AcademicTerm!.IsArchived || x.Course!.IsArchived || x.Teachers.Any(t => t.Teacher!.IsArchived), ct); + private async Task SaveAsync( Guid id, bool created, diff --git a/src/Jiaowu.Api/Domain/Academic/Archiving.cs b/src/Jiaowu.Api/Domain/Academic/Archiving.cs new file mode 100644 index 0000000..302d5e3 --- /dev/null +++ b/src/Jiaowu.Api/Domain/Academic/Archiving.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using Jiaowu.Api.Domain.Common; + +namespace Jiaowu.Api.Domain.Academic; + +public interface IArchivable +{ + bool IsArchived { get; set; } + DateTime? ArchivedAt { get; set; } + Guid? ArchivedByUserId { get; set; } + string? ArchiveReason { get; set; } +} + +// Append-only history; deliberately independent of entity deletion and login-account lifecycle. +public sealed class ArchiveEvent : EntityBase +{ + [MaxLength(20)] public required string Kind { get; set; } + public Guid EntityId { get; set; } + public bool IsArchived { get; set; } + public Guid ActorUserId { get; set; } + [MaxLength(256)] public string? ActorName { get; set; } + [MaxLength(500)] public required string Reason { get; set; } +} diff --git a/src/Jiaowu.Api/Domain/Academic/GradeEntities.cs b/src/Jiaowu.Api/Domain/Academic/GradeEntities.cs index 6b98a63..020d7c9 100644 --- a/src/Jiaowu.Api/Domain/Academic/GradeEntities.cs +++ b/src/Jiaowu.Api/Domain/Academic/GradeEntities.cs @@ -2,8 +2,14 @@ using Jiaowu.Api.Domain.Common; namespace Jiaowu.Api.Domain.Academic; -public sealed class GradeSheet : EntityBase +public sealed class GradeSheet : EntityBase, IArchivable { + public bool IsArchived { get; set; } + public DateTime? ArchivedAt { get; set; } + public Guid? ArchivedByUserId { get; set; } + [global::System.ComponentModel.DataAnnotations.MaxLength(500)] + public string? ArchiveReason { get; set; } + public Guid TeachingTaskId { get; set; } public TeachingTask? TeachingTask { get; set; } public decimal RegularWeight { get; set; } = 30; diff --git a/src/Jiaowu.Api/Domain/Academic/OrganizationEntities.cs b/src/Jiaowu.Api/Domain/Academic/OrganizationEntities.cs index 6b21cb7..8ac6860 100644 --- a/src/Jiaowu.Api/Domain/Academic/OrganizationEntities.cs +++ b/src/Jiaowu.Api/Domain/Academic/OrganizationEntities.cs @@ -84,8 +84,12 @@ public static class TeachingVenueNatureRules (value & ExperimentTeaching) != 0; } -public sealed class AcademicTerm : CatalogEntity +public sealed class AcademicTerm : CatalogEntity, IArchivable { + public Guid? ArchivedByUserId { get; set; } + [global::System.ComponentModel.DataAnnotations.MaxLength(500)] + public string? ArchiveReason { get; set; } + public required string AcademicYear { get; set; } public TermSeason Season { get; set; } public DateOnly StartDate { get; set; } diff --git a/src/Jiaowu.Api/Domain/Academic/PeopleAndCourseEntities.cs b/src/Jiaowu.Api/Domain/Academic/PeopleAndCourseEntities.cs index 61416ac..0c06284 100644 --- a/src/Jiaowu.Api/Domain/Academic/PeopleAndCourseEntities.cs +++ b/src/Jiaowu.Api/Domain/Academic/PeopleAndCourseEntities.cs @@ -2,8 +2,14 @@ using Jiaowu.Api.Domain.Common; namespace Jiaowu.Api.Domain.Academic; -public sealed class Teacher : EntityBase +public sealed class Teacher : EntityBase, IArchivable { + public bool IsArchived { get; set; } + public DateTime? ArchivedAt { get; set; } + public Guid? ArchivedByUserId { get; set; } + [global::System.ComponentModel.DataAnnotations.MaxLength(500)] + public string? ArchiveReason { get; set; } + public required string TeacherNumber { get; set; } public required string Name { get; set; } public Gender Gender { get; set; } @@ -19,8 +25,14 @@ public sealed class Teacher : EntityBase public Guid? UserId { get; set; } } -public sealed class Student : EntityBase +public sealed class Student : EntityBase, IArchivable { + public bool IsArchived { get; set; } + public DateTime? ArchivedAt { get; set; } + public Guid? ArchivedByUserId { get; set; } + [global::System.ComponentModel.DataAnnotations.MaxLength(500)] + public string? ArchiveReason { get; set; } + public required string StudentNumber { get; set; } public required string Name { get; set; } public Gender Gender { get; set; } @@ -53,8 +65,14 @@ public sealed class Student : EntityBase public Guid? UserId { get; set; } } -public sealed class Course : CatalogEntity +public sealed class Course : CatalogEntity, IArchivable { + public bool IsArchived { get; set; } + public DateTime? ArchivedAt { get; set; } + public Guid? ArchivedByUserId { get; set; } + [global::System.ComponentModel.DataAnnotations.MaxLength(500)] + public string? ArchiveReason { get; set; } + public Guid CollegeId { get; set; } public College? College { get; set; } public Guid? CourseCategoryId { get; set; } diff --git a/src/Jiaowu.Api/Infrastructure/Academic/ArchiveService.cs b/src/Jiaowu.Api/Infrastructure/Academic/ArchiveService.cs new file mode 100644 index 0000000..a8338e5 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Academic/ArchiveService.cs @@ -0,0 +1,144 @@ +using System.Data; +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Infrastructure.Auth; +using Jiaowu.Api.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace Jiaowu.Api.Infrastructure.Academic; + +public sealed record ArchiveCheck(string Message, string? Link = null); +public sealed record ArchivePreview(Guid Id, string Name, bool IsArchived, + DateTime? ArchivedAt, string? ArchiveReason, IReadOnlyList Blockers, + IReadOnlyList Notices); + +public sealed class ArchiveService(AppDbContext db, ICurrentUserDataScope user) +{ + public async Task<(IArchivable Entity, ArchivePreview Preview)?> InspectAsync( + string kind, Guid id, bool restoring, CancellationToken ct) + { + var scope = user.Current; + var school = scope.IsInRole(SystemRoles.SuperAdmin) || scope.IsInRole(SystemRoles.AcademicAdmin); + if (!school && !scope.IsInRole(SystemRoles.CollegeAdmin)) return null; + IArchivable? entity = null; + string name = ""; + var blockers = new List(); + var notices = new List(); + switch (kind) + { + case "teachers": + var teacher = await db.Teachers.FirstOrDefaultAsync(x => x.Id == id, ct); + if (teacher is null || !scope.CanAccessCollege(teacher.CollegeId)) return null; + entity = teacher; name = teacher.Name; + if (!restoring && !teacher.IsArchived) + { + if (teacher.Status is not (TeacherStatus.Retired or TeacherStatus.Departed)) + blockers.Add(new("仅退休或离职教师可归档。", "/teachers")); + if (await db.TeachingTasks.AnyAsync(x => x.Status != TeachingTaskStatus.Closed && x.Teachers.Any(t => t.TeacherId == id), ct)) + blockers.Add(new("仍有未结课教学任务,请先结课或调整任课教师。", "/teaching-tasks")); + if (await db.GradeSheets.AnyAsync(x => x.Status != GradeSheetStatus.Published && x.TeachingTask!.Teachers.Any(t => t.TeacherId == id), ct)) + blockers.Add(new("仍有未发布成绩。", "/grades")); + } + notices.Add(new("归档和恢复不会改变任职状态或登录账号状态。")); + break; + case "students": + var student = await db.Students.Include(x => x.AdministrativeClass).ThenInclude(x => x!.Major).FirstOrDefaultAsync(x => x.Id == id, ct); + if (student is null || !scope.CanAccessCollege(student.AdministrativeClass!.Major!.CollegeId)) return null; + entity = student; name = student.Name; + if (!restoring && !student.IsArchived) + { + if (student.Status is not (StudentStatus.Graduated or StudentStatus.Withdrawn)) + blockers.Add(new("仅毕业或退学学生可归档。", "/students")); + if (await db.CourseEnrollments.AnyAsync(x => x.StudentId == id && (x.Status == CourseEnrollmentStatus.Enrolled || x.Status == CourseEnrollmentStatus.Waitlisted) && x.CourseSelectionOffering!.TeachingTask!.Status != TeachingTaskStatus.Closed, ct)) + blockers.Add(new("仍有未结课选课或候补记录。", "/course-selections")); + if (await db.GradeRecords.AnyAsync(x => x.StudentId == id && x.GradeSheet!.Status != GradeSheetStatus.Published, ct)) + blockers.Add(new("仍有未发布成绩。", "/grades")); + if (await db.StudentStatusChanges.AnyAsync(x => x.StudentId == id && (x.State == StudentStatusChangeState.Submitted || x.State == StudentStatusChangeState.CounselorApproved || x.State == StudentStatusChangeState.CollegeApproved), ct)) + blockers.Add(new("仍有待审批学籍异动。", "/student-status-changes")); + if (await db.GraduationClearanceRecords.AnyAsync(x => x.StudentId == id && x.Status == GraduationClearanceRecordStatus.Pending && x.GraduationClearanceItem!.IsRequired, ct)) + blockers.Add(new("仍有未完成的必办离校事项。", "/graduation-clearance")); + if (await db.CourseExemptions.AnyAsync(x => x.StudentId == id && x.Status == ApprovalStatus.Submitted, ct) || + await db.DeferredExams.AnyAsync(x => x.StudentId == id && x.Status == ApprovalStatus.Submitted, ct) || + await db.CourseSubstitutions.AnyAsync(x => x.StudentId == id && x.Status == ApprovalStatus.Submitted, ct) || + await db.GradeModifications.AnyAsync(x => x.GradeRecord!.StudentId == id && (x.Status == GradeModificationStatus.TeacherSubmitted || x.Status == GradeModificationStatus.CollegeApproved), ct)) + blockers.Add(new("仍有待审批的课程或成绩申请。", "/approvals")); + } + notices.Add(new("保留学籍状态、成绩单及毕业审核依据;账号状态单独管理。")); + break; + case "courses": + var course = await db.Courses.FirstOrDefaultAsync(x => x.Id == id, ct); + if (course is null || !CourseMaintenancePolicy.CanManage(scope, course.CollegeId, course.Nature)) return null; + entity = course; name = course.Name; + if (!restoring && !course.IsArchived) + { + if (await db.TeachingTasks.AnyAsync(x => x.CourseId == id && x.Status != TeachingTaskStatus.Closed, ct)) + blockers.Add(new("仍有草稿或未结课教学任务。", "/teaching-tasks")); + if (await db.CurriculumCourses.AnyAsync(x => x.CourseId == id && x.CurriculumModule!.CurriculumPlan!.Status == CurriculumPlanStatus.Published && db.Students.Any(s => s.Status == StudentStatus.Active && s.EnrollmentYear == x.CurriculumModule.CurriculumPlan.EffectiveGrade && s.AdministrativeClass!.MajorId == x.CurriculumModule.CurriculumPlan.MajorId), ct)) + blockers.Add(new("在读学生适用的已发布培养方案仍使用该课程。", "/curriculum")); + } + notices.Add(new("归档后不可用于新开课;历史教学、成绩及培养方案引用保留,启停状态不变。")); + break; + case "grades": + var sheet = await db.GradeSheets.Include(x => x.TeachingTask).ThenInclude(x => x!.Course).FirstOrDefaultAsync(x => x.Id == id, ct); + if (sheet is null || !scope.CanAccessCollege(sheet.TeachingTask!.Course!.CollegeId)) return null; + entity = sheet; name = sheet.TeachingTask!.Name; + if (!restoring && !sheet.IsArchived) + { + if (sheet.Status != GradeSheetStatus.Published) blockers.Add(new("仅已发布成绩单可归档。", "/grades")); + if (await db.GradeModifications.AnyAsync(x => x.GradeRecord!.GradeSheetId == id && (x.Status == GradeModificationStatus.TeacherSubmitted || x.Status == GradeModificationStatus.CollegeApproved), ct)) + blockers.Add(new("仍有待审批成绩更正。", "/approvals")); + } + notices.Add(new("归档不改变已发布状态、绩点或毕业审核结果;补考和成绩更正继续走原有流程。")); + break; + case "terms": + if (!school) return null; + var term = await db.AcademicTerms.FirstOrDefaultAsync(x => x.Id == id, ct); + if (term is null) return null; + entity = term; name = term.Name; + if (!restoring && !term.IsArchived) + { + if (term.IsCurrent) blockers.Add(new("当前学期不能归档,请先切换当前学期。", "/base-data")); + if (await db.CourseSelectionRounds.AnyAsync(x => x.AcademicTermId == id && x.Status == CourseSelectionRoundStatus.Open, ct)) + blockers.Add(new("仍有开放的选课批次。", "/course-selections")); + if (await db.TeachingTasks.AnyAsync(x => x.AcademicTermId == id && x.Status != TeachingTaskStatus.Closed, ct)) + blockers.Add(new("仍有草稿或未结课教学任务。", "/teaching-tasks")); + if (await db.GradeSheets.AnyAsync(x => x.TeachingTask!.AcademicTermId == id && x.Status != GradeSheetStatus.Published, ct)) + blockers.Add(new("仍有未发布成绩单。", "/grades")); + if (await db.TeachingTasks.AnyAsync(x => x.AcademicTermId == id && x.Status == TeachingTaskStatus.Closed && !db.GradeSheets.Any(s => s.TeachingTaskId == x.Id), ct)) + blockers.Add(new("有已结课教学任务尚未建立成绩单。", "/grades")); + } + notices.Add(new("不会批量归档人员或课程,也不会冻结补考和成绩更正;恢复后不会自动成为当前学期。")); + break; + } + return entity is null ? null : (entity, new(id, name, entity.IsArchived, entity.ArchivedAt, entity.ArchiveReason, blockers, notices)); + } + + public Task ChangeAsync(string kind, Guid id, bool archived, string reason, CancellationToken ct) => + db.ExecuteInRetriableTransactionAsync(async transaction => + { + db.ChangeTracker.Clear(); + var item = await InspectAsync(kind, id, !archived, ct); + if (item is null) return new(id, false, "记录不存在或无权操作。"); + if (item.Value.Preview.Blockers.Count > 0) + return new(id, false, string.Join(";", item.Value.Preview.Blockers.Select(x => x.Message))); + if (item.Value.Entity.IsArchived != archived) + { + Apply(kind, id, item.Value.Entity, archived, reason); + await db.SaveChangesAsync(ct); + } + await transaction.CommitAsync(ct); + return new(id, true, archived ? "已归档" : "已恢复"); + }, ct, IsolationLevel.Serializable); + + public void Apply(string kind, Guid id, IArchivable entity, bool archived, string reason) + { + entity.IsArchived = archived; + entity.ArchivedAt = archived ? DateTime.UtcNow : null; + entity.ArchivedByUserId = archived ? user.Current.UserId : null; + entity.ArchiveReason = archived ? reason.Trim() : null; + db.ArchiveEvents.Add(new ArchiveEvent { Kind = kind, EntityId = id, IsArchived = archived, + ActorUserId = user.Current.UserId, ActorName = user.Current.DisplayName, Reason = reason.Trim() }); + } +} + +public sealed record ArchiveOperationResult(Guid Id, bool Success, string Message); diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs index 62bc83c..c750a2d 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs @@ -209,9 +209,23 @@ public sealed class AppDbContext(DbContextOptions options) .HaveColumnType("datetime(6)"); } + public DbSet ArchiveEvents => Set(); + protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); + builder.Entity().HasIndex(x => new { x.Kind, x.EntityId, x.CreatedAt }); + builder.Entity().HasIndex(x => x.IsArchived); + builder.Entity().Property(x => x.IsArchived).IsConcurrencyToken(); + builder.Entity().HasIndex(x => x.IsArchived); + builder.Entity().Property(x => x.IsArchived).IsConcurrencyToken(); + builder.Entity().HasIndex(x => x.IsArchived); + builder.Entity().Property(x => x.IsArchived).IsConcurrencyToken(); + builder.Entity().HasIndex(x => x.IsArchived); + builder.Entity().Property(x => x.IsArchived).IsConcurrencyToken(); + builder.Entity().HasIndex(x => x.IsArchived); + builder.Entity().Property(x => x.IsArchived).IsConcurrencyToken(); + builder.Entity(entity => { diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs index 6dd2148..edf876a 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs @@ -956,6 +956,21 @@ public sealed class DevelopmentSqliteMigrator( campusWalletAccountsExist ? [] : CampusWalletBalanceStatements, cancellationToken); var tuitionExists = await db.Database.SqlQueryRaw("SELECT COUNT(*) AS \"Value\" FROM sqlite_master WHERE type = 'table' AND name = 'TuitionBills'").AnyAsync(value => value > 0, cancellationToken); await ApplyMigrationAsync(TuitionBillingMigration, tuitionExists ? [] : TuitionBillingStatements, cancellationToken); + var archiveStatements = new List(); + foreach (var table in new[] { "Teachers", "Students", "Courses", "GradeSheets", "AcademicTerms" }) + { + foreach (var (column, type) in new[] { ("IsArchived", "INTEGER NOT NULL DEFAULT 0"), ("ArchivedAt", "TEXT NULL"), ("ArchivedByUserId", "TEXT NULL"), ("ArchiveReason", "TEXT NULL") }) + { + var exists = await db.Database.SqlQuery( + $"SELECT COUNT(*) AS Value FROM pragma_table_info({table}) WHERE name = {column}") + .AnyAsync(x => x > 0, cancellationToken); + if (!exists) archiveStatements.Add($"ALTER TABLE {table} ADD COLUMN {column} {type};"); + } + archiveStatements.Add($"CREATE INDEX IF NOT EXISTS IX_{table}_IsArchived ON {table} (IsArchived);"); + } + archiveStatements.Add("CREATE TABLE IF NOT EXISTS ArchiveEvents (Id TEXT NOT NULL PRIMARY KEY, Kind TEXT NOT NULL, EntityId TEXT NOT NULL, IsArchived INTEGER NOT NULL, ActorUserId TEXT NOT NULL, ActorName TEXT NULL, Reason TEXT NOT NULL, CreatedAt TEXT NOT NULL, UpdatedAt TEXT NOT NULL);"); + archiveStatements.Add("CREATE INDEX IF NOT EXISTS IX_ArchiveEvents_Kind_EntityId_CreatedAt ON ArchiveEvents (Kind, EntityId, CreatedAt);"); + await ApplyMigrationAsync("20260905_unified_archiving", archiveStatements, cancellationToken); } private async Task ApplyMigrationAsync( diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260905093721_UnifiedArchiving.Designer.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260905093721_UnifiedArchiving.Designer.cs new file mode 100644 index 0000000..531a432 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260905093721_UnifiedArchiving.Designer.cs @@ -0,0 +1,8805 @@ +// +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("20260905093721_UnifiedArchiving")] + partial class UnifiedArchiving + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AcademicCalendarEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("AudienceRoles") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("EndsOn") + .HasColumnType("date"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("IsOffDay") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("ReminderDays") + .HasColumnType("int"); + + b.Property("Source") + .HasColumnType("int"); + + b.Property("SourceYear") + .HasColumnType("int"); + + b.Property("StartsOn") + .HasColumnType("date"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId", "StartsOn"); + + b.HasIndex("IsEnabled", "StartsOn"); + + b.HasIndex("AcademicTermId", "Source", "SourceYear", "StartsOn"); + + b.ToTable("AcademicCalendarEvents"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AcademicRiskCase", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AssignedCounselorUserId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FollowUpDueDate") + .HasColumnType("date"); + + b.Property("ReviewDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("TaskCompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WarningRecordId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("WarningRecordId") + .IsUnique(); + + b.HasIndex("StudentId", "Status"); + + b.HasIndex("AssignedCounselorUserId", "Status", "FollowUpDueDate"); + + b.ToTable("AcademicRiskCases"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AcademicRiskCaseEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicRiskCaseId") + .HasColumnType("char(36)"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("OccurredAt") + .HasColumnType("datetime(6)"); + + b.Property("RecordedByUserId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("RecordedByUserId"); + + b.HasIndex("AcademicRiskCaseId", "OccurredAt"); + + b.ToTable("AcademicRiskCaseEntries"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AcademicTerm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicYear") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ArchiveReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ArchivedAt") + .HasColumnType("datetime(6)"); + + b.Property("ArchivedByUserId") + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("IsArchived") + .IsConcurrencyToken() + .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.ArchiveEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActorName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ActorUserId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EntityId") + .HasColumnType("char(36)"); + + b.Property("IsArchived") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Kind", "EntityId", "CreatedAt"); + + b.ToTable("ArchiveEvents"); + }); + + 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("CurrentGeneration") + .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("TotalGenerations") + .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.CampusPaymentOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("ApplicationId") + .HasColumnType("char(36)"); + + b.Property("ConfirmedAt") + .HasColumnType("datetime(6)"); + + b.Property("ConfirmedByUserId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("OrderNumber") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("PaymentMethod") + .HasColumnType("int"); + + b.Property("ReferenceNumber") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId") + .IsUnique(); + + b.HasIndex("ConfirmedByUserId"); + + b.HasIndex("OrderNumber") + .IsUnique(); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("CampusPaymentOrders"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CampusRegistrationApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApplicantNote") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("CancelledAt") + .HasColumnType("datetime(6)"); + + b.Property("ConfirmedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + 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("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ReviewedByUserId"); + + b.HasIndex("StudentId"); + + b.HasIndex("ProjectId", "StudentId") + .IsUnique(); + + b.HasIndex("ProjectId", "Status", "CreatedAt"); + + b.ToTable("CampusRegistrationApplications"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CampusRegistrationProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Capacity") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CollegeId") + .HasColumnType("char(36)"); + + b.Property("ContactName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ContactPhone") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByUserId") + .HasColumnType("char(36)"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("FeeAmount") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RegistrationEndsAt") + .HasColumnType("datetime(6)"); + + b.Property("RegistrationStartsAt") + .HasColumnType("datetime(6)"); + + b.Property("RequiresReview") + .HasColumnType("tinyint(1)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("CollegeId", "Status", "RegistrationEndsAt"); + + b.ToTable("CampusRegistrationProjects"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CampusWalletAccount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Balance") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("StudentId") + .IsUnique(); + + b.ToTable("CampusWalletAccounts"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CampusWalletTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("BalanceAfter") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("OperatedByUserId") + .HasColumnType("char(36)"); + + b.Property("PaymentOrderId") + .HasColumnType("char(36)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WalletAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OperatedByUserId"); + + b.HasIndex("PaymentOrderId") + .IsUnique(); + + b.HasIndex("WalletAccountId", "CreatedAt"); + + b.ToTable("CampusWalletTransactions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Classroom", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AvailabilityStatus") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(1); + + 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("Floor") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(1); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RoomType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("TeachingVenueNature") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("BuildingId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Classrooms"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ClassroomEquipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsAvailable") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("varchar(80)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ClassroomId", "IsAvailable"); + + b.ToTable("ClassroomEquipment"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ClassroomRepairTicket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AssignedAt") + .HasColumnType("datetime(6)"); + + b.Property("AssignedByUserId") + .HasColumnType("char(36)"); + + b.Property("AssignedToUserId") + .HasColumnType("char(36)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ClassroomEquipmentId") + .HasColumnType("char(36)"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EscalationTeacherNotifiedAt") + .HasColumnType("datetime(6)"); + + b.Property("FaultItem") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("varchar(80)"); + + b.Property("InitialTeacherNotifiedAt") + .HasColumnType("datetime(6)"); + + b.Property("RepairResult") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("ReporterContact") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ReporterName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ReporterUserId") + .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("AssignedByUserId"); + + b.HasIndex("AssignedToUserId"); + + b.HasIndex("ClassroomEquipmentId"); + + b.HasIndex("ReporterUserId"); + + b.HasIndex("ClassroomId", "Status"); + + b.HasIndex("Status", "EscalationTeacherNotifiedAt"); + + b.ToTable("ClassroomRepairTickets"); + }); + + 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("ArchiveReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ArchivedAt") + .HasColumnType("datetime(6)"); + + b.Property("ArchivedByUserId") + .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("IsArchived") + .IsConcurrencyToken() + .HasColumnType("tinyint(1)"); + + 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("IsArchived"); + + 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("WaitlistResolutionReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("WaitlistResolvedAt") + .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.CourseEquivalencyRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EffectiveGrade") + .HasColumnType("int"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("MajorId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("OriginalCourseId") + .HasColumnType("char(36)"); + + b.Property("SubstituteCourseId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("MajorId"); + + b.HasIndex("SubstituteCourseId"); + + b.HasIndex("IsEnabled", "OriginalCourseId"); + + b.HasIndex("OriginalCourseId", "SubstituteCourseId", "MajorId", "EffectiveGrade") + .IsUnique(); + + b.ToTable("CourseEquivalencyRules"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseExemption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewedByUserId") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("Status", "CreatedAt"); + + b.HasIndex("StudentId", "TeachingTaskId") + .IsUnique(); + + b.ToTable("CourseExemptions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGradeStatistic", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("AverageScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("Below60Count") + .HasColumnType("int"); + + b.Property("CalculatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("From60To69Count") + .HasColumnType("int"); + + b.Property("From70To79Count") + .HasColumnType("int"); + + b.Property("From80To89Count") + .HasColumnType("int"); + + b.Property("From90To100Count") + .HasColumnType("int"); + + b.Property("HighestScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("LowestScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("PassRate") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("PassedCount") + .HasColumnType("int"); + + b.Property("Scope") + .HasColumnType("int"); + + b.Property("ScopeEntityId") + .HasColumnType("char(36)"); + + b.Property("StudentCount") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId", "Scope", "ScopeEntityId"); + + b.HasIndex("CourseId", "AcademicTermId", "Scope", "ScopeEntityId") + .IsUnique() + .HasDatabaseName("UX_CourseGradeStatistics_Scope"); + + b.ToTable("CourseGradeStatistics"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGradeStatisticsRefreshJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("GradeSheetId") + .HasColumnType("char(36)"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GradeSheetId"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("CourseGradeStatisticsRefreshJobs"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGradeStatisticsRefreshSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BatchSize") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IntervalSeconds") + .HasColumnType("int"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LastRunAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("CourseGradeStatisticsRefreshSettings"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("CourseGroups"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGroupCourse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CourseGroupId") + .HasColumnType("char(36)"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.HasIndex("CourseGroupId", "CourseId") + .IsUnique(); + + b.ToTable("CourseGroupCourses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CoursePrerequisite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("PrerequisiteCourseId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PrerequisiteCourseId"); + + b.HasIndex("CourseId", "PrerequisiteCourseId") + .IsUnique(); + + b.ToTable("CoursePrerequisites"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionOffering", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Capacity") + .HasColumnType("int"); + + b.Property("CourseSelectionRoundId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsOpenToAll") + .HasColumnType("tinyint(1)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("CourseSelectionRoundId", "TeachingTaskId") + .IsUnique(); + + b.ToTable("CourseSelectionOfferings"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionRound", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndsAt") + .HasColumnType("datetime(6)"); + + b.Property("MaxCourseCount") + .HasColumnType("int"); + + b.Property("MaxCredits") + .HasPrecision(6, 1) + .HasColumnType("decimal(6,1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Scope") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(1); + + 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("CourseEquivalencyRuleId") + .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("CourseEquivalencyRuleId"); + + 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("ProjectIdsJson") + .HasColumnType("longtext"); + + 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("ProjectIdsJson") + .HasColumnType("longtext"); + + 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.ExperimentCourseGrade", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("PublishedProjectCount") + .HasColumnType("int"); + + b.Property("RefreshedAt") + .HasColumnType("datetime(6)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("TotalWeight") + .HasPrecision(8, 1) + .HasColumnType("decimal(8,1)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WeightedAverageScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.HasKey("Id"); + + b.HasIndex("StudentId"); + + b.HasIndex("TeachingTaskId", "StudentId") + .IsUnique(); + + b.ToTable("ExperimentCourseGrades"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentGradeSheetId") + .HasColumnType("char(36)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("varchar(60)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Weight") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.HasKey("Id"); + + b.HasIndex("ExperimentGradeSheetId", "SortOrder"); + + b.ToTable("ExperimentGradeItems"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItemScore", b => + { + b.Property("ExperimentGradeRecordId") + .HasColumnType("char(36)"); + + b.Property("ExperimentGradeItemId") + .HasColumnType("char(36)"); + + b.Property("Comment") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Score") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.HasKey("ExperimentGradeRecordId", "ExperimentGradeItemId"); + + b.HasIndex("ExperimentGradeItemId"); + + b.ToTable("ExperimentGradeItemScores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentGradeSheetId") + .HasColumnType("char(36)"); + + b.Property("ExperimentSessionId") + .HasColumnType("char(36)"); + + b.Property("IsLate") + .HasColumnType("tinyint(1)"); + + b.Property("IsPassed") + .HasColumnType("tinyint(1)"); + + b.Property("ParticipationStatus") + .HasColumnType("int"); + + b.Property("SafetyViolation") + .HasColumnType("tinyint(1)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("SubmissionReference") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TeacherComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("TotalScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ExperimentSessionId"); + + b.HasIndex("StudentId"); + + b.HasIndex("ExperimentGradeSheetId", "StudentId") + .IsUnique(); + + b.ToTable("ExperimentGradeRecords"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContributionWeight") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentProjectId") + .HasColumnType("char(36)"); + + b.Property("PassScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ExperimentProjectId") + .IsUnique(); + + b.ToTable("ExperimentGradeSheets"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ArrangementMode") + .HasColumnType("int"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Requirements") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("ScheduleEntryId") + .HasColumnType("char(36)"); + + b.Property("ScheduleWeek") + .HasColumnType("int"); + + b.Property("SelectionEndsAt") + .HasColumnType("datetime(6)"); + + b.Property("SelectionStartsAt") + .HasColumnType("datetime(6)"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ScheduleEntryId"); + + b.HasIndex("Status", "StartDate", "EndDate"); + + b.HasIndex("TeachingTaskId", "Code", "ScheduleEntryId", "ScheduleWeek") + .IsUnique(); + + b.ToTable("ExperimentProjects"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CancelledAt") + .HasColumnType("datetime(6)"); + + b.Property("Capacity") + .HasColumnType("int"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentProjectId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("ReservedCount") + .HasColumnType("int"); + + b.Property("SessionDate") + .HasColumnType("date"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ExperimentProjectId", "SessionDate", "StartPeriod"); + + b.HasIndex("ClassroomId", "SessionDate", "Status", "StartPeriod"); + + b.ToTable("ExperimentSessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentSessionInstructor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentSessionId") + .HasColumnType("char(36)"); + + b.Property("TeacherId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("TeacherId"); + + b.HasIndex("ExperimentSessionId", "TeacherId") + .IsUnique(); + + b.ToTable("ExperimentSessionInstructors"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeAnalyticsComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AuthorUserId") + .HasColumnType("char(36)"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsAnonymous") + .HasColumnType("tinyint(1)"); + + b.Property("ParentCommentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorUserId"); + + b.HasIndex("CourseId", "CreatedAt", "Id"); + + b.HasIndex("ParentCommentId", "CreatedAt", "Id"); + + b.ToTable("GradeAnalyticsComments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeAnalyticsCommentReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CommentId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("CommentId", "UserId", "Type") + .IsUnique(); + + b.ToTable("GradeAnalyticsCommentReactions"); + }); + + 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("ArchiveReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ArchivedAt") + .HasColumnType("datetime(6)"); + + b.Property("ArchivedByUserId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FinalWeight") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("IsArchived") + .IsConcurrencyToken() + .HasColumnType("tinyint(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("IsArchived"); + + 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", "CreatedAt", "Id"); + + b.HasIndex("UserId", "Category", "CreatedAt", "Id"); + + b.HasIndex("UserId", "IsRead", "CreatedAt", "Id"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DocumentNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvalidatedAt") + .HasColumnType("datetime(6)"); + + b.Property("InvalidatedByUserId") + .HasColumnType("char(36)"); + + b.Property("InvalidationReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IssuedAt") + .HasColumnType("datetime(6)"); + + b.Property("IssuedByUserId") + .HasColumnType("char(36)"); + + b.Property("PdfContent") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("PdfSha256") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Purpose") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ReissuedFromDocumentId") + .HasColumnType("char(36)"); + + b.Property("SnapshotJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("VerificationCodeHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentNumber") + .IsUnique(); + + b.HasIndex("InvalidatedByUserId"); + + b.HasIndex("IssuedByUserId"); + + b.HasIndex("ReissuedFromDocumentId") + .IsUnique(); + + b.HasIndex("VerificationCodeHash") + .IsUnique(); + + b.HasIndex("Status", "IssuedAt"); + + b.HasIndex("StudentId", "IssuedAt"); + + b.ToTable("OfficialDocuments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocumentDownload", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DownloadedByUserId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("OfficialDocumentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("DownloadedByUserId", "CreatedAt"); + + b.HasIndex("OfficialDocumentId", "CreatedAt"); + + b.ToTable("OfficialDocumentDownloads"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OtherExamBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExamCode") + .HasMaxLength(60) + .HasColumnType("varchar(60)"); + + b.Property("ExamDate") + .HasColumnType("date"); + + b.Property("LevelOptions") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("MaxScore") + .HasPrecision(8, 2) + .HasColumnType("decimal(8,2)"); + + b.Property("MetricKind") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("Organizer") + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("PublicationCount") + .HasColumnType("int"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Status", "ExamDate"); + + b.ToTable("OtherExamBatches"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OtherExamResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsPassed") + .HasColumnType("tinyint(1)"); + + b.Property("Level") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("OtherExamBatchId") + .HasColumnType("char(36)"); + + b.Property("Score") + .HasPrecision(8, 2) + .HasColumnType("decimal(8,2)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("StudentId", "OtherExamBatchId"); + + b.HasIndex("OtherExamBatchId", "StudentId", "AttemptNumber") + .IsUnique(); + + b.ToTable("OtherExamResults"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.PublishedScheduleOccurrence", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DayOfWeek") + .HasColumnType("int"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("ScheduleEntryId") + .HasColumnType("char(36)"); + + b.Property("SchedulePlanId") + .HasColumnType("char(36)"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Week") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ClassroomId"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("ScheduleEntryId", "Week") + .IsUnique(); + + b.HasIndex("AcademicTermId", "TeachingTaskId", "Week"); + + b.HasIndex("AcademicTermId", "Week", "DayOfWeek", "StartPeriod", "ClassroomId"); + + b.HasIndex("SchedulePlanId", "ClassroomId", "Week", "DayOfWeek", "StartPeriod"); + + b.ToTable("PublishedScheduleOccurrences"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ScheduleEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DayOfWeek") + .HasColumnType("int"); + + b.Property("EndWeek") + .HasColumnType("int"); + + b.Property("Kind") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(1); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("SchedulePlanId") + .HasColumnType("char(36)"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("StartWeek") + .HasColumnType("int"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WeekPattern") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ClassroomId"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("SchedulePlanId", "DayOfWeek", "StartPeriod"); + + b.ToTable("ScheduleEntries"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ScheduleOptimizationSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("ClassGapWeight") + .HasColumnType("int"); + + b.Property("ClassSameDayWeight") + .HasColumnType("int"); + + b.Property("ConsecutiveClassWeight") + .HasColumnType("int"); + + b.Property("CoursePreferenceWeight") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EarlyPeriodPenalty") + .HasColumnType("int"); + + b.Property("GeneticGenerationCount") + .HasColumnType("int"); + + b.Property("GeneticMutationRatePercent") + .HasColumnType("int"); + + b.Property("GeneticPopulationSize") + .HasColumnType("int"); + + b.Property("LatePeriodPenalty") + .HasColumnType("int"); + + b.Property("RoomCapacityWasteWeight") + .HasColumnType("int"); + + b.Property("TeacherSameDayWeight") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId") + .IsUnique(); + + b.ToTable("ScheduleOptimizationSettings"); + }); + + 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("ArchiveReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ArchivedAt") + .HasColumnType("datetime(6)"); + + b.Property("ArchivedByUserId") + .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("IsArchived") + .IsConcurrencyToken() + .HasColumnType("tinyint(1)"); + + 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("IsArchived"); + + b.HasIndex("StudentNumber") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("AdministrativeClassId", "Status"); + + b.ToTable("Students"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.StudentAcademicStatistic", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AverageScore") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("CalculatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CourseCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Gpa") + .HasPrecision(4, 2) + .HasColumnType("decimal(4,2)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WeightedAverageScore") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CalculatedAt"); + + b.HasIndex("StudentId") + .IsUnique(); + + b.ToTable("StudentAcademicStatistics"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.StudentAcademicTermStatistic", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("AverageScore") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("CalculatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CourseCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Gpa") + .HasPrecision(4, 2) + .HasColumnType("decimal(4,2)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WeightedAverageScore") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId"); + + b.HasIndex("StudentId", "AcademicTermId") + .IsUnique(); + + b.ToTable("StudentAcademicTermStatistics"); + }); + + 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("ArchiveReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ArchivedAt") + .HasColumnType("datetime(6)"); + + b.Property("ArchivedByUserId") + .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("IsArchived") + .IsConcurrencyToken() + .HasColumnType("tinyint(1)"); + + 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("IsArchived"); + + 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.TeachingImprovementPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("Actions") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DueDate") + .HasColumnType("date"); + + b.Property("ProblemSummary") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TeacherId") + .HasColumnType("char(36)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("TeacherId"); + + b.HasIndex("TeachingTaskId", "TeacherId") + .IsUnique(); + + b.HasIndex("AcademicTermId", "TeacherId", "Status"); + + b.ToTable("TeachingImprovementPlans"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingImprovementReview", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Comment") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsApproved") + .HasColumnType("tinyint(1)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewerUserId") + .HasColumnType("char(36)"); + + b.Property("TeachingImprovementPlanId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("TeachingImprovementPlanId", "ReviewedAt"); + + b.ToTable("TeachingImprovementReviews"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingObservationEvaluation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EvaluatorUserId") + .HasColumnType("char(36)"); + + b.Property("Issues") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Recommendation") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Score") + .HasColumnType("int"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Strengths") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId", "TeachingTaskId", "Source"); + + b.HasIndex("TeachingTaskId", "EvaluatorUserId", "Source") + .IsUnique(); + + b.ToTable("TeachingObservationEvaluations"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("Capacity") + .HasColumnType("int"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndWeek") + .HasColumnType("int"); + + b.Property("GenerationBatchCode") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("SchedulingMode") + .HasColumnType("int"); + + b.Property("StartWeek") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TaskNumber") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WeeklyHours") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.HasIndex("TaskNumber") + .IsUnique(); + + b.HasIndex("AcademicTermId", "Status"); + + b.ToTable("TeachingTasks"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskAllowedClassroom", b => + { + b.Property("TeachingTaskScheduleConstraintId") + .HasColumnType("char(36)"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.HasKey("TeachingTaskScheduleConstraintId", "ClassroomId"); + + b.HasIndex("ClassroomId"); + + b.ToTable("TeachingTaskAllowedClassrooms"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskAllowedExperimentClassroom", b => + { + b.Property("TeachingTaskScheduleConstraintId") + .HasColumnType("char(36)"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.HasKey("TeachingTaskScheduleConstraintId", "ClassroomId"); + + b.HasIndex("ClassroomId"); + + b.ToTable("TeachingTaskAllowedExperimentClassrooms"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskClass", b => + { + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("AdministrativeClassId") + .HasColumnType("char(36)"); + + b.HasKey("TeachingTaskId", "AdministrativeClassId"); + + b.HasIndex("AdministrativeClassId"); + + b.ToTable("TeachingTaskClasses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskGradeScoreBand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("LowerBound") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("StudentCount") + .HasColumnType("int"); + + b.Property("TeachingTaskGradeStatisticId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpperBound") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.HasKey("Id"); + + b.HasIndex("TeachingTaskGradeStatisticId", "SortOrder") + .IsUnique(); + + b.ToTable("TeachingTaskGradeScoreBands"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskGradeStatistic", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("AverageScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("CalculatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExcellentCount") + .HasColumnType("int"); + + b.Property("ExcellentRate") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("GradeSheetId") + .HasColumnType("char(36)"); + + b.Property("HighestScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("LowestScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("MedianScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("PassRate") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("PassedCount") + .HasColumnType("int"); + + b.Property("StandardDeviation") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("StudentCount") + .HasColumnType("int"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId"); + + b.HasIndex("GradeSheetId") + .IsUnique(); + + b.HasIndex("TeachingTaskId") + .IsUnique(); + + b.HasIndex("CourseId", "AcademicTermId"); + + b.ToTable("TeachingTaskGradeStatistics"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllowedDayOfWeeks") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("AllowedExperimentVenueNatures") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EarliestPeriod") + .HasColumnType("int"); + + b.Property("ExperimentRequiredBuildingId") + .HasColumnType("char(36)"); + + b.Property("ExperimentRequiredCampusId") + .HasColumnType("char(36)"); + + b.Property("LatestPeriod") + .HasColumnType("int"); + + b.Property("PreferredDayOfWeeks") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("PreferredEarliestPeriod") + .HasColumnType("int"); + + b.Property("PreferredLatestPeriod") + .HasColumnType("int"); + + b.Property("RequiredBuildingId") + .HasColumnType("char(36)"); + + b.Property("RequiredCampusId") + .HasColumnType("char(36)"); + + b.Property("RequiresClassroom") + .HasColumnType("tinyint(1)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ExperimentRequiredBuildingId"); + + b.HasIndex("ExperimentRequiredCampusId"); + + b.HasIndex("RequiredBuildingId"); + + b.HasIndex("RequiredCampusId"); + + b.HasIndex("TeachingTaskId") + .IsUnique(); + + b.ToTable("TeachingTaskScheduleConstraints"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskTeacher", b => + { + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("TeacherId") + .HasColumnType("char(36)"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)"); + + b.HasKey("TeachingTaskId", "TeacherId"); + + b.HasIndex("TeacherId"); + + b.ToTable("TeachingTaskTeachers"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TuitionBill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("AccommodationFee") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("AccommodationPaid") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("AdjustmentPaid") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("BasePaid") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("BaseTuition") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreditCount") + .HasPrecision(8, 2) + .HasColumnType("decimal(8,2)"); + + b.Property("CreditFee") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("CreditPaid") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("ReconciliationAdjustment") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("StudentId"); + + b.HasIndex("AcademicTermId", "StudentId") + .IsUnique(); + + b.ToTable("TuitionBills"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TuitionPayment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("ConfirmedAt") + .HasColumnType("datetime(6)"); + + b.Property("ConfirmedByUserId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Item") + .HasColumnType("int"); + + b.Property("Method") + .HasColumnType("int"); + + b.Property("ReferenceNumber") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("TuitionBillId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("TuitionBillId", "Item", "CreatedAt"); + + b.ToTable("TuitionPayments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TuitionTermSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreditUnitPrice") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("CreditsLockedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreditsLockedByUserId") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId") + .IsUnique(); + + b.ToTable("TuitionTermSettings"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.VenueDisplayAnnouncement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndsAt") + .HasColumnType("datetime(6)"); + + b.Property("IsPinned") + .HasColumnType("tinyint(1)"); + + b.Property("IsPublished") + .HasColumnType("tinyint(1)"); + + b.Property("IsUrgent") + .HasColumnType("tinyint(1)"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("Scope") + .HasColumnType("int"); + + b.Property("StartsAt") + .HasColumnType("datetime(6)"); + + b.Property("TargetId") + .HasColumnType("char(36)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("IsPublished", "StartsAt", "EndsAt"); + + b.HasIndex("Scope", "TargetId", "IsPublished"); + + b.ToTable("VenueDisplayAnnouncements"); + }); + + 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.PasskeyCredential", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CredentialId") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("varchar(2048)"); + + b.Property("CredentialIdHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("FriendlyName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("IsBackedUp") + .HasColumnType("tinyint(1)"); + + b.Property("IsBackupEligible") + .HasColumnType("tinyint(1)"); + + b.Property("LastUsedAt") + .HasColumnType("datetime(6)"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("SignatureCounter") + .HasColumnType("int unsigned"); + + b.Property("Transports") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("CredentialIdHash") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("PasskeyCredentials"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Identity.RefreshSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ClientType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("LastRefreshedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReplacedBySessionId") + .HasColumnType("char(36)"); + + b.Property("RevokedAt") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.HasIndex("UserId", "ExpiresAt"); + + b.ToTable("RefreshSessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.System.AppUpdateRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BundleContent") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("Channel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByUserName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("varchar(180)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("NativeVersion") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("PublishedByUserName") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ReleaseNotes") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Sha256") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Platform", "Channel", "NativeVersion", "Status"); + + b.HasIndex("Platform", "Channel", "NativeVersion", "Version") + .IsUnique(); + + b.ToTable("AppUpdateReleases"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.System.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Method") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("StatusCode") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("UserName") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.System.BackgroundJobOutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("JobId") + .HasColumnType("char(36)"); + + b.Property("JobKind") + .HasColumnType("int"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("LeaseExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("ProcessingAttempts") + .HasColumnType("int"); + + b.Property("ProcessingToken") + .HasColumnType("char(36)"); + + b.Property("PublishAttempts") + .HasColumnType("int"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("LeaseExpiresAt"); + + b.HasIndex("JobKind", "JobId") + .IsUnique(); + + b.HasIndex("State", "CompletedAt"); + + b.HasIndex("State", "CreatedAt"); + + b.ToTable("BackgroundJobOutboxMessages"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.System.SystemFeatureSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("SystemFeatureSettings"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AcademicCalendarEvent", 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.AcademicRiskCase", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "AssignedCounselorUser") + .WithMany() + .HasForeignKey("AssignedCounselorUserId") + .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.WarningRecord", "WarningRecord") + .WithOne("RiskCase") + .HasForeignKey("Jiaowu.Api.Domain.Academic.AcademicRiskCase", "WarningRecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AssignedCounselorUser"); + + b.Navigation("Student"); + + b.Navigation("WarningRecord"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AcademicRiskCaseEntry", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicRiskCase", "AcademicRiskCase") + .WithMany("Entries") + .HasForeignKey("AcademicRiskCaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "RecordedByUser") + .WithMany() + .HasForeignKey("RecordedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AcademicRiskCase"); + + b.Navigation("RecordedByUser"); + }); + + 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.CampusPaymentOrder", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.CampusRegistrationApplication", "Application") + .WithOne("PaymentOrder") + .HasForeignKey("Jiaowu.Api.Domain.Academic.CampusPaymentOrder", "ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "ConfirmedByUser") + .WithMany() + .HasForeignKey("ConfirmedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Application"); + + b.Navigation("ConfirmedByUser"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CampusRegistrationApplication", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.CampusRegistrationProject", "Project") + .WithMany("Applications") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "ReviewedByUser") + .WithMany() + .HasForeignKey("ReviewedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("ReviewedByUser"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CampusRegistrationProject", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.College", "College") + .WithMany() + .HasForeignKey("CollegeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("College"); + + b.Navigation("CreatedByUser"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CampusWalletAccount", 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.CampusWalletTransaction", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "OperatedByUser") + .WithMany() + .HasForeignKey("OperatedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.CampusPaymentOrder", "PaymentOrder") + .WithMany() + .HasForeignKey("PaymentOrderId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.CampusWalletAccount", "WalletAccount") + .WithMany("Transactions") + .HasForeignKey("WalletAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OperatedByUser"); + + b.Navigation("PaymentOrder"); + + b.Navigation("WalletAccount"); + }); + + 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.ClassroomEquipment", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Classroom"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ClassroomRepairTicket", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "AssignedByUser") + .WithMany() + .HasForeignKey("AssignedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "AssignedToUser") + .WithMany() + .HasForeignKey("AssignedToUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.ClassroomEquipment", "ClassroomEquipment") + .WithMany() + .HasForeignKey("ClassroomEquipmentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "ReporterUser") + .WithMany() + .HasForeignKey("ReporterUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignedByUser"); + + b.Navigation("AssignedToUser"); + + b.Navigation("Classroom"); + + b.Navigation("ClassroomEquipment"); + + b.Navigation("ReporterUser"); + }); + + 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.CourseEquivalencyRule", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Major", "Major") + .WithMany() + .HasForeignKey("MajorId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "OriginalCourse") + .WithMany() + .HasForeignKey("OriginalCourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "SubstituteCourse") + .WithMany() + .HasForeignKey("SubstituteCourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Major"); + + b.Navigation("OriginalCourse"); + + b.Navigation("SubstituteCourse"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseExemption", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Student"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGradeStatistic", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", null) + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", null) + .WithMany() + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGradeStatisticsRefreshJob", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GradeSheet", null) + .WithMany() + .HasForeignKey("GradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGroupCourse", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.CourseGroup", "CourseGroup") + .WithMany("Courses") + .HasForeignKey("CourseGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "Course") + .WithMany() + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Course"); + + b.Navigation("CourseGroup"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CoursePrerequisite", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "Course") + .WithMany("Prerequisites") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "PrerequisiteCourse") + .WithMany("RequiredByCourses") + .HasForeignKey("PrerequisiteCourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Course"); + + b.Navigation("PrerequisiteCourse"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionOffering", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.CourseSelectionRound", "CourseSelectionRound") + .WithMany("Offerings") + .HasForeignKey("CourseSelectionRoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CourseSelectionRound"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionRound", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AcademicTerm"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionRoundGrade", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.CourseSelectionRound", "CourseSelectionRound") + .WithMany("EligibleGrades") + .HasForeignKey("CourseSelectionRoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CourseSelectionRound"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSubstitution", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.CourseEquivalencyRule", "CourseEquivalencyRule") + .WithMany() + .HasForeignKey("CourseEquivalencyRuleId") + .OnDelete(DeleteBehavior.Restrict); + + 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("CourseEquivalencyRule"); + + 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.ExperimentCourseGrade", 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.Cascade) + .IsRequired(); + + b.Navigation("Student"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItem", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", "ExperimentGradeSheet") + .WithMany("Items") + .HasForeignKey("ExperimentGradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExperimentGradeSheet"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItemScore", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeItem", "ExperimentGradeItem") + .WithMany("Scores") + .HasForeignKey("ExperimentGradeItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", "ExperimentGradeRecord") + .WithMany("ItemScores") + .HasForeignKey("ExperimentGradeRecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExperimentGradeItem"); + + b.Navigation("ExperimentGradeRecord"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", "ExperimentGradeSheet") + .WithMany("Records") + .HasForeignKey("ExperimentGradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentSession", "ExperimentSession") + .WithMany() + .HasForeignKey("ExperimentSessionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ExperimentGradeSheet"); + + b.Navigation("ExperimentSession"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentProject", "ExperimentProject") + .WithOne("GradeSheet") + .HasForeignKey("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", "ExperimentProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExperimentProject"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ScheduleEntry", "ScheduleEntry") + .WithMany() + .HasForeignKey("ScheduleEntryId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ScheduleEntry"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentSession", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentProject", "ExperimentProject") + .WithMany("Sessions") + .HasForeignKey("ExperimentProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("ExperimentProject"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentSessionInstructor", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentSession", "ExperimentSession") + .WithMany("Instructors") + .HasForeignKey("ExperimentSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Teacher", "Teacher") + .WithMany() + .HasForeignKey("TeacherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ExperimentSession"); + + b.Navigation("Teacher"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeAnalyticsComment", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("AuthorUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", null) + .WithMany() + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.GradeAnalyticsComment", null) + .WithMany() + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeAnalyticsCommentReaction", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GradeAnalyticsComment", null) + .WithMany() + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItem", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GradeSheet", "GradeSheet") + .WithMany("Items") + .HasForeignKey("GradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GradeSheet"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItemScore", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GradeItem", "GradeItem") + .WithMany("Scores") + .HasForeignKey("GradeItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.GradeRecord", "GradeRecord") + .WithMany("ItemScores") + .HasForeignKey("GradeRecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GradeItem"); + + b.Navigation("GradeRecord"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeModification", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GradeRecord", "GradeRecord") + .WithMany() + .HasForeignKey("GradeRecordId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GradeRecord"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeRecord", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GradeSheet", "GradeSheet") + .WithMany("Records") + .HasForeignKey("GradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GradeSheet"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeSheet", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationAuditResult", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.CurriculumPlan", "CurriculumPlan") + .WithMany() + .HasForeignKey("CurriculumPlanId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.GraduationAuditBatch", "GraduationAuditBatch") + .WithMany("Results") + .HasForeignKey("GraduationAuditBatchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CurriculumPlan"); + + b.Navigation("GraduationAuditBatch"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceItem", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GraduationClearanceBatch", "GraduationClearanceBatch") + .WithMany("Items") + .HasForeignKey("GraduationClearanceBatchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GraduationClearanceBatch"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceRecord", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GraduationClearanceItem", "GraduationClearanceItem") + .WithMany("Records") + .HasForeignKey("GraduationClearanceItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GraduationClearanceItem"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Major", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.College", "College") + .WithMany() + .HasForeignKey("CollegeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("College"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamAutoJob", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.MakeupExamPlan", "MakeupExamPlan") + .WithMany() + .HasForeignKey("MakeupExamPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MakeupExamPlan"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamEnrollment", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.MakeupExamSession", "MakeupExamSession") + .WithMany("Enrollments") + .HasForeignKey("MakeupExamSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.DeferredExam", "SourceDeferredExam") + .WithMany() + .HasForeignKey("SourceDeferredExamId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.GradeRecord", "SourceGradeRecord") + .WithMany() + .HasForeignKey("SourceGradeRecordId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("MakeupExamSession"); + + b.Navigation("SourceDeferredExam"); + + b.Navigation("SourceGradeRecord"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamPlan", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AcademicTerm"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamSession", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.MakeupExamPlan", "MakeupExamPlan") + .WithMany("Sessions") + .HasForeignKey("MakeupExamPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Building", "RequiredBuilding") + .WithMany() + .HasForeignKey("RequiredBuildingId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("MakeupExamPlan"); + + b.Navigation("RequiredBuilding"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamSessionInvigilator", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.MakeupExamSession", "MakeupExamSession") + .WithMany("Invigilators") + .HasForeignKey("MakeupExamSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Teacher", "Teacher") + .WithMany() + .HasForeignKey("TeacherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("MakeupExamSession"); + + b.Navigation("Teacher"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Notification", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.MessageDispatch", "MessageDispatch") + .WithMany("Notifications") + .HasForeignKey("MessageDispatchId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("MessageDispatch"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocument", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "InvalidatedByUser") + .WithMany() + .HasForeignKey("InvalidatedByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "IssuedByUser") + .WithMany() + .HasForeignKey("IssuedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.OfficialDocument", "ReissuedFromDocument") + .WithMany() + .HasForeignKey("ReissuedFromDocumentId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("InvalidatedByUser"); + + b.Navigation("IssuedByUser"); + + b.Navigation("ReissuedFromDocument"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocumentDownload", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "DownloadedByUser") + .WithMany() + .HasForeignKey("DownloadedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.OfficialDocument", "OfficialDocument") + .WithMany("Downloads") + .HasForeignKey("OfficialDocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DownloadedByUser"); + + b.Navigation("OfficialDocument"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OtherExamResult", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.OtherExamBatch", "OtherExamBatch") + .WithMany("Results") + .HasForeignKey("OtherExamBatchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("OtherExamBatch"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.PublishedScheduleOccurrence", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.ScheduleEntry", "ScheduleEntry") + .WithMany() + .HasForeignKey("ScheduleEntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("ScheduleEntry"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ScheduleEntry", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.SchedulePlan", "SchedulePlan") + .WithMany("Entries") + .HasForeignKey("SchedulePlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("SchedulePlan"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ScheduleOptimizationSettings", 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.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.StudentAcademicStatistic", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.StudentAcademicTermStatistic", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AcademicTerm"); + + b.Navigation("Student"); + }); + + 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.TeachingImprovementPlan", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Teacher", "Teacher") + .WithMany() + .HasForeignKey("TeacherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AcademicTerm"); + + b.Navigation("Teacher"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingImprovementReview", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingImprovementPlan", "TeachingImprovementPlan") + .WithMany("Reviews") + .HasForeignKey("TeachingImprovementPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TeachingImprovementPlan"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingObservationEvaluation", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AcademicTerm"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTask", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "Course") + .WithMany() + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AcademicTerm"); + + b.Navigation("Course"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskAllowedClassroom", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", "TeachingTaskScheduleConstraint") + .WithMany("AllowedClassrooms") + .HasForeignKey("TeachingTaskScheduleConstraintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("TeachingTaskScheduleConstraint"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskAllowedExperimentClassroom", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", "TeachingTaskScheduleConstraint") + .WithMany("AllowedExperimentClassrooms") + .HasForeignKey("TeachingTaskScheduleConstraintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("TeachingTaskScheduleConstraint"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskClass", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AdministrativeClass", "AdministrativeClass") + .WithMany() + .HasForeignKey("AdministrativeClassId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany("Classes") + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AdministrativeClass"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskGradeScoreBand", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTaskGradeStatistic", "TeachingTaskGradeStatistic") + .WithMany("ScoreBands") + .HasForeignKey("TeachingTaskGradeStatisticId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TeachingTaskGradeStatistic"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskGradeStatistic", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", null) + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", null) + .WithMany() + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.GradeSheet", "GradeSheet") + .WithOne() + .HasForeignKey("Jiaowu.Api.Domain.Academic.TeachingTaskGradeStatistic", "GradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithOne() + .HasForeignKey("Jiaowu.Api.Domain.Academic.TeachingTaskGradeStatistic", "TeachingTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GradeSheet"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Building", "ExperimentRequiredBuilding") + .WithMany() + .HasForeignKey("ExperimentRequiredBuildingId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.Campus", "ExperimentRequiredCampus") + .WithMany() + .HasForeignKey("ExperimentRequiredCampusId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.Building", "RequiredBuilding") + .WithMany() + .HasForeignKey("RequiredBuildingId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.Campus", "RequiredCampus") + .WithMany() + .HasForeignKey("RequiredCampusId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExperimentRequiredBuilding"); + + b.Navigation("ExperimentRequiredCampus"); + + b.Navigation("RequiredBuilding"); + + b.Navigation("RequiredCampus"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskTeacher", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Teacher", "Teacher") + .WithMany() + .HasForeignKey("TeacherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany("Teachers") + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Teacher"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TuitionBill", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AcademicTerm"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TuitionPayment", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.TuitionBill", "TuitionBill") + .WithMany("Payments") + .HasForeignKey("TuitionBillId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TuitionBill"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TuitionTermSetting", 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.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.PasskeyCredential", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + 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.AcademicRiskCase", b => + { + b.Navigation("Entries"); + }); + + 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.CampusRegistrationApplication", b => + { + b.Navigation("PaymentOrder"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CampusRegistrationProject", b => + { + b.Navigation("Applications"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CampusWalletAccount", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Course", b => + { + b.Navigation("Prerequisites"); + + b.Navigation("RequiredByCourses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseGroup", b => + { + b.Navigation("Courses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionOffering", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionRound", b => + { + b.Navigation("EligibleGrades"); + + b.Navigation("Offerings"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumModule", b => + { + b.Navigation("Courses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumPlan", b => + { + b.Navigation("Modules"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.DegreeAwardBatch", b => + { + b.Navigation("Results"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationDimension", b => + { + b.Navigation("Scores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationRecord", b => + { + b.Navigation("Scores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationSetup", b => + { + b.Navigation("Dimensions"); + + b.Navigation("Records"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamPlan", b => + { + b.Navigation("Rooms"); + + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomAssignment", b => + { + b.Navigation("Invigilators"); + + b.Navigation("Seats"); + + b.Navigation("SessionLinks"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSession", b => + { + b.Navigation("Invigilators"); + + b.Navigation("RoomLinks"); + + b.Navigation("SeatAssignments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItem", b => + { + b.Navigation("Scores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", b => + { + b.Navigation("ItemScores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", b => + { + b.Navigation("Items"); + + b.Navigation("Records"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b => + { + b.Navigation("Bookings"); + + b.Navigation("GradeSheet"); + + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentSession", b => + { + b.Navigation("Bookings"); + + b.Navigation("Instructors"); + }); + + 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.TeachingImprovementPlan", b => + { + b.Navigation("Reviews"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTask", b => + { + b.Navigation("Classes"); + + b.Navigation("Teachers"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskGradeStatistic", b => + { + b.Navigation("ScoreBands"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", b => + { + b.Navigation("AllowedClassrooms"); + + b.Navigation("AllowedExperimentClassrooms"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TuitionBill", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.WarningRecord", b => + { + b.Navigation("RiskCase"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260905093721_UnifiedArchiving.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260905093721_UnifiedArchiving.cs new file mode 100644 index 0000000..463ec5e --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260905093721_UnifiedArchiving.cs @@ -0,0 +1,272 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql +{ + /// + public partial class UnifiedArchiving : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ArchiveReason", + table: "Teachers", + type: "varchar(500)", + maxLength: 500, + nullable: true); + + migrationBuilder.AddColumn( + name: "ArchivedAt", + table: "Teachers", + type: "datetime(6)", + nullable: true); + + migrationBuilder.AddColumn( + name: "ArchivedByUserId", + table: "Teachers", + type: "char(36)", + nullable: true); + + migrationBuilder.AddColumn( + name: "IsArchived", + table: "Teachers", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "ArchiveReason", + table: "Students", + type: "varchar(500)", + maxLength: 500, + nullable: true); + + migrationBuilder.AddColumn( + name: "ArchivedAt", + table: "Students", + type: "datetime(6)", + nullable: true); + + migrationBuilder.AddColumn( + name: "ArchivedByUserId", + table: "Students", + type: "char(36)", + nullable: true); + + migrationBuilder.AddColumn( + name: "IsArchived", + table: "Students", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "ArchiveReason", + table: "GradeSheets", + type: "varchar(500)", + maxLength: 500, + nullable: true); + + migrationBuilder.AddColumn( + name: "ArchivedAt", + table: "GradeSheets", + type: "datetime(6)", + nullable: true); + + migrationBuilder.AddColumn( + name: "ArchivedByUserId", + table: "GradeSheets", + type: "char(36)", + nullable: true); + + migrationBuilder.AddColumn( + name: "IsArchived", + table: "GradeSheets", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "ArchiveReason", + table: "Courses", + type: "varchar(500)", + maxLength: 500, + nullable: true); + + migrationBuilder.AddColumn( + name: "ArchivedAt", + table: "Courses", + type: "datetime(6)", + nullable: true); + + migrationBuilder.AddColumn( + name: "ArchivedByUserId", + table: "Courses", + type: "char(36)", + nullable: true); + + migrationBuilder.AddColumn( + name: "IsArchived", + table: "Courses", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "ArchiveReason", + table: "AcademicTerms", + type: "varchar(500)", + maxLength: 500, + nullable: true); + + migrationBuilder.AddColumn( + name: "ArchivedByUserId", + table: "AcademicTerms", + type: "char(36)", + nullable: true); + + migrationBuilder.CreateTable( + name: "ArchiveEvents", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + Kind = table.Column(type: "varchar(20)", maxLength: 20, nullable: false), + EntityId = table.Column(type: "char(36)", nullable: false), + IsArchived = table.Column(type: "tinyint(1)", nullable: false), + ActorUserId = table.Column(type: "char(36)", nullable: false), + ActorName = table.Column(type: "varchar(256)", maxLength: 256, nullable: true), + Reason = table.Column(type: "varchar(500)", maxLength: 500, nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ArchiveEvents", x => x.Id); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_Teachers_IsArchived", + table: "Teachers", + column: "IsArchived"); + + migrationBuilder.CreateIndex( + name: "IX_Students_IsArchived", + table: "Students", + column: "IsArchived"); + + migrationBuilder.CreateIndex( + name: "IX_GradeSheets_IsArchived", + table: "GradeSheets", + column: "IsArchived"); + + migrationBuilder.CreateIndex( + name: "IX_Courses_IsArchived", + table: "Courses", + column: "IsArchived"); + + migrationBuilder.CreateIndex( + name: "IX_ArchiveEvents_Kind_EntityId_CreatedAt", + table: "ArchiveEvents", + columns: new[] { "Kind", "EntityId", "CreatedAt" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ArchiveEvents"); + + migrationBuilder.DropIndex( + name: "IX_Teachers_IsArchived", + table: "Teachers"); + + migrationBuilder.DropIndex( + name: "IX_Students_IsArchived", + table: "Students"); + + migrationBuilder.DropIndex( + name: "IX_GradeSheets_IsArchived", + table: "GradeSheets"); + + migrationBuilder.DropIndex( + name: "IX_Courses_IsArchived", + table: "Courses"); + + migrationBuilder.DropColumn( + name: "ArchiveReason", + table: "Teachers"); + + migrationBuilder.DropColumn( + name: "ArchivedAt", + table: "Teachers"); + + migrationBuilder.DropColumn( + name: "ArchivedByUserId", + table: "Teachers"); + + migrationBuilder.DropColumn( + name: "IsArchived", + table: "Teachers"); + + migrationBuilder.DropColumn( + name: "ArchiveReason", + table: "Students"); + + migrationBuilder.DropColumn( + name: "ArchivedAt", + table: "Students"); + + migrationBuilder.DropColumn( + name: "ArchivedByUserId", + table: "Students"); + + migrationBuilder.DropColumn( + name: "IsArchived", + table: "Students"); + + migrationBuilder.DropColumn( + name: "ArchiveReason", + table: "GradeSheets"); + + migrationBuilder.DropColumn( + name: "ArchivedAt", + table: "GradeSheets"); + + migrationBuilder.DropColumn( + name: "ArchivedByUserId", + table: "GradeSheets"); + + migrationBuilder.DropColumn( + name: "IsArchived", + table: "GradeSheets"); + + migrationBuilder.DropColumn( + name: "ArchiveReason", + table: "Courses"); + + migrationBuilder.DropColumn( + name: "ArchivedAt", + table: "Courses"); + + migrationBuilder.DropColumn( + name: "ArchivedByUserId", + table: "Courses"); + + migrationBuilder.DropColumn( + name: "IsArchived", + table: "Courses"); + + migrationBuilder.DropColumn( + name: "ArchiveReason", + table: "AcademicTerms"); + + migrationBuilder.DropColumn( + name: "ArchivedByUserId", + table: "AcademicTerms"); + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs index f6b5ec0..5d72f43 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs @@ -175,9 +175,16 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql .IsRequired() .HasColumnType("longtext"); + b.Property("ArchiveReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + b.Property("ArchivedAt") .HasColumnType("datetime(6)"); + b.Property("ArchivedByUserId") + .HasColumnType("char(36)"); + b.Property("Code") .IsRequired() .HasMaxLength(40) @@ -190,6 +197,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql .HasColumnType("date"); b.Property("IsArchived") + .IsConcurrencyToken() .HasColumnType("tinyint(1)"); b.Property("IsCurrent") @@ -283,6 +291,48 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.ToTable("AdministrativeClasses"); }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ArchiveEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActorName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ActorUserId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EntityId") + .HasColumnType("char(36)"); + + b.Property("IsArchived") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Kind", "EntityId", "CreatedAt"); + + b.ToTable("ArchiveEvents"); + }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AttendanceCheckInAttempt", b => { b.Property("Id") @@ -1234,6 +1284,16 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql .ValueGeneratedOnAdd() .HasColumnType("char(36)"); + b.Property("ArchiveReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ArchivedAt") + .HasColumnType("datetime(6)"); + + b.Property("ArchivedByUserId") + .HasColumnType("char(36)"); + b.Property("AssessmentMethod") .HasColumnType("int"); @@ -1263,6 +1323,10 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql .HasMaxLength(150) .HasColumnType("varchar(150)"); + b.Property("IsArchived") + .IsConcurrencyToken() + .HasColumnType("tinyint(1)"); + b.Property("IsEnabled") .HasColumnType("tinyint(1)"); @@ -1296,6 +1360,8 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasIndex("CourseCategoryId"); + b.HasIndex("IsArchived"); + b.HasIndex("CollegeId", "Nature"); b.HasIndex("IsEnabled", "SortOrder"); @@ -3496,6 +3562,16 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql .ValueGeneratedOnAdd() .HasColumnType("char(36)"); + b.Property("ArchiveReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ArchivedAt") + .HasColumnType("datetime(6)"); + + b.Property("ArchivedByUserId") + .HasColumnType("char(36)"); + b.Property("CreatedAt") .HasColumnType("datetime(6)"); @@ -3503,6 +3579,10 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql .HasPrecision(5, 1) .HasColumnType("decimal(5,1)"); + b.Property("IsArchived") + .IsConcurrencyToken() + .HasColumnType("tinyint(1)"); + b.Property("PublishedAt") .HasColumnType("datetime(6)"); @@ -3531,6 +3611,8 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasKey("Id"); + b.HasIndex("IsArchived"); + b.HasIndex("Status"); b.HasIndex("TeachingTaskId") @@ -4701,6 +4783,16 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.Property("AdministrativeClassId") .HasColumnType("char(36)"); + b.Property("ArchiveReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ArchivedAt") + .HasColumnType("datetime(6)"); + + b.Property("ArchivedByUserId") + .HasColumnType("char(36)"); + b.Property("Biography") .HasMaxLength(1000) .HasColumnType("varchar(1000)"); @@ -4756,6 +4848,10 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql .HasMaxLength(30) .HasColumnType("varchar(30)"); + b.Property("IsArchived") + .IsConcurrencyToken() + .HasColumnType("tinyint(1)"); + b.Property("Name") .IsRequired() .HasMaxLength(50) @@ -4819,6 +4915,8 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasIndex("EnrollmentYear"); + b.HasIndex("IsArchived"); + b.HasIndex("StudentNumber") .IsUnique(); @@ -4976,6 +5074,16 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql .ValueGeneratedOnAdd() .HasColumnType("char(36)"); + b.Property("ArchiveReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ArchivedAt") + .HasColumnType("datetime(6)"); + + b.Property("ArchivedByUserId") + .HasColumnType("char(36)"); + b.Property("CollegeId") .HasColumnType("char(36)"); @@ -4992,6 +5100,10 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.Property("HireDate") .HasColumnType("date"); + b.Property("IsArchived") + .IsConcurrencyToken() + .HasColumnType("tinyint(1)"); + b.Property("IsExternal") .HasColumnType("tinyint(1)"); @@ -5028,6 +5140,8 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasKey("Id"); + b.HasIndex("IsArchived"); + b.HasIndex("TeacherNumber") .IsUnique(); diff --git a/tests/Jiaowu.Api.Tests/ArchivesControllerTests.cs b/tests/Jiaowu.Api.Tests/ArchivesControllerTests.cs new file mode 100644 index 0000000..fbdaf7f --- /dev/null +++ b/tests/Jiaowu.Api.Tests/ArchivesControllerTests.cs @@ -0,0 +1,204 @@ +using System.Text.Json; +using ClosedXML.Excel; +using Jiaowu.Api.Controllers; +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Infrastructure.Academic; +using Jiaowu.Api.Infrastructure.Auth; +using Jiaowu.Api.Infrastructure.Caching; +using Jiaowu.Api.Infrastructure.Persistence; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Jiaowu.Api.Tests; + +public sealed class ArchivesControllerTests : IAsyncDisposable +{ + private readonly SqliteConnection connection = new("Data Source=:memory:"); + private readonly AppDbContext db; + private readonly ArchiveTestScope school = new(); + private readonly College college = new() { Code = "C", Name = "学院" }; + private readonly AcademicTerm term = new() { Code = "T", Name = "历史学期", AcademicYear = "2025", Season = TermSeason.Autumn, StartDate = new(2025, 9, 1), EndDate = new(2026, 1, 1) }; + private readonly Course course; + + public ArchivesControllerTests() + { + connection.Open(); + db = new(new DbContextOptionsBuilder().UseSqlite(connection).Options); + db.Database.EnsureCreated(); + course = new() { Code = "C1", Name = "课程", CollegeId = college.Id, Nature = CourseNature.MajorRequired, Credits = 3 }; + db.AddRange(college, term, course); + db.SaveChanges(); + } + + private ArchivesController Controller(ICurrentUserDataScope? scope = null) => new(db, scope ?? school, NoOpAppCache.Instance); + private async Task Apply(string kind, Guid[] ids, bool archived = true, ICurrentUserDataScope? scope = null) + { + var response = Assert.IsType(await Controller(scope).Apply(kind, new(ids, archived, "测试原因"), default)); + return Assert.IsAssignableFrom>(response.Value).Cast().ToArray(); + } + + [Fact] + public async Task Teacher_batch_reports_blockers_and_keeps_audit_after_restore() + { + var retired = new Teacher { TeacherNumber = "R", Name = "退休教师", CollegeId = college.Id, Status = TeacherStatus.Retired }; + var active = new Teacher { TeacherNumber = "A", Name = "在职教师", CollegeId = college.Id }; + db.AddRange(retired, active); await db.SaveChangesAsync(); + var results = await Apply("teachers", [retired.Id, active.Id]); + Assert.True(results[0].Success); Assert.False(results[1].Success); + Assert.True((await db.Teachers.FindAsync(retired.Id))!.IsArchived); + Assert.False((await db.Teachers.FindAsync(active.Id))!.IsArchived); + Assert.True((await Apply("teachers", [retired.Id], false))[0].Success); + var restored = (await db.Teachers.FindAsync(retired.Id))!; + Assert.Equal(TeacherStatus.Retired, restored.Status); Assert.Null(restored.ArchivedAt); + Assert.Equal(2, await db.ArchiveEvents.CountAsync()); + } + + [Fact] + public async Task Permissions_block_other_colleges_public_courses_and_non_managers() + { + Assert.False((await Apply("courses", [course.Id], scope: new ArchiveTestScope(Guid.NewGuid())))[0].Success); + var entity = (await db.Courses.FindAsync(course.Id))!; + entity.Nature = CourseNature.GeneralRequired; await db.SaveChangesAsync(); + Assert.False((await Apply("courses", [course.Id], scope: new ArchiveTestScope(college.Id)))[0].Success); + Assert.False((await Apply("courses", [course.Id], scope: new ArchiveTestScope(role: SystemRoles.Teacher)))[0].Success); + Assert.Empty(await db.ArchiveEvents.ToArrayAsync()); + } + + [Fact] + public async Task Apply_rechecks_state_after_preview_and_is_idempotent() + { + var service = new ArchiveService(db, school); + Assert.Empty((await service.InspectAsync("courses", course.Id, false, default))!.Value.Preview.Blockers); + var task = Task(TeachingTaskStatus.Draft); + db.Add(task); await db.SaveChangesAsync(); + Assert.False((await Apply("courses", [course.Id]))[0].Success); + db.TeachingTasks.Remove((await db.TeachingTasks.FindAsync(task.Id))!); await db.SaveChangesAsync(); + Assert.True((await Apply("courses", [course.Id]))[0].Success); + Assert.True((await Apply("courses", [course.Id]))[0].Success); + Assert.Single(await db.ArchiveEvents.ToArrayAsync()); + Assert.True((await db.Courses.FindAsync(course.Id))!.IsEnabled); + } + + [Fact] + public async Task Archived_entities_preserve_published_transcript_and_grade_calculations() + { + var major = new Major { Code = "M", Name = "专业", CollegeId = college.Id, DegreeType = "学士" }; + var group = new AdministrativeClass { Code = "G", Name = "班级", MajorId = major.Id }; + var studentScope = new ArchiveTestScope(role: SystemRoles.Student); + db.Users.Add(new ApplicationUser { Id = studentScope.Current.UserId, DisplayName = "学生", UserName = "student" }); + var student = new Student { StudentNumber = "S", Name = "学生", AdministrativeClassId = group.Id, UserId = studentScope.Current.UserId, Status = StudentStatus.Graduated }; + var task = Task(TeachingTaskStatus.Closed); + var sheet = new GradeSheet { TeachingTaskId = task.Id, Status = GradeSheetStatus.Published }; + var record = new GradeRecord { GradeSheetId = sheet.Id, StudentId = student.Id, TotalScore = 90, GradePoint = 4 }; + db.AddRange(major, group, student, task, sheet, record); await db.SaveChangesAsync(); + async Task Transcript() => JsonSerializer.Serialize(Assert.IsType(await new GradesController(db, studentScope).GetStudentTranscript(null, default)).Value); + var before = await Transcript(); + foreach (var (kind, id) in new[] { ("grades", sheet.Id), ("students", student.Id), ("courses", course.Id), ("terms", term.Id) }) + Assert.True((await Apply(kind, [id]))[0].Success); + Assert.Equal(before, await Transcript()); + var stored = (await db.GradeSheets.FindAsync(sheet.Id))!; + Assert.Equal(GradeSheetStatus.Published, stored.Status); + Assert.Equal(4, (await db.GradeRecords.FindAsync(record.Id))!.GradePoint); + Assert.True((await Apply("grades", [sheet.Id], false))[0].Success); + Assert.Equal(GradeSheetStatus.Published, (await db.GradeSheets.FindAsync(sheet.Id))!.Status); + } + + [Fact] + public async Task Term_requires_closed_tasks_and_published_grade_sheets() + { + var task = Task(TeachingTaskStatus.Closed); db.Add(task); await db.SaveChangesAsync(); + Assert.False((await Apply("terms", [term.Id]))[0].Success); + var sheet = new GradeSheet { TeachingTaskId = task.Id }; db.Add(sheet); await db.SaveChangesAsync(); + Assert.False((await Apply("terms", [term.Id]))[0].Success); + Assert.False((await Apply("grades", [sheet.Id]))[0].Success); + var loaded = (await db.GradeSheets.FindAsync(sheet.Id))!; loaded.Status = GradeSheetStatus.Published; await db.SaveChangesAsync(); + Assert.True((await Apply("terms", [term.Id]))[0].Success); + } + + [Fact] + public async Task Archived_courses_are_hidden_from_options_but_available_in_filtered_history() + { + Assert.True((await Apply("courses", [course.Id]))[0].Success); + var controller = new CoursesController(db, school, NoOpAppCache.Instance); + var options = Assert.IsType((await controller.GetOptions()).Result); + Assert.Equal(0, JsonSerializer.SerializeToElement(options.Value).GetArrayLength()); + var archived = Assert.IsType((await controller.Get(isArchived: true)).Result); + Assert.Equal(1, JsonSerializer.SerializeToElement(archived.Value).GetProperty("Total").GetInt32()); + var current = Assert.IsType((await controller.Get(isArchived: false)).Result); + Assert.Equal(0, JsonSerializer.SerializeToElement(current.Value).GetProperty("Total").GetInt32()); + Assert.True((await Apply("courses", [course.Id], false))[0].Success); + options = Assert.IsType((await controller.GetOptions()).Result); + Assert.Equal(1, JsonSerializer.SerializeToElement(options.Value).GetArrayLength()); + } + + [Fact] + public async Task History_has_its_own_scope_check_and_empty_reason_cannot_mutate() + { + Assert.IsType(await Controller().Apply("courses", new([course.Id], true, " "), default)); + Assert.Empty(await db.ArchiveEvents.ToArrayAsync()); + Assert.True((await Apply("courses", [course.Id]))[0].Success); + Assert.IsType(await Controller(new ArchiveTestScope(Guid.NewGuid())).History("courses", course.Id, default)); + var response = Assert.IsType(await Controller().History("courses", course.Id, default)); + Assert.Single(Assert.IsAssignableFrom>(response.Value)); + } + + [Fact] + public async Task Term_excel_cannot_overwrite_archived_data_and_export_matches_archive_filter() + { + Assert.True((await Apply("terms", [term.Id]))[0].Success); + var controller = new BaseDataExcelController(db, NoOpAppCache.Instance); + var file = Assert.IsType(await controller.Export("terms", default, true)); + using var workbook = new XLWorkbook(new MemoryStream(file.FileContents)); + Assert.Equal(term.Code, workbook.Worksheet(1).Cell(2, 1).GetString()); + workbook.Worksheet(1).Cell(2, 2).Value = "不应被覆盖"; + using var stream = new MemoryStream(); workbook.SaveAs(stream); stream.Position = 0; + var result = await controller.Import("terms", new FormFile(stream, 0, stream.Length, "file", "terms.xlsx"), default); + var error = Assert.IsAssignableFrom(result.Result); + var details = Assert.IsType(error.Value); + Assert.Contains(details.Errors.SelectMany(x => x.Value), x => x.Contains("已归档")); + db.ChangeTracker.Clear(); + Assert.Equal(term.Name, (await db.AcademicTerms.FindAsync(term.Id))!.Name); + var empty = Assert.IsType(await controller.Export("terms", default, false)); + using var emptyWorkbook = new XLWorkbook(new MemoryStream(empty.FileContents)); + Assert.True(emptyWorkbook.Worksheet(1).Cell(2, 1).IsEmpty()); + } + + [Fact] + public async Task Sqlite_upgrade_preserves_existing_rows_and_is_repeatable() + { + await db.Database.ExecuteSqlRawAsync("DROP TABLE ArchiveEvents"); + // Identifiers below come exclusively from this fixed test schema. +#pragma warning disable EF1002 + foreach (var table in new[] { "Teachers", "Students", "Courses", "GradeSheets", "AcademicTerms" }) + { + if (table != "AcademicTerms") + { + await db.Database.ExecuteSqlRawAsync($"DROP INDEX IX_{table}_IsArchived"); + await db.Database.ExecuteSqlRawAsync($"ALTER TABLE {table} DROP COLUMN IsArchived"); + await db.Database.ExecuteSqlRawAsync($"ALTER TABLE {table} DROP COLUMN ArchivedAt"); + } + await db.Database.ExecuteSqlRawAsync($"ALTER TABLE {table} DROP COLUMN ArchivedByUserId"); + await db.Database.ExecuteSqlRawAsync($"ALTER TABLE {table} DROP COLUMN ArchiveReason"); + } + #pragma warning restore EF1002 + var migrator = new DevelopmentSqliteMigrator(db, NullLogger.Instance); + await migrator.MigrateAsync(); await migrator.MigrateAsync(); + db.ChangeTracker.Clear(); + Assert.Equal("课程", (await db.Courses.FindAsync(course.Id))!.Name); + Assert.True((await Apply("courses", [course.Id]))[0].Success); + } + + private TeachingTask Task(TeachingTaskStatus status) => new() { TaskNumber = Guid.NewGuid().ToString(), Name = "教学班", CourseId = course.Id, AcademicTermId = term.Id, Status = status }; + public async ValueTask DisposeAsync() { await db.DisposeAsync(); await connection.DisposeAsync(); } +} + +internal sealed class ArchiveTestScope(Guid? collegeId = null, string? role = null) : ICurrentUserDataScope +{ + public CurrentUserScope Current { get; } = new(Guid.NewGuid(), "测试管理员", collegeId, + collegeId.HasValue ? DataScope.College : DataScope.All, + new HashSet { role ?? (collegeId.HasValue ? SystemRoles.CollegeAdmin : SystemRoles.SuperAdmin) }); +} diff --git a/tests/Jiaowu.Api.Tests/AuthControllerTests.cs b/tests/Jiaowu.Api.Tests/AuthControllerTests.cs index 8d7f963..0f40604 100644 --- a/tests/Jiaowu.Api.Tests/AuthControllerTests.cs +++ b/tests/Jiaowu.Api.Tests/AuthControllerTests.cs @@ -108,6 +108,7 @@ public sealed class AuthControllerTests db, userManager, new StubAuthSessionService(), + new TwoFactorLoginTicketService(new Microsoft.AspNetCore.DataProtection.EphemeralDataProtectionProvider()), NoOpAppCache.Instance); var request = new StudentActivationRequest( student.Name, diff --git a/tests/Jiaowu.Api.Tests/BaseDataControllerTests.cs b/tests/Jiaowu.Api.Tests/BaseDataControllerTests.cs index 7e0f81d..251d99f 100644 --- a/tests/Jiaowu.Api.Tests/BaseDataControllerTests.cs +++ b/tests/Jiaowu.Api.Tests/BaseDataControllerTests.cs @@ -22,7 +22,7 @@ public sealed class BaseDataControllerTests : IAsyncDisposable .Options; db = new AppDbContext(options); db.Database.EnsureCreated(); - controller = new BaseDataController(db, NoOpAppCache.Instance); + controller = new BaseDataController(db, NoOpAppCache.Instance, new ArchiveTestScope()); } [Fact] @@ -57,6 +57,7 @@ public sealed class BaseDataControllerTests : IAsyncDisposable var archiveResult = await controller.ArchiveTerm( history.Id, CancellationToken.None); Assert.Null(archiveResult.Result); + history = archiveResult.Value!; Assert.True(history.IsArchived); Assert.NotNull(history.ArchivedAt); Assert.True(history.IsEnabled); @@ -64,6 +65,7 @@ public sealed class BaseDataControllerTests : IAsyncDisposable var unarchiveResult = await controller.UnarchiveTerm( history.Id, CancellationToken.None); Assert.Null(unarchiveResult.Result); + history = unarchiveResult.Value!; Assert.False(history.IsArchived); Assert.Null(history.ArchivedAt); } diff --git a/web/src/components.d.ts b/web/src/components.d.ts index 1c5889e..a08b30d 100644 --- a/web/src/components.d.ts +++ b/web/src/components.d.ts @@ -13,6 +13,7 @@ declare module 'vue' { export interface GlobalComponents { AcademicCalendarReminder: typeof import('./components/AcademicCalendarReminder.vue')['default'] AppUpdateManagementPanel: typeof import('./components/AppUpdateManagementPanel.vue')['default'] + ArchiveDialog: typeof import('./components/ArchiveDialog.vue')['default'] ChinesePostalCodeInput: typeof import('./components/ChinesePostalCodeInput.vue')['default'] CourseDiscussionPanel: typeof import('./components/CourseDiscussionPanel.vue')['default'] ElAlert: typeof import('element-plus/es')['ElAlert'] diff --git a/web/src/components/ArchiveDialog.vue b/web/src/components/ArchiveDialog.vue new file mode 100644 index 0000000..bfb7728 --- /dev/null +++ b/web/src/components/ArchiveDialog.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/web/src/views/BaseDataView.vue b/web/src/views/BaseDataView.vue index 88cf6ae..2b6c867 100644 --- a/web/src/views/BaseDataView.vue +++ b/web/src/views/BaseDataView.vue @@ -1,4 +1,5 @@