diff --git a/src/Jiaowu.Api/Controllers/ExperimentsController.cs b/src/Jiaowu.Api/Controllers/ExperimentsController.cs new file mode 100644 index 0000000..bc3a19a --- /dev/null +++ b/src/Jiaowu.Api/Controllers/ExperimentsController.cs @@ -0,0 +1,1044 @@ +using System.ComponentModel.DataAnnotations; +using System.Data; +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Infrastructure.Auth; +using Jiaowu.Api.Infrastructure.Persistence; +using Jiaowu.Api.Infrastructure.Teaching; +using Jiaowu.Api.Infrastructure.Timetables; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Jiaowu.Api.Controllers; + +[ApiController] +[Authorize] +[Route("api/experiments")] +public sealed class ExperimentsController( + AppDbContext db, + ICurrentUserDataScope currentUserDataScope, + ClassroomReservationAvailabilityService classroomAvailability) : ControllerBase +{ + private const string Managers = + SystemRoles.SuperAdmin + "," + + SystemRoles.AcademicAdmin + "," + + SystemRoles.CollegeAdmin + "," + + SystemRoles.Teacher; + + [HttpGet("options")] + [Authorize(Roles = Managers)] + public async Task GetOptions( + Guid? academicTermId, + CancellationToken cancellationToken) + { + var tasks = AccessibleTeachingTasks().AsNoTracking() + .Where(x => x.Status == TeachingTaskStatus.Published); + if (academicTermId.HasValue) + tasks = tasks.Where(x => x.AcademicTermId == academicTermId); + + var periods = db.ScheduleTimeSlots.AsNoTracking() + .Where(x => x.IsEnabled); + if (academicTermId.HasValue) + periods = periods.Where(x => x.AcademicTermId == academicTermId); + var periodItems = await periods + .OrderBy(x => x.PeriodNumber) + .Select(x => new ExperimentPeriodOption( + x.AcademicTermId, + x.PeriodNumber, + x.Name, + x.StartsAt.ToString("HH:mm"), + x.EndsAt.ToString("HH:mm"))) + .ToListAsync(cancellationToken); + if (academicTermId.HasValue && periodItems.Count == 0) + { + periodItems = Enumerable.Range(1, 12) + .Select(period => new ExperimentPeriodOption( + academicTermId.Value, + period, + $"第 {period} 节", + "", + "")) + .ToList(); + } + + return Ok(new + { + Tasks = await tasks + .OrderByDescending(x => x.AcademicTerm!.StartDate) + .ThenBy(x => x.Course!.Code) + .ThenBy(x => x.TaskNumber) + .Select(x => new + { + x.Id, + x.TaskNumber, + x.Name, + x.AcademicTermId, + TermName = x.AcademicTerm!.Name, + TermStartDate = x.AcademicTerm.StartDate, + TermEndDate = x.AcademicTerm.EndDate, + CourseCode = x.Course!.Code, + CourseName = x.Course.Name, + CollegeName = x.Course.College!.Name, + TeacherNames = x.Teachers + .OrderByDescending(item => item.IsPrimary) + .ThenBy(item => item.Teacher!.TeacherNumber) + .Select(item => item.Teacher!.Name), + ClassNames = x.Classes + .OrderBy(item => item.AdministrativeClass!.Code) + .Select(item => item.AdministrativeClass!.Name) + }) + .ToListAsync(cancellationToken), + Classrooms = await db.Classrooms.AsNoTracking() + .Where(x => x.IsEnabled) + .OrderBy(x => x.Building!.Campus!.SortOrder) + .ThenBy(x => x.Building!.SortOrder) + .ThenBy(x => x.SortOrder) + .Select(x => new + { + x.Id, + x.Name, + BuildingName = x.Building!.Name, + CampusName = x.Building.Campus!.Name, + x.Capacity + }) + .ToListAsync(cancellationToken), + Periods = periodItems + }); + } + + [HttpGet("management")] + [Authorize(Roles = Managers)] + public async Task GetManagementProjects( + Guid? academicTermId, + ExperimentArrangementMode? arrangementMode, + ExperimentProjectStatus? status, + CancellationToken cancellationToken) + { + var source = ScopedProjects().AsNoTracking(); + if (academicTermId.HasValue) + source = source.Where(x => + x.TeachingTask!.AcademicTermId == academicTermId); + if (arrangementMode.HasValue) + source = source.Where(x => + x.ArrangementMode == arrangementMode); + if (status.HasValue) + source = source.Where(x => x.Status == status); + + return Ok(await source + .OrderByDescending(x => x.Status == ExperimentProjectStatus.Published) + .ThenBy(x => x.StartDate) + .ThenBy(x => x.Code) + .Select(x => new + { + x.Id, + x.TeachingTaskId, + x.Code, + x.Name, + x.ArrangementMode, + x.Description, + x.Requirements, + x.StartDate, + x.EndDate, + x.Status, + x.PublishedAt, + x.ClosedAt, + AcademicTermId = x.TeachingTask!.AcademicTermId, + TermName = x.TeachingTask.AcademicTerm!.Name, + TaskNumber = x.TeachingTask.TaskNumber, + CourseCode = x.TeachingTask.Course!.Code, + CourseName = x.TeachingTask.Course.Name, + CollegeName = x.TeachingTask.Course.College!.Name, + TeacherNames = x.TeachingTask.Teachers + .OrderByDescending(item => item.IsPrimary) + .Select(item => item.Teacher!.Name), + ClassNames = x.TeachingTask.Classes + .OrderBy(item => item.AdministrativeClass!.Code) + .Select(item => item.AdministrativeClass!.Name), + Sessions = x.Sessions + .OrderBy(item => item.SessionDate) + .ThenBy(item => item.StartPeriod) + .Select(item => new + { + item.Id, + item.SessionDate, + item.StartPeriod, + item.PeriodCount, + item.Capacity, + item.ReservedCount, + item.Notes, + item.Status, + item.ClassroomId, + ClassroomName = item.Classroom!.Name, + BuildingName = item.Classroom.Building!.Name, + CampusName = item.Classroom.Building.Campus!.Name + }) + }) + .ToListAsync(cancellationToken)); + } + + [HttpGet("student")] + [Authorize(Roles = SystemRoles.Student)] + public async Task GetStudentProjects( + Guid? academicTermId, + CancellationToken cancellationToken) + { + var student = await CurrentStudentAsync(cancellationToken); + if (student is null) + return ConflictProblem("当前账号未关联有效学生档案。"); + + var taskIds = TeachingTaskRosterQuery.TaskIdsForStudent(db, student.Id); + var source = db.ExperimentProjects.AsNoTracking() + .Where(x => + taskIds.Contains(x.TeachingTaskId) && + (x.Status == ExperimentProjectStatus.Published || + x.Status == ExperimentProjectStatus.Closed)); + if (academicTermId.HasValue) + source = source.Where(x => + x.TeachingTask!.AcademicTermId == academicTermId); + + return Ok(await source + .OrderByDescending(x => x.Status == ExperimentProjectStatus.Published) + .ThenBy(x => x.EndDate) + .ThenBy(x => x.Code) + .Select(x => new + { + x.Id, + x.Code, + x.Name, + x.ArrangementMode, + x.Description, + x.Requirements, + x.StartDate, + x.EndDate, + x.Status, + AcademicTermId = x.TeachingTask!.AcademicTermId, + TermName = x.TeachingTask.AcademicTerm!.Name, + TaskNumber = x.TeachingTask.TaskNumber, + CourseCode = x.TeachingTask.Course!.Code, + CourseName = x.TeachingTask.Course.Name, + TeacherNames = x.TeachingTask.Teachers + .OrderByDescending(item => item.IsPrimary) + .Select(item => item.Teacher!.Name), + Sessions = x.Sessions + .Where(item => item.Status == ExperimentSessionStatus.Scheduled) + .OrderBy(item => item.SessionDate) + .ThenBy(item => item.StartPeriod) + .Select(item => new + { + item.Id, + item.SessionDate, + item.StartPeriod, + item.PeriodCount, + item.Capacity, + item.ReservedCount, + RemainingCount = item.Capacity - item.ReservedCount, + item.Notes, + ClassroomName = item.Classroom!.Name, + BuildingName = item.Classroom.Building!.Name, + CampusName = item.Classroom.Building.Campus!.Name + }), + MyBooking = x.Bookings + .Where(item => + item.StudentId == student.Id && + item.Status == ExperimentBookingStatus.Booked) + .Select(item => new + { + item.Id, + item.ExperimentSessionId, + item.BookedAt + }) + .FirstOrDefault() + }) + .ToListAsync(cancellationToken)); + } + + [HttpPost] + [Authorize(Roles = Managers)] + public async Task CreateProject( + ExperimentProjectRequest request, + CancellationToken cancellationToken) + { + var task = await AccessibleTeachingTasks().AsNoTracking() + .Include(x => x.AcademicTerm) + .FirstOrDefaultAsync(x => + x.Id == request.TeachingTaskId && + x.Status == TeachingTaskStatus.Published, + cancellationToken); + if (task is null) + return ValidationProblem("教学任务不存在、未发布或不在当前管理范围内。"); + + var problem = ValidateProjectRequest(request, task.AcademicTerm!); + if (problem is not null) return ValidationProblem(problem); + + var code = request.Code.Trim(); + if (await db.ExperimentProjects.AnyAsync(x => + x.TeachingTaskId == request.TeachingTaskId && + x.Code == code, + cancellationToken)) + return ConflictProblem("该教学任务下已存在相同实验项目编码。"); + + var project = new ExperimentProject + { + TeachingTaskId = request.TeachingTaskId, + Code = code, + Name = request.Name.Trim(), + ArrangementMode = request.ArrangementMode, + Description = Normalize(request.Description), + Requirements = Normalize(request.Requirements), + StartDate = request.StartDate, + EndDate = request.EndDate + }; + db.ExperimentProjects.Add(project); + await db.SaveChangesAsync(cancellationToken); + return Created(string.Empty, new { project.Id }); + } + + [HttpPut("{id:guid}")] + [Authorize(Roles = Managers)] + public async Task UpdateProject( + Guid id, + ExperimentProjectRequest request, + CancellationToken cancellationToken) + { + var project = await ScopedProjects() + .Include(x => x.TeachingTask) + .ThenInclude(x => x!.AcademicTerm) + .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + if (project is null) return NotFound(); + if (project.Status != ExperimentProjectStatus.Draft) + return ConflictProblem("只有草稿实验项目可以修改。"); + if (project.TeachingTaskId != request.TeachingTaskId) + return ValidationProblem("创建后不能更换实验项目所属教学任务。"); + + var problem = ValidateProjectRequest( + request, + project.TeachingTask!.AcademicTerm!); + if (problem is not null) return ValidationProblem(problem); + + var code = request.Code.Trim(); + if (await db.ExperimentProjects.AnyAsync(x => + x.Id != id && + x.TeachingTaskId == request.TeachingTaskId && + x.Code == code, + cancellationToken)) + return ConflictProblem("该教学任务下已存在相同实验项目编码。"); + + project.Code = code; + project.Name = request.Name.Trim(); + project.ArrangementMode = request.ArrangementMode; + project.Description = Normalize(request.Description); + project.Requirements = Normalize(request.Requirements); + project.StartDate = request.StartDate; + project.EndDate = request.EndDate; + await db.SaveChangesAsync(cancellationToken); + return NoContent(); + } + + [HttpDelete("{id:guid}")] + [Authorize(Roles = Managers)] + public async Task DeleteProject( + Guid id, + CancellationToken cancellationToken) + { + var project = await ScopedProjects() + .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + if (project is null) return NotFound(); + if (project.Status != ExperimentProjectStatus.Draft) + return ConflictProblem("只有草稿实验项目可以删除。"); + + db.ExperimentProjects.Remove(project); + await db.SaveChangesAsync(cancellationToken); + return NoContent(); + } + + [HttpPost("{id:guid}/publish")] + [Authorize(Roles = Managers)] + public async Task PublishProject( + Guid id, + CancellationToken cancellationToken) + { + var project = await ScopedProjects() + .Include(x => x.Sessions) + .Include(x => x.TeachingTask) + .ThenInclude(x => x!.Course) + .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + if (project is null) return NotFound(); + if (project.Status != ExperimentProjectStatus.Draft) + return ConflictProblem("只有草稿实验项目可以发布。"); + if (!project.Sessions.Any(x => + x.Status == ExperimentSessionStatus.Scheduled)) + return ConflictProblem("请至少安排一个有效实验场次后再发布。"); + if (project.Sessions.Any(x => + x.Status == ExperimentSessionStatus.Scheduled && + (x.SessionDate < project.StartDate || + x.SessionDate > project.EndDate))) + return ConflictProblem("存在不在项目开放日期范围内的实验场次。"); + + project.Status = ExperimentProjectStatus.Published; + project.PublishedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + + var userIds = await TeachingTaskRosterQuery + .ForTask(db, project.TeachingTaskId) + .Where(x => x.UserId.HasValue) + .Select(x => x.UserId!.Value) + .Distinct() + .ToListAsync(cancellationToken); + if (userIds.Count > 0) + { + var mode = project.ArrangementMode == + ExperimentArrangementMode.Centralized + ? "集中安排" + : "自行预约"; + await NotificationService.SendToUserIdsAsync( + db, + userIds, + "实验项目已发布", + $"《{project.TeachingTask!.Course!.Name}》已发布“{project.Name}”({mode}),请查看实验安排。", + "/experiments", + cancellationToken, + NotificationCategory.Schedule); + } + return NoContent(); + } + + [HttpPost("{id:guid}/close")] + [Authorize(Roles = Managers)] + public async Task CloseProject( + Guid id, + CancellationToken cancellationToken) + { + var project = await ScopedProjects() + .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + if (project is null) return NotFound(); + if (project.Status != ExperimentProjectStatus.Published) + return ConflictProblem("只有已发布实验项目可以关闭。"); + project.Status = ExperimentProjectStatus.Closed; + project.ClosedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + return NoContent(); + } + + [HttpPost("{projectId:guid}/sessions")] + [Authorize(Roles = Managers)] + public async Task CreateSession( + Guid projectId, + ExperimentSessionRequest request, + CancellationToken cancellationToken) + { + var project = await ScopedProjects() + .Include(x => x.TeachingTask) + .ThenInclude(x => x!.AcademicTerm) + .FirstOrDefaultAsync(x => x.Id == projectId, cancellationToken); + if (project is null) return NotFound(); + if (project.Status == ExperimentProjectStatus.Closed) + return ConflictProblem("已关闭实验项目不能再增加场次。"); + + var problem = await ValidateSessionAsync( + project, + request, + cancellationToken); + if (problem is not null) return ConflictProblem(problem); + + var session = new ExperimentSession + { + ExperimentProjectId = project.Id, + ClassroomId = request.ClassroomId, + SessionDate = request.SessionDate, + StartPeriod = request.StartPeriod, + PeriodCount = request.PeriodCount, + Capacity = await ResolveSessionCapacityAsync( + project, + request.Capacity, + cancellationToken), + Notes = Normalize(request.Notes) + }; + db.ExperimentSessions.Add(session); + await db.SaveChangesAsync(cancellationToken); + + if (project.Status == ExperimentProjectStatus.Published) + { + var userIds = await RosterUserIdsAsync( + project.TeachingTaskId, + cancellationToken); + if (userIds.Count > 0) + { + await NotificationService.SendToUserIdsAsync( + db, + userIds, + "新增实验场次", + $"“{project.Name}”新增 {session.SessionDate:yyyy-MM-dd} 第 {session.StartPeriod}—{session.StartPeriod + session.PeriodCount - 1} 节场次,请查看实验安排。", + "/experiments", + cancellationToken, + NotificationCategory.Schedule); + } + } + return Created(string.Empty, new { session.Id }); + } + + [HttpDelete("sessions/{id:guid}")] + [Authorize(Roles = Managers)] + public async Task CancelSession( + Guid id, + CancellationToken cancellationToken) + { + var session = await db.ExperimentSessions + .Include(x => x.ExperimentProject) + .ThenInclude(x => x!.TeachingTask) + .ThenInclude(x => x!.Course) + .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + if (session is null || + !await ScopedProjects().AnyAsync( + x => x.Id == session.ExperimentProjectId, + cancellationToken)) + return NotFound(); + if (session.Status == ExperimentSessionStatus.Cancelled) + return NoContent(); + + if (session.ExperimentProject!.Status == ExperimentProjectStatus.Draft) + { + db.ExperimentSessions.Remove(session); + await db.SaveChangesAsync(cancellationToken); + return NoContent(); + } + + var affectedUserIds = + session.ExperimentProject.ArrangementMode == + ExperimentArrangementMode.Centralized + ? await RosterUserIdsAsync( + session.ExperimentProject.TeachingTaskId, + cancellationToken) + : await db.ExperimentBookings + .Where(x => + x.ExperimentSessionId == id && + x.Status == ExperimentBookingStatus.Booked && + x.Student!.UserId.HasValue) + .Select(x => x.Student!.UserId!.Value) + .Distinct() + .ToListAsync(cancellationToken); + var bookings = await db.ExperimentBookings + .Where(x => + x.ExperimentSessionId == id && + x.Status == ExperimentBookingStatus.Booked) + .ToListAsync(cancellationToken); + foreach (var booking in bookings) + { + booking.Status = ExperimentBookingStatus.Cancelled; + booking.CancelledAt = DateTime.UtcNow; + } + session.Status = ExperimentSessionStatus.Cancelled; + session.CancelledAt = DateTime.UtcNow; + session.ReservedCount = 0; + await db.SaveChangesAsync(cancellationToken); + + if (affectedUserIds.Count > 0) + { + await NotificationService.SendToUserIdsAsync( + db, + affectedUserIds, + "实验场次已取消", + $"“{session.ExperimentProject.Name}”原定于 {session.SessionDate:yyyy-MM-dd} 的实验场次已取消,请重新查看安排。", + "/experiments", + cancellationToken, + NotificationCategory.Schedule); + } + return NoContent(); + } + + [HttpGet("sessions/{id:guid}/participants")] + [Authorize(Roles = Managers)] + public async Task GetParticipants( + Guid id, + CancellationToken cancellationToken) + { + var session = await db.ExperimentSessions.AsNoTracking() + .Include(x => x.ExperimentProject) + .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + if (session is null || + !await ScopedProjects().AnyAsync( + x => x.Id == session.ExperimentProjectId, + cancellationToken)) + return NotFound(); + + if (session.ExperimentProject!.ArrangementMode == + ExperimentArrangementMode.Centralized) + { + var roster = await TeachingTaskRosterQuery.LoadForTasksAsync( + db, + [session.ExperimentProject.TeachingTaskId], + cancellationToken); + return Ok(roster.Select(x => new + { + x.StudentId, + x.StudentNumber, + x.Name, + x.ClassName, + ParticipationType = "Centralized" + })); + } + + return Ok(await db.ExperimentBookings.AsNoTracking() + .Where(x => + x.ExperimentSessionId == id && + x.Status == ExperimentBookingStatus.Booked) + .OrderBy(x => x.Student!.StudentNumber) + .Select(x => new + { + x.StudentId, + x.Student!.StudentNumber, + x.Student.Name, + ClassName = x.Student.AdministrativeClass!.Name, + ParticipationType = "Booked", + x.BookedAt + }) + .ToListAsync(cancellationToken)); + } + + [HttpPost("sessions/{id:guid}/book")] + [Authorize(Roles = SystemRoles.Student)] + public async Task Book( + Guid id, + CancellationToken cancellationToken) + { + return await db.ExecuteInRetriableTransactionAsync( + async transaction => + { + db.ChangeTracker.Clear(); + var student = await CurrentStudentAsync(cancellationToken); + if (student is null) + return ConflictProblem("当前账号未关联有效学生档案。"); + + var session = await db.ExperimentSessions + .Include(x => x.ExperimentProject) + .ThenInclude(x => x!.TeachingTask) + .ThenInclude(x => x!.AcademicTerm) + .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + if (session is null || + session.Status != ExperimentSessionStatus.Scheduled || + session.ExperimentProject!.Status != + ExperimentProjectStatus.Published) + return NotFound(); + if (session.ExperimentProject.ArrangementMode != + ExperimentArrangementMode.SelfScheduled) + return ConflictProblem("集中安排实验无需学生预约。"); + if (!await TeachingTaskRosterQuery + .TaskIdsForStudent(db, student.Id) + .ContainsAsync( + session.ExperimentProject.TeachingTaskId, + cancellationToken)) + return Forbid(); + if (session.SessionDate < + DateOnly.FromDateTime(DateTime.UtcNow.AddHours(8))) + return ConflictProblem("该实验场次已经结束,不能预约。"); + + var existing = await db.ExperimentBookings + .FirstOrDefaultAsync(x => + x.ExperimentProjectId == + session.ExperimentProjectId && + x.StudentId == student.Id, + cancellationToken); + if (existing?.Status == ExperimentBookingStatus.Booked) + { + return existing.ExperimentSessionId == session.Id + ? NoContent() + : ConflictProblem("该实验项目已有预约,请先取消原预约。"); + } + + var scheduleProblem = await StudentConflictAsync( + student.Id, + session, + cancellationToken); + if (scheduleProblem is not null) + return ConflictProblem(scheduleProblem); + + var reservedCount = await db.ExperimentBookings + .CountAsync(x => + x.ExperimentSessionId == session.Id && + x.Status == ExperimentBookingStatus.Booked, + cancellationToken); + if (reservedCount >= session.Capacity) + return ConflictProblem("该实验场次名额已满,请选择其他时间。"); + + if (existing is null) + { + db.ExperimentBookings.Add(new ExperimentBooking + { + ExperimentProjectId = session.ExperimentProjectId, + ExperimentSessionId = session.Id, + StudentId = student.Id + }); + } + else + { + existing.ExperimentSessionId = session.Id; + existing.Status = ExperimentBookingStatus.Booked; + existing.BookedAt = DateTime.UtcNow; + existing.CancelledAt = null; + } + session.ReservedCount = reservedCount + 1; + await db.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + return NoContent(); + }, + cancellationToken, + IsolationLevel.Serializable); + } + + [HttpDelete("bookings/{id:guid}")] + [Authorize(Roles = SystemRoles.Student)] + public async Task CancelBooking( + Guid id, + CancellationToken cancellationToken) + { + return await db.ExecuteInRetriableTransactionAsync( + async transaction => + { + db.ChangeTracker.Clear(); + var student = await CurrentStudentAsync(cancellationToken); + if (student is null) + return ConflictProblem("当前账号未关联有效学生档案。"); + + var booking = await db.ExperimentBookings + .Include(x => x.ExperimentSession) + .FirstOrDefaultAsync(x => + x.Id == id && + x.StudentId == student.Id, + cancellationToken); + if (booking is null) return NotFound(); + if (booking.Status == ExperimentBookingStatus.Cancelled) + return NoContent(); + + booking.Status = ExperimentBookingStatus.Cancelled; + booking.CancelledAt = DateTime.UtcNow; + booking.ExperimentSession!.ReservedCount = Math.Max( + 0, + await db.ExperimentBookings.CountAsync(x => + x.ExperimentSessionId == + booking.ExperimentSessionId && + x.Status == ExperimentBookingStatus.Booked && + x.Id != booking.Id, + cancellationToken)); + await db.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + return NoContent(); + }, + cancellationToken, + IsolationLevel.Serializable); + } + + private async Task ValidateSessionAsync( + ExperimentProject project, + ExperimentSessionRequest request, + CancellationToken cancellationToken) + { + if (request.SessionDate < project.StartDate || + request.SessionDate > project.EndDate) + return "实验场次日期必须在项目开放日期范围内。"; + + var configuredPeriodCount = await db.ScheduleTimeSlots.AsNoTracking() + .CountAsync(x => + x.AcademicTermId == + project.TeachingTask!.AcademicTermId && + x.IsEnabled, + cancellationToken); + var validPeriodCount = await db.ScheduleTimeSlots.AsNoTracking() + .CountAsync(x => + x.AcademicTermId == + project.TeachingTask!.AcademicTermId && + x.IsEnabled && + x.PeriodNumber >= request.StartPeriod && + x.PeriodNumber < + request.StartPeriod + request.PeriodCount, + cancellationToken); + var usesValidDefaultPeriods = + configuredPeriodCount == 0 && + request.StartPeriod + request.PeriodCount - 1 <= 12; + if (!usesValidDefaultPeriods && + validPeriodCount != request.PeriodCount) + return "所选实验节次不在该学期启用节次范围内。"; + + var classroom = await db.Classrooms.AsNoTracking() + .FirstOrDefaultAsync(x => + x.Id == request.ClassroomId && x.IsEnabled, + cancellationToken); + if (classroom is null) return "实验教室不存在或已停用。"; + if (request.Capacity > classroom.Capacity) + return $"场次容量不能超过教室容量 {classroom.Capacity} 人。"; + if (project.ArrangementMode == + ExperimentArrangementMode.Centralized) + { + var rosterCount = await TeachingTaskRosterQuery + .ForTask(db, project.TeachingTaskId) + .CountAsync(cancellationToken); + if (rosterCount > classroom.Capacity) + return $"集中实验应到 {rosterCount} 人,超过教室容量 {classroom.Capacity} 人。"; + } + + var occupiedRooms = + await classroomAvailability.GetOccupiedClassroomIdsAsync( + project.TeachingTask!.AcademicTerm!, + request.SessionDate, + request.StartPeriod, + request.PeriodCount, + null, + cancellationToken); + if (occupiedRooms.Contains(request.ClassroomId)) + return "所选教室与已发布课程、考试或已批准借用安排冲突。"; + + var experimentRoomConflict = await db.ExperimentSessions.AsNoTracking() + .AnyAsync(x => + x.ClassroomId == request.ClassroomId && + x.SessionDate == request.SessionDate && + x.Status == ExperimentSessionStatus.Scheduled && + x.StartPeriod < + request.StartPeriod + request.PeriodCount && + request.StartPeriod < x.StartPeriod + x.PeriodCount, + cancellationToken); + if (experimentRoomConflict) + return "所选教室与其他实验场次冲突。"; + + if (project.ArrangementMode == + ExperimentArrangementMode.Centralized) + { + return await CentralizedTaskConflictAsync( + project.TeachingTaskId, + project.TeachingTask.AcademicTerm!, + request.SessionDate, + request.StartPeriod, + request.PeriodCount, + cancellationToken); + } + return null; + } + + private async Task CentralizedTaskConflictAsync( + Guid teachingTaskId, + AcademicTerm term, + DateOnly date, + int startPeriod, + int periodCount, + CancellationToken cancellationToken) + { + var teacherIds = await db.TeachingTaskTeachers.AsNoTracking() + .Where(x => x.TeachingTaskId == teachingTaskId) + .Select(x => x.TeacherId) + .ToListAsync(cancellationToken); + var classIds = await db.TeachingTaskClasses.AsNoTracking() + .Where(x => x.TeachingTaskId == teachingTaskId) + .Select(x => x.AdministrativeClassId) + .ToListAsync(cancellationToken); + var (week, dayOfWeek) = + ClassroomReservationAvailabilityService.ResolveTeachingWeek( + term, + date); + + var scheduleConflicts = await db.ScheduleEntries.AsNoTracking() + .Where(x => + x.SchedulePlan!.Status == SchedulePlanStatus.Published && + x.SchedulePlan.AcademicTermId == term.Id && + x.DayOfWeek == dayOfWeek && + x.StartWeek <= week && + x.EndWeek >= week && + x.StartPeriod < startPeriod + periodCount && + startPeriod < x.StartPeriod + x.PeriodCount && + (x.TeachingTask!.Teachers.Any(item => + teacherIds.Contains(item.TeacherId)) || + x.TeachingTask.Classes.Any(item => + classIds.Contains(item.AdministrativeClassId)))) + .Select(x => new { x.TeachingTaskId, x.WeekPattern }) + .ToListAsync(cancellationToken); + if (scheduleConflicts.Any(x => + FreeClassroomRules.MatchesWeek(x.WeekPattern, week))) + return "集中实验与相关教师或行政班的已发布课表冲突。"; + + var experimentConflict = await db.ExperimentSessions.AsNoTracking() + .AnyAsync(x => + x.Status == ExperimentSessionStatus.Scheduled && + x.SessionDate == date && + x.StartPeriod < startPeriod + periodCount && + startPeriod < x.StartPeriod + x.PeriodCount && + x.ExperimentProject!.ArrangementMode == + ExperimentArrangementMode.Centralized && + (x.ExperimentProject.TeachingTask!.Teachers.Any(item => + teacherIds.Contains(item.TeacherId)) || + x.ExperimentProject.TeachingTask.Classes.Any(item => + classIds.Contains(item.AdministrativeClassId))), + cancellationToken); + return experimentConflict + ? "集中实验与相关教师或行政班的其他实验安排冲突。" + : null; + } + + private async Task StudentConflictAsync( + Guid studentId, + ExperimentSession target, + CancellationToken cancellationToken) + { + var project = target.ExperimentProject!; + var term = project.TeachingTask!.AcademicTerm!; + var taskIds = await TeachingTaskRosterQuery + .TaskIdsForStudent(db, studentId) + .ToListAsync(cancellationToken); + var (week, dayOfWeek) = + ClassroomReservationAvailabilityService.ResolveTeachingWeek( + term, + target.SessionDate); + + var scheduleEntries = await db.ScheduleEntries.AsNoTracking() + .Where(x => + taskIds.Contains(x.TeachingTaskId) && + x.SchedulePlan!.Status == SchedulePlanStatus.Published && + x.SchedulePlan.AcademicTermId == term.Id && + x.DayOfWeek == dayOfWeek && + x.StartWeek <= week && + x.EndWeek >= week && + x.StartPeriod < target.StartPeriod + target.PeriodCount && + target.StartPeriod < x.StartPeriod + x.PeriodCount) + .Select(x => x.WeekPattern) + .ToListAsync(cancellationToken); + if (scheduleEntries.Any(pattern => + FreeClassroomRules.MatchesWeek(pattern, week))) + return "该时间与您的已发布课表冲突。"; + + var bookingConflict = await db.ExperimentBookings.AsNoTracking() + .AnyAsync(x => + x.StudentId == studentId && + x.Status == ExperimentBookingStatus.Booked && + x.ExperimentSessionId != target.Id && + x.ExperimentSession!.Status == + ExperimentSessionStatus.Scheduled && + x.ExperimentSession.SessionDate == target.SessionDate && + x.ExperimentSession.StartPeriod < + target.StartPeriod + target.PeriodCount && + target.StartPeriod < + x.ExperimentSession.StartPeriod + + x.ExperimentSession.PeriodCount, + cancellationToken); + if (bookingConflict) return "该时间与您已预约的其他实验冲突。"; + + var centralizedConflict = await db.ExperimentSessions.AsNoTracking() + .AnyAsync(x => + x.Id != target.Id && + x.Status == ExperimentSessionStatus.Scheduled && + x.SessionDate == target.SessionDate && + x.StartPeriod < target.StartPeriod + target.PeriodCount && + target.StartPeriod < x.StartPeriod + x.PeriodCount && + x.ExperimentProject!.Status == + ExperimentProjectStatus.Published && + x.ExperimentProject.ArrangementMode == + ExperimentArrangementMode.Centralized && + taskIds.Contains(x.ExperimentProject.TeachingTaskId), + cancellationToken); + return centralizedConflict + ? "该时间与您的集中实验安排冲突。" + : null; + } + + private IQueryable AccessibleTeachingTasks() + { + var source = db.TeachingTasks.AsQueryable(); + var scope = currentUserDataScope.Current; + if (scope.Scope == DataScope.All) return source; + if (scope.Scope == DataScope.College) + return source.Where(x => + x.Course!.CollegeId == scope.RestrictedCollegeId); + if (scope.IsInRole(SystemRoles.Teacher)) + return source.Where(x => + x.Teachers.Any(item => + item.Teacher!.UserId == scope.UserId)); + return source.Where(_ => false); + } + + private IQueryable ScopedProjects() + { + var taskIds = AccessibleTeachingTasks().Select(x => x.Id); + return db.ExperimentProjects.Where(x => + taskIds.Contains(x.TeachingTaskId)); + } + + private Task CurrentStudentAsync( + CancellationToken cancellationToken) => + db.Students.FirstOrDefaultAsync(x => + x.UserId == currentUserDataScope.Current.UserId && + x.Status == StudentStatus.Active, + cancellationToken); + + private async Task ResolveSessionCapacityAsync( + ExperimentProject project, + int requestedCapacity, + CancellationToken cancellationToken) + { + if (project.ArrangementMode != + ExperimentArrangementMode.Centralized) + return requestedCapacity; + + var rosterCount = await TeachingTaskRosterQuery + .ForTask(db, project.TeachingTaskId) + .CountAsync(cancellationToken); + return rosterCount > 0 ? rosterCount : requestedCapacity; + } + + private Task> RosterUserIdsAsync( + Guid teachingTaskId, + CancellationToken cancellationToken) => + TeachingTaskRosterQuery.ForTask(db, teachingTaskId) + .Where(x => x.UserId.HasValue) + .Select(x => x.UserId!.Value) + .Distinct() + .ToListAsync(cancellationToken); + + private static string? ValidateProjectRequest( + ExperimentProjectRequest request, + AcademicTerm term) + { + if (string.IsNullOrWhiteSpace(request.Code)) + return "请填写实验项目编码。"; + if (string.IsNullOrWhiteSpace(request.Name)) + return "请填写实验项目名称。"; + if (!Enum.IsDefined(request.ArrangementMode)) + return "实验安排方式无效。"; + if (request.StartDate > request.EndDate) + return "项目开始日期不能晚于结束日期。"; + if (request.StartDate < term.StartDate || + request.EndDate > term.EndDate) + return "实验项目日期必须在所属学期起止日期内。"; + return null; + } + + private static string? Normalize(string? value) => + string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + + private ActionResult ConflictProblem(string detail) => + Conflict(new ProblemDetails + { + Title = "无法完成操作", + Detail = detail, + Status = StatusCodes.Status409Conflict + }); +} + +public sealed record ExperimentProjectRequest( + Guid TeachingTaskId, + [Required, MaxLength(40)] string Code, + [Required, MaxLength(120)] string Name, + ExperimentArrangementMode ArrangementMode, + [MaxLength(1000)] string? Description, + [MaxLength(1000)] string? Requirements, + DateOnly StartDate, + DateOnly EndDate); + +public sealed record ExperimentSessionRequest( + Guid ClassroomId, + DateOnly SessionDate, + [Range(1, 30)] int StartPeriod, + [Range(1, 30)] int PeriodCount, + [Range(1, 10000)] int Capacity, + [MaxLength(500)] string? Notes); + +public sealed record ExperimentPeriodOption( + Guid AcademicTermId, + int PeriodNumber, + string Name, + string StartsAt, + string EndsAt); diff --git a/src/Jiaowu.Api/Domain/Academic/ExperimentEntities.cs b/src/Jiaowu.Api/Domain/Academic/ExperimentEntities.cs new file mode 100644 index 0000000..728fc54 --- /dev/null +++ b/src/Jiaowu.Api/Domain/Academic/ExperimentEntities.cs @@ -0,0 +1,79 @@ +using Jiaowu.Api.Domain.Common; + +namespace Jiaowu.Api.Domain.Academic; + +public sealed class ExperimentProject : EntityBase +{ + public Guid TeachingTaskId { get; set; } + public TeachingTask? TeachingTask { get; set; } + public required string Code { get; set; } + public required string Name { get; set; } + public ExperimentArrangementMode ArrangementMode { get; set; } + public string? Description { get; set; } + public string? Requirements { get; set; } + public DateOnly StartDate { get; set; } + public DateOnly EndDate { get; set; } + public ExperimentProjectStatus Status { get; set; } = + ExperimentProjectStatus.Draft; + public DateTime? PublishedAt { get; set; } + public DateTime? ClosedAt { get; set; } + public ICollection Sessions { get; set; } = []; + public ICollection Bookings { get; set; } = []; +} + +public sealed class ExperimentSession : EntityBase +{ + public Guid ExperimentProjectId { get; set; } + public ExperimentProject? ExperimentProject { get; set; } + public Guid ClassroomId { get; set; } + public Classroom? Classroom { get; set; } + public DateOnly SessionDate { get; set; } + public int StartPeriod { get; set; } + public int PeriodCount { get; set; } + public int Capacity { get; set; } + public int ReservedCount { get; set; } + public string? Notes { get; set; } + public ExperimentSessionStatus Status { get; set; } = + ExperimentSessionStatus.Scheduled; + public DateTime? CancelledAt { get; set; } + public ICollection Bookings { get; set; } = []; +} + +public sealed class ExperimentBooking : EntityBase +{ + public Guid ExperimentProjectId { get; set; } + public ExperimentProject? ExperimentProject { get; set; } + public Guid ExperimentSessionId { get; set; } + public ExperimentSession? ExperimentSession { get; set; } + public Guid StudentId { get; set; } + public Student? Student { get; set; } + public ExperimentBookingStatus Status { get; set; } = + ExperimentBookingStatus.Booked; + public DateTime BookedAt { get; set; } = DateTime.UtcNow; + public DateTime? CancelledAt { get; set; } +} + +public enum ExperimentArrangementMode +{ + Centralized = 1, + SelfScheduled = 2 +} + +public enum ExperimentProjectStatus +{ + Draft = 1, + Published = 2, + Closed = 3 +} + +public enum ExperimentSessionStatus +{ + Scheduled = 1, + Cancelled = 2 +} + +public enum ExperimentBookingStatus +{ + Booked = 1, + Cancelled = 2 +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs index 484eece..ce4a3d7 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs @@ -44,6 +44,9 @@ public sealed class AppDbContext(DbContextOptions options) Set(); public DbSet ClassroomReservations => Set(); + public DbSet ExperimentProjects => Set(); + public DbSet ExperimentSessions => Set(); + public DbSet ExperimentBookings => Set(); public DbSet CourseSelectionRounds => Set(); public DbSet CourseSelectionRoundGrades => @@ -547,6 +550,64 @@ public sealed class AppDbContext(DbContextOptions options) .OnDelete(DeleteBehavior.SetNull); }); + builder.Entity(entity => + { + entity.Property(x => x.Code).HasMaxLength(40); + entity.Property(x => x.Name).HasMaxLength(120); + entity.Property(x => x.Description).HasMaxLength(1000); + entity.Property(x => x.Requirements).HasMaxLength(1000); + entity.HasIndex(x => new { x.TeachingTaskId, x.Code }).IsUnique(); + entity.HasIndex(x => new { x.Status, x.StartDate, x.EndDate }); + entity.HasOne(x => x.TeachingTask).WithMany() + .HasForeignKey(x => x.TeachingTaskId) + .OnDelete(DeleteBehavior.Restrict); + }); + + builder.Entity(entity => + { + entity.Property(x => x.Notes).HasMaxLength(500); + entity.HasIndex(x => new + { + x.ExperimentProjectId, + x.SessionDate, + x.StartPeriod + }); + entity.HasIndex(x => new + { + x.ClassroomId, + x.SessionDate, + x.Status, + x.StartPeriod + }); + entity.HasOne(x => x.ExperimentProject).WithMany(x => x.Sessions) + .HasForeignKey(x => x.ExperimentProjectId) + .OnDelete(DeleteBehavior.Cascade); + entity.HasOne(x => x.Classroom).WithMany() + .HasForeignKey(x => x.ClassroomId) + .OnDelete(DeleteBehavior.Restrict); + }); + + builder.Entity(entity => + { + entity.HasIndex(x => new { x.ExperimentProjectId, x.StudentId }) + .IsUnique(); + entity.HasIndex(x => new + { + x.ExperimentSessionId, + x.Status, + x.BookedAt + }); + entity.HasOne(x => x.ExperimentProject).WithMany(x => x.Bookings) + .HasForeignKey(x => x.ExperimentProjectId) + .OnDelete(DeleteBehavior.Cascade); + entity.HasOne(x => x.ExperimentSession).WithMany(x => x.Bookings) + .HasForeignKey(x => x.ExperimentSessionId) + .OnDelete(DeleteBehavior.Restrict); + entity.HasOne(x => x.Student).WithMany() + .HasForeignKey(x => x.StudentId) + .OnDelete(DeleteBehavior.Restrict); + }); + builder.Entity(entity => { entity.Property(x => x.Name).HasMaxLength(120); diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs index 4a137bb..373058f 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs @@ -68,6 +68,10 @@ public sealed class DevelopmentSqliteMigrator( "20260727_36_exam_room_mixing"; private const string AttendanceCheckInAuditMigration = "20260728_37_attendance_check_in_audit"; + private const string ExamPublishJobsMigration = + "20260728_38_exam_publish_jobs"; + private const string ExperimentManagementMigration = + "20260728_39_experiment_management"; public async Task MigrateAsync(CancellationToken cancellationToken = default) { @@ -513,6 +517,32 @@ public sealed class DevelopmentSqliteMigrator( ExamRoomMixingMigration, examRoomsExist ? [] : ExamRoomMixingStatements, cancellationToken); + + var examPublishJobsExist = await db.Database + .SqlQueryRaw( + """ + SELECT COUNT(*) AS "Value" + FROM sqlite_master + WHERE type = 'table' AND name = 'ExamPublishJobs' + """) + .AnyAsync(value => value > 0, cancellationToken); + await ApplyMigrationAsync( + ExamPublishJobsMigration, + examPublishJobsExist ? [] : ExamPublishJobStatements, + cancellationToken); + + var experimentProjectsExist = await db.Database + .SqlQueryRaw( + """ + SELECT COUNT(*) AS "Value" + FROM sqlite_master + WHERE type = 'table' AND name = 'ExperimentProjects' + """) + .AnyAsync(value => value > 0, cancellationToken); + await ApplyMigrationAsync( + ExperimentManagementMigration, + experimentProjectsExist ? [] : ExperimentManagementStatements, + cancellationToken); } private async Task ApplyMigrationAsync( @@ -2351,4 +2381,134 @@ public sealed class DevelopmentSqliteMigrator( ON "ExamRoomInvigilators" ("TeacherId"); """ ]; + + private static readonly string[] ExamPublishJobStatements = + [ + """ + CREATE TABLE "ExamPublishJobs" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_ExamPublishJobs" PRIMARY KEY, + "Kind" INTEGER NOT NULL, + "PlanId" TEXT NOT NULL, + "ActivePlanId" TEXT NULL, + "RequestedByUserId" TEXT NULL, + "Status" INTEGER NOT NULL, + "CurrentStep" TEXT NULL, + "ErrorMessage" TEXT NULL, + "StartedAt" TEXT NULL, + "CompletedAt" TEXT NULL, + "CreatedAt" TEXT NOT NULL, + "UpdatedAt" TEXT NOT NULL + ); + """ + ]; + + private static readonly string[] ExperimentManagementStatements = + [ + """ + CREATE TABLE "ExperimentProjects" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_ExperimentProjects" PRIMARY KEY, + "TeachingTaskId" TEXT NOT NULL, + "Code" TEXT NOT NULL, + "Name" TEXT NOT NULL, + "ArrangementMode" INTEGER NOT NULL, + "Description" TEXT NULL, + "Requirements" TEXT NULL, + "StartDate" TEXT NOT NULL, + "EndDate" TEXT NOT NULL, + "Status" INTEGER NOT NULL, + "PublishedAt" TEXT NULL, + "ClosedAt" TEXT NULL, + "CreatedAt" TEXT NOT NULL, + "UpdatedAt" TEXT NOT NULL, + CONSTRAINT "FK_ExperimentProjects_TeachingTasks_TeachingTaskId" + FOREIGN KEY ("TeachingTaskId") REFERENCES "TeachingTasks" ("Id") + ON DELETE RESTRICT + ); + """, + """ + CREATE UNIQUE INDEX "IX_ExperimentProjects_TeachingTaskId_Code" + ON "ExperimentProjects" ("TeachingTaskId", "Code"); + """, + """ + CREATE INDEX "IX_ExperimentProjects_Status_StartDate_EndDate" + ON "ExperimentProjects" ("Status", "StartDate", "EndDate"); + """, + """ + CREATE TABLE "ExperimentSessions" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_ExperimentSessions" PRIMARY KEY, + "ExperimentProjectId" TEXT NOT NULL, + "ClassroomId" TEXT NOT NULL, + "SessionDate" TEXT NOT NULL, + "StartPeriod" INTEGER NOT NULL, + "PeriodCount" INTEGER NOT NULL, + "Capacity" INTEGER NOT NULL, + "ReservedCount" INTEGER NOT NULL, + "Notes" TEXT NULL, + "Status" INTEGER NOT NULL, + "CancelledAt" TEXT NULL, + "CreatedAt" TEXT NOT NULL, + "UpdatedAt" TEXT NOT NULL, + CONSTRAINT "FK_ExperimentSessions_ExperimentProjects_ExperimentProjectId" + FOREIGN KEY ("ExperimentProjectId") + REFERENCES "ExperimentProjects" ("Id") + ON DELETE CASCADE, + CONSTRAINT "FK_ExperimentSessions_Classrooms_ClassroomId" + FOREIGN KEY ("ClassroomId") REFERENCES "Classrooms" ("Id") + ON DELETE RESTRICT + ); + """, + """ + CREATE INDEX "IX_ExperimentSessions_ExperimentProjectId_SessionDate_StartPeriod" + ON "ExperimentSessions" ( + "ExperimentProjectId", + "SessionDate", + "StartPeriod" + ); + """, + """ + CREATE INDEX "IX_ExperimentSessions_ClassroomId_SessionDate_Status_StartPeriod" + ON "ExperimentSessions" ( + "ClassroomId", + "SessionDate", + "Status", + "StartPeriod" + ); + """, + """ + CREATE TABLE "ExperimentBookings" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_ExperimentBookings" PRIMARY KEY, + "ExperimentProjectId" TEXT NOT NULL, + "ExperimentSessionId" TEXT NOT NULL, + "StudentId" TEXT NOT NULL, + "Status" INTEGER NOT NULL, + "BookedAt" TEXT NOT NULL, + "CancelledAt" TEXT NULL, + "CreatedAt" TEXT NOT NULL, + "UpdatedAt" TEXT NOT NULL, + CONSTRAINT "FK_ExperimentBookings_ExperimentProjects_ExperimentProjectId" + FOREIGN KEY ("ExperimentProjectId") + REFERENCES "ExperimentProjects" ("Id") + ON DELETE CASCADE, + CONSTRAINT "FK_ExperimentBookings_ExperimentSessions_ExperimentSessionId" + FOREIGN KEY ("ExperimentSessionId") + REFERENCES "ExperimentSessions" ("Id") + ON DELETE RESTRICT, + CONSTRAINT "FK_ExperimentBookings_Students_StudentId" + FOREIGN KEY ("StudentId") REFERENCES "Students" ("Id") + ON DELETE RESTRICT + ); + """, + """ + CREATE UNIQUE INDEX "IX_ExperimentBookings_ExperimentProjectId_StudentId" + ON "ExperimentBookings" ("ExperimentProjectId", "StudentId"); + """, + """ + CREATE INDEX "IX_ExperimentBookings_ExperimentSessionId_Status_BookedAt" + ON "ExperimentBookings" ( + "ExperimentSessionId", + "Status", + "BookedAt" + ); + """ + ]; } diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260728122033_ExperimentManagement.Designer.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260728122033_ExperimentManagement.Designer.cs new file mode 100644 index 0000000..af8561f --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260728122033_ExperimentManagement.Designer.cs @@ -0,0 +1,5641 @@ +// +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("20260728122033_ExperimentManagement")] + partial class ExperimentManagement + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AcademicTerm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicYear") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ArchivedAt") + .HasColumnType("datetime(6)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("IsArchived") + .HasColumnType("tinyint(1)"); + + b.Property("IsCurrent") + .HasColumnType("tinyint(1)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Season") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsArchived"); + + b.HasIndex("IsCurrent"); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("AcademicTerms"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AdministrativeClass", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CounselorName") + .HasColumnType("longtext"); + + b.Property("CounselorUserId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Grade") + .HasColumnType("int"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("MajorId") + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("CounselorUserId"); + + b.HasIndex("MajorId"); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("AdministrativeClasses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AttendanceCheckInAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AccuracyMeters") + .HasColumnType("double"); + + b.Property("AttendanceSheetId") + .HasColumnType("char(36)"); + + b.Property("CheckInMethod") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DeviceIdentifierHash") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("DevicePlatform") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("DistanceMeters") + .HasColumnType("double"); + + b.Property("FailureCode") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("IsSuccessful") + .HasColumnType("tinyint(1)"); + + b.Property("Latitude") + .HasPrecision(10, 7) + .HasColumnType("decimal(10,7)"); + + b.Property("Longitude") + .HasPrecision(10, 7) + .HasColumnType("decimal(10,7)"); + + b.Property("RiskFlags") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("StudentId"); + + b.HasIndex("DeviceIdentifierHash", "CreatedAt"); + + b.HasIndex("IpAddress", "CreatedAt"); + + b.HasIndex("AttendanceSheetId", "StudentId", "CreatedAt"); + + b.ToTable("AttendanceCheckInAttempts"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AttendanceRecord", b => + { + b.Property("AttendanceSheetId") + .HasColumnType("char(36)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("AppealReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("AppealReviewComment") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("AppealReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("AppealStatus") + .HasColumnType("int"); + + b.Property("AppealSubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("CheckInAccuracyMeters") + .HasColumnType("double"); + + b.Property("CheckInAt") + .HasColumnType("datetime(6)"); + + b.Property("CheckInDistanceMeters") + .HasColumnType("double"); + + b.Property("CheckInLatitude") + .HasPrecision(10, 7) + .HasColumnType("decimal(10,7)"); + + b.Property("CheckInLongitude") + .HasPrecision(10, 7) + .HasColumnType("decimal(10,7)"); + + b.Property("CheckedInMethod") + .HasColumnType("int"); + + b.Property("Notes") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("AttendanceSheetId", "StudentId"); + + b.HasIndex("AppealStatus"); + + b.HasIndex("StudentId"); + + b.ToTable("AttendanceRecords"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AttendanceSheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AttendanceDate") + .HasColumnType("datetime(6)"); + + b.Property("CheckInEndsAt") + .HasColumnType("datetime(6)"); + + b.Property("CheckInMethod") + .HasColumnType("int"); + + b.Property("CheckInStartsAt") + .HasColumnType("datetime(6)"); + + b.Property("CheckInToken") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("LocationRadiusMeters") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TargetLatitude") + .HasPrecision(10, 7) + .HasColumnType("decimal(10,7)"); + + b.Property("TargetLongitude") + .HasPrecision(10, 7) + .HasColumnType("decimal(10,7)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CheckInToken") + .IsUnique(); + + b.HasIndex("TeachingTaskId", "AttendanceDate"); + + b.ToTable("AttendanceSheets"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AutomaticScheduleJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActiveSchedulePlanId") + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CompletedTasks") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedEntries") + .HasColumnType("int"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessagesJson") + .HasColumnType("longtext"); + + b.Property("ProcessedTasks") + .HasColumnType("int"); + + b.Property("RequestedByUserId") + .HasColumnType("char(36)"); + + b.Property("SchedulePlanId") + .HasColumnType("char(36)"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalTasks") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ActiveSchedulePlanId") + .IsUnique(); + + b.HasIndex("RequestedByUserId"); + + b.HasIndex("SchedulePlanId", "CreatedAt"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("AutomaticScheduleJobs"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Building", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CampusId") + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CampusId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Buildings"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Campus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Address") + .HasColumnType("longtext"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Campuses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Classroom", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BuildingId") + .HasColumnType("char(36)"); + + b.Property("Capacity") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Equipment") + .HasColumnType("longtext"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RoomType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("BuildingId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Classrooms"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ClassroomReservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("ApplicantCollegeId") + .HasColumnType("char(36)"); + + b.Property("ApplicantName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ApplicantUserId") + .HasColumnType("char(36)"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CancelledAt") + .HasColumnType("datetime(6)"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("ContactPhone") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("Purpose") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ReservationDate") + .HasColumnType("date"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewedByUserId") + .HasColumnType("char(36)"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId"); + + b.HasIndex("ReviewedByUserId"); + + b.HasIndex("ApplicantCollegeId", "Status", "ReservationDate"); + + b.HasIndex("ApplicantUserId", "Status", "CreatedAt"); + + b.HasIndex("ClassroomId", "ReservationDate", "Status", "StartPeriod"); + + b.ToTable("ClassroomReservations"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.College", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CampusId") + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ShortName") + .HasColumnType("longtext"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CampusId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Colleges"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Course", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AssessmentMethod") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CollegeId") + .HasColumnType("char(36)"); + + b.Property("CourseCategoryId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Credits") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("EnglishName") + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LectureHours") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Nature") + .HasColumnType("int"); + + b.Property("PracticeHours") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("TotalHours") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("CourseCategoryId"); + + b.HasIndex("CollegeId", "Nature"); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Courses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseAdjustment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApplicantUserId") + .HasColumnType("char(36)"); + + b.Property("CancelDate") + .HasColumnType("date"); + + b.Property("CancelWeek") + .HasColumnType("int"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DayOfWeek") + .HasColumnType("int"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewedByUserId") + .HasColumnType("char(36)"); + + b.Property("SourceClassroomId") + .HasColumnType("char(36)"); + + b.Property("SourceDate") + .HasColumnType("date"); + + b.Property("SourcePeriodCount") + .HasColumnType("int"); + + b.Property("SourceScheduleEntryId") + .HasColumnType("char(36)"); + + b.Property("SourceStartPeriod") + .HasColumnType("int"); + + b.Property("SourceWeek") + .HasColumnType("int"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("SubstituteTeacherId") + .HasColumnType("char(36)"); + + b.Property("TargetDate") + .HasColumnType("date"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantUserId"); + + b.HasIndex("ClassroomId"); + + b.HasIndex("SubstituteTeacherId"); + + b.HasIndex("SourceScheduleEntryId", "SourceWeek"); + + b.HasIndex("Status", "CreatedAt"); + + b.HasIndex("TeachingTaskId", "Status"); + + b.ToTable("CourseAdjustments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("CourseCategories"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CourseSelectionOfferingId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EnrolledAt") + .HasColumnType("datetime(6)"); + + b.Property("EnrollmentType") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WaitlistedAt") + .HasColumnType("datetime(6)"); + + b.Property("WithdrawnAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CourseSelectionOfferingId", "StudentId") + .IsUnique(); + + b.HasIndex("StudentId", "Status"); + + b.HasIndex("CourseSelectionOfferingId", "Status", "WaitlistedAt") + .HasDatabaseName("IX_CE_Offering_Status_WaitlistedAt"); + + b.ToTable("CourseEnrollments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseExemption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewedByUserId") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("Status", "CreatedAt"); + + b.HasIndex("StudentId", "TeachingTaskId") + .IsUnique(); + + b.ToTable("CourseExemptions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CoursePrerequisite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("PrerequisiteCourseId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PrerequisiteCourseId"); + + b.HasIndex("CourseId", "PrerequisiteCourseId") + .IsUnique(); + + b.ToTable("CoursePrerequisites"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionOffering", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Capacity") + .HasColumnType("int"); + + b.Property("CourseSelectionRoundId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsOpenToAll") + .HasColumnType("tinyint(1)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("CourseSelectionRoundId", "TeachingTaskId") + .IsUnique(); + + b.ToTable("CourseSelectionOfferings"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionRound", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndsAt") + .HasColumnType("datetime(6)"); + + b.Property("MaxCourseCount") + .HasColumnType("int"); + + b.Property("MaxCredits") + .HasPrecision(6, 1) + .HasColumnType("decimal(6,1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("StartsAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WithdrawalEndsAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId", "Status"); + + b.ToTable("CourseSelectionRounds"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionRoundGrade", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CourseSelectionRoundId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Grade") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Grade"); + + b.HasIndex("CourseSelectionRoundId", "Grade") + .IsUnique(); + + b.ToTable("CourseSelectionRoundGrades"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSubstitution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("OriginalCourseId") + .HasColumnType("char(36)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewedByUserId") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("SubstituteCourseId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OriginalCourseId"); + + b.HasIndex("SubstituteCourseId"); + + b.HasIndex("Status", "CreatedAt"); + + b.HasIndex("StudentId", "OriginalCourseId") + .IsUnique(); + + b.ToTable("CourseSubstitutions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumCourse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurriculumModuleId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RecommendedSemester") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.HasIndex("CurriculumModuleId", "CourseId") + .IsUnique(); + + b.ToTable("CurriculumCourses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumModule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurriculumPlanId") + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RequiredCredits") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CurriculumPlanId", "Code") + .IsUnique(); + + b.ToTable("CurriculumModules"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("EffectiveGrade") + .HasColumnType("int"); + + b.Property("MajorId") + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalCredits") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.HasKey("Id"); + + b.HasIndex("Status", "EffectiveGrade"); + + b.HasIndex("MajorId", "EffectiveGrade", "Version") + .IsUnique(); + + b.ToTable("CurriculumPlans"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.DeferredExam", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewedByUserId") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("Status", "CreatedAt"); + + b.HasIndex("StudentId", "TeachingTaskId") + .IsUnique(); + + b.ToTable("DeferredExams"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.DegreeAwardBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CalculatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DegreeName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("varchar(80)"); + + b.Property("GraduationYear") + .HasColumnType("int"); + + b.Property("MinimumGradePoint") + .HasPrecision(3, 2) + .HasColumnType("decimal(3,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GraduationYear", "Status"); + + b.ToTable("DegreeAwardBatches"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.DegreeAwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AverageGradePoint") + .HasPrecision(4, 2) + .HasColumnType("decimal(4,2)"); + + b.Property("CalculatedConclusion") + .HasColumnType("int"); + + b.Property("Conclusion") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DegreeAwardBatchId") + .HasColumnType("char(36)"); + + b.Property("ExceptionReason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("GraduationAuditResultId") + .HasColumnType("char(36)"); + + b.Property("IsOverridden") + .HasColumnType("tinyint(1)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GraduationAuditResultId"); + + b.HasIndex("StudentId"); + + b.HasIndex("Conclusion", "IsOverridden"); + + b.HasIndex("DegreeAwardBatchId", "StudentId") + .IsUnique(); + + b.ToTable("DegreeAwardResults"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationDimension", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EvaluationSetupId") + .HasColumnType("char(36)"); + + b.Property("MaxScore") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("varchar(60)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("EvaluationSetupId", "SortOrder"); + + b.ToTable("EvaluationDimensions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EvaluationSetupId") + .HasColumnType("char(36)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("StudentId"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("EvaluationSetupId", "StudentId", "TeachingTaskId") + .IsUnique(); + + b.ToTable("EvaluationRecords"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationScore", b => + { + b.Property("EvaluationRecordId") + .HasColumnType("char(36)"); + + b.Property("EvaluationDimensionId") + .HasColumnType("char(36)"); + + b.Property("Score") + .HasColumnType("int"); + + b.HasKey("EvaluationRecordId", "EvaluationDimensionId"); + + b.HasIndex("EvaluationDimensionId"); + + b.ToTable("EvaluationScores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationSetup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndsAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("StartsAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId", "Status"); + + b.ToTable("EvaluationSetups"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamArrangementJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActivePlanId") + .HasColumnType("char(36)"); + + b.Property("AssignClassrooms") + .HasColumnType("tinyint(1)"); + + b.Property("AssignInvigilators") + .HasColumnType("tinyint(1)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentStep") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("PlanId") + .HasColumnType("char(36)"); + + b.Property("ProcessedSessions") + .HasColumnType("int"); + + b.Property("RequestedByUserId") + .HasColumnType("char(36)"); + + b.Property("ResultMessage") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("SessionIdsJson") + .HasColumnType("longtext"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalSessions") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("RequestedByUserId"); + + b.HasIndex("Kind", "ActivePlanId") + .IsUnique() + .HasDatabaseName("UX_ExamArrangementJobs_Kind_ActivePlan"); + + b.HasIndex("Status", "CreatedAt"); + + b.HasIndex("Kind", "PlanId", "CreatedAt") + .HasDatabaseName("IX_ExamArrangementJobs_Kind_Plan_CreatedAt"); + + b.ToTable("ExamArrangementJobs"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId", "Status"); + + b.ToTable("ExamPlans"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamPublishJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActivePlanId") + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentStep") + .HasColumnType("longtext"); + + b.Property("ErrorMessage") + .HasColumnType("longtext"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("PlanId") + .HasColumnType("char(36)"); + + b.Property("RequestedByUserId") + .HasColumnType("char(36)"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("ExamPublishJobs"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndsAt") + .HasColumnType("datetime(6)"); + + b.Property("ExamDate") + .HasColumnType("date"); + + b.Property("ExamPlanId") + .HasColumnType("char(36)"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("RequiredInvigilatorCount") + .HasColumnType("int"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("StartsAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ClassroomId"); + + b.HasIndex("CourseId") + .HasDatabaseName("IX_ExamRooms_CourseId"); + + b.HasIndex("ExamPlanId", "StartsAt") + .HasDatabaseName("IX_ExamRooms_Plan_Time"); + + b.HasIndex("ExamPlanId", "ClassroomId", "StartsAt") + .HasDatabaseName("IX_ExamRooms_Plan_Room_Time"); + + b.ToTable("ExamRooms", (string)null); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomInvigilator", b => + { + b.Property("ExamRoomId") + .HasColumnType("char(36)"); + + b.Property("TeacherId") + .HasColumnType("char(36)"); + + b.HasKey("ExamRoomId", "TeacherId"); + + b.HasIndex("TeacherId") + .HasDatabaseName("IX_ExamRoomInvigilators_TeacherId"); + + b.ToTable("ExamRoomInvigilators", (string)null); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomSession", b => + { + b.Property("ExamRoomId") + .HasColumnType("char(36)"); + + b.Property("ExamSessionId") + .HasColumnType("char(36)"); + + b.HasKey("ExamRoomId", "ExamSessionId"); + + b.HasIndex("ExamSessionId") + .HasDatabaseName("IX_ExamRoomSessions_SessionId"); + + b.ToTable("ExamRoomSessions", (string)null); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSeatAssignment", b => + { + b.Property("ExamRoomId") + .HasColumnType("char(36)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("ExamSessionId") + .HasColumnType("char(36)"); + + b.Property("SeatNumber") + .HasColumnType("int"); + + b.HasKey("ExamRoomId", "StudentId"); + + b.HasIndex("StudentId") + .HasDatabaseName("IX_ExamSeats_StudentId"); + + b.HasIndex("ExamSessionId", "StudentId") + .IsUnique() + .HasDatabaseName("UX_ExamSeats_Session_Student"); + + b.ToTable("ExamSeats", (string)null); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndsAt") + .HasColumnType("datetime(6)"); + + b.Property("ExamDate") + .HasColumnType("date"); + + b.Property("ExamPlanId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("RequiredBuildingId") + .HasColumnType("char(36)"); + + b.Property("RequiredBuildingIds") + .HasColumnType("longtext"); + + b.Property("RequiredInvigilatorCount") + .HasColumnType("int"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("StartsAt") + .HasColumnType("datetime(6)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ClassroomId"); + + b.HasIndex("RequiredBuildingId"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("ExamPlanId", "ExamDate"); + + b.HasIndex("ExamPlanId", "StartsAt"); + + b.ToTable("ExamSessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSessionInvigilator", b => + { + b.Property("ExamSessionId") + .HasColumnType("char(36)"); + + b.Property("TeacherId") + .HasColumnType("char(36)"); + + b.HasKey("ExamSessionId", "TeacherId"); + + b.HasIndex("TeacherId"); + + b.ToTable("ExamSessionInvigilators"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSignInExportJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentStep") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("FileBytes") + .HasColumnType("longblob"); + + b.Property("FileName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("FileSize") + .HasColumnType("int"); + + b.Property("PlanId") + .HasColumnType("char(36)"); + + b.Property("RequestedByUserId") + .HasColumnType("char(36)"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("RequestedByUserId"); + + b.HasIndex("PlanId", "CreatedAt"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("ExamSignInExportJobs"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentBooking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BookedAt") + .HasColumnType("datetime(6)"); + + b.Property("CancelledAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentProjectId") + .HasColumnType("char(36)"); + + b.Property("ExperimentSessionId") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("StudentId"); + + b.HasIndex("ExperimentProjectId", "StudentId") + .IsUnique(); + + b.HasIndex("ExperimentSessionId", "Status", "BookedAt"); + + b.ToTable("ExperimentBookings"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ArrangementMode") + .HasColumnType("int"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Requirements") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("TeachingTaskId", "Code") + .IsUnique(); + + b.HasIndex("Status", "StartDate", "EndDate"); + + b.ToTable("ExperimentProjects"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CancelledAt") + .HasColumnType("datetime(6)"); + + b.Property("Capacity") + .HasColumnType("int"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentProjectId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("ReservedCount") + .HasColumnType("int"); + + b.Property("SessionDate") + .HasColumnType("date"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ExperimentProjectId", "SessionDate", "StartPeriod"); + + b.HasIndex("ClassroomId", "SessionDate", "Status", "StartPeriod"); + + b.ToTable("ExperimentSessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("GradeSheetId") + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("varchar(60)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Weight") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.HasKey("Id"); + + b.HasIndex("GradeSheetId", "SortOrder"); + + b.ToTable("GradeItems"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItemScore", b => + { + b.Property("GradeRecordId") + .HasColumnType("char(36)"); + + b.Property("GradeItemId") + .HasColumnType("char(36)"); + + b.Property("Score") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.HasKey("GradeRecordId", "GradeItemId"); + + b.HasIndex("GradeItemId"); + + b.ToTable("GradeItemScores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeModification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApplicantUserId") + .HasColumnType("char(36)"); + + b.Property("CollegeReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("CollegeReviewedByUserId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("FinalReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("FinalReviewedByUserId") + .HasColumnType("char(36)"); + + b.Property("GradeRecordId") + .HasColumnType("char(36)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RequestedScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GradeRecordId"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("GradeModifications"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExamStatus") + .HasColumnType("int"); + + b.Property("FinalScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("GradePoint") + .HasPrecision(3, 1) + .HasColumnType("decimal(3,1)"); + + b.Property("GradeSheetId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RegularScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("TotalScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GradeSheetId", "StudentId") + .IsUnique(); + + b.HasIndex("StudentId", "TotalScore"); + + b.ToTable("GradeRecords"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeSheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FinalWeight") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("RegularWeight") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("TeachingTaskId") + .IsUnique(); + + b.ToTable("GradeSheets"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationAuditBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CalculatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EnrollmentYear") + .HasColumnType("int"); + + b.Property("GraduationYear") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("GraduationYear", "EnrollmentYear"); + + b.ToTable("GraduationAuditBatches"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationAuditResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CalculatedConclusion") + .HasColumnType("int"); + + b.Property("Conclusion") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurriculumPlanId") + .HasColumnType("char(36)"); + + b.Property("EarnedCredits") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("FailedCourseCount") + .HasColumnType("int"); + + b.Property("GraduationAuditBatchId") + .HasColumnType("char(36)"); + + b.Property("IsOverridden") + .HasColumnType("tinyint(1)"); + + b.Property("MissingCourseNames") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("PassedRequiredCourseCount") + .HasColumnType("int"); + + b.Property("RequiredCourseCount") + .HasColumnType("int"); + + b.Property("RequiredCredits") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("StudentStatusSnapshot") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CurriculumPlanId"); + + b.HasIndex("StudentId"); + + b.HasIndex("Conclusion", "IsOverridden"); + + b.HasIndex("GraduationAuditBatchId", "StudentId") + .IsUnique(); + + b.ToTable("GraduationAuditResults"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("GraduationYear") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GraduationYear", "Status"); + + b.ToTable("GraduationClearanceBatches"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("GraduationClearanceBatchId") + .HasColumnType("char(36)"); + + b.Property("IsRequired") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ResponsibleRole") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("ResponsibleUnit") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GraduationClearanceBatchId", "Code") + .IsUnique(); + + b.ToTable("GraduationClearanceItems"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CompletedByUserId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("GraduationClearanceItemId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GraduationClearanceItemId", "StudentId") + .IsUnique(); + + b.HasIndex("StudentId", "Status"); + + b.ToTable("GraduationClearanceRecords"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Major", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CollegeId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DegreeType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SchoolingYears") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("CollegeId"); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Majors"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamAutoJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedSessions") + .HasColumnType("int"); + + b.Property("EnrolledStudents") + .HasColumnType("int"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MakeupExamPlanId") + .HasColumnType("char(36)"); + + b.Property("MessagesJson") + .HasColumnType("longtext"); + + b.Property("ProcessedCourses") + .HasColumnType("int"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalCourses") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("MakeupExamPlanId", "CreatedAt"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("MakeupExamAutoJobs"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamEnrollment", b => + { + b.Property("MakeupExamSessionId") + .HasColumnType("char(36)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("MakeupScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("Reason") + .HasColumnType("int"); + + b.Property("SourceDeferredExamId") + .HasColumnType("char(36)"); + + b.Property("SourceGradeRecordId") + .HasColumnType("char(36)"); + + b.HasKey("MakeupExamSessionId", "StudentId"); + + b.HasIndex("SourceDeferredExamId"); + + b.HasIndex("SourceGradeRecordId"); + + b.HasIndex("StudentId", "MakeupExamSessionId"); + + b.ToTable("MakeupExamEnrollments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId", "Status"); + + b.ToTable("MakeupExamPlans"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndsAt") + .HasColumnType("datetime(6)"); + + b.Property("ExamDate") + .HasColumnType("date"); + + b.Property("MakeupExamPlanId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("RequiredBuildingId") + .HasColumnType("char(36)"); + + b.Property("RequiredBuildingIds") + .HasColumnType("longtext"); + + b.Property("RequiredInvigilatorCount") + .HasColumnType("int"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("StartsAt") + .HasColumnType("datetime(6)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ClassroomId"); + + b.HasIndex("RequiredBuildingId"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("MakeupExamPlanId", "ExamDate"); + + b.HasIndex("MakeupExamPlanId", "StartsAt"); + + b.ToTable("MakeupExamSessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamSessionInvigilator", b => + { + b.Property("MakeupExamSessionId") + .HasColumnType("char(36)"); + + b.Property("TeacherId") + .HasColumnType("char(36)"); + + b.HasKey("MakeupExamSessionId", "TeacherId"); + + b.HasIndex("TeacherId"); + + b.ToTable("MakeupExamSessionInvigilators"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MessageDispatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AudienceId") + .HasColumnType("char(36)"); + + b.Property("AudienceName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("AudienceType") + .HasColumnType("int"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("Content") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("LinkUrl") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RecipientCount") + .HasColumnType("int"); + + b.Property("SenderName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SenderUserId") + .HasColumnType("char(36)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SenderUserId", "CreatedAt"); + + b.ToTable("MessageDispatches"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("Content") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsRead") + .HasColumnType("tinyint(1)"); + + b.Property("LinkUrl") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MessageDispatchId") + .HasColumnType("char(36)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("MessageDispatchId"); + + b.HasIndex("UserId", "IsRead"); + + b.HasIndex("UserId", "Category", "CreatedAt"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DocumentNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvalidatedAt") + .HasColumnType("datetime(6)"); + + b.Property("InvalidatedByUserId") + .HasColumnType("char(36)"); + + b.Property("InvalidationReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IssuedAt") + .HasColumnType("datetime(6)"); + + b.Property("IssuedByUserId") + .HasColumnType("char(36)"); + + b.Property("PdfContent") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("PdfSha256") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Purpose") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ReissuedFromDocumentId") + .HasColumnType("char(36)"); + + b.Property("SnapshotJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("VerificationCodeHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentNumber") + .IsUnique(); + + b.HasIndex("InvalidatedByUserId"); + + b.HasIndex("IssuedByUserId"); + + b.HasIndex("ReissuedFromDocumentId") + .IsUnique(); + + b.HasIndex("VerificationCodeHash") + .IsUnique(); + + b.HasIndex("Status", "IssuedAt"); + + b.HasIndex("StudentId", "IssuedAt"); + + b.ToTable("OfficialDocuments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocumentDownload", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DownloadedByUserId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("OfficialDocumentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("DownloadedByUserId", "CreatedAt"); + + b.HasIndex("OfficialDocumentId", "CreatedAt"); + + b.ToTable("OfficialDocumentDownloads"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.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("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("SchedulePlanId") + .HasColumnType("char(36)"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("StartWeek") + .HasColumnType("int"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WeekPattern") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ClassroomId"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("SchedulePlanId", "DayOfWeek", "StartPeriod"); + + b.ToTable("ScheduleEntries"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.SchedulePlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId", "Status"); + + b.HasIndex("AcademicTermId", "Version") + .IsUnique(); + + b.ToTable("SchedulePlans"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.SchedulePublishJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("ActiveAcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CompletedSteps") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentStep") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("RequestedByUserId") + .HasColumnType("char(36)"); + + b.Property("SchedulePlanId") + .HasColumnType("char(36)"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalSteps") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ActiveAcademicTermId") + .IsUnique(); + + b.HasIndex("RequestedByUserId"); + + b.HasIndex("SchedulePlanId", "CreatedAt"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("SchedulePublishJobs"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ScheduleTimeSlot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndsAt") + .HasColumnType("time"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("PeriodNumber") + .HasColumnType("int"); + + b.Property("StartsAt") + .HasColumnType("time"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId", "PeriodNumber") + .IsUnique(); + + b.ToTable("ScheduleTimeSlots"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Student", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AdministrativeClassId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("Email") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("EnrollmentDate") + .HasColumnType("date"); + + b.Property("EnrollmentYear") + .HasColumnType("int"); + + b.Property("Gender") + .HasColumnType("int"); + + 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("StudentNumber") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("EnrollmentYear"); + + b.HasIndex("StudentNumber") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("AdministrativeClassId", "Status"); + + b.ToTable("Students"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.StudentStatusChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApprovedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("OriginalStatus") + .HasColumnType("int"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TargetStatus") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("StudentId", "State"); + + b.ToTable("StudentStatusChanges"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Teacher", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CollegeId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gender") + .HasColumnType("int"); + + b.Property("HireDate") + .HasColumnType("date"); + + b.Property("IsExternal") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TeacherNumber") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("Title") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TeacherNumber") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("CollegeId", "Status"); + + b.ToTable("Teachers"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeacherCourseApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewedByUserId") + .HasColumnType("char(36)"); + + b.Property("Statement") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TeacherId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.HasIndex("ReviewedByUserId"); + + b.HasIndex("TeacherId"); + + b.HasIndex("Status", "AcademicTermId"); + + b.HasIndex("AcademicTermId", "TeacherId", "CourseId") + .IsUnique(); + + b.ToTable("TeacherCourseApplications"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("Capacity") + .HasColumnType("int"); + + b.Property("CourseId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndWeek") + .HasColumnType("int"); + + b.Property("GenerationBatchCode") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("SchedulingMode") + .HasColumnType("int"); + + b.Property("StartWeek") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TaskNumber") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WeeklyHours") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.HasIndex("TaskNumber") + .IsUnique(); + + b.HasIndex("AcademicTermId", "Status"); + + b.ToTable("TeachingTasks"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskAllowedClassroom", b => + { + b.Property("TeachingTaskScheduleConstraintId") + .HasColumnType("char(36)"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.HasKey("TeachingTaskScheduleConstraintId", "ClassroomId"); + + b.HasIndex("ClassroomId"); + + b.ToTable("TeachingTaskAllowedClassrooms"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskClass", b => + { + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("AdministrativeClassId") + .HasColumnType("char(36)"); + + b.HasKey("TeachingTaskId", "AdministrativeClassId"); + + b.HasIndex("AdministrativeClassId"); + + b.ToTable("TeachingTaskClasses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllowedDayOfWeeks") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EarliestPeriod") + .HasColumnType("int"); + + b.Property("LatestPeriod") + .HasColumnType("int"); + + b.Property("RequiredBuildingId") + .HasColumnType("char(36)"); + + b.Property("RequiredCampusId") + .HasColumnType("char(36)"); + + b.Property("RequiresClassroom") + .HasColumnType("tinyint(1)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("RequiredBuildingId"); + + b.HasIndex("RequiredCampusId"); + + b.HasIndex("TeachingTaskId") + .IsUnique(); + + b.ToTable("TeachingTaskScheduleConstraints"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskTeacher", b => + { + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("TeacherId") + .HasColumnType("char(36)"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)"); + + b.HasKey("TeachingTaskId", "TeacherId"); + + b.HasIndex("TeacherId"); + + b.ToTable("TeachingTaskTeachers"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.WarningRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("AcknowledgeComment") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("AcknowledgedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Detail") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("TriggerValue") + .HasPrecision(7, 2) + .HasColumnType("decimal(7,2)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("StudentId", "AcademicTermId", "Type") + .IsUnique(); + + b.ToTable("WarningRecords"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.WarningRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("AutoCheckEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("CheckDayOfWeek") + .HasColumnType("int"); + + b.Property("CheckHour") + .HasColumnType("int"); + + b.Property("CheckMinute") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LastCheckAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("NotifyCounselor") + .HasColumnType("tinyint(1)"); + + b.Property("NotifyStudent") + .HasColumnType("tinyint(1)"); + + b.Property("Threshold") + .HasPrecision(7, 2) + .HasColumnType("decimal(7,2)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId", "Type") + .IsUnique(); + + b.ToTable("WarningRules"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Identity.ApplicationRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("DataScope") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Identity.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("CalendarSubscriptionCreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CalendarSubscriptionStamp") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CollegeId") + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LastLoginAt") + .HasColumnType("datetime(6)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("StaffNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("StaffNumber"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.System.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Method") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("StatusCode") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("UserName") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.System.BackgroundJobOutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("JobId") + .HasColumnType("char(36)"); + + b.Property("JobKind") + .HasColumnType("int"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("LeaseExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("ProcessingAttempts") + .HasColumnType("int"); + + b.Property("ProcessingToken") + .HasColumnType("char(36)"); + + b.Property("PublishAttempts") + .HasColumnType("int"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("LeaseExpiresAt"); + + b.HasIndex("JobKind", "JobId") + .IsUnique(); + + b.HasIndex("State", "CompletedAt"); + + b.HasIndex("State", "CreatedAt"); + + b.ToTable("BackgroundJobOutboxMessages"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AdministrativeClass", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "CounselorUser") + .WithMany() + .HasForeignKey("CounselorUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.Major", "Major") + .WithMany() + .HasForeignKey("MajorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CounselorUser"); + + b.Navigation("Major"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AttendanceCheckInAttempt", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AttendanceSheet", "AttendanceSheet") + .WithMany("CheckInAttempts") + .HasForeignKey("AttendanceSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AttendanceSheet"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AttendanceRecord", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AttendanceSheet", "AttendanceSheet") + .WithMany("Records") + .HasForeignKey("AttendanceSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AttendanceSheet"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AttendanceSheet", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AutomaticScheduleJob", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("RequestedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.SchedulePlan", "SchedulePlan") + .WithMany() + .HasForeignKey("SchedulePlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SchedulePlan"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Building", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Campus", "Campus") + .WithMany() + .HasForeignKey("CampusId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Campus"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Classroom", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Building", "Building") + .WithMany() + .HasForeignKey("BuildingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Building"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ClassroomReservation", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.College", "ApplicantCollege") + .WithMany() + .HasForeignKey("ApplicantCollegeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "ApplicantUser") + .WithMany() + .HasForeignKey("ApplicantUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "ReviewedByUser") + .WithMany() + .HasForeignKey("ReviewedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AcademicTerm"); + + b.Navigation("ApplicantCollege"); + + b.Navigation("ApplicantUser"); + + b.Navigation("Classroom"); + + b.Navigation("ReviewedByUser"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.College", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Campus", "Campus") + .WithMany() + .HasForeignKey("CampusId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Campus"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Course", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.College", "College") + .WithMany() + .HasForeignKey("CollegeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.CourseCategory", "CourseCategory") + .WithMany() + .HasForeignKey("CourseCategoryId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("College"); + + b.Navigation("CourseCategory"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseAdjustment", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.Teacher", "SubstituteTeacher") + .WithMany() + .HasForeignKey("SubstituteTeacherId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("SubstituteTeacher"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseEnrollment", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.CourseSelectionOffering", "CourseSelectionOffering") + .WithMany("Enrollments") + .HasForeignKey("CourseSelectionOfferingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CourseSelectionOffering"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseExemption", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Student"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CoursePrerequisite", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "Course") + .WithMany("Prerequisites") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "PrerequisiteCourse") + .WithMany("RequiredByCourses") + .HasForeignKey("PrerequisiteCourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Course"); + + b.Navigation("PrerequisiteCourse"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionOffering", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.CourseSelectionRound", "CourseSelectionRound") + .WithMany("Offerings") + .HasForeignKey("CourseSelectionRoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CourseSelectionRound"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionRound", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AcademicTerm"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionRoundGrade", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.CourseSelectionRound", "CourseSelectionRound") + .WithMany("EligibleGrades") + .HasForeignKey("CourseSelectionRoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CourseSelectionRound"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSubstitution", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "OriginalCourse") + .WithMany() + .HasForeignKey("OriginalCourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "SubstituteCourse") + .WithMany() + .HasForeignKey("SubstituteCourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("OriginalCourse"); + + b.Navigation("Student"); + + b.Navigation("SubstituteCourse"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumCourse", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "Course") + .WithMany() + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.CurriculumModule", "CurriculumModule") + .WithMany("Courses") + .HasForeignKey("CurriculumModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Course"); + + b.Navigation("CurriculumModule"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumModule", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.CurriculumPlan", "CurriculumPlan") + .WithMany("Modules") + .HasForeignKey("CurriculumPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurriculumPlan"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumPlan", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Major", "Major") + .WithMany() + .HasForeignKey("MajorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Major"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.DeferredExam", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Student"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.DegreeAwardResult", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.DegreeAwardBatch", "DegreeAwardBatch") + .WithMany("Results") + .HasForeignKey("DegreeAwardBatchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.GraduationAuditResult", "GraduationAuditResult") + .WithMany() + .HasForeignKey("GraduationAuditResultId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DegreeAwardBatch"); + + b.Navigation("GraduationAuditResult"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationDimension", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.EvaluationSetup", "EvaluationSetup") + .WithMany("Dimensions") + .HasForeignKey("EvaluationSetupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EvaluationSetup"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationRecord", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.EvaluationSetup", "EvaluationSetup") + .WithMany("Records") + .HasForeignKey("EvaluationSetupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("EvaluationSetup"); + + b.Navigation("Student"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationScore", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.EvaluationDimension", "EvaluationDimension") + .WithMany("Scores") + .HasForeignKey("EvaluationDimensionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.EvaluationRecord", "EvaluationRecord") + .WithMany("Scores") + .HasForeignKey("EvaluationRecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EvaluationDimension"); + + b.Navigation("EvaluationRecord"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationSetup", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AcademicTerm"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamPlan", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AcademicTerm"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomAssignment", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "Course") + .WithMany() + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExamPlan", "ExamPlan") + .WithMany("Rooms") + .HasForeignKey("ExamPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("Course"); + + b.Navigation("ExamPlan"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomInvigilator", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExamRoomAssignment", "ExamRoom") + .WithMany("Invigilators") + .HasForeignKey("ExamRoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Teacher", "Teacher") + .WithMany() + .HasForeignKey("TeacherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ExamRoom"); + + b.Navigation("Teacher"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomSession", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExamRoomAssignment", "ExamRoom") + .WithMany("SessionLinks") + .HasForeignKey("ExamRoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExamSession", "ExamSession") + .WithMany("RoomLinks") + .HasForeignKey("ExamSessionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ExamRoom"); + + b.Navigation("ExamSession"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSeatAssignment", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExamRoomAssignment", "ExamRoom") + .WithMany("Seats") + .HasForeignKey("ExamRoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExamSession", "ExamSession") + .WithMany("SeatAssignments") + .HasForeignKey("ExamSessionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ExamRoom"); + + b.Navigation("ExamSession"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSession", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExamPlan", "ExamPlan") + .WithMany("Sessions") + .HasForeignKey("ExamPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Building", "RequiredBuilding") + .WithMany() + .HasForeignKey("RequiredBuildingId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("ExamPlan"); + + b.Navigation("RequiredBuilding"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSessionInvigilator", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExamSession", "ExamSession") + .WithMany("Invigilators") + .HasForeignKey("ExamSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Teacher", "Teacher") + .WithMany() + .HasForeignKey("TeacherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ExamSession"); + + b.Navigation("Teacher"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentBooking", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentProject", "ExperimentProject") + .WithMany("Bookings") + .HasForeignKey("ExperimentProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentSession", "ExperimentSession") + .WithMany("Bookings") + .HasForeignKey("ExperimentSessionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ExperimentProject"); + + b.Navigation("ExperimentSession"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentSession", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentProject", "ExperimentProject") + .WithMany("Sessions") + .HasForeignKey("ExperimentProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("ExperimentProject"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItem", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GradeSheet", "GradeSheet") + .WithMany("Items") + .HasForeignKey("GradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GradeSheet"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItemScore", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GradeItem", "GradeItem") + .WithMany("Scores") + .HasForeignKey("GradeItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.GradeRecord", "GradeRecord") + .WithMany("ItemScores") + .HasForeignKey("GradeRecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GradeItem"); + + b.Navigation("GradeRecord"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeModification", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GradeRecord", "GradeRecord") + .WithMany() + .HasForeignKey("GradeRecordId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GradeRecord"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeRecord", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GradeSheet", "GradeSheet") + .WithMany("Records") + .HasForeignKey("GradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GradeSheet"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeSheet", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationAuditResult", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.CurriculumPlan", "CurriculumPlan") + .WithMany() + .HasForeignKey("CurriculumPlanId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.GraduationAuditBatch", "GraduationAuditBatch") + .WithMany("Results") + .HasForeignKey("GraduationAuditBatchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CurriculumPlan"); + + b.Navigation("GraduationAuditBatch"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceItem", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GraduationClearanceBatch", "GraduationClearanceBatch") + .WithMany("Items") + .HasForeignKey("GraduationClearanceBatchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GraduationClearanceBatch"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceRecord", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GraduationClearanceItem", "GraduationClearanceItem") + .WithMany("Records") + .HasForeignKey("GraduationClearanceItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GraduationClearanceItem"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Major", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.College", "College") + .WithMany() + .HasForeignKey("CollegeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("College"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamAutoJob", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.MakeupExamPlan", "MakeupExamPlan") + .WithMany() + .HasForeignKey("MakeupExamPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MakeupExamPlan"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamEnrollment", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.MakeupExamSession", "MakeupExamSession") + .WithMany("Enrollments") + .HasForeignKey("MakeupExamSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.DeferredExam", "SourceDeferredExam") + .WithMany() + .HasForeignKey("SourceDeferredExamId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.GradeRecord", "SourceGradeRecord") + .WithMany() + .HasForeignKey("SourceGradeRecordId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("MakeupExamSession"); + + b.Navigation("SourceDeferredExam"); + + b.Navigation("SourceGradeRecord"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamPlan", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AcademicTerm"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamSession", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.MakeupExamPlan", "MakeupExamPlan") + .WithMany("Sessions") + .HasForeignKey("MakeupExamPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Building", "RequiredBuilding") + .WithMany() + .HasForeignKey("RequiredBuildingId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("MakeupExamPlan"); + + b.Navigation("RequiredBuilding"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamSessionInvigilator", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.MakeupExamSession", "MakeupExamSession") + .WithMany("Invigilators") + .HasForeignKey("MakeupExamSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Teacher", "Teacher") + .WithMany() + .HasForeignKey("TeacherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("MakeupExamSession"); + + b.Navigation("Teacher"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Notification", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.MessageDispatch", "MessageDispatch") + .WithMany("Notifications") + .HasForeignKey("MessageDispatchId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("MessageDispatch"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocument", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "InvalidatedByUser") + .WithMany() + .HasForeignKey("InvalidatedByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "IssuedByUser") + .WithMany() + .HasForeignKey("IssuedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.OfficialDocument", "ReissuedFromDocument") + .WithMany() + .HasForeignKey("ReissuedFromDocumentId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("InvalidatedByUser"); + + b.Navigation("IssuedByUser"); + + b.Navigation("ReissuedFromDocument"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocumentDownload", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "DownloadedByUser") + .WithMany() + .HasForeignKey("DownloadedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.OfficialDocument", "OfficialDocument") + .WithMany("Downloads") + .HasForeignKey("OfficialDocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DownloadedByUser"); + + b.Navigation("OfficialDocument"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ScheduleEntry", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.SchedulePlan", "SchedulePlan") + .WithMany("Entries") + .HasForeignKey("SchedulePlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("SchedulePlan"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.SchedulePlan", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AcademicTerm"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.SchedulePublishJob", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("RequestedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.SchedulePlan", "SchedulePlan") + .WithMany() + .HasForeignKey("SchedulePlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SchedulePlan"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ScheduleTimeSlot", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AcademicTerm"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Student", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AdministrativeClass", "AdministrativeClass") + .WithMany("Students") + .HasForeignKey("AdministrativeClassId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AdministrativeClass"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.StudentStatusChange", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Teacher", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.College", "College") + .WithMany() + .HasForeignKey("CollegeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("College"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeacherCourseApplication", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "Course") + .WithMany() + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("ReviewedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.Teacher", "Teacher") + .WithMany() + .HasForeignKey("TeacherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AcademicTerm"); + + b.Navigation("Course"); + + b.Navigation("Teacher"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTask", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Course", "Course") + .WithMany() + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AcademicTerm"); + + b.Navigation("Course"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskAllowedClassroom", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", "TeachingTaskScheduleConstraint") + .WithMany("AllowedClassrooms") + .HasForeignKey("TeachingTaskScheduleConstraintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("TeachingTaskScheduleConstraint"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskClass", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AdministrativeClass", "AdministrativeClass") + .WithMany() + .HasForeignKey("AdministrativeClassId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany("Classes") + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AdministrativeClass"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Building", "RequiredBuilding") + .WithMany() + .HasForeignKey("RequiredBuildingId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.Campus", "RequiredCampus") + .WithMany() + .HasForeignKey("RequiredCampusId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RequiredBuilding"); + + b.Navigation("RequiredCampus"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskTeacher", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Teacher", "Teacher") + .WithMany() + .HasForeignKey("TeacherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany("Teachers") + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Teacher"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.WarningRecord", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.WarningRule", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AcademicTerm"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AdministrativeClass", b => + { + b.Navigation("Students"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AttendanceSheet", b => + { + b.Navigation("CheckInAttempts"); + + b.Navigation("Records"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Course", b => + { + b.Navigation("Prerequisites"); + + b.Navigation("RequiredByCourses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionOffering", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionRound", b => + { + b.Navigation("EligibleGrades"); + + b.Navigation("Offerings"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumModule", b => + { + b.Navigation("Courses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumPlan", b => + { + b.Navigation("Modules"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.DegreeAwardBatch", b => + { + b.Navigation("Results"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationDimension", b => + { + b.Navigation("Scores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationRecord", b => + { + b.Navigation("Scores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.EvaluationSetup", b => + { + b.Navigation("Dimensions"); + + b.Navigation("Records"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamPlan", b => + { + b.Navigation("Rooms"); + + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomAssignment", b => + { + b.Navigation("Invigilators"); + + b.Navigation("Seats"); + + b.Navigation("SessionLinks"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSession", b => + { + b.Navigation("Invigilators"); + + b.Navigation("RoomLinks"); + + b.Navigation("SeatAssignments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b => + { + b.Navigation("Bookings"); + + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentSession", b => + { + b.Navigation("Bookings"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItem", b => + { + b.Navigation("Scores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeRecord", b => + { + b.Navigation("ItemScores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeSheet", b => + { + b.Navigation("Items"); + + b.Navigation("Records"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationAuditBatch", b => + { + b.Navigation("Results"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceBatch", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceItem", b => + { + b.Navigation("Records"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamPlan", b => + { + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamSession", b => + { + b.Navigation("Enrollments"); + + b.Navigation("Invigilators"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MessageDispatch", b => + { + b.Navigation("Notifications"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocument", b => + { + b.Navigation("Downloads"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.SchedulePlan", b => + { + b.Navigation("Entries"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTask", b => + { + b.Navigation("Classes"); + + b.Navigation("Teachers"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", b => + { + b.Navigation("AllowedClassrooms"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260728122033_ExperimentManagement.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260728122033_ExperimentManagement.cs new file mode 100644 index 0000000..bb3c26a --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260728122033_ExperimentManagement.cs @@ -0,0 +1,170 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql +{ + /// + public partial class ExperimentManagement : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ExperimentProjects", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + TeachingTaskId = table.Column(type: "char(36)", nullable: false), + Code = table.Column(type: "varchar(40)", maxLength: 40, nullable: false), + Name = table.Column(type: "varchar(120)", maxLength: 120, nullable: false), + ArrangementMode = table.Column(type: "int", nullable: false), + Description = table.Column(type: "varchar(1000)", maxLength: 1000, nullable: true), + Requirements = table.Column(type: "varchar(1000)", maxLength: 1000, nullable: true), + StartDate = table.Column(type: "date", nullable: false), + EndDate = table.Column(type: "date", nullable: false), + Status = table.Column(type: "int", nullable: false), + PublishedAt = table.Column(type: "datetime(6)", nullable: true), + ClosedAt = table.Column(type: "datetime(6)", nullable: true), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExperimentProjects", x => x.Id); + table.ForeignKey( + name: "FK_ExperimentProjects_TeachingTasks_TeachingTaskId", + column: x => x.TeachingTaskId, + principalTable: "TeachingTasks", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "ExperimentSessions", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + ExperimentProjectId = table.Column(type: "char(36)", nullable: false), + ClassroomId = table.Column(type: "char(36)", nullable: false), + SessionDate = table.Column(type: "date", nullable: false), + StartPeriod = table.Column(type: "int", nullable: false), + PeriodCount = table.Column(type: "int", nullable: false), + Capacity = table.Column(type: "int", nullable: false), + ReservedCount = table.Column(type: "int", nullable: false), + Notes = table.Column(type: "varchar(500)", maxLength: 500, nullable: true), + Status = table.Column(type: "int", nullable: false), + CancelledAt = table.Column(type: "datetime(6)", nullable: true), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExperimentSessions", x => x.Id); + table.ForeignKey( + name: "FK_ExperimentSessions_Classrooms_ClassroomId", + column: x => x.ClassroomId, + principalTable: "Classrooms", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_ExperimentSessions_ExperimentProjects_ExperimentProjectId", + column: x => x.ExperimentProjectId, + principalTable: "ExperimentProjects", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "ExperimentBookings", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + ExperimentProjectId = table.Column(type: "char(36)", nullable: false), + ExperimentSessionId = table.Column(type: "char(36)", nullable: false), + StudentId = table.Column(type: "char(36)", nullable: false), + Status = table.Column(type: "int", nullable: false), + BookedAt = table.Column(type: "datetime(6)", nullable: false), + CancelledAt = table.Column(type: "datetime(6)", nullable: true), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExperimentBookings", x => x.Id); + table.ForeignKey( + name: "FK_ExperimentBookings_ExperimentProjects_ExperimentProjectId", + column: x => x.ExperimentProjectId, + principalTable: "ExperimentProjects", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ExperimentBookings_ExperimentSessions_ExperimentSessionId", + column: x => x.ExperimentSessionId, + principalTable: "ExperimentSessions", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_ExperimentBookings_Students_StudentId", + column: x => x.StudentId, + principalTable: "Students", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_ExperimentBookings_ExperimentProjectId_StudentId", + table: "ExperimentBookings", + columns: new[] { "ExperimentProjectId", "StudentId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ExperimentBookings_ExperimentSessionId_Status_BookedAt", + table: "ExperimentBookings", + columns: new[] { "ExperimentSessionId", "Status", "BookedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_ExperimentBookings_StudentId", + table: "ExperimentBookings", + column: "StudentId"); + + migrationBuilder.CreateIndex( + name: "IX_ExperimentProjects_Status_StartDate_EndDate", + table: "ExperimentProjects", + columns: new[] { "Status", "StartDate", "EndDate" }); + + migrationBuilder.CreateIndex( + name: "IX_ExperimentProjects_TeachingTaskId_Code", + table: "ExperimentProjects", + columns: new[] { "TeachingTaskId", "Code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ExperimentSessions_ClassroomId_SessionDate_Status_StartPeriod", + table: "ExperimentSessions", + columns: new[] { "ClassroomId", "SessionDate", "Status", "StartPeriod" }); + + migrationBuilder.CreateIndex( + name: "IX_ExperimentSessions_ExperimentProjectId_SessionDate_StartPeri~", + table: "ExperimentSessions", + columns: new[] { "ExperimentProjectId", "SessionDate", "StartPeriod" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ExperimentBookings"); + + migrationBuilder.DropTable( + name: "ExperimentSessions"); + + migrationBuilder.DropTable( + name: "ExperimentProjects"); + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs index 35f992e..6f52d3e 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs @@ -2001,6 +2001,161 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql 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.ExperimentProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ArrangementMode") + .HasColumnType("int"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Requirements") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("TeachingTaskId", "Code") + .IsUnique(); + + b.HasIndex("Status", "StartDate", "EndDate"); + + b.ToTable("ExperimentProjects"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CancelledAt") + .HasColumnType("datetime(6)"); + + b.Property("Capacity") + .HasColumnType("int"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentProjectId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("ReservedCount") + .HasColumnType("int"); + + b.Property("SessionDate") + .HasColumnType("date"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ExperimentProjectId", "SessionDate", "StartPeriod"); + + b.HasIndex("ClassroomId", "SessionDate", "Status", "StartPeriod"); + + b.ToTable("ExperimentSessions"); + }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItem", b => { b.Property("Id") @@ -4630,6 +4785,63 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql 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.ExperimentProject", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentSession", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentProject", "ExperimentProject") + .WithMany("Sessions") + .HasForeignKey("ExperimentProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("ExperimentProject"); + }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItem", b => { b.HasOne("Jiaowu.Api.Domain.Academic.GradeSheet", "GradeSheet") @@ -5338,6 +5550,18 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.Navigation("SeatAssignments"); }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b => + { + b.Navigation("Bookings"); + + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentSession", b => + { + b.Navigation("Bookings"); + }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItem", b => { b.Navigation("Scores"); diff --git a/src/Jiaowu.Api/Infrastructure/Timetables/ClassroomReservationAvailabilityService.cs b/src/Jiaowu.Api/Infrastructure/Timetables/ClassroomReservationAvailabilityService.cs index 8ac097f..d4fb481 100644 --- a/src/Jiaowu.Api/Infrastructure/Timetables/ClassroomReservationAvailabilityService.cs +++ b/src/Jiaowu.Api/Infrastructure/Timetables/ClassroomReservationAvailabilityService.cs @@ -81,6 +81,20 @@ public sealed class ClassroomReservationAvailabilityService(AppDbContext db) .ToListAsync(cancellationToken); occupiedIds.UnionWith(makeupExamRoomIds); + var experimentRoomIds = await db.ExperimentSessions.AsNoTracking() + .Where(session => + session.ExperimentProject!.TeachingTask!.AcademicTermId == + term.Id && + session.ExperimentProject.Status != + ExperimentProjectStatus.Closed && + session.Status == ExperimentSessionStatus.Scheduled && + session.SessionDate == reservationDate && + session.StartPeriod < startPeriod + periodCount && + startPeriod < session.StartPeriod + session.PeriodCount) + .Select(session => session.ClassroomId) + .ToListAsync(cancellationToken); + occupiedIds.UnionWith(experimentRoomIds); + var reservationQuery = db.ClassroomReservations.AsNoTracking() .Where(reservation => reservation.AcademicTermId == term.Id && diff --git a/tests/Jiaowu.Api.Tests/ExperimentsControllerTests.cs b/tests/Jiaowu.Api.Tests/ExperimentsControllerTests.cs new file mode 100644 index 0000000..2a98a25 --- /dev/null +++ b/tests/Jiaowu.Api.Tests/ExperimentsControllerTests.cs @@ -0,0 +1,394 @@ +using Jiaowu.Api.Controllers; +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Infrastructure.Auth; +using Jiaowu.Api.Infrastructure.Persistence; +using Jiaowu.Api.Infrastructure.Timetables; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace Jiaowu.Api.Tests; + +public sealed class ExperimentsControllerTests +{ + [Fact] + public async Task CentralizedProject_PublishesAndUsesTeachingTaskRoster() + { + await using var fixture = await ExperimentFixture.CreateAsync(); + var controller = fixture.Controller(fixture.ManagerScope); + var created = await controller.CreateProject( + fixture.ProjectRequest(ExperimentArrangementMode.Centralized), + CancellationToken.None); + Assert.IsType(created); + var project = await fixture.Db.ExperimentProjects.SingleAsync(); + + var sessionResult = await controller.CreateSession( + project.Id, + fixture.SessionRequest(1, 2, 10), + CancellationToken.None); + Assert.IsType(sessionResult); + Assert.Equal( + 1, + (await fixture.Db.ExperimentSessions.SingleAsync()).Capacity); + + var published = await controller.PublishProject( + project.Id, + CancellationToken.None); + Assert.IsType(published); + Assert.Equal( + ExperimentProjectStatus.Published, + (await fixture.Db.ExperimentProjects.SingleAsync()).Status); + + var session = await fixture.Db.ExperimentSessions.SingleAsync(); + var participants = await controller.GetParticipants( + session.Id, + CancellationToken.None); + var ok = Assert.IsType(participants); + var rows = Assert.IsAssignableFrom>(ok.Value); + Assert.Single(rows); + } + + [Fact] + public async Task SelfScheduledBooking_IsSinglePerProjectAndCanBeChanged() + { + await using var fixture = await ExperimentFixture.CreateAsync(); + var manager = fixture.Controller(fixture.ManagerScope); + await manager.CreateProject( + fixture.ProjectRequest(ExperimentArrangementMode.SelfScheduled), + CancellationToken.None); + var project = await fixture.Db.ExperimentProjects.SingleAsync(); + await manager.CreateSession( + project.Id, + fixture.SessionRequest(3, 2), + CancellationToken.None); + await manager.CreateSession( + project.Id, + fixture.SessionRequest(5, 2), + CancellationToken.None); + await manager.PublishProject(project.Id, CancellationToken.None); + + var sessions = await fixture.Db.ExperimentSessions + .OrderBy(x => x.StartPeriod) + .ToListAsync(); + var student = fixture.Controller(fixture.StudentScope); + Assert.IsType( + await student.Book(sessions[0].Id, CancellationToken.None)); + Assert.IsType( + await student.Book(sessions[1].Id, CancellationToken.None)); + + var booking = await fixture.Db.ExperimentBookings.SingleAsync(); + Assert.IsType( + await student.CancelBooking(booking.Id, CancellationToken.None)); + Assert.IsType( + await student.Book(sessions[1].Id, CancellationToken.None)); + + var active = await fixture.Db.ExperimentBookings.SingleAsync(); + Assert.Equal(ExperimentBookingStatus.Booked, active.Status); + Assert.Equal(sessions[1].Id, active.ExperimentSessionId); + Assert.Equal(0, (await fixture.Db.ExperimentSessions.FindAsync( + sessions[0].Id))!.ReservedCount); + Assert.Equal(1, (await fixture.Db.ExperimentSessions.FindAsync( + sessions[1].Id))!.ReservedCount); + } + + [Fact] + public async Task StudentBooking_RejectsPublishedTimetableConflict() + { + await using var fixture = await ExperimentFixture.CreateAsync(); + var manager = fixture.Controller(fixture.ManagerScope); + await manager.CreateProject( + fixture.ProjectRequest(ExperimentArrangementMode.SelfScheduled), + CancellationToken.None); + var project = await fixture.Db.ExperimentProjects.SingleAsync(); + await manager.CreateSession( + project.Id, + fixture.SessionRequest(1, 2), + CancellationToken.None); + await manager.PublishProject(project.Id, CancellationToken.None); + + fixture.Db.SchedulePlans.Add(new SchedulePlan + { + AcademicTermId = fixture.Term.Id, + Name = "正式课表", + Version = "V1", + Status = SchedulePlanStatus.Published, + Entries = + [ + new ScheduleEntry + { + TeachingTaskId = fixture.Task.Id, + ClassroomId = fixture.SecondClassroom.Id, + DayOfWeek = 1, + StartPeriod = 1, + PeriodCount = 2, + StartWeek = 1, + EndWeek = 18, + WeekPattern = WeekPattern.All + } + ] + }); + await fixture.Db.SaveChangesAsync(); + + var session = await fixture.Db.ExperimentSessions.SingleAsync(); + var result = await fixture.Controller(fixture.StudentScope) + .Book(session.Id, CancellationToken.None); + + Assert.IsType(result); + Assert.Empty(fixture.Db.ExperimentBookings); + } + + private sealed class ExperimentFixture : IAsyncDisposable + { + private ExperimentFixture( + SqliteConnection connection, + AppDbContext db, + AcademicTerm term, + TeachingTask task, + Classroom classroom, + Classroom secondClassroom, + ICurrentUserDataScope managerScope, + ICurrentUserDataScope studentScope) + { + Connection = connection; + Db = db; + Term = term; + Task = task; + Classroom = classroom; + SecondClassroom = secondClassroom; + ManagerScope = managerScope; + StudentScope = studentScope; + } + + private SqliteConnection Connection { get; } + public AppDbContext Db { get; } + public AcademicTerm Term { get; } + public TeachingTask Task { get; } + public Classroom Classroom { get; } + public Classroom SecondClassroom { get; } + public ICurrentUserDataScope ManagerScope { get; } + public ICurrentUserDataScope StudentScope { get; } + + public static async Task CreateAsync() + { + var connection = new SqliteConnection("Data Source=:memory:"); + await connection.OpenAsync(); + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + var db = new AppDbContext(options); + await db.Database.EnsureCreatedAsync(); + + var manager = User("manager", "学院实验管理员"); + var studentUser = User("student", "实验学生"); + var campus = new Campus { Code = "MAIN", Name = "主校区" }; + var building = new Building + { + Code = "LAB", + Name = "实验中心", + CampusId = campus.Id + }; + var classroom = new Classroom + { + Code = "LAB101", + Name = "实验室 101", + BuildingId = building.Id, + Capacity = 40 + }; + var secondClassroom = new Classroom + { + Code = "LAB102", + Name = "实验室 102", + BuildingId = building.Id, + Capacity = 40 + }; + var college = new College { Code = "CS", Name = "计算机学院" }; + manager.CollegeId = college.Id; + var major = new Major + { + Code = "CS", + Name = "计算机科学与技术", + CollegeId = college.Id, + DegreeType = "工学" + }; + var administrativeClass = new AdministrativeClass + { + Code = "CS2099", + Name = "计科 2099", + MajorId = major.Id, + Grade = 2099 + }; + var student = new Student + { + StudentNumber = "20990001", + Name = "实验学生", + AdministrativeClassId = administrativeClass.Id, + EnrollmentYear = 2099, + EnrollmentDate = new DateOnly(2099, 9, 1), + UserId = studentUser.Id + }; + var teacher = new Teacher + { + TeacherNumber = "T2099", + Name = "实验教师", + CollegeId = college.Id + }; + var term = new AcademicTerm + { + Code = "2099-1", + Name = "2099—2100 学年第一学期", + AcademicYear = "2099-2100", + Season = TermSeason.Autumn, + StartDate = new DateOnly(2099, 9, 7), + EndDate = new DateOnly(2100, 1, 17), + IsCurrent = true + }; + var course = new Course + { + Code = "CSLAB", + Name = "系统实验", + CollegeId = college.Id, + Credits = 2, + TotalHours = 32, + LectureHours = 16, + PracticeHours = 16, + Nature = CourseNature.Practice, + AssessmentMethod = AssessmentMethod.Assessment + }; + var task = new TeachingTask + { + TaskNumber = "2099-1-CSLAB-01", + Name = "系统实验教学班", + AcademicTermId = term.Id, + CourseId = course.Id, + Capacity = 40, + Status = TeachingTaskStatus.Published, + Teachers = + [ + new TeachingTaskTeacher + { + TeacherId = teacher.Id, + IsPrimary = true + } + ], + Classes = + [ + new TeachingTaskClass + { + AdministrativeClassId = administrativeClass.Id + } + ] + }; + db.AddRange( + manager, + studentUser, + campus, + building, + classroom, + secondClassroom, + college, + major, + administrativeClass, + student, + teacher, + term, + course, + task); + for (var period = 1; period <= 12; period++) + { + db.ScheduleTimeSlots.Add(new ScheduleTimeSlot + { + AcademicTermId = term.Id, + PeriodNumber = period, + Name = $"第 {period} 节", + StartsAt = new TimeOnly(8, 0).AddMinutes((period - 1) * 50), + EndsAt = new TimeOnly(8, 45).AddMinutes((period - 1) * 50) + }); + } + await db.SaveChangesAsync(); + + return new ExperimentFixture( + connection, + db, + term, + task, + classroom, + secondClassroom, + Scope( + manager, + SystemRoles.CollegeAdmin, + DataScope.College), + Scope(studentUser, SystemRoles.Student, DataScope.Self)); + } + + public ExperimentsController Controller( + ICurrentUserDataScope currentScope) => + new( + Db, + currentScope, + new ClassroomReservationAvailabilityService(Db)); + + public ExperimentProjectRequest ProjectRequest( + ExperimentArrangementMode mode) => + new( + Task.Id, + mode == ExperimentArrangementMode.Centralized + ? "LAB-C" + : "LAB-S", + mode == ExperimentArrangementMode.Centralized + ? "集中上机实验" + : "自主上机实验", + mode, + "完成规定实验项目。", + "携带校园卡。", + Term.StartDate, + Term.StartDate.AddDays(14)); + + public ExperimentSessionRequest SessionRequest( + int startPeriod, + int periodCount, + int capacity = 1) => + new( + Classroom.Id, + Term.StartDate, + startPeriod, + periodCount, + capacity, + null); + + public async ValueTask DisposeAsync() + { + await Db.DisposeAsync(); + await Connection.DisposeAsync(); + } + + private static ApplicationUser User( + string userName, + string displayName) => + new() + { + Id = Guid.NewGuid(), + UserName = userName, + NormalizedUserName = userName.ToUpperInvariant(), + DisplayName = displayName, + IsEnabled = true + }; + + private static ICurrentUserDataScope Scope( + ApplicationUser user, + string role, + DataScope dataScope) => + new FixedScope(new CurrentUserScope( + user.Id, + user.DisplayName, + user.CollegeId, + dataScope, + new HashSet([role]))); + } + + private sealed class FixedScope(CurrentUserScope current) + : ICurrentUserDataScope + { + public CurrentUserScope Current { get; } = current; + } +} diff --git a/web/src/layouts/AdminLayout.vue b/web/src/layouts/AdminLayout.vue index e31e090..11b7db5 100644 --- a/web/src/layouts/AdminLayout.vue +++ b/web/src/layouts/AdminLayout.vue @@ -49,6 +49,10 @@ const isStatisticsViewer = computed(() => ), ) +const canUseExperiments = computed(() => + hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher', 'Student']), +) + function hasAnyRole(allowedRoles: string[]) { return roles.value.some((role) => allowedRoles.includes(role)) } @@ -72,6 +76,17 @@ const navigationGroups = computed(() => [ items: [{ path: '/statistics', label: '统计报表' }], } as NavigationGroup] : []), + ...(canUseExperiments.value + ? [{ + key: 'experiments', + label: isStudent.value ? '我的实验' : '实验管理', + direct: true, + items: [{ + path: '/experiments', + label: isStudent.value ? '我的实验' : '实验管理', + }], + } as NavigationGroup] + : []), { key: 'organization', label: '组织与权限', diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 29dc1af..ee43810 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -192,6 +192,14 @@ const router = createRouter({ name: 'classroom-reservations', component: () => import('../views/ClassroomReservationsView.vue'), }, + { + path: 'experiments', + name: 'experiments', + component: () => import('../views/ExperimentsView.vue'), + meta: { + roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher', 'Student'], + }, + }, { path: 'course-selections', name: 'course-selections', diff --git a/web/src/views/ExperimentsView.vue b/web/src/views/ExperimentsView.vue new file mode 100644 index 0000000..62a15e9 --- /dev/null +++ b/web/src/views/ExperimentsView.vue @@ -0,0 +1,940 @@ + + + + +