3
This commit is contained in:
@@ -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);
|
||||
Reference in New Issue
Block a user