607 lines
24 KiB
C#
607 lines
24 KiB
C#
using System.ComponentModel.DataAnnotations;
|
|
using Jiaowu.Api.Contracts;
|
|
using Jiaowu.Api.Domain.Academic;
|
|
using Jiaowu.Api.Domain.Identity;
|
|
using Jiaowu.Api.Infrastructure.Auth;
|
|
using Jiaowu.Api.Infrastructure.Caching;
|
|
using Jiaowu.Api.Infrastructure.Persistence;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Jiaowu.Api.Controllers;
|
|
|
|
[ApiController]
|
|
[Authorize(Roles = ReadRoles)]
|
|
[Route("api/personnel")]
|
|
public sealed class PersonnelController(
|
|
AppDbContext db,
|
|
ICurrentUserDataScope currentUserDataScope,
|
|
UserManager<ApplicationUser> userManager,
|
|
IAppCache cache) : ControllerBase
|
|
{
|
|
private const string ReadRoles =
|
|
SystemRoles.SuperAdmin + "," +
|
|
SystemRoles.AcademicAdmin + "," +
|
|
SystemRoles.CollegeAdmin + "," +
|
|
SystemRoles.Counselor + "," +
|
|
SystemRoles.Teacher + "," +
|
|
SystemRoles.Student;
|
|
|
|
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 = ApplyTeacherScope(db.Teachers.AsNoTracking());
|
|
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);
|
|
}
|
|
|
|
[HttpPost("teachers/{id:guid}/activate-account")]
|
|
[Authorize(Roles = WriteRoles)]
|
|
public async Task<ActionResult> ActivateTeacherAccount(
|
|
Guid id,
|
|
TeacherAccountActivationRequest request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return await db.ExecuteInRetriableTransactionAsync<ActionResult>(
|
|
async transaction =>
|
|
{
|
|
db.ChangeTracker.Clear();
|
|
var teacher = await db.Teachers.FindAsync([id], cancellationToken);
|
|
if (teacher is null) return NotFound();
|
|
if (!CanAccessCollege(teacher.CollegeId)) return Forbid();
|
|
if (teacher.Status != TeacherStatus.Active)
|
|
{
|
|
return ConflictProblem("仅在职教师可以激活登录账号。");
|
|
}
|
|
if (teacher.UserId.HasValue)
|
|
{
|
|
return ConflictProblem("该教师档案已经关联登录账号。");
|
|
}
|
|
|
|
var userName = teacher.TeacherNumber.Trim();
|
|
if (await userManager.FindByNameAsync(userName) is not null)
|
|
{
|
|
return ConflictProblem(
|
|
"该工号已有登录账号但未正确关联,请到账号管理中核对。");
|
|
}
|
|
|
|
var user = new ApplicationUser
|
|
{
|
|
UserName = userName,
|
|
DisplayName = teacher.Name,
|
|
StaffNumber = userName,
|
|
CollegeId = teacher.CollegeId,
|
|
IsEnabled = true,
|
|
LockoutEnabled = true
|
|
};
|
|
var result = await userManager.CreateAsync(user, request.Password);
|
|
if (!result.Succeeded)
|
|
{
|
|
await transaction.RollbackAsync(cancellationToken);
|
|
return IdentityValidationProblem(result);
|
|
}
|
|
|
|
result = await userManager.AddToRoleAsync(
|
|
user,
|
|
SystemRoles.Teacher);
|
|
if (!result.Succeeded)
|
|
{
|
|
await transaction.RollbackAsync(cancellationToken);
|
|
return IdentityValidationProblem(result);
|
|
}
|
|
|
|
teacher.UserId = user.Id;
|
|
await db.SaveChangesAsync(cancellationToken);
|
|
await transaction.CommitAsync(cancellationToken);
|
|
await cache.RemoveByTagAsync(
|
|
AppCacheTags.Timetables,
|
|
cancellationToken);
|
|
return Ok(new { user.Id, UserName = userName });
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
[HttpGet("students")]
|
|
public async Task<ActionResult<PagedResult<object>>> GetStudents(
|
|
[FromQuery] PersonnelQuery query,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var roles = currentUserDataScope.Current.Roles;
|
|
var canReadFullProfile = roles.Any(role =>
|
|
role is SystemRoles.SuperAdmin or
|
|
SystemRoles.AcademicAdmin or
|
|
SystemRoles.CollegeAdmin or
|
|
SystemRoles.Counselor);
|
|
var page = NormalizePage(query.Page);
|
|
var pageSize = NormalizePageSize(query.PageSize);
|
|
var source = ApplyStudentScope(db.Students.AsNoTracking());
|
|
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,
|
|
EnglishName = canReadFullProfile ? x.EnglishName : null,
|
|
IdCardNumber = canReadFullProfile ? x.IdCardNumber : null,
|
|
Nationality = canReadFullProfile ? x.Nationality : null,
|
|
Ethnicity = canReadFullProfile ? x.Ethnicity : null,
|
|
PoliticalStatus = canReadFullProfile ? x.PoliticalStatus : null,
|
|
NativePlace = canReadFullProfile ? x.NativePlace : null,
|
|
HouseholdAddress = canReadFullProfile ? x.HouseholdAddress : null,
|
|
CurrentAddress = canReadFullProfile ? x.CurrentAddress : null,
|
|
PostalCode = canReadFullProfile ? x.PostalCode : null,
|
|
x.Phone,
|
|
x.Email,
|
|
Qq = canReadFullProfile ? x.Qq : null,
|
|
x.WeChat,
|
|
x.EmergencyContactName,
|
|
x.EmergencyContactRelationship,
|
|
x.EmergencyContactPhone,
|
|
x.SpecialTags,
|
|
x.SpecialNeeds,
|
|
Biography = canReadFullProfile ? x.Biography : null,
|
|
Notes = canReadFullProfile ? x.Notes : null,
|
|
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,
|
|
EnglishName = Normalize(request.EnglishName),
|
|
IdCardNumber = Normalize(request.IdCardNumber),
|
|
Nationality = Normalize(request.Nationality),
|
|
Ethnicity = Normalize(request.Ethnicity),
|
|
PoliticalStatus = Normalize(request.PoliticalStatus),
|
|
NativePlace = Normalize(request.NativePlace),
|
|
HouseholdAddress = Normalize(request.HouseholdAddress),
|
|
CurrentAddress = Normalize(request.CurrentAddress),
|
|
PostalCode = Normalize(request.PostalCode),
|
|
Phone = Normalize(request.Phone),
|
|
Email = Normalize(request.Email),
|
|
Qq = Normalize(request.Qq),
|
|
WeChat = Normalize(request.WeChat),
|
|
EmergencyContactName = Normalize(request.EmergencyContactName),
|
|
EmergencyContactRelationship = Normalize(
|
|
request.EmergencyContactRelationship),
|
|
EmergencyContactPhone = Normalize(request.EmergencyContactPhone),
|
|
SpecialTags = Normalize(request.SpecialTags),
|
|
SpecialNeeds = Normalize(request.SpecialNeeds),
|
|
Biography = Normalize(request.Biography),
|
|
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.EnglishName = Normalize(request.EnglishName);
|
|
entity.IdCardNumber = Normalize(request.IdCardNumber);
|
|
entity.Nationality = Normalize(request.Nationality);
|
|
entity.Ethnicity = Normalize(request.Ethnicity);
|
|
entity.PoliticalStatus = Normalize(request.PoliticalStatus);
|
|
entity.NativePlace = Normalize(request.NativePlace);
|
|
entity.HouseholdAddress = Normalize(request.HouseholdAddress);
|
|
entity.CurrentAddress = Normalize(request.CurrentAddress);
|
|
entity.PostalCode = Normalize(request.PostalCode);
|
|
entity.Phone = Normalize(request.Phone);
|
|
entity.Email = Normalize(request.Email);
|
|
entity.Qq = Normalize(request.Qq);
|
|
entity.WeChat = Normalize(request.WeChat);
|
|
entity.EmergencyContactName = Normalize(request.EmergencyContactName);
|
|
entity.EmergencyContactRelationship = Normalize(
|
|
request.EmergencyContactRelationship);
|
|
entity.EmergencyContactPhone = Normalize(request.EmergencyContactPhone);
|
|
entity.SpecialTags = Normalize(request.SpecialTags);
|
|
entity.SpecialNeeds = Normalize(request.SpecialNeeds);
|
|
entity.Biography = Normalize(request.Biography);
|
|
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 IQueryable<Teacher> ApplyTeacherScope(IQueryable<Teacher> source)
|
|
{
|
|
var scope = currentUserDataScope.Current;
|
|
if (scope.Scope == DataScope.All) return source;
|
|
if (scope.Scope == DataScope.College)
|
|
return source.Where(x => x.CollegeId == scope.RestrictedCollegeId);
|
|
if (scope.Scope == DataScope.Class)
|
|
{
|
|
return source.Where(teacher => db.TeachingTasks.Any(task =>
|
|
task.Teachers.Any(item => item.TeacherId == teacher.Id) &&
|
|
task.Classes.Any(item =>
|
|
item.AdministrativeClass!.CounselorUserId == scope.UserId)));
|
|
}
|
|
|
|
var userId = scope.UserId;
|
|
return scope.IsInRole(SystemRoles.Teacher)
|
|
? source.Where(x => x.UserId == userId)
|
|
: source.Where(_ => false);
|
|
}
|
|
|
|
private IQueryable<Student> ApplyStudentScope(IQueryable<Student> source)
|
|
{
|
|
var scope = currentUserDataScope.Current;
|
|
if (scope.Scope == DataScope.All) return source;
|
|
if (scope.Scope == DataScope.College)
|
|
{
|
|
return source.Where(x =>
|
|
x.AdministrativeClass!.Major!.CollegeId == scope.RestrictedCollegeId);
|
|
}
|
|
if (scope.Scope == DataScope.Class)
|
|
{
|
|
return source.Where(x =>
|
|
x.AdministrativeClass!.CounselorUserId == scope.UserId);
|
|
}
|
|
|
|
var userId = scope.UserId;
|
|
var isTeacher = scope.IsInRole(SystemRoles.Teacher);
|
|
var isStudent = scope.IsInRole(SystemRoles.Student);
|
|
return source.Where(student =>
|
|
isStudent && student.UserId == userId ||
|
|
isTeacher && db.TeachingTasks.Any(task =>
|
|
task.Teachers.Any(item => item.Teacher!.UserId == userId) &&
|
|
task.Classes.Any(item =>
|
|
item.AdministrativeClassId == student.AdministrativeClassId)));
|
|
}
|
|
|
|
private bool CanAccessCollege(Guid collegeId) =>
|
|
currentUserDataScope.Current.CanAccessCollege(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);
|
|
await cache.RemoveByTagAsync(
|
|
AppCacheTags.Timetables,
|
|
cancellationToken);
|
|
await cache.RemoveByTagAsync(
|
|
AppCacheTags.Analytics,
|
|
cancellationToken);
|
|
return Created(string.Empty, new { id });
|
|
}
|
|
catch (DbUpdateException)
|
|
{
|
|
return ConflictProblem("编号已存在,或关联数据无效。");
|
|
}
|
|
}
|
|
|
|
private async Task<ActionResult> SaveNoContentAsync(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await db.SaveChangesAsync(cancellationToken);
|
|
await cache.RemoveByTagAsync(
|
|
AppCacheTags.Timetables,
|
|
cancellationToken);
|
|
await cache.RemoveByTagAsync(
|
|
AppCacheTags.Analytics,
|
|
cancellationToken);
|
|
return NoContent();
|
|
}
|
|
catch (DbUpdateException)
|
|
{
|
|
return ConflictProblem("编号已存在,或该档案已被其他业务引用。");
|
|
}
|
|
}
|
|
|
|
private ActionResult ConflictProblem(string detail) =>
|
|
Conflict(new ProblemDetails
|
|
{
|
|
Title = "无法完成操作",
|
|
Detail = detail,
|
|
Status = StatusCodes.Status409Conflict
|
|
});
|
|
|
|
private ActionResult IdentityValidationProblem(IdentityResult result)
|
|
{
|
|
foreach (var error in result.Errors)
|
|
ModelState.AddModelError(error.Code, error.Description);
|
|
return ValidationProblem(ModelState);
|
|
}
|
|
|
|
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(100)] string? EnglishName,
|
|
[MaxLength(30)] string? IdCardNumber,
|
|
[MaxLength(50)] string? Nationality,
|
|
[MaxLength(50)] string? Ethnicity,
|
|
[MaxLength(50)] string? PoliticalStatus,
|
|
[MaxLength(100)] string? NativePlace,
|
|
[MaxLength(300)] string? HouseholdAddress,
|
|
[MaxLength(300)] string? CurrentAddress,
|
|
[MaxLength(20)] string? PostalCode,
|
|
[MaxLength(30)] string? Phone,
|
|
[EmailAddress, MaxLength(100)] string? Email,
|
|
[MaxLength(30)] string? Qq,
|
|
[MaxLength(60)] string? WeChat,
|
|
[MaxLength(50)] string? EmergencyContactName,
|
|
[MaxLength(30)] string? EmergencyContactRelationship,
|
|
[MaxLength(30)] string? EmergencyContactPhone,
|
|
[MaxLength(300)] string? SpecialTags,
|
|
[MaxLength(1000)] string? SpecialNeeds,
|
|
[MaxLength(1000)] string? Biography,
|
|
[MaxLength(500)] string? Notes);
|
|
|
|
public sealed record TeacherAccountActivationRequest(
|
|
[Required, MinLength(8), MaxLength(100)] string Password);
|