diff --git a/README.md b/README.md index a9bfe22..114a8dd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 面向普通高校的教务管理系统。后端使用 ASP.NET Core 10、EF Core 10,前端使用 Vue 3、TypeScript 和 Element Plus。 -当前已实现系统登录与角色权限、基础数据、用户管理、教师档案、学生档案、课程库和首页统计。人员及课程列表支持组合筛选、服务端分页和完整增删改查。 +当前已实现系统登录与角色权限、基础数据、用户管理、教师档案、学生档案、课程库、培养方案、教学任务和首页统计。人员及课程列表支持组合筛选、服务端分页和完整增删改查;培养方案支持课程模块、专业年级版本、复制新版本、发布锁定和旧版本归档;教学任务支持学期课程开设、多教师、合班、容量校验、发布与结课。 ## 本地开发:热更新模式 diff --git a/scripts/smoke-test.ps1 b/scripts/smoke-test.ps1 index 2d0f4b2..015268b 100644 --- a/scripts/smoke-test.ps1 +++ b/scripts/smoke-test.ps1 @@ -69,6 +69,13 @@ 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 + $curriculumPlans = Invoke-RestMethod -Uri 'http://localhost:5255/api/curriculum-plans?page=1&pageSize=10' -Headers $headers + if ($curriculumPlans.total -gt 0) { + $curriculumDetail = Invoke-RestMethod ` + -Uri "http://localhost:5255/api/curriculum-plans/$($curriculumPlans.items[0].id)" ` + -Headers $headers + } + $teachingTasks = Invoke-RestMethod -Uri 'http://localhost:5255/api/teaching-tasks?page=1&pageSize=10' -Headers $headers $frontend = Invoke-WebRequest -Uri 'http://localhost:5255/' -TimeoutSec 5 $spaFallback = Invoke-WebRequest -Uri 'http://localhost:5255/base-data' -TimeoutSec 5 $unknownApiParameters = @{ @@ -87,6 +94,9 @@ try { Teachers = $teachers.total Students = $students.total Courses = $courses.total + Plans = $curriculumPlans.total + PlanModules = if ($null -ne $curriculumDetail) { @($curriculumDetail.modules).Count } else { 0 } + TeachingTasks = $teachingTasks.total StaticIndex = $frontend.Content.Contains('明序教务管理系统') SpaFallback = $spaFallback.StatusCode ApiNotFound = $unknownApi.StatusCode diff --git a/src/Jiaowu.Api/Controllers/CurriculumPlansController.cs b/src/Jiaowu.Api/Controllers/CurriculumPlansController.cs new file mode 100644 index 0000000..896dba8 --- /dev/null +++ b/src/Jiaowu.Api/Controllers/CurriculumPlansController.cs @@ -0,0 +1,518 @@ +using System.ComponentModel.DataAnnotations; +using System.Security.Claims; +using Jiaowu.Api.Contracts; +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Infrastructure.Persistence; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Jiaowu.Api.Controllers; + +[ApiController] +[Authorize(Roles = ReadRoles)] +[Route("api/curriculum-plans")] +public sealed class CurriculumPlansController(AppDbContext db) : ControllerBase +{ + private const string ReadRoles = + SystemRoles.SuperAdmin + "," + + SystemRoles.AcademicAdmin + "," + + SystemRoles.CollegeAdmin; + + [HttpGet] + public async Task>> GetPlans( + int page = 1, + int pageSize = 20, + string? keyword = null, + Guid? majorId = null, + int? effectiveGrade = null, + CurriculumPlanStatus? status = null, + CancellationToken cancellationToken = default) + { + page = Math.Max(1, page); + pageSize = Math.Clamp(pageSize, 10, 100); + var source = ScopedPlans().AsNoTracking(); + if (majorId.HasValue) source = source.Where(x => x.MajorId == majorId); + if (effectiveGrade.HasValue) + source = source.Where(x => x.EffectiveGrade == effectiveGrade); + if (status.HasValue) source = source.Where(x => x.Status == status); + if (!string.IsNullOrWhiteSpace(keyword)) + { + keyword = keyword.Trim(); + source = source.Where(x => + x.Name.Contains(keyword) || + x.Version.Contains(keyword) || + x.Major!.Name.Contains(keyword)); + } + + var total = await source.CountAsync(cancellationToken); + var items = await source + .OrderByDescending(x => x.EffectiveGrade) + .ThenByDescending(x => x.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(x => new + { + x.Id, + x.Name, + x.Version, + x.MajorId, + MajorName = x.Major!.Name, + CollegeId = x.Major.CollegeId, + CollegeName = x.Major.College!.Name, + x.EffectiveGrade, + x.TotalCredits, + x.Status, + x.PublishedAt, + ModuleCount = x.Modules.Count, + CourseCount = x.Modules.SelectMany(module => module.Courses).Count(), + x.UpdatedAt + }) + .ToListAsync(cancellationToken); + + return Ok(new PagedResult(items, total, page, pageSize)); + } + + [HttpGet("{id:guid}")] + public async Task GetPlan(Guid id, CancellationToken cancellationToken) + { + var plan = await ScopedPlans() + .AsNoTracking() + .Where(x => x.Id == id) + .Select(x => new + { + x.Id, + x.Name, + x.Version, + x.MajorId, + MajorName = x.Major!.Name, + SchoolingYears = x.Major.SchoolingYears, + CollegeId = x.Major.CollegeId, + CollegeName = x.Major.College!.Name, + x.EffectiveGrade, + x.TotalCredits, + x.Status, + x.Description, + x.PublishedAt, + x.CreatedAt, + x.UpdatedAt, + Modules = x.Modules + .OrderBy(module => module.SortOrder) + .ThenBy(module => module.Code) + .Select(module => new + { + module.Id, + module.Code, + module.Name, + module.RequiredCredits, + module.SortOrder, + AssignedCredits = module.Courses.Sum(course => course.Course!.Credits), + Courses = module.Courses + .OrderBy(course => course.RecommendedSemester) + .ThenBy(course => course.Course!.Code) + .Select(course => new + { + course.Id, + course.CourseId, + CourseCode = course.Course!.Code, + CourseName = course.Course.Name, + course.Course.Credits, + course.Course.TotalHours, + course.Course.Nature, + course.RecommendedSemester, + course.Type, + course.Notes + }) + }) + }) + .FirstOrDefaultAsync(cancellationToken); + + return plan is null ? NotFound() : Ok(plan); + } + + [HttpPost] + public async Task Create( + CurriculumPlanRequest request, + CancellationToken cancellationToken) + { + var major = await FindAccessibleMajorAsync(request.MajorId, cancellationToken); + if (major is null) return ValidationProblem("所选专业不存在或不在当前数据范围内。"); + + var plan = new CurriculumPlan + { + MajorId = request.MajorId, + Name = request.Name.Trim(), + Version = request.Version.Trim(), + EffectiveGrade = request.EffectiveGrade, + TotalCredits = request.TotalCredits, + Description = Normalize(request.Description) + }; + db.CurriculumPlans.Add(plan); + return await SaveCreatedAsync(plan.Id, cancellationToken); + } + + [HttpPut("{id:guid}")] + public async Task Update( + Guid id, + CurriculumPlanRequest request, + CancellationToken cancellationToken) + { + var plan = await EditablePlanAsync(id, cancellationToken); + if (plan is null) return NotFound(); + if (plan.Status != CurriculumPlanStatus.Draft) + return ConflictProblem("已发布或已归档的培养方案不可直接修改,请复制为新版本。"); + var major = await FindAccessibleMajorAsync(request.MajorId, cancellationToken); + if (major is null) return ValidationProblem("所选专业不存在或不在当前数据范围内。"); + + plan.MajorId = request.MajorId; + plan.Name = request.Name.Trim(); + plan.Version = request.Version.Trim(); + plan.EffectiveGrade = request.EffectiveGrade; + plan.TotalCredits = request.TotalCredits; + plan.Description = Normalize(request.Description); + return await SaveNoContentAsync(cancellationToken); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id, CancellationToken cancellationToken) + { + var plan = await EditablePlanAsync(id, cancellationToken); + if (plan is null) return NotFound(); + if (plan.Status != CurriculumPlanStatus.Draft) + return ConflictProblem("仅草稿方案可以删除。"); + db.CurriculumPlans.Remove(plan); + return await SaveNoContentAsync(cancellationToken); + } + + [HttpPost("{id:guid}/clone")] + public async Task Clone( + Guid id, + CloneCurriculumPlanRequest request, + CancellationToken cancellationToken) + { + var source = await ScopedPlans() + .AsNoTracking() + .Include(x => x.Modules) + .ThenInclude(x => x.Courses) + .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + if (source is null) return NotFound(); + + var copy = new CurriculumPlan + { + MajorId = source.MajorId, + Name = request.Name.Trim(), + Version = request.Version.Trim(), + EffectiveGrade = request.EffectiveGrade, + TotalCredits = source.TotalCredits, + Description = source.Description, + Modules = source.Modules.Select(module => new CurriculumModule + { + Code = module.Code, + Name = module.Name, + RequiredCredits = module.RequiredCredits, + SortOrder = module.SortOrder, + Courses = module.Courses.Select(course => new CurriculumCourse + { + CourseId = course.CourseId, + RecommendedSemester = course.RecommendedSemester, + Type = course.Type, + Notes = course.Notes + }).ToList() + }).ToList() + }; + db.CurriculumPlans.Add(copy); + return await SaveCreatedAsync(copy.Id, cancellationToken); + } + + [HttpPost("{id:guid}/publish")] + public async Task Publish(Guid id, CancellationToken cancellationToken) + { + var plan = await ScopedPlans() + .Include(x => x.Major) + .Include(x => x.Modules) + .ThenInclude(x => x.Courses) + .ThenInclude(x => x.Course) + .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + if (plan is null) return NotFound(); + if (plan.Status != CurriculumPlanStatus.Draft) + return ConflictProblem("只有草稿方案可以发布。"); + if (plan.Modules.Count == 0 || plan.Modules.Any(x => x.Courses.Count == 0)) + return ConflictProblem("发布前每个课程模块都必须配置课程。"); + if (plan.Modules.Sum(x => x.RequiredCredits) != plan.TotalCredits) + return ConflictProblem("各模块最低学分之和必须等于方案总学分。"); + if (plan.Modules.Any(module => + module.Courses.Sum(x => x.Course!.Credits) < module.RequiredCredits)) + return ConflictProblem("存在课程学分合计低于最低学分要求的模块。"); + if (plan.Modules.SelectMany(x => x.Courses).GroupBy(x => x.CourseId).Any(x => x.Count() > 1)) + return ConflictProblem("同一门课程不能重复加入多个模块。"); + if (plan.Modules.SelectMany(x => x.Courses).Any(x => + x.RecommendedSemester > plan.Major!.SchoolingYears * 2)) + return ConflictProblem("建议学期超出了该专业学制。"); + + await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken); + var previousPlans = await ScopedPlans() + .Where(x => + x.Id != plan.Id && + x.MajorId == plan.MajorId && + x.EffectiveGrade == plan.EffectiveGrade && + x.Status == CurriculumPlanStatus.Published) + .ToListAsync(cancellationToken); + foreach (var previous in previousPlans) + { + previous.Status = CurriculumPlanStatus.Archived; + } + + plan.Status = CurriculumPlanStatus.Published; + plan.PublishedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + return NoContent(); + } + + [HttpPost("{planId:guid}/modules")] + public async Task CreateModule( + Guid planId, + CurriculumModuleRequest request, + CancellationToken cancellationToken) + { + var plan = await DraftPlanAsync(planId, cancellationToken); + if (plan is null) return NotFound(); + var module = new CurriculumModule + { + CurriculumPlanId = plan.Id, + Code = request.Code.Trim(), + Name = request.Name.Trim(), + RequiredCredits = request.RequiredCredits, + SortOrder = request.SortOrder + }; + db.CurriculumModules.Add(module); + return await SaveCreatedAsync(module.Id, cancellationToken); + } + + [HttpPut("{planId:guid}/modules/{moduleId:guid}")] + public async Task UpdateModule( + Guid planId, + Guid moduleId, + CurriculumModuleRequest request, + CancellationToken cancellationToken) + { + if (await DraftPlanAsync(planId, cancellationToken) is null) return NotFound(); + var module = await db.CurriculumModules + .FirstOrDefaultAsync(x => x.Id == moduleId && x.CurriculumPlanId == planId, cancellationToken); + if (module is null) return NotFound(); + module.Code = request.Code.Trim(); + module.Name = request.Name.Trim(); + module.RequiredCredits = request.RequiredCredits; + module.SortOrder = request.SortOrder; + return await SaveNoContentAsync(cancellationToken); + } + + [HttpDelete("{planId:guid}/modules/{moduleId:guid}")] + public async Task DeleteModule( + Guid planId, + Guid moduleId, + CancellationToken cancellationToken) + { + if (await DraftPlanAsync(planId, cancellationToken) is null) return NotFound(); + var module = await db.CurriculumModules + .FirstOrDefaultAsync(x => x.Id == moduleId && x.CurriculumPlanId == planId, cancellationToken); + if (module is null) return NotFound(); + db.CurriculumModules.Remove(module); + return await SaveNoContentAsync(cancellationToken); + } + + [HttpPost("{planId:guid}/modules/{moduleId:guid}/courses")] + public async Task AddCourse( + Guid planId, + Guid moduleId, + CurriculumCourseRequest request, + CancellationToken cancellationToken) + { + var plan = await DraftPlanAsync(planId, cancellationToken); + if (plan is null) return NotFound(); + if (!await db.CurriculumModules.AnyAsync( + x => x.Id == moduleId && x.CurriculumPlanId == planId, + cancellationToken)) + return NotFound(); + if (!await db.Courses.AnyAsync( + x => x.Id == request.CourseId && x.IsEnabled, + cancellationToken)) + return ValidationProblem("所选课程不存在或已停用。"); + if (request.RecommendedSemester > plan.Major!.SchoolingYears * 2) + return ValidationProblem("建议学期超出了该专业学制。"); + if (await db.CurriculumCourses.AnyAsync( + x => x.CurriculumModule!.CurriculumPlanId == planId && + x.CourseId == request.CourseId, + cancellationToken)) + return ConflictProblem("该课程已存在于本方案中。"); + + var item = new CurriculumCourse + { + CurriculumModuleId = moduleId, + CourseId = request.CourseId, + RecommendedSemester = request.RecommendedSemester, + Type = request.Type, + Notes = Normalize(request.Notes) + }; + db.CurriculumCourses.Add(item); + return await SaveCreatedAsync(item.Id, cancellationToken); + } + + [HttpPut("{planId:guid}/modules/{moduleId:guid}/courses/{itemId:guid}")] + public async Task UpdateCourse( + Guid planId, + Guid moduleId, + Guid itemId, + CurriculumCourseRequest request, + CancellationToken cancellationToken) + { + var plan = await DraftPlanAsync(planId, cancellationToken); + if (plan is null) return NotFound(); + var item = await db.CurriculumCourses + .FirstOrDefaultAsync(x => + x.Id == itemId && + x.CurriculumModuleId == moduleId && + x.CurriculumModule!.CurriculumPlanId == planId, + cancellationToken); + if (item is null) return NotFound(); + if (!await db.Courses.AnyAsync( + x => x.Id == request.CourseId && x.IsEnabled, + cancellationToken)) + return ValidationProblem("所选课程不存在或已停用。"); + if (request.RecommendedSemester > plan.Major!.SchoolingYears * 2) + return ValidationProblem("建议学期超出了该专业学制。"); + if (await db.CurriculumCourses.AnyAsync( + x => x.Id != itemId && + x.CurriculumModule!.CurriculumPlanId == planId && + x.CourseId == request.CourseId, + cancellationToken)) + return ConflictProblem("该课程已存在于本方案中。"); + + item.CourseId = request.CourseId; + item.RecommendedSemester = request.RecommendedSemester; + item.Type = request.Type; + item.Notes = Normalize(request.Notes); + return await SaveNoContentAsync(cancellationToken); + } + + [HttpDelete("{planId:guid}/modules/{moduleId:guid}/courses/{itemId:guid}")] + public async Task DeleteCourse( + Guid planId, + Guid moduleId, + Guid itemId, + CancellationToken cancellationToken) + { + if (await DraftPlanAsync(planId, cancellationToken) is null) return NotFound(); + var item = await db.CurriculumCourses + .FirstOrDefaultAsync(x => + x.Id == itemId && + x.CurriculumModuleId == moduleId && + x.CurriculumModule!.CurriculumPlanId == planId, + cancellationToken); + if (item is null) return NotFound(); + db.CurriculumCourses.Remove(item); + return await SaveNoContentAsync(cancellationToken); + } + + private IQueryable ScopedPlans() + { + var source = db.CurriculumPlans.AsQueryable(); + var collegeId = ScopedCollegeId(); + return collegeId.HasValue + ? source.Where(x => x.Major!.CollegeId == collegeId.Value) + : source; + } + + private async Task FindAccessibleMajorAsync(Guid majorId, CancellationToken cancellationToken) + { + var source = db.Majors.AsQueryable(); + var collegeId = ScopedCollegeId(); + if (collegeId.HasValue) source = source.Where(x => x.CollegeId == collegeId.Value); + return await source.FirstOrDefaultAsync(x => x.Id == majorId, cancellationToken); + } + + private async Task EditablePlanAsync( + Guid id, + CancellationToken cancellationToken) => + await ScopedPlans().FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + + private async Task DraftPlanAsync( + Guid id, + CancellationToken cancellationToken) => + await ScopedPlans() + .Include(x => x.Major) + .FirstOrDefaultAsync( + x => x.Id == id && x.Status == CurriculumPlanStatus.Draft, + cancellationToken); + + private Guid? ScopedCollegeId() + { + if (!User.IsInRole(SystemRoles.CollegeAdmin)) return null; + return Guid.TryParse(User.FindFirstValue("college_id"), out var collegeId) + ? collegeId + : Guid.Empty; + } + + private async Task SaveCreatedAsync(Guid id, CancellationToken cancellationToken) + { + try + { + await db.SaveChangesAsync(cancellationToken); + return Created(string.Empty, new { id }); + } + catch (DbUpdateException) + { + return ConflictProblem("版本、模块编码或课程配置重复。"); + } + } + + private async Task SaveNoContentAsync(CancellationToken cancellationToken) + { + try + { + await db.SaveChangesAsync(cancellationToken); + return NoContent(); + } + catch (DbUpdateException) + { + return ConflictProblem("数据重复,或记录已被其他业务引用。"); + } + } + + private ActionResult ConflictProblem(string detail) => + Conflict(new ProblemDetails + { + Title = "无法完成操作", + Detail = detail, + Status = StatusCodes.Status409Conflict + }); + + private static string? Normalize(string? value) => + string.IsNullOrWhiteSpace(value) ? null : value.Trim(); +} + +public sealed record CurriculumPlanRequest( + Guid MajorId, + [Required, MaxLength(120)] string Name, + [Required, MaxLength(30)] string Version, + [Range(2000, 2200)] int EffectiveGrade, + [Range(typeof(decimal), "0.1", "999")] decimal TotalCredits, + [MaxLength(1000)] string? Description); + +public sealed record CloneCurriculumPlanRequest( + [Required, MaxLength(120)] string Name, + [Required, MaxLength(30)] string Version, + [Range(2000, 2200)] int EffectiveGrade); + +public sealed record CurriculumModuleRequest( + [Required, MaxLength(30)] string Code, + [Required, MaxLength(100)] string Name, + [Range(typeof(decimal), "0", "999")] decimal RequiredCredits, + [Range(0, 9999)] int SortOrder); + +public sealed record CurriculumCourseRequest( + Guid CourseId, + [Range(1, 20)] int RecommendedSemester, + CurriculumCourseType Type, + [MaxLength(500)] string? Notes); diff --git a/src/Jiaowu.Api/Controllers/DashboardController.cs b/src/Jiaowu.Api/Controllers/DashboardController.cs index 1e9f71b..57fc261 100644 --- a/src/Jiaowu.Api/Controllers/DashboardController.cs +++ b/src/Jiaowu.Api/Controllers/DashboardController.cs @@ -32,6 +32,8 @@ public sealed class DashboardController(AppDbContext db) : ControllerBase Teachers = await db.Teachers.CountAsync(cancellationToken), Students = await db.Students.CountAsync(cancellationToken), Courses = await db.Courses.CountAsync(cancellationToken), + CurriculumPlans = await db.CurriculumPlans.CountAsync(cancellationToken), + TeachingTasks = await db.TeachingTasks.CountAsync(cancellationToken), Users = await db.Users.CountAsync(cancellationToken) } }; diff --git a/src/Jiaowu.Api/Controllers/TeachingTasksController.cs b/src/Jiaowu.Api/Controllers/TeachingTasksController.cs new file mode 100644 index 0000000..579e24a --- /dev/null +++ b/src/Jiaowu.Api/Controllers/TeachingTasksController.cs @@ -0,0 +1,364 @@ +using System.ComponentModel.DataAnnotations; +using System.Security.Claims; +using Jiaowu.Api.Contracts; +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Infrastructure.Persistence; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Jiaowu.Api.Controllers; + +[ApiController] +[Authorize(Roles = ManagementRoles)] +[Route("api/teaching-tasks")] +public sealed class TeachingTasksController(AppDbContext db) : ControllerBase +{ + private const string ManagementRoles = + SystemRoles.SuperAdmin + "," + + SystemRoles.AcademicAdmin + "," + + SystemRoles.CollegeAdmin; + + [HttpGet] + public async Task>> Get( + int page = 1, + int pageSize = 20, + string? keyword = null, + Guid? academicTermId = null, + Guid? collegeId = null, + TeachingTaskStatus? status = null, + CancellationToken cancellationToken = default) + { + page = Math.Max(1, page); + pageSize = Math.Clamp(pageSize, 10, 100); + var source = ScopedTasks().AsNoTracking(); + if (academicTermId.HasValue) + source = source.Where(x => x.AcademicTermId == academicTermId); + if (collegeId.HasValue) + source = source.Where(x => x.Course!.CollegeId == collegeId); + if (status.HasValue) source = source.Where(x => x.Status == status); + if (!string.IsNullOrWhiteSpace(keyword)) + { + keyword = keyword.Trim(); + source = source.Where(x => + x.TaskNumber.Contains(keyword) || + x.Name.Contains(keyword) || + x.Course!.Code.Contains(keyword) || + x.Course.Name.Contains(keyword)); + } + + var total = await source.CountAsync(cancellationToken); + var items = await source + .OrderByDescending(x => x.AcademicTerm!.StartDate) + .ThenBy(x => x.TaskNumber) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(x => new + { + x.Id, + x.TaskNumber, + x.Name, + x.AcademicTermId, + TermName = x.AcademicTerm!.Name, + x.CourseId, + CourseCode = x.Course!.Code, + CourseName = x.Course.Name, + CollegeId = x.Course.CollegeId, + CollegeName = x.Course.College!.Name, + x.Capacity, + x.StartWeek, + x.EndWeek, + x.WeeklyHours, + x.Status, + TeacherNames = x.Teachers + .OrderByDescending(item => item.IsPrimary) + .Select(item => item.Teacher!.Name), + ClassNames = x.Classes.Select(item => item.AdministrativeClass!.Name), + StudentCount = x.Classes.Sum(item => + item.AdministrativeClass!.Students.Count(student => + student.Status == StudentStatus.Active)), + x.UpdatedAt + }) + .ToListAsync(cancellationToken); + return Ok(new PagedResult(items, total, page, pageSize)); + } + + [HttpGet("{id:guid}")] + public async Task GetOne(Guid id, CancellationToken cancellationToken) + { + var task = await ScopedTasks() + .AsNoTracking() + .Where(x => x.Id == id) + .Select(x => new + { + x.Id, + x.TaskNumber, + x.Name, + x.AcademicTermId, + TermName = x.AcademicTerm!.Name, + x.CourseId, + CourseCode = x.Course!.Code, + CourseName = x.Course.Name, + CollegeId = x.Course.CollegeId, + CollegeName = x.Course.College!.Name, + x.Capacity, + x.StartWeek, + x.EndWeek, + x.WeeklyHours, + x.Status, + x.Notes, + x.PublishedAt, + TeacherIds = x.Teachers.Select(item => item.TeacherId), + PrimaryTeacherId = x.Teachers + .Where(item => item.IsPrimary) + .Select(item => (Guid?)item.TeacherId) + .FirstOrDefault(), + Teachers = x.Teachers + .OrderByDescending(item => item.IsPrimary) + .ThenBy(item => item.Teacher!.TeacherNumber) + .Select(item => new + { + item.TeacherId, + item.Teacher!.TeacherNumber, + item.Teacher.Name, + item.Teacher.Title, + item.IsPrimary + }), + ClassIds = x.Classes.Select(item => item.AdministrativeClassId), + Classes = x.Classes + .OrderBy(item => item.AdministrativeClass!.Code) + .Select(item => new + { + item.AdministrativeClassId, + item.AdministrativeClass!.Code, + item.AdministrativeClass.Name, + item.AdministrativeClass.Grade, + StudentCount = item.AdministrativeClass.Students.Count(student => + student.Status == StudentStatus.Active) + }), + x.CreatedAt, + x.UpdatedAt + }) + .FirstOrDefaultAsync(cancellationToken); + return task is null ? NotFound() : Ok(task); + } + + [HttpPost] + public async Task Create( + TeachingTaskRequest request, + CancellationToken cancellationToken) + { + var validation = await ValidateRequestAsync(request, cancellationToken); + if (validation is not null) return validation; + var task = new TeachingTask + { + TaskNumber = request.TaskNumber.Trim(), + Name = request.Name.Trim(), + AcademicTermId = request.AcademicTermId, + CourseId = request.CourseId, + Capacity = request.Capacity, + StartWeek = request.StartWeek, + EndWeek = request.EndWeek, + WeeklyHours = request.WeeklyHours, + Notes = Normalize(request.Notes) + }; + SetAssignments(task, request); + db.TeachingTasks.Add(task); + return await SaveAsync(task.Id, true, cancellationToken); + } + + [HttpPut("{id:guid}")] + public async Task Update( + Guid id, + TeachingTaskRequest request, + CancellationToken cancellationToken) + { + var task = await ScopedTasks() + .Include(x => x.Teachers) + .Include(x => x.Classes) + .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + if (task is null) return NotFound(); + if (task.Status != TeachingTaskStatus.Draft) + return ConflictProblem("已发布或已结课的教学任务不可直接修改。"); + var validation = await ValidateRequestAsync(request, cancellationToken); + if (validation is not null) return validation; + + task.TaskNumber = request.TaskNumber.Trim(); + task.Name = request.Name.Trim(); + task.AcademicTermId = request.AcademicTermId; + task.CourseId = request.CourseId; + task.Capacity = request.Capacity; + task.StartWeek = request.StartWeek; + task.EndWeek = request.EndWeek; + task.WeeklyHours = request.WeeklyHours; + task.Notes = Normalize(request.Notes); + db.TeachingTaskTeachers.RemoveRange(task.Teachers); + db.TeachingTaskClasses.RemoveRange(task.Classes); + task.Teachers = []; + task.Classes = []; + SetAssignments(task, request); + return await SaveAsync(task.Id, false, cancellationToken); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id, CancellationToken cancellationToken) + { + var task = await ScopedTasks().FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + if (task is null) return NotFound(); + if (task.Status != TeachingTaskStatus.Draft) + return ConflictProblem("仅草稿教学任务可以删除。"); + db.TeachingTasks.Remove(task); + return await SaveAsync(id, false, cancellationToken); + } + + [HttpPost("{id:guid}/publish")] + public async Task Publish(Guid id, CancellationToken cancellationToken) + { + var task = await ScopedTasks() + .Include(x => x.Teachers) + .Include(x => x.Classes) + .ThenInclude(x => x.AdministrativeClass) + .ThenInclude(x => x!.Students) + .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + if (task is null) return NotFound(); + if (task.Status != TeachingTaskStatus.Draft) + return ConflictProblem("只有草稿教学任务可以发布。"); + if (task.Teachers.Count == 0 || task.Teachers.Count(x => x.IsPrimary) != 1) + return ConflictProblem("发布前必须指定且只能指定一名主讲教师。"); + if (task.Classes.Count == 0) + return ConflictProblem("发布前至少需要关联一个行政班。"); + var studentCount = task.Classes.Sum(x => + x.AdministrativeClass!.Students.Count(student => + student.Status == StudentStatus.Active)); + if (studentCount > task.Capacity) + return ConflictProblem($"教学班容量不足:关联班级共有 {studentCount} 名在籍学生。"); + + task.Status = TeachingTaskStatus.Published; + task.PublishedAt = DateTime.UtcNow; + return await SaveAsync(id, false, cancellationToken); + } + + [HttpPost("{id:guid}/close")] + public async Task Close(Guid id, CancellationToken cancellationToken) + { + var task = await ScopedTasks().FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + if (task is null) return NotFound(); + if (task.Status != TeachingTaskStatus.Published) + return ConflictProblem("只有已发布的教学任务可以结课。"); + task.Status = TeachingTaskStatus.Closed; + return await SaveAsync(id, false, cancellationToken); + } + + private IQueryable ScopedTasks() + { + var source = db.TeachingTasks.AsQueryable(); + var collegeId = ScopedCollegeId(); + return collegeId.HasValue + ? source.Where(x => x.Course!.CollegeId == collegeId.Value) + : source; + } + + private async Task ValidateRequestAsync( + TeachingTaskRequest request, + CancellationToken cancellationToken) + { + if (request.StartWeek > request.EndWeek) + return ValidationProblem("开始周不能晚于结束周。"); + var course = await db.Courses.AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == request.CourseId && x.IsEnabled, cancellationToken); + if (course is null) return ValidationProblem("所选课程不存在或已停用。"); + var collegeId = ScopedCollegeId(); + if (collegeId.HasValue && course.CollegeId != collegeId.Value) return Forbid(); + if (!await db.AcademicTerms.AnyAsync( + x => x.Id == request.AcademicTermId && x.IsEnabled, + cancellationToken)) + return ValidationProblem("所选学期不存在或已停用。"); + + var teacherIds = request.TeacherIds.Distinct().ToArray(); + if (request.PrimaryTeacherId.HasValue && + !teacherIds.Contains(request.PrimaryTeacherId.Value)) + return ValidationProblem("主讲教师必须包含在授课教师中。"); + if (await db.Teachers.CountAsync( + x => teacherIds.Contains(x.Id) && x.Status == TeacherStatus.Active, + cancellationToken) != teacherIds.Length) + return ValidationProblem("存在无效或非在职授课教师。"); + + var classIds = request.ClassIds.Distinct().ToArray(); + var classes = db.AdministrativeClasses.AsNoTracking() + .Where(x => classIds.Contains(x.Id)); + if (collegeId.HasValue) + classes = classes.Where(x => x.Major!.CollegeId == collegeId.Value); + if (await classes.CountAsync(cancellationToken) != classIds.Length) + return ValidationProblem("存在无效或不在当前数据范围内的行政班。"); + return null; + } + + private static void SetAssignments(TeachingTask task, TeachingTaskRequest request) + { + foreach (var teacherId in request.TeacherIds.Distinct()) + { + task.Teachers.Add(new TeachingTaskTeacher + { + TeacherId = teacherId, + IsPrimary = teacherId == request.PrimaryTeacherId + }); + } + foreach (var classId in request.ClassIds.Distinct()) + { + task.Classes.Add(new TeachingTaskClass + { + AdministrativeClassId = classId + }); + } + } + + private Guid? ScopedCollegeId() + { + if (!User.IsInRole(SystemRoles.CollegeAdmin)) return null; + return Guid.TryParse(User.FindFirstValue("college_id"), out var collegeId) + ? collegeId + : Guid.Empty; + } + + private async Task SaveAsync( + Guid id, + bool created, + CancellationToken cancellationToken) + { + try + { + await db.SaveChangesAsync(cancellationToken); + return created ? Created(string.Empty, new { id }) : NoContent(); + } + catch (DbUpdateException) + { + return ConflictProblem("教学任务编号重复,或关联数据已失效。"); + } + } + + private ActionResult ConflictProblem(string detail) => + Conflict(new ProblemDetails + { + Title = "无法完成操作", + Detail = detail, + Status = StatusCodes.Status409Conflict + }); + + private static string? Normalize(string? value) => + string.IsNullOrWhiteSpace(value) ? null : value.Trim(); +} + +public sealed record TeachingTaskRequest( + [Required, MaxLength(40)] string TaskNumber, + [Required, MaxLength(120)] string Name, + Guid AcademicTermId, + Guid CourseId, + [Range(1, 10000)] int Capacity, + [Range(1, 30)] int StartWeek, + [Range(1, 30)] int EndWeek, + [Range(1, 40)] int WeeklyHours, + IReadOnlyCollection TeacherIds, + Guid? PrimaryTeacherId, + IReadOnlyCollection ClassIds, + [MaxLength(500)] string? Notes); diff --git a/src/Jiaowu.Api/Domain/Academic/CurriculumEntities.cs b/src/Jiaowu.Api/Domain/Academic/CurriculumEntities.cs new file mode 100644 index 0000000..b625c78 --- /dev/null +++ b/src/Jiaowu.Api/Domain/Academic/CurriculumEntities.cs @@ -0,0 +1,52 @@ +using Jiaowu.Api.Domain.Common; + +namespace Jiaowu.Api.Domain.Academic; + +public sealed class CurriculumPlan : EntityBase +{ + public Guid MajorId { get; set; } + public Major? Major { get; set; } + public required string Name { get; set; } + public required string Version { get; set; } + public int EffectiveGrade { get; set; } + public decimal TotalCredits { get; set; } + public CurriculumPlanStatus Status { get; set; } = CurriculumPlanStatus.Draft; + public string? Description { get; set; } + public DateTime? PublishedAt { get; set; } + public ICollection Modules { get; set; } = []; +} + +public sealed class CurriculumModule : EntityBase +{ + public Guid CurriculumPlanId { get; set; } + public CurriculumPlan? CurriculumPlan { get; set; } + public required string Code { get; set; } + public required string Name { get; set; } + public decimal RequiredCredits { get; set; } + public int SortOrder { get; set; } + public ICollection Courses { get; set; } = []; +} + +public sealed class CurriculumCourse : EntityBase +{ + public Guid CurriculumModuleId { get; set; } + public CurriculumModule? CurriculumModule { get; set; } + public Guid CourseId { get; set; } + public Course? Course { get; set; } + public int RecommendedSemester { get; set; } + public CurriculumCourseType Type { get; set; } + public string? Notes { get; set; } +} + +public enum CurriculumPlanStatus +{ + Draft = 1, + Published = 2, + Archived = 3 +} + +public enum CurriculumCourseType +{ + Required = 1, + Elective = 2 +} diff --git a/src/Jiaowu.Api/Domain/Academic/OrganizationEntities.cs b/src/Jiaowu.Api/Domain/Academic/OrganizationEntities.cs index 11ac351..3405e32 100644 --- a/src/Jiaowu.Api/Domain/Academic/OrganizationEntities.cs +++ b/src/Jiaowu.Api/Domain/Academic/OrganizationEntities.cs @@ -28,6 +28,7 @@ public sealed class AdministrativeClass : CatalogEntity public Major? Major { get; set; } public int Grade { get; set; } public string? CounselorName { get; set; } + public ICollection Students { get; set; } = []; } public sealed class Building : CatalogEntity diff --git a/src/Jiaowu.Api/Domain/Academic/TeachingTaskEntities.cs b/src/Jiaowu.Api/Domain/Academic/TeachingTaskEntities.cs new file mode 100644 index 0000000..5a9a303 --- /dev/null +++ b/src/Jiaowu.Api/Domain/Academic/TeachingTaskEntities.cs @@ -0,0 +1,46 @@ +using Jiaowu.Api.Domain.Common; + +namespace Jiaowu.Api.Domain.Academic; + +public sealed class TeachingTask : EntityBase +{ + public required string TaskNumber { get; set; } + public required string Name { get; set; } + public Guid AcademicTermId { get; set; } + public AcademicTerm? AcademicTerm { get; set; } + public Guid CourseId { get; set; } + public Course? Course { get; set; } + public int Capacity { get; set; } + public int StartWeek { get; set; } = 1; + public int EndWeek { get; set; } = 16; + public int WeeklyHours { get; set; } = 2; + public TeachingTaskStatus Status { get; set; } = TeachingTaskStatus.Draft; + public string? Notes { get; set; } + public DateTime? PublishedAt { get; set; } + public ICollection Teachers { get; set; } = []; + public ICollection Classes { get; set; } = []; +} + +public sealed class TeachingTaskTeacher +{ + public Guid TeachingTaskId { get; set; } + public TeachingTask? TeachingTask { get; set; } + public Guid TeacherId { get; set; } + public Teacher? Teacher { get; set; } + public bool IsPrimary { get; set; } +} + +public sealed class TeachingTaskClass +{ + public Guid TeachingTaskId { get; set; } + public TeachingTask? TeachingTask { get; set; } + public Guid AdministrativeClassId { get; set; } + public AdministrativeClass? AdministrativeClass { get; set; } +} + +public enum TeachingTaskStatus +{ + Draft = 1, + Published = 2, + Closed = 3 +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs index 0e92bac..5566621 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs @@ -20,6 +20,12 @@ public sealed class AppDbContext(DbContextOptions options) public DbSet Teachers => Set(); public DbSet Students => Set(); public DbSet Courses => Set(); + public DbSet CurriculumPlans => Set(); + public DbSet CurriculumModules => Set(); + public DbSet CurriculumCourses => Set(); + public DbSet TeachingTasks => Set(); + public DbSet TeachingTaskTeachers => Set(); + public DbSet TeachingTaskClasses => Set(); public DbSet AuditLogs => Set(); protected override void OnModelCreating(ModelBuilder builder) @@ -111,7 +117,7 @@ public sealed class AppDbContext(DbContextOptions options) entity.HasIndex(x => new { x.AdministrativeClassId, x.Status }); entity.HasIndex(x => x.EnrollmentYear); entity.HasOne(x => x.AdministrativeClass) - .WithMany() + .WithMany(x => x.Students) .HasForeignKey(x => x.AdministrativeClassId) .OnDelete(DeleteBehavior.Restrict); entity.HasOne() @@ -132,6 +138,89 @@ public sealed class AppDbContext(DbContextOptions options) .OnDelete(DeleteBehavior.Restrict); }); + builder.Entity(entity => + { + entity.Property(x => x.Name).HasMaxLength(120); + entity.Property(x => x.Version).HasMaxLength(30); + entity.Property(x => x.TotalCredits).HasPrecision(6, 2); + entity.Property(x => x.Description).HasMaxLength(1000); + entity.HasIndex(x => new { x.MajorId, x.EffectiveGrade, x.Version }).IsUnique(); + entity.HasIndex(x => new { x.Status, x.EffectiveGrade }); + entity.HasOne(x => x.Major) + .WithMany() + .HasForeignKey(x => x.MajorId) + .OnDelete(DeleteBehavior.Restrict); + }); + + builder.Entity(entity => + { + entity.Property(x => x.Code).HasMaxLength(30); + entity.Property(x => x.Name).HasMaxLength(100); + entity.Property(x => x.RequiredCredits).HasPrecision(6, 2); + entity.HasIndex(x => new { x.CurriculumPlanId, x.Code }).IsUnique(); + entity.HasOne(x => x.CurriculumPlan) + .WithMany(x => x.Modules) + .HasForeignKey(x => x.CurriculumPlanId) + .OnDelete(DeleteBehavior.Cascade); + }); + + builder.Entity(entity => + { + entity.Property(x => x.Notes).HasMaxLength(500); + entity.HasIndex(x => new { x.CurriculumModuleId, x.CourseId }).IsUnique(); + entity.HasOne(x => x.CurriculumModule) + .WithMany(x => x.Courses) + .HasForeignKey(x => x.CurriculumModuleId) + .OnDelete(DeleteBehavior.Cascade); + entity.HasOne(x => x.Course) + .WithMany() + .HasForeignKey(x => x.CourseId) + .OnDelete(DeleteBehavior.Restrict); + }); + + builder.Entity(entity => + { + entity.Property(x => x.TaskNumber).HasMaxLength(40); + entity.Property(x => x.Name).HasMaxLength(120); + entity.Property(x => x.Notes).HasMaxLength(500); + entity.HasIndex(x => x.TaskNumber).IsUnique(); + entity.HasIndex(x => new { x.AcademicTermId, x.Status }); + entity.HasOne(x => x.AcademicTerm) + .WithMany() + .HasForeignKey(x => x.AcademicTermId) + .OnDelete(DeleteBehavior.Restrict); + entity.HasOne(x => x.Course) + .WithMany() + .HasForeignKey(x => x.CourseId) + .OnDelete(DeleteBehavior.Restrict); + }); + + builder.Entity(entity => + { + entity.HasKey(x => new { x.TeachingTaskId, x.TeacherId }); + entity.HasOne(x => x.TeachingTask) + .WithMany(x => x.Teachers) + .HasForeignKey(x => x.TeachingTaskId) + .OnDelete(DeleteBehavior.Cascade); + entity.HasOne(x => x.Teacher) + .WithMany() + .HasForeignKey(x => x.TeacherId) + .OnDelete(DeleteBehavior.Restrict); + }); + + builder.Entity(entity => + { + entity.HasKey(x => new { x.TeachingTaskId, x.AdministrativeClassId }); + entity.HasOne(x => x.TeachingTask) + .WithMany(x => x.Classes) + .HasForeignKey(x => x.TeachingTaskId) + .OnDelete(DeleteBehavior.Cascade); + entity.HasOne(x => x.AdministrativeClass) + .WithMany() + .HasForeignKey(x => x.AdministrativeClassId) + .OnDelete(DeleteBehavior.Restrict); + }); + builder.Entity(entity => { entity.Property(x => x.Method).HasMaxLength(10); diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/DatabaseInitializer.cs b/src/Jiaowu.Api/Infrastructure/Persistence/DatabaseInitializer.cs index 2a1ddd5..2ee99b6 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/DatabaseInitializer.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/DatabaseInitializer.cs @@ -288,6 +288,107 @@ public sealed class DatabaseInitializer( } await db.SaveChangesAsync(); + + if (!await db.CurriculumPlans.AnyAsync()) + { + var seededCourses = await db.Courses + .Where(x => x.Code == "CS101" || x.Code == "CS201" || x.Code == "CS305") + .ToDictionaryAsync(x => x.Code); + if (seededCourses.Count == 3) + { + db.CurriculumPlans.Add(new CurriculumPlan + { + MajorId = (await db.Majors.SingleAsync(x => x.Code == "080901")).Id, + Name = "计算机科学与技术专业培养方案", + Version = "2026版", + EffectiveGrade = 2026, + TotalCredits = 9.5m, + Status = CurriculumPlanStatus.Published, + PublishedAt = DateTime.UtcNow, + Description = "用于本地开发的精简培养方案示例。", + Modules = + [ + new CurriculumModule + { + Code = "BASIC", + Name = "专业基础课程", + RequiredCredits = 7.5m, + SortOrder = 10, + Courses = + [ + new CurriculumCourse + { + CourseId = seededCourses["CS101"].Id, + RecommendedSemester = 1, + Type = CurriculumCourseType.Required + }, + new CurriculumCourse + { + CourseId = seededCourses["CS201"].Id, + RecommendedSemester = 3, + Type = CurriculumCourseType.Required + } + ] + }, + new CurriculumModule + { + Code = "PRACTICE", + Name = "实践教学", + RequiredCredits = 2m, + SortOrder = 20, + Courses = + [ + new CurriculumCourse + { + CourseId = seededCourses["CS305"].Id, + RecommendedSemester = 5, + Type = CurriculumCourseType.Required + } + ] + } + ] + }); + await db.SaveChangesAsync(); + } + } + + if (!await db.TeachingTasks.AnyAsync()) + { + var term = await db.AcademicTerms.SingleAsync(x => x.IsCurrent); + var course = await db.Courses.SingleAsync(x => x.Code == "CS101"); + var teacher = await db.Teachers.SingleAsync(x => x.TeacherNumber == "T2026001"); + var administrativeClass = await db.AdministrativeClasses + .SingleAsync(x => x.Code == "CS2026-01"); + db.TeachingTasks.Add(new TeachingTask + { + TaskNumber = "2026-1-CS101-01", + Name = "程序设计基础教学班 01", + AcademicTermId = term.Id, + CourseId = course.Id, + Capacity = 60, + StartWeek = 1, + EndWeek = 16, + WeeklyHours = 4, + Status = TeachingTaskStatus.Published, + PublishedAt = DateTime.UtcNow, + Teachers = + [ + new TeachingTaskTeacher + { + TeacherId = teacher.Id, + IsPrimary = true + } + ], + Classes = + [ + new TeachingTaskClass + { + AdministrativeClassId = administrativeClass.Id + } + ] + }); + await db.SaveChangesAsync(); + } } private static void EnsureSucceeded(IdentityResult result, string action) diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs index 9298077..740f2dd 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs @@ -7,6 +7,8 @@ public sealed class DevelopmentSqliteMigrator( ILogger logger) { private const string PeopleAndCoursesMigration = "20260724_01_people_courses"; + private const string CurriculumPlansMigration = "20260724_02_curriculum_plans"; + private const string TeachingTasksMigration = "20260724_03_teaching_tasks"; public async Task MigrateAsync(CancellationToken cancellationToken = default) { @@ -24,12 +26,31 @@ public sealed class DevelopmentSqliteMigrator( """, cancellationToken); + await ApplyMigrationAsync( + PeopleAndCoursesMigration, + PeopleAndCoursesStatements, + cancellationToken); + await ApplyMigrationAsync( + CurriculumPlansMigration, + CurriculumPlansStatements, + cancellationToken); + await ApplyMigrationAsync( + TeachingTasksMigration, + TeachingTasksStatements, + cancellationToken); + } + + private async Task ApplyMigrationAsync( + string migrationId, + IEnumerable statements, + CancellationToken cancellationToken) + { var applied = await db.Database .SqlQuery( $""" SELECT "MigrationId" AS "Value" FROM "__DevelopmentSchemaHistory" - WHERE "MigrationId" = {PeopleAndCoursesMigration} + WHERE "MigrationId" = {migrationId} """) .AnyAsync(cancellationToken); if (applied) @@ -38,7 +59,7 @@ public sealed class DevelopmentSqliteMigrator( } await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken); - foreach (var statement in PeopleAndCoursesStatements) + foreach (var statement in statements) { await db.Database.ExecuteSqlRawAsync(statement, cancellationToken); } @@ -46,13 +67,13 @@ public sealed class DevelopmentSqliteMigrator( await db.Database.ExecuteSqlInterpolatedAsync( $""" INSERT INTO "__DevelopmentSchemaHistory" ("MigrationId", "AppliedAt") - VALUES ({PeopleAndCoursesMigration}, {DateTime.UtcNow}); + VALUES ({migrationId}, {DateTime.UtcNow}); """, cancellationToken); await transaction.CommitAsync(cancellationToken); logger.LogInformation( "Applied SQLite development schema migration {MigrationId}.", - PeopleAndCoursesMigration); + migrationId); } private static readonly string[] PeopleAndCoursesStatements = @@ -166,4 +187,149 @@ public sealed class DevelopmentSqliteMigrator( ON "Courses" ("CollegeId", "Nature"); """ ]; + + private static readonly string[] CurriculumPlansStatements = + [ + """ + CREATE TABLE IF NOT EXISTS "CurriculumPlans" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_CurriculumPlans" PRIMARY KEY, + "MajorId" TEXT NOT NULL, + "Name" TEXT NOT NULL, + "Version" TEXT NOT NULL, + "EffectiveGrade" INTEGER NOT NULL, + "TotalCredits" TEXT NOT NULL, + "Status" INTEGER NOT NULL, + "Description" TEXT NULL, + "PublishedAt" TEXT NULL, + "CreatedAt" TEXT NOT NULL, + "UpdatedAt" TEXT NOT NULL, + CONSTRAINT "FK_CurriculumPlans_Majors_MajorId" + FOREIGN KEY ("MajorId") REFERENCES "Majors" ("Id") ON DELETE RESTRICT + ); + """, + """ + CREATE UNIQUE INDEX IF NOT EXISTS "IX_CurriculumPlans_MajorId_EffectiveGrade_Version" + ON "CurriculumPlans" ("MajorId", "EffectiveGrade", "Version"); + """, + """ + CREATE INDEX IF NOT EXISTS "IX_CurriculumPlans_Status_EffectiveGrade" + ON "CurriculumPlans" ("Status", "EffectiveGrade"); + """, + """ + CREATE TABLE IF NOT EXISTS "CurriculumModules" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_CurriculumModules" PRIMARY KEY, + "CurriculumPlanId" TEXT NOT NULL, + "Code" TEXT NOT NULL, + "Name" TEXT NOT NULL, + "RequiredCredits" TEXT NOT NULL, + "SortOrder" INTEGER NOT NULL, + "CreatedAt" TEXT NOT NULL, + "UpdatedAt" TEXT NOT NULL, + CONSTRAINT "FK_CurriculumModules_CurriculumPlans_CurriculumPlanId" + FOREIGN KEY ("CurriculumPlanId") REFERENCES "CurriculumPlans" ("Id") + ON DELETE CASCADE + ); + """, + """ + CREATE UNIQUE INDEX IF NOT EXISTS "IX_CurriculumModules_CurriculumPlanId_Code" + ON "CurriculumModules" ("CurriculumPlanId", "Code"); + """, + """ + CREATE TABLE IF NOT EXISTS "CurriculumCourses" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_CurriculumCourses" PRIMARY KEY, + "CurriculumModuleId" TEXT NOT NULL, + "CourseId" TEXT NOT NULL, + "RecommendedSemester" INTEGER NOT NULL, + "Type" INTEGER NOT NULL, + "Notes" TEXT NULL, + "CreatedAt" TEXT NOT NULL, + "UpdatedAt" TEXT NOT NULL, + CONSTRAINT "FK_CurriculumCourses_CurriculumModules_CurriculumModuleId" + FOREIGN KEY ("CurriculumModuleId") REFERENCES "CurriculumModules" ("Id") + ON DELETE CASCADE, + CONSTRAINT "FK_CurriculumCourses_Courses_CourseId" + FOREIGN KEY ("CourseId") REFERENCES "Courses" ("Id") ON DELETE RESTRICT + ); + """, + """ + CREATE UNIQUE INDEX IF NOT EXISTS "IX_CurriculumCourses_CurriculumModuleId_CourseId" + ON "CurriculumCourses" ("CurriculumModuleId", "CourseId"); + """, + """ + CREATE INDEX IF NOT EXISTS "IX_CurriculumCourses_CourseId" + ON "CurriculumCourses" ("CourseId"); + """ + ]; + + private static readonly string[] TeachingTasksStatements = + [ + """ + CREATE TABLE IF NOT EXISTS "TeachingTasks" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_TeachingTasks" PRIMARY KEY, + "TaskNumber" TEXT NOT NULL, + "Name" TEXT NOT NULL, + "AcademicTermId" TEXT NOT NULL, + "CourseId" TEXT NOT NULL, + "Capacity" INTEGER NOT NULL, + "StartWeek" INTEGER NOT NULL, + "EndWeek" INTEGER NOT NULL, + "WeeklyHours" INTEGER NOT NULL, + "Status" INTEGER NOT NULL, + "Notes" TEXT NULL, + "PublishedAt" TEXT NULL, + "CreatedAt" TEXT NOT NULL, + "UpdatedAt" TEXT NOT NULL, + CONSTRAINT "FK_TeachingTasks_AcademicTerms_AcademicTermId" + FOREIGN KEY ("AcademicTermId") REFERENCES "AcademicTerms" ("Id") ON DELETE RESTRICT, + CONSTRAINT "FK_TeachingTasks_Courses_CourseId" + FOREIGN KEY ("CourseId") REFERENCES "Courses" ("Id") ON DELETE RESTRICT + ); + """, + """ + CREATE UNIQUE INDEX IF NOT EXISTS "IX_TeachingTasks_TaskNumber" + ON "TeachingTasks" ("TaskNumber"); + """, + """ + CREATE INDEX IF NOT EXISTS "IX_TeachingTasks_AcademicTermId_Status" + ON "TeachingTasks" ("AcademicTermId", "Status"); + """, + """ + CREATE INDEX IF NOT EXISTS "IX_TeachingTasks_CourseId" + ON "TeachingTasks" ("CourseId"); + """, + """ + CREATE TABLE IF NOT EXISTS "TeachingTaskTeachers" ( + "TeachingTaskId" TEXT NOT NULL, + "TeacherId" TEXT NOT NULL, + "IsPrimary" INTEGER NOT NULL, + CONSTRAINT "PK_TeachingTaskTeachers" + PRIMARY KEY ("TeachingTaskId", "TeacherId"), + CONSTRAINT "FK_TeachingTaskTeachers_TeachingTasks_TeachingTaskId" + FOREIGN KEY ("TeachingTaskId") REFERENCES "TeachingTasks" ("Id") ON DELETE CASCADE, + CONSTRAINT "FK_TeachingTaskTeachers_Teachers_TeacherId" + FOREIGN KEY ("TeacherId") REFERENCES "Teachers" ("Id") ON DELETE RESTRICT + ); + """, + """ + CREATE INDEX IF NOT EXISTS "IX_TeachingTaskTeachers_TeacherId" + ON "TeachingTaskTeachers" ("TeacherId"); + """, + """ + CREATE TABLE IF NOT EXISTS "TeachingTaskClasses" ( + "TeachingTaskId" TEXT NOT NULL, + "AdministrativeClassId" TEXT NOT NULL, + CONSTRAINT "PK_TeachingTaskClasses" + PRIMARY KEY ("TeachingTaskId", "AdministrativeClassId"), + CONSTRAINT "FK_TeachingTaskClasses_TeachingTasks_TeachingTaskId" + FOREIGN KEY ("TeachingTaskId") REFERENCES "TeachingTasks" ("Id") ON DELETE CASCADE, + CONSTRAINT "FK_TeachingTaskClasses_AdministrativeClasses_AdministrativeClassId" + FOREIGN KEY ("AdministrativeClassId") REFERENCES "AdministrativeClasses" ("Id") + ON DELETE RESTRICT + ); + """, + """ + CREATE INDEX IF NOT EXISTS "IX_TeachingTaskClasses_AdministrativeClassId" + ON "TeachingTaskClasses" ("AdministrativeClassId"); + """ + ]; } diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724051514_CurriculumPlans.Designer.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724051514_CurriculumPlans.Designer.cs new file mode 100644 index 0000000..5c25f22 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724051514_CurriculumPlans.Designer.cs @@ -0,0 +1,1165 @@ +// +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("20260724051514_CurriculumPlans")] + partial class CurriculumPlans + { + /// + 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("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("MajorId"); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("AdministrativeClasses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Building", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CampusId") + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CampusId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Buildings"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Campus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Address") + .HasColumnType("longtext"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Campuses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Classroom", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BuildingId") + .HasColumnType("char(36)"); + + b.Property("Capacity") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Equipment") + .HasColumnType("longtext"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RoomType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("BuildingId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Classrooms"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.College", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CampusId") + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ShortName") + .HasColumnType("longtext"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CampusId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Colleges"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Course", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AssessmentMethod") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CollegeId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Credits") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("EnglishName") + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LectureHours") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Nature") + .HasColumnType("int"); + + b.Property("PracticeHours") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("TotalHours") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("CollegeId", "Nature"); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Courses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.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.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.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.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.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.Academic.Major", "Major") + .WithMany() + .HasForeignKey("MajorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Major"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Building", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Campus", "Campus") + .WithMany() + .HasForeignKey("CampusId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Campus"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Classroom", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Building", "Building") + .WithMany() + .HasForeignKey("BuildingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Building"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.College", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Campus", "Campus") + .WithMany() + .HasForeignKey("CampusId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Campus"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Course", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.College", "College") + .WithMany() + .HasForeignKey("CollegeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("College"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.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.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.Student", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.AdministrativeClass", "AdministrativeClass") + .WithMany() + .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.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("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.CurriculumModule", b => + { + b.Navigation("Courses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumPlan", b => + { + b.Navigation("Modules"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724051514_CurriculumPlans.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724051514_CurriculumPlans.cs new file mode 100644 index 0000000..7d6269f --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724051514_CurriculumPlans.cs @@ -0,0 +1,140 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql +{ + /// + public partial class CurriculumPlans : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CurriculumPlans", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + MajorId = table.Column(type: "char(36)", nullable: false), + Name = table.Column(type: "varchar(120)", maxLength: 120, nullable: false), + Version = table.Column(type: "varchar(30)", maxLength: 30, nullable: false), + EffectiveGrade = table.Column(type: "int", nullable: false), + TotalCredits = table.Column(type: "decimal(6,2)", precision: 6, scale: 2, nullable: false), + Status = table.Column(type: "int", nullable: false), + Description = table.Column(type: "varchar(1000)", maxLength: 1000, nullable: true), + PublishedAt = table.Column(type: "datetime(6)", nullable: true), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CurriculumPlans", x => x.Id); + table.ForeignKey( + name: "FK_CurriculumPlans_Majors_MajorId", + column: x => x.MajorId, + principalTable: "Majors", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "CurriculumModules", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + CurriculumPlanId = table.Column(type: "char(36)", nullable: false), + Code = table.Column(type: "varchar(30)", maxLength: 30, nullable: false), + Name = table.Column(type: "varchar(100)", maxLength: 100, nullable: false), + RequiredCredits = table.Column(type: "decimal(6,2)", precision: 6, scale: 2, nullable: false), + SortOrder = table.Column(type: "int", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CurriculumModules", x => x.Id); + table.ForeignKey( + name: "FK_CurriculumModules_CurriculumPlans_CurriculumPlanId", + column: x => x.CurriculumPlanId, + principalTable: "CurriculumPlans", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "CurriculumCourses", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + CurriculumModuleId = table.Column(type: "char(36)", nullable: false), + CourseId = table.Column(type: "char(36)", nullable: false), + RecommendedSemester = table.Column(type: "int", nullable: false), + Type = table.Column(type: "int", nullable: false), + Notes = table.Column(type: "varchar(500)", maxLength: 500, nullable: true), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CurriculumCourses", x => x.Id); + table.ForeignKey( + name: "FK_CurriculumCourses_Courses_CourseId", + column: x => x.CourseId, + principalTable: "Courses", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_CurriculumCourses_CurriculumModules_CurriculumModuleId", + column: x => x.CurriculumModuleId, + principalTable: "CurriculumModules", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_CurriculumCourses_CourseId", + table: "CurriculumCourses", + column: "CourseId"); + + migrationBuilder.CreateIndex( + name: "IX_CurriculumCourses_CurriculumModuleId_CourseId", + table: "CurriculumCourses", + columns: new[] { "CurriculumModuleId", "CourseId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_CurriculumModules_CurriculumPlanId_Code", + table: "CurriculumModules", + columns: new[] { "CurriculumPlanId", "Code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_CurriculumPlans_MajorId_EffectiveGrade_Version", + table: "CurriculumPlans", + columns: new[] { "MajorId", "EffectiveGrade", "Version" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_CurriculumPlans_Status_EffectiveGrade", + table: "CurriculumPlans", + columns: new[] { "Status", "EffectiveGrade" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CurriculumCourses"); + + migrationBuilder.DropTable( + name: "CurriculumModules"); + + migrationBuilder.DropTable( + name: "CurriculumPlans"); + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724060331_TeachingTasks.Designer.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724060331_TeachingTasks.Designer.cs new file mode 100644 index 0000000..f575f54 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724060331_TeachingTasks.Designer.cs @@ -0,0 +1,1329 @@ +// +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("20260724060331_TeachingTasks")] + partial class TeachingTasks + { + /// + 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("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("MajorId"); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("AdministrativeClasses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Building", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CampusId") + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CampusId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Buildings"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Campus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Address") + .HasColumnType("longtext"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Campuses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Classroom", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BuildingId") + .HasColumnType("char(36)"); + + b.Property("Capacity") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Equipment") + .HasColumnType("longtext"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RoomType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("BuildingId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Classrooms"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.College", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CampusId") + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ShortName") + .HasColumnType("longtext"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CampusId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Colleges"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Course", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AssessmentMethod") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("CollegeId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Credits") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("EnglishName") + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LectureHours") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Nature") + .HasColumnType("int"); + + b.Property("PracticeHours") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("TotalHours") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("CollegeId", "Nature"); + + b.HasIndex("IsEnabled", "SortOrder"); + + b.ToTable("Courses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.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.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.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.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.Academic.Major", "Major") + .WithMany() + .HasForeignKey("MajorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Major"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Building", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Campus", "Campus") + .WithMany() + .HasForeignKey("CampusId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Campus"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Classroom", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Building", "Building") + .WithMany() + .HasForeignKey("BuildingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Building"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.College", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.Campus", "Campus") + .WithMany() + .HasForeignKey("CampusId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Campus"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Course", b => + { + b.HasOne("Jiaowu.Api.Domain.Academic.College", "College") + .WithMany() + .HasForeignKey("CollegeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("College"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.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.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.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.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.CurriculumModule", b => + { + b.Navigation("Courses"); + }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CurriculumPlan", b => + { + b.Navigation("Modules"); + }); + + 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/20260724060331_TeachingTasks.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724060331_TeachingTasks.cs new file mode 100644 index 0000000..2639eba --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260724060331_TeachingTasks.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql +{ + /// + public partial class TeachingTasks : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TeachingTasks", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + TaskNumber = table.Column(type: "varchar(40)", maxLength: 40, nullable: false), + Name = table.Column(type: "varchar(120)", maxLength: 120, nullable: false), + AcademicTermId = table.Column(type: "char(36)", nullable: false), + CourseId = table.Column(type: "char(36)", nullable: false), + Capacity = table.Column(type: "int", nullable: false), + StartWeek = table.Column(type: "int", nullable: false), + EndWeek = table.Column(type: "int", nullable: false), + WeeklyHours = table.Column(type: "int", nullable: false), + Status = table.Column(type: "int", nullable: false), + Notes = table.Column(type: "varchar(500)", maxLength: 500, nullable: true), + PublishedAt = table.Column(type: "datetime(6)", nullable: true), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TeachingTasks", x => x.Id); + table.ForeignKey( + name: "FK_TeachingTasks_AcademicTerms_AcademicTermId", + column: x => x.AcademicTermId, + principalTable: "AcademicTerms", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_TeachingTasks_Courses_CourseId", + column: x => x.CourseId, + principalTable: "Courses", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "TeachingTaskClasses", + columns: table => new + { + TeachingTaskId = table.Column(type: "char(36)", nullable: false), + AdministrativeClassId = table.Column(type: "char(36)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TeachingTaskClasses", x => new { x.TeachingTaskId, x.AdministrativeClassId }); + table.ForeignKey( + name: "FK_TeachingTaskClasses_AdministrativeClasses_AdministrativeClas~", + column: x => x.AdministrativeClassId, + principalTable: "AdministrativeClasses", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_TeachingTaskClasses_TeachingTasks_TeachingTaskId", + column: x => x.TeachingTaskId, + principalTable: "TeachingTasks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "TeachingTaskTeachers", + columns: table => new + { + TeachingTaskId = table.Column(type: "char(36)", nullable: false), + TeacherId = table.Column(type: "char(36)", nullable: false), + IsPrimary = table.Column(type: "tinyint(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TeachingTaskTeachers", x => new { x.TeachingTaskId, x.TeacherId }); + table.ForeignKey( + name: "FK_TeachingTaskTeachers_Teachers_TeacherId", + column: x => x.TeacherId, + principalTable: "Teachers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_TeachingTaskTeachers_TeachingTasks_TeachingTaskId", + column: x => x.TeachingTaskId, + principalTable: "TeachingTasks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_TeachingTaskClasses_AdministrativeClassId", + table: "TeachingTaskClasses", + column: "AdministrativeClassId"); + + migrationBuilder.CreateIndex( + name: "IX_TeachingTasks_AcademicTermId_Status", + table: "TeachingTasks", + columns: new[] { "AcademicTermId", "Status" }); + + migrationBuilder.CreateIndex( + name: "IX_TeachingTasks_CourseId", + table: "TeachingTasks", + column: "CourseId"); + + migrationBuilder.CreateIndex( + name: "IX_TeachingTasks_TaskNumber", + table: "TeachingTasks", + column: "TaskNumber", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_TeachingTaskTeachers_TeacherId", + table: "TeachingTaskTeachers", + column: "TeacherId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TeachingTaskClasses"); + + migrationBuilder.DropTable( + name: "TeachingTaskTeachers"); + + migrationBuilder.DropTable( + name: "TeachingTasks"); + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs index ca54a64..5f55589 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs @@ -72,7 +72,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasIndex("IsEnabled", "SortOrder"); - b.ToTable("AcademicTerms", (string)null); + b.ToTable("AcademicTerms"); }); modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AdministrativeClass", b => @@ -121,7 +121,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasIndex("IsEnabled", "SortOrder"); - b.ToTable("AdministrativeClasses", (string)null); + b.ToTable("AdministrativeClasses"); }); modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Building", b => @@ -164,7 +164,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasIndex("IsEnabled", "SortOrder"); - b.ToTable("Buildings", (string)null); + b.ToTable("Buildings"); }); modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Campus", b => @@ -205,7 +205,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasIndex("IsEnabled", "SortOrder"); - b.ToTable("Campuses", (string)null); + b.ToTable("Campuses"); }); modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Classroom", b => @@ -258,7 +258,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasIndex("IsEnabled", "SortOrder"); - b.ToTable("Classrooms", (string)null); + b.ToTable("Classrooms"); }); modelBuilder.Entity("Jiaowu.Api.Domain.Academic.College", b => @@ -304,7 +304,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasIndex("IsEnabled", "SortOrder"); - b.ToTable("Colleges", (string)null); + b.ToTable("Colleges"); }); modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Course", b => @@ -374,7 +374,137 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasIndex("IsEnabled", "SortOrder"); - b.ToTable("Courses", (string)null); + b.ToTable("Courses"); + }); + + 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.Major", b => @@ -424,7 +554,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasIndex("IsEnabled", "SortOrder"); - b.ToTable("Majors", (string)null); + b.ToTable("Majors"); }); modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Student", b => @@ -493,7 +623,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasIndex("AdministrativeClassId", "Status"); - b.ToTable("Students", (string)null); + b.ToTable("Students"); }); modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Teacher", b => @@ -561,7 +691,102 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasIndex("CollegeId", "Status"); - b.ToTable("Teachers", (string)null); + 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 => @@ -726,7 +951,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasIndex("CreatedAt"); - b.ToTable("AuditLogs", (string)null); + b.ToTable("AuditLogs"); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => @@ -882,6 +1107,47 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.Navigation("College"); }); + 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.Major", b => { b.HasOne("Jiaowu.Api.Domain.Academic.College", "College") @@ -896,7 +1162,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Student", b => { b.HasOne("Jiaowu.Api.Domain.Academic.AdministrativeClass", "AdministrativeClass") - .WithMany() + .WithMany("Students") .HasForeignKey("AdministrativeClassId") .OnDelete(DeleteBehavior.Restrict) .IsRequired(); @@ -925,6 +1191,63 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql 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) @@ -975,6 +1298,28 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); + + modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AdministrativeClass", b => + { + b.Navigation("Students"); + }); + + 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.TeachingTask", b => + { + b.Navigation("Classes"); + + b.Navigation("Teachers"); + }); #pragma warning restore 612, 618 } } diff --git a/tests/Jiaowu.Api.Tests/PersistenceTests.cs b/tests/Jiaowu.Api.Tests/PersistenceTests.cs index 272d939..f8f80aa 100644 --- a/tests/Jiaowu.Api.Tests/PersistenceTests.cs +++ b/tests/Jiaowu.Api.Tests/PersistenceTests.cs @@ -89,18 +89,41 @@ public sealed class PersistenceTests : IAsyncLifetime MajorId = major.Id, Grade = 2026 }; + var course = new Course + { + Code = "CS101", + Name = "程序设计基础", + CollegeId = college.Id, + Credits = 4, + TotalHours = 64, + LectureHours = 40, + PracticeHours = 24, + Nature = CourseNature.MajorRequired, + AssessmentMethod = AssessmentMethod.Examination + }; + var teacher = new Teacher + { + TeacherNumber = "T2026001", + Name = "陈老师", + CollegeId = college.Id, + Status = TeacherStatus.Active + }; + var term = new AcademicTerm + { + Code = "2026-1", + Name = "2026—2027 学年第一学期", + AcademicYear = "2026-2027", + Season = TermSeason.Autumn, + StartDate = new DateOnly(2026, 9, 7), + EndDate = new DateOnly(2027, 1, 17), + IsCurrent = true + }; _db.AddRange( campus, college, major, administrativeClass, - new Teacher - { - TeacherNumber = "T2026001", - Name = "陈老师", - CollegeId = college.Id, - Status = TeacherStatus.Active - }, + teacher, new Student { StudentNumber = "202601001", @@ -110,30 +133,77 @@ public sealed class PersistenceTests : IAsyncLifetime EnrollmentDate = new DateOnly(2026, 9, 7), Status = StudentStatus.Active }, - new Course - { - Code = "CS101", - Name = "程序设计基础", - CollegeId = college.Id, - Credits = 4, - TotalHours = 64, - LectureHours = 40, - PracticeHours = 24, - Nature = CourseNature.MajorRequired, - AssessmentMethod = AssessmentMethod.Examination - }); + course, + term); await _db.SaveChangesAsync(); - var teacher = await _db.Teachers.Include(x => x.College).SingleAsync(); + _db.CurriculumPlans.Add(new CurriculumPlan + { + MajorId = major.Id, + Name = "计科培养方案", + Version = "2026版", + EffectiveGrade = 2026, + TotalCredits = 4, + Status = CurriculumPlanStatus.Draft, + Modules = + [ + new CurriculumModule + { + Code = "BASIC", + Name = "专业基础", + RequiredCredits = 4, + Courses = + [ + new CurriculumCourse + { + CourseId = course.Id, + RecommendedSemester = 1, + Type = CurriculumCourseType.Required + } + ] + } + ] + }); + _db.TeachingTasks.Add(new TeachingTask + { + TaskNumber = "2026-1-CS101-01", + Name = "程序设计基础教学班", + AcademicTermId = term.Id, + CourseId = course.Id, + Capacity = 60, + Teachers = + [ + new TeachingTaskTeacher { TeacherId = teacher.Id, IsPrimary = true } + ], + Classes = + [ + new TeachingTaskClass { AdministrativeClassId = administrativeClass.Id } + ] + }); + await _db.SaveChangesAsync(); + + var savedTeacher = await _db.Teachers.Include(x => x.College).SingleAsync(); var student = await _db.Students .Include(x => x.AdministrativeClass) .ThenInclude(x => x!.Major) .SingleAsync(); - var course = await _db.Courses.Include(x => x.College).SingleAsync(); + var savedCourse = await _db.Courses.Include(x => x.College).SingleAsync(); + var curriculumPlan = await _db.CurriculumPlans + .Include(x => x.Modules) + .ThenInclude(x => x.Courses) + .SingleAsync(); + var teachingTask = await _db.TeachingTasks + .Include(x => x.Teachers) + .Include(x => x.Classes) + .SingleAsync(); - Assert.Equal("计算机学院", teacher.College!.Name); + Assert.Equal("计算机学院", savedTeacher.College!.Name); Assert.Equal("计算机科学与技术", student.AdministrativeClass!.Major!.Name); - Assert.Equal(4m, course.Credits); - Assert.Equal(course.TotalHours, course.LectureHours + course.PracticeHours); + Assert.Equal(4m, savedCourse.Credits); + Assert.Equal(savedCourse.TotalHours, savedCourse.LectureHours + savedCourse.PracticeHours); + Assert.Single(curriculumPlan.Modules); + Assert.Single(curriculumPlan.Modules.Single().Courses); + Assert.Single(teachingTask.Teachers); + Assert.Single(teachingTask.Classes); } } diff --git a/web/src/layouts/AdminLayout.vue b/web/src/layouts/AdminLayout.vue index 723b806..2e03d3e 100644 --- a/web/src/layouts/AdminLayout.vue +++ b/web/src/layouts/AdminLayout.vue @@ -6,6 +6,8 @@ import { DataAnalysis, OfficeBuilding, Operation, + Reading, + Tickets, User, UserFilled, } from '@element-plus/icons-vue' @@ -23,6 +25,8 @@ const pageTitle = computed(() => { 'base-data': '基础数据', personnel: '人员档案', courses: '课程库', + curriculum: '培养方案', + 'teaching-tasks': '教学任务', users: '用户与权限', } return titles[String(route.name)] ?? '教务管理' @@ -80,6 +84,20 @@ onMounted(() => auth.refresh().catch(() => undefined)) + + + + + + + + @@ -87,8 +105,8 @@ onMounted(() => auth.refresh().catch(() => undefined))
- 第一阶段 · 基础底座 -

组织、权限和教学资源正在运行

+ 第一阶段 · 核心可用版 +

主数据与培养方案正在运行

diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 811498e..be64311 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -39,6 +39,18 @@ const router = createRouter({ name: 'courses', component: () => import('../views/CoursesView.vue'), }, + { + path: 'curriculum', + name: 'curriculum', + component: () => import('../views/CurriculumView.vue'), + meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'] }, + }, + { + path: 'teaching-tasks', + name: 'teaching-tasks', + component: () => import('../views/TeachingTasksView.vue'), + meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'] }, + }, { path: 'users', name: 'users', diff --git a/web/src/style.css b/web/src/style.css index 49b523a..87b7690 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -102,6 +102,7 @@ button { cursor: pointer; } .phase-line span.active::before { background: var(--amber); box-shadow: 0 0 0 4px #f5ead6; } .page-stack { display: grid; gap: 18px; } +.page-stack > * { min-width: 0; } .page-intro { min-height: 106px; padding: 8px 4px; display: flex; align-items: center; justify-content: space-between; gap: 20px; } .page-intro h2 { margin: 7px 0 7px; font-family: "STZhongsong", "Songti SC", serif; font-size: 27px; } .page-intro p { margin: 0; color: var(--muted); font-size: 13px; } @@ -156,6 +157,55 @@ button { cursor: pointer; } .ledger-rule { display: flex; align-items: center; gap: 0; position: relative; z-index: 1; } .ledger-rule span { padding: 8px 28px; border-right: 1px solid rgba(255,255,255,.12); color: #d2d8e9; font-size: 11px; letter-spacing: .08em; } +.curriculum-filter { padding: 14px 18px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; border: 1px solid var(--line); background: white; } +.curriculum-filter > .el-input { width: 260px; } +.curriculum-filter > .el-select { width: 170px; } +.curriculum-workspace { min-height: 620px; display: grid; grid-template-columns: 285px minmax(0, 1fr); border: 1px solid var(--line); background: white; } +.plan-list { min-width: 0; display: flex; flex-direction: column; border-right: 1px solid var(--line); background: #fafbfc; } +.plan-list > button { position: relative; padding: 19px 20px; display: grid; gap: 6px; text-align: left; border: none; border-bottom: 1px solid var(--line); background: transparent; } +.plan-list > button:hover { background: white; } +.plan-list > button.active { padding-left: 17px; border-left: 3px solid var(--teal); background: white; } +.plan-list > button span { padding-right: 55px; color: var(--muted); font-size: 10px; } +.plan-list > button b { font-size: 13px; line-height: 1.5; } +.plan-list > button small { color: #969eac; font-size: 10px; } +.plan-list > button i { position: absolute; right: 14px; top: 16px; padding: 3px 7px; border-radius: 10px; color: #777f8d; background: #eef0f3; font-size: 9px; font-style: normal; } +.plan-list > button i.published { color: var(--teal); background: #e5f4f1; } +.plan-list > button i.archived { color: #8d7760; background: #f2ece4; } +.plan-pagination { margin-top: auto; min-height: 58px; padding: 10px 14px; display: flex; align-items: center; justify-content: space-between; border-top: 1px solid var(--line); } +.plan-pagination > span { color: var(--muted); font-size: 10px; } +.plan-detail { min-width: 0; padding: 26px; } +.plan-detail-head { display: flex; justify-content: space-between; gap: 20px; padding-bottom: 21px; border-bottom: 1px solid var(--line); } +.plan-detail-head > div:first-child > span { color: var(--teal); font-size: 10px; letter-spacing: .08em; } +.plan-detail-head h3 { margin: 7px 0 5px; font-family: "STZhongsong", "Songti SC", serif; font-size: 23px; } +.plan-detail-head p { margin: 0; color: var(--muted); font-size: 11px; } +.plan-actions { display: flex; flex-wrap: wrap; align-content: flex-start; justify-content: flex-end; gap: 7px; } +.plan-actions .el-button + .el-button { margin-left: 0; } +.plan-metrics { margin: 18px 0; display: grid; grid-template-columns: repeat(4, 1fr); border: 1px solid var(--line); } +.plan-metrics > div { padding: 15px 17px; border-right: 1px solid var(--line); } +.plan-metrics > div:last-child { border-right: none; } +.plan-metrics span, .plan-metrics b { display: block; } +.plan-metrics span { color: var(--muted); font-size: 10px; } +.plan-metrics b { margin-top: 6px; font: 700 18px/1.2 Consolas, monospace; } +.plan-metrics b.warning { color: #b36f12; } +.module-toolbar { margin: 25px 0 12px; display: flex; align-items: center; justify-content: space-between; } +.module-toolbar b, .module-toolbar span { display: block; } +.module-toolbar b { font-size: 15px; } +.module-toolbar span { margin-top: 4px; color: var(--muted); font-size: 10px; } +.curriculum-module { margin-top: 12px; border: 1px solid var(--line); } +.curriculum-module > header { min-height: 72px; padding: 13px 16px; display: flex; align-items: center; gap: 18px; background: #f8fafb; border-bottom: 1px solid var(--line); } +.curriculum-module > header > div:first-child { min-width: 160px; } +.curriculum-module > header span { color: var(--teal); font: 700 9px/1 Consolas, monospace; letter-spacing: .12em; } +.curriculum-module h4 { margin: 6px 0 0; font-size: 14px; } +.curriculum-module > header p { margin: 0 auto 0 0; color: var(--muted); font-size: 11px; } +.curriculum-module > header > div:last-child { display: flex; align-items: center; white-space: nowrap; } +.curriculum-course-table { min-height: 80px; } +.task-summary { min-height: 82px; padding: 15px 22px; display: flex; align-items: center; gap: 28px; color: white; background: linear-gradient(108deg, #17295a, #263f80); } +.task-summary > div { min-width: 170px; display: flex; align-items: baseline; gap: 8px; } +.task-summary span, .task-summary small { color: #b8c1de; font-size: 10px; } +.task-summary b { margin-left: auto; color: #58d1c0; font: 700 30px/1 Consolas, monospace; } +.task-summary p { margin: 0; padding-left: 28px; border-left: 1px solid rgba(255,255,255,.16); color: #d4daeb; font-size: 11px; letter-spacing: .03em; } +.muted-action { color: #9aa1ae; font-size: 11px; } + .login-page { min-height: 100vh; display: grid; grid-template-columns: minmax(440px, 1.2fr) minmax(420px, .8fr); background: white; } .login-story { min-height: 100vh; padding: 54px clamp(45px, 6vw, 90px); display: flex; flex-direction: column; color: white; background: linear-gradient(142deg, #13224d, #243a77 62%, #176b71); overflow: hidden; position: relative; } .login-story::before { content: ""; position: absolute; inset: 0; opacity: .28; background-image: linear-gradient(rgba(255,255,255,.06) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.06) 1px, transparent 1px); background-size: 46px 46px; } @@ -222,6 +272,22 @@ button { cursor: pointer; } .ledger-rule { overflow-x: auto; } .ledger-rule span { padding: 12px 20px; white-space: nowrap; } .data-card { overflow: hidden; } + .curriculum-filter > .el-input { width: 100%; } + .curriculum-filter > .el-select { flex: 1 1 140px; width: auto; } + .curriculum-workspace { display: block; } + .plan-list { max-height: 310px; overflow-y: auto; border-right: none; border-bottom: 1px solid var(--line); } + .plan-pagination { position: sticky; bottom: 0; background: #fafbfc; } + .plan-detail { padding: 18px 14px; } + .plan-detail-head { display: block; } + .plan-actions { margin-top: 16px; justify-content: flex-start; } + .plan-metrics { grid-template-columns: 1fr 1fr; } + .plan-metrics > div:nth-child(2) { border-right: none; } + .plan-metrics > div:nth-child(-n+2) { border-bottom: 1px solid var(--line); } + .curriculum-module > header { align-items: flex-start; flex-wrap: wrap; } + .curriculum-module > header p { order: 3; width: 100%; } + .task-summary { align-items: flex-start; flex-direction: column; gap: 12px; } + .task-summary > div { width: 100%; } + .task-summary p { padding: 12px 0 0; border-left: none; border-top: 1px solid rgba(255,255,255,.16); line-height: 1.6; } .form-grid, .form-grid.three { grid-template-columns: 1fr; gap: 0; } .el-dialog { width: calc(100vw - 24px) !important; } .login-page { display: block; min-height: 100vh; background: #f4f6f9; } diff --git a/web/src/views/CurriculumView.vue b/web/src/views/CurriculumView.vue new file mode 100644 index 0000000..9e3ebfb --- /dev/null +++ b/web/src/views/CurriculumView.vue @@ -0,0 +1,453 @@ + + + diff --git a/web/src/views/DashboardView.vue b/web/src/views/DashboardView.vue index 58afddb..410f8d9 100644 --- a/web/src/views/DashboardView.vue +++ b/web/src/views/DashboardView.vue @@ -86,18 +86,28 @@ onMounted(async () => { 教师档案 + 学生档案 + 课程库 已建立 +
+ 培养方案 + {{ data.counts.curriculumPlans ?? 0 }} 个版本 · 模块化课程结构 + 已建立 +
+
+ 教学任务 + {{ data.counts.teachingTasks ?? 0 }} 个教学班 · 教师与行政班 + 已建立 +
NEXT MILESTONE

下一段业务链

-

人员与课程主数据已就绪,下一步进入培养方案、课程开设与教学任务。

+

学期教学任务已可发布,下一步进入时间、教室冲突校验与课表发布。

基础底座 人员档案 - 培养方案 - 教学运行 + 培养方案 + 教学任务
diff --git a/web/src/views/TeachingTasksView.vue b/web/src/views/TeachingTasksView.vue new file mode 100644 index 0000000..c600903 --- /dev/null +++ b/web/src/views/TeachingTasksView.vue @@ -0,0 +1,284 @@ + + +