培养方案:专业/年级版本、课程模块、学分校验、复制版本、发布锁定、旧版本归档。

教学任务:学期开课、授课教师、唯一主讲、合班、容量与周次校验、发布及结课。
SQLite 开发库已自动升级,无需删除原数据库。
新增对应 MySQL EF Core 迁移。
Vue 已编译进 wwwroot,仍可只运行 ASP.NET Core 单服务。
桌面端及 390px 窄屏页面验证通过,无前端控制台错误。
This commit is contained in:
2026-07-24 14:10:43 +08:00 Unverified
parent 30d793773c
commit 628944f64d
23 changed files with 5430 additions and 47 deletions
@@ -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<ActionResult<PagedResult<object>>> 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<object>(items, total, page, pageSize));
}
[HttpGet("{id:guid}")]
public async Task<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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<CurriculumPlan> ScopedPlans()
{
var source = db.CurriculumPlans.AsQueryable();
var collegeId = ScopedCollegeId();
return collegeId.HasValue
? source.Where(x => x.Major!.CollegeId == collegeId.Value)
: source;
}
private async Task<Major?> 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<CurriculumPlan?> EditablePlanAsync(
Guid id,
CancellationToken cancellationToken) =>
await ScopedPlans().FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
private async Task<CurriculumPlan?> 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<ActionResult> SaveCreatedAsync(Guid id, CancellationToken cancellationToken)
{
try
{
await db.SaveChangesAsync(cancellationToken);
return Created(string.Empty, new { id });
}
catch (DbUpdateException)
{
return ConflictProblem("版本、模块编码或课程配置重复。");
}
}
private async Task<ActionResult> 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);
@@ -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)
}
};
@@ -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<ActionResult<PagedResult<object>>> 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<object>(items, total, page, pageSize));
}
[HttpGet("{id:guid}")]
public async Task<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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<TeachingTask> ScopedTasks()
{
var source = db.TeachingTasks.AsQueryable();
var collegeId = ScopedCollegeId();
return collegeId.HasValue
? source.Where(x => x.Course!.CollegeId == collegeId.Value)
: source;
}
private async Task<ActionResult?> 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<ActionResult> 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<Guid> TeacherIds,
Guid? PrimaryTeacherId,
IReadOnlyCollection<Guid> ClassIds,
[MaxLength(500)] string? Notes);
@@ -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<CurriculumModule> 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<CurriculumCourse> 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
}
@@ -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<Student> Students { get; set; } = [];
}
public sealed class Building : CatalogEntity
@@ -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<TeachingTaskTeacher> Teachers { get; set; } = [];
public ICollection<TeachingTaskClass> 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
}
@@ -20,6 +20,12 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
public DbSet<Teacher> Teachers => Set<Teacher>();
public DbSet<Student> Students => Set<Student>();
public DbSet<Course> Courses => Set<Course>();
public DbSet<CurriculumPlan> CurriculumPlans => Set<CurriculumPlan>();
public DbSet<CurriculumModule> CurriculumModules => Set<CurriculumModule>();
public DbSet<CurriculumCourse> CurriculumCourses => Set<CurriculumCourse>();
public DbSet<TeachingTask> TeachingTasks => Set<TeachingTask>();
public DbSet<TeachingTaskTeacher> TeachingTaskTeachers => Set<TeachingTaskTeacher>();
public DbSet<TeachingTaskClass> TeachingTaskClasses => Set<TeachingTaskClass>();
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
protected override void OnModelCreating(ModelBuilder builder)
@@ -111,7 +117,7 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> 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<ApplicationUser>()
@@ -132,6 +138,89 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
.OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<CurriculumPlan>(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<CurriculumModule>(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<CurriculumCourse>(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<TeachingTask>(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<TeachingTaskTeacher>(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<TeachingTaskClass>(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<AuditLog>(entity =>
{
entity.Property(x => x.Method).HasMaxLength(10);
@@ -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)
@@ -7,6 +7,8 @@ public sealed class DevelopmentSqliteMigrator(
ILogger<DevelopmentSqliteMigrator> 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<string> statements,
CancellationToken cancellationToken)
{
var applied = await db.Database
.SqlQuery<string>(
$"""
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");
"""
];
}
@@ -0,0 +1,140 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
{
/// <inheritdoc />
public partial class CurriculumPlans : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "CurriculumPlans",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
MajorId = table.Column<Guid>(type: "char(36)", nullable: false),
Name = table.Column<string>(type: "varchar(120)", maxLength: 120, nullable: false),
Version = table.Column<string>(type: "varchar(30)", maxLength: 30, nullable: false),
EffectiveGrade = table.Column<int>(type: "int", nullable: false),
TotalCredits = table.Column<decimal>(type: "decimal(6,2)", precision: 6, scale: 2, nullable: false),
Status = table.Column<int>(type: "int", nullable: false),
Description = table.Column<string>(type: "varchar(1000)", maxLength: 1000, nullable: true),
PublishedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedAt = table.Column<DateTime>(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<Guid>(type: "char(36)", nullable: false),
CurriculumPlanId = table.Column<Guid>(type: "char(36)", nullable: false),
Code = table.Column<string>(type: "varchar(30)", maxLength: 30, nullable: false),
Name = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false),
RequiredCredits = table.Column<decimal>(type: "decimal(6,2)", precision: 6, scale: 2, nullable: false),
SortOrder = table.Column<int>(type: "int", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedAt = table.Column<DateTime>(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<Guid>(type: "char(36)", nullable: false),
CurriculumModuleId = table.Column<Guid>(type: "char(36)", nullable: false),
CourseId = table.Column<Guid>(type: "char(36)", nullable: false),
RecommendedSemester = table.Column<int>(type: "int", nullable: false),
Type = table.Column<int>(type: "int", nullable: false),
Notes = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedAt = table.Column<DateTime>(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" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CurriculumCourses");
migrationBuilder.DropTable(
name: "CurriculumModules");
migrationBuilder.DropTable(
name: "CurriculumPlans");
}
}
}
@@ -0,0 +1,142 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
{
/// <inheritdoc />
public partial class TeachingTasks : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "TeachingTasks",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
TaskNumber = table.Column<string>(type: "varchar(40)", maxLength: 40, nullable: false),
Name = table.Column<string>(type: "varchar(120)", maxLength: 120, nullable: false),
AcademicTermId = table.Column<Guid>(type: "char(36)", nullable: false),
CourseId = table.Column<Guid>(type: "char(36)", nullable: false),
Capacity = table.Column<int>(type: "int", nullable: false),
StartWeek = table.Column<int>(type: "int", nullable: false),
EndWeek = table.Column<int>(type: "int", nullable: false),
WeeklyHours = table.Column<int>(type: "int", nullable: false),
Status = table.Column<int>(type: "int", nullable: false),
Notes = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true),
PublishedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedAt = table.Column<DateTime>(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<Guid>(type: "char(36)", nullable: false),
AdministrativeClassId = table.Column<Guid>(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<Guid>(type: "char(36)", nullable: false),
TeacherId = table.Column<Guid>(type: "char(36)", nullable: false),
IsPrimary = table.Column<bool>(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");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "TeachingTaskClasses");
migrationBuilder.DropTable(
name: "TeachingTaskTeachers");
migrationBuilder.DropTable(
name: "TeachingTasks");
}
}
}
@@ -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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<Guid>("CourseId")
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("CurriculumModuleId")
.HasColumnType("char(36)");
b.Property<string>("Notes")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<int>("RecommendedSemester")
.HasColumnType("int");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<DateTime>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("varchar(30)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("CurriculumPlanId")
.HasColumnType("char(36)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<decimal>("RequiredCredits")
.HasPrecision(6, 2)
.HasColumnType("decimal(6,2)");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<DateTime>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("varchar(1000)");
b.Property<int>("EffectiveGrade")
.HasColumnType("int");
b.Property<Guid>("MajorId")
.HasColumnType("char(36)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("varchar(120)");
b.Property<DateTime?>("PublishedAt")
.HasColumnType("datetime(6)");
b.Property<int>("Status")
.HasColumnType("int");
b.Property<decimal>("TotalCredits")
.HasPrecision(6, 2)
.HasColumnType("decimal(6,2)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<Guid>("AcademicTermId")
.HasColumnType("char(36)");
b.Property<int>("Capacity")
.HasColumnType("int");
b.Property<Guid>("CourseId")
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("EndWeek")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("varchar(120)");
b.Property<string>("Notes")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<DateTime?>("PublishedAt")
.HasColumnType("datetime(6)");
b.Property<int>("StartWeek")
.HasColumnType("int");
b.Property<int>("Status")
.HasColumnType("int");
b.Property<string>("TaskNumber")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("varchar(40)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("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<Guid>("TeachingTaskId")
.HasColumnType("char(36)");
b.Property<Guid>("AdministrativeClassId")
.HasColumnType("char(36)");
b.HasKey("TeachingTaskId", "AdministrativeClassId");
b.HasIndex("AdministrativeClassId");
b.ToTable("TeachingTaskClasses");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskTeacher", b =>
{
b.Property<Guid>("TeachingTaskId")
.HasColumnType("char(36)");
b.Property<Guid>("TeacherId")
.HasColumnType("char(36)");
b.Property<bool>("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<System.Guid>", 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<System.Guid>", 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
}
}