diff --git a/src/Jiaowu.Api/Controllers/ExperimentGradesController.cs b/src/Jiaowu.Api/Controllers/ExperimentGradesController.cs new file mode 100644 index 0000000..53fce0f --- /dev/null +++ b/src/Jiaowu.Api/Controllers/ExperimentGradesController.cs @@ -0,0 +1,945 @@ +using System.ComponentModel.DataAnnotations; +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Infrastructure.Auth; +using Jiaowu.Api.Infrastructure.Grades; +using Jiaowu.Api.Infrastructure.Persistence; +using Jiaowu.Api.Infrastructure.Teaching; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Jiaowu.Api.Controllers; + +[ApiController] +[Authorize] +[Route("api/experiment-grades")] +public sealed class ExperimentGradesController( + AppDbContext db, + ICurrentUserDataScope currentUserDataScope) : ControllerBase +{ + private const string Managers = + SystemRoles.SuperAdmin + "," + + SystemRoles.AcademicAdmin + "," + + SystemRoles.CollegeAdmin + "," + + SystemRoles.Teacher; + + private const string Reviewers = + SystemRoles.SuperAdmin + "," + + SystemRoles.AcademicAdmin + "," + + SystemRoles.CollegeAdmin; + + private const string Publishers = + SystemRoles.SuperAdmin + "," + + SystemRoles.AcademicAdmin; + + [HttpGet("management")] + [Authorize(Roles = Managers)] + public async Task GetManagement( + Guid? academicTermId, + ExperimentGradeSheetStatus? status, + CancellationToken cancellationToken) + { + var source = ScopedProjects().AsNoTracking() + .Where(x => + x.Status == ExperimentProjectStatus.Published || + x.Status == ExperimentProjectStatus.Closed); + if (academicTermId.HasValue) + source = source.Where(x => + x.TeachingTask!.AcademicTermId == academicTermId); + if (status.HasValue) + source = source.Where(x => + x.GradeSheet != null && + x.GradeSheet.Status == status.Value); + + return Ok(await source + .OrderByDescending(x => x.TeachingTask!.AcademicTerm!.StartDate) + .ThenBy(x => x.TeachingTask!.TaskNumber) + .ThenBy(x => x.Code) + .Select(x => new + { + x.Id, + x.Code, + x.Name, + x.ArrangementMode, + ProjectStatus = x.Status, + x.TeachingTaskId, + x.TeachingTask!.TaskNumber, + TaskName = x.TeachingTask.Name, + AcademicTermId = x.TeachingTask.AcademicTermId, + TermName = x.TeachingTask.AcademicTerm!.Name, + 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), + Sheet = x.GradeSheet == null + ? null + : new + { + x.GradeSheet.Id, + x.GradeSheet.Status, + x.GradeSheet.ContributionWeight, + x.GradeSheet.PassScore, + ItemCount = x.GradeSheet.Items.Count, + StudentCount = x.GradeSheet.Records.Count, + CompletedCount = x.GradeSheet.Records.Count(record => + record.ParticipationStatus != + ExperimentParticipationStatus.Pending), + ScoredCount = x.GradeSheet.Records.Count(record => + record.TotalScore != null), + PassedCount = x.GradeSheet.Records.Count(record => + record.IsPassed == true), + x.GradeSheet.SubmittedAt, + x.GradeSheet.ReviewedAt, + x.GradeSheet.PublishedAt + } + }) + .ToListAsync(cancellationToken)); + } + + [HttpGet("mine")] + [Authorize(Roles = SystemRoles.Student)] + public async Task GetMine(CancellationToken cancellationToken) + { + var student = await CurrentStudentAsync(cancellationToken); + if (student is null) + return ConflictProblem("当前账号未关联有效学生档案。"); + + return Ok(await db.ExperimentGradeRecords.AsNoTracking() + .Where(x => + x.StudentId == student.Id && + x.ExperimentGradeSheet!.Status == + ExperimentGradeSheetStatus.Published) + .OrderByDescending(x => + x.ExperimentGradeSheet!.PublishedAt) + .ThenBy(x => + x.ExperimentGradeSheet!.ExperimentProject!.Code) + .Select(x => new + { + x.Id, + x.ExperimentGradeSheetId, + ProjectId = x.ExperimentGradeSheet!.ExperimentProjectId, + ProjectCode = + x.ExperimentGradeSheet.ExperimentProject!.Code, + ProjectName = + x.ExperimentGradeSheet.ExperimentProject.Name, + x.ExperimentGradeSheet.ExperimentProject.ArrangementMode, + CourseCode = x.ExperimentGradeSheet.ExperimentProject + .TeachingTask!.Course!.Code, + CourseName = x.ExperimentGradeSheet.ExperimentProject + .TeachingTask.Course.Name, + TermName = x.ExperimentGradeSheet.ExperimentProject + .TeachingTask.AcademicTerm!.Name, + x.ExperimentGradeSheet.PassScore, + x.ParticipationStatus, + x.TotalScore, + x.IsPassed, + x.SafetyViolation, + x.AttemptNumber, + x.SubmissionReference, + x.SubmittedAt, + x.IsLate, + x.TeacherComment, + Items = x.ItemScores + .OrderBy(score => + score.ExperimentGradeItem!.SortOrder) + .Select(score => new + { + score.ExperimentGradeItemId, + score.ExperimentGradeItem!.Name, + score.ExperimentGradeItem.Kind, + score.ExperimentGradeItem.Weight, + score.Score, + score.Comment + }), + x.ExperimentGradeSheet.PublishedAt + }) + .ToListAsync(cancellationToken)); + } + + [HttpPost("sheets")] + [Authorize(Roles = Managers)] + public async Task CreateSheet( + ExperimentGradeSheetRequest request, + CancellationToken cancellationToken) + { + var items = BuildItems(request.Items); + if (!ExperimentGradeCalculator.AreWeightsValid( + request.ContributionWeight, + request.PassScore, + items)) + return ValidationProblem( + "实验评分项至少设置一项、权重合计 100%,项目贡献权重应大于 0。"); + + var project = await ScopedProjects() + .Include(x => x.TeachingTask) + .ThenInclude(x => x!.Teachers) + .ThenInclude(x => x.Teacher) + .FirstOrDefaultAsync(x => + x.Id == request.ExperimentProjectId, + cancellationToken); + if (project is null) return NotFound(); + if (project.Status is not ( + ExperimentProjectStatus.Published or + ExperimentProjectStatus.Closed)) + return ConflictProblem("只有已发布或已关闭实验项目可以建立成绩单。"); + if (!CanInitialize(project.TeachingTask!)) return Forbid(); + if (await db.ExperimentGradeSheets.AnyAsync(x => + x.ExperimentProjectId == project.Id, + cancellationToken)) + return ConflictProblem("该实验项目已经建立成绩单。"); + + var participants = await LoadParticipantsAsync( + project, + cancellationToken); + if (participants.Count == 0) + return ConflictProblem("实验项目当前没有可评分学生。"); + + var sheet = new ExperimentGradeSheet + { + ExperimentProjectId = project.Id, + ContributionWeight = request.ContributionWeight, + PassScore = request.PassScore, + Items = items, + Records = participants.Select(participant => + new ExperimentGradeRecord + { + StudentId = participant.StudentId, + ExperimentSessionId = participant.SessionId, + ItemScores = items.Select(item => + new ExperimentGradeItemScore + { + ExperimentGradeItemId = item.Id + }).ToList() + }).ToList() + }; + db.ExperimentGradeSheets.Add(sheet); + await db.SaveChangesAsync(cancellationToken); + return Created(string.Empty, new { sheet.Id }); + } + + [HttpGet("sheets/{id:guid}")] + [Authorize(Roles = Managers)] + public async Task GetSheet( + Guid id, + int recordPage = 1, + int recordPageSize = 50, + string? studentKeyword = null, + CancellationToken cancellationToken = default) + { + recordPage = Math.Max(1, recordPage); + recordPageSize = Math.Clamp(recordPageSize, 10, 100); + studentKeyword = string.IsNullOrWhiteSpace(studentKeyword) + ? null + : studentKeyword.Trim(); + + var sheet = await ScopedSheets().AsNoTracking() + .Where(x => x.Id == id) + .Select(x => new + { + x.Id, + x.ExperimentProjectId, + ProjectCode = x.ExperimentProject!.Code, + ProjectName = x.ExperimentProject.Name, + x.ExperimentProject.ArrangementMode, + x.ExperimentProject.TeachingTaskId, + x.ExperimentProject.TeachingTask!.TaskNumber, + TaskName = x.ExperimentProject.TeachingTask.Name, + AcademicTermId = + x.ExperimentProject.TeachingTask.AcademicTermId, + TermName = x.ExperimentProject.TeachingTask + .AcademicTerm!.Name, + CourseCollegeId = x.ExperimentProject.TeachingTask + .Course!.CollegeId, + CourseCollegeName = x.ExperimentProject.TeachingTask + .Course.College!.Name, + CourseCode = + x.ExperimentProject.TeachingTask.Course.Code, + CourseName = + x.ExperimentProject.TeachingTask.Course.Name, + TeacherNames = x.ExperimentProject.TeachingTask.Teachers + .OrderByDescending(item => item.IsPrimary) + .Select(item => item.Teacher!.Name), + x.ContributionWeight, + x.PassScore, + Items = x.Items.OrderBy(item => item.SortOrder) + .Select(item => new + { + item.Id, + item.Name, + item.Kind, + item.Weight + }), + x.Status, + x.ReviewComment, + x.SubmittedAt, + x.ReviewedAt, + x.PublishedAt, + x.CreatedAt, + x.UpdatedAt + }) + .FirstOrDefaultAsync(cancellationToken); + if (sheet is null) return NotFound(); + + var recordsSource = db.ExperimentGradeRecords.AsNoTracking() + .Where(x => x.ExperimentGradeSheetId == id); + if (studentKeyword is not null) + recordsSource = recordsSource.Where(x => + x.Student!.StudentNumber.Contains(studentKeyword) || + x.Student.Name.Contains(studentKeyword) || + x.Student.AdministrativeClass!.Name.Contains(studentKeyword)); + var recordTotal = await recordsSource.CountAsync(cancellationToken); + var records = await recordsSource + .OrderBy(x => x.Student!.StudentNumber) + .Skip((recordPage - 1) * recordPageSize) + .Take(recordPageSize) + .Select(x => new + { + x.Id, + x.StudentId, + x.Student!.StudentNumber, + x.Student.Name, + ClassName = x.Student.AdministrativeClass!.Name, + x.ExperimentSessionId, + Session = x.ExperimentSession == null + ? null + : new + { + x.ExperimentSession.SessionDate, + x.ExperimentSession.StartPeriod, + x.ExperimentSession.PeriodCount, + ClassroomName = + x.ExperimentSession.Classroom!.Name + }, + x.ParticipationStatus, + x.TotalScore, + x.IsPassed, + x.SafetyViolation, + x.AttemptNumber, + x.SubmissionReference, + x.SubmittedAt, + x.IsLate, + x.TeacherComment, + ItemScores = x.ItemScores + .OrderBy(score => + score.ExperimentGradeItem!.SortOrder) + .Select(score => new + { + score.ExperimentGradeItemId, + score.ExperimentGradeItem!.Name, + score.Score, + score.Comment + }), + x.UpdatedAt + }) + .ToListAsync(cancellationToken); + + var task = await db.TeachingTasks.AsNoTracking() + .Include(x => x.Teachers) + .ThenInclude(x => x.Teacher) + .SingleAsync( + x => x.Id == sheet.TeachingTaskId, + cancellationToken); + var canReview = CanReviewCollege(sheet.CourseCollegeId) && + sheet.Status == + ExperimentGradeSheetStatus.Submitted; + return Ok(new + { + Sheet = new + { + sheet.Id, + sheet.ExperimentProjectId, + sheet.ProjectCode, + sheet.ProjectName, + sheet.ArrangementMode, + sheet.TeachingTaskId, + sheet.TaskNumber, + sheet.TaskName, + sheet.AcademicTermId, + sheet.TermName, + sheet.CourseCollegeName, + sheet.CourseCode, + sheet.CourseName, + sheet.TeacherNames, + sheet.ContributionWeight, + sheet.PassScore, + sheet.Items, + sheet.Status, + sheet.ReviewComment, + sheet.SubmittedAt, + sheet.ReviewedAt, + sheet.PublishedAt, + RecordTotal = recordTotal, + RecordPage = recordPage, + RecordPageSize = recordPageSize, + Records = records, + sheet.CreatedAt, + sheet.UpdatedAt + }, + CanEdit = CanEdit(task) && + sheet.Status is + ExperimentGradeSheetStatus.Draft or + ExperimentGradeSheetStatus.Returned, + CanReview = canReview, + CanPublish = IsPublisher() && + sheet.Status == ExperimentGradeSheetStatus.Approved + }); + } + + [HttpPut("sheets/{id:guid}/settings")] + [Authorize(Roles = Managers)] + public async Task UpdateSettings( + Guid id, + ExperimentGradeSettingsRequest request, + CancellationToken cancellationToken) + { + var replacementItems = BuildItems(request.Items); + if (!ExperimentGradeCalculator.AreWeightsValid( + request.ContributionWeight, + request.PassScore, + replacementItems)) + return ValidationProblem( + "实验评分项至少设置一项、权重合计 100%,项目贡献权重应大于 0。"); + + var sheet = await ScopedSheets() + .Include(x => x.Items) + .Include(x => x.Records) + .ThenInclude(x => x.ItemScores) + .Include(x => x.ExperimentProject) + .ThenInclude(x => x!.TeachingTask) + .ThenInclude(x => x!.Teachers) + .ThenInclude(x => x.Teacher) + .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + if (sheet is null) return NotFound(); + if (!CanEdit(sheet.ExperimentProject!.TeachingTask!)) return Forbid(); + if (sheet.Status is not ( + ExperimentGradeSheetStatus.Draft or + ExperimentGradeSheetStatus.Returned)) + return ConflictProblem("当前状态不能修改实验评分方案。"); + + sheet.ContributionWeight = request.ContributionWeight; + sheet.PassScore = request.PassScore; + db.ExperimentGradeItems.RemoveRange(sheet.Items); + foreach (var record in sheet.Records) + { + record.ItemScores.Clear(); + record.TotalScore = null; + record.IsPassed = null; + } + sheet.Items = replacementItems; + foreach (var record in sheet.Records) + foreach (var item in replacementItems) + record.ItemScores.Add(new ExperimentGradeItemScore + { + ExperimentGradeItemId = item.Id + }); + await db.SaveChangesAsync(cancellationToken); + return NoContent(); + } + + [HttpPut("sheets/{id:guid}/records")] + [Authorize(Roles = Managers)] + public async Task UpdateRecords( + Guid id, + ExperimentGradeRecordsRequest request, + CancellationToken cancellationToken) + { + var sheet = await ScopedSheets() + .Include(x => x.Items) + .Include(x => x.Records) + .ThenInclude(x => x.ItemScores) + .Include(x => x.ExperimentProject) + .ThenInclude(x => x!.TeachingTask) + .ThenInclude(x => x!.Teachers) + .ThenInclude(x => x.Teacher) + .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + if (sheet is null) return NotFound(); + if (!CanEdit(sheet.ExperimentProject!.TeachingTask!)) return Forbid(); + if (sheet.Status is not ( + ExperimentGradeSheetStatus.Draft or + ExperimentGradeSheetStatus.Returned)) + return ConflictProblem("成绩单提交后不能继续修改。"); + + var records = sheet.Records.ToDictionary(x => x.Id); + if (request.Records.Select(x => x.Id).Distinct().Count() != + request.Records.Count || + request.Records.Any(x => !records.ContainsKey(x.Id))) + return ValidationProblem("包含无效或重复的实验成绩记录。"); + var itemIds = sheet.Items.Select(x => x.Id).ToHashSet(); + foreach (var item in request.Records) + { + if (item.AttemptNumber is < 1 or > 20) + return ValidationProblem("实验尝试次数应在 1—20 次之间。"); + if (item.ItemScores.Select(x => x.ExperimentGradeItemId) + .Distinct().Count() != item.ItemScores.Count || + item.ItemScores.Any(x => + !itemIds.Contains(x.ExperimentGradeItemId) || + !ValidScore(x.Score))) + return ValidationProblem("包含无效、重复或超出 0—100 分的评分项。"); + + var record = records[item.Id]; + record.ParticipationStatus = item.ParticipationStatus; + record.SafetyViolation = item.SafetyViolation; + record.AttemptNumber = item.AttemptNumber; + record.SubmissionReference = + Normalize(item.SubmissionReference); + record.SubmittedAt = item.SubmittedAt; + record.IsLate = item.IsLate; + record.TeacherComment = Normalize(item.TeacherComment); + var scores = record.ItemScores.ToDictionary( + x => x.ExperimentGradeItemId); + foreach (var score in item.ItemScores) + { + var target = scores[score.ExperimentGradeItemId]; + target.Score = score.Score; + target.Comment = Normalize(score.Comment); + } + Recalculate(sheet, record); + } + await db.SaveChangesAsync(cancellationToken); + return NoContent(); + } + + [HttpPost("sheets/{id:guid}/sync-participants")] + [Authorize(Roles = Managers)] + public async Task SyncParticipants( + Guid id, + CancellationToken cancellationToken) + { + var sheet = await ScopedSheets() + .Include(x => x.Items) + .Include(x => x.Records) + .ThenInclude(x => x.ItemScores) + .Include(x => x.ExperimentProject) + .ThenInclude(x => x!.TeachingTask) + .ThenInclude(x => x!.Teachers) + .ThenInclude(x => x.Teacher) + .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + if (sheet is null) return NotFound(); + if (!CanEdit(sheet.ExperimentProject!.TeachingTask!)) return Forbid(); + if (sheet.Status is not ( + ExperimentGradeSheetStatus.Draft or + ExperimentGradeSheetStatus.Returned)) + return ConflictProblem("成绩单提交后不能同步参与学生。"); + + var expected = await LoadParticipantsAsync( + sheet.ExperimentProject, + cancellationToken); + var expectedByStudent = expected.ToDictionary(x => x.StudentId); + var existingByStudent = sheet.Records.ToDictionary(x => x.StudentId); + var added = 0; + var removed = 0; + var retained = 0; + var recordsToRemove = new List(); + var recordsToAdd = new List(); + + foreach (var participant in expected) + { + if (existingByStudent.TryGetValue( + participant.StudentId, + out var record)) + { + if (record.ParticipationStatus == + ExperimentParticipationStatus.Pending && + record.ItemScores.All(x => !x.Score.HasValue)) + record.ExperimentSessionId = participant.SessionId; + retained++; + continue; + } + + var newRecord = new ExperimentGradeRecord + { + ExperimentGradeSheetId = sheet.Id, + StudentId = participant.StudentId, + ExperimentSessionId = participant.SessionId + }; + db.ExperimentGradeRecords.Add(newRecord); + recordsToAdd.Add(newRecord); + added++; + } + + foreach (var record in sheet.Records + .Where(x => !expectedByStudent.ContainsKey(x.StudentId)) + .ToList()) + { + if (record.ParticipationStatus != + ExperimentParticipationStatus.Pending || + record.ItemScores.Any(x => x.Score.HasValue)) + { + retained++; + continue; + } + + db.ExperimentGradeItemScores.RemoveRange(record.ItemScores); + recordsToRemove.Add(record); + removed++; + } + + if (recordsToRemove.Count > 0 || recordsToAdd.Count > 0) + { + await db.SaveChangesAsync(cancellationToken); + foreach (var record in recordsToRemove) + sheet.Records.Remove(record); + db.ExperimentGradeRecords.RemoveRange(recordsToRemove); + } + foreach (var record in recordsToAdd) + foreach (var item in sheet.Items) + record.ItemScores.Add(new ExperimentGradeItemScore + { + ExperimentGradeItemId = item.Id + }); + await db.SaveChangesAsync(cancellationToken); + return Ok(new + { + Added = added, + Removed = removed, + Retained = retained, + Total = await db.ExperimentGradeRecords.CountAsync( + x => x.ExperimentGradeSheetId == sheet.Id, + cancellationToken) + }); + } + + [HttpPost("sheets/{id:guid}/submit")] + [Authorize(Roles = Managers)] + public async Task Submit( + Guid id, + CancellationToken cancellationToken) + { + var sheet = await LoadForWorkflowAsync(id, cancellationToken); + if (sheet is null) return NotFound(); + if (!CanEdit(sheet.ExperimentProject!.TeachingTask!)) return Forbid(); + if (sheet.Status is not ( + ExperimentGradeSheetStatus.Draft or + ExperimentGradeSheetStatus.Returned)) + return ConflictProblem("只有草稿或已退回实验成绩单可以提交。"); + if (sheet.Records.Count == 0) + return ConflictProblem("实验成绩单没有学生记录。"); + + foreach (var record in sheet.Records) Recalculate(sheet, record); + if (sheet.Records.Any(x => + x.ParticipationStatus == + ExperimentParticipationStatus.Pending)) + return ConflictProblem("仍有学生未登记实验参与状态。"); + if (sheet.Records.Any(x => + x.ParticipationStatus is + ExperimentParticipationStatus.Completed or + ExperimentParticipationStatus.Makeup && + !x.TotalScore.HasValue)) + return ConflictProblem("仍有已完成或补做学生缺少必填评分项。"); + + sheet.Status = ExperimentGradeSheetStatus.Submitted; + sheet.SubmittedAt = DateTime.UtcNow; + sheet.ReviewComment = null; + await db.SaveChangesAsync(cancellationToken); + await NotificationService.SendToRoleAsync( + db, + SystemRoles.CollegeAdmin, + "实验成绩待审核", + $"“{sheet.ExperimentProject!.Name}”实验成绩已提交,请及时审核。", + sheet.ExperimentProject.TeachingTask!.Course!.CollegeId, + "/experiment-grades", + cancellationToken, + NotificationCategory.Grade); + return NoContent(); + } + + [HttpPost("sheets/{id:guid}/approve")] + [Authorize(Roles = Reviewers)] + public async Task Approve( + Guid id, + CancellationToken cancellationToken) + { + var sheet = await LoadForWorkflowAsync(id, cancellationToken); + if (sheet is null) return NotFound(); + var collegeId = sheet.ExperimentProject!.TeachingTask!.Course!.CollegeId; + if (!CanReviewCollege(collegeId)) return Forbid(); + if (sheet.Status != ExperimentGradeSheetStatus.Submitted) + return ConflictProblem("只有待审核实验成绩单可以通过审核。"); + + sheet.Status = ExperimentGradeSheetStatus.Approved; + sheet.ReviewedAt = DateTime.UtcNow; + sheet.ReviewComment = null; + await db.SaveChangesAsync(cancellationToken); + await NotifyTeachersAsync( + sheet.ExperimentProject.TeachingTaskId, + "实验成绩审核通过", + $"“{sheet.ExperimentProject.Name}”实验成绩已通过学院审核。", + cancellationToken); + return NoContent(); + } + + [HttpPost("sheets/{id:guid}/return")] + [Authorize(Roles = Reviewers)] + public async Task Return( + Guid id, + ExperimentGradeReviewRequest request, + CancellationToken cancellationToken) + { + var sheet = await LoadForWorkflowAsync(id, cancellationToken); + if (sheet is null) return NotFound(); + var collegeId = sheet.ExperimentProject!.TeachingTask!.Course!.CollegeId; + if (!CanReviewCollege(collegeId)) return Forbid(); + if (sheet.Status != ExperimentGradeSheetStatus.Submitted) + return ConflictProblem("只有待审核实验成绩单可以退回。"); + if (string.IsNullOrWhiteSpace(request.Comment)) + return ValidationProblem("退回时必须填写修改意见。"); + + sheet.Status = ExperimentGradeSheetStatus.Returned; + sheet.ReviewedAt = DateTime.UtcNow; + sheet.ReviewComment = request.Comment.Trim(); + await db.SaveChangesAsync(cancellationToken); + await NotifyTeachersAsync( + sheet.ExperimentProject.TeachingTaskId, + "实验成绩被退回", + $"“{sheet.ExperimentProject.Name}”实验成绩被退回:{sheet.ReviewComment}", + cancellationToken); + return NoContent(); + } + + [HttpPost("sheets/{id:guid}/publish")] + [Authorize(Roles = Publishers)] + public async Task Publish( + Guid id, + CancellationToken cancellationToken) + { + var sheet = await LoadForWorkflowAsync(id, cancellationToken); + if (sheet is null) return NotFound(); + if (!IsPublisher()) return Forbid(); + if (sheet.Status != ExperimentGradeSheetStatus.Approved) + return ConflictProblem("只有审核通过的实验成绩单可以发布。"); + + sheet.Status = ExperimentGradeSheetStatus.Published; + sheet.PublishedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + var userIds = await db.ExperimentGradeRecords + .Where(x => x.ExperimentGradeSheetId == sheet.Id) + .Select(x => x.Student!.UserId) + .Where(x => x != null) + .Select(x => x!.Value) + .Distinct() + .ToListAsync(cancellationToken); + await NotificationService.SendToUserIdsAsync( + db, + userIds, + "实验成绩已发布", + $"“{sheet.ExperimentProject!.Name}”实验成绩已发布。", + "/experiment-grades", + cancellationToken, + NotificationCategory.Grade); + return NoContent(); + } + + private async Task LoadForWorkflowAsync( + Guid id, + CancellationToken cancellationToken) + { + if (!await ScopedSheets().AnyAsync( + x => x.Id == id, + cancellationToken)) + return null; + return await db.ExperimentGradeSheets + .Include(x => x.Items) + .Include(x => x.Records) + .ThenInclude(x => x.ItemScores) + .Include(x => x.ExperimentProject) + .ThenInclude(x => x!.TeachingTask) + .ThenInclude(x => x!.Course) + .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + } + + private async Task> LoadParticipantsAsync( + ExperimentProject project, + CancellationToken cancellationToken) + { + if (project.ArrangementMode == + ExperimentArrangementMode.Centralized) + { + return await TeachingTaskRosterQuery + .ForTask(db, project.TeachingTaskId) + .AsNoTracking() + .OrderBy(x => x.StudentNumber) + .Select(x => new ExperimentParticipantSeed(x.Id, null)) + .ToListAsync(cancellationToken); + } + + return await db.ExperimentBookings.AsNoTracking() + .Where(x => + x.ExperimentProjectId == project.Id && + x.Status == ExperimentBookingStatus.Booked && + x.Student!.Status == StudentStatus.Active) + .OrderBy(x => x.Student!.StudentNumber) + .Select(x => new ExperimentParticipantSeed( + x.StudentId, + x.ExperimentSessionId)) + .ToListAsync(cancellationToken); + } + + private static List BuildItems( + IReadOnlyCollection? requests) => + requests?.Select((item, index) => new ExperimentGradeItem + { + Name = item.Name.Trim(), + Kind = item.Kind, + Weight = item.Weight, + SortOrder = index + }).ToList() ?? []; + + private static void Recalculate( + ExperimentGradeSheet sheet, + ExperimentGradeRecord record) + { + record.TotalScore = ExperimentGradeCalculator.CalculateTotal( + record, + sheet.Items.ToList()); + record.IsPassed = record.TotalScore.HasValue + ? record.TotalScore.Value >= sheet.PassScore && + !record.SafetyViolation + : 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 IQueryable ScopedSheets() + { + var projectIds = ScopedProjects().Select(x => x.Id); + return db.ExperimentGradeSheets.Where(x => + projectIds.Contains(x.ExperimentProjectId)); + } + + private bool CanInitialize(TeachingTask task) => + currentUserDataScope.Current.IsInRole(SystemRoles.SuperAdmin) || + currentUserDataScope.Current.IsInRole(SystemRoles.AcademicAdmin) || + currentUserDataScope.Current.IsInRole(SystemRoles.CollegeAdmin) || + IsAssignedTeacher(task); + + private bool CanEdit(TeachingTask task) => + currentUserDataScope.Current.IsInRole(SystemRoles.SuperAdmin) || + IsAssignedTeacher(task); + + private bool IsAssignedTeacher(TeachingTask task) => + task.Teachers.Any(x => + x.Teacher?.UserId == currentUserDataScope.Current.UserId); + + private bool CanReviewCollege(Guid collegeId) + { + var scope = currentUserDataScope.Current; + if (!scope.IsInRole(SystemRoles.SuperAdmin) && + !scope.IsInRole(SystemRoles.AcademicAdmin) && + !scope.IsInRole(SystemRoles.CollegeAdmin)) + return false; + return scope.Scope == DataScope.All || + scope.RestrictedCollegeId == collegeId; + } + + private bool IsPublisher() => + currentUserDataScope.Current.IsInRole(SystemRoles.SuperAdmin) || + currentUserDataScope.Current.IsInRole(SystemRoles.AcademicAdmin); + + private Task CurrentStudentAsync( + CancellationToken cancellationToken) => + db.Students.FirstOrDefaultAsync(x => + x.UserId == currentUserDataScope.Current.UserId && + x.Status == StudentStatus.Active, + cancellationToken); + + private async Task NotifyTeachersAsync( + Guid teachingTaskId, + string title, + string content, + CancellationToken cancellationToken) + { + var userIds = await db.TeachingTaskTeachers + .Where(x => x.TeachingTaskId == teachingTaskId) + .Select(x => x.Teacher!.UserId) + .Where(x => x != null) + .Select(x => x!.Value) + .Distinct() + .ToListAsync(cancellationToken); + await NotificationService.SendToUserIdsAsync( + db, + userIds, + title, + content, + "/experiment-grades", + cancellationToken, + NotificationCategory.Grade); + } + + private static bool ValidScore(decimal? score) => + !score.HasValue || score.Value is >= 0 and <= 100; + + 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 + }); + + private sealed record ExperimentParticipantSeed( + Guid StudentId, + Guid? SessionId); +} + +public sealed record ExperimentGradeSheetRequest( + Guid ExperimentProjectId, + [Range(typeof(decimal), "0.1", "100")] decimal ContributionWeight, + [Range(typeof(decimal), "0", "100")] decimal PassScore, + IReadOnlyCollection Items); + +public sealed record ExperimentGradeSettingsRequest( + [Range(typeof(decimal), "0.1", "100")] decimal ContributionWeight, + [Range(typeof(decimal), "0", "100")] decimal PassScore, + IReadOnlyCollection Items); + +public sealed record ExperimentGradeItemRequest( + [Required, MaxLength(60)] string Name, + ExperimentGradeItemKind Kind, + [Range(typeof(decimal), "0.1", "100")] decimal Weight); + +public sealed record ExperimentGradeRecordsRequest( + IReadOnlyCollection Records); + +public sealed record ExperimentGradeRecordRequest( + Guid Id, + ExperimentParticipationStatus ParticipationStatus, + bool SafetyViolation, + [Range(1, 20)] int AttemptNumber, + [MaxLength(500)] string? SubmissionReference, + DateTime? SubmittedAt, + bool IsLate, + [MaxLength(500)] string? TeacherComment, + IReadOnlyCollection ItemScores); + +public sealed record ExperimentGradeItemScoreRequest( + Guid ExperimentGradeItemId, + decimal? Score, + [MaxLength(300)] string? Comment); + +public sealed record ExperimentGradeReviewRequest( + [MaxLength(500)] string? Comment); diff --git a/src/Jiaowu.Api/Controllers/GradesController.cs b/src/Jiaowu.Api/Controllers/GradesController.cs index 972bdd0..2308038 100644 --- a/src/Jiaowu.Api/Controllers/GradesController.cs +++ b/src/Jiaowu.Api/Controllers/GradesController.cs @@ -101,7 +101,9 @@ public sealed class GradesController( { item.Id, item.Name, - item.Weight + item.Weight, + item.SourceType, + item.SourceSnapshotAt }), StudentCount = sheet.Records.Count, CompletedCount = sheet.Records.Count(record => @@ -220,7 +222,9 @@ public sealed class GradesController( { item.Id, item.Name, - item.Weight + item.Weight, + item.SourceType, + item.SourceSnapshotAt }), x.Status, x.ReviewComment, @@ -408,7 +412,7 @@ public sealed class GradesController( request.Records.Any(x => !records.ContainsKey(x.Id))) return ValidationProblem("包含无效或重复的成绩记录。"); - var itemIds = sheet.Items.Select(item => item.Id).ToHashSet(); + var itemsById = sheet.Items.ToDictionary(item => item.Id); foreach (var item in request.Records) { if (!ValidScore(item.RegularScore) || @@ -428,8 +432,19 @@ public sealed class GradesController( { if (!ValidScore(scoreEntry.Score)) return ValidationProblem("分项成绩必须在 0—100 分之间。"); - if (scoreMap.TryGetValue(scoreEntry.GradeItemId, out var existing)) - existing.Score = scoreEntry.Score; + if (!itemsById.TryGetValue( + scoreEntry.GradeItemId, + out var gradeItem) || + !scoreMap.TryGetValue( + scoreEntry.GradeItemId, + out var existing)) + continue; + if (gradeItem.SourceType == + GradeItemSourceType.ExperimentSummary && + existing.Score != scoreEntry.Score) + return ConflictProblem( + $"“{gradeItem.Name}”来自实验成绩快照,不能手工修改;请重新导入实验成绩。"); + existing.Score = scoreEntry.Score; } } @@ -438,6 +453,66 @@ public sealed class GradesController( return await SaveAsync(id, false, cancellationToken); } + [HttpPost("sheets/{id:guid}/import-experiment-scores")] + [Authorize(Roles = SheetUsers)] + public async Task ImportExperimentScores( + Guid id, + ImportExperimentScoresRequest request, + CancellationToken cancellationToken) + { + var sheet = await AccessibleSheets() + .Include(x => x.Items) + .Include(x => x.Records) + .ThenInclude(x => x.ItemScores) + .Include(x => x.TeachingTask) + .ThenInclude(x => x!.Teachers) + .ThenInclude(x => x.Teacher) + .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + if (sheet is null) return NotFound(); + if (!CanEditScores(sheet.TeachingTask!)) return Forbid(); + if (sheet.Status is not ( + GradeSheetStatus.Draft or + GradeSheetStatus.Returned)) + return ConflictProblem("成绩单提交后不能导入实验成绩。"); + + var targetItem = sheet.Items.FirstOrDefault(x => + x.Id == request.GradeItemId); + if (targetItem is null) + return ValidationProblem("请选择当前成绩单中的有效分项。"); + + var aggregate = await ExperimentGradeAggregationService.CalculateAsync( + db, + sheet.TeachingTaskId, + sheet.Records.Select(x => x.StudentId).ToArray(), + cancellationToken); + if (aggregate.PublishedProjectCount == 0) + return ConflictProblem( + "该教学任务尚无已发布的实验项目成绩,无法导入。"); + + var imported = 0; + var missing = 0; + foreach (var record in sheet.Records) + { + var targetScore = record.ItemScores.First(x => + x.GradeItemId == targetItem.Id); + targetScore.Score = aggregate.Scores[record.StudentId]; + if (targetScore.Score.HasValue) imported++; + else missing++; + Recalculate(sheet, record); + } + targetItem.SourceType = GradeItemSourceType.ExperimentSummary; + targetItem.SourceSnapshotAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + return Ok(new + { + aggregate.PublishedProjectCount, + aggregate.Projects, + ImportedCount = imported, + MissingCount = missing, + targetItem.SourceSnapshotAt + }); + } + [HttpPost("sheets/{id:guid}/submit")] [Authorize(Roles = SheetUsers)] public async Task Submit(Guid id, CancellationToken cancellationToken) @@ -998,5 +1073,7 @@ public sealed record GradeItemScoreRequest( Guid GradeItemId, decimal? Score); +public sealed record ImportExperimentScoresRequest(Guid GradeItemId); + public sealed record GradeReviewRequest( [MaxLength(500)] string? Comment); diff --git a/src/Jiaowu.Api/Domain/Academic/ExperimentEntities.cs b/src/Jiaowu.Api/Domain/Academic/ExperimentEntities.cs index 728fc54..3079b97 100644 --- a/src/Jiaowu.Api/Domain/Academic/ExperimentEntities.cs +++ b/src/Jiaowu.Api/Domain/Academic/ExperimentEntities.cs @@ -19,6 +19,7 @@ public sealed class ExperimentProject : EntityBase public DateTime? ClosedAt { get; set; } public ICollection Sessions { get; set; } = []; public ICollection Bookings { get; set; } = []; + public ExperimentGradeSheet? GradeSheet { get; set; } } public sealed class ExperimentSession : EntityBase diff --git a/src/Jiaowu.Api/Domain/Academic/ExperimentGradeEntities.cs b/src/Jiaowu.Api/Domain/Academic/ExperimentGradeEntities.cs new file mode 100644 index 0000000..0b3778f --- /dev/null +++ b/src/Jiaowu.Api/Domain/Academic/ExperimentGradeEntities.cs @@ -0,0 +1,92 @@ +using Jiaowu.Api.Domain.Common; + +namespace Jiaowu.Api.Domain.Academic; + +public sealed class ExperimentGradeSheet : EntityBase +{ + public Guid ExperimentProjectId { get; set; } + public ExperimentProject? ExperimentProject { get; set; } + public decimal ContributionWeight { get; set; } = 1; + public decimal PassScore { get; set; } = 60; + public ExperimentGradeSheetStatus Status { get; set; } = + ExperimentGradeSheetStatus.Draft; + public string? ReviewComment { get; set; } + public DateTime? SubmittedAt { get; set; } + public DateTime? ReviewedAt { get; set; } + public DateTime? PublishedAt { get; set; } + public ICollection Items { get; set; } = []; + public ICollection Records { get; set; } = []; +} + +public sealed class ExperimentGradeItem : EntityBase +{ + public Guid ExperimentGradeSheetId { get; set; } + public ExperimentGradeSheet? ExperimentGradeSheet { get; set; } + public required string Name { get; set; } + public ExperimentGradeItemKind Kind { get; set; } = + ExperimentGradeItemKind.Other; + public decimal Weight { get; set; } + public int SortOrder { get; set; } + public ICollection Scores { get; set; } = []; +} + +public sealed class ExperimentGradeRecord : EntityBase +{ + public Guid ExperimentGradeSheetId { get; set; } + public ExperimentGradeSheet? ExperimentGradeSheet { get; set; } + public Guid StudentId { get; set; } + public Student? Student { get; set; } + public Guid? ExperimentSessionId { get; set; } + public ExperimentSession? ExperimentSession { get; set; } + public ExperimentParticipationStatus ParticipationStatus { get; set; } = + ExperimentParticipationStatus.Pending; + public decimal? TotalScore { get; set; } + public bool? IsPassed { get; set; } + public bool SafetyViolation { get; set; } + public int AttemptNumber { get; set; } = 1; + public string? SubmissionReference { get; set; } + public DateTime? SubmittedAt { get; set; } + public bool IsLate { get; set; } + public string? TeacherComment { get; set; } + public ICollection ItemScores { get; set; } = []; +} + +public sealed class ExperimentGradeItemScore +{ + public Guid ExperimentGradeRecordId { get; set; } + public ExperimentGradeRecord? ExperimentGradeRecord { get; set; } + public Guid ExperimentGradeItemId { get; set; } + public ExperimentGradeItem? ExperimentGradeItem { get; set; } + public decimal? Score { get; set; } + public string? Comment { get; set; } +} + +public enum ExperimentGradeSheetStatus +{ + Draft = 1, + Submitted = 2, + Approved = 3, + Published = 4, + Returned = 5 +} + +public enum ExperimentGradeItemKind +{ + Operation = 1, + Report = 2, + Result = 3, + Defense = 4, + Attendance = 5, + Safety = 6, + Other = 7 +} + +public enum ExperimentParticipationStatus +{ + Pending = 1, + Completed = 2, + Absent = 3, + Excused = 4, + Makeup = 5, + Exempt = 6 +} diff --git a/src/Jiaowu.Api/Domain/Academic/GradeEntities.cs b/src/Jiaowu.Api/Domain/Academic/GradeEntities.cs index c2ab028..1429d2a 100644 --- a/src/Jiaowu.Api/Domain/Academic/GradeEntities.cs +++ b/src/Jiaowu.Api/Domain/Academic/GradeEntities.cs @@ -24,6 +24,9 @@ public sealed class GradeItem : EntityBase public required string Name { get; set; } public decimal Weight { get; set; } public int SortOrder { get; set; } + public GradeItemSourceType SourceType { get; set; } = + GradeItemSourceType.Manual; + public DateTime? SourceSnapshotAt { get; set; } public ICollection Scores { get; set; } = []; } @@ -68,3 +71,9 @@ public enum GradeExamStatus Exempt = 4, Makeup = 5 } + +public enum GradeItemSourceType +{ + Manual = 1, + ExperimentSummary = 2 +} diff --git a/src/Jiaowu.Api/Infrastructure/Grades/ExperimentGradeAggregationService.cs b/src/Jiaowu.Api/Infrastructure/Grades/ExperimentGradeAggregationService.cs new file mode 100644 index 0000000..ec3f177 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Grades/ExperimentGradeAggregationService.cs @@ -0,0 +1,88 @@ +using Jiaowu.Api.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace Jiaowu.Api.Infrastructure.Grades; + +public static class ExperimentGradeAggregationService +{ + public static async Task CalculateAsync( + AppDbContext db, + Guid teachingTaskId, + IReadOnlyCollection studentIds, + CancellationToken cancellationToken) + { + var sheets = await db.ExperimentGradeSheets.AsNoTracking() + .Where(x => + x.Status == + Domain.Academic.ExperimentGradeSheetStatus.Published && + x.ExperimentProject!.TeachingTaskId == teachingTaskId) + .OrderBy(x => x.ExperimentProject!.Code) + .Select(x => new PublishedExperimentSheet( + x.Id, + x.ExperimentProject!.Code, + x.ExperimentProject.Name, + x.ContributionWeight, + x.Records.Select(record => new PublishedExperimentScore( + record.StudentId, + record.TotalScore)).ToList())) + .AsSplitQuery() + .ToListAsync(cancellationToken); + + var scores = new Dictionary(); + foreach (var studentId in studentIds.Distinct()) + { + decimal weightedTotal = 0; + decimal totalWeight = 0; + var complete = sheets.Count > 0; + foreach (var sheet in sheets) + { + var score = sheet.Scores.FirstOrDefault(x => + x.StudentId == studentId); + if (score?.TotalScore is not decimal totalScore) + { + complete = false; + break; + } + weightedTotal += totalScore * sheet.ContributionWeight; + totalWeight += sheet.ContributionWeight; + } + scores[studentId] = complete && totalWeight > 0 + ? Math.Round( + weightedTotal / totalWeight, + 1, + MidpointRounding.AwayFromZero) + : null; + } + + return new ExperimentGradeAggregateResult( + sheets.Count, + sheets.Select(x => new ExperimentGradeAggregateProject( + x.Id, + x.Code, + x.Name, + x.ContributionWeight)).ToList(), + scores); + } + + private sealed record PublishedExperimentSheet( + Guid Id, + string Code, + string Name, + decimal ContributionWeight, + List Scores); + + private sealed record PublishedExperimentScore( + Guid StudentId, + decimal? TotalScore); +} + +public sealed record ExperimentGradeAggregateResult( + int PublishedProjectCount, + IReadOnlyList Projects, + IReadOnlyDictionary Scores); + +public sealed record ExperimentGradeAggregateProject( + Guid Id, + string Code, + string Name, + decimal ContributionWeight); diff --git a/src/Jiaowu.Api/Infrastructure/Grades/ExperimentGradeCalculator.cs b/src/Jiaowu.Api/Infrastructure/Grades/ExperimentGradeCalculator.cs new file mode 100644 index 0000000..b6616bb --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Grades/ExperimentGradeCalculator.cs @@ -0,0 +1,45 @@ +using Jiaowu.Api.Domain.Academic; + +namespace Jiaowu.Api.Infrastructure.Grades; + +public static class ExperimentGradeCalculator +{ + public static bool AreWeightsValid( + decimal contributionWeight, + decimal passScore, + IEnumerable items) + { + var itemList = items.ToList(); + return contributionWeight is > 0 and <= 100 && + passScore is >= 0 and <= 100 && + itemList.Count > 0 && + itemList.All(item => item.Weight is > 0 and <= 100) && + itemList.Sum(item => item.Weight) == 100; + } + + public static decimal? CalculateTotal( + ExperimentGradeRecord record, + IReadOnlyCollection items) + { + if (record.SafetyViolation || + record.ParticipationStatus == ExperimentParticipationStatus.Absent) + return 0; + if (record.ParticipationStatus is + ExperimentParticipationStatus.Pending or + ExperimentParticipationStatus.Excused or + ExperimentParticipationStatus.Exempt) + return null; + + var scores = record.ItemScores.ToDictionary( + x => x.ExperimentGradeItemId); + decimal total = 0; + foreach (var item in items) + { + if (!scores.TryGetValue(item.Id, out var score) || + !score.Score.HasValue) + return null; + total += score.Score.Value * item.Weight / 100; + } + return Math.Round(total, 1, MidpointRounding.AwayFromZero); + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs index ce4a3d7..e490967 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs @@ -47,6 +47,14 @@ public sealed class AppDbContext(DbContextOptions options) public DbSet ExperimentProjects => Set(); public DbSet ExperimentSessions => Set(); public DbSet ExperimentBookings => Set(); + public DbSet ExperimentGradeSheets => + Set(); + public DbSet ExperimentGradeItems => + Set(); + public DbSet ExperimentGradeRecords => + Set(); + public DbSet ExperimentGradeItemScores => + Set(); public DbSet CourseSelectionRounds => Set(); public DbSet CourseSelectionRoundGrades => @@ -608,6 +616,75 @@ public sealed class AppDbContext(DbContextOptions options) .OnDelete(DeleteBehavior.Restrict); }); + builder.Entity(entity => + { + entity.Property(x => x.ContributionWeight).HasPrecision(5, 1); + entity.Property(x => x.PassScore).HasPrecision(5, 1); + entity.Property(x => x.ReviewComment).HasMaxLength(500); + entity.HasIndex(x => x.ExperimentProjectId).IsUnique(); + entity.HasOne(x => x.ExperimentProject) + .WithOne(x => x.GradeSheet) + .HasForeignKey( + x => x.ExperimentProjectId) + .OnDelete(DeleteBehavior.Cascade); + }); + + builder.Entity(entity => + { + entity.Property(x => x.Name).HasMaxLength(60); + entity.Property(x => x.Weight).HasPrecision(5, 1); + entity.HasIndex(x => new + { + x.ExperimentGradeSheetId, + x.SortOrder + }); + entity.HasOne(x => x.ExperimentGradeSheet) + .WithMany(x => x.Items) + .HasForeignKey(x => x.ExperimentGradeSheetId) + .OnDelete(DeleteBehavior.Cascade); + }); + + builder.Entity(entity => + { + entity.Property(x => x.TotalScore).HasPrecision(5, 1); + entity.Property(x => x.SubmissionReference).HasMaxLength(500); + entity.Property(x => x.TeacherComment).HasMaxLength(500); + entity.HasIndex(x => new + { + x.ExperimentGradeSheetId, + x.StudentId + }).IsUnique(); + entity.HasOne(x => x.ExperimentGradeSheet) + .WithMany(x => x.Records) + .HasForeignKey(x => x.ExperimentGradeSheetId) + .OnDelete(DeleteBehavior.Cascade); + entity.HasOne(x => x.Student).WithMany() + .HasForeignKey(x => x.StudentId) + .OnDelete(DeleteBehavior.Restrict); + entity.HasOne(x => x.ExperimentSession).WithMany() + .HasForeignKey(x => x.ExperimentSessionId) + .OnDelete(DeleteBehavior.Restrict); + }); + + builder.Entity(entity => + { + entity.HasKey(x => new + { + x.ExperimentGradeRecordId, + x.ExperimentGradeItemId + }); + entity.Property(x => x.Score).HasPrecision(5, 1); + entity.Property(x => x.Comment).HasMaxLength(300); + entity.HasOne(x => x.ExperimentGradeRecord) + .WithMany(x => x.ItemScores) + .HasForeignKey(x => x.ExperimentGradeRecordId) + .OnDelete(DeleteBehavior.Cascade); + entity.HasOne(x => x.ExperimentGradeItem) + .WithMany(x => x.Scores) + .HasForeignKey(x => x.ExperimentGradeItemId) + .OnDelete(DeleteBehavior.Cascade); + }); + builder.Entity(entity => { entity.Property(x => x.Name).HasMaxLength(120); @@ -687,6 +764,8 @@ public sealed class AppDbContext(DbContextOptions options) { entity.Property(x => x.Name).HasMaxLength(60); entity.Property(x => x.Weight).HasPrecision(5, 1); + entity.Property(x => x.SourceType) + .HasDefaultValue(GradeItemSourceType.Manual); entity.HasIndex(x => new { x.GradeSheetId, x.SortOrder }); entity.HasOne(x => x.GradeSheet) .WithMany(x => x.Items) diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs index 373058f..afed11c 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs @@ -72,6 +72,8 @@ public sealed class DevelopmentSqliteMigrator( "20260728_38_exam_publish_jobs"; private const string ExperimentManagementMigration = "20260728_39_experiment_management"; + private const string ExperimentGradeManagementMigration = + "20260728_40_experiment_grade_management"; public async Task MigrateAsync(CancellationToken cancellationToken = default) { @@ -543,6 +545,32 @@ public sealed class DevelopmentSqliteMigrator( ExperimentManagementMigration, experimentProjectsExist ? [] : ExperimentManagementStatements, cancellationToken); + + var gradeItemSourceExists = await db.Database + .SqlQueryRaw( + """ + SELECT COUNT(*) AS "Value" + FROM pragma_table_info('GradeItems') + WHERE name = 'SourceType' + """) + .AnyAsync(value => value > 0, cancellationToken); + var experimentGradeSheetsExist = await db.Database + .SqlQueryRaw( + """ + SELECT COUNT(*) AS "Value" + FROM sqlite_master + WHERE type = 'table' AND name = 'ExperimentGradeSheets' + """) + .AnyAsync(value => value > 0, cancellationToken); + await ApplyMigrationAsync( + ExperimentGradeManagementMigration, + (gradeItemSourceExists + ? [] + : ExperimentGradeItemSourceStatements) + .Concat(experimentGradeSheetsExist + ? [] + : ExperimentGradeManagementStatements), + cancellationToken); } private async Task ApplyMigrationAsync( @@ -2511,4 +2539,133 @@ public sealed class DevelopmentSqliteMigrator( ); """ ]; + + private static readonly string[] ExperimentGradeItemSourceStatements = + [ + """ + ALTER TABLE "GradeItems" + ADD "SourceType" INTEGER NOT NULL DEFAULT 1; + """, + """ + ALTER TABLE "GradeItems" + ADD "SourceSnapshotAt" TEXT NULL; + """ + ]; + + private static readonly string[] ExperimentGradeManagementStatements = + [ + """ + CREATE TABLE "ExperimentGradeSheets" ( + "Id" TEXT NOT NULL + CONSTRAINT "PK_ExperimentGradeSheets" PRIMARY KEY, + "ExperimentProjectId" TEXT NOT NULL, + "ContributionWeight" TEXT NOT NULL, + "PassScore" TEXT NOT NULL, + "Status" INTEGER NOT NULL, + "ReviewComment" TEXT NULL, + "SubmittedAt" TEXT NULL, + "ReviewedAt" TEXT NULL, + "PublishedAt" TEXT NULL, + "CreatedAt" TEXT NOT NULL, + "UpdatedAt" TEXT NOT NULL, + CONSTRAINT "FK_ExperimentGradeSheets_ExperimentProjects_ExperimentProjectId" + FOREIGN KEY ("ExperimentProjectId") + REFERENCES "ExperimentProjects" ("Id") + ON DELETE CASCADE + ); + """, + """ + CREATE UNIQUE INDEX "IX_ExperimentGradeSheets_ExperimentProjectId" + ON "ExperimentGradeSheets" ("ExperimentProjectId"); + """, + """ + CREATE TABLE "ExperimentGradeItems" ( + "Id" TEXT NOT NULL + CONSTRAINT "PK_ExperimentGradeItems" PRIMARY KEY, + "ExperimentGradeSheetId" TEXT NOT NULL, + "Name" TEXT NOT NULL, + "Kind" INTEGER NOT NULL, + "Weight" TEXT NOT NULL, + "SortOrder" INTEGER NOT NULL, + "CreatedAt" TEXT NOT NULL, + "UpdatedAt" TEXT NOT NULL, + CONSTRAINT "FK_ExperimentGradeItems_ExperimentGradeSheets_ExperimentGradeSheetId" + FOREIGN KEY ("ExperimentGradeSheetId") + REFERENCES "ExperimentGradeSheets" ("Id") + ON DELETE CASCADE + ); + """, + """ + CREATE INDEX "IX_ExperimentGradeItems_ExperimentGradeSheetId_SortOrder" + ON "ExperimentGradeItems" ("ExperimentGradeSheetId", "SortOrder"); + """, + """ + CREATE TABLE "ExperimentGradeRecords" ( + "Id" TEXT NOT NULL + CONSTRAINT "PK_ExperimentGradeRecords" PRIMARY KEY, + "ExperimentGradeSheetId" TEXT NOT NULL, + "StudentId" TEXT NOT NULL, + "ExperimentSessionId" TEXT NULL, + "ParticipationStatus" INTEGER NOT NULL, + "TotalScore" TEXT NULL, + "IsPassed" INTEGER NULL, + "SafetyViolation" INTEGER NOT NULL, + "AttemptNumber" INTEGER NOT NULL, + "SubmissionReference" TEXT NULL, + "SubmittedAt" TEXT NULL, + "IsLate" INTEGER NOT NULL, + "TeacherComment" TEXT NULL, + "CreatedAt" TEXT NOT NULL, + "UpdatedAt" TEXT NOT NULL, + CONSTRAINT "FK_ExperimentGradeRecords_ExperimentGradeSheets_ExperimentGradeSheetId" + FOREIGN KEY ("ExperimentGradeSheetId") + REFERENCES "ExperimentGradeSheets" ("Id") + ON DELETE CASCADE, + CONSTRAINT "FK_ExperimentGradeRecords_Students_StudentId" + FOREIGN KEY ("StudentId") REFERENCES "Students" ("Id") + ON DELETE RESTRICT, + CONSTRAINT "FK_ExperimentGradeRecords_ExperimentSessions_ExperimentSessionId" + FOREIGN KEY ("ExperimentSessionId") + REFERENCES "ExperimentSessions" ("Id") + ON DELETE RESTRICT + ); + """, + """ + CREATE UNIQUE INDEX "IX_ExperimentGradeRecords_ExperimentGradeSheetId_StudentId" + ON "ExperimentGradeRecords" ("ExperimentGradeSheetId", "StudentId"); + """, + """ + CREATE INDEX "IX_ExperimentGradeRecords_StudentId" + ON "ExperimentGradeRecords" ("StudentId"); + """, + """ + CREATE INDEX "IX_ExperimentGradeRecords_ExperimentSessionId" + ON "ExperimentGradeRecords" ("ExperimentSessionId"); + """, + """ + CREATE TABLE "ExperimentGradeItemScores" ( + "ExperimentGradeRecordId" TEXT NOT NULL, + "ExperimentGradeItemId" TEXT NOT NULL, + "Score" TEXT NULL, + "Comment" TEXT NULL, + CONSTRAINT "PK_ExperimentGradeItemScores" + PRIMARY KEY ( + "ExperimentGradeRecordId", + "ExperimentGradeItemId" + ), + CONSTRAINT "FK_ExperimentGradeItemScores_ExperimentGradeRecords_ExperimentGradeRecordId" + FOREIGN KEY ("ExperimentGradeRecordId") + REFERENCES "ExperimentGradeRecords" ("Id") + ON DELETE CASCADE, + CONSTRAINT "FK_ExperimentGradeItemScores_ExperimentGradeItems_ExperimentGradeItemId" + FOREIGN KEY ("ExperimentGradeItemId") + REFERENCES "ExperimentGradeItems" ("Id") + ON DELETE CASCADE + ); + """, + """ + CREATE INDEX "IX_ExperimentGradeItemScores_ExperimentGradeItemId" + ON "ExperimentGradeItemScores" ("ExperimentGradeItemId"); + """ + ]; } diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260728134931_ExperimentGradeManagement.Designer.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260728134931_ExperimentGradeManagement.Designer.cs new file mode 100644 index 0000000..34794a4 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260728134931_ExperimentGradeManagement.Designer.cs @@ -0,0 +1,5905 @@ +// +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("20260728134931_ExperimentGradeManagement")] + partial class ExperimentGradeManagement + { + /// + 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.ExperimentGradeItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentGradeSheetId") + .HasColumnType("char(36)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("varchar(60)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Weight") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.HasKey("Id"); + + b.HasIndex("ExperimentGradeSheetId", "SortOrder"); + + b.ToTable("ExperimentGradeItems"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItemScore", b => + { + b.Property("ExperimentGradeRecordId") + .HasColumnType("char(36)"); + + b.Property("ExperimentGradeItemId") + .HasColumnType("char(36)"); + + b.Property("Comment") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Score") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.HasKey("ExperimentGradeRecordId", "ExperimentGradeItemId"); + + b.HasIndex("ExperimentGradeItemId"); + + b.ToTable("ExperimentGradeItemScores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentGradeSheetId") + .HasColumnType("char(36)"); + + b.Property("ExperimentSessionId") + .HasColumnType("char(36)"); + + b.Property("IsLate") + .HasColumnType("tinyint(1)"); + + b.Property("IsPassed") + .HasColumnType("tinyint(1)"); + + b.Property("ParticipationStatus") + .HasColumnType("int"); + + b.Property("SafetyViolation") + .HasColumnType("tinyint(1)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("SubmissionReference") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TeacherComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("TotalScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ExperimentSessionId"); + + b.HasIndex("StudentId"); + + b.HasIndex("ExperimentGradeSheetId", "StudentId") + .IsUnique(); + + b.ToTable("ExperimentGradeRecords"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContributionWeight") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentProjectId") + .HasColumnType("char(36)"); + + b.Property("PassScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ExperimentProjectId") + .IsUnique(); + + b.ToTable("ExperimentGradeSheets"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ArrangementMode") + .HasColumnType("int"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Requirements") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("TeachingTaskId", "Code") + .IsUnique(); + + b.HasIndex("Status", "StartDate", "EndDate"); + + b.ToTable("ExperimentProjects"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CancelledAt") + .HasColumnType("datetime(6)"); + + b.Property("Capacity") + .HasColumnType("int"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentProjectId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("ReservedCount") + .HasColumnType("int"); + + b.Property("SessionDate") + .HasColumnType("date"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ExperimentProjectId", "SessionDate", "StartPeriod"); + + b.HasIndex("ClassroomId", "SessionDate", "Status", "StartPeriod"); + + b.ToTable("ExperimentSessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("GradeSheetId") + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("varchar(60)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("SourceSnapshotAt") + .HasColumnType("datetime(6)"); + + b.Property("SourceType") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(1); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Weight") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.HasKey("Id"); + + b.HasIndex("GradeSheetId", "SortOrder"); + + b.ToTable("GradeItems"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItemScore", b => + { + b.Property("GradeRecordId") + .HasColumnType("char(36)"); + + b.Property("GradeItemId") + .HasColumnType("char(36)"); + + b.Property("Score") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.HasKey("GradeRecordId", "GradeItemId"); + + b.HasIndex("GradeItemId"); + + b.ToTable("GradeItemScores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeModification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApplicantUserId") + .HasColumnType("char(36)"); + + b.Property("CollegeReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("CollegeReviewedByUserId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("FinalReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("FinalReviewedByUserId") + .HasColumnType("char(36)"); + + b.Property("GradeRecordId") + .HasColumnType("char(36)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RequestedScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GradeRecordId"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("GradeModifications"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExamStatus") + .HasColumnType("int"); + + b.Property("FinalScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("GradePoint") + .HasPrecision(3, 1) + .HasColumnType("decimal(3,1)"); + + b.Property("GradeSheetId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RegularScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("TotalScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GradeSheetId", "StudentId") + .IsUnique(); + + b.HasIndex("StudentId", "TotalScore"); + + b.ToTable("GradeRecords"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeSheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FinalWeight") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("RegularWeight") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("TeachingTaskId") + .IsUnique(); + + b.ToTable("GradeSheets"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationAuditBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CalculatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EnrollmentYear") + .HasColumnType("int"); + + b.Property("GraduationYear") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("GraduationYear", "EnrollmentYear"); + + b.ToTable("GraduationAuditBatches"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationAuditResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CalculatedConclusion") + .HasColumnType("int"); + + b.Property("Conclusion") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurriculumPlanId") + .HasColumnType("char(36)"); + + b.Property("EarnedCredits") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("FailedCourseCount") + .HasColumnType("int"); + + b.Property("GraduationAuditBatchId") + .HasColumnType("char(36)"); + + b.Property("IsOverridden") + .HasColumnType("tinyint(1)"); + + b.Property("MissingCourseNames") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("PassedRequiredCourseCount") + .HasColumnType("int"); + + b.Property("RequiredCourseCount") + .HasColumnType("int"); + + b.Property("RequiredCredits") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("StudentStatusSnapshot") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CurriculumPlanId"); + + b.HasIndex("StudentId"); + + b.HasIndex("Conclusion", "IsOverridden"); + + b.HasIndex("GraduationAuditBatchId", "StudentId") + .IsUnique(); + + b.ToTable("GraduationAuditResults"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("GraduationYear") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GraduationYear", "Status"); + + b.ToTable("GraduationClearanceBatches"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("GraduationClearanceBatchId") + .HasColumnType("char(36)"); + + b.Property("IsRequired") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ResponsibleRole") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("ResponsibleUnit") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GraduationClearanceBatchId", "Code") + .IsUnique(); + + b.ToTable("GraduationClearanceItems"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CompletedByUserId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("GraduationClearanceItemId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("GraduationClearanceItemId", "StudentId") + .IsUnique(); + + b.HasIndex("StudentId", "Status"); + + b.ToTable("GraduationClearanceRecords"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Major", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CollegeId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DegreeType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SchoolingYears") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("CollegeId"); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Majors"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamAutoJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedSessions") + .HasColumnType("int"); + + b.Property("EnrolledStudents") + .HasColumnType("int"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MakeupExamPlanId") + .HasColumnType("char(36)"); + + b.Property("MessagesJson") + .HasColumnType("longtext"); + + b.Property("ProcessedCourses") + .HasColumnType("int"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalCourses") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("MakeupExamPlanId", "CreatedAt"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("MakeupExamAutoJobs"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamEnrollment", b => + { + b.Property("MakeupExamSessionId") + .HasColumnType("char(36)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("MakeupScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("Reason") + .HasColumnType("int"); + + b.Property("SourceDeferredExamId") + .HasColumnType("char(36)"); + + b.Property("SourceGradeRecordId") + .HasColumnType("char(36)"); + + b.HasKey("MakeupExamSessionId", "StudentId"); + + b.HasIndex("SourceDeferredExamId"); + + b.HasIndex("SourceGradeRecordId"); + + b.HasIndex("StudentId", "MakeupExamSessionId"); + + b.ToTable("MakeupExamEnrollments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcademicTermId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AcademicTermId", "Status"); + + b.ToTable("MakeupExamPlans"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ClassroomId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndsAt") + .HasColumnType("datetime(6)"); + + b.Property("ExamDate") + .HasColumnType("date"); + + b.Property("MakeupExamPlanId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PeriodCount") + .HasColumnType("int"); + + b.Property("RequiredBuildingId") + .HasColumnType("char(36)"); + + b.Property("RequiredBuildingIds") + .HasColumnType("longtext"); + + b.Property("RequiredInvigilatorCount") + .HasColumnType("int"); + + b.Property("StartPeriod") + .HasColumnType("int"); + + b.Property("StartsAt") + .HasColumnType("datetime(6)"); + + b.Property("TeachingTaskId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ClassroomId"); + + b.HasIndex("RequiredBuildingId"); + + b.HasIndex("TeachingTaskId"); + + b.HasIndex("MakeupExamPlanId", "ExamDate"); + + b.HasIndex("MakeupExamPlanId", "StartsAt"); + + b.ToTable("MakeupExamSessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamSessionInvigilator", b => + { + b.Property("MakeupExamSessionId") + .HasColumnType("char(36)"); + + b.Property("TeacherId") + .HasColumnType("char(36)"); + + b.HasKey("MakeupExamSessionId", "TeacherId"); + + b.HasIndex("TeacherId"); + + b.ToTable("MakeupExamSessionInvigilators"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MessageDispatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AudienceId") + .HasColumnType("char(36)"); + + b.Property("AudienceName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("AudienceType") + .HasColumnType("int"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("Content") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("LinkUrl") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RecipientCount") + .HasColumnType("int"); + + b.Property("SenderName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SenderUserId") + .HasColumnType("char(36)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SenderUserId", "CreatedAt"); + + b.ToTable("MessageDispatches"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("Content") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsRead") + .HasColumnType("tinyint(1)"); + + b.Property("LinkUrl") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MessageDispatchId") + .HasColumnType("char(36)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("MessageDispatchId"); + + b.HasIndex("UserId", "IsRead"); + + b.HasIndex("UserId", "Category", "CreatedAt"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DocumentNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvalidatedAt") + .HasColumnType("datetime(6)"); + + b.Property("InvalidatedByUserId") + .HasColumnType("char(36)"); + + b.Property("InvalidationReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IssuedAt") + .HasColumnType("datetime(6)"); + + b.Property("IssuedByUserId") + .HasColumnType("char(36)"); + + b.Property("PdfContent") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("PdfSha256") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Purpose") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ReissuedFromDocumentId") + .HasColumnType("char(36)"); + + b.Property("SnapshotJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("VerificationCodeHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentNumber") + .IsUnique(); + + b.HasIndex("InvalidatedByUserId"); + + b.HasIndex("IssuedByUserId"); + + b.HasIndex("ReissuedFromDocumentId") + .IsUnique(); + + b.HasIndex("VerificationCodeHash") + .IsUnique(); + + b.HasIndex("Status", "IssuedAt"); + + b.HasIndex("StudentId", "IssuedAt"); + + b.ToTable("OfficialDocuments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocumentDownload", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DownloadedByUserId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("OfficialDocumentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("DownloadedByUserId", "CreatedAt"); + + b.HasIndex("OfficialDocumentId", "CreatedAt"); + + b.ToTable("OfficialDocumentDownloads"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.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.ExperimentGradeItem", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", "ExperimentGradeSheet") + .WithMany("Items") + .HasForeignKey("ExperimentGradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExperimentGradeSheet"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItemScore", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeItem", "ExperimentGradeItem") + .WithMany("Scores") + .HasForeignKey("ExperimentGradeItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", "ExperimentGradeRecord") + .WithMany("ItemScores") + .HasForeignKey("ExperimentGradeRecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExperimentGradeItem"); + + b.Navigation("ExperimentGradeRecord"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", "ExperimentGradeSheet") + .WithMany("Records") + .HasForeignKey("ExperimentGradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentSession", "ExperimentSession") + .WithMany() + .HasForeignKey("ExperimentSessionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ExperimentGradeSheet"); + + b.Navigation("ExperimentSession"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentProject", "ExperimentProject") + .WithOne("GradeSheet") + .HasForeignKey("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", "ExperimentProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExperimentProject"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentSession", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentProject", "ExperimentProject") + .WithMany("Sessions") + .HasForeignKey("ExperimentProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("ExperimentProject"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItem", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GradeSheet", "GradeSheet") + .WithMany("Items") + .HasForeignKey("GradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GradeSheet"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItemScore", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GradeItem", "GradeItem") + .WithMany("Scores") + .HasForeignKey("GradeItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.GradeRecord", "GradeRecord") + .WithMany("ItemScores") + .HasForeignKey("GradeRecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GradeItem"); + + b.Navigation("GradeRecord"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeModification", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GradeRecord", "GradeRecord") + .WithMany() + .HasForeignKey("GradeRecordId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GradeRecord"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeRecord", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GradeSheet", "GradeSheet") + .WithMany("Records") + .HasForeignKey("GradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GradeSheet"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeSheet", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationAuditResult", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.CurriculumPlan", "CurriculumPlan") + .WithMany() + .HasForeignKey("CurriculumPlanId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.GraduationAuditBatch", "GraduationAuditBatch") + .WithMany("Results") + .HasForeignKey("GraduationAuditBatchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CurriculumPlan"); + + b.Navigation("GraduationAuditBatch"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceItem", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GraduationClearanceBatch", "GraduationClearanceBatch") + .WithMany("Items") + .HasForeignKey("GraduationClearanceBatchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GraduationClearanceBatch"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceRecord", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.GraduationClearanceItem", "GraduationClearanceItem") + .WithMany("Records") + .HasForeignKey("GraduationClearanceItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GraduationClearanceItem"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Major", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.College", "College") + .WithMany() + .HasForeignKey("CollegeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("College"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamAutoJob", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.MakeupExamPlan", "MakeupExamPlan") + .WithMany() + .HasForeignKey("MakeupExamPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MakeupExamPlan"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamEnrollment", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.MakeupExamSession", "MakeupExamSession") + .WithMany("Enrollments") + .HasForeignKey("MakeupExamSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.DeferredExam", "SourceDeferredExam") + .WithMany() + .HasForeignKey("SourceDeferredExamId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.GradeRecord", "SourceGradeRecord") + .WithMany() + .HasForeignKey("SourceGradeRecordId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("MakeupExamSession"); + + b.Navigation("SourceDeferredExam"); + + b.Navigation("SourceGradeRecord"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamPlan", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") + .WithMany() + .HasForeignKey("AcademicTermId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AcademicTerm"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamSession", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.MakeupExamPlan", "MakeupExamPlan") + .WithMany("Sessions") + .HasForeignKey("MakeupExamPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Building", "RequiredBuilding") + .WithMany() + .HasForeignKey("RequiredBuildingId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("MakeupExamPlan"); + + b.Navigation("RequiredBuilding"); + + b.Navigation("TeachingTask"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamSessionInvigilator", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.MakeupExamSession", "MakeupExamSession") + .WithMany("Invigilators") + .HasForeignKey("MakeupExamSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.Teacher", "Teacher") + .WithMany() + .HasForeignKey("TeacherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("MakeupExamSession"); + + b.Navigation("Teacher"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Notification", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.MessageDispatch", "MessageDispatch") + .WithMany("Notifications") + .HasForeignKey("MessageDispatchId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("MessageDispatch"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocument", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "InvalidatedByUser") + .WithMany() + .HasForeignKey("InvalidatedByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "IssuedByUser") + .WithMany() + .HasForeignKey("IssuedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.OfficialDocument", "ReissuedFromDocument") + .WithMany() + .HasForeignKey("ReissuedFromDocumentId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("InvalidatedByUser"); + + b.Navigation("IssuedByUser"); + + b.Navigation("ReissuedFromDocument"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocumentDownload", b => + { + b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "DownloadedByUser") + .WithMany() + .HasForeignKey("DownloadedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.OfficialDocument", "OfficialDocument") + .WithMany("Downloads") + .HasForeignKey("OfficialDocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DownloadedByUser"); + + b.Navigation("OfficialDocument"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.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.ExperimentGradeItem", b => + { + b.Navigation("Scores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", b => + { + b.Navigation("ItemScores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", b => + { + b.Navigation("Items"); + + b.Navigation("Records"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b => + { + b.Navigation("Bookings"); + + b.Navigation("GradeSheet"); + + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentSession", b => + { + b.Navigation("Bookings"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItem", b => + { + b.Navigation("Scores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeRecord", b => + { + b.Navigation("ItemScores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeSheet", b => + { + b.Navigation("Items"); + + b.Navigation("Records"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationAuditBatch", b => + { + b.Navigation("Results"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceBatch", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationClearanceItem", b => + { + b.Navigation("Records"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamPlan", b => + { + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MakeupExamSession", b => + { + b.Navigation("Enrollments"); + + b.Navigation("Invigilators"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MessageDispatch", b => + { + b.Navigation("Notifications"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocument", b => + { + b.Navigation("Downloads"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.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/20260728134931_ExperimentGradeManagement.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260728134931_ExperimentGradeManagement.cs new file mode 100644 index 0000000..79a293a --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260728134931_ExperimentGradeManagement.cs @@ -0,0 +1,208 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql +{ + /// + public partial class ExperimentGradeManagement : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SourceSnapshotAt", + table: "GradeItems", + type: "datetime(6)", + nullable: true); + + migrationBuilder.AddColumn( + name: "SourceType", + table: "GradeItems", + type: "int", + nullable: false, + defaultValue: 1); + + migrationBuilder.CreateTable( + name: "ExperimentGradeSheets", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + ExperimentProjectId = table.Column(type: "char(36)", nullable: false), + ContributionWeight = table.Column(type: "decimal(5,1)", precision: 5, scale: 1, nullable: false), + PassScore = table.Column(type: "decimal(5,1)", precision: 5, scale: 1, nullable: false), + Status = table.Column(type: "int", nullable: false), + ReviewComment = table.Column(type: "varchar(500)", maxLength: 500, nullable: true), + SubmittedAt = table.Column(type: "datetime(6)", nullable: true), + ReviewedAt = table.Column(type: "datetime(6)", nullable: true), + PublishedAt = 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_ExperimentGradeSheets", x => x.Id); + table.ForeignKey( + name: "FK_ExperimentGradeSheets_ExperimentProjects_ExperimentProjectId", + column: x => x.ExperimentProjectId, + principalTable: "ExperimentProjects", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "ExperimentGradeItems", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + ExperimentGradeSheetId = table.Column(type: "char(36)", nullable: false), + Name = table.Column(type: "varchar(60)", maxLength: 60, nullable: false), + Kind = table.Column(type: "int", nullable: false), + Weight = table.Column(type: "decimal(5,1)", precision: 5, scale: 1, nullable: false), + SortOrder = table.Column(type: "int", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExperimentGradeItems", x => x.Id); + table.ForeignKey( + name: "FK_ExperimentGradeItems_ExperimentGradeSheets_ExperimentGradeSh~", + column: x => x.ExperimentGradeSheetId, + principalTable: "ExperimentGradeSheets", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "ExperimentGradeRecords", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + ExperimentGradeSheetId = table.Column(type: "char(36)", nullable: false), + StudentId = table.Column(type: "char(36)", nullable: false), + ExperimentSessionId = table.Column(type: "char(36)", nullable: true), + ParticipationStatus = table.Column(type: "int", nullable: false), + TotalScore = table.Column(type: "decimal(5,1)", precision: 5, scale: 1, nullable: true), + IsPassed = table.Column(type: "tinyint(1)", nullable: true), + SafetyViolation = table.Column(type: "tinyint(1)", nullable: false), + AttemptNumber = table.Column(type: "int", nullable: false), + SubmissionReference = table.Column(type: "varchar(500)", maxLength: 500, nullable: true), + SubmittedAt = table.Column(type: "datetime(6)", nullable: true), + IsLate = table.Column(type: "tinyint(1)", nullable: false), + TeacherComment = table.Column(type: "varchar(500)", maxLength: 500, nullable: true), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExperimentGradeRecords", x => x.Id); + table.ForeignKey( + name: "FK_ExperimentGradeRecords_ExperimentGradeSheets_ExperimentGrade~", + column: x => x.ExperimentGradeSheetId, + principalTable: "ExperimentGradeSheets", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ExperimentGradeRecords_ExperimentSessions_ExperimentSessionId", + column: x => x.ExperimentSessionId, + principalTable: "ExperimentSessions", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_ExperimentGradeRecords_Students_StudentId", + column: x => x.StudentId, + principalTable: "Students", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "ExperimentGradeItemScores", + columns: table => new + { + ExperimentGradeRecordId = table.Column(type: "char(36)", nullable: false), + ExperimentGradeItemId = table.Column(type: "char(36)", nullable: false), + Score = table.Column(type: "decimal(5,1)", precision: 5, scale: 1, nullable: true), + Comment = table.Column(type: "varchar(300)", maxLength: 300, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ExperimentGradeItemScores", x => new { x.ExperimentGradeRecordId, x.ExperimentGradeItemId }); + table.ForeignKey( + name: "FK_ExperimentGradeItemScores_ExperimentGradeItems_ExperimentGra~", + column: x => x.ExperimentGradeItemId, + principalTable: "ExperimentGradeItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ExperimentGradeItemScores_ExperimentGradeRecords_ExperimentG~", + column: x => x.ExperimentGradeRecordId, + principalTable: "ExperimentGradeRecords", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_ExperimentGradeItems_ExperimentGradeSheetId_SortOrder", + table: "ExperimentGradeItems", + columns: new[] { "ExperimentGradeSheetId", "SortOrder" }); + + migrationBuilder.CreateIndex( + name: "IX_ExperimentGradeItemScores_ExperimentGradeItemId", + table: "ExperimentGradeItemScores", + column: "ExperimentGradeItemId"); + + migrationBuilder.CreateIndex( + name: "IX_ExperimentGradeRecords_ExperimentGradeSheetId_StudentId", + table: "ExperimentGradeRecords", + columns: new[] { "ExperimentGradeSheetId", "StudentId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ExperimentGradeRecords_ExperimentSessionId", + table: "ExperimentGradeRecords", + column: "ExperimentSessionId"); + + migrationBuilder.CreateIndex( + name: "IX_ExperimentGradeRecords_StudentId", + table: "ExperimentGradeRecords", + column: "StudentId"); + + migrationBuilder.CreateIndex( + name: "IX_ExperimentGradeSheets_ExperimentProjectId", + table: "ExperimentGradeSheets", + column: "ExperimentProjectId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ExperimentGradeItemScores"); + + migrationBuilder.DropTable( + name: "ExperimentGradeItems"); + + migrationBuilder.DropTable( + name: "ExperimentGradeRecords"); + + migrationBuilder.DropTable( + name: "ExperimentGradeSheets"); + + migrationBuilder.DropColumn( + name: "SourceSnapshotAt", + table: "GradeItems"); + + migrationBuilder.DropColumn( + name: "SourceType", + table: "GradeItems"); + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs index 6f52d3e..7c677e3 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs @@ -2043,6 +2043,176 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.ToTable("ExperimentBookings"); }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentGradeSheetId") + .HasColumnType("char(36)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("varchar(60)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Weight") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.HasKey("Id"); + + b.HasIndex("ExperimentGradeSheetId", "SortOrder"); + + b.ToTable("ExperimentGradeItems"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItemScore", b => + { + b.Property("ExperimentGradeRecordId") + .HasColumnType("char(36)"); + + b.Property("ExperimentGradeItemId") + .HasColumnType("char(36)"); + + b.Property("Comment") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Score") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.HasKey("ExperimentGradeRecordId", "ExperimentGradeItemId"); + + b.HasIndex("ExperimentGradeItemId"); + + b.ToTable("ExperimentGradeItemScores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentGradeSheetId") + .HasColumnType("char(36)"); + + b.Property("ExperimentSessionId") + .HasColumnType("char(36)"); + + b.Property("IsLate") + .HasColumnType("tinyint(1)"); + + b.Property("IsPassed") + .HasColumnType("tinyint(1)"); + + b.Property("ParticipationStatus") + .HasColumnType("int"); + + b.Property("SafetyViolation") + .HasColumnType("tinyint(1)"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("SubmissionReference") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("TeacherComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("TotalScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ExperimentSessionId"); + + b.HasIndex("StudentId"); + + b.HasIndex("ExperimentGradeSheetId", "StudentId") + .IsUnique(); + + b.ToTable("ExperimentGradeRecords"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContributionWeight") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExperimentProjectId") + .HasColumnType("char(36)"); + + b.Property("PassScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewComment") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ExperimentProjectId") + .IsUnique(); + + b.ToTable("ExperimentGradeSheets"); + }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b => { b.Property("Id") @@ -2176,6 +2346,14 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.Property("SortOrder") .HasColumnType("int"); + b.Property("SourceSnapshotAt") + .HasColumnType("datetime(6)"); + + b.Property("SourceType") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(1); + b.Property("UpdatedAt") .HasColumnType("datetime(6)"); @@ -4812,6 +4990,73 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.Navigation("Student"); }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItem", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", "ExperimentGradeSheet") + .WithMany("Items") + .HasForeignKey("ExperimentGradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExperimentGradeSheet"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItemScore", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeItem", "ExperimentGradeItem") + .WithMany("Scores") + .HasForeignKey("ExperimentGradeItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", "ExperimentGradeRecord") + .WithMany("ItemScores") + .HasForeignKey("ExperimentGradeRecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExperimentGradeItem"); + + b.Navigation("ExperimentGradeRecord"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", "ExperimentGradeSheet") + .WithMany("Records") + .HasForeignKey("ExperimentGradeSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentSession", "ExperimentSession") + .WithMany() + .HasForeignKey("ExperimentSessionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ExperimentGradeSheet"); + + b.Navigation("ExperimentSession"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentProject", "ExperimentProject") + .WithOne("GradeSheet") + .HasForeignKey("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", "ExperimentProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExperimentProject"); + }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b => { b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") @@ -5550,10 +5795,29 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.Navigation("SeatAssignments"); }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItem", b => + { + b.Navigation("Scores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", b => + { + b.Navigation("ItemScores"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", b => + { + b.Navigation("Items"); + + b.Navigation("Records"); + }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b => { b.Navigation("Bookings"); + b.Navigation("GradeSheet"); + b.Navigation("Sessions"); }); diff --git a/tests/Jiaowu.Api.Tests/ExperimentGradesControllerTests.cs b/tests/Jiaowu.Api.Tests/ExperimentGradesControllerTests.cs new file mode 100644 index 0000000..a68907d --- /dev/null +++ b/tests/Jiaowu.Api.Tests/ExperimentGradesControllerTests.cs @@ -0,0 +1,454 @@ +using Jiaowu.Api.Controllers; +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Infrastructure.Auth; +using Jiaowu.Api.Infrastructure.Persistence; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace Jiaowu.Api.Tests; + +public sealed class ExperimentGradesControllerTests +{ + [Fact] + public async Task IndependentExperimentGrade_CanPublishAndImportAsCourseSnapshot() + { + await using var fixture = await ExperimentGradeFixture.CreateAsync(); + fixture.Db.ExperimentProjects.Add(new ExperimentProject + { + TeachingTaskId = fixture.Task.Id, + Code = "LAB-C", + Name = "集中实验", + ArrangementMode = ExperimentArrangementMode.Centralized, + StartDate = fixture.Term.StartDate, + EndDate = fixture.Term.EndDate, + Status = ExperimentProjectStatus.Published, + PublishedAt = DateTime.UtcNow + }); + await fixture.Db.SaveChangesAsync(); + var project = await fixture.Db.ExperimentProjects.SingleAsync(); + var experimentGrades = fixture.ExperimentGrades(fixture.AdminScope); + + var created = await experimentGrades.CreateSheet( + new ExperimentGradeSheetRequest( + project.Id, + 1, + 60, + [ + new ExperimentGradeItemRequest( + "操作", + ExperimentGradeItemKind.Operation, + 50), + new ExperimentGradeItemRequest( + "报告", + ExperimentGradeItemKind.Report, + 50) + ]), + CancellationToken.None); + Assert.IsType(created); + + var sheet = await fixture.Db.ExperimentGradeSheets + .Include(x => x.Items) + .Include(x => x.Records) + .ThenInclude(x => x.ItemScores) + .SingleAsync(); + var record = Assert.Single(sheet.Records); + var operation = sheet.Items.Single(x => x.Name == "操作"); + var report = sheet.Items.Single(x => x.Name == "报告"); + + Assert.IsType( + await experimentGrades.UpdateRecords( + sheet.Id, + new ExperimentGradeRecordsRequest( + [ + new ExperimentGradeRecordRequest( + record.Id, + ExperimentParticipationStatus.Completed, + false, + 1, + "https://example.test/report", + DateTime.UtcNow, + false, + "完成规范", + [ + new ExperimentGradeItemScoreRequest( + operation.Id, + 80, + "操作正确"), + new ExperimentGradeItemScoreRequest( + report.Id, + 90, + "报告完整") + ]) + ]), + CancellationToken.None)); + Assert.IsType( + await experimentGrades.Submit( + sheet.Id, + CancellationToken.None)); + Assert.IsType( + await experimentGrades.Approve( + sheet.Id, + CancellationToken.None)); + Assert.IsType( + await experimentGrades.Publish( + sheet.Id, + CancellationToken.None)); + + fixture.Db.ChangeTracker.Clear(); + var publishedRecord = await fixture.Db.ExperimentGradeRecords + .SingleAsync(); + Assert.Equal(85m, publishedRecord.TotalScore); + Assert.True(publishedRecord.IsPassed); + Assert.Equal( + ExperimentGradeSheetStatus.Published, + (await fixture.Db.ExperimentGradeSheets.SingleAsync()).Status); + + var studentResult = await fixture + .ExperimentGrades(fixture.StudentScope) + .GetMine(CancellationToken.None); + Assert.IsType(studentResult); + + var courseSheet = new GradeSheet + { + TeachingTaskId = fixture.Task.Id, + RegularWeight = 0, + FinalWeight = 50, + Items = + [ + new GradeItem + { + Name = "实验", + Weight = 50, + SortOrder = 0 + } + ], + Records = + [ + new GradeRecord + { + StudentId = fixture.Student.Id, + FinalScore = 80 + } + ] + }; + var courseItem = courseSheet.Items.Single(); + courseSheet.Records.Single().ItemScores.Add(new GradeItemScore + { + GradeItemId = courseItem.Id + }); + fixture.Db.GradeSheets.Add(courseSheet); + await fixture.Db.SaveChangesAsync(); + + var grades = new GradesController(fixture.Db, fixture.AdminScope); + Assert.IsType( + await grades.ImportExperimentScores( + courseSheet.Id, + new ImportExperimentScoresRequest(courseItem.Id), + CancellationToken.None)); + + fixture.Db.ChangeTracker.Clear(); + var importedItem = await fixture.Db.GradeItems.SingleAsync(); + var importedRecord = await fixture.Db.GradeRecords + .Include(x => x.ItemScores) + .SingleAsync(); + Assert.Equal( + GradeItemSourceType.ExperimentSummary, + importedItem.SourceType); + Assert.NotNull(importedItem.SourceSnapshotAt); + Assert.Equal(85m, importedRecord.ItemScores.Single().Score); + Assert.Equal(82.5m, importedRecord.TotalScore); + + var manualOverride = await grades.UpdateRecords( + courseSheet.Id, + new GradeRecordsRequest( + [ + new GradeRecordRequest( + importedRecord.Id, + null, + 80, + [new GradeItemScoreRequest(importedItem.Id, 90)], + GradeExamStatus.Normal, + null) + ]), + CancellationToken.None); + Assert.IsType(manualOverride); + } + + [Fact] + public async Task SelfScheduledSheet_SyncsOnlyActiveBookings() + { + await using var fixture = await ExperimentGradeFixture.CreateAsync(); + var project = new ExperimentProject + { + TeachingTaskId = fixture.Task.Id, + Code = "LAB-S", + Name = "自主实验", + ArrangementMode = ExperimentArrangementMode.SelfScheduled, + StartDate = fixture.Term.StartDate, + EndDate = fixture.Term.EndDate, + Status = ExperimentProjectStatus.Published, + PublishedAt = DateTime.UtcNow + }; + var session = new ExperimentSession + { + ExperimentProjectId = project.Id, + ClassroomId = fixture.Classroom.Id, + SessionDate = fixture.Term.StartDate, + StartPeriod = 1, + PeriodCount = 2, + Capacity = 10, + ReservedCount = 1 + }; + var booking = new ExperimentBooking + { + ExperimentProjectId = project.Id, + ExperimentSessionId = session.Id, + StudentId = fixture.Student.Id, + Status = ExperimentBookingStatus.Booked, + BookedAt = DateTime.UtcNow + }; + fixture.Db.AddRange(project, session, booking); + await fixture.Db.SaveChangesAsync(); + var controller = fixture.ExperimentGrades(fixture.AdminScope); + + await controller.CreateSheet( + new ExperimentGradeSheetRequest( + project.Id, + 1, + 60, + [ + new ExperimentGradeItemRequest( + "操作", + ExperimentGradeItemKind.Operation, + 100) + ]), + CancellationToken.None); + var sheet = await fixture.Db.ExperimentGradeSheets.SingleAsync(); + Assert.Single(fixture.Db.ExperimentGradeRecords); + + booking.Status = ExperimentBookingStatus.Cancelled; + booking.CancelledAt = DateTime.UtcNow; + await fixture.Db.SaveChangesAsync(); + Assert.IsType( + await controller.SyncParticipants( + sheet.Id, + CancellationToken.None)); + Assert.Empty(fixture.Db.ExperimentGradeRecords); + + fixture.Db.ChangeTracker.Clear(); + booking = await fixture.Db.ExperimentBookings.SingleAsync(); + controller = fixture.ExperimentGrades(fixture.AdminScope); + booking.Status = ExperimentBookingStatus.Booked; + booking.CancelledAt = null; + await fixture.Db.SaveChangesAsync(); + Assert.IsType( + await controller.SyncParticipants( + sheet.Id, + CancellationToken.None)); + Assert.Single(fixture.Db.ExperimentGradeRecords); + } + + private sealed class ExperimentGradeFixture : IAsyncDisposable + { + private ExperimentGradeFixture( + SqliteConnection connection, + AppDbContext db, + AcademicTerm term, + TeachingTask task, + Student student, + Classroom classroom, + ICurrentUserDataScope adminScope, + ICurrentUserDataScope studentScope) + { + Connection = connection; + Db = db; + Term = term; + Task = task; + Student = student; + Classroom = classroom; + AdminScope = adminScope; + StudentScope = studentScope; + } + + private SqliteConnection Connection { get; } + public AppDbContext Db { get; } + public AcademicTerm Term { get; } + public TeachingTask Task { get; } + public Student Student { get; } + public Classroom Classroom { get; } + public ICurrentUserDataScope AdminScope { get; } + public ICurrentUserDataScope StudentScope { get; } + + public static async Task CreateAsync() + { + var connection = new SqliteConnection("Data Source=:memory:"); + await connection.OpenAsync(); + var db = new AppDbContext( + new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options); + await db.Database.EnsureCreatedAsync(); + + var admin = User("lab-admin", "实验管理员"); + var studentUser = User("lab-student", "实验学生"); + var teacherUser = User("lab-teacher", "实验教师"); + var campus = new Campus { Code = "MAIN", Name = "主校区" }; + var building = new Building + { + CampusId = campus.Id, + Code = "LAB", + Name = "实验楼" + }; + var classroom = new Classroom + { + BuildingId = building.Id, + Code = "LAB201", + Name = "实验室 201", + Capacity = 30 + }; + var college = new College { Code = "CS", Name = "计算机学院" }; + var major = new Major + { + CollegeId = college.Id, + Code = "CS", + Name = "计算机科学与技术", + DegreeType = "工学" + }; + var administrativeClass = new AdministrativeClass + { + MajorId = major.Id, + Code = "CS2099", + Name = "计科 2099", + Grade = 2099 + }; + var student = new Student + { + UserId = studentUser.Id, + AdministrativeClassId = administrativeClass.Id, + StudentNumber = "20990001", + Name = "实验学生", + EnrollmentYear = 2099, + EnrollmentDate = new DateOnly(2099, 9, 1) + }; + var teacher = new Teacher + { + UserId = teacherUser.Id, + CollegeId = college.Id, + TeacherNumber = "T2099", + Name = "实验教师" + }; + 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 + { + CollegeId = college.Id, + Code = "CSLAB", + Name = "系统实验", + Credits = 2, + TotalHours = 32, + LectureHours = 16, + PracticeHours = 16, + Nature = CourseNature.Practice, + AssessmentMethod = AssessmentMethod.Assessment + }; + var task = new TeachingTask + { + AcademicTermId = term.Id, + CourseId = course.Id, + TaskNumber = "2099-1-CSLAB-01", + Name = "系统实验教学班", + Capacity = 30, + Status = TeachingTaskStatus.Published, + Teachers = + [ + new TeachingTaskTeacher + { + TeacherId = teacher.Id, + IsPrimary = true + } + ], + Classes = + [ + new TeachingTaskClass + { + AdministrativeClassId = administrativeClass.Id + } + ] + }; + db.AddRange( + admin, + studentUser, + teacherUser, + campus, + building, + classroom, + college, + major, + administrativeClass, + student, + teacher, + term, + course, + task); + await db.SaveChangesAsync(); + + return new ExperimentGradeFixture( + connection, + db, + term, + task, + student, + classroom, + Scope(admin, SystemRoles.SuperAdmin, DataScope.All), + Scope(studentUser, SystemRoles.Student, DataScope.Self)); + } + + public ExperimentGradesController ExperimentGrades( + ICurrentUserDataScope scope) => new(Db, scope); + + 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 11b7db5..cc0cd0c 100644 --- a/web/src/layouts/AdminLayout.vue +++ b/web/src/layouts/AdminLayout.vue @@ -80,11 +80,16 @@ const navigationGroups = computed(() => [ ? [{ key: 'experiments', label: isStudent.value ? '我的实验' : '实验管理', - direct: true, - items: [{ - path: '/experiments', - label: isStudent.value ? '我的实验' : '实验管理', - }], + items: [ + { + path: '/experiments', + label: isStudent.value ? '实验安排' : '项目与场次', + }, + { + path: '/experiment-grades', + label: '实验成绩', + }, + ], } as NavigationGroup] : []), { diff --git a/web/src/router/index.ts b/web/src/router/index.ts index ee43810..c428e1e 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -200,6 +200,14 @@ const router = createRouter({ roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher', 'Student'], }, }, + { + path: 'experiment-grades', + name: 'experiment-grades', + component: () => import('../views/ExperimentGradesView.vue'), + meta: { + roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher', 'Student'], + }, + }, { path: 'course-selections', name: 'course-selections', diff --git a/web/src/views/ExperimentGradesView.vue b/web/src/views/ExperimentGradesView.vue new file mode 100644 index 0000000..c5b1b04 --- /dev/null +++ b/web/src/views/ExperimentGradesView.vue @@ -0,0 +1,834 @@ + + + + + diff --git a/web/src/views/GradesView.vue b/web/src/views/GradesView.vue index d5e5254..0be6c33 100644 --- a/web/src/views/GradesView.vue +++ b/web/src/views/GradesView.vue @@ -39,6 +39,7 @@ const taskTotal = ref(0) const recordKeyword = ref('') const recordPage = ref(1) const recordPageSize = 50 +const experimentGradeItemId = ref('') const createForm = reactive({ regularWeight: 30, finalWeight: 70, @@ -186,6 +187,12 @@ async function loadDetail() { detail.value.canEdit = response.canEdit detail.value.canReview = response.canReview detail.value.canPublish = response.canPublish + if (!detail.value.items?.some((item: any) => item.id === experimentGradeItemId.value)) { + experimentGradeItemId.value = + detail.value.items?.find((item: any) => item.name.includes('实验'))?.id + ?? detail.value.items?.[0]?.id + ?? '' + } } catch (error) { ElMessage.error(apiErrorMessage(error)) } finally { @@ -254,6 +261,29 @@ async function saveRecords() { } } +async function importExperimentScores() { + if (!experimentGradeItemId.value) { + ElMessage.warning('请先选择用于承接实验成绩的课程成绩项。') + return + } + + try { + const result = (await http.post( + `/grades/sheets/${detail.value.id}/import-experiment-scores`, + { gradeItemId: experimentGradeItemId.value }, + )).data + const missingHint = result.missingCount > 0 + ? `;${result.missingCount} 名学生因实验成绩尚未完整发布而保持空白` + : '' + ElMessage.success( + `已从 ${result.publishedProjectCount} 个实验项目导入 ${result.importedCount} 名学生的成绩快照${missingHint}`, + ) + await loadDetail() + } catch (error) { + ElMessage.error(apiErrorMessage(error)) + } +} + async function submitSheet() { try { await ElMessageBox.confirm( @@ -583,17 +613,20 @@ onMounted(async () => { @@ -654,6 +687,21 @@ onMounted(async () => {
+
+ + + + 导入实验成绩 +
下载模板 Excel导入 保存成绩 @@ -765,6 +813,10 @@ onMounted(async () => { .workflow-status .college-hint { color: #6b4e16; font-weight: 600; } .grade-actions { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; padding: 12px 16px; border-top: 1px solid #e4e7ed; background: #fafbfc; } .grade-actions-buttons { display: flex; gap: 8px; flex-wrap: wrap; } +.experiment-import { display: flex; gap: 6px; align-items: center; padding-right: 8px; border-right: 1px solid var(--line); } +.experiment-import .el-select { width: 165px; } +.item-score-readonly { display: inline-flex; flex-direction: column; align-items: center; line-height: 1.1; } +.item-score-readonly small { margin-top: 3px; color: #2d8975; font-size: 10px; font-weight: 650; } .grade-toolbar > .el-input { width: min(320px, 100%); } .grade-task-pagination { justify-content: center; padding: 14px 8px; border-top: 1px solid var(--line); } .grade-record-toolbar { @@ -777,5 +829,7 @@ onMounted(async () => { @media (max-width: 760px) { .grade-record-toolbar > span { width: 100%; margin-left: 0; } .grade-record-pagination { justify-content: center; } + .experiment-import { width: 100%; padding-right: 0; padding-bottom: 8px; border-right: 0; border-bottom: 1px solid var(--line); } + .experiment-import .el-select { flex: 1; width: auto; } }