3
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
面向普通高校的教务管理系统。后端使用 ASP.NET Core 10、EF Core 10,前端使用 Vue 3、TypeScript 和 Element Plus。
|
面向普通高校的教务管理系统。后端使用 ASP.NET Core 10、EF Core 10,前端使用 Vue 3、TypeScript 和 Element Plus。
|
||||||
|
|
||||||
|
当前已实现系统登录与角色权限、基础数据、用户管理、教师档案、学生档案、课程库和首页统计。人员及课程列表支持组合筛选、服务端分页和完整增删改查。
|
||||||
|
|
||||||
## 本地开发:热更新模式
|
## 本地开发:热更新模式
|
||||||
|
|
||||||
本地开发固定使用 SQLite。首次启动会自动创建 `src/Jiaowu.Api/data/jiaowu-dev.sqlite` 并写入演示组织数据。
|
本地开发固定使用 SQLite。首次启动会自动创建 `src/Jiaowu.Api/data/jiaowu-dev.sqlite` 并写入演示组织数据。
|
||||||
@@ -55,7 +57,7 @@ dotnet tool restore
|
|||||||
dotnet ef migrations list --project src/Jiaowu.Api --startup-project src/Jiaowu.Api
|
dotnet ef migrations list --project src/Jiaowu.Api --startup-project src/Jiaowu.Api
|
||||||
```
|
```
|
||||||
|
|
||||||
应用启动时会自动执行尚未应用的 MySQL 迁移。SQLite 只用于本地开发,并通过 `EnsureCreated` 建立本地数据库;开发模型变化后,可删除本地 SQLite 文件重新生成,不能把该文件用于生产。
|
应用启动时会自动执行尚未应用的 MySQL 迁移。SQLite 只用于本地开发:新库通过 `EnsureCreated` 建立,已有开发库通过轻量、版本化的本地升级脚本补齐结构,不需要手动删除数据文件。SQLite 文件不能用于生产。
|
||||||
|
|
||||||
## 验证
|
## 验证
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ try {
|
|||||||
$headers = @{ Authorization = "Bearer $($login.token)" }
|
$headers = @{ Authorization = "Bearer $($login.token)" }
|
||||||
$dashboard = Invoke-RestMethod -Uri 'http://localhost:5255/api/dashboard' -Headers $headers
|
$dashboard = Invoke-RestMethod -Uri 'http://localhost:5255/api/dashboard' -Headers $headers
|
||||||
$campuses = Invoke-RestMethod -Uri 'http://localhost:5255/api/base-data/campuses' -Headers $headers
|
$campuses = Invoke-RestMethod -Uri 'http://localhost:5255/api/base-data/campuses' -Headers $headers
|
||||||
|
$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
|
||||||
$frontend = Invoke-WebRequest -Uri 'http://localhost:5255/' -TimeoutSec 5
|
$frontend = Invoke-WebRequest -Uri 'http://localhost:5255/' -TimeoutSec 5
|
||||||
$spaFallback = Invoke-WebRequest -Uri 'http://localhost:5255/base-data' -TimeoutSec 5
|
$spaFallback = Invoke-WebRequest -Uri 'http://localhost:5255/base-data' -TimeoutSec 5
|
||||||
$unknownApiParameters = @{
|
$unknownApiParameters = @{
|
||||||
@@ -81,6 +84,9 @@ try {
|
|||||||
User = $login.user.displayName
|
User = $login.user.displayName
|
||||||
Term = $dashboard.currentTerm.name
|
Term = $dashboard.currentTerm.name
|
||||||
Campuses = @($campuses).Count
|
Campuses = @($campuses).Count
|
||||||
|
Teachers = $teachers.total
|
||||||
|
Students = $students.total
|
||||||
|
Courses = $courses.total
|
||||||
StaticIndex = $frontend.Content.Contains('明序教务管理系统')
|
StaticIndex = $frontend.Content.Contains('明序教务管理系统')
|
||||||
SpaFallback = $spaFallback.StatusCode
|
SpaFallback = $spaFallback.StatusCode
|
||||||
ApiNotFound = $unknownApi.StatusCode
|
ApiNotFound = $unknownApi.StatusCode
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$env:ASPNETCORE_ENVIRONMENT = 'Development'
|
||||||
|
$env:ASPNETCORE_URLS = 'http://localhost:5256'
|
||||||
|
|
||||||
|
$workspaceRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
$publishRoot = Join-Path $workspaceRoot '.artifacts/publish'
|
||||||
|
$process = Start-Process `
|
||||||
|
-FilePath (Join-Path $publishRoot 'Jiaowu.Api.exe') `
|
||||||
|
-WorkingDirectory $publishRoot `
|
||||||
|
-WindowStyle Hidden `
|
||||||
|
-PassThru
|
||||||
|
|
||||||
|
$process.Id
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Jiaowu.Api.Contracts;
|
||||||
|
|
||||||
|
public sealed record PagedResult<T>(
|
||||||
|
IReadOnlyCollection<T> Items,
|
||||||
|
int Total,
|
||||||
|
int Page,
|
||||||
|
int PageSize);
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
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]
|
||||||
|
[Route("api/courses")]
|
||||||
|
public sealed class CoursesController(AppDbContext db) : ControllerBase
|
||||||
|
{
|
||||||
|
private const string WriteRoles =
|
||||||
|
SystemRoles.SuperAdmin + "," +
|
||||||
|
SystemRoles.AcademicAdmin + "," +
|
||||||
|
SystemRoles.CollegeAdmin;
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<PagedResult<object>>> Get(
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 20,
|
||||||
|
string? keyword = null,
|
||||||
|
Guid? collegeId = null,
|
||||||
|
CourseNature? nature = null,
|
||||||
|
bool? isEnabled = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
page = Math.Max(1, page);
|
||||||
|
pageSize = Math.Clamp(pageSize, 10, 100);
|
||||||
|
var source = db.Courses.AsNoTracking().AsQueryable();
|
||||||
|
var scopedCollegeId = GetScopedCollegeId();
|
||||||
|
if (scopedCollegeId.HasValue)
|
||||||
|
source = source.Where(x => x.CollegeId == scopedCollegeId.Value);
|
||||||
|
if (collegeId.HasValue)
|
||||||
|
source = source.Where(x => x.CollegeId == collegeId.Value);
|
||||||
|
if (nature.HasValue)
|
||||||
|
source = source.Where(x => x.Nature == nature.Value);
|
||||||
|
if (isEnabled.HasValue)
|
||||||
|
source = source.Where(x => x.IsEnabled == isEnabled.Value);
|
||||||
|
if (!string.IsNullOrWhiteSpace(keyword))
|
||||||
|
{
|
||||||
|
keyword = keyword.Trim();
|
||||||
|
source = source.Where(x =>
|
||||||
|
x.Code.Contains(keyword) ||
|
||||||
|
x.Name.Contains(keyword) ||
|
||||||
|
(x.EnglishName != null && x.EnglishName.Contains(keyword)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = await source.CountAsync(cancellationToken);
|
||||||
|
var items = await source
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.ThenBy(x => x.Code)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
x.Id,
|
||||||
|
x.Code,
|
||||||
|
x.Name,
|
||||||
|
x.EnglishName,
|
||||||
|
x.CollegeId,
|
||||||
|
CollegeName = x.College!.Name,
|
||||||
|
x.Credits,
|
||||||
|
x.TotalHours,
|
||||||
|
x.LectureHours,
|
||||||
|
x.PracticeHours,
|
||||||
|
x.Nature,
|
||||||
|
x.AssessmentMethod,
|
||||||
|
x.Description,
|
||||||
|
x.IsEnabled,
|
||||||
|
x.SortOrder,
|
||||||
|
x.CreatedAt
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Ok(new PagedResult<object>(items, total, page, pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize(Roles = WriteRoles)]
|
||||||
|
public async Task<ActionResult> Create(
|
||||||
|
CourseRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var validation = await ValidateAsync(request, cancellationToken);
|
||||||
|
if (validation is not null) return validation;
|
||||||
|
|
||||||
|
var entity = new Course
|
||||||
|
{
|
||||||
|
Code = request.Code.Trim(),
|
||||||
|
Name = request.Name.Trim(),
|
||||||
|
EnglishName = Normalize(request.EnglishName),
|
||||||
|
CollegeId = request.CollegeId,
|
||||||
|
Credits = request.Credits,
|
||||||
|
TotalHours = request.TotalHours,
|
||||||
|
LectureHours = request.LectureHours,
|
||||||
|
PracticeHours = request.PracticeHours,
|
||||||
|
Nature = request.Nature,
|
||||||
|
AssessmentMethod = request.AssessmentMethod,
|
||||||
|
Description = Normalize(request.Description),
|
||||||
|
IsEnabled = request.IsEnabled,
|
||||||
|
SortOrder = request.SortOrder
|
||||||
|
};
|
||||||
|
db.Courses.Add(entity);
|
||||||
|
return await SaveAsync(entity.Id, true, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id:guid}")]
|
||||||
|
[Authorize(Roles = WriteRoles)]
|
||||||
|
public async Task<ActionResult> Update(
|
||||||
|
Guid id,
|
||||||
|
CourseRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var entity = await db.Courses.FindAsync([id], cancellationToken);
|
||||||
|
if (entity is null) return NotFound();
|
||||||
|
if (!CanAccessCollege(entity.CollegeId)) return Forbid();
|
||||||
|
var validation = await ValidateAsync(request, cancellationToken);
|
||||||
|
if (validation is not null) return validation;
|
||||||
|
|
||||||
|
entity.Code = request.Code.Trim();
|
||||||
|
entity.Name = request.Name.Trim();
|
||||||
|
entity.EnglishName = Normalize(request.EnglishName);
|
||||||
|
entity.CollegeId = request.CollegeId;
|
||||||
|
entity.Credits = request.Credits;
|
||||||
|
entity.TotalHours = request.TotalHours;
|
||||||
|
entity.LectureHours = request.LectureHours;
|
||||||
|
entity.PracticeHours = request.PracticeHours;
|
||||||
|
entity.Nature = request.Nature;
|
||||||
|
entity.AssessmentMethod = request.AssessmentMethod;
|
||||||
|
entity.Description = Normalize(request.Description);
|
||||||
|
entity.IsEnabled = request.IsEnabled;
|
||||||
|
entity.SortOrder = request.SortOrder;
|
||||||
|
return await SaveAsync(entity.Id, false, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id:guid}")]
|
||||||
|
[Authorize(Roles = WriteRoles)]
|
||||||
|
public async Task<ActionResult> Delete(Guid id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var entity = await db.Courses.FindAsync([id], cancellationToken);
|
||||||
|
if (entity is null) return NotFound();
|
||||||
|
if (!CanAccessCollege(entity.CollegeId)) return Forbid();
|
||||||
|
db.Courses.Remove(entity);
|
||||||
|
return await SaveAsync(id, false, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ActionResult?> ValidateAsync(
|
||||||
|
CourseRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!CanAccessCollege(request.CollegeId)) return Forbid();
|
||||||
|
if (!await db.Colleges.AnyAsync(x => x.Id == request.CollegeId, cancellationToken))
|
||||||
|
return ValidationProblem("所选学院不存在。");
|
||||||
|
if (request.LectureHours + request.PracticeHours > request.TotalHours)
|
||||||
|
return ValidationProblem("讲授学时与实践学时之和不能超过总学时。");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Guid? GetScopedCollegeId()
|
||||||
|
{
|
||||||
|
if (!User.IsInRole(SystemRoles.CollegeAdmin))
|
||||||
|
return null;
|
||||||
|
return Guid.TryParse(User.FindFirstValue("college_id"), out var collegeId)
|
||||||
|
? collegeId
|
||||||
|
: Guid.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanAccessCollege(Guid collegeId) =>
|
||||||
|
!GetScopedCollegeId().HasValue || GetScopedCollegeId() == collegeId;
|
||||||
|
|
||||||
|
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 Conflict(new ProblemDetails
|
||||||
|
{
|
||||||
|
Title = "无法完成操作",
|
||||||
|
Detail = "课程编码已存在,或课程已被其他教学业务引用。",
|
||||||
|
Status = StatusCodes.Status409Conflict
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Normalize(string? value) =>
|
||||||
|
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record CourseRequest(
|
||||||
|
[Required, MaxLength(40)] string Code,
|
||||||
|
[Required, MaxLength(100)] string Name,
|
||||||
|
[MaxLength(150)] string? EnglishName,
|
||||||
|
Guid CollegeId,
|
||||||
|
[Range(typeof(decimal), "0.1", "99")] decimal Credits,
|
||||||
|
[Range(1, 1000)] int TotalHours,
|
||||||
|
[Range(0, 1000)] int LectureHours,
|
||||||
|
[Range(0, 1000)] int PracticeHours,
|
||||||
|
CourseNature Nature,
|
||||||
|
AssessmentMethod AssessmentMethod,
|
||||||
|
[MaxLength(1000)] string? Description,
|
||||||
|
bool IsEnabled,
|
||||||
|
int SortOrder);
|
||||||
@@ -29,6 +29,9 @@ public sealed class DashboardController(AppDbContext db) : ControllerBase
|
|||||||
Majors = await db.Majors.CountAsync(cancellationToken),
|
Majors = await db.Majors.CountAsync(cancellationToken),
|
||||||
Classes = await db.AdministrativeClasses.CountAsync(cancellationToken),
|
Classes = await db.AdministrativeClasses.CountAsync(cancellationToken),
|
||||||
Classrooms = await db.Classrooms.CountAsync(cancellationToken),
|
Classrooms = await db.Classrooms.CountAsync(cancellationToken),
|
||||||
|
Teachers = await db.Teachers.CountAsync(cancellationToken),
|
||||||
|
Students = await db.Students.CountAsync(cancellationToken),
|
||||||
|
Courses = await db.Courses.CountAsync(cancellationToken),
|
||||||
Users = await db.Users.CountAsync(cancellationToken)
|
Users = await db.Users.CountAsync(cancellationToken)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,410 @@
|
|||||||
|
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/personnel")]
|
||||||
|
public sealed class PersonnelController(AppDbContext db) : ControllerBase
|
||||||
|
{
|
||||||
|
private const string ReadRoles =
|
||||||
|
SystemRoles.SuperAdmin + "," +
|
||||||
|
SystemRoles.AcademicAdmin + "," +
|
||||||
|
SystemRoles.CollegeAdmin + "," +
|
||||||
|
SystemRoles.Counselor;
|
||||||
|
|
||||||
|
private const string WriteRoles =
|
||||||
|
SystemRoles.SuperAdmin + "," +
|
||||||
|
SystemRoles.AcademicAdmin + "," +
|
||||||
|
SystemRoles.CollegeAdmin;
|
||||||
|
|
||||||
|
[HttpGet("teachers")]
|
||||||
|
public async Task<ActionResult<PagedResult<object>>> GetTeachers(
|
||||||
|
[FromQuery] PersonnelQuery query,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var page = NormalizePage(query.Page);
|
||||||
|
var pageSize = NormalizePageSize(query.PageSize);
|
||||||
|
var source = db.Teachers.AsNoTracking().AsQueryable();
|
||||||
|
var scopedCollegeId = GetScopedCollegeId();
|
||||||
|
if (scopedCollegeId.HasValue)
|
||||||
|
source = source.Where(x => x.CollegeId == scopedCollegeId.Value);
|
||||||
|
if (query.CollegeId.HasValue)
|
||||||
|
source = source.Where(x => x.CollegeId == query.CollegeId);
|
||||||
|
if (!string.IsNullOrWhiteSpace(query.Keyword))
|
||||||
|
{
|
||||||
|
var keyword = query.Keyword.Trim();
|
||||||
|
source = source.Where(x =>
|
||||||
|
x.TeacherNumber.Contains(keyword) ||
|
||||||
|
x.Name.Contains(keyword) ||
|
||||||
|
(x.Title != null && x.Title.Contains(keyword)));
|
||||||
|
}
|
||||||
|
if (query.TeacherStatus.HasValue)
|
||||||
|
source = source.Where(x => x.Status == query.TeacherStatus);
|
||||||
|
|
||||||
|
var total = await source.CountAsync(cancellationToken);
|
||||||
|
var items = await source
|
||||||
|
.OrderBy(x => x.TeacherNumber)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
x.Id,
|
||||||
|
x.TeacherNumber,
|
||||||
|
x.Name,
|
||||||
|
x.Gender,
|
||||||
|
x.CollegeId,
|
||||||
|
CollegeName = x.College!.Name,
|
||||||
|
x.Title,
|
||||||
|
x.Status,
|
||||||
|
x.HireDate,
|
||||||
|
x.IsExternal,
|
||||||
|
x.Phone,
|
||||||
|
x.Email,
|
||||||
|
x.Notes,
|
||||||
|
x.UserId,
|
||||||
|
x.CreatedAt
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Ok(new PagedResult<object>(items, total, page, pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("teachers")]
|
||||||
|
[Authorize(Roles = WriteRoles)]
|
||||||
|
public async Task<ActionResult> CreateTeacher(
|
||||||
|
TeacherRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var validation = await ValidateCollegeAsync(request.CollegeId, cancellationToken);
|
||||||
|
if (validation is not null) return validation;
|
||||||
|
|
||||||
|
var entity = new Teacher
|
||||||
|
{
|
||||||
|
TeacherNumber = request.TeacherNumber.Trim(),
|
||||||
|
Name = request.Name.Trim(),
|
||||||
|
Gender = request.Gender,
|
||||||
|
CollegeId = request.CollegeId,
|
||||||
|
Title = Normalize(request.Title),
|
||||||
|
Status = request.Status,
|
||||||
|
HireDate = request.HireDate,
|
||||||
|
IsExternal = request.IsExternal,
|
||||||
|
Phone = Normalize(request.Phone),
|
||||||
|
Email = Normalize(request.Email),
|
||||||
|
Notes = Normalize(request.Notes)
|
||||||
|
};
|
||||||
|
db.Teachers.Add(entity);
|
||||||
|
return await SaveCreatedAsync(entity.Id, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("teachers/{id:guid}")]
|
||||||
|
[Authorize(Roles = WriteRoles)]
|
||||||
|
public async Task<ActionResult> UpdateTeacher(
|
||||||
|
Guid id,
|
||||||
|
TeacherRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var entity = await db.Teachers.FindAsync([id], cancellationToken);
|
||||||
|
if (entity is null) return NotFound();
|
||||||
|
if (!CanAccessCollege(entity.CollegeId)) return Forbid();
|
||||||
|
var validation = await ValidateCollegeAsync(request.CollegeId, cancellationToken);
|
||||||
|
if (validation is not null) return validation;
|
||||||
|
|
||||||
|
entity.TeacherNumber = request.TeacherNumber.Trim();
|
||||||
|
entity.Name = request.Name.Trim();
|
||||||
|
entity.Gender = request.Gender;
|
||||||
|
entity.CollegeId = request.CollegeId;
|
||||||
|
entity.Title = Normalize(request.Title);
|
||||||
|
entity.Status = request.Status;
|
||||||
|
entity.HireDate = request.HireDate;
|
||||||
|
entity.IsExternal = request.IsExternal;
|
||||||
|
entity.Phone = Normalize(request.Phone);
|
||||||
|
entity.Email = Normalize(request.Email);
|
||||||
|
entity.Notes = Normalize(request.Notes);
|
||||||
|
return await SaveNoContentAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("teachers/{id:guid}")]
|
||||||
|
[Authorize(Roles = WriteRoles)]
|
||||||
|
public async Task<IActionResult> DeleteTeacher(Guid id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var entity = await db.Teachers.FindAsync([id], cancellationToken);
|
||||||
|
if (entity is null) return NotFound();
|
||||||
|
if (!CanAccessCollege(entity.CollegeId)) return Forbid();
|
||||||
|
if (entity.UserId.HasValue)
|
||||||
|
{
|
||||||
|
return ConflictProblem("教师已关联登录账号,请先解除账号关联后再删除。");
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Teachers.Remove(entity);
|
||||||
|
return await SaveNoContentAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("students")]
|
||||||
|
public async Task<ActionResult<PagedResult<object>>> GetStudents(
|
||||||
|
[FromQuery] PersonnelQuery query,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var page = NormalizePage(query.Page);
|
||||||
|
var pageSize = NormalizePageSize(query.PageSize);
|
||||||
|
var source = db.Students.AsNoTracking().AsQueryable();
|
||||||
|
var scopedCollegeId = GetScopedCollegeId();
|
||||||
|
if (scopedCollegeId.HasValue)
|
||||||
|
{
|
||||||
|
source = source.Where(x =>
|
||||||
|
x.AdministrativeClass!.Major!.CollegeId == scopedCollegeId.Value);
|
||||||
|
}
|
||||||
|
if (query.CollegeId.HasValue)
|
||||||
|
{
|
||||||
|
source = source.Where(x =>
|
||||||
|
x.AdministrativeClass!.Major!.CollegeId == query.CollegeId.Value);
|
||||||
|
}
|
||||||
|
if (query.MajorId.HasValue)
|
||||||
|
source = source.Where(x => x.AdministrativeClass!.MajorId == query.MajorId);
|
||||||
|
if (query.ClassId.HasValue)
|
||||||
|
source = source.Where(x => x.AdministrativeClassId == query.ClassId);
|
||||||
|
if (query.EnrollmentYear.HasValue)
|
||||||
|
source = source.Where(x => x.EnrollmentYear == query.EnrollmentYear);
|
||||||
|
if (query.StudentStatus.HasValue)
|
||||||
|
source = source.Where(x => x.Status == query.StudentStatus);
|
||||||
|
if (!string.IsNullOrWhiteSpace(query.Keyword))
|
||||||
|
{
|
||||||
|
var keyword = query.Keyword.Trim();
|
||||||
|
source = source.Where(x =>
|
||||||
|
x.StudentNumber.Contains(keyword) || x.Name.Contains(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = await source.CountAsync(cancellationToken);
|
||||||
|
var items = await source
|
||||||
|
.OrderBy(x => x.StudentNumber)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
x.Id,
|
||||||
|
x.StudentNumber,
|
||||||
|
x.Name,
|
||||||
|
x.Gender,
|
||||||
|
x.AdministrativeClassId,
|
||||||
|
ClassName = x.AdministrativeClass!.Name,
|
||||||
|
MajorId = x.AdministrativeClass.MajorId,
|
||||||
|
MajorName = x.AdministrativeClass.Major!.Name,
|
||||||
|
CollegeId = x.AdministrativeClass.Major.CollegeId,
|
||||||
|
CollegeName = x.AdministrativeClass.Major.College!.Name,
|
||||||
|
x.EnrollmentYear,
|
||||||
|
x.EnrollmentDate,
|
||||||
|
x.Status,
|
||||||
|
x.DateOfBirth,
|
||||||
|
x.Phone,
|
||||||
|
x.Email,
|
||||||
|
x.Notes,
|
||||||
|
x.UserId,
|
||||||
|
x.CreatedAt
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Ok(new PagedResult<object>(items, total, page, pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("students")]
|
||||||
|
[Authorize(Roles = WriteRoles)]
|
||||||
|
public async Task<ActionResult> CreateStudent(
|
||||||
|
StudentRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var targetClass = await db.AdministrativeClasses.AsNoTracking()
|
||||||
|
.Where(x => x.Id == request.AdministrativeClassId)
|
||||||
|
.Select(x => new { x.Id, CollegeId = x.Major!.CollegeId })
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
if (targetClass is null)
|
||||||
|
return ValidationProblem("所选行政班不存在。");
|
||||||
|
if (!CanAccessCollege(targetClass.CollegeId))
|
||||||
|
return Forbid();
|
||||||
|
|
||||||
|
var entity = new Student
|
||||||
|
{
|
||||||
|
StudentNumber = request.StudentNumber.Trim(),
|
||||||
|
Name = request.Name.Trim(),
|
||||||
|
Gender = request.Gender,
|
||||||
|
AdministrativeClassId = request.AdministrativeClassId,
|
||||||
|
EnrollmentYear = request.EnrollmentYear,
|
||||||
|
EnrollmentDate = request.EnrollmentDate,
|
||||||
|
Status = request.Status,
|
||||||
|
DateOfBirth = request.DateOfBirth,
|
||||||
|
Phone = Normalize(request.Phone),
|
||||||
|
Email = Normalize(request.Email),
|
||||||
|
Notes = Normalize(request.Notes)
|
||||||
|
};
|
||||||
|
db.Students.Add(entity);
|
||||||
|
return await SaveCreatedAsync(entity.Id, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("students/{id:guid}")]
|
||||||
|
[Authorize(Roles = WriteRoles)]
|
||||||
|
public async Task<ActionResult> UpdateStudent(
|
||||||
|
Guid id,
|
||||||
|
StudentRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var entity = await db.Students
|
||||||
|
.Include(x => x.AdministrativeClass)
|
||||||
|
.ThenInclude(x => x!.Major)
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||||
|
if (entity is null) return NotFound();
|
||||||
|
if (!CanAccessCollege(entity.AdministrativeClass!.Major!.CollegeId)) return Forbid();
|
||||||
|
|
||||||
|
var targetClass = await db.AdministrativeClasses.AsNoTracking()
|
||||||
|
.Where(x => x.Id == request.AdministrativeClassId)
|
||||||
|
.Select(x => new { x.Id, CollegeId = x.Major!.CollegeId })
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
if (targetClass is null)
|
||||||
|
return ValidationProblem("所选行政班不存在。");
|
||||||
|
if (!CanAccessCollege(targetClass.CollegeId))
|
||||||
|
return Forbid();
|
||||||
|
|
||||||
|
entity.StudentNumber = request.StudentNumber.Trim();
|
||||||
|
entity.Name = request.Name.Trim();
|
||||||
|
entity.Gender = request.Gender;
|
||||||
|
entity.AdministrativeClassId = request.AdministrativeClassId;
|
||||||
|
entity.EnrollmentYear = request.EnrollmentYear;
|
||||||
|
entity.EnrollmentDate = request.EnrollmentDate;
|
||||||
|
entity.Status = request.Status;
|
||||||
|
entity.DateOfBirth = request.DateOfBirth;
|
||||||
|
entity.Phone = Normalize(request.Phone);
|
||||||
|
entity.Email = Normalize(request.Email);
|
||||||
|
entity.Notes = Normalize(request.Notes);
|
||||||
|
return await SaveNoContentAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("students/{id:guid}")]
|
||||||
|
[Authorize(Roles = WriteRoles)]
|
||||||
|
public async Task<IActionResult> DeleteStudent(Guid id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var entity = await db.Students
|
||||||
|
.Include(x => x.AdministrativeClass)
|
||||||
|
.ThenInclude(x => x!.Major)
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||||
|
if (entity is null) return NotFound();
|
||||||
|
if (!CanAccessCollege(entity.AdministrativeClass!.Major!.CollegeId)) return Forbid();
|
||||||
|
if (entity.UserId.HasValue)
|
||||||
|
{
|
||||||
|
return ConflictProblem("学生已关联登录账号,请先解除账号关联后再删除。");
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Students.Remove(entity);
|
||||||
|
return await SaveNoContentAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Guid? GetScopedCollegeId()
|
||||||
|
{
|
||||||
|
if (!User.IsInRole(SystemRoles.CollegeAdmin) &&
|
||||||
|
!User.IsInRole(SystemRoles.Counselor))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Guid.TryParse(User.FindFirstValue("college_id"), out var collegeId)
|
||||||
|
? collegeId
|
||||||
|
: Guid.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanAccessCollege(Guid collegeId) =>
|
||||||
|
!GetScopedCollegeId().HasValue || GetScopedCollegeId() == collegeId;
|
||||||
|
|
||||||
|
private async Task<ActionResult?> ValidateCollegeAsync(
|
||||||
|
Guid collegeId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!CanAccessCollege(collegeId)) return Forbid();
|
||||||
|
if (!await db.Colleges.AnyAsync(x => x.Id == collegeId, cancellationToken))
|
||||||
|
return ValidationProblem("所选学院不存在。");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
private static int NormalizePage(int page) => Math.Max(1, page);
|
||||||
|
private static int NormalizePageSize(int pageSize) => Math.Clamp(pageSize, 10, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record PersonnelQuery(
|
||||||
|
int Page = 1,
|
||||||
|
int PageSize = 20,
|
||||||
|
string? Keyword = null,
|
||||||
|
Guid? CollegeId = null,
|
||||||
|
Guid? MajorId = null,
|
||||||
|
Guid? ClassId = null,
|
||||||
|
int? EnrollmentYear = null,
|
||||||
|
TeacherStatus? TeacherStatus = null,
|
||||||
|
StudentStatus? StudentStatus = null);
|
||||||
|
|
||||||
|
public sealed record TeacherRequest(
|
||||||
|
[Required, MaxLength(30)] string TeacherNumber,
|
||||||
|
[Required, MaxLength(50)] string Name,
|
||||||
|
Gender Gender,
|
||||||
|
Guid CollegeId,
|
||||||
|
[MaxLength(30)] string? Title,
|
||||||
|
TeacherStatus Status,
|
||||||
|
DateOnly? HireDate,
|
||||||
|
bool IsExternal,
|
||||||
|
[MaxLength(30)] string? Phone,
|
||||||
|
[EmailAddress, MaxLength(100)] string? Email,
|
||||||
|
[MaxLength(500)] string? Notes);
|
||||||
|
|
||||||
|
public sealed record StudentRequest(
|
||||||
|
[Required, MaxLength(30)] string StudentNumber,
|
||||||
|
[Required, MaxLength(50)] string Name,
|
||||||
|
Gender Gender,
|
||||||
|
Guid AdministrativeClassId,
|
||||||
|
[Range(2000, 2200)] int EnrollmentYear,
|
||||||
|
DateOnly EnrollmentDate,
|
||||||
|
StudentStatus Status,
|
||||||
|
DateOnly? DateOfBirth,
|
||||||
|
[MaxLength(30)] string? Phone,
|
||||||
|
[EmailAddress, MaxLength(100)] string? Email,
|
||||||
|
[MaxLength(500)] string? Notes);
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using Jiaowu.Api.Domain.Common;
|
||||||
|
|
||||||
|
namespace Jiaowu.Api.Domain.Academic;
|
||||||
|
|
||||||
|
public sealed class Teacher : EntityBase
|
||||||
|
{
|
||||||
|
public required string TeacherNumber { get; set; }
|
||||||
|
public required string Name { get; set; }
|
||||||
|
public Gender Gender { get; set; }
|
||||||
|
public Guid CollegeId { get; set; }
|
||||||
|
public College? College { get; set; }
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public TeacherStatus Status { get; set; } = TeacherStatus.Active;
|
||||||
|
public DateOnly? HireDate { get; set; }
|
||||||
|
public bool IsExternal { get; set; }
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
public Guid? UserId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class Student : EntityBase
|
||||||
|
{
|
||||||
|
public required string StudentNumber { get; set; }
|
||||||
|
public required string Name { get; set; }
|
||||||
|
public Gender Gender { get; set; }
|
||||||
|
public Guid AdministrativeClassId { get; set; }
|
||||||
|
public AdministrativeClass? AdministrativeClass { get; set; }
|
||||||
|
public int EnrollmentYear { get; set; }
|
||||||
|
public DateOnly EnrollmentDate { get; set; }
|
||||||
|
public StudentStatus Status { get; set; } = StudentStatus.Active;
|
||||||
|
public DateOnly? DateOfBirth { get; set; }
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
public Guid? UserId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class Course : CatalogEntity
|
||||||
|
{
|
||||||
|
public Guid CollegeId { get; set; }
|
||||||
|
public College? College { get; set; }
|
||||||
|
public string? EnglishName { get; set; }
|
||||||
|
public decimal Credits { get; set; }
|
||||||
|
public int TotalHours { get; set; }
|
||||||
|
public int LectureHours { get; set; }
|
||||||
|
public int PracticeHours { get; set; }
|
||||||
|
public CourseNature Nature { get; set; }
|
||||||
|
public AssessmentMethod AssessmentMethod { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Gender
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Male = 1,
|
||||||
|
Female = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum TeacherStatus
|
||||||
|
{
|
||||||
|
Active = 1,
|
||||||
|
OnLeave = 2,
|
||||||
|
Retired = 3,
|
||||||
|
Departed = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum StudentStatus
|
||||||
|
{
|
||||||
|
Active = 1,
|
||||||
|
Suspended = 2,
|
||||||
|
Graduated = 3,
|
||||||
|
Withdrawn = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum CourseNature
|
||||||
|
{
|
||||||
|
GeneralRequired = 1,
|
||||||
|
MajorRequired = 2,
|
||||||
|
MajorElective = 3,
|
||||||
|
GeneralElective = 4,
|
||||||
|
Practice = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AssessmentMethod
|
||||||
|
{
|
||||||
|
Examination = 1,
|
||||||
|
Assessment = 2
|
||||||
|
}
|
||||||
@@ -17,6 +17,9 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
|||||||
public DbSet<Building> Buildings => Set<Building>();
|
public DbSet<Building> Buildings => Set<Building>();
|
||||||
public DbSet<Classroom> Classrooms => Set<Classroom>();
|
public DbSet<Classroom> Classrooms => Set<Classroom>();
|
||||||
public DbSet<AcademicTerm> AcademicTerms => Set<AcademicTerm>();
|
public DbSet<AcademicTerm> AcademicTerms => Set<AcademicTerm>();
|
||||||
|
public DbSet<Teacher> Teachers => Set<Teacher>();
|
||||||
|
public DbSet<Student> Students => Set<Student>();
|
||||||
|
public DbSet<Course> Courses => Set<Course>();
|
||||||
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
|
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
@@ -42,6 +45,7 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
|||||||
ConfigureCatalog<Building>(builder);
|
ConfigureCatalog<Building>(builder);
|
||||||
ConfigureCatalog<Classroom>(builder);
|
ConfigureCatalog<Classroom>(builder);
|
||||||
ConfigureCatalog<AcademicTerm>(builder);
|
ConfigureCatalog<AcademicTerm>(builder);
|
||||||
|
ConfigureCatalog<Course>(builder);
|
||||||
|
|
||||||
builder.Entity<College>()
|
builder.Entity<College>()
|
||||||
.HasOne(x => x.Campus)
|
.HasOne(x => x.Campus)
|
||||||
@@ -76,6 +80,58 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
|||||||
builder.Entity<AcademicTerm>()
|
builder.Entity<AcademicTerm>()
|
||||||
.HasIndex(x => x.IsCurrent);
|
.HasIndex(x => x.IsCurrent);
|
||||||
|
|
||||||
|
builder.Entity<Teacher>(entity =>
|
||||||
|
{
|
||||||
|
entity.Property(x => x.TeacherNumber).HasMaxLength(30);
|
||||||
|
entity.Property(x => x.Name).HasMaxLength(50);
|
||||||
|
entity.Property(x => x.Title).HasMaxLength(30);
|
||||||
|
entity.Property(x => x.Phone).HasMaxLength(30);
|
||||||
|
entity.Property(x => x.Email).HasMaxLength(100);
|
||||||
|
entity.Property(x => x.Notes).HasMaxLength(500);
|
||||||
|
entity.HasIndex(x => x.TeacherNumber).IsUnique();
|
||||||
|
entity.HasIndex(x => new { x.CollegeId, x.Status });
|
||||||
|
entity.HasOne(x => x.College)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.CollegeId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
entity.HasOne<ApplicationUser>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Entity<Student>(entity =>
|
||||||
|
{
|
||||||
|
entity.Property(x => x.StudentNumber).HasMaxLength(30);
|
||||||
|
entity.Property(x => x.Name).HasMaxLength(50);
|
||||||
|
entity.Property(x => x.Phone).HasMaxLength(30);
|
||||||
|
entity.Property(x => x.Email).HasMaxLength(100);
|
||||||
|
entity.Property(x => x.Notes).HasMaxLength(500);
|
||||||
|
entity.HasIndex(x => x.StudentNumber).IsUnique();
|
||||||
|
entity.HasIndex(x => new { x.AdministrativeClassId, x.Status });
|
||||||
|
entity.HasIndex(x => x.EnrollmentYear);
|
||||||
|
entity.HasOne(x => x.AdministrativeClass)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.AdministrativeClassId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
entity.HasOne<ApplicationUser>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Entity<Course>(entity =>
|
||||||
|
{
|
||||||
|
entity.Property(x => x.EnglishName).HasMaxLength(150);
|
||||||
|
entity.Property(x => x.Credits).HasPrecision(5, 2);
|
||||||
|
entity.Property(x => x.Description).HasMaxLength(1000);
|
||||||
|
entity.HasIndex(x => new { x.CollegeId, x.Nature });
|
||||||
|
entity.HasOne(x => x.College)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.CollegeId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
builder.Entity<AuditLog>(entity =>
|
builder.Entity<AuditLog>(entity =>
|
||||||
{
|
{
|
||||||
entity.Property(x => x.Method).HasMaxLength(10);
|
entity.Property(x => x.Method).HasMaxLength(10);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ public sealed class DatabaseInitializer(
|
|||||||
AppDbContext db,
|
AppDbContext db,
|
||||||
RoleManager<ApplicationRole> roleManager,
|
RoleManager<ApplicationRole> roleManager,
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
|
DevelopmentSqliteMigrator sqliteMigrator,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IHostEnvironment environment,
|
IHostEnvironment environment,
|
||||||
ILogger<DatabaseInitializer> logger)
|
ILogger<DatabaseInitializer> logger)
|
||||||
@@ -18,6 +19,7 @@ public sealed class DatabaseInitializer(
|
|||||||
if (environment.IsDevelopment())
|
if (environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
await db.Database.EnsureCreatedAsync();
|
await db.Database.EnsureCreatedAsync();
|
||||||
|
await sqliteMigrator.MigrateAsync();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -108,71 +110,182 @@ public sealed class DatabaseInitializer(
|
|||||||
|
|
||||||
private async Task SeedDevelopmentDataAsync()
|
private async Task SeedDevelopmentDataAsync()
|
||||||
{
|
{
|
||||||
if (await db.Campuses.AnyAsync())
|
if (!await db.Campuses.AnyAsync())
|
||||||
{
|
{
|
||||||
return;
|
var campus = new Campus
|
||||||
|
{
|
||||||
|
Code = "MAIN",
|
||||||
|
Name = "主校区",
|
||||||
|
Address = "大学路 1 号"
|
||||||
|
};
|
||||||
|
var college = new College
|
||||||
|
{
|
||||||
|
Code = "CS",
|
||||||
|
Name = "计算机学院",
|
||||||
|
ShortName = "计算机学院",
|
||||||
|
CampusId = campus.Id
|
||||||
|
};
|
||||||
|
var major = new Major
|
||||||
|
{
|
||||||
|
Code = "080901",
|
||||||
|
Name = "计算机科学与技术",
|
||||||
|
CollegeId = college.Id,
|
||||||
|
DegreeType = "工学学士",
|
||||||
|
SchoolingYears = 4
|
||||||
|
};
|
||||||
|
var building = new Building
|
||||||
|
{
|
||||||
|
Code = "J1",
|
||||||
|
Name = "第一教学楼",
|
||||||
|
CampusId = campus.Id
|
||||||
|
};
|
||||||
|
|
||||||
|
db.AddRange(
|
||||||
|
campus,
|
||||||
|
college,
|
||||||
|
major,
|
||||||
|
new AdministrativeClass
|
||||||
|
{
|
||||||
|
Code = "CS2026-01",
|
||||||
|
Name = "计科 2026-1 班",
|
||||||
|
MajorId = major.Id,
|
||||||
|
Grade = 2026,
|
||||||
|
CounselorName = "陈老师"
|
||||||
|
},
|
||||||
|
building,
|
||||||
|
new Classroom
|
||||||
|
{
|
||||||
|
Code = "J1-201",
|
||||||
|
Name = "J1-201",
|
||||||
|
BuildingId = building.Id,
|
||||||
|
Capacity = 60,
|
||||||
|
RoomType = "多媒体教室",
|
||||||
|
Equipment = "投影、扩声、录播"
|
||||||
|
},
|
||||||
|
new AcademicTerm
|
||||||
|
{
|
||||||
|
Code = "2026-2027-1",
|
||||||
|
Name = "2026—2027 学年第一学期",
|
||||||
|
AcademicYear = "2026-2027",
|
||||||
|
Season = TermSeason.Autumn,
|
||||||
|
StartDate = new DateOnly(2026, 9, 7),
|
||||||
|
EndDate = new DateOnly(2027, 1, 17),
|
||||||
|
IsCurrent = true
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
var campus = new Campus
|
var computerCollege = await db.Colleges.SingleAsync(x => x.Code == "CS");
|
||||||
{
|
var computerClass = await db.AdministrativeClasses
|
||||||
Code = "MAIN",
|
.SingleAsync(x => x.Code == "CS2026-01");
|
||||||
Name = "主校区",
|
|
||||||
Address = "大学路 1 号"
|
|
||||||
};
|
|
||||||
var college = new College
|
|
||||||
{
|
|
||||||
Code = "CS",
|
|
||||||
Name = "计算机学院",
|
|
||||||
ShortName = "计算机学院",
|
|
||||||
CampusId = campus.Id
|
|
||||||
};
|
|
||||||
var major = new Major
|
|
||||||
{
|
|
||||||
Code = "080901",
|
|
||||||
Name = "计算机科学与技术",
|
|
||||||
CollegeId = college.Id,
|
|
||||||
DegreeType = "工学学士",
|
|
||||||
SchoolingYears = 4
|
|
||||||
};
|
|
||||||
var building = new Building
|
|
||||||
{
|
|
||||||
Code = "J1",
|
|
||||||
Name = "第一教学楼",
|
|
||||||
CampusId = campus.Id
|
|
||||||
};
|
|
||||||
|
|
||||||
db.AddRange(
|
if (!await db.Teachers.AnyAsync())
|
||||||
campus,
|
{
|
||||||
college,
|
db.Teachers.AddRange(
|
||||||
major,
|
new Teacher
|
||||||
new AdministrativeClass
|
{
|
||||||
{
|
TeacherNumber = "T2026001",
|
||||||
Code = "CS2026-01",
|
Name = "陈明远",
|
||||||
Name = "计科 2026-1 班",
|
Gender = Gender.Male,
|
||||||
MajorId = major.Id,
|
CollegeId = computerCollege.Id,
|
||||||
Grade = 2026,
|
Title = "副教授",
|
||||||
CounselorName = "陈老师"
|
Status = TeacherStatus.Active,
|
||||||
},
|
HireDate = new DateOnly(2018, 7, 1),
|
||||||
building,
|
Email = "chenmy@example.edu.cn"
|
||||||
new Classroom
|
},
|
||||||
{
|
new Teacher
|
||||||
Code = "J1-201",
|
{
|
||||||
Name = "J1-201",
|
TeacherNumber = "T2026002",
|
||||||
BuildingId = building.Id,
|
Name = "林书雅",
|
||||||
Capacity = 60,
|
Gender = Gender.Female,
|
||||||
RoomType = "多媒体教室",
|
CollegeId = computerCollege.Id,
|
||||||
Equipment = "投影、扩声、录播"
|
Title = "讲师",
|
||||||
},
|
Status = TeacherStatus.Active,
|
||||||
new AcademicTerm
|
HireDate = new DateOnly(2022, 9, 1),
|
||||||
{
|
Email = "linsy@example.edu.cn"
|
||||||
Code = "2026-2027-1",
|
});
|
||||||
Name = "2026—2027 学年第一学期",
|
}
|
||||||
AcademicYear = "2026-2027",
|
|
||||||
Season = TermSeason.Autumn,
|
if (!await db.Students.AnyAsync())
|
||||||
StartDate = new DateOnly(2026, 9, 7),
|
{
|
||||||
EndDate = new DateOnly(2027, 1, 17),
|
db.Students.AddRange(
|
||||||
IsCurrent = true
|
new Student
|
||||||
});
|
{
|
||||||
|
StudentNumber = "202601001",
|
||||||
|
Name = "周启航",
|
||||||
|
Gender = Gender.Male,
|
||||||
|
AdministrativeClassId = computerClass.Id,
|
||||||
|
EnrollmentYear = 2026,
|
||||||
|
EnrollmentDate = new DateOnly(2026, 9, 7),
|
||||||
|
Status = StudentStatus.Active
|
||||||
|
},
|
||||||
|
new Student
|
||||||
|
{
|
||||||
|
StudentNumber = "202601002",
|
||||||
|
Name = "许知夏",
|
||||||
|
Gender = Gender.Female,
|
||||||
|
AdministrativeClassId = computerClass.Id,
|
||||||
|
EnrollmentYear = 2026,
|
||||||
|
EnrollmentDate = new DateOnly(2026, 9, 7),
|
||||||
|
Status = StudentStatus.Active
|
||||||
|
},
|
||||||
|
new Student
|
||||||
|
{
|
||||||
|
StudentNumber = "202601003",
|
||||||
|
Name = "方嘉树",
|
||||||
|
Gender = Gender.Male,
|
||||||
|
AdministrativeClassId = computerClass.Id,
|
||||||
|
EnrollmentYear = 2026,
|
||||||
|
EnrollmentDate = new DateOnly(2026, 9, 7),
|
||||||
|
Status = StudentStatus.Active
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await db.Courses.AnyAsync())
|
||||||
|
{
|
||||||
|
db.Courses.AddRange(
|
||||||
|
new Course
|
||||||
|
{
|
||||||
|
Code = "CS101",
|
||||||
|
Name = "程序设计基础",
|
||||||
|
EnglishName = "Fundamentals of Programming",
|
||||||
|
CollegeId = computerCollege.Id,
|
||||||
|
Credits = 4,
|
||||||
|
TotalHours = 64,
|
||||||
|
LectureHours = 40,
|
||||||
|
PracticeHours = 24,
|
||||||
|
Nature = CourseNature.MajorRequired,
|
||||||
|
AssessmentMethod = AssessmentMethod.Examination,
|
||||||
|
Description = "面向一年级学生的程序设计入门课程。"
|
||||||
|
},
|
||||||
|
new Course
|
||||||
|
{
|
||||||
|
Code = "CS201",
|
||||||
|
Name = "数据结构",
|
||||||
|
EnglishName = "Data Structures",
|
||||||
|
CollegeId = computerCollege.Id,
|
||||||
|
Credits = 3.5m,
|
||||||
|
TotalHours = 56,
|
||||||
|
LectureHours = 40,
|
||||||
|
PracticeHours = 16,
|
||||||
|
Nature = CourseNature.MajorRequired,
|
||||||
|
AssessmentMethod = AssessmentMethod.Examination
|
||||||
|
},
|
||||||
|
new Course
|
||||||
|
{
|
||||||
|
Code = "CS305",
|
||||||
|
Name = "软件工程实践",
|
||||||
|
EnglishName = "Software Engineering Practice",
|
||||||
|
CollegeId = computerCollege.Id,
|
||||||
|
Credits = 2,
|
||||||
|
TotalHours = 48,
|
||||||
|
LectureHours = 8,
|
||||||
|
PracticeHours = 40,
|
||||||
|
Nature = CourseNature.Practice,
|
||||||
|
AssessmentMethod = AssessmentMethod.Assessment
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Jiaowu.Api.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
public sealed class DevelopmentSqliteMigrator(
|
||||||
|
AppDbContext db,
|
||||||
|
ILogger<DevelopmentSqliteMigrator> logger)
|
||||||
|
{
|
||||||
|
private const string PeopleAndCoursesMigration = "20260724_01_people_courses";
|
||||||
|
|
||||||
|
public async Task MigrateAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (!db.Database.IsSqlite())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.Database.ExecuteSqlRawAsync(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS "__DevelopmentSchemaHistory" (
|
||||||
|
"MigrationId" TEXT NOT NULL CONSTRAINT "PK___DevelopmentSchemaHistory" PRIMARY KEY,
|
||||||
|
"AppliedAt" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
""",
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var applied = await db.Database
|
||||||
|
.SqlQuery<string>(
|
||||||
|
$"""
|
||||||
|
SELECT "MigrationId" AS "Value"
|
||||||
|
FROM "__DevelopmentSchemaHistory"
|
||||||
|
WHERE "MigrationId" = {PeopleAndCoursesMigration}
|
||||||
|
""")
|
||||||
|
.AnyAsync(cancellationToken);
|
||||||
|
if (applied)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken);
|
||||||
|
foreach (var statement in PeopleAndCoursesStatements)
|
||||||
|
{
|
||||||
|
await db.Database.ExecuteSqlRawAsync(statement, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.Database.ExecuteSqlInterpolatedAsync(
|
||||||
|
$"""
|
||||||
|
INSERT INTO "__DevelopmentSchemaHistory" ("MigrationId", "AppliedAt")
|
||||||
|
VALUES ({PeopleAndCoursesMigration}, {DateTime.UtcNow});
|
||||||
|
""",
|
||||||
|
cancellationToken);
|
||||||
|
await transaction.CommitAsync(cancellationToken);
|
||||||
|
logger.LogInformation(
|
||||||
|
"Applied SQLite development schema migration {MigrationId}.",
|
||||||
|
PeopleAndCoursesMigration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly string[] PeopleAndCoursesStatements =
|
||||||
|
[
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS "Teachers" (
|
||||||
|
"Id" TEXT NOT NULL CONSTRAINT "PK_Teachers" PRIMARY KEY,
|
||||||
|
"TeacherNumber" TEXT NOT NULL,
|
||||||
|
"Name" TEXT NOT NULL,
|
||||||
|
"Gender" INTEGER NOT NULL,
|
||||||
|
"CollegeId" TEXT NOT NULL,
|
||||||
|
"Title" TEXT NULL,
|
||||||
|
"Status" INTEGER NOT NULL,
|
||||||
|
"HireDate" TEXT NULL,
|
||||||
|
"IsExternal" INTEGER NOT NULL,
|
||||||
|
"Phone" TEXT NULL,
|
||||||
|
"Email" TEXT NULL,
|
||||||
|
"Notes" TEXT NULL,
|
||||||
|
"UserId" TEXT NULL,
|
||||||
|
"CreatedAt" TEXT NOT NULL,
|
||||||
|
"UpdatedAt" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "FK_Teachers_Colleges_CollegeId"
|
||||||
|
FOREIGN KEY ("CollegeId") REFERENCES "Colleges" ("Id") ON DELETE RESTRICT,
|
||||||
|
CONSTRAINT "FK_Teachers_AspNetUsers_UserId"
|
||||||
|
FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "IX_Teachers_TeacherNumber"
|
||||||
|
ON "Teachers" ("TeacherNumber");
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_Teachers_CollegeId_Status"
|
||||||
|
ON "Teachers" ("CollegeId", "Status");
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_Teachers_UserId"
|
||||||
|
ON "Teachers" ("UserId");
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS "Students" (
|
||||||
|
"Id" TEXT NOT NULL CONSTRAINT "PK_Students" PRIMARY KEY,
|
||||||
|
"StudentNumber" TEXT NOT NULL,
|
||||||
|
"Name" TEXT NOT NULL,
|
||||||
|
"Gender" INTEGER NOT NULL,
|
||||||
|
"AdministrativeClassId" TEXT NOT NULL,
|
||||||
|
"EnrollmentYear" INTEGER NOT NULL,
|
||||||
|
"EnrollmentDate" TEXT NOT NULL,
|
||||||
|
"Status" INTEGER NOT NULL,
|
||||||
|
"DateOfBirth" TEXT NULL,
|
||||||
|
"Phone" TEXT NULL,
|
||||||
|
"Email" TEXT NULL,
|
||||||
|
"Notes" TEXT NULL,
|
||||||
|
"UserId" TEXT NULL,
|
||||||
|
"CreatedAt" TEXT NOT NULL,
|
||||||
|
"UpdatedAt" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "FK_Students_AdministrativeClasses_AdministrativeClassId"
|
||||||
|
FOREIGN KEY ("AdministrativeClassId") REFERENCES "AdministrativeClasses" ("Id")
|
||||||
|
ON DELETE RESTRICT,
|
||||||
|
CONSTRAINT "FK_Students_AspNetUsers_UserId"
|
||||||
|
FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "IX_Students_StudentNumber"
|
||||||
|
ON "Students" ("StudentNumber");
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_Students_AdministrativeClassId_Status"
|
||||||
|
ON "Students" ("AdministrativeClassId", "Status");
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_Students_EnrollmentYear"
|
||||||
|
ON "Students" ("EnrollmentYear");
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_Students_UserId"
|
||||||
|
ON "Students" ("UserId");
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS "Courses" (
|
||||||
|
"Id" TEXT NOT NULL CONSTRAINT "PK_Courses" PRIMARY KEY,
|
||||||
|
"CollegeId" TEXT NOT NULL,
|
||||||
|
"EnglishName" TEXT NULL,
|
||||||
|
"Credits" TEXT NOT NULL,
|
||||||
|
"TotalHours" INTEGER NOT NULL,
|
||||||
|
"LectureHours" INTEGER NOT NULL,
|
||||||
|
"PracticeHours" INTEGER NOT NULL,
|
||||||
|
"Nature" INTEGER NOT NULL,
|
||||||
|
"AssessmentMethod" INTEGER NOT NULL,
|
||||||
|
"Description" TEXT NULL,
|
||||||
|
"CreatedAt" TEXT NOT NULL,
|
||||||
|
"UpdatedAt" TEXT NOT NULL,
|
||||||
|
"Code" TEXT NOT NULL,
|
||||||
|
"Name" TEXT NOT NULL,
|
||||||
|
"SortOrder" INTEGER NOT NULL,
|
||||||
|
"IsEnabled" INTEGER NOT NULL,
|
||||||
|
CONSTRAINT "FK_Courses_Colleges_CollegeId"
|
||||||
|
FOREIGN KEY ("CollegeId") REFERENCES "Colleges" ("Id") ON DELETE RESTRICT
|
||||||
|
);
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "IX_Courses_Code" ON "Courses" ("Code");
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_Courses_IsEnabled_SortOrder"
|
||||||
|
ON "Courses" ("IsEnabled", "SortOrder");
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_Courses_CollegeId_Nature"
|
||||||
|
ON "Courses" ("CollegeId", "Nature");
|
||||||
|
"""
|
||||||
|
];
|
||||||
|
}
|
||||||
+984
@@ -0,0 +1,984 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
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("20260724045307_PeopleAndCourses")]
|
||||||
|
partial class PeopleAndCourses
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("AcademicYear")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("varchar(40)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EndDate")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<bool>("IsCurrent")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Season")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("StartDate")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<DateTime>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("varchar(40)");
|
||||||
|
|
||||||
|
b.Property<string>("CounselorName")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int>("Grade")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<Guid>("MajorId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid>("CampusId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("varchar(40)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("Address")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("varchar(40)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid>("BuildingId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<int>("Capacity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("varchar(40)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Equipment")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("RoomType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("CampusId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("varchar(40)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("ShortName")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<int>("AssessmentMethod")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("varchar(40)");
|
||||||
|
|
||||||
|
b.Property<Guid>("CollegeId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Credits")
|
||||||
|
.HasPrecision(5, 2)
|
||||||
|
.HasColumnType("decimal(5,2)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("varchar(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("EnglishName")
|
||||||
|
.HasMaxLength(150)
|
||||||
|
.HasColumnType("varchar(150)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<int>("LectureHours")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Nature")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("PracticeHours")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("TotalHours")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("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.Major", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("varchar(40)");
|
||||||
|
|
||||||
|
b.Property<Guid>("CollegeId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("DegreeType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("SchoolingYears")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid>("AdministrativeClassId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateOnly?>("DateOfBirth")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EnrollmentDate")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<int>("EnrollmentYear")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Gender")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("varchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("varchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("Phone")
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("varchar(30)");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("StudentNumber")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("varchar(30)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid>("CollegeId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Gender")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateOnly?>("HireDate")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<bool>("IsExternal")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("varchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("varchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("Phone")
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("varchar(30)");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("TeacherNumber")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("varchar(30)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("varchar(30)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("DataScope")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<Guid?>("CollegeId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("varchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLoginAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("datetime");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("StaffNumber")
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("varchar(30)");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("varchar(64)");
|
||||||
|
|
||||||
|
b.Property<string>("Method")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("varchar(10)");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("varchar(300)");
|
||||||
|
|
||||||
|
b.Property<int>("StatusCode")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UserId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.ToTable("AuditLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("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.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<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", 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<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+190
@@ -0,0 +1,190 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class PeopleAndCourses : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Courses",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||||
|
CollegeId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||||
|
EnglishName = table.Column<string>(type: "varchar(150)", maxLength: 150, nullable: true),
|
||||||
|
Credits = table.Column<decimal>(type: "decimal(5,2)", precision: 5, scale: 2, nullable: false),
|
||||||
|
TotalHours = table.Column<int>(type: "int", nullable: false),
|
||||||
|
LectureHours = table.Column<int>(type: "int", nullable: false),
|
||||||
|
PracticeHours = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Nature = table.Column<int>(type: "int", nullable: false),
|
||||||
|
AssessmentMethod = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "varchar(1000)", maxLength: 1000, nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
Code = table.Column<string>(type: "varchar(40)", maxLength: 40, nullable: false),
|
||||||
|
Name = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false),
|
||||||
|
SortOrder = table.Column<int>(type: "int", nullable: false),
|
||||||
|
IsEnabled = table.Column<bool>(type: "tinyint(1)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Courses", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Courses_Colleges_CollegeId",
|
||||||
|
column: x => x.CollegeId,
|
||||||
|
principalTable: "Colleges",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
})
|
||||||
|
.Annotation("MySQL:Charset", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Students",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||||
|
StudentNumber = table.Column<string>(type: "varchar(30)", maxLength: 30, nullable: false),
|
||||||
|
Name = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false),
|
||||||
|
Gender = table.Column<int>(type: "int", nullable: false),
|
||||||
|
AdministrativeClassId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||||
|
EnrollmentYear = table.Column<int>(type: "int", nullable: false),
|
||||||
|
EnrollmentDate = table.Column<DateOnly>(type: "date", nullable: false),
|
||||||
|
Status = table.Column<int>(type: "int", nullable: false),
|
||||||
|
DateOfBirth = table.Column<DateOnly>(type: "date", nullable: true),
|
||||||
|
Phone = table.Column<string>(type: "varchar(30)", maxLength: 30, nullable: true),
|
||||||
|
Email = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: true),
|
||||||
|
Notes = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true),
|
||||||
|
UserId = table.Column<Guid>(type: "char(36)", 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_Students", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Students_AdministrativeClasses_AdministrativeClassId",
|
||||||
|
column: x => x.AdministrativeClassId,
|
||||||
|
principalTable: "AdministrativeClasses",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Students_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
})
|
||||||
|
.Annotation("MySQL:Charset", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Teachers",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||||
|
TeacherNumber = table.Column<string>(type: "varchar(30)", maxLength: 30, nullable: false),
|
||||||
|
Name = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false),
|
||||||
|
Gender = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CollegeId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||||
|
Title = table.Column<string>(type: "varchar(30)", maxLength: 30, nullable: true),
|
||||||
|
Status = table.Column<int>(type: "int", nullable: false),
|
||||||
|
HireDate = table.Column<DateOnly>(type: "date", nullable: true),
|
||||||
|
IsExternal = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
|
Phone = table.Column<string>(type: "varchar(30)", maxLength: 30, nullable: true),
|
||||||
|
Email = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: true),
|
||||||
|
Notes = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true),
|
||||||
|
UserId = table.Column<Guid>(type: "char(36)", 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_Teachers", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Teachers_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Teachers_Colleges_CollegeId",
|
||||||
|
column: x => x.CollegeId,
|
||||||
|
principalTable: "Colleges",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
})
|
||||||
|
.Annotation("MySQL:Charset", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Courses_Code",
|
||||||
|
table: "Courses",
|
||||||
|
column: "Code",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Courses_CollegeId_Nature",
|
||||||
|
table: "Courses",
|
||||||
|
columns: new[] { "CollegeId", "Nature" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Courses_IsEnabled_SortOrder",
|
||||||
|
table: "Courses",
|
||||||
|
columns: new[] { "IsEnabled", "SortOrder" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Students_AdministrativeClassId_Status",
|
||||||
|
table: "Students",
|
||||||
|
columns: new[] { "AdministrativeClassId", "Status" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Students_EnrollmentYear",
|
||||||
|
table: "Students",
|
||||||
|
column: "EnrollmentYear");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Students_StudentNumber",
|
||||||
|
table: "Students",
|
||||||
|
column: "StudentNumber",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Students_UserId",
|
||||||
|
table: "Students",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Teachers_CollegeId_Status",
|
||||||
|
table: "Teachers",
|
||||||
|
columns: new[] { "CollegeId", "Status" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Teachers_TeacherNumber",
|
||||||
|
table: "Teachers",
|
||||||
|
column: "TeacherNumber",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Teachers_UserId",
|
||||||
|
table: "Teachers",
|
||||||
|
column: "UserId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Courses");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Students");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Teachers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+258
-8
@@ -72,7 +72,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
|
|
||||||
b.HasIndex("IsEnabled", "SortOrder");
|
b.HasIndex("IsEnabled", "SortOrder");
|
||||||
|
|
||||||
b.ToTable("AcademicTerms");
|
b.ToTable("AcademicTerms", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AdministrativeClass", b =>
|
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AdministrativeClass", b =>
|
||||||
@@ -121,7 +121,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
|
|
||||||
b.HasIndex("IsEnabled", "SortOrder");
|
b.HasIndex("IsEnabled", "SortOrder");
|
||||||
|
|
||||||
b.ToTable("AdministrativeClasses");
|
b.ToTable("AdministrativeClasses", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Building", b =>
|
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Building", b =>
|
||||||
@@ -164,7 +164,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
|
|
||||||
b.HasIndex("IsEnabled", "SortOrder");
|
b.HasIndex("IsEnabled", "SortOrder");
|
||||||
|
|
||||||
b.ToTable("Buildings");
|
b.ToTable("Buildings", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Campus", b =>
|
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Campus", b =>
|
||||||
@@ -205,7 +205,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
|
|
||||||
b.HasIndex("IsEnabled", "SortOrder");
|
b.HasIndex("IsEnabled", "SortOrder");
|
||||||
|
|
||||||
b.ToTable("Campuses");
|
b.ToTable("Campuses", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Classroom", b =>
|
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Classroom", b =>
|
||||||
@@ -258,7 +258,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
|
|
||||||
b.HasIndex("IsEnabled", "SortOrder");
|
b.HasIndex("IsEnabled", "SortOrder");
|
||||||
|
|
||||||
b.ToTable("Classrooms");
|
b.ToTable("Classrooms", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.College", b =>
|
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.College", b =>
|
||||||
@@ -304,7 +304,77 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
|
|
||||||
b.HasIndex("IsEnabled", "SortOrder");
|
b.HasIndex("IsEnabled", "SortOrder");
|
||||||
|
|
||||||
b.ToTable("Colleges");
|
b.ToTable("Colleges", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Course", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<int>("AssessmentMethod")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("varchar(40)");
|
||||||
|
|
||||||
|
b.Property<Guid>("CollegeId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Credits")
|
||||||
|
.HasPrecision(5, 2)
|
||||||
|
.HasColumnType("decimal(5,2)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("varchar(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("EnglishName")
|
||||||
|
.HasMaxLength(150)
|
||||||
|
.HasColumnType("varchar(150)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<int>("LectureHours")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Nature")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("PracticeHours")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("TotalHours")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Code")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("CollegeId", "Nature");
|
||||||
|
|
||||||
|
b.HasIndex("IsEnabled", "SortOrder");
|
||||||
|
|
||||||
|
b.ToTable("Courses", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Major", b =>
|
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Major", b =>
|
||||||
@@ -354,7 +424,144 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
|
|
||||||
b.HasIndex("IsEnabled", "SortOrder");
|
b.HasIndex("IsEnabled", "SortOrder");
|
||||||
|
|
||||||
b.ToTable("Majors");
|
b.ToTable("Majors", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Student", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid>("AdministrativeClassId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateOnly?>("DateOfBirth")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EnrollmentDate")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<int>("EnrollmentYear")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Gender")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("varchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("varchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("Phone")
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("varchar(30)");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("StudentNumber")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("varchar(30)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UserId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("EnrollmentYear");
|
||||||
|
|
||||||
|
b.HasIndex("StudentNumber")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.HasIndex("AdministrativeClassId", "Status");
|
||||||
|
|
||||||
|
b.ToTable("Students", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Teacher", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid>("CollegeId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Gender")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateOnly?>("HireDate")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<bool>("IsExternal")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("varchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("varchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("Phone")
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("varchar(30)");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("TeacherNumber")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("varchar(30)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("varchar(30)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UserId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TeacherNumber")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.HasIndex("CollegeId", "Status");
|
||||||
|
|
||||||
|
b.ToTable("Teachers", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jiaowu.Api.Domain.Identity.ApplicationRole", b =>
|
modelBuilder.Entity("Jiaowu.Api.Domain.Identity.ApplicationRole", b =>
|
||||||
@@ -519,7 +726,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
|
|
||||||
b.HasIndex("CreatedAt");
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
b.ToTable("AuditLogs");
|
b.ToTable("AuditLogs", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
@@ -664,6 +871,17 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
b.Navigation("Campus");
|
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.Major", b =>
|
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Major", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Jiaowu.Api.Domain.Academic.College", "College")
|
b.HasOne("Jiaowu.Api.Domain.Academic.College", "College")
|
||||||
@@ -675,6 +893,38 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
b.Navigation("College");
|
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<System.Guid>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationRole", null)
|
b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationRole", null)
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ builder.Services.Configure<JwtOptions>(
|
|||||||
builder.Configuration.GetSection(JwtOptions.SectionName));
|
builder.Configuration.GetSection(JwtOptions.SectionName));
|
||||||
builder.Services.AddScoped<ITokenService, TokenService>();
|
builder.Services.AddScoped<ITokenService, TokenService>();
|
||||||
builder.Services.AddScoped<DatabaseInitializer>();
|
builder.Services.AddScoped<DatabaseInitializer>();
|
||||||
|
builder.Services.AddScoped<DevelopmentSqliteMigrator>();
|
||||||
|
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
|||||||
@@ -64,4 +64,76 @@ public sealed class PersistenceTests : IAsyncLifetime
|
|||||||
|
|
||||||
Assert.Equal("东校区", saved.College!.Campus!.Name);
|
Assert.Equal("东校区", saved.College!.Campus!.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task People_and_course_relations_can_be_persisted()
|
||||||
|
{
|
||||||
|
var campus = new Campus { Code = "MAIN", Name = "主校区" };
|
||||||
|
var college = new College
|
||||||
|
{
|
||||||
|
Code = "CS",
|
||||||
|
Name = "计算机学院",
|
||||||
|
CampusId = campus.Id
|
||||||
|
};
|
||||||
|
var major = new Major
|
||||||
|
{
|
||||||
|
Code = "080901",
|
||||||
|
Name = "计算机科学与技术",
|
||||||
|
CollegeId = college.Id,
|
||||||
|
DegreeType = "工学学士"
|
||||||
|
};
|
||||||
|
var administrativeClass = new AdministrativeClass
|
||||||
|
{
|
||||||
|
Code = "CS2026-01",
|
||||||
|
Name = "计科 2026-1 班",
|
||||||
|
MajorId = major.Id,
|
||||||
|
Grade = 2026
|
||||||
|
};
|
||||||
|
_db.AddRange(
|
||||||
|
campus,
|
||||||
|
college,
|
||||||
|
major,
|
||||||
|
administrativeClass,
|
||||||
|
new Teacher
|
||||||
|
{
|
||||||
|
TeacherNumber = "T2026001",
|
||||||
|
Name = "陈老师",
|
||||||
|
CollegeId = college.Id,
|
||||||
|
Status = TeacherStatus.Active
|
||||||
|
},
|
||||||
|
new Student
|
||||||
|
{
|
||||||
|
StudentNumber = "202601001",
|
||||||
|
Name = "周同学",
|
||||||
|
AdministrativeClassId = administrativeClass.Id,
|
||||||
|
EnrollmentYear = 2026,
|
||||||
|
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
|
||||||
|
});
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var teacher = 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();
|
||||||
|
|
||||||
|
Assert.Equal("计算机学院", teacher.College!.Name);
|
||||||
|
Assert.Equal("计算机科学与技术", student.AdministrativeClass!.Major!.Name);
|
||||||
|
Assert.Equal(4m, course.Credits);
|
||||||
|
Assert.Equal(course.TotalHours, course.LectureHours + course.PracticeHours);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
@@ -24,6 +24,7 @@ declare module 'vue' {
|
|||||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { DataAnalysis, OfficeBuilding, Operation, User } from '@element-plus/icons-vue'
|
import {
|
||||||
|
Collection,
|
||||||
|
DataAnalysis,
|
||||||
|
OfficeBuilding,
|
||||||
|
Operation,
|
||||||
|
User,
|
||||||
|
UserFilled,
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -14,6 +21,8 @@ const pageTitle = computed(() => {
|
|||||||
const titles: Record<string, string> = {
|
const titles: Record<string, string> = {
|
||||||
dashboard: '教务总览',
|
dashboard: '教务总览',
|
||||||
'base-data': '基础数据',
|
'base-data': '基础数据',
|
||||||
|
personnel: '人员档案',
|
||||||
|
courses: '课程库',
|
||||||
users: '用户与权限',
|
users: '用户与权限',
|
||||||
}
|
}
|
||||||
return titles[String(route.name)] ?? '教务管理'
|
return titles[String(route.name)] ?? '教务管理'
|
||||||
@@ -60,6 +69,17 @@ onMounted(() => auth.refresh().catch(() => undefined))
|
|||||||
<el-icon><OfficeBuilding /></el-icon>
|
<el-icon><OfficeBuilding /></el-icon>
|
||||||
<template #title>基础数据</template>
|
<template #title>基础数据</template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
<el-menu-item
|
||||||
|
v-if="auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor'].includes(role))"
|
||||||
|
index="/personnel"
|
||||||
|
>
|
||||||
|
<el-icon><UserFilled /></el-icon>
|
||||||
|
<template #title>人员档案</template>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/courses">
|
||||||
|
<el-icon><Collection /></el-icon>
|
||||||
|
<template #title>课程库</template>
|
||||||
|
</el-menu-item>
|
||||||
<el-menu-item v-if="auth.isSuperAdmin" index="/users">
|
<el-menu-item v-if="auth.isSuperAdmin" index="/users">
|
||||||
<el-icon><User /></el-icon>
|
<el-icon><User /></el-icon>
|
||||||
<template #title>用户与权限</template>
|
<template #title>用户与权限</template>
|
||||||
|
|||||||
@@ -26,6 +26,19 @@ const router = createRouter({
|
|||||||
name: 'base-data',
|
name: 'base-data',
|
||||||
component: () => import('../views/BaseDataView.vue'),
|
component: () => import('../views/BaseDataView.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'personnel',
|
||||||
|
name: 'personnel',
|
||||||
|
component: () => import('../views/PersonnelView.vue'),
|
||||||
|
meta: {
|
||||||
|
roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'courses',
|
||||||
|
name: 'courses',
|
||||||
|
component: () => import('../views/CoursesView.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'users',
|
path: 'users',
|
||||||
name: 'users',
|
name: 'users',
|
||||||
|
|||||||
+41
-2
@@ -126,6 +126,35 @@ button { cursor: pointer; }
|
|||||||
.entity-form .el-select, .entity-form .el-date-editor { width: 100%; }
|
.entity-form .el-select, .entity-form .el-date-editor { width: 100%; }
|
||||||
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||||
.form-grid.compact { align-items: center; }
|
.form-grid.compact { align-items: center; }
|
||||||
|
.form-grid.three { grid-template-columns: repeat(3, 1fr); }
|
||||||
|
|
||||||
|
.registry-switch { display: grid; grid-template-columns: 1fr 1fr 180px; min-height: 112px; border: 1px solid var(--line); background: white; }
|
||||||
|
.registry-switch button { padding: 22px 26px; display: grid; gap: 4px; text-align: left; border: none; border-right: 1px solid var(--line); border-bottom: 3px solid transparent; background: white; }
|
||||||
|
.registry-switch button:hover { background: #fafbfc; }
|
||||||
|
.registry-switch button.active { border-bottom-color: var(--teal); background: #f7fbfa; }
|
||||||
|
.registry-switch button > span { color: #9ba3b1; font: 700 9px/1.2 Consolas, monospace; letter-spacing: .13em; }
|
||||||
|
.registry-switch button > b { font-family: "STZhongsong", "Songti SC", serif; font-size: 19px; }
|
||||||
|
.registry-switch button > small { color: var(--muted); font-size: 11px; }
|
||||||
|
.registry-total { padding: 20px; display: grid; place-content: center; text-align: center; background: var(--indigo-deep); color: white; }
|
||||||
|
.registry-total span, .registry-total small { color: #aeb8d6; font-size: 10px; }
|
||||||
|
.registry-total strong { margin: 3px 0; font: 700 30px/1 Consolas, monospace; color: #59d3c1; }
|
||||||
|
.filter-bar { min-height: 72px; padding: 14px 18px; display: flex; flex-wrap: wrap; align-items: center; gap: 10px; border-bottom: 1px solid var(--line); background: #fbfcfd; }
|
||||||
|
.filter-bar > .el-input { width: 280px; }
|
||||||
|
.filter-bar > .el-select { width: 150px; }
|
||||||
|
.registry-number, .course-code { color: var(--indigo); font: 700 12px/1.2 Consolas, monospace; letter-spacing: .03em; }
|
||||||
|
.record-name, .course-name { display: grid; gap: 3px; }
|
||||||
|
.record-name b, .course-name b { color: var(--ink); font-size: 13px; }
|
||||||
|
.record-name span, .course-name span { color: #9299a7; font-size: 10px; }
|
||||||
|
.pagination-bar { min-height: 62px; padding: 12px 18px; display: flex; align-items: center; justify-content: space-between; border-top: 1px solid var(--line); }
|
||||||
|
.pagination-bar > span { color: var(--muted); font-size: 11px; }
|
||||||
|
.course-ledger { min-height: 92px; display: flex; align-items: stretch; color: white; background: linear-gradient(105deg, #1a2d61, #263f80); overflow: hidden; position: relative; }
|
||||||
|
.course-ledger::after { content: "CATALOGUE"; position: absolute; right: 22px; top: 8px; color: rgba(255,255,255,.045); font: 700 44px/1 Consolas, monospace; letter-spacing: .08em; }
|
||||||
|
.ledger-label { width: 220px; padding: 18px 26px; display: flex; align-items: baseline; gap: 7px; border-right: 1px solid rgba(255,255,255,.12); position: relative; z-index: 1; }
|
||||||
|
.ledger-label span { align-self: flex-start; color: #aeb8d6; font-size: 11px; }
|
||||||
|
.ledger-label b { margin-left: auto; font: 700 31px/1 Consolas, monospace; color: #58d0bf; }
|
||||||
|
.ledger-label small { color: #c4cce3; font-size: 10px; }
|
||||||
|
.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; }
|
||||||
|
|
||||||
.login-page { min-height: 100vh; display: grid; grid-template-columns: minmax(440px, 1.2fr) minmax(420px, .8fr); background: white; }
|
.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 { 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; }
|
||||||
@@ -182,8 +211,18 @@ button { cursor: pointer; }
|
|||||||
.foundation-list i { display: none; }
|
.foundation-list i { display: none; }
|
||||||
.page-intro { align-items: flex-end; }
|
.page-intro { align-items: flex-end; }
|
||||||
.page-intro p { display: none; }
|
.page-intro p { display: none; }
|
||||||
.data-card { overflow: visible; }
|
.registry-switch { grid-template-columns: 1fr 1fr; }
|
||||||
.form-grid { grid-template-columns: 1fr; gap: 0; }
|
.registry-switch button { padding: 17px; }
|
||||||
|
.registry-total { grid-column: 1 / -1; display: flex; align-items: center; gap: 8px; min-height: 54px; }
|
||||||
|
.registry-total strong { margin-left: auto; font-size: 22px; }
|
||||||
|
.filter-bar > .el-input { width: 100%; }
|
||||||
|
.filter-bar > .el-select { flex: 1 1 140px; width: auto; }
|
||||||
|
.course-ledger { min-height: auto; display: block; }
|
||||||
|
.ledger-label { width: 100%; }
|
||||||
|
.ledger-rule { overflow-x: auto; }
|
||||||
|
.ledger-rule span { padding: 12px 20px; white-space: nowrap; }
|
||||||
|
.data-card { overflow: hidden; }
|
||||||
|
.form-grid, .form-grid.three { grid-template-columns: 1fr; gap: 0; }
|
||||||
.el-dialog { width: calc(100vw - 24px) !important; }
|
.el-dialog { width: calc(100vw - 24px) !important; }
|
||||||
.login-page { display: block; min-height: 100vh; background: #f4f6f9; }
|
.login-page { display: block; min-height: 100vh; background: #f4f6f9; }
|
||||||
.login-story { min-height: 310px; padding: 30px 25px; }
|
.login-story { min-height: 310px; padding: 30px 25px; }
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
|
||||||
|
import http, { apiErrorMessage } from '../api/http'
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const loading = ref(false)
|
||||||
|
const rows = ref<any[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const colleges = ref<any[]>([])
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const editingId = ref('')
|
||||||
|
const query = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
keyword: '',
|
||||||
|
collegeId: undefined as string | undefined,
|
||||||
|
nature: undefined as string | undefined,
|
||||||
|
isEnabled: undefined as boolean | undefined,
|
||||||
|
})
|
||||||
|
const form = reactive<Record<string, any>>({})
|
||||||
|
|
||||||
|
const canManage = computed(() =>
|
||||||
|
auth.user?.roles.some((role) =>
|
||||||
|
['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(role),
|
||||||
|
) ?? false,
|
||||||
|
)
|
||||||
|
const availableColleges = computed(() =>
|
||||||
|
auth.user?.roles.includes('CollegeAdmin') && auth.user.collegeId
|
||||||
|
? colleges.value.filter((item) => item.id === auth.user!.collegeId)
|
||||||
|
: colleges.value,
|
||||||
|
)
|
||||||
|
const natureLabels: Record<string, string> = {
|
||||||
|
GeneralRequired: '通识必修',
|
||||||
|
MajorRequired: '专业必修',
|
||||||
|
MajorElective: '专业选修',
|
||||||
|
GeneralElective: '通识选修',
|
||||||
|
Practice: '实践课程',
|
||||||
|
}
|
||||||
|
const assessmentLabels: Record<string, string> = {
|
||||||
|
Examination: '考试',
|
||||||
|
Assessment: '考查',
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm(row?: any) {
|
||||||
|
Object.keys(form).forEach((key) => delete form[key])
|
||||||
|
Object.assign(form, {
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
englishName: '',
|
||||||
|
collegeId: auth.user?.roles.includes('CollegeAdmin') ? auth.user.collegeId : undefined,
|
||||||
|
credits: 2,
|
||||||
|
totalHours: 32,
|
||||||
|
lectureHours: 24,
|
||||||
|
practiceHours: 8,
|
||||||
|
nature: 'MajorRequired',
|
||||||
|
assessmentMethod: 'Examination',
|
||||||
|
description: '',
|
||||||
|
isEnabled: true,
|
||||||
|
sortOrder: 0,
|
||||||
|
}, row ?? {})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { data } = await http.get('/courses', {
|
||||||
|
params: {
|
||||||
|
page: query.page,
|
||||||
|
pageSize: query.pageSize,
|
||||||
|
keyword: query.keyword || undefined,
|
||||||
|
collegeId: query.collegeId,
|
||||||
|
nature: query.nature,
|
||||||
|
isEnabled: query.isEnabled,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
rows.value = data.items
|
||||||
|
total.value = data.total
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(apiErrorMessage(error))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetFilters() {
|
||||||
|
Object.assign(query, {
|
||||||
|
page: 1,
|
||||||
|
keyword: '',
|
||||||
|
collegeId: auth.user?.roles.includes('CollegeAdmin') ? auth.user.collegeId : undefined,
|
||||||
|
nature: undefined,
|
||||||
|
isEnabled: undefined,
|
||||||
|
})
|
||||||
|
await load()
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreate() {
|
||||||
|
editingId.value = ''
|
||||||
|
resetForm()
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(row: any) {
|
||||||
|
editingId.value = row.id
|
||||||
|
resetForm(row)
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (!form.code?.trim() || !form.name?.trim() || !form.collegeId) {
|
||||||
|
ElMessage.warning('请填写课程编码、名称和开课学院。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (form.lectureHours + form.practiceHours > form.totalHours) {
|
||||||
|
ElMessage.warning('讲授学时与实践学时之和不能超过总学时。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (editingId.value) await http.put(`/courses/${editingId.value}`, form)
|
||||||
|
else await http.post('/courses', form)
|
||||||
|
ElMessage.success(editingId.value ? '课程已更新' : '课程已加入课程库')
|
||||||
|
dialogVisible.value = false
|
||||||
|
await load()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(apiErrorMessage(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(row: any) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定删除课程“${row.name}”吗?已进入培养方案的课程将无法删除。`,
|
||||||
|
'删除课程',
|
||||||
|
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' },
|
||||||
|
)
|
||||||
|
await http.delete(`/courses/${row.id}`)
|
||||||
|
ElMessage.success('课程已删除')
|
||||||
|
await load()
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
colleges.value = (await http.get('/base-data/colleges')).data
|
||||||
|
if (auth.user?.roles.includes('CollegeAdmin') && auth.user.collegeId) {
|
||||||
|
query.collegeId = auth.user.collegeId
|
||||||
|
}
|
||||||
|
await load()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-stack">
|
||||||
|
<section class="page-intro">
|
||||||
|
<div>
|
||||||
|
<span class="section-kicker">COURSE CATALOGUE</span>
|
||||||
|
<h2>课程库</h2>
|
||||||
|
<p>维护全校统一课程编码、学分学时和考核方式,供培养方案与教学任务引用。</p>
|
||||||
|
</div>
|
||||||
|
<el-button v-if="canManage" type="primary" :icon="Plus" @click="openCreate">新增课程</el-button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="course-ledger">
|
||||||
|
<div class="ledger-label">
|
||||||
|
<span>课程目录</span>
|
||||||
|
<b>{{ total }}</b>
|
||||||
|
<small>门课程</small>
|
||||||
|
</div>
|
||||||
|
<div class="ledger-rule">
|
||||||
|
<span>编码唯一</span>
|
||||||
|
<span>学时守恒</span>
|
||||||
|
<span>归属明确</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="data-card">
|
||||||
|
<div class="filter-bar">
|
||||||
|
<el-input
|
||||||
|
v-model="query.keyword"
|
||||||
|
:prefix-icon="Search"
|
||||||
|
clearable
|
||||||
|
placeholder="搜索课程编码、中英文名称"
|
||||||
|
@keyup.enter="query.page = 1; load()"
|
||||||
|
/>
|
||||||
|
<el-select v-model="query.collegeId" clearable placeholder="全部开课学院">
|
||||||
|
<el-option v-for="item in availableColleges" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="query.nature" clearable placeholder="全部课程性质">
|
||||||
|
<el-option v-for="(label, value) in natureLabels" :key="value" :label="label" :value="value" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="query.isEnabled" clearable placeholder="全部状态">
|
||||||
|
<el-option label="启用" :value="true" />
|
||||||
|
<el-option label="停用" :value="false" />
|
||||||
|
</el-select>
|
||||||
|
<el-button type="primary" @click="query.page = 1; load()">查询</el-button>
|
||||||
|
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="rows" class="data-table course-table">
|
||||||
|
<el-table-column label="课程编码" width="130">
|
||||||
|
<template #default="{ row }"><span class="course-code">{{ row.code }}</span></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="课程名称" min-width="210">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="course-name">
|
||||||
|
<b>{{ row.name }}</b>
|
||||||
|
<span>{{ row.englishName || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="collegeName" label="开课学院" min-width="145" />
|
||||||
|
<el-table-column label="性质" width="105">
|
||||||
|
<template #default="{ row }">{{ natureLabels[row.nature] }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="学分/学时" width="120">
|
||||||
|
<template #default="{ row }"><b>{{ row.credits }}</b> 学分 · {{ row.totalHours }} 学时</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="学时构成" width="135">
|
||||||
|
<template #default="{ row }">讲授 {{ row.lectureHours }} / 实践 {{ row.practiceHours }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="考核" width="80">
|
||||||
|
<template #default="{ row }">{{ assessmentLabels[row.assessmentMethod] }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="85">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="table-status" :class="{ off: !row.isEnabled }">{{ row.isEnabled ? '启用' : '停用' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column v-if="canManage" label="操作" width="145" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||||
|
<el-button link type="danger" @click="remove(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<template #empty><el-empty description="没有符合条件的课程" /></template>
|
||||||
|
</el-table>
|
||||||
|
<div class="pagination-bar">
|
||||||
|
<span>共 {{ total }} 门</span>
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="query.page"
|
||||||
|
:page-size="query.pageSize"
|
||||||
|
:total="total"
|
||||||
|
layout="prev, pager, next"
|
||||||
|
@current-change="load"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<el-dialog v-model="dialogVisible" :title="`${editingId ? '编辑' : '新增'}课程`" width="660px">
|
||||||
|
<el-form label-position="top" class="entity-form">
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="课程编码" required><el-input v-model="form.code" placeholder="全校唯一" /></el-form-item>
|
||||||
|
<el-form-item label="课程名称" required><el-input v-model="form.name" /></el-form-item>
|
||||||
|
</div>
|
||||||
|
<el-form-item label="英文名称"><el-input v-model="form.englishName" /></el-form-item>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="开课学院" required>
|
||||||
|
<el-select v-model="form.collegeId">
|
||||||
|
<el-option v-for="item in availableColleges" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="课程性质">
|
||||||
|
<el-select v-model="form.nature">
|
||||||
|
<el-option v-for="(label, value) in natureLabels" :key="value" :label="label" :value="value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid three">
|
||||||
|
<el-form-item label="学分"><el-input-number v-model="form.credits" :min="0.1" :max="99" :step="0.5" /></el-form-item>
|
||||||
|
<el-form-item label="总学时"><el-input-number v-model="form.totalHours" :min="1" /></el-form-item>
|
||||||
|
<el-form-item label="考核方式">
|
||||||
|
<el-select v-model="form.assessmentMethod">
|
||||||
|
<el-option v-for="(label, value) in assessmentLabels" :key="value" :label="label" :value="value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="讲授学时"><el-input-number v-model="form.lectureHours" :min="0" /></el-form-item>
|
||||||
|
<el-form-item label="实践学时"><el-input-number v-model="form.practiceHours" :min="0" /></el-form-item>
|
||||||
|
</div>
|
||||||
|
<el-form-item label="课程简介"><el-input v-model="form.description" type="textarea" :rows="3" /></el-form-item>
|
||||||
|
<div class="form-grid compact">
|
||||||
|
<el-form-item label="排序"><el-input-number v-model="form.sortOrder" :min="0" /></el-form-item>
|
||||||
|
<el-form-item label="状态"><el-switch v-model="form.isEnabled" active-text="启用" inactive-text="停用" /></el-form-item>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="save">保存课程</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { Calendar, Connection, OfficeBuilding, School } from '@element-plus/icons-vue'
|
import { Collection, OfficeBuilding, School, UserFilled } from '@element-plus/icons-vue'
|
||||||
import http from '../api/http'
|
import http from '../api/http'
|
||||||
|
|
||||||
interface DashboardData {
|
interface DashboardData {
|
||||||
@@ -14,10 +14,10 @@ const loading = ref(true)
|
|||||||
const data = ref<DashboardData>({ counts: {} })
|
const data = ref<DashboardData>({ counts: {} })
|
||||||
|
|
||||||
const stats = computed(() => [
|
const stats = computed(() => [
|
||||||
{ label: '校区', value: data.value.counts.campuses ?? 0, unit: '个', icon: OfficeBuilding },
|
|
||||||
{ label: '学院', value: data.value.counts.colleges ?? 0, unit: '个', icon: School },
|
{ label: '学院', value: data.value.counts.colleges ?? 0, unit: '个', icon: School },
|
||||||
{ label: '专业', value: data.value.counts.majors ?? 0, unit: '个', icon: Connection },
|
{ label: '教师档案', value: data.value.counts.teachers ?? 0, unit: '人', icon: UserFilled },
|
||||||
{ label: '行政班', value: data.value.counts.classes ?? 0, unit: '个', icon: Calendar },
|
{ label: '学生档案', value: data.value.counts.students ?? 0, unit: '人', icon: OfficeBuilding },
|
||||||
|
{ label: '课程库', value: data.value.counts.courses ?? 0, unit: '门', icon: Collection },
|
||||||
])
|
])
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -81,16 +81,21 @@ onMounted(async () => {
|
|||||||
<b>角色权限 + 学院数据范围</b>
|
<b>角色权限 + 学院数据范围</b>
|
||||||
<i class="done">已建立</i>
|
<i class="done">已建立</i>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>人员课程</span>
|
||||||
|
<b>教师档案 + 学生档案 + 课程库</b>
|
||||||
|
<i class="done">已建立</i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="work-card phase-card">
|
<article class="work-card phase-card">
|
||||||
<span class="section-kicker">NEXT MILESTONE</span>
|
<span class="section-kicker">NEXT MILESTONE</span>
|
||||||
<h3>下一段业务链</h3>
|
<h3>下一段业务链</h3>
|
||||||
<p>基础数据确认后,将进入学生、教师、课程库和培养方案。</p>
|
<p>人员与课程主数据已就绪,下一步进入培养方案、课程开设与教学任务。</p>
|
||||||
<div class="phase-line">
|
<div class="phase-line">
|
||||||
<span class="active">基础底座</span>
|
<span class="active">基础底座</span>
|
||||||
<span>人员档案</span>
|
<span class="active">人员档案</span>
|
||||||
<span>培养方案</span>
|
<span>培养方案</span>
|
||||||
<span>教学运行</span>
|
<span>教学运行</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,447 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
|
||||||
|
import http, { apiErrorMessage } from '../api/http'
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
|
||||||
|
type Registry = 'teachers' | 'students'
|
||||||
|
interface PageResult {
|
||||||
|
items: any[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const active = ref<Registry>('teachers')
|
||||||
|
const loading = ref(false)
|
||||||
|
const rows = ref<any[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const editingId = ref('')
|
||||||
|
const colleges = ref<any[]>([])
|
||||||
|
const majors = ref<any[]>([])
|
||||||
|
const classes = ref<any[]>([])
|
||||||
|
const query = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
keyword: '',
|
||||||
|
collegeId: undefined as string | undefined,
|
||||||
|
majorId: undefined as string | undefined,
|
||||||
|
classId: undefined as string | undefined,
|
||||||
|
enrollmentYear: undefined as number | undefined,
|
||||||
|
status: undefined as string | undefined,
|
||||||
|
})
|
||||||
|
const form = reactive<Record<string, any>>({})
|
||||||
|
|
||||||
|
const canManage = computed(() =>
|
||||||
|
auth.user?.roles.some((role) =>
|
||||||
|
['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(role),
|
||||||
|
) ?? false,
|
||||||
|
)
|
||||||
|
const scopedCollegeId = computed(() =>
|
||||||
|
auth.user?.roles.some((role) => ['CollegeAdmin', 'Counselor'].includes(role))
|
||||||
|
? auth.user.collegeId
|
||||||
|
: undefined,
|
||||||
|
)
|
||||||
|
const availableColleges = computed(() =>
|
||||||
|
scopedCollegeId.value
|
||||||
|
? colleges.value.filter((item) => item.id === scopedCollegeId.value)
|
||||||
|
: colleges.value,
|
||||||
|
)
|
||||||
|
const availableMajors = computed(() =>
|
||||||
|
scopedCollegeId.value
|
||||||
|
? majors.value.filter((item) => item.collegeId === scopedCollegeId.value)
|
||||||
|
: majors.value,
|
||||||
|
)
|
||||||
|
const availableClasses = computed(() => {
|
||||||
|
const majorIds = new Set(availableMajors.value.map((item) => item.id))
|
||||||
|
return classes.value.filter((item) => majorIds.has(item.majorId))
|
||||||
|
})
|
||||||
|
const pageTitle = computed(() => active.value === 'teachers' ? '教师档案' : '学生档案')
|
||||||
|
const numberLabel = computed(() => active.value === 'teachers' ? '工号' : '学号')
|
||||||
|
const filteredMajors = computed(() =>
|
||||||
|
query.collegeId
|
||||||
|
? availableMajors.value.filter((item) => item.collegeId === query.collegeId)
|
||||||
|
: availableMajors.value,
|
||||||
|
)
|
||||||
|
const filteredClasses = computed(() => {
|
||||||
|
if (query.majorId) {
|
||||||
|
return availableClasses.value.filter((item) => item.majorId === query.majorId)
|
||||||
|
}
|
||||||
|
if (query.collegeId) {
|
||||||
|
const majorIds = new Set(
|
||||||
|
availableMajors.value
|
||||||
|
.filter((item) => item.collegeId === query.collegeId)
|
||||||
|
.map((item) => item.id),
|
||||||
|
)
|
||||||
|
return availableClasses.value.filter((item) => majorIds.has(item.majorId))
|
||||||
|
}
|
||||||
|
return availableClasses.value
|
||||||
|
})
|
||||||
|
const formClasses = computed(() => availableClasses.value)
|
||||||
|
|
||||||
|
const genderLabels: Record<string, string> = {
|
||||||
|
Unknown: '未设置', Male: '男', Female: '女',
|
||||||
|
}
|
||||||
|
const teacherStatusLabels: Record<string, string> = {
|
||||||
|
Active: '在职', OnLeave: '离岗', Retired: '退休', Departed: '离职',
|
||||||
|
}
|
||||||
|
const studentStatusLabels: Record<string, string> = {
|
||||||
|
Active: '在籍', Suspended: '休学', Graduated: '毕业', Withdrawn: '退学',
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm(row?: any) {
|
||||||
|
Object.keys(form).forEach((key) => delete form[key])
|
||||||
|
if (active.value === 'teachers') {
|
||||||
|
Object.assign(form, {
|
||||||
|
teacherNumber: '',
|
||||||
|
name: '',
|
||||||
|
gender: 'Unknown',
|
||||||
|
collegeId: scopedCollegeId.value,
|
||||||
|
title: '',
|
||||||
|
status: 'Active',
|
||||||
|
hireDate: '',
|
||||||
|
isExternal: false,
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
notes: '',
|
||||||
|
}, row ?? {})
|
||||||
|
} else {
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
Object.assign(form, {
|
||||||
|
studentNumber: '',
|
||||||
|
name: '',
|
||||||
|
gender: 'Unknown',
|
||||||
|
administrativeClassId: undefined,
|
||||||
|
enrollmentYear: currentYear,
|
||||||
|
enrollmentDate: `${currentYear}-09-01`,
|
||||||
|
status: 'Active',
|
||||||
|
dateOfBirth: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
notes: '',
|
||||||
|
}, row ?? {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params: Record<string, any> = {
|
||||||
|
page: query.page,
|
||||||
|
pageSize: query.pageSize,
|
||||||
|
keyword: query.keyword || undefined,
|
||||||
|
collegeId: query.collegeId,
|
||||||
|
}
|
||||||
|
if (active.value === 'teachers') {
|
||||||
|
params.teacherStatus = query.status
|
||||||
|
} else {
|
||||||
|
Object.assign(params, {
|
||||||
|
majorId: query.majorId,
|
||||||
|
classId: query.classId,
|
||||||
|
enrollmentYear: query.enrollmentYear,
|
||||||
|
studentStatus: query.status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const { data } = await http.get<PageResult>(`/personnel/${active.value}`, { params })
|
||||||
|
rows.value = data.items
|
||||||
|
total.value = data.total
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(apiErrorMessage(error))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadReferences() {
|
||||||
|
const [collegeRes, majorRes, classRes] = await Promise.all([
|
||||||
|
http.get('/base-data/colleges'),
|
||||||
|
http.get('/base-data/majors'),
|
||||||
|
http.get('/base-data/classes'),
|
||||||
|
])
|
||||||
|
colleges.value = collegeRes.data
|
||||||
|
majors.value = majorRes.data
|
||||||
|
classes.value = classRes.data
|
||||||
|
if (scopedCollegeId.value) query.collegeId = scopedCollegeId.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchRegistry(kind: Registry) {
|
||||||
|
active.value = kind
|
||||||
|
Object.assign(query, {
|
||||||
|
page: 1,
|
||||||
|
keyword: '',
|
||||||
|
collegeId: scopedCollegeId.value,
|
||||||
|
majorId: undefined,
|
||||||
|
classId: undefined,
|
||||||
|
enrollmentYear: undefined,
|
||||||
|
status: undefined,
|
||||||
|
})
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFilters() {
|
||||||
|
Object.assign(query, {
|
||||||
|
page: 1,
|
||||||
|
keyword: '',
|
||||||
|
collegeId: scopedCollegeId.value,
|
||||||
|
majorId: undefined,
|
||||||
|
classId: undefined,
|
||||||
|
enrollmentYear: undefined,
|
||||||
|
status: undefined,
|
||||||
|
})
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreate() {
|
||||||
|
editingId.value = ''
|
||||||
|
resetForm()
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(row: any) {
|
||||||
|
editingId.value = row.id
|
||||||
|
resetForm(row)
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
const number = active.value === 'teachers' ? form.teacherNumber : form.studentNumber
|
||||||
|
if (!number?.trim() || !form.name?.trim()) {
|
||||||
|
ElMessage.warning(`请填写${numberLabel.value}和姓名。`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const path = `/personnel/${active.value}`
|
||||||
|
if (editingId.value) await http.put(`${path}/${editingId.value}`, form)
|
||||||
|
else await http.post(path, form)
|
||||||
|
ElMessage.success(editingId.value ? '档案已更新' : '档案已建立')
|
||||||
|
dialogVisible.value = false
|
||||||
|
await load()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(apiErrorMessage(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(row: any) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定删除“${row.name}”的${pageTitle.value}吗?`,
|
||||||
|
'删除档案',
|
||||||
|
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' },
|
||||||
|
)
|
||||||
|
await http.delete(`/personnel/${active.value}/${row.id}`)
|
||||||
|
ElMessage.success('档案已删除')
|
||||||
|
await load()
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([loadReferences(), load()])
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-stack">
|
||||||
|
<section class="page-intro registry-intro">
|
||||||
|
<div>
|
||||||
|
<span class="section-kicker">ACADEMIC REGISTRY</span>
|
||||||
|
<h2>人员档案</h2>
|
||||||
|
<p>以学校统一编号管理教师和学生的归属、状态与联系信息。</p>
|
||||||
|
</div>
|
||||||
|
<el-button v-if="canManage" type="primary" :icon="Plus" @click="openCreate">
|
||||||
|
新增{{ pageTitle }}
|
||||||
|
</el-button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="registry-switch">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:class="{ active: active === 'teachers' }"
|
||||||
|
@click="switchRegistry('teachers')"
|
||||||
|
>
|
||||||
|
<span>TEACHING STAFF</span>
|
||||||
|
<b>教师档案</b>
|
||||||
|
<small>工号、职称与任职状态</small>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:class="{ active: active === 'students' }"
|
||||||
|
@click="switchRegistry('students')"
|
||||||
|
>
|
||||||
|
<span>STUDENT ROLL</span>
|
||||||
|
<b>学生档案</b>
|
||||||
|
<small>学号、班级与学籍状态</small>
|
||||||
|
</button>
|
||||||
|
<div class="registry-total">
|
||||||
|
<span>当前结果</span>
|
||||||
|
<strong>{{ total }}</strong>
|
||||||
|
<small>条档案</small>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="data-card">
|
||||||
|
<div class="filter-bar">
|
||||||
|
<el-input
|
||||||
|
v-model="query.keyword"
|
||||||
|
:prefix-icon="Search"
|
||||||
|
clearable
|
||||||
|
:placeholder="`搜索${numberLabel}、姓名${active === 'teachers' ? '或职称' : ''}`"
|
||||||
|
@keyup.enter="query.page = 1; load()"
|
||||||
|
/>
|
||||||
|
<el-select v-model="query.collegeId" clearable placeholder="全部学院">
|
||||||
|
<el-option v-for="item in availableColleges" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
<template v-if="active === 'students'">
|
||||||
|
<el-select v-model="query.majorId" clearable filterable placeholder="全部专业">
|
||||||
|
<el-option v-for="item in filteredMajors" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="query.classId" clearable filterable placeholder="全部班级">
|
||||||
|
<el-option v-for="item in filteredClasses" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
<el-select v-model="query.status" clearable placeholder="全部状态">
|
||||||
|
<el-option
|
||||||
|
v-for="(label, value) in active === 'teachers' ? teacherStatusLabels : studentStatusLabels"
|
||||||
|
:key="value"
|
||||||
|
:label="label"
|
||||||
|
:value="value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-button type="primary" @click="query.page = 1; load()">查询</el-button>
|
||||||
|
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="rows" class="data-table registry-table">
|
||||||
|
<el-table-column :label="numberLabel" width="135">
|
||||||
|
<template #default="{ row }"><span class="registry-number">{{ active === 'teachers' ? row.teacherNumber : row.studentNumber }}</span></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="姓名" min-width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="record-name">
|
||||||
|
<b>{{ row.name }}</b>
|
||||||
|
<span>{{ genderLabels[row.gender] }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column v-if="active === 'teachers'" prop="collegeName" label="所属学院" min-width="150" />
|
||||||
|
<el-table-column v-if="active === 'teachers'" prop="title" label="职称" width="110">
|
||||||
|
<template #default="{ row }">{{ row.title || '—' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column v-if="active === 'teachers'" label="教师类别" width="105">
|
||||||
|
<template #default="{ row }">{{ row.isExternal ? '外聘' : '校内' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column v-if="active === 'students'" prop="className" label="行政班" min-width="155" />
|
||||||
|
<el-table-column v-if="active === 'students'" prop="majorName" label="专业" min-width="170" />
|
||||||
|
<el-table-column v-if="active === 'students'" prop="enrollmentYear" label="入学年级" width="105" />
|
||||||
|
<el-table-column label="状态" width="95">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="table-status" :class="{ off: row.status !== 'Active' }">
|
||||||
|
{{ (active === 'teachers' ? teacherStatusLabels : studentStatusLabels)[row.status] }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="联系方式" min-width="170">
|
||||||
|
<template #default="{ row }">{{ row.phone || row.email || '—' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column v-if="canManage" label="操作" width="145" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||||
|
<el-button link type="danger" @click="remove(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<template #empty><el-empty description="没有符合条件的档案" /></template>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination-bar">
|
||||||
|
<span>共 {{ total }} 条</span>
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="query.page"
|
||||||
|
:page-size="query.pageSize"
|
||||||
|
:total="total"
|
||||||
|
layout="prev, pager, next"
|
||||||
|
@current-change="load"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<el-dialog v-model="dialogVisible" :title="`${editingId ? '编辑' : '新增'}${pageTitle}`" width="620px">
|
||||||
|
<el-form label-position="top" class="entity-form">
|
||||||
|
<template v-if="active === 'teachers'">
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="工号" required><el-input v-model="form.teacherNumber" /></el-form-item>
|
||||||
|
<el-form-item label="姓名" required><el-input v-model="form.name" /></el-form-item>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="所属学院" required>
|
||||||
|
<el-select v-model="form.collegeId">
|
||||||
|
<el-option v-for="item in availableColleges" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="职称"><el-input v-model="form.title" placeholder="教授、副教授、讲师等" /></el-form-item>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="性别">
|
||||||
|
<el-select v-model="form.gender">
|
||||||
|
<el-option v-for="(label, value) in genderLabels" :key="value" :label="label" :value="value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="任职状态">
|
||||||
|
<el-select v-model="form.status">
|
||||||
|
<el-option v-for="(label, value) in teacherStatusLabels" :key="value" :label="label" :value="value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="入职日期"><el-date-picker v-model="form.hireDate" value-format="YYYY-MM-DD" /></el-form-item>
|
||||||
|
<el-form-item label="教师类别"><el-switch v-model="form.isExternal" active-text="外聘" inactive-text="校内" /></el-form-item>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="学号" required><el-input v-model="form.studentNumber" /></el-form-item>
|
||||||
|
<el-form-item label="姓名" required><el-input v-model="form.name" /></el-form-item>
|
||||||
|
</div>
|
||||||
|
<el-form-item label="行政班" required>
|
||||||
|
<el-select v-model="form.administrativeClassId" filterable>
|
||||||
|
<el-option
|
||||||
|
v-for="item in formClasses"
|
||||||
|
:key="item.id"
|
||||||
|
:label="`${item.collegeName} · ${item.majorName} · ${item.name}`"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="入学年级"><el-input-number v-model="form.enrollmentYear" :min="2000" :max="2200" /></el-form-item>
|
||||||
|
<el-form-item label="入学日期"><el-date-picker v-model="form.enrollmentDate" value-format="YYYY-MM-DD" /></el-form-item>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="性别">
|
||||||
|
<el-select v-model="form.gender">
|
||||||
|
<el-option v-for="(label, value) in genderLabels" :key="value" :label="label" :value="value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="学籍状态">
|
||||||
|
<el-select v-model="form.status">
|
||||||
|
<el-option v-for="(label, value) in studentStatusLabels" :key="value" :label="label" :value="value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<el-form-item label="出生日期"><el-date-picker v-model="form.dateOfBirth" value-format="YYYY-MM-DD" /></el-form-item>
|
||||||
|
</template>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="联系电话"><el-input v-model="form.phone" /></el-form-item>
|
||||||
|
<el-form-item label="电子邮箱"><el-input v-model="form.email" /></el-form-item>
|
||||||
|
</div>
|
||||||
|
<el-form-item label="备注"><el-input v-model="form.notes" type="textarea" :rows="3" /></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="save">保存档案</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user