Files
Academic-Affairs-System/src/Jiaowu.Api/Controllers/PersonnelController.cs
T
2026-07-24 22:18:32 +08:00

521 lines
19 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.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,
ICurrentUserDataScope currentUserDataScope,
PersonnelAccountService personnelAccountService) : 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 CreateWithAccountAsync(
entity.Id,
request.InitialPassword,
() => personnelAccountService.EnsureTeacherAccountAsync(
entity,
request.InitialPassword,
cancellationToken),
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);
if (!entity.UserId.HasValue &&
(string.IsNullOrWhiteSpace(request.InitialPassword) ||
request.InitialPassword.Length < 8))
return ValidationProblem("该教师档案尚未开通账号,请填写至少 8 位初始密码。");
if (entity.UserId.HasValue || !string.IsNullOrWhiteSpace(request.InitialPassword))
{
var account = await personnelAccountService.EnsureTeacherAccountAsync(
entity,
request.InitialPassword,
cancellationToken);
if (!account.Success) return AccountProblem(account.Error!);
}
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 = 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,
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 CreateWithAccountAsync(
entity.Id,
request.InitialPassword,
() => personnelAccountService.EnsureStudentAccountAsync(
entity,
request.InitialPassword,
cancellationToken),
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);
if (!entity.UserId.HasValue &&
(string.IsNullOrWhiteSpace(request.InitialPassword) ||
request.InitialPassword.Length < 8))
return ValidationProblem("该学生档案尚未开通账号,请填写至少 8 位初始密码。");
if (entity.UserId.HasValue || !string.IsNullOrWhiteSpace(request.InitialPassword))
{
var account = await personnelAccountService.EnsureStudentAccountAsync(
entity,
request.InitialPassword,
cancellationToken);
if (!account.Success) return AccountProblem(account.Error!);
}
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);
return Created(string.Empty, new { id });
}
catch (DbUpdateException)
{
return ConflictProblem("编号已存在,或关联数据无效。");
}
}
private async Task<ActionResult> CreateWithAccountAsync(
Guid id,
string? initialPassword,
Func<Task<PersonnelAccountResult>> createAccount,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(initialPassword) || initialPassword.Length < 8)
return ValidationProblem("新增人员时必须填写至少 8 位初始密码。");
await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken);
try
{
var account = await createAccount();
if (!account.Success)
{
await transaction.RollbackAsync(cancellationToken);
return AccountProblem(account.Error!);
}
await transaction.CommitAsync(cancellationToken);
return Created(string.Empty, new
{
id,
account.UserId,
account.UserName
});
}
catch (DbUpdateException)
{
await transaction.RollbackAsync(cancellationToken);
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 ActionResult AccountProblem(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,
[MinLength(8), MaxLength(100)] string? InitialPassword = null);
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,
[MinLength(8), MaxLength(100)] string? InitialPassword = null);