Files
Academic-Affairs-System/src/Jiaowu.Api/Controllers/StudentStatusChangesController.cs
T
2026-08-11 11:52:43 +08:00

270 lines
11 KiB
C#

using System.ComponentModel.DataAnnotations;
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]
[Route("api/student-status-changes")]
public sealed class StudentStatusChangesController(
AppDbContext db,
ICurrentUserDataScope currentUserDataScope) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<StudentStatusChangePage>> Get(
int page = 1,
int pageSize = 20,
CancellationToken token = default)
{
if (page < 1 || pageSize is < 1 or > 100)
return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。");
var source = ScopedChanges().AsNoTracking();
var userScope = currentUserDataScope.Current;
var total = await source.CountAsync(token);
var actionableTotal = await source.CountAsync(change =>
(change.State == StudentStatusChangeState.Submitted &&
userScope.IsInRole(SystemRoles.Counselor)) ||
(change.State == StudentStatusChangeState.CounselorApproved &&
userScope.IsInRole(SystemRoles.CollegeAdmin)) ||
(change.State == StudentStatusChangeState.CollegeApproved &&
(userScope.IsInRole(SystemRoles.AcademicAdmin) ||
userScope.IsInRole(SystemRoles.SuperAdmin))), token);
var finishedTotal = await source.CountAsync(change =>
change.State == StudentStatusChangeState.Approved ||
change.State == StudentStatusChangeState.Rejected ||
change.State == StudentStatusChangeState.Cancelled, token);
var items = await source
.OrderByDescending(x => x.SubmittedAt)
.ThenByDescending(x => x.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(x => new StudentStatusChangeListItem(
x.Id,
x.StudentId,
x.Student!.StudentNumber,
x.Student.Name,
x.Student.AdministrativeClass!.Name,
x.Student.AdministrativeClass.Major!.College!.Name,
x.Type,
x.OriginalStatus,
x.TargetStatus,
x.Reason,
x.State,
x.ReviewComment,
x.SubmittedAt,
x.ReviewedAt,
x.ApprovedAt))
.ToListAsync(token);
return Ok(new StudentStatusChangePage(
items, total, page, pageSize, actionableTotal, finishedTotal));
}
[HttpGet("options")]
[Authorize(Roles = SystemRoles.Student)]
public async Task<ActionResult> GetOptions(CancellationToken token)
{
var userId = currentUserDataScope.Current.UserId;
var student = await db.Students.AsNoTracking()
.FirstOrDefaultAsync(x => x.UserId == userId, token);
if (student is null) return ConflictProblem("当前账号未关联学生档案。");
var types = student.Status switch
{
StudentStatus.Active => new[]
{
StudentStatusChangeType.Suspension,
StudentStatusChangeType.Withdrawal
},
StudentStatus.Suspended => new[]
{
StudentStatusChangeType.Resumption,
StudentStatusChangeType.Withdrawal
},
_ => []
};
var hasPending = await db.StudentStatusChanges.AnyAsync(x =>
x.StudentId == student.Id &&
(x.State == StudentStatusChangeState.Submitted ||
x.State == StudentStatusChangeState.CounselorApproved ||
x.State == StudentStatusChangeState.CollegeApproved), token);
return Ok(new { student.Status, Types = types, HasPending = hasPending });
}
[HttpPost]
[Authorize(Roles = SystemRoles.Student)]
public async Task<ActionResult> Create(
StudentStatusChangeRequest request,
CancellationToken token)
{
var userId = currentUserDataScope.Current.UserId;
var student = await db.Students.FirstOrDefaultAsync(x => x.UserId == userId, token);
if (student is null) return ConflictProblem("当前账号未关联学生档案。");
var target = request.Type switch
{
StudentStatusChangeType.Suspension when student.Status == StudentStatus.Active =>
StudentStatus.Suspended,
StudentStatusChangeType.Resumption when student.Status == StudentStatus.Suspended =>
StudentStatus.Active,
StudentStatusChangeType.Withdrawal when student.Status is StudentStatus.Active or StudentStatus.Suspended =>
StudentStatus.Withdrawn,
_ => (StudentStatus?)null
};
if (!target.HasValue) return ConflictProblem("当前学籍状态不能申请该类异动。");
if (await db.StudentStatusChanges.AnyAsync(x => x.StudentId == student.Id &&
x.State != StudentStatusChangeState.Approved &&
x.State != StudentStatusChangeState.Rejected &&
x.State != StudentStatusChangeState.Cancelled, token))
return ConflictProblem("已有一项学籍异动正在审核中。");
var change = new StudentStatusChange
{
StudentId = student.Id, Type = request.Type,
OriginalStatus = student.Status, TargetStatus = target.Value,
Reason = request.Reason.Trim()
};
db.StudentStatusChanges.Add(change);
await db.SaveChangesAsync(token);
return Created(string.Empty, new { change.Id });
}
[HttpPost("{id:guid}/review")]
public async Task<ActionResult> Review(
Guid id,
StudentStatusReviewRequest request,
CancellationToken token)
{
var change = await ScopedChanges().Include(x => x.Student)
.FirstOrDefaultAsync(x => x.Id == id, token);
if (change is null) return NotFound();
var scope = currentUserDataScope.Current;
if (!CanReviewCurrentStage(scope, change.State))
return ConflictProblem("当前角色或审核阶段不允许执行该操作。");
if (!request.Approved)
{
if (string.IsNullOrWhiteSpace(request.Comment))
return BadRequest(new ProblemDetails
{
Title = "审核意见不完整",
Detail = "驳回申请时必须填写审核意见。",
Status = StatusCodes.Status400BadRequest
});
change.State = StudentStatusChangeState.Rejected;
}
else if (scope.IsInRole(SystemRoles.Counselor) &&
change.State == StudentStatusChangeState.Submitted)
change.State = StudentStatusChangeState.CounselorApproved;
else if (scope.IsInRole(SystemRoles.CollegeAdmin) &&
change.State == StudentStatusChangeState.CounselorApproved)
change.State = StudentStatusChangeState.CollegeApproved;
else if ((scope.IsInRole(SystemRoles.AcademicAdmin) ||
scope.IsInRole(SystemRoles.SuperAdmin)) &&
change.State == StudentStatusChangeState.CollegeApproved)
{
change.State = StudentStatusChangeState.Approved;
change.Student!.Status = change.TargetStatus;
change.ApprovedAt = DateTime.UtcNow;
}
else return ConflictProblem("当前角色或审核阶段不允许执行该操作。");
change.ReviewComment = request.Comment?.Trim();
change.ReviewedAt = DateTime.UtcNow;
await db.SaveChangesAsync(token);
return NoContent();
}
[HttpPost("{id:guid}/cancel")]
[Authorize(Roles = SystemRoles.Student)]
public async Task<ActionResult> Cancel(Guid id, CancellationToken token)
{
var userId = currentUserDataScope.Current.UserId;
var change = await db.StudentStatusChanges.FirstOrDefaultAsync(
x => x.Id == id && x.Student!.UserId == userId, token);
if (change is null) return NotFound();
if (change.State != StudentStatusChangeState.Submitted)
return ConflictProblem("只有尚未进入审核的申请可以撤回。");
change.State = StudentStatusChangeState.Cancelled;
change.ReviewedAt = DateTime.UtcNow;
await db.SaveChangesAsync(token);
return NoContent();
}
private IQueryable<StudentStatusChange> ScopedChanges()
{
var scope = currentUserDataScope.Current;
var source = db.StudentStatusChanges.AsQueryable();
if (scope.IsInRole(SystemRoles.Student))
return source.Where(x => x.Student!.UserId == scope.UserId);
if (scope.IsInRole(SystemRoles.Counselor))
return source.Where(x =>
x.Student!.AdministrativeClass!.CounselorUserId == scope.UserId);
if (scope.IsInRole(SystemRoles.CollegeAdmin))
return source.Where(x =>
x.Student!.AdministrativeClass!.Major!.CollegeId == scope.CollegeId);
if (scope.IsInRole(SystemRoles.AcademicAdmin) ||
scope.IsInRole(SystemRoles.SuperAdmin)) return source;
return source.Where(_ => false);
}
private static bool CanReviewCurrentStage(
CurrentUserScope scope,
StudentStatusChangeState state) =>
state switch
{
StudentStatusChangeState.Submitted =>
scope.IsInRole(SystemRoles.Counselor),
StudentStatusChangeState.CounselorApproved =>
scope.IsInRole(SystemRoles.CollegeAdmin),
StudentStatusChangeState.CollegeApproved =>
scope.IsInRole(SystemRoles.AcademicAdmin) ||
scope.IsInRole(SystemRoles.SuperAdmin),
_ => false
};
private ActionResult ConflictProblem(string detail) => Conflict(new ProblemDetails
{
Title = "无法完成学籍异动操作", Detail = detail,
Status = StatusCodes.Status409Conflict
});
}
public sealed record StudentStatusChangePage(
IReadOnlyCollection<StudentStatusChangeListItem> Items,
int Total,
int Page,
int PageSize,
int ActionableTotal,
int FinishedTotal);
public sealed record StudentStatusChangeListItem(
Guid Id,
Guid StudentId,
string StudentNumber,
string Name,
string ClassName,
string CollegeName,
StudentStatusChangeType Type,
StudentStatus OriginalStatus,
StudentStatus TargetStatus,
string Reason,
StudentStatusChangeState State,
string? ReviewComment,
DateTime SubmittedAt,
DateTime? ReviewedAt,
DateTime? ApprovedAt);
public sealed record StudentStatusChangeRequest(
StudentStatusChangeType Type,
[Required, MinLength(10), MaxLength(1000)] string Reason);
public sealed record StudentStatusReviewRequest(
bool Approved,
[MaxLength(500)] string? Comment);