From d7cf3f9e7624f4644ea26606f8b200adea669ccd Mon Sep 17 00:00:00 2001 From: biss Date: Fri, 24 Jul 2026 18:56:25 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=BE=E7=A8=8B=E5=BA=93=20Excel=20=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E5=AF=BC=E5=87=BA=E5=B7=B2=E5=AE=8C=E6=88=90=EF=BC=9A?= =?UTF-8?q?=20=E6=94=AF=E6=8C=81=E4=B8=8B=E8=BD=BD=E6=A0=87=E5=87=86?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=E6=A8=A1=E6=9D=BF=E3=80=82=20=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E4=B8=A5=E6=A0=BC=E6=B2=BF=E7=94=A8=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E5=85=B3=E9=94=AE=E8=AF=8D=E3=80=81=E5=AD=A6=E9=99=A2=E3=80=81?= =?UTF-8?q?=E5=88=86=E7=B1=BB=E3=80=81=E8=AF=BE=E7=A8=8B=E6=80=A7=E8=B4=A8?= =?UTF-8?q?=E5=92=8C=E7=8A=B6=E6=80=81=E7=AD=9B=E9=80=89=E3=80=82=20?= =?UTF-8?q?=E4=BB=A5=E8=AF=BE=E7=A8=8B=E7=BC=96=E7=A0=81=E4=B8=BA=E5=94=AF?= =?UTF-8?q?=E4=B8=80=E9=94=AE=EF=BC=9A=E5=AD=98=E5=9C=A8=E5=88=99=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=EF=BC=8C=E4=B8=8D=E5=AD=98=E5=9C=A8=E5=88=99=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=82=20=E6=A0=A1=E9=AA=8C=E5=AD=A6=E9=99=A2?= =?UTF-8?q?=E7=BC=96=E7=A0=81=E3=80=81=E8=AF=BE=E7=A8=8B=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E3=80=81=E8=AF=BE=E7=A8=8B=E6=80=A7=E8=B4=A8=E3=80=81=E5=AD=A6?= =?UTF-8?q?=E5=88=86=E5=AD=A6=E6=97=B6=E5=8F=8A=E8=80=83=E6=A0=B8=E6=96=B9?= =?UTF-8?q?=E5=BC=8F=E3=80=82=20=E6=95=B4=E6=89=B9=E5=8E=9F=E5=AD=90?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=EF=BC=8C=E4=BB=BB=E4=B8=80=E8=A1=8C=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=88=99=E5=85=A8=E9=83=A8=E4=B8=8D=E5=86=99=E5=85=A5?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E6=8F=90=E7=A4=BA=E5=85=B7=E4=BD=93=E8=A1=8C?= =?UTF-8?q?=E5=8F=B7=E3=80=82=20=E5=AD=A6=E9=99=A2=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E4=BB=8D=E5=8F=AA=E8=83=BD=E7=BB=B4=E6=8A=A4=E6=9C=AC?= =?UTF-8?q?=E5=AD=A6=E9=99=A2=E7=9A=84=E4=B8=93=E4=B8=9A=E8=AF=BE=E5=92=8C?= =?UTF-8?q?=E5=AE=9E=E8=B7=B5=E8=AF=BE=EF=BC=8C=E6=97=A0=E6=B3=95=E5=80=9F?= =?UTF-8?q?=20Excel=20=E8=B6=8A=E6=9D=83=E3=80=82=20=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E6=B2=BF=E7=94=A8=E7=8E=B0=E6=9C=89=E8=AF=BE?= =?UTF-8?q?=E7=A8=8B=E5=BA=93=E5=B7=A5=E5=85=B7=E6=A0=8F=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/smoke-test.ps1 | 38 + .../Controllers/BaseDataController.cs | 39 + .../Controllers/BaseDataExcelController.cs | 40 +- .../Controllers/CoursesController.cs | 13 + .../Controllers/CoursesExcelController.cs | 482 ++++ .../Controllers/CurriculumPlansController.cs | 3 + .../Controllers/GraduationAuditsController.cs | 22 +- .../Academic/PeopleAndCourseEntities.cs | 6 + .../Graduation/CurriculumCompletionRules.cs | 57 + .../Persistence/AppDbContext.cs | 7 + .../Persistence/DatabaseInitializer.cs | 57 +- .../Persistence/DevelopmentSqliteMigrator.cs | 47 + ...0260724104203_CourseCategories.Designer.cs | 2541 +++++++++++++++++ .../MySql/20260724104203_CourseCategories.cs | 82 + .../MySql/AppDbContextModelSnapshot.cs | 50 + .../GraduationAuditRulesTests.cs | 63 + tests/Jiaowu.Api.Tests/PersistenceTests.cs | 13 +- web/src/layouts/AdminLayout.vue | 4 + web/src/router/index.ts | 9 + web/src/style.css | 8 + web/src/views/BaseDataView.vue | 9 +- web/src/views/CoursesView.vue | 119 +- web/src/views/CurriculumView.vue | 29 +- web/src/views/GraduationAuditsView.vue | 4 +- 24 files changed, 3710 insertions(+), 32 deletions(-) create mode 100644 src/Jiaowu.Api/Controllers/CoursesExcelController.cs create mode 100644 src/Jiaowu.Api/Infrastructure/Graduation/CurriculumCompletionRules.cs create mode 100644 src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724104203_CourseCategories.Designer.cs create mode 100644 src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724104203_CourseCategories.cs diff --git a/scripts/smoke-test.ps1 b/scripts/smoke-test.ps1 index a1b8929..2e16e70 100644 --- a/scripts/smoke-test.ps1 +++ b/scripts/smoke-test.ps1 @@ -20,6 +20,9 @@ else { } $stdoutPath = Join-Path $env:TEMP 'jiaowu-api-smoke.out.log' $stderrPath = Join-Path $env:TEMP 'jiaowu-api-smoke.err.log' +$courseExcelToken = [guid]::NewGuid().ToString('N') +$courseTemplatePath = Join-Path $env:TEMP "jiaowu-course-template-$courseExcelToken.xlsx" +$courseExportPath = Join-Path $env:TEMP "jiaowu-course-export-$courseExcelToken.xlsx" $env:ASPNETCORE_ENVIRONMENT = 'Development' $env:ASPNETCORE_URLS = 'http://localhost:5255' $startParameters = @{ @@ -78,6 +81,37 @@ try { $teachers = Invoke-RestMethod -Uri 'http://localhost:5255/api/personnel/teachers?page=1&pageSize=10' -Headers $headers $students = Invoke-RestMethod -Uri 'http://localhost:5255/api/personnel/students?page=1&pageSize=10' -Headers $headers $courses = Invoke-RestMethod -Uri 'http://localhost:5255/api/courses?page=1&pageSize=10' -Headers $headers + $courseCategories = Invoke-RestMethod ` + -Uri 'http://localhost:5255/api/base-data/course-categories' ` + -Headers $headers + if (@($courseCategories).Count -lt 10 -or + @($courses.items | Where-Object { $null -eq $_.courseCategoryId }).Count -gt 0) { + throw 'Course categories were not seeded or assigned to all courses.' + } + Invoke-WebRequest ` + -Uri 'http://localhost:5255/api/courses/template' ` + -Headers $headers ` + -OutFile $courseTemplatePath | + Out-Null + Invoke-WebRequest ` + -Uri 'http://localhost:5255/api/courses/export' ` + -Headers $headers ` + -OutFile $courseExportPath | + Out-Null + if ((Get-Item -LiteralPath $courseTemplatePath).Length -lt 1 -or + (Get-Item -LiteralPath $courseExportPath).Length -lt 1) { + throw 'Course Excel template or export is empty.' + } + $courseImport = Invoke-RestMethod ` + -Method Post ` + -Uri 'http://localhost:5255/api/courses/import' ` + -Headers $headers ` + -Form @{ file = Get-Item -LiteralPath $courseExportPath } + if ($courseImport.created -ne 0 -or + $courseImport.updated -ne $courses.total -or + $courseImport.total -ne $courses.total) { + throw 'Course Excel round-trip did not update the expected records.' + } $curriculumPlans = Invoke-RestMethod -Uri 'http://localhost:5255/api/curriculum-plans?page=1&pageSize=10' -Headers $headers if ($curriculumPlans.total -gt 0) { $curriculumDetail = Invoke-RestMethod ` @@ -757,6 +791,8 @@ try { Teachers = $teachers.total Students = $students.total Courses = $courses.total + CourseCategories = @($courseCategories).Count + CourseExcelRows = $courseImport.total Plans = $curriculumPlans.total PlanModules = if ($null -ne $curriculumDetail) { @($curriculumDetail.modules).Count } else { 0 } TeachingTasks = $teachingTasks.total @@ -790,4 +826,6 @@ finally { if (-not $process.HasExited) { Stop-Process -Id $process.Id -Force } + Remove-Item -LiteralPath $courseTemplatePath -Force -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $courseExportPath -Force -ErrorAction SilentlyContinue } diff --git a/src/Jiaowu.Api/Controllers/BaseDataController.cs b/src/Jiaowu.Api/Controllers/BaseDataController.cs index e155aef..3958254 100644 --- a/src/Jiaowu.Api/Controllers/BaseDataController.cs +++ b/src/Jiaowu.Api/Controllers/BaseDataController.cs @@ -120,6 +120,44 @@ public sealed class BaseDataController(AppDbContext db) : ControllerBase }) .ToListAsync(cancellationToken)); + [HttpGet("course-categories")] + public async Task>> GetCourseCategories( + CancellationToken cancellationToken) => + await db.CourseCategories.AsNoTracking() + .OrderBy(x => x.SortOrder) + .ThenBy(x => x.Code) + .ToListAsync(cancellationToken); + + [HttpPost("course-categories")] + [Authorize(Roles = Administrators)] + public async Task> CreateCourseCategory( + CatalogRequest request, + CancellationToken cancellationToken) + { + var entity = new CourseCategory + { + Code = request.Code.Trim(), + Name = request.Name.Trim(), + SortOrder = request.SortOrder, + IsEnabled = request.IsEnabled + }; + return await CreateAsync(entity, nameof(GetCourseCategories), cancellationToken); + } + + [HttpPut("course-categories/{id:guid}")] + [Authorize(Roles = Administrators)] + public async Task> UpdateCourseCategory( + Guid id, + CatalogRequest request, + CancellationToken cancellationToken) + { + var entity = await db.CourseCategories.FindAsync([id], cancellationToken); + if (entity is null) return NotFound(); + ApplyCatalog(entity, request); + await db.SaveChangesAsync(cancellationToken); + return entity; + } + [HttpPost("majors")] [Authorize(Roles = Administrators)] public async Task> CreateMajor( @@ -403,6 +441,7 @@ public sealed class BaseDataController(AppDbContext db) : ControllerBase "terms" => await db.AcademicTerms.FindAsync([id], cancellationToken), "buildings" => await db.Buildings.FindAsync([id], cancellationToken), "classrooms" => await db.Classrooms.FindAsync([id], cancellationToken), + "course-categories" => await db.CourseCategories.FindAsync([id], cancellationToken), _ => null }; diff --git a/src/Jiaowu.Api/Controllers/BaseDataExcelController.cs b/src/Jiaowu.Api/Controllers/BaseDataExcelController.cs index 61c7a53..d2ab7fd 100644 --- a/src/Jiaowu.Api/Controllers/BaseDataExcelController.cs +++ b/src/Jiaowu.Api/Controllers/BaseDataExcelController.cs @@ -26,7 +26,8 @@ public sealed class BaseDataExcelController(AppDbContext db) : ControllerBase ["classes"] = ["编码", "名称", "所属专业编码", "年级", "辅导员工号", "排序", "状态"], ["terms"] = ["编码", "名称", "学年", "学期季", "开始日期", "结束日期", "当前学期", "状态"], ["buildings"] = ["编码", "名称", "所属校区编码", "排序", "状态"], - ["classrooms"] = ["编码", "名称", "所属教学楼编码", "容量", "教室类型", "设备", "排序", "状态"] + ["classrooms"] = ["编码", "名称", "所属教学楼编码", "容量", "教室类型", "设备", "排序", "状态"], + ["course-categories"] = ["编码", "名称", "排序", "状态"] }; [HttpGet("{kind}/template")] @@ -90,6 +91,8 @@ public sealed class BaseDataExcelController(AppDbContext db) : ControllerBase "terms" => await ImportTermsAsync(rows, errors, cancellationToken), "buildings" => await ImportBuildingsAsync(rows, errors, cancellationToken), "classrooms" => await ImportClassroomsAsync(rows, errors, cancellationToken), + "course-categories" => await ImportCourseCategoriesAsync( + rows, errors, cancellationToken), _ => throw new InvalidOperationException() }; @@ -147,6 +150,10 @@ public sealed class BaseDataExcelController(AppDbContext db) : ControllerBase .OrderBy(x => x.Code).ToListAsync(cancellationToken)) .Select(x => Row(x.Code, x.Name, x.Building!.Code, x.Capacity, x.RoomType, x.Equipment, x.SortOrder, Status(x.IsEnabled))).ToList(), + "course-categories" => (await db.CourseCategories.AsNoTracking() + .OrderBy(x => x.SortOrder).ThenBy(x => x.Code) + .ToListAsync(cancellationToken)) + .Select(x => Row(x.Code, x.Name, x.SortOrder, Status(x.IsEnabled))).ToList(), _ => [] }; @@ -471,6 +478,36 @@ public sealed class BaseDataExcelController(AppDbContext db) : ControllerBase return new(created, updated, rows.Count); } + private async Task ImportCourseCategoriesAsync( + IReadOnlyList rows, + List errors, + CancellationToken cancellationToken) + { + var existing = await db.CourseCategories.ToDictionaryAsync( + x => x.Code, + StringComparer.OrdinalIgnoreCase, + cancellationToken); + var created = 0; + var updated = 0; + foreach (var row in rows) + { + var code = Required(row, "编码", errors); + var name = Required(row, "名称", errors); + if (code is null || name is null) continue; + var entity = existing.GetValueOrDefault(code); + if (entity is null) + { + entity = new CourseCategory { Code = code, Name = name }; + db.CourseCategories.Add(entity); + existing[code] = entity; + created++; + } + else updated++; + ApplyCatalog(entity, row, name, errors); + } + return new(created, updated, rows.Count); + } + private ActionResult ImportValidationProblem(IReadOnlyList errors) { foreach (var error in errors.Take(50)) @@ -582,6 +619,7 @@ public sealed class BaseDataExcelController(AppDbContext db) : ControllerBase "terms" => "学期", "buildings" => "教学楼", "classrooms" => "教室", + "course-categories" => "课程分类", _ => "基础数据" }; } diff --git a/src/Jiaowu.Api/Controllers/CoursesController.cs b/src/Jiaowu.Api/Controllers/CoursesController.cs index c77adc1..c7b58d5 100644 --- a/src/Jiaowu.Api/Controllers/CoursesController.cs +++ b/src/Jiaowu.Api/Controllers/CoursesController.cs @@ -28,6 +28,7 @@ public sealed class CoursesController( int pageSize = 20, string? keyword = null, Guid? collegeId = null, + Guid? categoryId = null, CourseNature? nature = null, bool? isEnabled = null, CancellationToken cancellationToken = default) @@ -42,6 +43,8 @@ public sealed class CoursesController( var source = ScopedCourses().AsNoTracking(); if (collegeId.HasValue) source = source.Where(x => x.CollegeId == collegeId.Value); + if (categoryId.HasValue) + source = source.Where(x => x.CourseCategoryId == categoryId.Value); if (nature.HasValue) source = source.Where(x => x.Nature == nature.Value); if (isEnabled.HasValue) @@ -69,6 +72,9 @@ public sealed class CoursesController( x.EnglishName, x.CollegeId, CollegeName = x.College!.Name, + x.CourseCategoryId, + CategoryCode = x.CourseCategory != null ? x.CourseCategory.Code : null, + CategoryName = x.CourseCategory != null ? x.CourseCategory.Name : null, x.Credits, x.TotalHours, x.LectureHours, @@ -106,6 +112,7 @@ public sealed class CoursesController( Name = request.Name.Trim(), EnglishName = Normalize(request.EnglishName), CollegeId = request.CollegeId, + CourseCategoryId = request.CourseCategoryId, Credits = request.Credits, TotalHours = request.TotalHours, LectureHours = request.LectureHours, @@ -137,6 +144,7 @@ public sealed class CoursesController( entity.Name = request.Name.Trim(); entity.EnglishName = Normalize(request.EnglishName); entity.CollegeId = request.CollegeId; + entity.CourseCategoryId = request.CourseCategoryId; entity.Credits = request.Credits; entity.TotalHours = request.TotalHours; entity.LectureHours = request.LectureHours; @@ -167,6 +175,10 @@ public sealed class CoursesController( if (!CanManage(request.CollegeId, request.Nature)) return Forbid(); if (!await db.Colleges.AnyAsync(x => x.Id == request.CollegeId, cancellationToken)) return ValidationProblem("所选学院不存在。"); + if (!await db.CourseCategories.AnyAsync( + x => x.Id == request.CourseCategoryId && x.IsEnabled, + cancellationToken)) + return ValidationProblem("所选课程分类不存在或已停用。"); if (request.LectureHours + request.PracticeHours > request.TotalHours) return ValidationProblem("讲授学时与实践学时之和不能超过总学时。"); return null; @@ -242,6 +254,7 @@ public sealed record CourseRequest( [Required, MaxLength(100)] string Name, [MaxLength(150)] string? EnglishName, Guid CollegeId, + Guid CourseCategoryId, [Range(typeof(decimal), "0.1", "99")] decimal Credits, [Range(1, 1000)] int TotalHours, [Range(0, 1000)] int LectureHours, diff --git a/src/Jiaowu.Api/Controllers/CoursesExcelController.cs b/src/Jiaowu.Api/Controllers/CoursesExcelController.cs new file mode 100644 index 0000000..d8df342 --- /dev/null +++ b/src/Jiaowu.Api/Controllers/CoursesExcelController.cs @@ -0,0 +1,482 @@ +using System.Globalization; +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Infrastructure.Auth; +using Jiaowu.Api.Infrastructure.Excel; +using Jiaowu.Api.Infrastructure.Persistence; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Jiaowu.Api.Controllers; + +[ApiController] +[Authorize] +[Route("api/courses")] +public sealed class CoursesExcelController( + AppDbContext db, + ICurrentUserDataScope currentUserDataScope) : ControllerBase +{ + private const string WriteRoles = + SystemRoles.SuperAdmin + "," + + SystemRoles.AcademicAdmin + "," + + SystemRoles.CollegeAdmin; + + private static readonly string[] Headers = + [ + "课程编码", "课程名称", "英文名称", "开课学院编码", "课程分类编码", + "课程性质", "学分", "总学时", "讲授学时", "实践学时", + "考核方式", "课程简介", "启用状态", "排序" + ]; + + [HttpGet("template")] + [Authorize(Roles = WriteRoles)] + public IActionResult DownloadTemplate() + { + var bytes = ExcelWorkbookHelper.Create( + "课程库导入", + Headers, + [], + [ + "请勿修改第一行列名;每行填写一门课程,课程编码是全校唯一标识。", + "开课学院编码、课程分类编码必须已在系统中存在;课程分类还必须处于启用状态。", + "课程性质请填写:通识必修、专业必修、专业选修、通识选修或实践课程。", + "考核方式请填写考试或考查;启用状态请填写是或否。", + "课程编码已存在时更新课程,不存在时新增课程。", + "学院管理员只能维护本学院的专业必修、专业选修和实践课程。", + "讲授学时与实践学时之和不能超过总学时;整批任一行有误时均不会写入。" + ]); + return File( + bytes, + ExcelWorkbookHelper.ContentType, + "课程库导入模板.xlsx"); + } + + [HttpGet("export")] + public async Task Export( + string? keyword = null, + Guid? collegeId = null, + Guid? categoryId = null, + CourseNature? nature = null, + bool? isEnabled = null, + CancellationToken cancellationToken = default) + { + var source = ApplyFilters( + ScopedCourses().AsNoTracking(), + keyword, + collegeId, + categoryId, + nature, + isEnabled); + var courses = await source + .Include(x => x.College) + .Include(x => x.CourseCategory) + .OrderBy(x => x.SortOrder) + .ThenBy(x => x.Code) + .ToListAsync(cancellationToken); + var rows = courses + .Select(x => Row( + x.Code, + x.Name, + x.EnglishName, + x.College!.Code, + x.CourseCategory?.Code, + NatureName(x.Nature), + x.Credits, + x.TotalHours, + x.LectureHours, + x.PracticeHours, + AssessmentName(x.AssessmentMethod), + x.Description, + x.IsEnabled ? "是" : "否", + x.SortOrder)) + .ToList(); + + var bytes = ExcelWorkbookHelper.Create("课程库", Headers, rows); + return File( + bytes, + ExcelWorkbookHelper.ContentType, + $"课程库-{DateTime.Now:yyyyMMdd-HHmm}.xlsx"); + } + + [HttpPost("import")] + [Authorize(Roles = WriteRoles)] + [RequestSizeLimit(10 * 1024 * 1024)] + public async Task> Import( + IFormFile file, + CancellationToken cancellationToken) + { + IReadOnlyList rows; + try + { + rows = await ExcelWorkbookHelper.ReadAsync( + file, + Headers, + cancellationToken); + } + catch (InvalidDataException exception) + { + return ValidationProblem(exception.Message); + } + + if (rows.Count == 0) + return ValidationProblem("Excel 中没有可导入的课程数据。"); + + var errors = new List(); + await using var transaction = + await db.Database.BeginTransactionAsync(cancellationToken); + try + { + var result = await ImportRowsAsync(rows, errors, cancellationToken); + if (errors.Count > 0) + { + await transaction.RollbackAsync(cancellationToken); + return ImportValidationProblem(errors); + } + + await db.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + return Ok(result); + } + catch (DbUpdateException) + { + await transaction.RollbackAsync(cancellationToken); + return Conflict(new ProblemDetails + { + Title = "导入失败", + Detail = "存在重复课程编码或无效关联,未写入任何课程。", + Status = StatusCodes.Status409Conflict + }); + } + } + + private async Task ImportRowsAsync( + IReadOnlyList rows, + List errors, + CancellationToken cancellationToken) + { + var existing = await db.Courses.ToDictionaryAsync( + x => x.Code, + StringComparer.OrdinalIgnoreCase, + cancellationToken); + var colleges = await db.Colleges.AsNoTracking().ToDictionaryAsync( + x => x.Code, + StringComparer.OrdinalIgnoreCase, + cancellationToken); + var categories = await db.CourseCategories.AsNoTracking().ToDictionaryAsync( + x => x.Code, + StringComparer.OrdinalIgnoreCase, + cancellationToken); + var importedCodes = new HashSet(StringComparer.OrdinalIgnoreCase); + var created = 0; + var updated = 0; + + foreach (var row in rows) + { + var code = Required(row, "课程编码", 40, errors); + var name = Required(row, "课程名称", 100, errors); + var englishName = Optional(row, "英文名称", 150, errors); + var collegeCode = Required(row, "开课学院编码", 40, errors); + var categoryCode = Required(row, "课程分类编码", 40, errors); + var nature = ParseNature(row, errors); + var credits = ParseDecimal(row, "学分", 0.1m, 99m, errors); + var totalHours = ParseInt(row, "总学时", 1, 1000, errors); + var lectureHours = ParseInt(row, "讲授学时", 0, 1000, errors); + var practiceHours = ParseInt(row, "实践学时", 0, 1000, errors); + var assessment = ParseAssessment(row, errors); + var description = Optional(row, "课程简介", 1000, errors); + var enabled = ParseEnabled(row, errors); + var sortOrder = ParseInt(row, "排序", 0, int.MaxValue, errors); + + if (code is null || name is null || collegeCode is null || + categoryCode is null || nature is null || credits is null || + totalHours is null || lectureHours is null || + practiceHours is null || assessment is null || + enabled is null || sortOrder is null) + continue; + + if (!importedCodes.Add(code)) + { + errors.Add($"第 {row.RowNumber} 行:课程编码“{code}”在文件中重复。"); + continue; + } + if (!colleges.TryGetValue(collegeCode, out var college)) + { + errors.Add($"第 {row.RowNumber} 行:开课学院编码“{collegeCode}”不存在。"); + continue; + } + if (!categories.TryGetValue(categoryCode, out var category)) + { + errors.Add($"第 {row.RowNumber} 行:课程分类编码“{categoryCode}”不存在。"); + continue; + } + if (!category.IsEnabled) + { + errors.Add($"第 {row.RowNumber} 行:课程分类“{category.Name}”已停用。"); + continue; + } + if (lectureHours.Value + practiceHours.Value > totalHours.Value) + { + errors.Add( + $"第 {row.RowNumber} 行:讲授学时与实践学时之和不能超过总学时。"); + continue; + } + if (!CanManage(college.Id, nature.Value)) + { + errors.Add( + $"第 {row.RowNumber} 行:无权维护学院“{college.Name}”的“{NatureName(nature.Value)}”课程。"); + continue; + } + + var entity = existing.GetValueOrDefault(code); + if (entity is not null && !CanManage(entity.CollegeId, entity.Nature)) + { + errors.Add( + $"第 {row.RowNumber} 行:课程编码“{code}”已属于无权维护的课程。"); + continue; + } + + if (entity is null) + { + entity = new Course + { + Code = code, + Name = name, + CollegeId = college.Id + }; + db.Courses.Add(entity); + existing[code] = entity; + created++; + } + else + { + updated++; + } + + entity.Name = name; + entity.EnglishName = englishName; + entity.CollegeId = college.Id; + entity.CourseCategoryId = category.Id; + entity.Credits = credits.Value; + entity.TotalHours = totalHours.Value; + entity.LectureHours = lectureHours.Value; + entity.PracticeHours = practiceHours.Value; + entity.Nature = nature.Value; + entity.AssessmentMethod = assessment.Value; + entity.Description = description; + entity.IsEnabled = enabled.Value; + entity.SortOrder = sortOrder.Value; + } + + return new(created, updated, rows.Count); + } + + private IQueryable ScopedCourses() + { + var scope = currentUserDataScope.Current; + var source = db.Courses.AsQueryable(); + if (scope.Scope == DataScope.All) return source; + if (scope.Scope == DataScope.College) + { + return source.Where(x => + x.Nature == CourseNature.GeneralRequired || + x.Nature == CourseNature.GeneralElective || + x.CollegeId == scope.RestrictedCollegeId); + } + if (scope.Scope == DataScope.Class) + { + return source.Where(course => db.TeachingTasks.Any(task => + task.CourseId == course.Id && + task.Classes.Any(item => + item.AdministrativeClass!.CounselorUserId == scope.UserId))); + } + + var userId = scope.UserId; + var isTeacher = scope.IsInRole(SystemRoles.Teacher); + var isStudent = scope.IsInRole(SystemRoles.Student); + return source.Where(course => + isTeacher && db.TeachingTasks.Any(task => + task.CourseId == course.Id && + task.Teachers.Any(item => item.Teacher!.UserId == userId)) || + isStudent && db.TeachingTasks.Any(task => + task.CourseId == course.Id && + task.Classes.Any(item => + item.AdministrativeClass!.Students.Any(student => + student.UserId == userId)))); + } + + private static IQueryable ApplyFilters( + IQueryable source, + string? keyword, + Guid? collegeId, + Guid? categoryId, + CourseNature? nature, + bool? isEnabled) + { + if (collegeId.HasValue) + source = source.Where(x => x.CollegeId == collegeId.Value); + if (categoryId.HasValue) + source = source.Where(x => x.CourseCategoryId == categoryId.Value); + if (nature.HasValue) + source = source.Where(x => x.Nature == nature.Value); + if (isEnabled.HasValue) + source = source.Where(x => x.IsEnabled == isEnabled.Value); + if (!string.IsNullOrWhiteSpace(keyword)) + { + keyword = keyword.Trim(); + source = source.Where(x => + x.Code.Contains(keyword) || + x.Name.Contains(keyword) || + (x.EnglishName != null && x.EnglishName.Contains(keyword))); + } + return source; + } + + private bool CanManage(Guid collegeId, CourseNature nature) => + CourseMaintenancePolicy.CanManage( + currentUserDataScope.Current, + collegeId, + nature); + + private ActionResult ImportValidationProblem(IReadOnlyList errors) + { + foreach (var error in errors.Take(50)) + ModelState.AddModelError("file", error); + if (errors.Count > 50) + ModelState.AddModelError("file", $"另有 {errors.Count - 50} 条错误未显示。"); + return ValidationProblem(ModelState); + } + + private static string? Required( + ExcelRow row, + string header, + int maximumLength, + List errors) + { + var value = Optional(row, header, maximumLength, errors); + if (value is not null) return value; + if (string.IsNullOrWhiteSpace(row[header])) + errors.Add($"第 {row.RowNumber} 行:“{header}”不能为空。"); + return null; + } + + private static string? Optional( + ExcelRow row, + string header, + int maximumLength, + List errors) + { + var value = string.IsNullOrWhiteSpace(row[header]) + ? null + : row[header].Trim(); + if (value is null || value.Length <= maximumLength) return value; + errors.Add( + $"第 {row.RowNumber} 行:“{header}”不能超过 {maximumLength} 个字符。"); + return null; + } + + private static int? ParseInt( + ExcelRow row, + string header, + int minimum, + int maximum, + List errors) + { + if (int.TryParse( + row[header], + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out var value) && + value >= minimum && + value <= maximum) + return value; + errors.Add( + $"第 {row.RowNumber} 行:“{header}”必须是 {minimum} 至 {maximum} 的整数。"); + return null; + } + + private static decimal? ParseDecimal( + ExcelRow row, + string header, + decimal minimum, + decimal maximum, + List errors) + { + if (decimal.TryParse( + row[header], + NumberStyles.Number, + CultureInfo.InvariantCulture, + out var value) && + value >= minimum && + value <= maximum) + return value; + errors.Add( + $"第 {row.RowNumber} 行:“{header}”必须是 {minimum:0.##} 至 {maximum:0.##} 的数值。"); + return null; + } + + private static CourseNature? ParseNature(ExcelRow row, List errors) + { + var value = row["课程性质"]; + if (value == "通识必修" || + value.Equals(nameof(CourseNature.GeneralRequired), StringComparison.OrdinalIgnoreCase)) + return CourseNature.GeneralRequired; + if (value == "专业必修" || + value.Equals(nameof(CourseNature.MajorRequired), StringComparison.OrdinalIgnoreCase)) + return CourseNature.MajorRequired; + if (value == "专业选修" || + value.Equals(nameof(CourseNature.MajorElective), StringComparison.OrdinalIgnoreCase)) + return CourseNature.MajorElective; + if (value == "通识选修" || + value.Equals(nameof(CourseNature.GeneralElective), StringComparison.OrdinalIgnoreCase)) + return CourseNature.GeneralElective; + if (value is "实践" or "实践课程" || + value.Equals(nameof(CourseNature.Practice), StringComparison.OrdinalIgnoreCase)) + return CourseNature.Practice; + errors.Add( + $"第 {row.RowNumber} 行:“课程性质”请填写通识必修、专业必修、专业选修、通识选修或实践课程。"); + return null; + } + + private static AssessmentMethod? ParseAssessment( + ExcelRow row, + List errors) + { + var value = row["考核方式"]; + if (value == "考试" || + value.Equals(nameof(AssessmentMethod.Examination), StringComparison.OrdinalIgnoreCase)) + return AssessmentMethod.Examination; + if (value == "考查" || + value.Equals(nameof(AssessmentMethod.Assessment), StringComparison.OrdinalIgnoreCase)) + return AssessmentMethod.Assessment; + errors.Add($"第 {row.RowNumber} 行:“考核方式”请填写考试或考查。"); + return null; + } + + private static bool? ParseEnabled(ExcelRow row, List errors) + { + var value = row["启用状态"]; + if (value is "是" or "启用" or "1" || + value.Equals("true", StringComparison.OrdinalIgnoreCase)) + return true; + if (value is "否" or "停用" or "0" || + value.Equals("false", StringComparison.OrdinalIgnoreCase)) + return false; + errors.Add($"第 {row.RowNumber} 行:“启用状态”请填写是或否。"); + return null; + } + + private static IReadOnlyList Row(params object?[] values) => values; + + private static string NatureName(CourseNature value) => value switch + { + CourseNature.GeneralRequired => "通识必修", + CourseNature.MajorRequired => "专业必修", + CourseNature.MajorElective => "专业选修", + CourseNature.GeneralElective => "通识选修", + _ => "实践课程" + }; + + private static string AssessmentName(AssessmentMethod value) => + value == AssessmentMethod.Examination ? "考试" : "考查"; +} diff --git a/src/Jiaowu.Api/Controllers/CurriculumPlansController.cs b/src/Jiaowu.Api/Controllers/CurriculumPlansController.cs index 530a899..3e60a50 100644 --- a/src/Jiaowu.Api/Controllers/CurriculumPlansController.cs +++ b/src/Jiaowu.Api/Controllers/CurriculumPlansController.cs @@ -168,6 +168,9 @@ public sealed class CurriculumPlansController( course.Course.Credits, course.Course.TotalHours, course.Course.Nature, + CategoryName = course.Course.CourseCategory != null + ? course.Course.CourseCategory.Name + : null, course.RecommendedSemester, course.Type, course.Notes diff --git a/src/Jiaowu.Api/Controllers/GraduationAuditsController.cs b/src/Jiaowu.Api/Controllers/GraduationAuditsController.cs index b6a9e9b..06c4f9d 100644 --- a/src/Jiaowu.Api/Controllers/GraduationAuditsController.cs +++ b/src/Jiaowu.Api/Controllers/GraduationAuditsController.cs @@ -156,10 +156,6 @@ public sealed class GraduationAuditsController( { 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) @@ -170,14 +166,12 @@ public sealed class GraduationAuditsController( .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 completion = CurriculumCompletionRules.Evaluate( + plan?.Modules ?? [], + passedCourseIds); var missingCourseNames = plan is null ? "未匹配已发布的培养方案" - : string.Join("、", missingCourses); + : string.Join("、", completion.MissingRequirements); if (missingCourseNames.Length > 2000) missingCourseNames = missingCourseNames[..2000]; var failedCourseCount = studentGrades @@ -188,8 +182,8 @@ public sealed class GraduationAuditsController( student.Status, plan?.TotalCredits ?? 0, earnedCredits, - requiredCourses.Count, - requiredCourses.Count - missingCourses.Length, + completion.RequirementCount, + completion.PassedRequirementCount, failedCourseCount); db.GraduationAuditResults.Add(new GraduationAuditResult { @@ -199,8 +193,8 @@ public sealed class GraduationAuditsController( StudentStatusSnapshot = student.Status, RequiredCredits = plan?.TotalCredits ?? 0, EarnedCredits = earnedCredits, - RequiredCourseCount = requiredCourses.Count, - PassedRequiredCourseCount = requiredCourses.Count - missingCourses.Length, + RequiredCourseCount = completion.RequirementCount, + PassedRequiredCourseCount = completion.PassedRequirementCount, FailedCourseCount = failedCourseCount, MissingCourseNames = missingCourseNames, CalculatedConclusion = conclusion, diff --git a/src/Jiaowu.Api/Domain/Academic/PeopleAndCourseEntities.cs b/src/Jiaowu.Api/Domain/Academic/PeopleAndCourseEntities.cs index 8fc442b..e7a6f19 100644 --- a/src/Jiaowu.Api/Domain/Academic/PeopleAndCourseEntities.cs +++ b/src/Jiaowu.Api/Domain/Academic/PeopleAndCourseEntities.cs @@ -40,6 +40,8 @@ public sealed class Course : CatalogEntity { public Guid CollegeId { get; set; } public College? College { get; set; } + public Guid? CourseCategoryId { get; set; } + public CourseCategory? CourseCategory { get; set; } public string? EnglishName { get; set; } public decimal Credits { get; set; } public int TotalHours { get; set; } @@ -50,6 +52,10 @@ public sealed class Course : CatalogEntity public string? Description { get; set; } } +public sealed class CourseCategory : CatalogEntity +{ +} + public enum Gender { Unknown = 0, diff --git a/src/Jiaowu.Api/Infrastructure/Graduation/CurriculumCompletionRules.cs b/src/Jiaowu.Api/Infrastructure/Graduation/CurriculumCompletionRules.cs new file mode 100644 index 0000000..829d108 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Graduation/CurriculumCompletionRules.cs @@ -0,0 +1,57 @@ +using Jiaowu.Api.Domain.Academic; + +namespace Jiaowu.Api.Infrastructure.Graduation; + +public sealed record CurriculumCompletionSummary( + int RequirementCount, + int PassedRequirementCount, + IReadOnlyList MissingRequirements); + +public static class CurriculumCompletionRules +{ + public static CurriculumCompletionSummary Evaluate( + IEnumerable modules, + IReadOnlySet passedCourseIds) + { + var moduleList = modules.ToList(); + var requiredCourses = moduleList + .SelectMany(x => x.Courses) + .Where(x => x.Type == CurriculumCourseType.Required) + .ToList(); + var optionGroups = moduleList + .Where(x => + x.RequiredCredits > 0 && + x.Courses.Any(course => course.Type == CurriculumCourseType.Elective)) + .ToList(); + + var missing = requiredCourses + .Where(x => !passedCourseIds.Contains(x.CourseId)) + .Select(x => x.Course?.Name ?? "未命名必修课程") + .Distinct() + .ToList(); + var passedRequiredCourses = requiredCourses.Count - + requiredCourses.Count(x => + !passedCourseIds.Contains(x.CourseId)); + var passedGroups = 0; + foreach (var module in optionGroups) + { + var earnedGroupCredits = module.Courses + .Where(x => passedCourseIds.Contains(x.CourseId)) + .GroupBy(x => x.CourseId) + .Sum(group => group.Max(x => x.Course?.Credits ?? 0)); + if (earnedGroupCredits >= module.RequiredCredits) + { + passedGroups++; + continue; + } + + var shortage = module.RequiredCredits - earnedGroupCredits; + missing.Add($"{module.Name}(还差 {shortage:0.#} 学分)"); + } + + return new CurriculumCompletionSummary( + requiredCourses.Count + optionGroups.Count, + passedRequiredCourses + passedGroups, + missing); + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs index 5f6e4e5..382d549 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs @@ -19,6 +19,7 @@ public sealed class AppDbContext(DbContextOptions options) public DbSet AcademicTerms => Set(); public DbSet Teachers => Set(); public DbSet Students => Set(); + public DbSet CourseCategories => Set(); public DbSet Courses => Set(); public DbSet CurriculumPlans => Set(); public DbSet CurriculumModules => Set(); @@ -78,6 +79,7 @@ public sealed class AppDbContext(DbContextOptions options) ConfigureCatalog(builder); ConfigureCatalog(builder); ConfigureCatalog(builder); + ConfigureCatalog(builder); ConfigureCatalog(builder); builder.Entity() @@ -167,10 +169,15 @@ public sealed class AppDbContext(DbContextOptions options) entity.Property(x => x.Credits).HasPrecision(5, 2); entity.Property(x => x.Description).HasMaxLength(1000); entity.HasIndex(x => new { x.CollegeId, x.Nature }); + entity.HasIndex(x => x.CourseCategoryId); entity.HasOne(x => x.College) .WithMany() .HasForeignKey(x => x.CollegeId) .OnDelete(DeleteBehavior.Restrict); + entity.HasOne(x => x.CourseCategory) + .WithMany() + .HasForeignKey(x => x.CourseCategoryId) + .OnDelete(DeleteBehavior.Restrict); }); builder.Entity(entity => diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/DatabaseInitializer.cs b/src/Jiaowu.Api/Infrastructure/Persistence/DatabaseInitializer.cs index c581e42..b55cda5 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/DatabaseInitializer.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/DatabaseInitializer.cs @@ -28,6 +28,7 @@ public sealed class DatabaseInitializer( await SeedRolesAsync(); await SeedAdministratorAsync(); + await SeedCourseCategoriesAsync(); if (environment.IsDevelopment()) { @@ -108,6 +109,55 @@ public sealed class DatabaseInitializer( } } + private async Task SeedCourseCategoriesAsync() + { + var defaults = new[] + { + ("BASIC", "基础课程", 10), + ("MORAL", "德育课程", 20), + ("AESTHETIC", "美育课程", 30), + ("LABOR", "劳动教育", 40), + ("INNOVATION", "创新创业", 50), + ("ENGLISH", "大学英语", 60), + ("SPORTS", "大学体育", 70), + ("MILITARY", "国防教育", 80), + ("MAJOR", "专业教育", 90), + ("PRACTICE", "实践教学", 100) + }; + var existingCodes = (await db.CourseCategories + .Select(x => x.Code) + .ToListAsync()) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + foreach (var (code, name, sortOrder) in defaults) + { + if (existingCodes.Contains(code)) continue; + db.CourseCategories.Add(new CourseCategory + { + Code = code, + Name = name, + SortOrder = sortOrder + }); + } + await db.SaveChangesAsync(); + + var categories = await db.CourseCategories + .ToDictionaryAsync(x => x.Code, StringComparer.OrdinalIgnoreCase); + var uncategorizedCourses = await db.Courses + .Where(x => x.CourseCategoryId == null) + .ToListAsync(); + foreach (var course in uncategorizedCourses) + { + var categoryCode = course.Nature switch + { + CourseNature.Practice => "PRACTICE", + CourseNature.MajorRequired or CourseNature.MajorElective => "MAJOR", + _ => "BASIC" + }; + course.CourseCategoryId = categories[categoryCode].Id; + } + await db.SaveChangesAsync(); + } + private async Task SeedDevelopmentDataAsync() { if (!await db.Campuses.AnyAsync()) @@ -242,6 +292,9 @@ public sealed class DatabaseInitializer( }); } + var courseCategories = await db.CourseCategories + .ToDictionaryAsync(x => x.Code, StringComparer.OrdinalIgnoreCase); + if (!await db.Courses.AnyAsync()) { db.Courses.AddRange( @@ -251,6 +304,7 @@ public sealed class DatabaseInitializer( Name = "程序设计基础", EnglishName = "Fundamentals of Programming", CollegeId = computerCollege.Id, + CourseCategoryId = courseCategories["BASIC"].Id, Credits = 4, TotalHours = 64, LectureHours = 40, @@ -265,6 +319,7 @@ public sealed class DatabaseInitializer( Name = "数据结构", EnglishName = "Data Structures", CollegeId = computerCollege.Id, + CourseCategoryId = courseCategories["BASIC"].Id, Credits = 3.5m, TotalHours = 56, LectureHours = 40, @@ -278,6 +333,7 @@ public sealed class DatabaseInitializer( Name = "软件工程实践", EnglishName = "Software Engineering Practice", CollegeId = computerCollege.Id, + CourseCategoryId = courseCategories["PRACTICE"].Id, Credits = 2, TotalHours = 48, LectureHours = 8, @@ -286,7 +342,6 @@ public sealed class DatabaseInitializer( AssessmentMethod = AssessmentMethod.Assessment }); } - await db.SaveChangesAsync(); if (!await db.CurriculumPlans.AnyAsync()) diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs index 1882b85..59f1cb3 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs @@ -18,6 +18,7 @@ public sealed class DevelopmentSqliteMigrator( private const string GraduationAuditsMigration = "20260724_10_graduation_audits"; private const string DegreeAwardsMigration = "20260724_11_degree_awards"; private const string GraduationClearanceMigration = "20260724_12_graduation_clearance"; + private const string CourseCategoriesMigration = "20260724_13_course_categories"; public async Task MigrateAsync(CancellationToken cancellationToken = default) { @@ -90,6 +91,22 @@ public sealed class DevelopmentSqliteMigrator( GraduationClearanceMigration, GraduationClearanceStatements, cancellationToken); + var courseCategoryColumnExists = await db.Database + .SqlQueryRaw( + """ + SELECT COUNT(*) AS "Value" + FROM pragma_table_info('Courses') + WHERE name = 'CourseCategoryId' + """) + .AnyAsync(value => value > 0, cancellationToken); + await ApplyMigrationAsync( + CourseCategoriesMigration, + courseCategoryColumnExists + ? CourseCategoriesStatements.Where( + statement => !statement.StartsWith( + "ALTER TABLE", StringComparison.OrdinalIgnoreCase)) + : CourseCategoriesStatements, + cancellationToken); } private async Task ApplyMigrationAsync( @@ -767,4 +784,34 @@ public sealed class DevelopmentSqliteMigrator( """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");""" ]; + + private static readonly string[] CourseCategoriesStatements = + [ + """ + CREATE TABLE IF NOT EXISTS "CourseCategories" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_CourseCategories" PRIMARY KEY, + "Code" TEXT NOT NULL, + "Name" TEXT NOT NULL, + "SortOrder" INTEGER NOT NULL, + "IsEnabled" INTEGER NOT NULL, + "CreatedAt" TEXT NOT NULL, + "UpdatedAt" TEXT NOT NULL + ); + """, + """ + CREATE UNIQUE INDEX IF NOT EXISTS "IX_CourseCategories_Code" + ON "CourseCategories" ("Code"); + """, + """ + CREATE INDEX IF NOT EXISTS "IX_CourseCategories_IsEnabled_SortOrder" + ON "CourseCategories" ("IsEnabled", "SortOrder"); + """, + """ + ALTER TABLE "Courses" ADD COLUMN "CourseCategoryId" TEXT NULL; + """, + """ + CREATE INDEX IF NOT EXISTS "IX_Courses_CourseCategoryId" + ON "Courses" ("CourseCategoryId"); + """ + ]; } diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724104203_CourseCategories.Designer.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724104203_CourseCategories.Designer.cs new file mode 100644 index 0000000..140ab5f --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724104203_CourseCategories.Designer.cs @@ -0,0 +1,2541 @@ +// +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("20260724104203_CourseCategories")] + partial class CourseCategories + { + /// + 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("CourseCategoryId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Credits") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("EnglishName") + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LectureHours") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Nature") + .HasColumnType("int"); + + b.Property("PracticeHours") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("TotalHours") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("CourseCategoryId"); + + b.HasIndex("CollegeId", "Nature"); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Courses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("CourseCategories"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CourseSelectionOfferingId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EnrolledAt") + .HasColumnType("datetime(6)"); + + b.Property("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.HasOne("Jiaowu.Api.Domain.Academic.CourseCategory", "CourseCategory") + .WithMany() + .HasForeignKey("CourseCategoryId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("College"); + + b.Navigation("CourseCategory"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.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/20260724104203_CourseCategories.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724104203_CourseCategories.cs new file mode 100644 index 0000000..545390c --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724104203_CourseCategories.cs @@ -0,0 +1,82 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql +{ + /// + public partial class CourseCategories : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CourseCategoryId", + table: "Courses", + type: "char(36)", + nullable: true); + + migrationBuilder.CreateTable( + name: "CourseCategories", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false), + Code = table.Column(type: "varchar(40)", maxLength: 40, nullable: false), + Name = table.Column(type: "varchar(100)", maxLength: 100, nullable: false), + SortOrder = table.Column(type: "int", nullable: false), + IsEnabled = table.Column(type: "tinyint(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CourseCategories", x => x.Id); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_Courses_CourseCategoryId", + table: "Courses", + column: "CourseCategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_CourseCategories_Code", + table: "CourseCategories", + column: "Code", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_CourseCategories_IsEnabled_SortOrder", + table: "CourseCategories", + columns: new[] { "IsEnabled", "SortOrder" }); + + migrationBuilder.AddForeignKey( + name: "FK_Courses_CourseCategories_CourseCategoryId", + table: "Courses", + column: "CourseCategoryId", + principalTable: "CourseCategories", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Courses_CourseCategories_CourseCategoryId", + table: "Courses"); + + migrationBuilder.DropTable( + name: "CourseCategories"); + + migrationBuilder.DropIndex( + name: "IX_Courses_CourseCategoryId", + table: "Courses"); + + migrationBuilder.DropColumn( + name: "CourseCategoryId", + table: "Courses"); + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs index 77acf5a..a63257f 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs @@ -329,6 +329,9 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.Property("CollegeId") .HasColumnType("char(36)"); + b.Property("CourseCategoryId") + .HasColumnType("char(36)"); + b.Property("CreatedAt") .HasColumnType("datetime(6)"); @@ -375,6 +378,8 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasIndex("Code") .IsUnique(); + b.HasIndex("CourseCategoryId"); + b.HasIndex("CollegeId", "Nature"); b.HasIndex("IsEnabled", "SortOrder"); @@ -382,6 +387,44 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.ToTable("Courses"); }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("CourseCategories"); + }); + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseEnrollment", b => { b.Property("Id") @@ -1948,7 +1991,14 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql .OnDelete(DeleteBehavior.Restrict) .IsRequired(); + b.HasOne("Jiaowu.Api.Domain.Academic.CourseCategory", "CourseCategory") + .WithMany() + .HasForeignKey("CourseCategoryId") + .OnDelete(DeleteBehavior.Restrict); + b.Navigation("College"); + + b.Navigation("CourseCategory"); }); modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseEnrollment", b => diff --git a/tests/Jiaowu.Api.Tests/GraduationAuditRulesTests.cs b/tests/Jiaowu.Api.Tests/GraduationAuditRulesTests.cs index bf0160b..b55dce1 100644 --- a/tests/Jiaowu.Api.Tests/GraduationAuditRulesTests.cs +++ b/tests/Jiaowu.Api.Tests/GraduationAuditRulesTests.cs @@ -5,6 +5,61 @@ namespace Jiaowu.Api.Tests; public sealed class GraduationAuditRulesTests { + [Fact] + public void Curriculum_groups_Accept_Any_Options_That_Fulfill_Required_Credits() + { + var football = new Course { Code = "PE101", Name = "足球", Credits = 1 }; + var basketball = new Course { Code = "PE102", Name = "篮球", Credits = 1 }; + var swimming = new Course { Code = "PE103", Name = "游泳", Credits = 1 }; + var module = new CurriculumModule + { + Code = "SPORTS", + Name = "大学体育", + RequiredCredits = 2, + Courses = [Option(football), Option(basketball), Option(swimming)] + }; + + var completed = CurriculumCompletionRules.Evaluate( + [module], + new HashSet([football.Id, swimming.Id])); + var incomplete = CurriculumCompletionRules.Evaluate( + [module], + new HashSet([football.Id])); + + Assert.Equal(1, completed.RequirementCount); + Assert.Equal(1, completed.PassedRequirementCount); + Assert.Empty(completed.MissingRequirements); + Assert.Equal(0, incomplete.PassedRequirementCount); + Assert.Contains("大学体育(还差 1 学分)", incomplete.MissingRequirements); + } + + [Fact] + public void Curriculum_groups_Still_Require_Every_Fixed_Course() + { + var ideology = new Course { Code = "MORAL101", Name = "思想道德与法治", Credits = 3 }; + var module = new CurriculumModule + { + Code = "MORAL", + Name = "德育课程", + RequiredCredits = 3, + Courses = + [ + new CurriculumCourse + { + CourseId = ideology.Id, + Course = ideology, + Type = CurriculumCourseType.Required + } + ] + }; + + var result = CurriculumCompletionRules.Evaluate([module], new HashSet()); + + Assert.Equal(1, result.RequirementCount); + Assert.Equal(0, result.PassedRequirementCount); + Assert.Contains("思想道德与法治", result.MissingRequirements); + } + [Fact] public void Active_student_with_full_credits_and_courses_is_eligible() { @@ -35,4 +90,12 @@ public sealed class GraduationAuditRulesTests Assert.Equal(GraduationAuditConclusion.Ineligible, conclusion); } + + private static CurriculumCourse Option(Course course) => + new() + { + CourseId = course.Id, + Course = course, + Type = CurriculumCourseType.Elective + }; } diff --git a/tests/Jiaowu.Api.Tests/PersistenceTests.cs b/tests/Jiaowu.Api.Tests/PersistenceTests.cs index e105522..e234515 100644 --- a/tests/Jiaowu.Api.Tests/PersistenceTests.cs +++ b/tests/Jiaowu.Api.Tests/PersistenceTests.cs @@ -111,6 +111,12 @@ public sealed class PersistenceTests : IAsyncLifetime Nature = CourseNature.MajorRequired, AssessmentMethod = AssessmentMethod.Examination }; + var category = new CourseCategory + { + Code = "BASIC", + Name = "基础课程" + }; + course.CourseCategoryId = category.Id; var teacher = new Teacher { TeacherNumber = "T2026001", @@ -148,6 +154,7 @@ public sealed class PersistenceTests : IAsyncLifetime counselor, administrativeClass, teacher, + category, new Student { StudentNumber = "202601001", @@ -237,7 +244,10 @@ public sealed class PersistenceTests : IAsyncLifetime var savedClass = await _db.AdministrativeClasses .Include(x => x.CounselorUser) .SingleAsync(); - var savedCourse = await _db.Courses.Include(x => x.College).SingleAsync(); + var savedCourse = await _db.Courses + .Include(x => x.College) + .Include(x => x.CourseCategory) + .SingleAsync(); var curriculumPlan = await _db.CurriculumPlans .Include(x => x.Modules) .ThenInclude(x => x.Courses) @@ -255,6 +265,7 @@ public sealed class PersistenceTests : IAsyncLifetime Assert.Equal("计算机科学与技术", student.AdministrativeClass!.Major!.Name); Assert.Equal("陈老师", savedClass.CounselorUser!.DisplayName); Assert.Equal(4m, savedCourse.Credits); + Assert.Equal("基础课程", savedCourse.CourseCategory!.Name); Assert.Equal(savedCourse.TotalHours, savedCourse.LectureHours + savedCourse.PracticeHours); Assert.Single(curriculumPlan.Modules); Assert.Single(curriculumPlan.Modules.Single().Courses); diff --git a/web/src/layouts/AdminLayout.vue b/web/src/layouts/AdminLayout.vue index c1d9638..dde3d42 100644 --- a/web/src/layouts/AdminLayout.vue +++ b/web/src/layouts/AdminLayout.vue @@ -74,6 +74,10 @@ const navigationGroups = computed(() => [ key: 'teaching-construction', label: '教学建设', items: [ + ...whenVisible( + hasAnyRole(['SuperAdmin', 'AcademicAdmin']), + { path: '/base-data/course-categories', label: '课程分类' }, + ), { path: '/courses', label: '课程库' }, ...whenVisible(isTeachingAdmin.value, { path: '/curriculum', label: '培养方案' }), ...whenVisible(isTeachingAdmin.value, { path: '/teaching-tasks', label: '教学任务' }), diff --git a/web/src/router/index.ts b/web/src/router/index.ts index ef0e363..5584a36 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -52,6 +52,15 @@ const router = createRouter({ baseGroup: 'facilities', }, }, + { + path: 'base-data/course-categories', + name: 'course-category-data', + component: () => import('../views/BaseDataView.vue'), + meta: { + roles: ['SuperAdmin', 'AcademicAdmin'], + baseGroup: 'course-categories', + }, + }, { path: 'personnel', redirect: '/teachers', diff --git a/web/src/style.css b/web/src/style.css index 4a2ac00..4ff1284 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -227,6 +227,14 @@ button { cursor: pointer; } .record-name, .course-name { display: grid; gap: 3px; } .record-name b, .course-name b { color: var(--ink); font-size: 13px; } .record-name span, .course-name span { color: #9299a7; font-size: 10px; } +.course-category { display: inline-flex; padding: 3px 8px; color: #335b75; background: #eaf2f5; border-radius: 12px; font-size: 10px; white-space: nowrap; } +.taxonomy-note { min-height: 56px; padding: 8px 12px; display: grid; align-content: center; gap: 4px; border-left: 3px solid var(--teal); background: #f4f8f8; } +.taxonomy-note b { color: var(--ink); font-size: 11px; } +.taxonomy-note span { color: var(--muted); font-size: 10px; line-height: 1.5; } +.form-help { margin-left: 10px; color: var(--muted); font-size: 10px; } +.course-rule-help { margin-top: 4px; padding: 12px 14px; display: grid; grid-template-columns: 74px 1fr; gap: 7px 12px; background: #f5f8fa; border-left: 3px solid var(--teal); } +.course-rule-help b { color: var(--ink); font-size: 11px; } +.course-rule-help span { color: var(--muted); font-size: 10px; line-height: 1.5; } .pagination-bar { min-height: 62px; padding: 12px 18px; display: flex; align-items: center; justify-content: space-between; border-top: 1px solid var(--line); } .pagination-bar > span { color: var(--muted); font-size: 11px; } .course-ledger { min-height: 92px; display: flex; align-items: stretch; color: white; background: linear-gradient(105deg, #1a2d61, #263f80); overflow: hidden; position: relative; } diff --git a/web/src/views/BaseDataView.vue b/web/src/views/BaseDataView.vue index 7fe7a00..7dddfa4 100644 --- a/web/src/views/BaseDataView.vue +++ b/web/src/views/BaseDataView.vue @@ -6,7 +6,7 @@ import http, { apiErrorMessage } from '../api/http' import { downloadApiFile, importExcel } from '../api/excel' import { useAuthStore } from '../stores/auth' -type Kind = 'campuses' | 'colleges' | 'majors' | 'classes' | 'terms' | 'buildings' | 'classrooms' +type Kind = 'campuses' | 'colleges' | 'majors' | 'classes' | 'terms' | 'buildings' | 'classrooms' | 'course-categories' interface Row { id: string; code: string; name: string; isEnabled: boolean; [key: string]: unknown } const allTabs: { key: Kind; label: string; hint: string }[] = [ @@ -17,6 +17,7 @@ const allTabs: { key: Kind; label: string; hint: string }[] = [ { key: 'terms', label: '学期', hint: '教学运行时间轴' }, { key: 'buildings', label: '教学楼', hint: '校区内教学建筑' }, { key: 'classrooms', label: '教室', hint: '可排课教学空间' }, + { key: 'course-categories', label: '课程分类', hint: '课程所属教学领域' }, ] const auth = useAuthStore() @@ -40,6 +41,12 @@ const groups = { description: '按校区和教学楼维护可用于排课的教室资源。', kinds: ['buildings', 'classrooms'] as Kind[], }, + 'course-categories': { + title: '课程分类', + kicker: 'COURSE TAXONOMY', + description: '统一维护基础课、美育、德育、创新创业等课程教学领域。课程性质与课程分类相互独立。', + kinds: ['course-categories'] as Kind[], + }, } const group = computed(() => { const key = String(route.meta.baseGroup ?? 'organization') as keyof typeof groups diff --git a/web/src/views/CoursesView.vue b/web/src/views/CoursesView.vue index 14c6444..a19e933 100644 --- a/web/src/views/CoursesView.vue +++ b/web/src/views/CoursesView.vue @@ -1,7 +1,8 @@ @@ -174,7 +242,10 @@ onMounted(async () => {

课程库

通识课程由校级统筹,专业课与实践课由开课学院维护,统一供培养方案与教学任务引用。

- 新增课程 +
+ 导出结果 + 新增课程 +
@@ -202,6 +273,9 @@ onMounted(async () => { + + + @@ -212,6 +286,20 @@ onMounted(async () => { 查询 重置 +
+ + 当前筛选条件会原样用于 Excel 导出 · 共 {{ total }} 门 +
@@ -226,6 +314,11 @@ onMounted(async () => { + + + @@ -279,11 +372,27 @@ onMounted(async () => { + + + + + + +
+
+ 两个维度 + 分类表示教学领域,性质决定课程在培养方案中的管理方式。 +
diff --git a/web/src/views/CurriculumView.vue b/web/src/views/CurriculumView.vue index f1dcb88..027e977 100644 --- a/web/src/views/CurriculumView.vue +++ b/web/src/views/CurriculumView.vue @@ -42,8 +42,8 @@ const statusLabels: Record = { Archived: '已归档', } const courseTypeLabels: Record = { - Required: '必修', - Elective: '选修', + Required: '指定必修', + Elective: '组内选修', } const isCollegeAdmin = computed(() => auth.user?.roles.includes('CollegeAdmin') ?? false) const availableMajors = computed(() => majors.value) @@ -410,7 +410,7 @@ onMounted(async () => {
课程结构 - 发布前模块学分合计须与方案总学分一致 + 指定必修须逐门通过;英语、体育等多选课程用“组内选修”,修满模块最低学分即可
新增模块
@@ -421,7 +421,12 @@ onMounted(async () => { {{ module.code }}

{{ module.name }}

-

最低 {{ module.requiredCredits }} 学分 · 已配置 {{ module.assignedCredits }} 学分

+

+ 最低 {{ module.requiredCredits }} 学分 · 已配置 {{ module.assignedCredits }} 学分 + +

编辑 删除 @@ -436,6 +441,9 @@ onMounted(async () => { + + + @@ -489,7 +497,10 @@ onMounted(async () => {
- + + + 体育、英语等课程组填写学生必须从组内修满的学分。 + @@ -498,14 +509,18 @@ onMounted(async () => { - +
- +
+
+ 指定必修该门课程必须通过 + 组内选修足球、篮球、英语分层等作为选项,按模块最低学分验收 +
diff --git a/web/src/views/GraduationAuditsView.vue b/web/src/views/GraduationAuditsView.vue index 1e01ff7..753c9bb 100644 --- a/web/src/views/GraduationAuditsView.vue +++ b/web/src/views/GraduationAuditsView.vue @@ -155,7 +155,7 @@ onMounted(load)
学分完成{{ myResult.earnedCredits }} / {{ myResult.requiredCredits }}
-
必修课程{{ myResult.passedRequiredCourseCount }} / {{ myResult.requiredCourseCount }}

已通过 / 应通过

+
必修要求{{ myResult.passedRequiredCourseCount }} / {{ myResult.requiredCourseCount }}

已完成 / 应完成

未解决不及格{{ myResult.failedCourseCount }}

门课程