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 Get(CancellationToken token) { var source = ScopedChanges().AsNoTracking(); return Ok(await source.OrderByDescending(x => x.SubmittedAt).Select(x => new { x.Id, x.StudentId, x.Student!.StudentNumber, x.Student.Name, ClassName = x.Student.AdministrativeClass!.Name, CollegeName = 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)); } [HttpGet("options")] [Authorize(Roles = SystemRoles.Student)] public async Task 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 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 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 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 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 StudentStatusChangeRequest( StudentStatusChangeType Type, [Required, MinLength(10), MaxLength(1000)] string Reason); public sealed record StudentStatusReviewRequest( bool Approved, [MaxLength(500)] string? Comment);