From b0eaa20da6fa90176b3e2af62cc95bb37e26f3da Mon Sep 17 00:00:00 2001 From: biss Date: Fri, 24 Jul 2026 17:30:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AD=A6=E7=B1=8D=E5=BC=82=E5=8A=A8=EF=BC=9A?= =?UTF-8?q?=E4=BC=91=E5=AD=A6=E3=80=81=E5=A4=8D=E5=AD=A6=E3=80=81=E9=80=80?= =?UTF-8?q?=E5=AD=A6=E7=94=B3=E8=AF=B7=E5=8F=8A=E8=BE=85=E5=AF=BC=E5=91=98?= =?UTF-8?q?=E2=86=92=E5=AD=A6=E9=99=A2=E2=86=92=E6=95=99=E5=8A=A1=E5=A4=84?= =?UTF-8?q?=E5=88=86=E7=BA=A7=E5=AE=A1=E6=89=B9=E3=80=82=20=E6=AF=95?= =?UTF-8?q?=E4=B8=9A=E5=AE=A1=E6=A0=B8=EF=BC=9A=E6=89=B9=E6=AC=A1=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=E3=80=81=E7=BC=BA=E5=A4=B1=E8=AF=BE=E7=A8=8B=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E3=80=81=E4=BA=BA=E5=B7=A5=E5=A4=8D=E6=A0=B8=E3=80=81?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E5=8F=91=E5=B8=83=E3=80=82=20=E5=AD=A6?= =?UTF-8?q?=E4=BD=8D=E6=8E=88=E4=BA=88=EF=BC=9A=E6=AF=95=E4=B8=9A=E8=B5=84?= =?UTF-8?q?=E6=A0=BC=E4=B8=8E=20GPA=20=E8=AE=A1=E7=AE=97=E3=80=81=E4=BA=BA?= =?UTF-8?q?=E5=B7=A5=E8=B0=83=E6=95=B4=E3=80=81=E5=8F=91=E5=B8=83=E6=8E=88?= =?UTF-8?q?=E4=BA=88=E7=BB=93=E6=9E=9C=E3=80=82=20=E6=AF=95=E4=B8=9A?= =?UTF-8?q?=E7=A6=BB=E6=A0=A1=EF=BC=9A=E7=A6=BB=E6=A0=A1=E4=BA=8B=E9=A1=B9?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E3=80=81=E8=B4=A3=E4=BB=BB=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E5=88=86=E5=B7=A5=E3=80=81=E9=80=90=E9=A1=B9=E5=8A=9E=E7=90=86?= =?UTF-8?q?=E5=8F=8A=E6=89=B9=E6=AC=A1=E5=85=B3=E9=97=AD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- scripts/smoke-test.ps1 | 418 +++ .../Controllers/DashboardController.cs | 25 + .../Controllers/DegreeAwardsController.cs | 278 ++ .../Controllers/GraduationAuditsController.cs | 332 +++ .../GraduationClearanceController.cs | 318 +++ .../StudentStatusChangesController.cs | 79 +- .../Domain/Academic/DegreeAwardEntities.cs | 45 + .../Academic/GraduationAuditEntities.cs | 50 + .../Academic/GraduationClearanceEntities.cs | 53 + .../Graduation/DegreeAwardRules.cs | 17 + .../Graduation/GraduationAuditRules.cs | 24 + .../Graduation/GraduationClearanceRules.cs | 16 + .../Persistence/AppDbContext.cs | 88 + .../Persistence/DevelopmentSqliteMigrator.cs | 130 + ...0260724075918_GraduationAudits.Designer.cs | 2183 +++++++++++++++ .../MySql/20260724075918_GraduationAudits.cs | 124 + .../20260724081821_DegreeAwards.Designer.cs | 2324 +++++++++++++++ .../MySql/20260724081821_DegreeAwards.cs | 115 + ...0724092052_GraduationClearance.Designer.cs | 2491 +++++++++++++++++ .../20260724092052_GraduationClearance.cs | 128 + .../MySql/AppDbContextModelSnapshot.cs | 460 +++ .../Jiaowu.Api.Tests/DegreeAwardRulesTests.cs | 32 + .../GraduationAuditRulesTests.cs | 38 + .../GraduationClearanceRulesTests.cs | 38 + web/src/components.d.ts | 3 + web/src/layouts/AdminLayout.vue | 36 +- web/src/router/index.ts | 32 + web/src/style.css | 257 ++ web/src/views/DashboardView.vue | 26 +- web/src/views/DegreeAwardsView.vue | 201 ++ web/src/views/GraduationAuditsView.vue | 248 ++ web/src/views/GraduationClearanceView.vue | 252 ++ web/src/views/StudentStatusChangesView.vue | 282 ++ 34 files changed, 11137 insertions(+), 8 deletions(-) create mode 100644 src/Jiaowu.Api/Controllers/DegreeAwardsController.cs create mode 100644 src/Jiaowu.Api/Controllers/GraduationAuditsController.cs create mode 100644 src/Jiaowu.Api/Controllers/GraduationClearanceController.cs create mode 100644 src/Jiaowu.Api/Domain/Academic/DegreeAwardEntities.cs create mode 100644 src/Jiaowu.Api/Domain/Academic/GraduationAuditEntities.cs create mode 100644 src/Jiaowu.Api/Domain/Academic/GraduationClearanceEntities.cs create mode 100644 src/Jiaowu.Api/Infrastructure/Graduation/DegreeAwardRules.cs create mode 100644 src/Jiaowu.Api/Infrastructure/Graduation/GraduationAuditRules.cs create mode 100644 src/Jiaowu.Api/Infrastructure/Graduation/GraduationClearanceRules.cs create mode 100644 src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724075918_GraduationAudits.Designer.cs create mode 100644 src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724075918_GraduationAudits.cs create mode 100644 src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724081821_DegreeAwards.Designer.cs create mode 100644 src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724081821_DegreeAwards.cs create mode 100644 src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724092052_GraduationClearance.Designer.cs create mode 100644 src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724092052_GraduationClearance.cs create mode 100644 tests/Jiaowu.Api.Tests/DegreeAwardRulesTests.cs create mode 100644 tests/Jiaowu.Api.Tests/GraduationAuditRulesTests.cs create mode 100644 tests/Jiaowu.Api.Tests/GraduationClearanceRulesTests.cs create mode 100644 web/src/views/DegreeAwardsView.vue create mode 100644 web/src/views/GraduationAuditsView.vue create mode 100644 web/src/views/GraduationClearanceView.vue create mode 100644 web/src/views/StudentStatusChangesView.vue diff --git a/README.md b/README.md index 5941a07..27a493d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 面向普通高校的教务管理系统。后端使用 ASP.NET Core 10、EF Core 10,前端使用 Vue 3、TypeScript 和 Element Plus。 -当前已实现系统登录与角色权限、基础数据、用户管理、教师档案、学生档案、课程库、培养方案、教学任务、排课课表、学生选课、成绩管理、考试考场和首页统计。人员及课程列表支持组合筛选、服务端分页和完整增删改查;培养方案支持课程模块、专业年级版本、复制新版本、发布锁定和旧版本归档;教学任务支持学期课程开设、多教师、合班、容量校验、发布与结课;排课支持单双周、周次节次、教室容量、教师/行政班/教室冲突校验和版本化发布;选课支持批次时间窗、投放范围、容量与学分上限、重复课程与课表冲突校验、退课截止时间和实时教学班名单;成绩管理支持分项比例、批量录入、特殊考试状态、自动总评与绩点、教师提交、学院审核、校级发布和学生成绩单;考试管理支持考试计划、场次、考场容量、监考教师、考生名单以及考场/监考/学生时间冲突校验。 +当前已实现系统登录与角色权限、基础数据、用户管理、教师档案、学生档案、课程库、培养方案、教学任务、排课课表、学生选课、成绩管理、考试考场、学籍异动、毕业审核、学位授予、毕业离校和首页统计。人员及课程列表支持组合筛选、服务端分页和完整增删改查;培养方案支持课程模块、专业年级版本、复制新版本、发布锁定和旧版本归档;教学任务支持学期课程开设、多教师、合班、容量校验、发布与结课;排课支持单双周、周次节次、教室容量、教师/行政班/教室冲突校验和版本化发布;选课支持批次时间窗、投放范围、容量与学分上限、重复课程与课表冲突校验、退课截止时间和实时教学班名单;成绩管理支持分项比例、批量录入、特殊考试状态、自动总评与绩点、教师提交、学院审核、校级发布和学生成绩单;考试管理支持考试计划、场次、考场容量、监考教师、考生名单以及考场/监考/学生时间冲突校验;学籍异动支持休学、复学、退学申请,辅导员、学院、学校三级顺序审核,学生撤回,以及最终审批后自动同步学籍状态;毕业审核按入学年级匹配已发布培养方案,以正式成绩计算总学分、必修通过和未解决不及格课程,支持学院范围查看、人工复核、校级锁定发布和学生结果查询;学位授予以已发布毕业资格为来源,按正式成绩加权平均绩点生成规则结论,支持学院人工复核、校级发布锁定和学生结果查询;毕业离校支持自定义事项与责任部门,按校级、学院、辅导员角色分工办理,强制数据范围校验,学生进度查询,以及必办事项全部完成后的批次锁定。 权限采用后端强制校验的角色与数据范围模型。多角色账号按 `All > College > Class > Self` 取最高数据范围:校级角色可访问全校数据,院系管理员限定本学院,辅导员通过稳定的账号 ID 绑定所带行政班,教师和学生限定本人及当前教学关系;前端菜单和路由限制仅作为交互辅助,不替代 API 授权。 diff --git a/scripts/smoke-test.ps1 b/scripts/smoke-test.ps1 index accd849..a1b8929 100644 --- a/scripts/smoke-test.ps1 +++ b/scripts/smoke-test.ps1 @@ -328,6 +328,416 @@ try { throw 'Personal exam schedule is not visible to student or invigilator.' } + $statusOptions = Invoke-RestMethod ` + -Uri 'http://localhost:5255/api/student-status-changes/options' ` + -Headers $studentHeaders + $statusChanges = Invoke-RestMethod ` + -Uri 'http://localhost:5255/api/student-status-changes' ` + -Headers $studentHeaders + $openStatusChange = @($statusChanges) | + Where-Object { $_.state -in @('Submitted', 'CounselorApproved', 'CollegeApproved') } | + Select-Object -First 1 + if ($null -eq $openStatusChange) { + $statusChangeType = if ($statusOptions.status -eq 'Active') { + 'Suspension' + } + elseif ($statusOptions.status -eq 'Suspended') { + 'Resumption' + } + else { + throw "Smoke-test student status '$($statusOptions.status)' cannot start a reversible change." + } + $statusChangeBody = @{ + type = $statusChangeType + reason = '端到端验证学籍异动三级审核流程。' + } | ConvertTo-Json + $createdStatusChange = Invoke-RestMethod ` + -Method Post ` + -Uri 'http://localhost:5255/api/student-status-changes' ` + -Headers $studentHeaders ` + -ContentType 'application/json' ` + -Body $statusChangeBody + $statusChanges = Invoke-RestMethod ` + -Uri 'http://localhost:5255/api/student-status-changes' ` + -Headers $studentHeaders + $openStatusChange = @($statusChanges) | + Where-Object { $_.id -eq $createdStatusChange.id } | + Select-Object -First 1 + } + $expectedStudentStatus = $openStatusChange.targetStatus + $approvalBody = @{ approved = $true; comment = '端到端审核通过。' } | ConvertTo-Json + + if ($openStatusChange.state -eq 'Submitted') { + $prematureCollegeReview = Invoke-WebRequest ` + -Method Post ` + -Uri "http://localhost:5255/api/student-status-changes/$($openStatusChange.id)/review" ` + -Headers $collegeHeaders ` + -ContentType 'application/json' ` + -Body $approvalBody ` + -SkipHttpErrorCheck + if ($prematureCollegeReview.StatusCode -ne 409) { + throw 'College reviewer was able to bypass the counselor stage.' + } + $counselorLoginBody = @{ + userName = 'counselor' + password = 'Counselor@123456' + } | ConvertTo-Json + $counselorLogin = Invoke-RestMethod ` + -Method Post ` + -Uri 'http://localhost:5255/api/auth/login' ` + -ContentType 'application/json' ` + -Body $counselorLoginBody + $counselorHeaders = @{ Authorization = "Bearer $($counselorLogin.token)" } + Invoke-RestMethod ` + -Method Post ` + -Uri "http://localhost:5255/api/student-status-changes/$($openStatusChange.id)/review" ` + -Headers $counselorHeaders ` + -ContentType 'application/json' ` + -Body $approvalBody | + Out-Null + $openStatusChange.state = 'CounselorApproved' + } + if ($openStatusChange.state -eq 'CounselorApproved') { + Invoke-RestMethod ` + -Method Post ` + -Uri "http://localhost:5255/api/student-status-changes/$($openStatusChange.id)/review" ` + -Headers $collegeHeaders ` + -ContentType 'application/json' ` + -Body $approvalBody | + Out-Null + $openStatusChange.state = 'CollegeApproved' + } + if ($openStatusChange.state -eq 'CollegeApproved') { + Invoke-RestMethod ` + -Method Post ` + -Uri "http://localhost:5255/api/student-status-changes/$($openStatusChange.id)/review" ` + -Headers $headers ` + -ContentType 'application/json' ` + -Body $approvalBody | + Out-Null + } + $completedStatusChanges = Invoke-RestMethod ` + -Uri 'http://localhost:5255/api/student-status-changes' ` + -Headers $studentHeaders + $completedStatusChange = @($completedStatusChanges) | + Where-Object { $_.id -eq $openStatusChange.id } | + Select-Object -First 1 + $updatedStatusOptions = Invoke-RestMethod ` + -Uri 'http://localhost:5255/api/student-status-changes/options' ` + -Headers $studentHeaders + if ($completedStatusChange.state -ne 'Approved' -or + $updatedStatusOptions.status -ne $expectedStudentStatus) { + throw 'Student status change was not finalized or did not update the student record.' + } + + $graduationBatches = Invoke-RestMethod ` + -Uri 'http://localhost:5255/api/graduation-audits/batches' ` + -Headers $headers + $graduationBatch = @($graduationBatches) | + Where-Object { $_.name -eq '2030届端到端毕业资格审核' } | + Select-Object -First 1 + if ($null -eq $graduationBatch) { + $graduationBatchBody = @{ + name = '2030届端到端毕业资格审核' + graduationYear = 2030 + enrollmentYear = 2026 + notes = '用于验证培养方案、成绩快照、人工复核与结果发布。' + } | ConvertTo-Json + $createdGraduationBatch = Invoke-RestMethod ` + -Method Post ` + -Uri 'http://localhost:5255/api/graduation-audits/batches' ` + -Headers $headers ` + -ContentType 'application/json' ` + -Body $graduationBatchBody + Invoke-RestMethod ` + -Method Post ` + -Uri "http://localhost:5255/api/graduation-audits/batches/$($createdGraduationBatch.id)/calculate" ` + -Headers $headers | + Out-Null + $graduationBatch = Invoke-RestMethod ` + -Uri "http://localhost:5255/api/graduation-audits/batches/$($createdGraduationBatch.id)" ` + -Headers $headers + $studentGraduationResult = @($graduationBatch.results) | + Where-Object { $_.studentNumber -eq '202601001' } | + Select-Object -First 1 + if ($null -eq $studentGraduationResult) { + throw 'Calculated graduation batch does not contain the smoke-test student.' + } + $overrideDecisionBody = @{ + conclusion = 'Eligible' + comment = '端到端验证人工复核调整。' + } | ConvertTo-Json + Invoke-RestMethod ` + -Method Put ` + -Uri "http://localhost:5255/api/graduation-audits/results/$($studentGraduationResult.id)" ` + -Headers $headers ` + -ContentType 'application/json' ` + -Body $overrideDecisionBody | + Out-Null + $restoreDecisionBody = @{ + conclusion = 'Ineligible' + comment = '恢复规则结论后发布测试结果。' + } | ConvertTo-Json + Invoke-RestMethod ` + -Method Put ` + -Uri "http://localhost:5255/api/graduation-audits/results/$($studentGraduationResult.id)" ` + -Headers $headers ` + -ContentType 'application/json' ` + -Body $restoreDecisionBody | + Out-Null + Invoke-RestMethod ` + -Method Post ` + -Uri "http://localhost:5255/api/graduation-audits/batches/$($graduationBatch.id)/publish" ` + -Headers $headers | + Out-Null + } + else { + $graduationBatch = Invoke-RestMethod ` + -Uri "http://localhost:5255/api/graduation-audits/batches/$($graduationBatch.id)" ` + -Headers $headers + } + $graduationBatch = Invoke-RestMethod ` + -Uri "http://localhost:5255/api/graduation-audits/batches/$($graduationBatch.id)" ` + -Headers $headers + $studentGraduationResult = Invoke-RestMethod ` + -Uri 'http://localhost:5255/api/graduation-audits/my-result' ` + -Headers $studentHeaders + if ($graduationBatch.status -ne 'Published' -or + $studentGraduationResult.conclusion -ne 'Ineligible') { + throw 'Published graduation result is not visible to the student.' + } + + $graduationBatches = Invoke-RestMethod ` + -Uri 'http://localhost:5255/api/graduation-audits/batches' ` + -Headers $headers + $degreeSourceBatch = @($graduationBatches) | + Where-Object { $_.name -eq '2030届学位授予资格演示' } | + Select-Object -First 1 + if ($null -eq $degreeSourceBatch) { + $degreeSourceBody = @{ + name = '2030届学位授予资格演示' + graduationYear = 2030 + enrollmentYear = 2026 + notes = '为未关联登录账号的演示学生生成学位授予前置资格。' + } | ConvertTo-Json + $createdDegreeSource = Invoke-RestMethod ` + -Method Post ` + -Uri 'http://localhost:5255/api/graduation-audits/batches' ` + -Headers $headers ` + -ContentType 'application/json' ` + -Body $degreeSourceBody + Invoke-RestMethod ` + -Method Post ` + -Uri "http://localhost:5255/api/graduation-audits/batches/$($createdDegreeSource.id)/calculate" ` + -Headers $headers | + Out-Null + $degreeSourceDetail = Invoke-RestMethod ` + -Uri "http://localhost:5255/api/graduation-audits/batches/$($createdDegreeSource.id)" ` + -Headers $headers + $degreeSourceResult = @($degreeSourceDetail.results) | + Where-Object { $_.studentNumber -eq '202601002' } | + Select-Object -First 1 + $degreeSourceDecision = @{ + conclusion = 'Eligible' + comment = '演示学生经校级专项复核符合毕业条件。' + } | ConvertTo-Json + Invoke-RestMethod ` + -Method Put ` + -Uri "http://localhost:5255/api/graduation-audits/results/$($degreeSourceResult.id)" ` + -Headers $headers ` + -ContentType 'application/json' ` + -Body $degreeSourceDecision | + Out-Null + Invoke-RestMethod ` + -Method Post ` + -Uri "http://localhost:5255/api/graduation-audits/batches/$($createdDegreeSource.id)/publish" ` + -Headers $headers | + Out-Null + } + + $degreeBatches = Invoke-RestMethod ` + -Uri 'http://localhost:5255/api/degree-awards/batches' ` + -Headers $headers + $degreeBatch = @($degreeBatches) | + Where-Object { $_.name -eq '2030届端到端学位授予审核' } | + Select-Object -First 1 + if ($null -eq $degreeBatch) { + $degreeBatchBody = @{ + name = '2030届端到端学位授予审核' + graduationYear = 2030 + degreeName = '工学学士' + minimumGradePoint = 0 + notes = '验证毕业资格来源、绩点快照、人工复核与发布锁定。' + } | ConvertTo-Json + $createdDegreeBatch = Invoke-RestMethod ` + -Method Post ` + -Uri 'http://localhost:5255/api/degree-awards/batches' ` + -Headers $headers ` + -ContentType 'application/json' ` + -Body $degreeBatchBody + Invoke-RestMethod ` + -Method Post ` + -Uri "http://localhost:5255/api/degree-awards/batches/$($createdDegreeBatch.id)/calculate" ` + -Headers $headers | + Out-Null + $degreeBatch = Invoke-RestMethod ` + -Uri "http://localhost:5255/api/degree-awards/batches/$($createdDegreeBatch.id)" ` + -Headers $headers + $degreeResult = @($degreeBatch.results) | Select-Object -First 1 + if ($null -eq $degreeResult -or $degreeResult.conclusion -ne 'Granted') { + throw 'Degree-award calculation did not include the eligible graduated student.' + } + $degreeRejectBody = @{ + conclusion = 'NotGranted' + comment = '端到端验证学位结论人工调整。' + } | ConvertTo-Json + Invoke-RestMethod ` + -Method Put ` + -Uri "http://localhost:5255/api/degree-awards/results/$($degreeResult.id)" ` + -Headers $collegeHeaders ` + -ContentType 'application/json' ` + -Body $degreeRejectBody | + Out-Null + $degreeRestoreBody = @{ + conclusion = 'Granted' + comment = '恢复规则结论并完成授予发布。' + } | ConvertTo-Json + Invoke-RestMethod ` + -Method Put ` + -Uri "http://localhost:5255/api/degree-awards/results/$($degreeResult.id)" ` + -Headers $headers ` + -ContentType 'application/json' ` + -Body $degreeRestoreBody | + Out-Null + Invoke-RestMethod ` + -Method Post ` + -Uri "http://localhost:5255/api/degree-awards/batches/$($degreeBatch.id)/publish" ` + -Headers $headers | + Out-Null + } + $degreeBatch = Invoke-RestMethod ` + -Uri "http://localhost:5255/api/degree-awards/batches/$($degreeBatch.id)" ` + -Headers $headers + $collegeDegreeBatch = Invoke-RestMethod ` + -Uri "http://localhost:5255/api/degree-awards/batches/$($degreeBatch.id)" ` + -Headers $collegeHeaders + if ($degreeBatch.status -ne 'Published' -or + @($degreeBatch.results).Count -lt 1 -or + @($collegeDegreeBatch.results).Count -lt 1) { + throw 'Published degree-award results are not visible in the authorized college scope.' + } + + $clearanceBatches = Invoke-RestMethod ` + -Uri 'http://localhost:5255/api/graduation-clearance/batches' ` + -Headers $headers + $clearanceBatch = @($clearanceBatches) | + Where-Object { $_.name -eq '2030届端到端毕业离校手续' } | + Select-Object -First 1 + if ($null -eq $clearanceBatch) { + $clearanceBatchBody = @{ + name = '2030届端到端毕业离校手续' + graduationYear = 2030 + notes = '验证校级、学院、辅导员分工办理与批次关闭。' + items = @( + @{ + code = 'FINANCE' + name = '财务费用结清' + responsibleUnit = '财务处' + responsibleRole = 'AcademicAdmin' + isRequired = $true + }, + @{ + code = 'COLLEGE' + name = '学院材料归档' + responsibleUnit = '计算机学院' + responsibleRole = 'CollegeAdmin' + isRequired = $true + }, + @{ + code = 'DORM' + name = '宿舍退宿确认' + responsibleUnit = '学生工作办公室' + responsibleRole = 'Counselor' + isRequired = $true + } + ) + } | ConvertTo-Json -Depth 5 + $createdClearanceBatch = Invoke-RestMethod ` + -Method Post ` + -Uri 'http://localhost:5255/api/graduation-clearance/batches' ` + -Headers $headers ` + -ContentType 'application/json' ` + -Body $clearanceBatchBody + Invoke-RestMethod ` + -Method Post ` + -Uri "http://localhost:5255/api/graduation-clearance/batches/$($createdClearanceBatch.id)/generate" ` + -Headers $headers | + Out-Null + $clearanceBatch = Invoke-RestMethod ` + -Uri "http://localhost:5255/api/graduation-clearance/batches/$($createdClearanceBatch.id)" ` + -Headers $headers + if (@($clearanceBatch.records).Count -ne 3) { + throw 'Graduation-clearance records were not generated for each configured item.' + } + $academicClearanceRecord = @($clearanceBatch.records) | + Where-Object { $_.responsibleRole -eq 'AcademicAdmin' } | + Select-Object -First 1 + $wrongRoleResponse = Invoke-WebRequest ` + -Method Put ` + -Uri "http://localhost:5255/api/graduation-clearance/records/$($academicClearanceRecord.id)" ` + -Headers $collegeHeaders ` + -ContentType 'application/json' ` + -Body (@{ status = 'Completed'; notes = '越权测试。' } | ConvertTo-Json) ` + -SkipHttpErrorCheck + if ($wrongRoleResponse.StatusCode -ne 403) { + throw 'College role was able to complete an academic-level clearance item.' + } + $counselorLoginBody = @{ + userName = 'counselor' + password = 'Counselor@123456' + } | ConvertTo-Json + $counselorLogin = Invoke-RestMethod ` + -Method Post ` + -Uri 'http://localhost:5255/api/auth/login' ` + -ContentType 'application/json' ` + -Body $counselorLoginBody + $counselorHeaders = @{ Authorization = "Bearer $($counselorLogin.token)" } + foreach ($record in @($clearanceBatch.records)) { + $recordHeaders = switch ($record.responsibleRole) { + 'CollegeAdmin' { $collegeHeaders } + 'Counselor' { $counselorHeaders } + default { $headers } + } + $recordBody = @{ + status = 'Completed' + notes = "$($record.responsibleUnit)端到端办理完成。" + } | ConvertTo-Json + Invoke-RestMethod ` + -Method Put ` + -Uri "http://localhost:5255/api/graduation-clearance/records/$($record.id)" ` + -Headers $recordHeaders ` + -ContentType 'application/json' ` + -Body $recordBody | + Out-Null + } + Invoke-RestMethod ` + -Method Post ` + -Uri "http://localhost:5255/api/graduation-clearance/batches/$($clearanceBatch.id)/close" ` + -Headers $headers | + Out-Null + } + $clearanceBatch = Invoke-RestMethod ` + -Uri "http://localhost:5255/api/graduation-clearance/batches/$($clearanceBatch.id)" ` + -Headers $headers + $collegeClearanceBatch = Invoke-RestMethod ` + -Uri "http://localhost:5255/api/graduation-clearance/batches/$($clearanceBatch.id)" ` + -Headers $collegeHeaders + if ($clearanceBatch.status -ne 'Closed' -or + @($clearanceBatch.records | Where-Object { $_.status -eq 'Pending' }).Count -ne 0 -or + @($collegeClearanceBatch.records).Count -lt 1) { + throw 'Graduation-clearance batch was not closed or is missing from college scope.' + } + $frontend = Invoke-WebRequest -Uri 'http://localhost:5255/' -TimeoutSec 5 $spaFallback = Invoke-WebRequest -Uri 'http://localhost:5255/base-data' -TimeoutSec 5 $unknownApiParameters = @{ @@ -361,6 +771,14 @@ try { TranscriptRecords = @($studentTranscript.records).Count ExamSessions = @($examDetail.sessions).Count ExamRoster = @($examRoster.students).Count + StatusChanges = @($completedStatusChanges).Count + StudentStatus = $updatedStatusOptions.status + GraduationResults = @($graduationBatch.results).Count + GraduationConclusion = $studentGraduationResult.conclusion + DegreeResults = @($degreeBatch.results).Count + DegreeConclusion = $degreeBatch.results[0].conclusion + ClearanceRecords = @($clearanceBatch.records).Count + ClearanceStatus = $clearanceBatch.status AccessUpdate = $true ScopeChecks = $scopeChecks -join ', ' StaticIndex = $frontend.Content.Contains('明序教务管理系统') diff --git a/src/Jiaowu.Api/Controllers/DashboardController.cs b/src/Jiaowu.Api/Controllers/DashboardController.cs index 27b44c0..a87e7d4 100644 --- a/src/Jiaowu.Api/Controllers/DashboardController.cs +++ b/src/Jiaowu.Api/Controllers/DashboardController.cs @@ -51,6 +51,31 @@ public sealed class DashboardController(AppDbContext db) : ControllerBase GradeRecords = await db.GradeRecords.CountAsync(cancellationToken), ExamPlans = await db.ExamPlans.CountAsync(cancellationToken), ExamSessions = await db.ExamSessions.CountAsync(cancellationToken), + StudentStatusChanges = await db.StudentStatusChanges + .CountAsync(cancellationToken), + PendingStudentStatusChanges = await db.StudentStatusChanges.CountAsync( + x => x.State == StudentStatusChangeState.Submitted || + x.State == StudentStatusChangeState.CounselorApproved || + x.State == StudentStatusChangeState.CollegeApproved, + cancellationToken), + GraduationAuditBatches = await db.GraduationAuditBatches + .CountAsync(cancellationToken), + PublishedGraduationAuditBatches = await db.GraduationAuditBatches + .CountAsync( + x => x.Status == GraduationAuditBatchStatus.Published, + cancellationToken), + DegreeAwardBatches = await db.DegreeAwardBatches + .CountAsync(cancellationToken), + PublishedDegreeAwardBatches = await db.DegreeAwardBatches + .CountAsync( + x => x.Status == DegreeAwardBatchStatus.Published, + cancellationToken), + GraduationClearanceBatches = await db.GraduationClearanceBatches + .CountAsync(cancellationToken), + OpenGraduationClearanceBatches = await db.GraduationClearanceBatches + .CountAsync( + x => x.Status == GraduationClearanceBatchStatus.Open, + cancellationToken), Users = await db.Users.CountAsync(cancellationToken) } }; diff --git a/src/Jiaowu.Api/Controllers/DegreeAwardsController.cs b/src/Jiaowu.Api/Controllers/DegreeAwardsController.cs new file mode 100644 index 0000000..2817883 --- /dev/null +++ b/src/Jiaowu.Api/Controllers/DegreeAwardsController.cs @@ -0,0 +1,278 @@ +using System.ComponentModel.DataAnnotations; +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Infrastructure.Auth; +using Jiaowu.Api.Infrastructure.Graduation; +using Jiaowu.Api.Infrastructure.Persistence; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Jiaowu.Api.Controllers; + +[ApiController] +[Authorize] +[Route("api/degree-awards")] +public sealed class DegreeAwardsController( + AppDbContext db, + ICurrentUserDataScope currentUserDataScope) : ControllerBase +{ + private const string Managers = + SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin; + private const string Reviewers = Managers + "," + SystemRoles.CollegeAdmin; + + [HttpGet("batches")] + [Authorize(Roles = Reviewers)] + public async Task GetBatches(CancellationToken token) + { + var collegeId = RestrictedCollegeId(); + return Ok(await db.DegreeAwardBatches.AsNoTracking() + .OrderByDescending(x => x.GraduationYear) + .ThenByDescending(x => x.CreatedAt) + .Select(x => new + { + x.Id, x.Name, x.GraduationYear, x.DegreeName, + x.MinimumGradePoint, x.Status, x.Notes, + x.CalculatedAt, x.PublishedAt, + ResultCount = x.Results.Count(result => + !collegeId.HasValue || + result.Student!.AdministrativeClass!.Major!.CollegeId == collegeId), + GrantedCount = x.Results.Count(result => + (!collegeId.HasValue || + result.Student!.AdministrativeClass!.Major!.CollegeId == collegeId) && + result.Conclusion == DegreeAwardConclusion.Granted), + NotGrantedCount = x.Results.Count(result => + (!collegeId.HasValue || + result.Student!.AdministrativeClass!.Major!.CollegeId == collegeId) && + result.Conclusion == DegreeAwardConclusion.NotGranted), + OverrideCount = x.Results.Count(result => + (!collegeId.HasValue || + result.Student!.AdministrativeClass!.Major!.CollegeId == collegeId) && + result.IsOverridden) + }).ToListAsync(token)); + } + + [HttpGet("batches/{id:guid}")] + [Authorize(Roles = Reviewers)] + public async Task GetBatch(Guid id, CancellationToken token) + { + var batch = await db.DegreeAwardBatches.AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == id, token); + if (batch is null) return NotFound(); + var collegeId = RestrictedCollegeId(); + var source = db.DegreeAwardResults.AsNoTracking() + .Where(x => x.DegreeAwardBatchId == id); + if (collegeId.HasValue) + source = source.Where(x => + x.Student!.AdministrativeClass!.Major!.CollegeId == collegeId); + return Ok(new + { + batch.Id, batch.Name, batch.GraduationYear, batch.DegreeName, + batch.MinimumGradePoint, batch.Status, batch.Notes, + batch.CalculatedAt, batch.PublishedAt, + Results = await source.OrderBy(x => x.Student!.StudentNumber) + .Select(x => new + { + x.Id, x.StudentId, x.Student!.StudentNumber, x.Student.Name, + ClassName = x.Student.AdministrativeClass!.Name, + MajorName = x.Student.AdministrativeClass.Major!.Name, + CollegeName = x.Student.AdministrativeClass.Major.College!.Name, + x.AverageGradePoint, x.CalculatedConclusion, x.Conclusion, + x.ExceptionReason, x.IsOverridden, + x.ReviewComment, x.ReviewedAt + }).ToListAsync(token) + }); + } + + [HttpPost("batches")] + [Authorize(Roles = Managers)] + public async Task Create( + DegreeAwardBatchRequest request, + CancellationToken token) + { + var batch = new DegreeAwardBatch + { + Name = request.Name.Trim(), + GraduationYear = request.GraduationYear, + DegreeName = request.DegreeName.Trim(), + MinimumGradePoint = request.MinimumGradePoint, + Notes = request.Notes?.Trim() + }; + db.DegreeAwardBatches.Add(batch); + await db.SaveChangesAsync(token); + return Created(string.Empty, new { batch.Id }); + } + + [HttpPost("batches/{id:guid}/calculate")] + [Authorize(Roles = Managers)] + public async Task Calculate(Guid id, CancellationToken token) + { + var batch = await db.DegreeAwardBatches + .FirstOrDefaultAsync(x => x.Id == id, token); + if (batch is null) return NotFound(); + if (batch.Status != DegreeAwardBatchStatus.Draft) + return ConflictProblem("已发布批次不能重新计算。"); + + var auditCandidates = await db.GraduationAuditResults.AsNoTracking() + .Include(x => x.GraduationAuditBatch) + .Include(x => x.Student) + .ThenInclude(x => x!.AdministrativeClass) + .ThenInclude(x => x!.Major) + .Where(x => + x.GraduationAuditBatch!.GraduationYear == batch.GraduationYear && + x.GraduationAuditBatch.Status == GraduationAuditBatchStatus.Published && + x.Conclusion == GraduationAuditConclusion.Eligible) + .OrderByDescending(x => x.GraduationAuditBatch!.PublishedAt) + .ToListAsync(token); + var audits = auditCandidates + .GroupBy(x => x.StudentId) + .Select(x => x.First()) + .ToList(); + var studentIds = audits.Select(x => x.StudentId).ToArray(); + var gradePoints = await db.GradeRecords.AsNoTracking() + .Where(x => studentIds.Contains(x.StudentId) && + x.GradeSheet!.Status == GradeSheetStatus.Published && + x.GradePoint.HasValue) + .Select(x => new + { + x.StudentId, + GradePoint = x.GradePoint!.Value, + x.GradeSheet!.TeachingTask!.Course!.Credits + }).ToListAsync(token); + + var oldResults = await db.DegreeAwardResults + .Where(x => x.DegreeAwardBatchId == id).ToListAsync(token); + db.DegreeAwardResults.RemoveRange(oldResults); + await db.SaveChangesAsync(token); + + foreach (var audit in audits) + { + var grades = gradePoints.Where(x => x.StudentId == audit.StudentId).ToList(); + var credits = grades.Sum(x => x.Credits); + var average = credits == 0 + ? 0 + : Math.Round( + grades.Sum(x => x.GradePoint * x.Credits) / credits, + 2, + MidpointRounding.AwayFromZero); + var conclusion = DegreeAwardRules.Evaluate( + true, audit.Student!.Status, average, batch.MinimumGradePoint); + var reason = audit.Student.Status != StudentStatus.Graduated + ? "学籍状态尚未转为毕业" + : average < batch.MinimumGradePoint + ? $"平均绩点 {average:0.00},低于批次要求 {batch.MinimumGradePoint:0.00}" + : string.Empty; + db.DegreeAwardResults.Add(new DegreeAwardResult + { + DegreeAwardBatchId = batch.Id, + StudentId = audit.StudentId, + GraduationAuditResultId = audit.Id, + AverageGradePoint = average, + CalculatedConclusion = conclusion, + Conclusion = conclusion, + ExceptionReason = reason + }); + } + batch.CalculatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(token); + return Ok(new { ResultCount = audits.Count }); + } + + [HttpPut("results/{id:guid}")] + [Authorize(Roles = Reviewers)] + public async Task Review( + Guid id, + DegreeAwardDecisionRequest request, + CancellationToken token) + { + var result = await db.DegreeAwardResults + .Include(x => x.DegreeAwardBatch) + .Include(x => x.Student) + .ThenInclude(x => x!.AdministrativeClass) + .ThenInclude(x => x!.Major) + .FirstOrDefaultAsync(x => x.Id == id, token); + if (result is null) return NotFound(); + if (result.DegreeAwardBatch!.Status != DegreeAwardBatchStatus.Draft) + return ConflictProblem("已发布的授予结果不能修改。"); + var collegeId = RestrictedCollegeId(); + if (collegeId.HasValue && + result.Student!.AdministrativeClass!.Major!.CollegeId != collegeId) + return Forbid(); + + result.Conclusion = request.Conclusion; + result.IsOverridden = request.Conclusion != result.CalculatedConclusion; + result.ReviewComment = request.Comment.Trim(); + result.ReviewedAt = DateTime.UtcNow; + await db.SaveChangesAsync(token); + return NoContent(); + } + + [HttpPost("batches/{id:guid}/publish")] + [Authorize(Roles = Managers)] + public async Task Publish(Guid id, CancellationToken token) + { + var batch = await db.DegreeAwardBatches.Include(x => x.Results) + .FirstOrDefaultAsync(x => x.Id == id, token); + if (batch is null) return NotFound(); + if (batch.Status != DegreeAwardBatchStatus.Draft) + return ConflictProblem("该批次已经发布。"); + if (batch.Results.Count == 0) + return ConflictProblem("没有可发布的学位授予审核结果。"); + batch.Status = DegreeAwardBatchStatus.Published; + batch.PublishedAt = DateTime.UtcNow; + await db.SaveChangesAsync(token); + return NoContent(); + } + + [HttpGet("my-result")] + [Authorize(Roles = SystemRoles.Student)] + public async Task GetMyResult(CancellationToken token) + { + var userId = currentUserDataScope.Current.UserId; + var result = await db.DegreeAwardResults.AsNoTracking() + .Where(x => x.Student!.UserId == userId && + x.DegreeAwardBatch!.Status == DegreeAwardBatchStatus.Published) + .OrderByDescending(x => x.DegreeAwardBatch!.GraduationYear) + .ThenByDescending(x => x.DegreeAwardBatch!.PublishedAt) + .Select(x => new + { + x.Id, + BatchName = x.DegreeAwardBatch!.Name, + x.DegreeAwardBatch.GraduationYear, + x.DegreeAwardBatch.DegreeName, + x.DegreeAwardBatch.MinimumGradePoint, + x.Student!.StudentNumber, x.Student.Name, + MajorName = x.Student.AdministrativeClass!.Major!.Name, + x.AverageGradePoint, x.Conclusion, x.ExceptionReason, + x.IsOverridden, x.ReviewComment, + x.DegreeAwardBatch.PublishedAt + }).FirstOrDefaultAsync(token); + return Ok(result); + } + + private Guid? RestrictedCollegeId() + { + var scope = currentUserDataScope.Current; + return scope.IsInRole(SystemRoles.CollegeAdmin) + ? scope.CollegeId ?? Guid.Empty + : null; + } + + private ActionResult ConflictProblem(string detail) => Conflict(new ProblemDetails + { + Title = "无法完成学位授予操作", + Detail = detail, + Status = StatusCodes.Status409Conflict + }); +} + +public sealed record DegreeAwardBatchRequest( + [Required, MinLength(3), MaxLength(120)] string Name, + [Range(2000, 2200)] int GraduationYear, + [Required, MinLength(2), MaxLength(80)] string DegreeName, + [Range(typeof(decimal), "0", "5")] decimal MinimumGradePoint, + [MaxLength(500)] string? Notes); + +public sealed record DegreeAwardDecisionRequest( + DegreeAwardConclusion Conclusion, + [Required, MinLength(5), MaxLength(500)] string Comment); diff --git a/src/Jiaowu.Api/Controllers/GraduationAuditsController.cs b/src/Jiaowu.Api/Controllers/GraduationAuditsController.cs new file mode 100644 index 0000000..b6a9e9b --- /dev/null +++ b/src/Jiaowu.Api/Controllers/GraduationAuditsController.cs @@ -0,0 +1,332 @@ +using System.ComponentModel.DataAnnotations; +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Infrastructure.Auth; +using Jiaowu.Api.Infrastructure.Graduation; +using Jiaowu.Api.Infrastructure.Persistence; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Jiaowu.Api.Controllers; + +[ApiController] +[Authorize] +[Route("api/graduation-audits")] +public sealed class GraduationAuditsController( + AppDbContext db, + ICurrentUserDataScope currentUserDataScope) : ControllerBase +{ + private const string Managers = + SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin; + private const string Reviewers = + Managers + "," + SystemRoles.CollegeAdmin; + + [HttpGet("batches")] + [Authorize(Roles = Reviewers)] + public async Task GetBatches(CancellationToken token) + { + var collegeId = RestrictedCollegeId(); + return Ok(await db.GraduationAuditBatches.AsNoTracking() + .OrderByDescending(x => x.GraduationYear) + .ThenByDescending(x => x.CreatedAt) + .Select(x => new + { + x.Id, x.Name, x.GraduationYear, x.EnrollmentYear, x.Status, + x.Notes, x.CalculatedAt, x.PublishedAt, x.CreatedAt, + ResultCount = x.Results.Count(result => + !collegeId.HasValue || + result.Student!.AdministrativeClass!.Major!.CollegeId == collegeId), + EligibleCount = x.Results.Count(result => + (!collegeId.HasValue || + result.Student!.AdministrativeClass!.Major!.CollegeId == collegeId) && + result.Conclusion == GraduationAuditConclusion.Eligible), + IneligibleCount = x.Results.Count(result => + (!collegeId.HasValue || + result.Student!.AdministrativeClass!.Major!.CollegeId == collegeId) && + result.Conclusion == GraduationAuditConclusion.Ineligible), + OverrideCount = x.Results.Count(result => + (!collegeId.HasValue || + result.Student!.AdministrativeClass!.Major!.CollegeId == collegeId) && + result.IsOverridden) + }).ToListAsync(token)); + } + + [HttpGet("batches/{id:guid}")] + [Authorize(Roles = Reviewers)] + public async Task GetBatch(Guid id, CancellationToken token) + { + var batch = await db.GraduationAuditBatches.AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == id, token); + if (batch is null) return NotFound(); + + var collegeId = RestrictedCollegeId(); + var results = db.GraduationAuditResults.AsNoTracking() + .Where(x => x.GraduationAuditBatchId == id); + if (collegeId.HasValue) + results = results.Where(x => + x.Student!.AdministrativeClass!.Major!.CollegeId == collegeId); + return Ok(new + { + batch.Id, batch.Name, batch.GraduationYear, batch.EnrollmentYear, + batch.Status, batch.Notes, batch.CalculatedAt, batch.PublishedAt, + Results = await results + .OrderBy(x => x.Student!.StudentNumber) + .Select(x => new + { + x.Id, x.StudentId, x.Student!.StudentNumber, x.Student.Name, + ClassName = x.Student.AdministrativeClass!.Name, + MajorName = x.Student.AdministrativeClass.Major!.Name, + CollegeName = x.Student.AdministrativeClass.Major.College!.Name, + x.StudentStatusSnapshot, + PlanName = x.CurriculumPlan != null ? x.CurriculumPlan.Name : null, + x.RequiredCredits, x.EarnedCredits, + x.RequiredCourseCount, x.PassedRequiredCourseCount, + x.FailedCourseCount, x.MissingCourseNames, + x.CalculatedConclusion, x.Conclusion, x.IsOverridden, + x.ReviewComment, x.ReviewedAt + }).ToListAsync(token) + }); + } + + [HttpPost("batches")] + [Authorize(Roles = Managers)] + public async Task CreateBatch( + GraduationAuditBatchRequest request, + CancellationToken token) + { + if (request.EnrollmentYear > request.GraduationYear) + return ValidationProblem("入学年级不能晚于毕业年份。"); + var batch = new GraduationAuditBatch + { + Name = request.Name.Trim(), + GraduationYear = request.GraduationYear, + EnrollmentYear = request.EnrollmentYear, + Notes = request.Notes?.Trim() + }; + db.GraduationAuditBatches.Add(batch); + await db.SaveChangesAsync(token); + return Created(string.Empty, new { batch.Id }); + } + + [HttpPost("batches/{id:guid}/calculate")] + [Authorize(Roles = Managers)] + public async Task Calculate(Guid id, CancellationToken token) + { + var batch = await db.GraduationAuditBatches + .FirstOrDefaultAsync(x => x.Id == id, token); + if (batch is null) return NotFound(); + if (batch.Status != GraduationAuditBatchStatus.Draft) + return ConflictProblem("已发布批次不能重新计算。"); + + var students = await db.Students.AsNoTracking() + .Include(x => x.AdministrativeClass) + .ThenInclude(x => x!.Major) + .Where(x => x.EnrollmentYear == batch.EnrollmentYear && + (x.Status == StudentStatus.Active || + x.Status == StudentStatus.Suspended)) + .OrderBy(x => x.StudentNumber) + .ToListAsync(token); + var plans = await db.CurriculumPlans.AsNoTracking() + .Include(x => x.Modules) + .ThenInclude(x => x.Courses) + .ThenInclude(x => x.Course) + .Where(x => x.EffectiveGrade == batch.EnrollmentYear && + x.Status == CurriculumPlanStatus.Published) + .ToListAsync(token); + var studentIds = students.Select(x => x.Id).ToArray(); + var grades = await db.GradeRecords.AsNoTracking() + .Where(x => studentIds.Contains(x.StudentId) && + x.GradeSheet!.Status == GradeSheetStatus.Published) + .Select(x => new GradeSnapshot( + x.StudentId, + x.GradeSheet!.TeachingTask!.CourseId, + x.GradeSheet.TeachingTask.Course!.Name, + x.GradeSheet.TeachingTask.Course.Credits, + x.TotalScore, + x.ExamStatus)) + .ToListAsync(token); + + var oldResults = await db.GraduationAuditResults + .Where(x => x.GraduationAuditBatchId == id).ToListAsync(token); + db.GraduationAuditResults.RemoveRange(oldResults); + await db.SaveChangesAsync(token); + + foreach (var student in students) + { + var plan = plans.FirstOrDefault(x => + x.MajorId == student.AdministrativeClass!.MajorId); + var requiredCourses = plan?.Modules + .SelectMany(x => x.Courses) + .Where(x => x.Type == CurriculumCourseType.Required) + .ToList() ?? []; + var studentGrades = grades.Where(x => x.StudentId == student.Id).ToList(); + var passedCourseIds = studentGrades + .Where(IsPassed) + .Select(x => x.CourseId) + .Distinct() + .ToHashSet(); + var earnedCredits = studentGrades + .Where(IsPassed) + .GroupBy(x => x.CourseId) + .Sum(x => x.Max(item => item.Credits)); + var missingCourses = requiredCourses + .Where(x => !passedCourseIds.Contains(x.CourseId)) + .Select(x => x.Course!.Name) + .Distinct() + .ToArray(); + var missingCourseNames = plan is null + ? "未匹配已发布的培养方案" + : string.Join("、", missingCourses); + if (missingCourseNames.Length > 2000) + missingCourseNames = missingCourseNames[..2000]; + var failedCourseCount = studentGrades + .GroupBy(x => x.CourseId) + .Count(x => !x.Any(IsPassed)); + var conclusion = GraduationAuditRules.Evaluate( + plan is not null, + student.Status, + plan?.TotalCredits ?? 0, + earnedCredits, + requiredCourses.Count, + requiredCourses.Count - missingCourses.Length, + failedCourseCount); + db.GraduationAuditResults.Add(new GraduationAuditResult + { + GraduationAuditBatchId = batch.Id, + StudentId = student.Id, + CurriculumPlanId = plan?.Id, + StudentStatusSnapshot = student.Status, + RequiredCredits = plan?.TotalCredits ?? 0, + EarnedCredits = earnedCredits, + RequiredCourseCount = requiredCourses.Count, + PassedRequiredCourseCount = requiredCourses.Count - missingCourses.Length, + FailedCourseCount = failedCourseCount, + MissingCourseNames = missingCourseNames, + CalculatedConclusion = conclusion, + Conclusion = conclusion + }); + } + batch.CalculatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(token); + return Ok(new { ResultCount = students.Count }); + } + + [HttpPut("results/{id:guid}")] + [Authorize(Roles = Reviewers)] + public async Task ReviewResult( + Guid id, + GraduationAuditDecisionRequest request, + CancellationToken token) + { + var result = await db.GraduationAuditResults + .Include(x => x.GraduationAuditBatch) + .Include(x => x.Student) + .ThenInclude(x => x!.AdministrativeClass) + .ThenInclude(x => x!.Major) + .FirstOrDefaultAsync(x => x.Id == id, token); + if (result is null) return NotFound(); + if (result.GraduationAuditBatch!.Status != GraduationAuditBatchStatus.Draft) + return ConflictProblem("已发布的审核结果不能修改。"); + var collegeId = RestrictedCollegeId(); + if (collegeId.HasValue && + result.Student!.AdministrativeClass!.Major!.CollegeId != collegeId) + return Forbid(); + + result.Conclusion = request.Conclusion; + result.IsOverridden = request.Conclusion != result.CalculatedConclusion; + result.ReviewComment = request.Comment.Trim(); + result.ReviewedAt = DateTime.UtcNow; + await db.SaveChangesAsync(token); + return NoContent(); + } + + [HttpPost("batches/{id:guid}/publish")] + [Authorize(Roles = Managers)] + public async Task Publish(Guid id, CancellationToken token) + { + var batch = await db.GraduationAuditBatches + .Include(x => x.Results) + .ThenInclude(x => x.Student) + .FirstOrDefaultAsync(x => x.Id == id, token); + if (batch is null) return NotFound(); + if (batch.Status != GraduationAuditBatchStatus.Draft) + return ConflictProblem("该批次已经发布。"); + if (batch.Results.Count == 0) + return ConflictProblem("请先计算毕业资格,再发布结果。"); + + batch.Status = GraduationAuditBatchStatus.Published; + batch.PublishedAt = DateTime.UtcNow; + foreach (var result in batch.Results.Where(x => + x.Conclusion == GraduationAuditConclusion.Eligible && + x.Student!.Status == StudentStatus.Active)) + result.Student!.Status = StudentStatus.Graduated; + await db.SaveChangesAsync(token); + return NoContent(); + } + + [HttpGet("my-result")] + [Authorize(Roles = SystemRoles.Student)] + public async Task GetMyResult(CancellationToken token) + { + var userId = currentUserDataScope.Current.UserId; + var result = await db.GraduationAuditResults.AsNoTracking() + .Where(x => x.Student!.UserId == userId && + x.GraduationAuditBatch!.Status == + GraduationAuditBatchStatus.Published) + .OrderByDescending(x => x.GraduationAuditBatch!.GraduationYear) + .ThenByDescending(x => x.GraduationAuditBatch!.PublishedAt) + .Select(x => new + { + x.Id, + BatchName = x.GraduationAuditBatch!.Name, + x.GraduationAuditBatch.GraduationYear, + x.Student!.StudentNumber, x.Student.Name, + MajorName = x.Student.AdministrativeClass!.Major!.Name, + PlanName = x.CurriculumPlan != null ? x.CurriculumPlan.Name : null, + x.RequiredCredits, x.EarnedCredits, + x.RequiredCourseCount, x.PassedRequiredCourseCount, + x.FailedCourseCount, x.MissingCourseNames, + x.Conclusion, x.IsOverridden, x.ReviewComment, + x.GraduationAuditBatch.PublishedAt + }).FirstOrDefaultAsync(token); + return Ok(result); + } + + private Guid? RestrictedCollegeId() + { + var scope = currentUserDataScope.Current; + return scope.IsInRole(SystemRoles.CollegeAdmin) + ? scope.CollegeId ?? Guid.Empty + : null; + } + + private static bool IsPassed(GradeSnapshot grade) => + grade.ExamStatus == GradeExamStatus.Exempt || + grade.TotalScore is decimal score && score >= 60; + + private sealed record GradeSnapshot( + Guid StudentId, + Guid CourseId, + string CourseName, + decimal Credits, + decimal? TotalScore, + GradeExamStatus ExamStatus); + + private ActionResult ConflictProblem(string detail) => Conflict(new ProblemDetails + { + Title = "无法完成毕业审核操作", + Detail = detail, + Status = StatusCodes.Status409Conflict + }); +} + +public sealed record GraduationAuditBatchRequest( + [Required, MinLength(3), MaxLength(120)] string Name, + [Range(2000, 2200)] int GraduationYear, + [Range(2000, 2200)] int EnrollmentYear, + [MaxLength(500)] string? Notes); + +public sealed record GraduationAuditDecisionRequest( + GraduationAuditConclusion Conclusion, + [Required, MinLength(5), MaxLength(500)] string Comment); diff --git a/src/Jiaowu.Api/Controllers/GraduationClearanceController.cs b/src/Jiaowu.Api/Controllers/GraduationClearanceController.cs new file mode 100644 index 0000000..258df36 --- /dev/null +++ b/src/Jiaowu.Api/Controllers/GraduationClearanceController.cs @@ -0,0 +1,318 @@ +using System.ComponentModel.DataAnnotations; +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Infrastructure.Auth; +using Jiaowu.Api.Infrastructure.Graduation; +using Jiaowu.Api.Infrastructure.Persistence; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Jiaowu.Api.Controllers; + +[ApiController] +[Authorize] +[Route("api/graduation-clearance")] +public sealed class GraduationClearanceController( + AppDbContext db, + ICurrentUserDataScope currentUserDataScope) : ControllerBase +{ + private const string Managers = + SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin; + private const string Workers = + Managers + "," + SystemRoles.CollegeAdmin + "," + SystemRoles.Counselor; + + [HttpGet("batches")] + [Authorize(Roles = Workers)] + public async Task GetBatches(CancellationToken token) + { + var batches = await db.GraduationClearanceBatches.AsNoTracking() + .OrderByDescending(x => x.GraduationYear) + .ThenByDescending(x => x.CreatedAt) + .Select(x => new + { + x.Id, x.Name, x.GraduationYear, x.Status, x.Notes, + x.ClosedAt, x.CreatedAt, ItemCount = x.Items.Count + }).ToListAsync(token); + var records = await ScopedRecords().AsNoTracking() + .Select(x => new + { + BatchId = x.GraduationClearanceItem!.GraduationClearanceBatchId, + x.StudentId, x.Status + }).ToListAsync(token); + return Ok(batches.Select(batch => + { + var scoped = records.Where(x => x.BatchId == batch.Id).ToList(); + return new + { + batch.Id, batch.Name, batch.GraduationYear, batch.Status, + batch.Notes, batch.ClosedAt, batch.CreatedAt, batch.ItemCount, + StudentCount = scoped.Select(x => x.StudentId).Distinct().Count(), + RecordCount = scoped.Count, + CompletedCount = scoped.Count(x => + x.Status != GraduationClearanceRecordStatus.Pending) + }; + })); + } + + [HttpGet("batches/{id:guid}")] + [Authorize(Roles = Workers)] + public async Task GetBatch(Guid id, CancellationToken token) + { + var batch = await db.GraduationClearanceBatches.AsNoTracking() + .Where(x => x.Id == id) + .Select(x => new + { + x.Id, x.Name, x.GraduationYear, x.Status, + x.Notes, x.ClosedAt, x.CreatedAt, + Items = x.Items.OrderBy(item => item.SortOrder).Select(item => new + { + item.Id, item.Code, item.Name, item.ResponsibleUnit, + item.ResponsibleRole, item.IsRequired, item.SortOrder + }) + }).FirstOrDefaultAsync(token); + if (batch is null) return NotFound(); + var records = await ScopedRecords().AsNoTracking() + .Where(x => x.GraduationClearanceItem!.GraduationClearanceBatchId == id) + .OrderBy(x => x.Student!.StudentNumber) + .ThenBy(x => x.GraduationClearanceItem!.SortOrder) + .Select(x => new + { + x.Id, x.StudentId, x.Student!.StudentNumber, x.Student.Name, + ClassName = x.Student.AdministrativeClass!.Name, + MajorName = x.Student.AdministrativeClass.Major!.Name, + CollegeName = x.Student.AdministrativeClass.Major.College!.Name, + ItemId = x.GraduationClearanceItemId, + ItemName = x.GraduationClearanceItem!.Name, + x.GraduationClearanceItem.ResponsibleUnit, + x.GraduationClearanceItem.ResponsibleRole, + x.GraduationClearanceItem.IsRequired, + x.Status, x.Notes, x.CompletedAt + }).ToListAsync(token); + return Ok(new + { + batch.Id, batch.Name, batch.GraduationYear, batch.Status, + batch.Notes, batch.ClosedAt, batch.CreatedAt, batch.Items, + Records = records + }); + } + + [HttpPost("batches")] + [Authorize(Roles = Managers)] + public async Task Create( + GraduationClearanceBatchRequest request, + CancellationToken token) + { + var allowedRoles = new[] + { + SystemRoles.AcademicAdmin, + SystemRoles.CollegeAdmin, + SystemRoles.Counselor + }; + if (request.Items.Count == 0) + return ValidationProblem("至少配置一个离校事项。"); + if (request.Items.Select(x => x.Code.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase).Count() != request.Items.Count) + return ValidationProblem("离校事项编码不能重复。"); + if (request.Items.Any(x => !allowedRoles.Contains( + x.ResponsibleRole, StringComparer.OrdinalIgnoreCase))) + return ValidationProblem("离校事项责任角色无效。"); + + var batch = new GraduationClearanceBatch + { + Name = request.Name.Trim(), + GraduationYear = request.GraduationYear, + Notes = request.Notes?.Trim(), + Items = request.Items.Select((item, index) => + new GraduationClearanceItem + { + Code = item.Code.Trim(), + Name = item.Name.Trim(), + ResponsibleUnit = item.ResponsibleUnit.Trim(), + ResponsibleRole = item.ResponsibleRole, + IsRequired = item.IsRequired, + SortOrder = index + 1 + }).ToList() + }; + db.GraduationClearanceBatches.Add(batch); + await db.SaveChangesAsync(token); + return Created(string.Empty, new { batch.Id }); + } + + [HttpPost("batches/{id:guid}/generate")] + [Authorize(Roles = Managers)] + public async Task Generate(Guid id, CancellationToken token) + { + var batch = await db.GraduationClearanceBatches + .Include(x => x.Items) + .FirstOrDefaultAsync(x => x.Id == id, token); + if (batch is null) return NotFound(); + if (batch.Status != GraduationClearanceBatchStatus.Open) + return ConflictProblem("已关闭批次不能重新生成办理记录。"); + + var eligibleAudits = await db.GraduationAuditResults.AsNoTracking() + .Where(x => + x.GraduationAuditBatch!.GraduationYear == batch.GraduationYear && + x.GraduationAuditBatch.Status == GraduationAuditBatchStatus.Published && + x.Conclusion == GraduationAuditConclusion.Eligible && + x.Student!.Status == StudentStatus.Graduated) + .OrderByDescending(x => x.GraduationAuditBatch!.PublishedAt) + .Select(x => new { x.StudentId }) + .ToListAsync(token); + var studentIds = eligibleAudits.Select(x => x.StudentId).Distinct().ToArray(); + var existing = await db.GraduationClearanceRecords.AsNoTracking() + .Where(x => x.GraduationClearanceItem!.GraduationClearanceBatchId == id) + .Select(x => new { x.GraduationClearanceItemId, x.StudentId }) + .ToListAsync(token); + var existingKeys = existing + .Select(x => (x.GraduationClearanceItemId, x.StudentId)) + .ToHashSet(); + var created = 0; + foreach (var item in batch.Items) + foreach (var studentId in studentIds) + { + if (existingKeys.Contains((item.Id, studentId))) continue; + db.GraduationClearanceRecords.Add(new GraduationClearanceRecord + { + GraduationClearanceItemId = item.Id, + StudentId = studentId + }); + created++; + } + await db.SaveChangesAsync(token); + return Ok(new { StudentCount = studentIds.Length, CreatedRecordCount = created }); + } + + [HttpPut("records/{id:guid}")] + [Authorize(Roles = Workers)] + public async Task UpdateRecord( + Guid id, + GraduationClearanceRecordRequest request, + CancellationToken token) + { + var record = await db.GraduationClearanceRecords + .Include(x => x.GraduationClearanceItem) + .ThenInclude(x => x!.GraduationClearanceBatch) + .Include(x => x.Student) + .ThenInclude(x => x!.AdministrativeClass) + .ThenInclude(x => x!.Major) + .FirstOrDefaultAsync(x => x.Id == id, token); + if (record is null) return NotFound(); + if (record.GraduationClearanceItem!.GraduationClearanceBatch!.Status != + GraduationClearanceBatchStatus.Open) + return ConflictProblem("批次已关闭,不能修改办理记录。"); + if (!CanManage(record)) return Forbid(); + + record.Status = request.Status; + record.Notes = request.Notes?.Trim(); + record.CompletedAt = request.Status == GraduationClearanceRecordStatus.Pending + ? null + : DateTime.UtcNow; + record.CompletedByUserId = + request.Status == GraduationClearanceRecordStatus.Pending + ? null + : currentUserDataScope.Current.UserId; + await db.SaveChangesAsync(token); + return NoContent(); + } + + [HttpPost("batches/{id:guid}/close")] + [Authorize(Roles = Managers)] + public async Task Close(Guid id, CancellationToken token) + { + var batch = await db.GraduationClearanceBatches + .Include(x => x.Items) + .ThenInclude(x => x.Records) + .FirstOrDefaultAsync(x => x.Id == id, token); + if (batch is null) return NotFound(); + if (batch.Status != GraduationClearanceBatchStatus.Open) + return ConflictProblem("该批次已经关闭。"); + var records = batch.Items.SelectMany(item => item.Records.Select(record => + (item.IsRequired, record.Status))); + if (!GraduationClearanceRules.CanClose(records)) + return ConflictProblem("仍有必办离校事项尚未完成。"); + batch.Status = GraduationClearanceBatchStatus.Closed; + batch.ClosedAt = DateTime.UtcNow; + await db.SaveChangesAsync(token); + return NoContent(); + } + + [HttpGet("my-clearance")] + [Authorize(Roles = SystemRoles.Student)] + public async Task GetMyClearance(CancellationToken token) + { + var userId = currentUserDataScope.Current.UserId; + var batch = await db.GraduationClearanceBatches.AsNoTracking() + .Where(x => x.Items.Any(item => + item.Records.Any(record => record.Student!.UserId == userId))) + .OrderByDescending(x => x.GraduationYear) + .ThenByDescending(x => x.CreatedAt) + .Select(x => new + { + x.Id, x.Name, x.GraduationYear, x.Status, x.Notes, x.ClosedAt, + Items = x.Items.OrderBy(item => item.SortOrder) + .SelectMany(item => item.Records + .Where(record => record.Student!.UserId == userId) + .Select(record => new + { + record.Id, ItemName = item.Name, item.ResponsibleUnit, + item.IsRequired, record.Status, + record.Notes, record.CompletedAt + })) + }).FirstOrDefaultAsync(token); + return Ok(batch); + } + + private IQueryable ScopedRecords() + { + var scope = currentUserDataScope.Current; + var source = db.GraduationClearanceRecords.AsQueryable(); + if (scope.IsInRole(SystemRoles.Counselor)) + return source.Where(x => + x.Student!.AdministrativeClass!.CounselorUserId == scope.UserId); + if (scope.IsInRole(SystemRoles.CollegeAdmin)) + return source.Where(x => + x.Student!.AdministrativeClass!.Major!.CollegeId == scope.CollegeId); + return source; + } + + private bool CanManage(GraduationClearanceRecord record) + { + var scope = currentUserDataScope.Current; + if (scope.IsInRole(SystemRoles.SuperAdmin)) return true; + var role = record.GraduationClearanceItem!.ResponsibleRole; + if (role == SystemRoles.AcademicAdmin && + scope.IsInRole(SystemRoles.AcademicAdmin)) return true; + if (role == SystemRoles.CollegeAdmin && + scope.IsInRole(SystemRoles.CollegeAdmin)) + return record.Student!.AdministrativeClass!.Major!.CollegeId == + scope.CollegeId; + return role == SystemRoles.Counselor && + scope.IsInRole(SystemRoles.Counselor) && + record.Student!.AdministrativeClass!.CounselorUserId == scope.UserId; + } + + private ActionResult ConflictProblem(string detail) => Conflict(new ProblemDetails + { + Title = "无法完成毕业离校操作", + Detail = detail, + Status = StatusCodes.Status409Conflict + }); +} + +public sealed record GraduationClearanceBatchRequest( + [Required, MinLength(3), MaxLength(120)] string Name, + [Range(2000, 2200)] int GraduationYear, + [MaxLength(500)] string? Notes, + IReadOnlyList Items); + +public sealed record GraduationClearanceItemRequest( + [Required, MinLength(2), MaxLength(30)] string Code, + [Required, MinLength(2), MaxLength(100)] string Name, + [Required, MinLength(2), MaxLength(100)] string ResponsibleUnit, + [Required, MaxLength(30)] string ResponsibleRole, + bool IsRequired); + +public sealed record GraduationClearanceRecordRequest( + GraduationClearanceRecordStatus Status, + [MaxLength(500)] string? Notes); diff --git a/src/Jiaowu.Api/Controllers/StudentStatusChangesController.cs b/src/Jiaowu.Api/Controllers/StudentStatusChangesController.cs index 23cbe3c..90aabfb 100644 --- a/src/Jiaowu.Api/Controllers/StudentStatusChangesController.cs +++ b/src/Jiaowu.Api/Controllers/StudentStatusChangesController.cs @@ -30,6 +30,37 @@ public sealed class StudentStatusChangesController( }).ToListAsync(token)); } + [HttpGet("options")] + [Authorize(Roles = SystemRoles.Student)] + public async Task GetOptions(CancellationToken token) + { + var userId = currentUserDataScope.Current.UserId; + var student = await db.Students.AsNoTracking() + .FirstOrDefaultAsync(x => x.UserId == userId, token); + if (student is null) return ConflictProblem("当前账号未关联学生档案。"); + + var types = student.Status switch + { + StudentStatus.Active => new[] + { + StudentStatusChangeType.Suspension, + StudentStatusChangeType.Withdrawal + }, + StudentStatus.Suspended => new[] + { + StudentStatusChangeType.Resumption, + StudentStatusChangeType.Withdrawal + }, + _ => [] + }; + var hasPending = await db.StudentStatusChanges.AnyAsync(x => + x.StudentId == student.Id && + (x.State == StudentStatusChangeState.Submitted || + x.State == StudentStatusChangeState.CounselorApproved || + x.State == StudentStatusChangeState.CollegeApproved), token); + return Ok(new { student.Status, Types = types, HasPending = hasPending }); + } + [HttpPost] [Authorize(Roles = SystemRoles.Student)] public async Task Create( @@ -76,14 +107,19 @@ public sealed class StudentStatusChangesController( .FirstOrDefaultAsync(x => x.Id == id, token); if (change is null) return NotFound(); var scope = currentUserDataScope.Current; + if (!CanReviewCurrentStage(scope, change.State)) + return ConflictProblem("当前角色或审核阶段不允许执行该操作。"); + if (!request.Approved) { - if (change.State is StudentStatusChangeState.Approved or - StudentStatusChangeState.Rejected or StudentStatusChangeState.Cancelled) - return ConflictProblem("该申请已经结束。"); + if (string.IsNullOrWhiteSpace(request.Comment)) + return BadRequest(new ProblemDetails + { + Title = "审核意见不完整", + Detail = "驳回申请时必须填写审核意见。", + Status = StatusCodes.Status400BadRequest + }); change.State = StudentStatusChangeState.Rejected; - change.ReviewComment = request.Comment?.Trim(); - change.ReviewedAt = DateTime.UtcNow; } else if (scope.IsInRole(SystemRoles.Counselor) && change.State == StudentStatusChangeState.Submitted) @@ -100,12 +136,30 @@ public sealed class StudentStatusChangesController( change.ApprovedAt = DateTime.UtcNow; } else return ConflictProblem("当前角色或审核阶段不允许执行该操作。"); + change.ReviewComment = request.Comment?.Trim(); change.ReviewedAt = DateTime.UtcNow; await db.SaveChangesAsync(token); return NoContent(); } + [HttpPost("{id:guid}/cancel")] + [Authorize(Roles = SystemRoles.Student)] + public async Task Cancel(Guid id, CancellationToken token) + { + var userId = currentUserDataScope.Current.UserId; + var change = await db.StudentStatusChanges.FirstOrDefaultAsync( + x => x.Id == id && x.Student!.UserId == userId, token); + if (change is null) return NotFound(); + if (change.State != StudentStatusChangeState.Submitted) + return ConflictProblem("只有尚未进入审核的申请可以撤回。"); + + change.State = StudentStatusChangeState.Cancelled; + change.ReviewedAt = DateTime.UtcNow; + await db.SaveChangesAsync(token); + return NoContent(); + } + private IQueryable ScopedChanges() { var scope = currentUserDataScope.Current; @@ -123,6 +177,21 @@ public sealed class StudentStatusChangesController( return source.Where(_ => false); } + private static bool CanReviewCurrentStage( + CurrentUserScope scope, + StudentStatusChangeState state) => + state switch + { + StudentStatusChangeState.Submitted => + scope.IsInRole(SystemRoles.Counselor), + StudentStatusChangeState.CounselorApproved => + scope.IsInRole(SystemRoles.CollegeAdmin), + StudentStatusChangeState.CollegeApproved => + scope.IsInRole(SystemRoles.AcademicAdmin) || + scope.IsInRole(SystemRoles.SuperAdmin), + _ => false + }; + private ActionResult ConflictProblem(string detail) => Conflict(new ProblemDetails { Title = "无法完成学籍异动操作", Detail = detail, diff --git a/src/Jiaowu.Api/Domain/Academic/DegreeAwardEntities.cs b/src/Jiaowu.Api/Domain/Academic/DegreeAwardEntities.cs new file mode 100644 index 0000000..82cd0cd --- /dev/null +++ b/src/Jiaowu.Api/Domain/Academic/DegreeAwardEntities.cs @@ -0,0 +1,45 @@ +using Jiaowu.Api.Domain.Common; + +namespace Jiaowu.Api.Domain.Academic; + +public sealed class DegreeAwardBatch : EntityBase +{ + public required string Name { get; set; } + public int GraduationYear { get; set; } + public required string DegreeName { get; set; } + public decimal MinimumGradePoint { get; set; } = 2.0m; + public DegreeAwardBatchStatus Status { get; set; } = DegreeAwardBatchStatus.Draft; + public string? Notes { get; set; } + public DateTime? CalculatedAt { get; set; } + public DateTime? PublishedAt { get; set; } + public ICollection Results { get; set; } = []; +} + +public sealed class DegreeAwardResult : EntityBase +{ + public Guid DegreeAwardBatchId { get; set; } + public DegreeAwardBatch? DegreeAwardBatch { get; set; } + public Guid StudentId { get; set; } + public Student? Student { get; set; } + public Guid GraduationAuditResultId { get; set; } + public GraduationAuditResult? GraduationAuditResult { get; set; } + public decimal AverageGradePoint { get; set; } + public DegreeAwardConclusion CalculatedConclusion { get; set; } + public DegreeAwardConclusion Conclusion { get; set; } + public required string ExceptionReason { get; set; } + public bool IsOverridden { get; set; } + public string? ReviewComment { get; set; } + public DateTime? ReviewedAt { get; set; } +} + +public enum DegreeAwardBatchStatus +{ + Draft = 1, + Published = 2 +} + +public enum DegreeAwardConclusion +{ + NotGranted = 1, + Granted = 2 +} diff --git a/src/Jiaowu.Api/Domain/Academic/GraduationAuditEntities.cs b/src/Jiaowu.Api/Domain/Academic/GraduationAuditEntities.cs new file mode 100644 index 0000000..2911889 --- /dev/null +++ b/src/Jiaowu.Api/Domain/Academic/GraduationAuditEntities.cs @@ -0,0 +1,50 @@ +using Jiaowu.Api.Domain.Common; + +namespace Jiaowu.Api.Domain.Academic; + +public sealed class GraduationAuditBatch : EntityBase +{ + public required string Name { get; set; } + public int GraduationYear { get; set; } + public int EnrollmentYear { get; set; } + public GraduationAuditBatchStatus Status { get; set; } = + GraduationAuditBatchStatus.Draft; + public string? Notes { get; set; } + public DateTime? CalculatedAt { get; set; } + public DateTime? PublishedAt { get; set; } + public ICollection Results { get; set; } = []; +} + +public sealed class GraduationAuditResult : EntityBase +{ + public Guid GraduationAuditBatchId { get; set; } + public GraduationAuditBatch? GraduationAuditBatch { get; set; } + public Guid StudentId { get; set; } + public Student? Student { get; set; } + public Guid? CurriculumPlanId { get; set; } + public CurriculumPlan? CurriculumPlan { get; set; } + public StudentStatus StudentStatusSnapshot { get; set; } + public decimal RequiredCredits { get; set; } + public decimal EarnedCredits { get; set; } + public int RequiredCourseCount { get; set; } + public int PassedRequiredCourseCount { get; set; } + public int FailedCourseCount { get; set; } + public required string MissingCourseNames { get; set; } + public GraduationAuditConclusion CalculatedConclusion { get; set; } + public GraduationAuditConclusion Conclusion { get; set; } + public bool IsOverridden { get; set; } + public string? ReviewComment { get; set; } + public DateTime? ReviewedAt { get; set; } +} + +public enum GraduationAuditBatchStatus +{ + Draft = 1, + Published = 2 +} + +public enum GraduationAuditConclusion +{ + Ineligible = 1, + Eligible = 2 +} diff --git a/src/Jiaowu.Api/Domain/Academic/GraduationClearanceEntities.cs b/src/Jiaowu.Api/Domain/Academic/GraduationClearanceEntities.cs new file mode 100644 index 0000000..9c24b23 --- /dev/null +++ b/src/Jiaowu.Api/Domain/Academic/GraduationClearanceEntities.cs @@ -0,0 +1,53 @@ +using Jiaowu.Api.Domain.Common; + +namespace Jiaowu.Api.Domain.Academic; + +public sealed class GraduationClearanceBatch : EntityBase +{ + public required string Name { get; set; } + public int GraduationYear { get; set; } + public GraduationClearanceBatchStatus Status { get; set; } = + GraduationClearanceBatchStatus.Open; + public string? Notes { get; set; } + public DateTime? ClosedAt { get; set; } + public ICollection Items { get; set; } = []; +} + +public sealed class GraduationClearanceItem : EntityBase +{ + public Guid GraduationClearanceBatchId { get; set; } + public GraduationClearanceBatch? GraduationClearanceBatch { get; set; } + public required string Code { get; set; } + public required string Name { get; set; } + public required string ResponsibleUnit { get; set; } + public required string ResponsibleRole { get; set; } + public bool IsRequired { get; set; } = true; + public int SortOrder { get; set; } + public ICollection Records { get; set; } = []; +} + +public sealed class GraduationClearanceRecord : EntityBase +{ + public Guid GraduationClearanceItemId { get; set; } + public GraduationClearanceItem? GraduationClearanceItem { get; set; } + public Guid StudentId { get; set; } + public Student? Student { get; set; } + public GraduationClearanceRecordStatus Status { get; set; } = + GraduationClearanceRecordStatus.Pending; + public string? Notes { get; set; } + public DateTime? CompletedAt { get; set; } + public Guid? CompletedByUserId { get; set; } +} + +public enum GraduationClearanceBatchStatus +{ + Open = 1, + Closed = 2 +} + +public enum GraduationClearanceRecordStatus +{ + Pending = 1, + Completed = 2, + Waived = 3 +} diff --git a/src/Jiaowu.Api/Infrastructure/Graduation/DegreeAwardRules.cs b/src/Jiaowu.Api/Infrastructure/Graduation/DegreeAwardRules.cs new file mode 100644 index 0000000..cd9006f --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Graduation/DegreeAwardRules.cs @@ -0,0 +1,17 @@ +using Jiaowu.Api.Domain.Academic; + +namespace Jiaowu.Api.Infrastructure.Graduation; + +public static class DegreeAwardRules +{ + public static DegreeAwardConclusion Evaluate( + bool graduationEligible, + StudentStatus studentStatus, + decimal averageGradePoint, + decimal minimumGradePoint) => + graduationEligible && + studentStatus == StudentStatus.Graduated && + averageGradePoint >= minimumGradePoint + ? DegreeAwardConclusion.Granted + : DegreeAwardConclusion.NotGranted; +} diff --git a/src/Jiaowu.Api/Infrastructure/Graduation/GraduationAuditRules.cs b/src/Jiaowu.Api/Infrastructure/Graduation/GraduationAuditRules.cs new file mode 100644 index 0000000..ed1bf05 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Graduation/GraduationAuditRules.cs @@ -0,0 +1,24 @@ +using Jiaowu.Api.Domain.Academic; + +namespace Jiaowu.Api.Infrastructure.Graduation; + +public static class GraduationAuditRules +{ + public static GraduationAuditConclusion Evaluate( + bool hasPublishedPlan, + StudentStatus studentStatus, + decimal requiredCredits, + decimal earnedCredits, + int requiredCourseCount, + int passedRequiredCourseCount, + int failedCourseCount) + { + if (!hasPublishedPlan || studentStatus != StudentStatus.Active) + return GraduationAuditConclusion.Ineligible; + if (earnedCredits < requiredCredits || + passedRequiredCourseCount < requiredCourseCount || + failedCourseCount > 0) + return GraduationAuditConclusion.Ineligible; + return GraduationAuditConclusion.Eligible; + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Graduation/GraduationClearanceRules.cs b/src/Jiaowu.Api/Infrastructure/Graduation/GraduationClearanceRules.cs new file mode 100644 index 0000000..35caad1 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Graduation/GraduationClearanceRules.cs @@ -0,0 +1,16 @@ +using Jiaowu.Api.Domain.Academic; + +namespace Jiaowu.Api.Infrastructure.Graduation; + +public static class GraduationClearanceRules +{ + public static bool CanClose( + IEnumerable<(bool IsRequired, GraduationClearanceRecordStatus Status)> records) + { + var snapshot = records.ToArray(); + return snapshot.Length > 0 && snapshot + .Where(x => x.IsRequired) + .All(x => x.Status is GraduationClearanceRecordStatus.Completed or + GraduationClearanceRecordStatus.Waived); + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs index 0adcf9d..5f6e4e5 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs @@ -41,6 +41,18 @@ public sealed class AppDbContext(DbContextOptions options) Set(); public DbSet StudentStatusChanges => Set(); + public DbSet GraduationAuditBatches => + Set(); + public DbSet GraduationAuditResults => + Set(); + public DbSet DegreeAwardBatches => Set(); + public DbSet DegreeAwardResults => Set(); + public DbSet GraduationClearanceBatches => + Set(); + public DbSet GraduationClearanceItems => + Set(); + public DbSet GraduationClearanceRecords => + Set(); public DbSet AuditLogs => Set(); protected override void OnModelCreating(ModelBuilder builder) @@ -396,6 +408,82 @@ public sealed class AppDbContext(DbContextOptions options) entity.HasOne(x => x.Student).WithMany() .HasForeignKey(x => x.StudentId).OnDelete(DeleteBehavior.Restrict); }); + builder.Entity(entity => + { + entity.Property(x => x.Name).HasMaxLength(120); + entity.Property(x => x.Notes).HasMaxLength(500); + entity.HasIndex(x => new { x.GraduationYear, x.EnrollmentYear }); + entity.HasIndex(x => x.Status); + }); + builder.Entity(entity => + { + entity.Property(x => x.RequiredCredits).HasPrecision(6, 2); + entity.Property(x => x.EarnedCredits).HasPrecision(6, 2); + entity.Property(x => x.MissingCourseNames).HasMaxLength(2000); + entity.Property(x => x.ReviewComment).HasMaxLength(500); + entity.HasIndex(x => new { x.GraduationAuditBatchId, x.StudentId }) + .IsUnique(); + entity.HasIndex(x => new { x.Conclusion, x.IsOverridden }); + entity.HasOne(x => x.GraduationAuditBatch).WithMany(x => x.Results) + .HasForeignKey(x => x.GraduationAuditBatchId) + .OnDelete(DeleteBehavior.Cascade); + entity.HasOne(x => x.Student).WithMany() + .HasForeignKey(x => x.StudentId).OnDelete(DeleteBehavior.Restrict); + entity.HasOne(x => x.CurriculumPlan).WithMany() + .HasForeignKey(x => x.CurriculumPlanId).OnDelete(DeleteBehavior.SetNull); + }); + builder.Entity(entity => + { + entity.Property(x => x.Name).HasMaxLength(120); + entity.Property(x => x.DegreeName).HasMaxLength(80); + entity.Property(x => x.MinimumGradePoint).HasPrecision(3, 2); + entity.Property(x => x.Notes).HasMaxLength(500); + entity.HasIndex(x => new { x.GraduationYear, x.Status }); + }); + builder.Entity(entity => + { + entity.Property(x => x.AverageGradePoint).HasPrecision(4, 2); + entity.Property(x => x.ExceptionReason).HasMaxLength(500); + entity.Property(x => x.ReviewComment).HasMaxLength(500); + entity.HasIndex(x => new { x.DegreeAwardBatchId, x.StudentId }).IsUnique(); + entity.HasIndex(x => new { x.Conclusion, x.IsOverridden }); + entity.HasOne(x => x.DegreeAwardBatch).WithMany(x => x.Results) + .HasForeignKey(x => x.DegreeAwardBatchId).OnDelete(DeleteBehavior.Cascade); + entity.HasOne(x => x.Student).WithMany() + .HasForeignKey(x => x.StudentId).OnDelete(DeleteBehavior.Restrict); + entity.HasOne(x => x.GraduationAuditResult).WithMany() + .HasForeignKey(x => x.GraduationAuditResultId) + .OnDelete(DeleteBehavior.Restrict); + }); + builder.Entity(entity => + { + entity.Property(x => x.Name).HasMaxLength(120); + entity.Property(x => x.Notes).HasMaxLength(500); + entity.HasIndex(x => new { x.GraduationYear, x.Status }); + }); + builder.Entity(entity => + { + entity.Property(x => x.Code).HasMaxLength(30); + entity.Property(x => x.Name).HasMaxLength(100); + entity.Property(x => x.ResponsibleUnit).HasMaxLength(100); + entity.Property(x => x.ResponsibleRole).HasMaxLength(30); + entity.HasIndex(x => new { x.GraduationClearanceBatchId, x.Code }).IsUnique(); + entity.HasOne(x => x.GraduationClearanceBatch).WithMany(x => x.Items) + .HasForeignKey(x => x.GraduationClearanceBatchId) + .OnDelete(DeleteBehavior.Cascade); + }); + builder.Entity(entity => + { + entity.Property(x => x.Notes).HasMaxLength(500); + entity.HasIndex(x => new { x.GraduationClearanceItemId, x.StudentId }) + .IsUnique(); + entity.HasIndex(x => new { x.StudentId, x.Status }); + entity.HasOne(x => x.GraduationClearanceItem).WithMany(x => x.Records) + .HasForeignKey(x => x.GraduationClearanceItemId) + .OnDelete(DeleteBehavior.Cascade); + entity.HasOne(x => x.Student).WithMany() + .HasForeignKey(x => x.StudentId).OnDelete(DeleteBehavior.Restrict); + }); builder.Entity(entity => { diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs index a16cacf..1882b85 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs @@ -15,6 +15,9 @@ public sealed class DevelopmentSqliteMigrator( private const string GradesMigration = "20260724_07_grades"; private const string ExamsMigration = "20260724_08_exams"; private const string StudentStatusChangesMigration = "20260724_09_student_status_changes"; + private const string GraduationAuditsMigration = "20260724_10_graduation_audits"; + private const string DegreeAwardsMigration = "20260724_11_degree_awards"; + private const string GraduationClearanceMigration = "20260724_12_graduation_clearance"; public async Task MigrateAsync(CancellationToken cancellationToken = default) { @@ -75,6 +78,18 @@ public sealed class DevelopmentSqliteMigrator( StudentStatusChangesMigration, StudentStatusChangesStatements, cancellationToken); + await ApplyMigrationAsync( + GraduationAuditsMigration, + GraduationAuditsStatements, + cancellationToken); + await ApplyMigrationAsync( + DegreeAwardsMigration, + DegreeAwardsStatements, + cancellationToken); + await ApplyMigrationAsync( + GraduationClearanceMigration, + GraduationClearanceStatements, + cancellationToken); } private async Task ApplyMigrationAsync( @@ -637,4 +652,119 @@ public sealed class DevelopmentSqliteMigrator( """, """CREATE INDEX IF NOT EXISTS "IX_StudentStatusChanges_StudentId_State" ON "StudentStatusChanges" ("StudentId", "State");""" ]; + + private static readonly string[] GraduationAuditsStatements = + [ + """ + CREATE TABLE IF NOT EXISTS "GraduationAuditBatches" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_GraduationAuditBatches" PRIMARY KEY, + "Name" TEXT NOT NULL, "GraduationYear" INTEGER NOT NULL, + "EnrollmentYear" INTEGER NOT NULL, "Status" INTEGER NOT NULL, + "Notes" TEXT NULL, "CalculatedAt" TEXT NULL, "PublishedAt" TEXT NULL, + "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL + ); + """, + """CREATE INDEX IF NOT EXISTS "IX_GraduationAuditBatches_GraduationYear_EnrollmentYear" ON "GraduationAuditBatches" ("GraduationYear", "EnrollmentYear");""", + """CREATE INDEX IF NOT EXISTS "IX_GraduationAuditBatches_Status" ON "GraduationAuditBatches" ("Status");""", + """ + CREATE TABLE IF NOT EXISTS "GraduationAuditResults" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_GraduationAuditResults" PRIMARY KEY, + "GraduationAuditBatchId" TEXT NOT NULL, "StudentId" TEXT NOT NULL, + "CurriculumPlanId" TEXT NULL, "StudentStatusSnapshot" INTEGER NOT NULL, + "RequiredCredits" TEXT NOT NULL, "EarnedCredits" TEXT NOT NULL, + "RequiredCourseCount" INTEGER NOT NULL, + "PassedRequiredCourseCount" INTEGER NOT NULL, + "FailedCourseCount" INTEGER NOT NULL, + "MissingCourseNames" TEXT NOT NULL, + "CalculatedConclusion" INTEGER NOT NULL, "Conclusion" INTEGER NOT NULL, + "IsOverridden" INTEGER NOT NULL, "ReviewComment" TEXT NULL, + "ReviewedAt" TEXT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, + CONSTRAINT "FK_GraduationAuditResults_GraduationAuditBatches_GraduationAuditBatchId" + FOREIGN KEY ("GraduationAuditBatchId") REFERENCES "GraduationAuditBatches" ("Id") ON DELETE CASCADE, + CONSTRAINT "FK_GraduationAuditResults_Students_StudentId" + FOREIGN KEY ("StudentId") REFERENCES "Students" ("Id") ON DELETE RESTRICT, + CONSTRAINT "FK_GraduationAuditResults_CurriculumPlans_CurriculumPlanId" + FOREIGN KEY ("CurriculumPlanId") REFERENCES "CurriculumPlans" ("Id") ON DELETE SET NULL + ); + """, + """CREATE UNIQUE INDEX IF NOT EXISTS "IX_GraduationAuditResults_GraduationAuditBatchId_StudentId" ON "GraduationAuditResults" ("GraduationAuditBatchId", "StudentId");""", + """CREATE INDEX IF NOT EXISTS "IX_GraduationAuditResults_StudentId" ON "GraduationAuditResults" ("StudentId");""", + """CREATE INDEX IF NOT EXISTS "IX_GraduationAuditResults_CurriculumPlanId" ON "GraduationAuditResults" ("CurriculumPlanId");""", + """CREATE INDEX IF NOT EXISTS "IX_GraduationAuditResults_Conclusion_IsOverridden" ON "GraduationAuditResults" ("Conclusion", "IsOverridden");""" + ]; + + private static readonly string[] DegreeAwardsStatements = + [ + """ + CREATE TABLE IF NOT EXISTS "DegreeAwardBatches" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_DegreeAwardBatches" PRIMARY KEY, + "Name" TEXT NOT NULL, "GraduationYear" INTEGER NOT NULL, + "DegreeName" TEXT NOT NULL, "MinimumGradePoint" TEXT NOT NULL, + "Status" INTEGER NOT NULL, "Notes" TEXT NULL, + "CalculatedAt" TEXT NULL, "PublishedAt" TEXT NULL, + "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL + ); + """, + """CREATE INDEX IF NOT EXISTS "IX_DegreeAwardBatches_GraduationYear_Status" ON "DegreeAwardBatches" ("GraduationYear", "Status");""", + """ + CREATE TABLE IF NOT EXISTS "DegreeAwardResults" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_DegreeAwardResults" PRIMARY KEY, + "DegreeAwardBatchId" TEXT NOT NULL, "StudentId" TEXT NOT NULL, + "GraduationAuditResultId" TEXT NOT NULL, "AverageGradePoint" TEXT NOT NULL, + "CalculatedConclusion" INTEGER NOT NULL, "Conclusion" INTEGER NOT NULL, + "ExceptionReason" TEXT NOT NULL, "IsOverridden" INTEGER NOT NULL, + "ReviewComment" TEXT NULL, "ReviewedAt" TEXT NULL, + "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, + CONSTRAINT "FK_DegreeAwardResults_DegreeAwardBatches_DegreeAwardBatchId" + FOREIGN KEY ("DegreeAwardBatchId") REFERENCES "DegreeAwardBatches" ("Id") ON DELETE CASCADE, + CONSTRAINT "FK_DegreeAwardResults_Students_StudentId" + FOREIGN KEY ("StudentId") REFERENCES "Students" ("Id") ON DELETE RESTRICT, + CONSTRAINT "FK_DegreeAwardResults_GraduationAuditResults_GraduationAuditResultId" + FOREIGN KEY ("GraduationAuditResultId") REFERENCES "GraduationAuditResults" ("Id") ON DELETE RESTRICT + ); + """, + """CREATE UNIQUE INDEX IF NOT EXISTS "IX_DegreeAwardResults_DegreeAwardBatchId_StudentId" ON "DegreeAwardResults" ("DegreeAwardBatchId", "StudentId");""", + """CREATE INDEX IF NOT EXISTS "IX_DegreeAwardResults_StudentId" ON "DegreeAwardResults" ("StudentId");""", + """CREATE INDEX IF NOT EXISTS "IX_DegreeAwardResults_GraduationAuditResultId" ON "DegreeAwardResults" ("GraduationAuditResultId");""", + """CREATE INDEX IF NOT EXISTS "IX_DegreeAwardResults_Conclusion_IsOverridden" ON "DegreeAwardResults" ("Conclusion", "IsOverridden");""" + ]; + + private static readonly string[] GraduationClearanceStatements = + [ + """ + CREATE TABLE IF NOT EXISTS "GraduationClearanceBatches" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_GraduationClearanceBatches" PRIMARY KEY, + "Name" TEXT NOT NULL, "GraduationYear" INTEGER NOT NULL, + "Status" INTEGER NOT NULL, "Notes" TEXT NULL, "ClosedAt" TEXT NULL, + "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL + ); + """, + """CREATE INDEX IF NOT EXISTS "IX_GraduationClearanceBatches_GraduationYear_Status" ON "GraduationClearanceBatches" ("GraduationYear", "Status");""", + """ + CREATE TABLE IF NOT EXISTS "GraduationClearanceItems" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_GraduationClearanceItems" PRIMARY KEY, + "GraduationClearanceBatchId" TEXT NOT NULL, "Code" TEXT NOT NULL, + "Name" TEXT NOT NULL, "ResponsibleUnit" TEXT NOT NULL, + "ResponsibleRole" TEXT NOT NULL, "IsRequired" INTEGER NOT NULL, + "SortOrder" INTEGER NOT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, + CONSTRAINT "FK_GraduationClearanceItems_GraduationClearanceBatches_GraduationClearanceBatchId" + FOREIGN KEY ("GraduationClearanceBatchId") REFERENCES "GraduationClearanceBatches" ("Id") ON DELETE CASCADE + ); + """, + """CREATE UNIQUE INDEX IF NOT EXISTS "IX_GraduationClearanceItems_GraduationClearanceBatchId_Code" ON "GraduationClearanceItems" ("GraduationClearanceBatchId", "Code");""", + """ + CREATE TABLE IF NOT EXISTS "GraduationClearanceRecords" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_GraduationClearanceRecords" PRIMARY KEY, + "GraduationClearanceItemId" TEXT NOT NULL, "StudentId" TEXT NOT NULL, + "Status" INTEGER NOT NULL, "Notes" TEXT NULL, "CompletedAt" TEXT NULL, + "CompletedByUserId" TEXT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, + CONSTRAINT "FK_GraduationClearanceRecords_GraduationClearanceItems_GraduationClearanceItemId" + FOREIGN KEY ("GraduationClearanceItemId") REFERENCES "GraduationClearanceItems" ("Id") ON DELETE CASCADE, + CONSTRAINT "FK_GraduationClearanceRecords_Students_StudentId" + FOREIGN KEY ("StudentId") REFERENCES "Students" ("Id") ON DELETE RESTRICT + ); + """, + """CREATE UNIQUE INDEX IF NOT EXISTS "IX_GraduationClearanceRecords_GraduationClearanceItemId_StudentId" ON "GraduationClearanceRecords" ("GraduationClearanceItemId", "StudentId");""", + """CREATE INDEX IF NOT EXISTS "IX_GraduationClearanceRecords_StudentId_Status" ON "GraduationClearanceRecords" ("StudentId", "Status");""" + ]; } diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724075918_GraduationAudits.Designer.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724075918_GraduationAudits.Designer.cs new file mode 100644 index 0000000..bf99c21 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724075918_GraduationAudits.Designer.cs @@ -0,0 +1,2183 @@ +// +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("20260724075918_GraduationAudits")] + partial class GraduationAudits + { + /// + 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("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndDate") + .HasColumnType("date"); + + 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("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.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.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("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("CollegeId", "Nature"); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Courses"); + }); + + 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("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WithdrawnAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CourseSelectionOfferingId", "StudentId") + .IsUnique(); + + b.HasIndex("StudentId", "Status"); + + b.ToTable("CourseEnrollments"); + }); + + 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("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.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.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.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("ExamPlanId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + 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("TeachingTaskId"); + + 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.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("MidtermScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + 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("MidtermWeight") + .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.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.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.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.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("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + 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.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.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.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("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("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.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.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.Navigation("College"); + }); + + 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.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.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.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.ExamSession", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExamPlan", "ExamPlan") + .WithMany("Sessions") + .HasForeignKey("ExamPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("ExamPlan"); + + 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.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.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.ScheduleEntry", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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.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.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.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.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("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.CourseSelectionOffering", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionRound", b => + { + 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.ExamPlan", b => + { + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSession", b => + { + b.Navigation("Invigilators"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeSheet", b => + { + b.Navigation("Records"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationAuditBatch", b => + { + b.Navigation("Results"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.SchedulePlan", b => + { + b.Navigation("Entries"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTask", b => + { + b.Navigation("Classes"); + + b.Navigation("Teachers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724075918_GraduationAudits.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724075918_GraduationAudits.cs new file mode 100644 index 0000000..a2f6abc --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724075918_GraduationAudits.cs @@ -0,0 +1,124 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql +{ + /// + public partial class GraduationAudits : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "GraduationAuditBatches", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + Name = table.Column(type: "varchar(120)", maxLength: 120, nullable: false), + GraduationYear = table.Column(type: "int", nullable: false), + EnrollmentYear = table.Column(type: "int", nullable: false), + Status = table.Column(type: "int", nullable: false), + Notes = table.Column(type: "varchar(500)", maxLength: 500, nullable: true), + CalculatedAt = 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_GraduationAuditBatches", x => x.Id); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "GraduationAuditResults", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + GraduationAuditBatchId = table.Column(type: "char(36)", nullable: false), + StudentId = table.Column(type: "char(36)", nullable: false), + CurriculumPlanId = table.Column(type: "char(36)", nullable: true), + StudentStatusSnapshot = table.Column(type: "int", nullable: false), + RequiredCredits = table.Column(type: "decimal(6,2)", precision: 6, scale: 2, nullable: false), + EarnedCredits = table.Column(type: "decimal(6,2)", precision: 6, scale: 2, nullable: false), + RequiredCourseCount = table.Column(type: "int", nullable: false), + PassedRequiredCourseCount = table.Column(type: "int", nullable: false), + FailedCourseCount = table.Column(type: "int", nullable: false), + MissingCourseNames = table.Column(type: "varchar(2000)", maxLength: 2000, nullable: false), + CalculatedConclusion = table.Column(type: "int", nullable: false), + Conclusion = table.Column(type: "int", nullable: false), + IsOverridden = table.Column(type: "tinyint(1)", nullable: false), + ReviewComment = table.Column(type: "varchar(500)", maxLength: 500, nullable: true), + ReviewedAt = 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_GraduationAuditResults", x => x.Id); + table.ForeignKey( + name: "FK_GraduationAuditResults_CurriculumPlans_CurriculumPlanId", + column: x => x.CurriculumPlanId, + principalTable: "CurriculumPlans", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_GraduationAuditResults_GraduationAuditBatches_GraduationAudi~", + column: x => x.GraduationAuditBatchId, + principalTable: "GraduationAuditBatches", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_GraduationAuditResults_Students_StudentId", + column: x => x.StudentId, + principalTable: "Students", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_GraduationAuditBatches_GraduationYear_EnrollmentYear", + table: "GraduationAuditBatches", + columns: new[] { "GraduationYear", "EnrollmentYear" }); + + migrationBuilder.CreateIndex( + name: "IX_GraduationAuditBatches_Status", + table: "GraduationAuditBatches", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_GraduationAuditResults_Conclusion_IsOverridden", + table: "GraduationAuditResults", + columns: new[] { "Conclusion", "IsOverridden" }); + + migrationBuilder.CreateIndex( + name: "IX_GraduationAuditResults_CurriculumPlanId", + table: "GraduationAuditResults", + column: "CurriculumPlanId"); + + migrationBuilder.CreateIndex( + name: "IX_GraduationAuditResults_GraduationAuditBatchId_StudentId", + table: "GraduationAuditResults", + columns: new[] { "GraduationAuditBatchId", "StudentId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_GraduationAuditResults_StudentId", + table: "GraduationAuditResults", + column: "StudentId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GraduationAuditResults"); + + migrationBuilder.DropTable( + name: "GraduationAuditBatches"); + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724081821_DegreeAwards.Designer.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724081821_DegreeAwards.Designer.cs new file mode 100644 index 0000000..eff533b --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724081821_DegreeAwards.Designer.cs @@ -0,0 +1,2324 @@ +// +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("20260724081821_DegreeAwards")] + partial class DegreeAwards + { + /// + 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("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndDate") + .HasColumnType("date"); + + 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("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.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.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("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("CollegeId", "Nature"); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Courses"); + }); + + 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("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WithdrawnAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CourseSelectionOfferingId", "StudentId") + .IsUnique(); + + b.HasIndex("StudentId", "Status"); + + b.ToTable("CourseEnrollments"); + }); + + 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("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.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.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.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.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("ExamPlanId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + 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("TeachingTaskId"); + + 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.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("MidtermScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + 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("MidtermWeight") + .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.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.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.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.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("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + 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.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.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.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("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("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.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.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.Navigation("College"); + }); + + 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.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.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.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.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.ExamSession", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExamPlan", "ExamPlan") + .WithMany("Sessions") + .HasForeignKey("ExamPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("ExamPlan"); + + 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.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.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.ScheduleEntry", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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.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.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.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.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("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.CourseSelectionOffering", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionRound", b => + { + 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.ExamPlan", b => + { + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSession", b => + { + b.Navigation("Invigilators"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeSheet", b => + { + b.Navigation("Records"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GraduationAuditBatch", b => + { + b.Navigation("Results"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.SchedulePlan", b => + { + b.Navigation("Entries"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTask", b => + { + b.Navigation("Classes"); + + b.Navigation("Teachers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724081821_DegreeAwards.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724081821_DegreeAwards.cs new file mode 100644 index 0000000..b77744d --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724081821_DegreeAwards.cs @@ -0,0 +1,115 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql +{ + /// + public partial class DegreeAwards : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DegreeAwardBatches", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + Name = table.Column(type: "varchar(120)", maxLength: 120, nullable: false), + GraduationYear = table.Column(type: "int", nullable: false), + DegreeName = table.Column(type: "varchar(80)", maxLength: 80, nullable: false), + MinimumGradePoint = table.Column(type: "decimal(3,2)", precision: 3, scale: 2, nullable: false), + Status = table.Column(type: "int", nullable: false), + Notes = table.Column(type: "varchar(500)", maxLength: 500, nullable: true), + CalculatedAt = 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_DegreeAwardBatches", x => x.Id); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "DegreeAwardResults", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + DegreeAwardBatchId = table.Column(type: "char(36)", nullable: false), + StudentId = table.Column(type: "char(36)", nullable: false), + GraduationAuditResultId = table.Column(type: "char(36)", nullable: false), + AverageGradePoint = table.Column(type: "decimal(4,2)", precision: 4, scale: 2, nullable: false), + CalculatedConclusion = table.Column(type: "int", nullable: false), + Conclusion = table.Column(type: "int", nullable: false), + ExceptionReason = table.Column(type: "varchar(500)", maxLength: 500, nullable: false), + IsOverridden = table.Column(type: "tinyint(1)", nullable: false), + ReviewComment = table.Column(type: "varchar(500)", maxLength: 500, nullable: true), + ReviewedAt = 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_DegreeAwardResults", x => x.Id); + table.ForeignKey( + name: "FK_DegreeAwardResults_DegreeAwardBatches_DegreeAwardBatchId", + column: x => x.DegreeAwardBatchId, + principalTable: "DegreeAwardBatches", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_DegreeAwardResults_GraduationAuditResults_GraduationAuditRes~", + column: x => x.GraduationAuditResultId, + principalTable: "GraduationAuditResults", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_DegreeAwardResults_Students_StudentId", + column: x => x.StudentId, + principalTable: "Students", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_DegreeAwardBatches_GraduationYear_Status", + table: "DegreeAwardBatches", + columns: new[] { "GraduationYear", "Status" }); + + migrationBuilder.CreateIndex( + name: "IX_DegreeAwardResults_Conclusion_IsOverridden", + table: "DegreeAwardResults", + columns: new[] { "Conclusion", "IsOverridden" }); + + migrationBuilder.CreateIndex( + name: "IX_DegreeAwardResults_DegreeAwardBatchId_StudentId", + table: "DegreeAwardResults", + columns: new[] { "DegreeAwardBatchId", "StudentId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_DegreeAwardResults_GraduationAuditResultId", + table: "DegreeAwardResults", + column: "GraduationAuditResultId"); + + migrationBuilder.CreateIndex( + name: "IX_DegreeAwardResults_StudentId", + table: "DegreeAwardResults", + column: "StudentId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DegreeAwardResults"); + + migrationBuilder.DropTable( + name: "DegreeAwardBatches"); + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724092052_GraduationClearance.Designer.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724092052_GraduationClearance.Designer.cs new file mode 100644 index 0000000..0eeb2f9 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724092052_GraduationClearance.Designer.cs @@ -0,0 +1,2491 @@ +// +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("20260724092052_GraduationClearance")] + partial class GraduationClearance + { + /// + 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("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndDate") + .HasColumnType("date"); + + 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("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.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.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("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("CollegeId", "Nature"); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Courses"); + }); + + 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("Status") + .HasColumnType("int"); + + b.Property("StudentId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WithdrawnAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CourseSelectionOfferingId", "StudentId") + .IsUnique(); + + b.HasIndex("StudentId", "Status"); + + b.ToTable("CourseEnrollments"); + }); + + 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("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.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.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.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.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("ExamPlanId") + .HasColumnType("char(36)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + 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("TeachingTaskId"); + + 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.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("MidtermScore") + .HasPrecision(5, 1) + .HasColumnType("decimal(5,1)"); + + 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("MidtermWeight") + .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.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.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.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("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + + 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.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.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.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("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("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.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.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.Navigation("College"); + }); + + 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.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.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.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.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.ExamSession", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.ExamPlan", "ExamPlan") + .WithMany("Sessions") + .HasForeignKey("ExamPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask") + .WithMany() + .HasForeignKey("TeachingTaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Classroom"); + + b.Navigation("ExamPlan"); + + 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.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.ScheduleEntry", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom") + .WithMany() + .HasForeignKey("ClassroomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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.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.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.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.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("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.CourseSelectionOffering", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionRound", b => + { + 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.ExamPlan", b => + { + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSession", b => + { + b.Navigation("Invigilators"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeSheet", b => + { + 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.SchedulePlan", b => + { + b.Navigation("Entries"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTask", b => + { + b.Navigation("Classes"); + + b.Navigation("Teachers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724092052_GraduationClearance.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724092052_GraduationClearance.cs new file mode 100644 index 0000000..96c78d8 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724092052_GraduationClearance.cs @@ -0,0 +1,128 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql +{ + /// + public partial class GraduationClearance : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "GraduationClearanceBatches", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + Name = table.Column(type: "varchar(120)", maxLength: 120, nullable: false), + GraduationYear = table.Column(type: "int", nullable: false), + Status = table.Column(type: "int", nullable: false), + Notes = table.Column(type: "varchar(500)", maxLength: 500, nullable: true), + ClosedAt = table.Column(type: "datetime(6)", nullable: true), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GraduationClearanceBatches", x => x.Id); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "GraduationClearanceItems", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + GraduationClearanceBatchId = table.Column(type: "char(36)", nullable: false), + Code = table.Column(type: "varchar(30)", maxLength: 30, nullable: false), + Name = table.Column(type: "varchar(100)", maxLength: 100, nullable: false), + ResponsibleUnit = table.Column(type: "varchar(100)", maxLength: 100, nullable: false), + ResponsibleRole = table.Column(type: "varchar(30)", maxLength: 30, nullable: false), + IsRequired = table.Column(type: "tinyint(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_GraduationClearanceItems", x => x.Id); + table.ForeignKey( + name: "FK_GraduationClearanceItems_GraduationClearanceBatches_Graduati~", + column: x => x.GraduationClearanceBatchId, + principalTable: "GraduationClearanceBatches", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "GraduationClearanceRecords", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + GraduationClearanceItemId = table.Column(type: "char(36)", nullable: false), + StudentId = table.Column(type: "char(36)", nullable: false), + Status = table.Column(type: "int", nullable: false), + Notes = table.Column(type: "varchar(500)", maxLength: 500, nullable: true), + CompletedAt = table.Column(type: "datetime(6)", nullable: true), + CompletedByUserId = table.Column(type: "char(36)", nullable: true), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GraduationClearanceRecords", x => x.Id); + table.ForeignKey( + name: "FK_GraduationClearanceRecords_GraduationClearanceItems_Graduati~", + column: x => x.GraduationClearanceItemId, + principalTable: "GraduationClearanceItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_GraduationClearanceRecords_Students_StudentId", + column: x => x.StudentId, + principalTable: "Students", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_GraduationClearanceBatches_GraduationYear_Status", + table: "GraduationClearanceBatches", + columns: new[] { "GraduationYear", "Status" }); + + migrationBuilder.CreateIndex( + name: "IX_GraduationClearanceItems_GraduationClearanceBatchId_Code", + table: "GraduationClearanceItems", + columns: new[] { "GraduationClearanceBatchId", "Code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_GraduationClearanceRecords_GraduationClearanceItemId_Student~", + table: "GraduationClearanceRecords", + columns: new[] { "GraduationClearanceItemId", "StudentId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_GraduationClearanceRecords_StudentId_Status", + table: "GraduationClearanceRecords", + columns: new[] { "StudentId", "Status" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GraduationClearanceRecords"); + + migrationBuilder.DropTable( + name: "GraduationClearanceItems"); + + migrationBuilder.DropTable( + name: "GraduationClearanceBatches"); + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs index ac0f08b..77acf5a 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs @@ -634,6 +634,115 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.ToTable("CurriculumPlans"); }); + 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.ExamPlan", b => { b.Property("Id") @@ -836,6 +945,254 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql 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") @@ -1684,6 +2041,33 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.Navigation("Major"); }); + 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.ExamPlan", b => { b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm") @@ -1771,6 +2155,62 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql 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") @@ -1996,6 +2436,11 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.Navigation("Modules"); }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.DegreeAwardBatch", b => + { + b.Navigation("Results"); + }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamPlan", b => { b.Navigation("Sessions"); @@ -2011,6 +2456,21 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql 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.SchedulePlan", b => { b.Navigation("Entries"); diff --git a/tests/Jiaowu.Api.Tests/DegreeAwardRulesTests.cs b/tests/Jiaowu.Api.Tests/DegreeAwardRulesTests.cs new file mode 100644 index 0000000..c9ae494 --- /dev/null +++ b/tests/Jiaowu.Api.Tests/DegreeAwardRulesTests.cs @@ -0,0 +1,32 @@ +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Infrastructure.Graduation; + +namespace Jiaowu.Api.Tests; + +public sealed class DegreeAwardRulesTests +{ + [Fact] + public void Graduated_student_meeting_gpa_requirement_is_granted() + { + var conclusion = DegreeAwardRules.Evaluate( + true, StudentStatus.Graduated, 3.12m, 2.0m); + + Assert.Equal(DegreeAwardConclusion.Granted, conclusion); + } + + [Theory] + [InlineData(false, StudentStatus.Graduated, 3.12, 2.0)] + [InlineData(true, StudentStatus.Active, 3.12, 2.0)] + [InlineData(true, StudentStatus.Graduated, 1.99, 2.0)] + public void Missing_prerequisite_prevents_degree_award( + bool graduationEligible, + StudentStatus status, + decimal averageGradePoint, + decimal minimumGradePoint) + { + var conclusion = DegreeAwardRules.Evaluate( + graduationEligible, status, averageGradePoint, minimumGradePoint); + + Assert.Equal(DegreeAwardConclusion.NotGranted, conclusion); + } +} diff --git a/tests/Jiaowu.Api.Tests/GraduationAuditRulesTests.cs b/tests/Jiaowu.Api.Tests/GraduationAuditRulesTests.cs new file mode 100644 index 0000000..bf0160b --- /dev/null +++ b/tests/Jiaowu.Api.Tests/GraduationAuditRulesTests.cs @@ -0,0 +1,38 @@ +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Infrastructure.Graduation; + +namespace Jiaowu.Api.Tests; + +public sealed class GraduationAuditRulesTests +{ + [Fact] + public void Active_student_with_full_credits_and_courses_is_eligible() + { + var conclusion = GraduationAuditRules.Evaluate( + true, StudentStatus.Active, 160, 162, 42, 42, 0); + + Assert.Equal(GraduationAuditConclusion.Eligible, conclusion); + } + + [Theory] + [InlineData(false, StudentStatus.Active, 160, 162, 42, 42, 0)] + [InlineData(true, StudentStatus.Suspended, 160, 162, 42, 42, 0)] + [InlineData(true, StudentStatus.Active, 160, 159, 42, 42, 0)] + [InlineData(true, StudentStatus.Active, 160, 162, 42, 41, 0)] + [InlineData(true, StudentStatus.Active, 160, 162, 42, 42, 1)] + public void Any_unresolved_requirement_makes_student_ineligible( + bool hasPlan, + StudentStatus status, + decimal requiredCredits, + decimal earnedCredits, + int requiredCourses, + int passedRequiredCourses, + int failedCourses) + { + var conclusion = GraduationAuditRules.Evaluate( + hasPlan, status, requiredCredits, earnedCredits, + requiredCourses, passedRequiredCourses, failedCourses); + + Assert.Equal(GraduationAuditConclusion.Ineligible, conclusion); + } +} diff --git a/tests/Jiaowu.Api.Tests/GraduationClearanceRulesTests.cs b/tests/Jiaowu.Api.Tests/GraduationClearanceRulesTests.cs new file mode 100644 index 0000000..1ab23af --- /dev/null +++ b/tests/Jiaowu.Api.Tests/GraduationClearanceRulesTests.cs @@ -0,0 +1,38 @@ +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Infrastructure.Graduation; + +namespace Jiaowu.Api.Tests; + +public sealed class GraduationClearanceRulesTests +{ + [Fact] + public void Required_items_may_be_completed_or_waived() + { + var records = new[] + { + (true, GraduationClearanceRecordStatus.Completed), + (true, GraduationClearanceRecordStatus.Waived), + (false, GraduationClearanceRecordStatus.Pending) + }; + + Assert.True(GraduationClearanceRules.CanClose(records)); + } + + [Fact] + public void Pending_required_item_prevents_batch_close() + { + var records = new[] + { + (true, GraduationClearanceRecordStatus.Completed), + (true, GraduationClearanceRecordStatus.Pending) + }; + + Assert.False(GraduationClearanceRules.CanClose(records)); + } + + [Fact] + public void Empty_batch_cannot_be_closed() + { + Assert.False(GraduationClearanceRules.CanClose([])); + } +} diff --git a/web/src/components.d.ts b/web/src/components.d.ts index bb9fedc..4fdc1eb 100644 --- a/web/src/components.d.ts +++ b/web/src/components.d.ts @@ -27,6 +27,9 @@ declare module 'vue' { ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElOption: typeof import('element-plus/es')['ElOption'] ElPagination: typeof import('element-plus/es')['ElPagination'] + ElProgress: typeof import('element-plus/es')['ElProgress'] + ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] + ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElTable: typeof import('element-plus/es')['ElTable'] diff --git a/web/src/layouts/AdminLayout.vue b/web/src/layouts/AdminLayout.vue index 2d8f641..d8dc835 100644 --- a/web/src/layouts/AdminLayout.vue +++ b/web/src/layouts/AdminLayout.vue @@ -12,6 +12,8 @@ import { CircleCheck, DocumentChecked, AlarmClock, + RefreshRight, + Medal, User, UserFilled, } from '@element-plus/icons-vue' @@ -45,6 +47,10 @@ const pageTitle = computed(() => { 'course-selections': '选课与教学班', grades: '成绩与学业档案', exams: '考试与考场', + 'student-status-changes': '学籍异动', + 'graduation-audits': '毕业资格审核', + 'degree-awards': '学位授予审核', + 'graduation-clearance': '毕业离校办理', users: '用户与权限', } return titles[String(route.name)] ?? '教务管理' @@ -147,6 +153,34 @@ onMounted(() => auth.refresh().catch(() => undefined)) + + + + + + + + + + + + + + + + @@ -155,7 +189,7 @@ onMounted(() => auth.refresh().catch(() => undefined))
第一阶段 · 核心可用版 -

教学运行、选课与成绩管理已就绪

+

教学运行、学籍与毕业审核已就绪

diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 36e385f..b3c7a72 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -80,6 +80,38 @@ const router = createRouter({ component: () => import('../views/ExamsView.vue'), meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'Teacher', 'Student'] }, }, + { + path: 'student-status-changes', + name: 'student-status-changes', + component: () => import('../views/StudentStatusChangesView.vue'), + meta: { + roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor', 'Student'], + }, + }, + { + path: 'graduation-audits', + name: 'graduation-audits', + component: () => import('../views/GraduationAuditsView.vue'), + meta: { + roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student'], + }, + }, + { + path: 'degree-awards', + name: 'degree-awards', + component: () => import('../views/DegreeAwardsView.vue'), + meta: { + roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student'], + }, + }, + { + path: 'graduation-clearance', + name: 'graduation-clearance', + component: () => import('../views/GraduationClearanceView.vue'), + meta: { + roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor', 'Student'], + }, + }, { path: 'users', name: 'users', diff --git a/web/src/style.css b/web/src/style.css index 40bca59..1e76705 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -546,6 +546,221 @@ button { cursor: pointer; } .exam-ticket-grid p { margin:0; color:var(--muted); font-size:10px; } .exam-ticket-grid footer { grid-column:1/-1; padding:12px 17px; display:flex; align-items:center; gap:7px; border-top:1px dashed var(--line); color:#586275; font-size:10px; } +.status-identity { + min-height: 130px; padding: 24px 28px; display: flex; align-items: center; + justify-content: space-between; gap: 28px; color: white; + background: + linear-gradient(90deg, rgba(255,255,255,.04) 1px, transparent 1px), + linear-gradient(135deg, #172858, #263e7c 65%, #176d70); + background-size: 35px 35px, auto; +} +.status-identity span { color: #70ded0; font: 700 9px Consolas, monospace; letter-spacing: .14em; } +.status-identity b { display: block; margin: 8px 0 6px; font: 600 27px "STZhongsong","Songti SC",serif; } +.status-identity p { margin: 0; color: #c4cee6; font-size: 10px; } +.status-available { min-width: 290px; padding: 17px 20px; border: 1px solid rgba(255,255,255,.15); } +.status-available > span { display: block; margin-bottom: 11px; } +.status-available strong { display: inline-block; margin-right: 7px; padding: 7px 12px; color: white; border: 1px solid rgba(112,222,208,.55); font-size: 11px; } +.status-available em { color: #c4cee6; font-size: 10px; font-style: normal; } +.status-review-summary { display: grid; grid-template-columns: repeat(3, 170px) 1fr; color: white; background: #182955; } +.status-review-summary > div { min-height: 104px; padding: 20px 24px; border-right: 1px solid rgba(255,255,255,.12); } +.status-review-summary span { display: block; color: #9eacd0; font-size: 9px; } +.status-review-summary b { display: inline-block; margin-top: 12px; color: #65d7c8; font: 700 28px/1 Consolas, monospace; } +.status-review-summary small { margin-left: 5px; color: #aeb9d6; font-size: 9px; } +.status-review-summary p { margin: 0; padding: 24px 28px; align-self: center; color: #bdc7e0; font-size: 10px; line-height: 1.7; } +.status-folio-list { display: grid; gap: 13px; min-height: 180px; } +.status-folio-list > article { border: 1px solid var(--line); background: white; box-shadow: 0 5px 18px rgba(30,44,77,.035); } +.status-folio-list > article.actionable { border-left: 3px solid var(--teal); } +.status-folio-list article > header { min-height: 100px; display: grid; grid-template-columns: 150px 1fr auto; align-items: center; gap: 24px; padding: 18px 22px; border-bottom: 1px solid var(--line); } +.folio-number { align-self: stretch; display: grid; place-content: center; padding-right: 22px; border-right: 1px dashed var(--line); } +.folio-number span { color: var(--teal); font: 700 8px Consolas,monospace; letter-spacing: .12em; } +.folio-number b { margin-top: 8px; color: var(--indigo); font: 700 16px Consolas,monospace; } +.folio-person > span { color: var(--teal); font-size: 9px; } +.folio-person h3 { margin: 7px 0 5px; font: 600 18px "STZhongsong","Songti SC",serif; } +.folio-person p { margin: 0; color: var(--muted); font-size: 9px; } +.folio-reason { padding: 17px 22px; display: grid; grid-template-columns: 125px 1fr; gap: 25px; border-bottom: 1px solid var(--line); } +.folio-reason span { color: var(--muted); font-size: 9px; } +.folio-reason p { margin: 0; color: #40495a; font-size: 11px; line-height: 1.7; } +.approval-track { padding: 20px 22px; display: grid; grid-template-columns: repeat(3, 1fr); border-bottom: 1px solid var(--line); } +.approval-track > div { min-height: 55px; display: grid; grid-template-columns: 36px 1fr; align-content: center; position: relative; } +.approval-track > div:not(:last-child)::after { content:""; position:absolute; left:30px; right:8px; top:17px; height:1px; background:#d9dee7; } +.approval-track i { width: 34px; height: 34px; display: grid; place-items: center; z-index:1; color:#8d95a4; border:1px solid #cfd5df; border-radius:50%; background:white; font:normal 700 10px Consolas,monospace; } +.approval-track b, .approval-track small { grid-column: 2; padding-left: 8px; } +.approval-track b { margin-top: -31px; font-size: 11px; } +.approval-track small { margin-top: 4px; color: var(--muted); font-size: 8px; } +.approval-track .done i { color:white; border-color:var(--teal); background:var(--teal); } +.approval-track .done::after { background:var(--teal) !important; } +.approval-track .active i { color:white; border-color:var(--indigo); background:var(--indigo); box-shadow:0 0 0 4px #e9edf7; } +.approval-track .stopped i { color:white; border-color:#b44c4c; background:#b44c4c; } +.status-folio-list article > footer { min-height: 65px; padding: 12px 22px; display:flex; align-items:center; justify-content:space-between; gap:18px; background:#fafbfc; } +.review-comment span, .review-comment b { display:block; } +.review-comment span { color:var(--muted); font-size:8px; } +.review-comment b { margin-top:5px; color:#515a6c; font-size:10px; } +.folio-actions { display:flex; gap:8px; } +.folio-actions .el-button + .el-button { margin-left:0; } +.review-target { margin-bottom: 18px; padding: 16px 18px; border-left: 3px solid var(--teal); background:#f5f7fa; } +.review-target span { color:var(--teal); font:700 9px Consolas,monospace; } +.review-target b { display:block; margin:6px 0 4px; font-size:14px; } +.review-target p { margin:0; color:var(--muted); font-size:9px; } + +.graduation-batch-strip { padding:10px; display:flex; gap:9px; overflow-x:auto; border:1px solid var(--line); background:white; } +.graduation-batch-strip button { flex:0 0 290px; padding:14px; display:grid; grid-template-columns:1fr auto; gap:6px; text-align:left; border:1px solid var(--line); background:#fafbfc; } +.graduation-batch-strip button.active { color:white; border-color:var(--indigo); background:var(--indigo); } +.graduation-batch-strip b { grid-column:1/-1; font:600 15px "STZhongsong","Songti SC",serif; } +.graduation-batch-strip span,.graduation-batch-strip small,.graduation-batch-strip i { color:#8b93a2; font-size:9px; font-style:normal; } +.graduation-batch-strip button.active span,.graduation-batch-strip button.active small { color:#c5cee5; } +.graduation-workbench { border:1px solid var(--line); background:white; } +.graduation-workbench > header { padding:21px 24px; display:flex; align-items:center; justify-content:space-between; gap:20px; border-bottom:1px solid var(--line); } +.graduation-workbench header span { color:var(--teal); font:700 9px Consolas,monospace; letter-spacing:.13em; } +.graduation-workbench header h3 { margin:7px 0 5px; font:600 21px "STZhongsong","Songti SC",serif; } +.graduation-workbench header p { margin:0; color:var(--muted); font-size:9px; } +.graduation-summary { display:grid; grid-template-columns:repeat(4,1fr); color:white; background:linear-gradient(110deg,#172958,#273e7c 70%,#176c70); } +.graduation-summary > div { min-height:82px; padding:17px 21px; border-right:1px solid rgba(255,255,255,.12); } +.graduation-summary span { display:block; color:#aeb8d7; font-size:8px; } +.graduation-summary b { display:inline-block; margin-top:10px; color:#66d9ca; font:700 24px/1 Consolas,monospace; } +.graduation-summary small { margin-left:5px; color:#bfc8df; font-size:8px; } +.graduation-filter { padding:13px 18px; display:flex; align-items:center; gap:9px; border-bottom:1px solid var(--line); background:#fafbfc; } +.graduation-filter .el-input { width:300px; } +.graduation-filter .el-select { width:155px; } +.graduation-filter > span { margin-left:auto; color:var(--muted); font-size:9px; } +.graduation-result-list { padding:0 20px 18px; } +.graduation-result-list article { min-height:105px; display:grid; grid-template-columns:minmax(200px,1.2fr) 170px 185px 130px 70px; align-items:center; gap:20px; border-bottom:1px solid var(--line); } +.graduation-student span { color:var(--teal); font-size:8px; } +.graduation-student h4 { margin:6px 0 4px; font-size:14px; } +.graduation-student p { margin:0; color:var(--muted); font-size:8px; } +.credit-progress span,.course-clearance span { color:var(--muted); font-size:8px; } +.credit-progress b,.course-clearance b { display:block; margin:7px 0; color:var(--indigo); font:700 15px Consolas,monospace; } +.course-clearance { padding-left:17px; border-left:1px solid var(--line); } +.course-clearance small { display:block; max-width:180px; color:#8e5960; font-size:8px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } +.graduation-conclusion { padding:12px; display:grid; grid-template-columns:24px 1fr; gap:2px 7px; color:#9a555b; border:1px solid #ead7d9; background:#fffafa; } +.graduation-conclusion.eligible { color:#21796d; border-color:#cbe4df; background:#f6fbfa; } +.graduation-conclusion .el-icon { grid-row:1/3; align-self:center; font-size:20px; } +.graduation-conclusion span { font-size:9px; font-weight:700; } +.graduation-conclusion small { font-size:7px; opacity:.7; } +.graduation-certificate { overflow:hidden; border:1px solid var(--line); background:white; box-shadow:0 12px 35px rgba(24,41,85,.07); } +.graduation-certificate > header { min-height:82px; padding:21px 28px; display:flex; align-items:center; justify-content:space-between; color:white; background:linear-gradient(110deg,#172958,#263e7c 70%,#176c70); } +.graduation-certificate > header span { color:#73dfd1; font:700 10px Consolas,monospace; letter-spacing:.18em; } +.graduation-certificate > header i { font:normal 700 28px "STZhongsong","Songti SC",serif; } +.certificate-person { display:grid; grid-template-columns:repeat(3,1fr); border-bottom:1px solid var(--line); } +.certificate-person > div { padding:20px 28px; border-right:1px solid var(--line); } +.certificate-person span,.certificate-person b { display:block; } +.certificate-person span { color:var(--muted); font-size:8px; } +.certificate-person b { margin-top:7px; font-size:14px; } +.certificate-conclusion { min-height:145px; padding:28px; display:flex; align-items:center; justify-content:center; gap:20px; color:#a14e55; background:#fffafa; border-bottom:1px solid var(--line); } +.certificate-conclusion.eligible { color:#19796b; background:#f5fbf9; } +.certificate-conclusion .el-icon { font-size:48px; } +.certificate-conclusion span { font-size:9px; letter-spacing:.08em; } +.certificate-conclusion h3 { margin:7px 0 5px; font:600 27px "STZhongsong","Songti SC",serif; } +.certificate-conclusion p { margin:0; color:var(--muted); font-size:9px; } +.certificate-metrics { display:grid; grid-template-columns:repeat(3,1fr); } +.certificate-metrics > div { min-height:115px; padding:23px 28px; border-right:1px solid var(--line); } +.certificate-metrics span { display:block; color:var(--muted); font-size:8px; } +.certificate-metrics b { display:block; margin:10px 0; color:var(--indigo); font:700 22px Consolas,monospace; } +.certificate-metrics p { margin:0; color:var(--muted); font-size:8px; } +.graduation-certificate > footer { padding:20px 28px; display:grid; gap:13px; background:#fafbfc; } +.graduation-certificate > footer span,.graduation-certificate > footer b { display:block; } +.graduation-certificate > footer span { color:var(--muted); font-size:8px; } +.graduation-certificate > footer b { margin-top:5px; font-size:10px; } + +.degree-batch-strip { padding:10px; display:flex; gap:9px; overflow-x:auto; border:1px solid var(--line); background:white; } +.degree-batch-strip button { flex:0 0 290px; padding:14px; display:grid; grid-template-columns:1fr auto; gap:6px; text-align:left; border:1px solid var(--line); background:#fafbfc; } +.degree-batch-strip button.active { color:white; border-color:#8b692d; background:linear-gradient(120deg,#263968,#8b692d); } +.degree-batch-strip b { grid-column:1/-1; font:600 15px "STZhongsong","Songti SC",serif; } +.degree-batch-strip span,.degree-batch-strip small,.degree-batch-strip i { color:#8b93a2; font-size:9px; font-style:normal; } +.degree-batch-strip button.active span,.degree-batch-strip button.active small { color:#e3d8bd; } +.degree-workbench { border:1px solid var(--line); background:white; } +.degree-workbench > header { padding:21px 24px; display:flex; align-items:center; justify-content:space-between; gap:20px; border-bottom:1px solid var(--line); } +.degree-workbench header span { color:#a27b36; font:700 9px Consolas,monospace; letter-spacing:.13em; } +.degree-workbench header h3 { margin:7px 0 5px; font:600 21px "STZhongsong","Songti SC",serif; } +.degree-workbench header p { margin:0; color:var(--muted); font-size:9px; } +.degree-summary { display:grid; grid-template-columns:repeat(4,1fr); color:white; background:linear-gradient(110deg,#182957,#2d4178 67%,#8b692d); } +.degree-summary > div { min-height:82px; padding:17px 21px; border-right:1px solid rgba(255,255,255,.12); } +.degree-summary span { display:block; color:#c3c9dc; font-size:8px; } +.degree-summary b { display:block; margin-top:10px; color:#e5c87f; font:700 24px Consolas,monospace; } +.degree-result-list { padding:0 20px 18px; } +.degree-result-list article { min-height:105px; display:grid; grid-template-columns:minmax(190px,1.2fr) 150px minmax(180px,1fr) 125px 70px; align-items:center; gap:20px; border-bottom:1px solid var(--line); } +.degree-result-list article > div:first-child span { color:#a27b36; font-size:8px; } +.degree-result-list h4 { margin:6px 0 4px; font-size:14px; } +.degree-result-list p { margin:0; color:var(--muted); font-size:8px; } +.degree-result-list article > div:nth-child(2) span,.degree-rule-note span { color:var(--muted); font-size:8px; } +.degree-result-list article > div:nth-child(2) b { display:block; margin:7px 0; color:var(--indigo); font:700 20px Consolas,monospace; } +.degree-result-list article > div:nth-child(2) small { color:var(--muted); font-size:8px; } +.degree-rule-note { padding-left:16px; border-left:1px solid var(--line); } +.degree-rule-note b { display:block; margin-top:7px; color:#5f6879; font-size:9px; line-height:1.5; } +.degree-result-chip { padding:11px; display:grid; grid-template-columns:22px 1fr; color:#9a555b; border:1px solid #ead7d9; background:#fffafa; } +.degree-result-chip.granted { color:#7b622d; border-color:#dfd1aa; background:#fdfbf5; } +.degree-result-chip .el-icon { grid-row:1/3; align-self:center; font-size:17px; } +.degree-result-chip b { font-size:9px; } +.degree-result-chip small { font-size:7px; opacity:.7; } +.degree-certificate { min-height:380px; display:grid; grid-template-columns:220px 1fr; border:1px solid var(--line); background:white; box-shadow:0 12px 35px rgba(24,41,85,.07); } +.degree-seal { display:grid; place-content:center; gap:16px; text-align:center; color:#c7a75d; background:linear-gradient(150deg,#172858,#2b3f76 70%,#80622b); } +.degree-seal .el-icon { margin:auto; font-size:70px; } +.degree-seal span { font:700 9px Consolas,monospace; letter-spacing:.17em; } +.degree-copy { padding:45px 48px; } +.degree-copy > span { color:#a27b36; font:700 9px Consolas,monospace; letter-spacing:.12em; } +.degree-copy h3 { margin:15px 0 10px; color:#9c555b; font:600 34px "STZhongsong","Songti SC",serif; } +.degree-certificate.granted .degree-copy h3 { color:#80652c; } +.degree-copy > p { margin:0; color:#596274; font-size:12px; } +.degree-copy dl { margin:32px 0; display:grid; grid-template-columns:repeat(3,1fr); border:1px solid var(--line); } +.degree-copy dl div { padding:17px 20px; border-right:1px solid var(--line); } +.degree-copy dt { color:var(--muted); font-size:8px; } +.degree-copy dd { margin:7px 0 0; color:var(--indigo); font:700 16px Consolas,monospace; } +.degree-copy footer { padding-top:18px; border-top:1px solid var(--line); } +.degree-copy footer span,.degree-copy footer b { display:block; } +.degree-copy footer span { color:var(--muted); font-size:8px; } +.degree-copy footer b { margin-top:7px; font-size:10px; } + +.clearance-batch-strip { padding:10px; display:flex; gap:9px; overflow-x:auto; border:1px solid var(--line); background:white; } +.clearance-batch-strip button { flex:0 0 300px; padding:14px; display:grid; grid-template-columns:1fr auto; gap:6px; text-align:left; border:1px solid var(--line); background:#fafbfc; } +.clearance-batch-strip button.active { color:white; border-color:var(--indigo); background:linear-gradient(120deg,#182958,#234477 66%,#19736f); } +.clearance-batch-strip b { grid-column:1/-1; font:600 15px "STZhongsong","Songti SC",serif; } +.clearance-batch-strip span,.clearance-batch-strip small,.clearance-batch-strip i { color:#8b93a2; font-size:9px; font-style:normal; } +.clearance-batch-strip button.active span,.clearance-batch-strip button.active small { color:#c4d6dd; } +.clearance-workbench { border:1px solid var(--line); background:white; } +.clearance-workbench > header { padding:21px 24px; display:flex; align-items:center; justify-content:space-between; gap:20px; border-bottom:1px solid var(--line); } +.clearance-workbench header span { color:var(--teal); font:700 9px Consolas,monospace; letter-spacing:.13em; } +.clearance-workbench header h3 { margin:7px 0 5px; font:600 21px "STZhongsong","Songti SC",serif; } +.clearance-workbench header p { margin:0; color:var(--muted); font-size:9px; } +.clearance-item-legend { display:flex; overflow-x:auto; color:white; background:linear-gradient(105deg,#182958,#29417c 70%,#19736f); } +.clearance-item-legend > div { flex:1 0 180px; min-height:80px; padding:16px 20px; border-right:1px solid rgba(255,255,255,.12); } +.clearance-item-legend span,.clearance-item-legend b,.clearance-item-legend small { display:block; } +.clearance-item-legend span { color:#64d7c8; font:700 8px Consolas,monospace; } +.clearance-item-legend b { margin:7px 0 5px; font-size:10px; } +.clearance-item-legend small { color:#bfc9e1; font-size:8px; } +.clearance-ledgers { padding:18px; display:grid; gap:14px; background:#f5f7fa; } +.clearance-ledgers > article { border:1px solid var(--line); background:white; } +.clearance-ledgers article > header { padding:16px 20px; display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid var(--line); } +.clearance-ledgers header span { color:var(--teal); font-size:8px; } +.clearance-ledgers header h4 { margin:5px 0 3px; font-size:14px; } +.clearance-ledgers header p { margin:0; color:var(--muted); font-size:8px; } +.clearance-ledgers header > b { color:var(--indigo); font:700 22px Consolas,monospace; } +.clearance-record-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); } +.clearance-record-grid > div { min-height:125px; padding:18px; display:grid; grid-template-columns:24px 1fr; align-content:start; border-right:1px solid var(--line); border-bottom:1px solid var(--line); } +.clearance-record-grid .el-icon { grid-row:1/4; color:#bbc1cc; font-size:18px; } +.clearance-record-grid > div.done .el-icon { color:var(--teal); } +.clearance-record-grid span { color:var(--muted); font-size:8px; } +.clearance-record-grid b { margin:6px 0 4px; font-size:10px; } +.clearance-record-grid small { color:#a06a6e; font-size:8px; } +.clearance-record-grid > div.done small { color:#318276; } +.clearance-record-grid > div > div { grid-column:1/-1; margin-top:13px; } +.clearance-record-grid .el-button + .el-button { margin-left:5px; } +.clearance-pass { border:1px solid var(--line); background:white; } +.clearance-pass > header { min-height:125px; padding:25px 28px; display:flex; align-items:center; justify-content:space-between; color:white; background:linear-gradient(115deg,#182958,#2a407c 68%,#19736f); } +.clearance-pass header span { color:#67d9ca; font:700 9px Consolas,monospace; letter-spacing:.14em; } +.clearance-pass header h3 { margin:9px 0 6px; font:600 24px "STZhongsong","Songti SC",serif; } +.clearance-pass header p { margin:0; color:#c1cce3; font-size:9px; } +.clearance-pass header > b { font:700 30px Consolas,monospace; } +.clearance-student-list { padding:12px 26px 25px; } +.clearance-student-list article { min-height:90px; display:grid; grid-template-columns:45px 1fr auto; align-items:center; gap:16px; position:relative; border-bottom:1px solid var(--line); } +.clearance-student-list article:not(:last-child)::after { content:""; position:absolute; left:17px; top:62px; bottom:-29px; width:1px; background:#d8dde6; } +.clearance-student-list i { width:35px; height:35px; display:grid; place-items:center; z-index:1; color:#8c94a3; border:1px solid #cfd5df; border-radius:50%; background:white; font:normal 700 10px Consolas,monospace; } +.clearance-student-list article.done i { color:white; border-color:var(--teal); background:var(--teal); } +.clearance-student-list article > div span { color:var(--teal); font-size:8px; } +.clearance-student-list h4 { margin:5px 0 3px; font-size:12px; } +.clearance-student-list p { margin:0; color:var(--muted); font-size:8px; } +.clearance-form-head { margin:10px 0; display:flex; align-items:center; justify-content:space-between; } +.clearance-form-list { display:grid; gap:8px; max-height:340px; overflow-y:auto; } +.clearance-form-list > div { display:grid; grid-template-columns:110px 1fr 1fr 130px 80px 45px; gap:8px; align-items:center; } + .login-page { min-height: 100vh; display: grid; grid-template-columns: minmax(440px, 1.2fr) minmax(420px, .8fr); background: white; } .login-story { min-height: 100vh; padding: 54px clamp(45px, 6vw, 90px); display: flex; flex-direction: column; color: white; background: linear-gradient(142deg, #13224d, #243a77 62%, #176b71); overflow: hidden; position: relative; } .login-story::before { content: ""; position: absolute; inset: 0; opacity: .28; background-image: linear-gradient(rgba(255,255,255,.06) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.06) 1px, transparent 1px); background-size: 46px 46px; } @@ -698,6 +913,48 @@ button { cursor: pointer; } .exam-staff { padding:10px 0 0; border-left:none; border-top:1px solid var(--line); } .exam-row-actions { grid-column:auto; padding:0; } .exam-ticket-grid { grid-template-columns:1fr; } + .status-identity { align-items:flex-start; flex-direction:column; } + .status-available { width:100%; min-width:0; } + .status-review-summary { grid-template-columns:repeat(3,1fr); } + .status-review-summary > div { min-height:86px; padding:16px; } + .status-review-summary p { grid-column:1/-1; padding:16px; border-top:1px solid rgba(255,255,255,.12); } + .status-folio-list article > header { grid-template-columns:1fr auto; gap:14px; } + .folio-number { grid-column:1/-1; display:block; padding:0 0 12px; border-right:none; border-bottom:1px dashed var(--line); } + .folio-number b { margin-left:10px; } + .folio-reason { grid-template-columns:1fr; gap:7px; } + .approval-track { grid-template-columns:1fr; gap:13px; } + .approval-track > div:not(:last-child)::after { left:17px; right:auto; top:31px; bottom:-14px; width:1px; height:auto; } + .status-folio-list article > footer { align-items:stretch; flex-direction:column; } + .folio-actions, .folio-actions .el-button { width:100%; } + .graduation-workbench > header { align-items:flex-start; flex-direction:column; } + .graduation-summary { grid-template-columns:1fr 1fr; } + .graduation-summary > div:nth-child(2) { border-right:none; } + .graduation-filter { flex-wrap:wrap; } + .graduation-filter .el-input { width:100%; } + .graduation-filter .el-select { flex:1; width:auto; } + .graduation-result-list article { padding:16px 0; grid-template-columns:1fr 1fr; gap:13px; } + .graduation-student { grid-column:1/-1; } + .graduation-result-list article > .el-button { justify-self:end; } + .certificate-person,.certificate-metrics { grid-template-columns:1fr; } + .certificate-person > div,.certificate-metrics > div { border-right:none; border-bottom:1px solid var(--line); } + .certificate-conclusion { justify-content:flex-start; padding:24px 20px; } + .degree-workbench > header { align-items:flex-start; flex-direction:column; } + .degree-summary { grid-template-columns:1fr 1fr; } + .degree-result-list article { padding:16px 0; grid-template-columns:1fr 1fr; gap:13px; } + .degree-result-list article > div:first-child { grid-column:1/-1; } + .degree-rule-note { padding-left:0; border-left:none; } + .degree-result-list article > .el-button { justify-self:end; } + .degree-certificate { grid-template-columns:1fr; } + .degree-seal { min-height:150px; } + .degree-seal .el-icon { font-size:48px; } + .degree-copy { padding:28px 22px; } + .degree-copy dl { grid-template-columns:1fr; } + .degree-copy dl div { border-right:none; border-bottom:1px solid var(--line); } + .clearance-workbench > header { align-items:flex-start; flex-direction:column; } + .clearance-ledgers { padding:10px; } + .clearance-record-grid { grid-template-columns:1fr; } + .clearance-pass > header { align-items:flex-start; gap:18px; } + .clearance-form-list > div { grid-template-columns:1fr 1fr; padding:10px; border:1px solid var(--line); } .el-drawer { width: 100% !important; } } diff --git a/web/src/views/DashboardView.vue b/web/src/views/DashboardView.vue index 975e6e3..ba59d90 100644 --- a/web/src/views/DashboardView.vue +++ b/web/src/views/DashboardView.vue @@ -122,13 +122,33 @@ onMounted(async () => { {{ data.counts.examPlans ?? 0 }} 个计划 · {{ data.counts.examSessions ?? 0 }} 个场次 已建立 +
+ 学籍异动 + {{ data.counts.studentStatusChanges ?? 0 }} 项申请 · {{ data.counts.pendingStudentStatusChanges ?? 0 }} 项审核中 + 已建立 +
+
+ 毕业审核 + {{ data.counts.graduationAuditBatches ?? 0 }} 个批次 · {{ data.counts.publishedGraduationAuditBatches ?? 0 }} 个已发布 + 已建立 +
+
+ 学位授予 + {{ data.counts.degreeAwardBatches ?? 0 }} 个批次 · {{ data.counts.publishedDegreeAwardBatches ?? 0 }} 个已发布 + 已建立 +
+
+ 毕业离校 + {{ data.counts.graduationClearanceBatches ?? 0 }} 个批次 · {{ data.counts.openGraduationClearanceBatches ?? 0 }} 个办理中 + 已建立 +
NEXT MILESTONE

下一段业务链

-

考试计划、考场、监考与个人日程已就绪,下一步进入学籍异动与毕业审核。

+

毕业离校事项配置、责任角色分工、学生进度与批次关闭已就绪,核心教务业务链已形成闭环。

基础底座 人员档案 @@ -137,6 +157,10 @@ onMounted(async () => { 学生选课 成绩管理 考试考场 + 学籍异动 + 毕业审核 + 学位授予 + 毕业离校
diff --git a/web/src/views/DegreeAwardsView.vue b/web/src/views/DegreeAwardsView.vue new file mode 100644 index 0000000..de9e220 --- /dev/null +++ b/web/src/views/DegreeAwardsView.vue @@ -0,0 +1,201 @@ + + + diff --git a/web/src/views/GraduationAuditsView.vue b/web/src/views/GraduationAuditsView.vue new file mode 100644 index 0000000..1e01ff7 --- /dev/null +++ b/web/src/views/GraduationAuditsView.vue @@ -0,0 +1,248 @@ + + + diff --git a/web/src/views/GraduationClearanceView.vue b/web/src/views/GraduationClearanceView.vue new file mode 100644 index 0000000..510bc4c --- /dev/null +++ b/web/src/views/GraduationClearanceView.vue @@ -0,0 +1,252 @@ + + + diff --git a/web/src/views/StudentStatusChangesView.vue b/web/src/views/StudentStatusChangesView.vue new file mode 100644 index 0000000..23516e2 --- /dev/null +++ b/web/src/views/StudentStatusChangesView.vue @@ -0,0 +1,282 @@ + + +