授课资格:学院可直接选择本院教师、学期和课程并分配资格,服务端强制学院数据范围。[后端接口 (line 178)](/E:/jiaowu/src/Jiaowu.Api/Controllers/TeacherCourseApplicationsController.cs:178) · [页面 (line 151)](/E:/jiaowu/web/src/views/TeachingPreferencesView.vue:151) 学生培养方案:新增“我的培养方案”,展示学分进度及已完成、在读、重修中、未通过、待完成、未修读课程,并支持搜索筛选。[学生接口 (line 15)](/E:/jiaowu/src/Jiaowu.Api/Controllers/StudentCurriculumController.cs:15) · [学生页面 (line 88)](/E:/jiaowu/web/src/views/StudentCurriculumView.vue:88)
367 lines
14 KiB
C#
367 lines
14 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/teacher-course-applications")]
|
|
public sealed class TeacherCourseApplicationsController(
|
|
AppDbContext db,
|
|
ICurrentUserDataScope currentUserDataScope) : ControllerBase
|
|
{
|
|
private const string ReviewRoles =
|
|
SystemRoles.SuperAdmin + "," +
|
|
SystemRoles.AcademicAdmin + "," +
|
|
SystemRoles.CollegeAdmin;
|
|
|
|
[HttpGet("course-options")]
|
|
[Authorize(Roles = SystemRoles.Teacher)]
|
|
public async Task<ActionResult> GetCourseOptions(CancellationToken cancellationToken)
|
|
{
|
|
return Ok(await db.Courses.AsNoTracking()
|
|
.Where(x => x.IsEnabled)
|
|
.OrderBy(x => x.SortOrder)
|
|
.ThenBy(x => x.Code)
|
|
.Select(x => new
|
|
{
|
|
x.Id,
|
|
x.Code,
|
|
x.Name,
|
|
x.CollegeId,
|
|
CollegeName = x.College!.Name,
|
|
x.Credits,
|
|
x.Nature
|
|
})
|
|
.ToListAsync(cancellationToken));
|
|
}
|
|
|
|
[HttpGet("mine")]
|
|
[Authorize(Roles = SystemRoles.Teacher)]
|
|
public async Task<ActionResult> GetMine(
|
|
Guid? academicTermId,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var teacher = await CurrentTeacherAsync(cancellationToken);
|
|
if (teacher is null) return ConflictProblem("当前账号尚未关联在职教师档案。");
|
|
var source = db.TeacherCourseApplications.AsNoTracking()
|
|
.Where(x => x.TeacherId == teacher.Id);
|
|
if (academicTermId.HasValue)
|
|
source = source.Where(x => x.AcademicTermId == academicTermId);
|
|
return Ok(await source
|
|
.OrderByDescending(x => x.AcademicTerm!.StartDate)
|
|
.ThenBy(x => x.Course!.Code)
|
|
.Select(x => new
|
|
{
|
|
x.Id,
|
|
x.AcademicTermId,
|
|
TermName = x.AcademicTerm!.Name,
|
|
x.CourseId,
|
|
CourseCode = x.Course!.Code,
|
|
CourseName = x.Course.Name,
|
|
CollegeName = x.Course.College!.Name,
|
|
x.Status,
|
|
x.Statement,
|
|
x.ReviewComment,
|
|
x.SubmittedAt,
|
|
x.ReviewedAt
|
|
})
|
|
.ToListAsync(cancellationToken));
|
|
}
|
|
|
|
[HttpPost("mine")]
|
|
[Authorize(Roles = SystemRoles.Teacher)]
|
|
public async Task<ActionResult> Submit(
|
|
TeacherCourseApplicationRequest request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var teacher = await CurrentTeacherAsync(cancellationToken);
|
|
if (teacher is null) return ConflictProblem("当前账号尚未关联在职教师档案。");
|
|
if (!await db.AcademicTerms.AnyAsync(
|
|
x => x.Id == request.AcademicTermId && x.IsEnabled,
|
|
cancellationToken))
|
|
return ValidationProblem("所选学期不存在或已停用。");
|
|
if (!await db.Courses.AnyAsync(
|
|
x => x.Id == request.CourseId && x.IsEnabled,
|
|
cancellationToken))
|
|
return ValidationProblem("所选课程不存在或已停用。");
|
|
|
|
var application = await db.TeacherCourseApplications
|
|
.FirstOrDefaultAsync(x =>
|
|
x.AcademicTermId == request.AcademicTermId &&
|
|
x.TeacherId == teacher.Id &&
|
|
x.CourseId == request.CourseId,
|
|
cancellationToken);
|
|
if (application is not null &&
|
|
application.Status is TeacherCourseApplicationStatus.Pending or
|
|
TeacherCourseApplicationStatus.Approved)
|
|
return ConflictProblem("该学期的课程申报已提交或已审核通过。");
|
|
if (application is null)
|
|
{
|
|
application = new TeacherCourseApplication
|
|
{
|
|
AcademicTermId = request.AcademicTermId,
|
|
TeacherId = teacher.Id,
|
|
CourseId = request.CourseId
|
|
};
|
|
db.TeacherCourseApplications.Add(application);
|
|
}
|
|
application.Status = TeacherCourseApplicationStatus.Pending;
|
|
application.Statement = Normalize(request.Statement);
|
|
application.ReviewComment = null;
|
|
application.SubmittedAt = DateTime.UtcNow;
|
|
application.ReviewedAt = null;
|
|
application.ReviewedByUserId = null;
|
|
await db.SaveChangesAsync(cancellationToken);
|
|
return Created(string.Empty, new { application.Id });
|
|
}
|
|
|
|
[HttpDelete("mine/{id:guid}")]
|
|
[Authorize(Roles = SystemRoles.Teacher)]
|
|
public async Task<ActionResult> Withdraw(Guid id, CancellationToken cancellationToken)
|
|
{
|
|
var teacher = await CurrentTeacherAsync(cancellationToken);
|
|
if (teacher is null) return ConflictProblem("当前账号尚未关联在职教师档案。");
|
|
var application = await db.TeacherCourseApplications.FirstOrDefaultAsync(
|
|
x => x.Id == id && x.TeacherId == teacher.Id,
|
|
cancellationToken);
|
|
if (application is null) return NotFound();
|
|
if (application.Status != TeacherCourseApplicationStatus.Pending)
|
|
return ConflictProblem("只有待审核申报可以撤回。");
|
|
application.Status = TeacherCourseApplicationStatus.Withdrawn;
|
|
await db.SaveChangesAsync(cancellationToken);
|
|
return NoContent();
|
|
}
|
|
|
|
[HttpGet("reviews")]
|
|
[Authorize(Roles = ReviewRoles)]
|
|
public async Task<ActionResult> GetReviews(
|
|
Guid? academicTermId,
|
|
TeacherCourseApplicationStatus? status,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var source = ScopedApplications().AsNoTracking();
|
|
if (academicTermId.HasValue)
|
|
source = source.Where(x => x.AcademicTermId == academicTermId);
|
|
if (status.HasValue) source = source.Where(x => x.Status == status);
|
|
return Ok(await source
|
|
.OrderBy(x => x.Status)
|
|
.ThenByDescending(x => x.SubmittedAt)
|
|
.Select(x => new
|
|
{
|
|
x.Id,
|
|
x.AcademicTermId,
|
|
TermName = x.AcademicTerm!.Name,
|
|
x.TeacherId,
|
|
x.Teacher!.TeacherNumber,
|
|
TeacherName = x.Teacher.Name,
|
|
TeacherCollegeName = x.Teacher.College!.Name,
|
|
x.CourseId,
|
|
CourseCode = x.Course!.Code,
|
|
CourseName = x.Course.Name,
|
|
CourseCollegeName = x.Course.College!.Name,
|
|
x.Status,
|
|
x.Statement,
|
|
x.ReviewComment,
|
|
x.SubmittedAt,
|
|
x.ReviewedAt
|
|
})
|
|
.ToListAsync(cancellationToken));
|
|
}
|
|
|
|
[HttpGet("assignment-options")]
|
|
[Authorize(Roles = ReviewRoles)]
|
|
public async Task<ActionResult> GetAssignmentOptions(
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var teachers = db.Teachers.AsNoTracking()
|
|
.Where(x => x.Status == TeacherStatus.Active);
|
|
var collegeId = currentUserDataScope.Current.RestrictedCollegeId;
|
|
if (collegeId.HasValue)
|
|
teachers = teachers.Where(x => x.CollegeId == collegeId.Value);
|
|
|
|
return Ok(new
|
|
{
|
|
Teachers = await teachers
|
|
.OrderBy(x => x.TeacherNumber)
|
|
.Select(x => new
|
|
{
|
|
x.Id,
|
|
x.TeacherNumber,
|
|
x.Name,
|
|
x.Title,
|
|
x.CollegeId,
|
|
CollegeName = x.College!.Name
|
|
})
|
|
.ToListAsync(cancellationToken),
|
|
Courses = await db.Courses.AsNoTracking()
|
|
.Where(x => x.IsEnabled)
|
|
.OrderBy(x => x.SortOrder)
|
|
.ThenBy(x => x.Code)
|
|
.Select(x => new
|
|
{
|
|
x.Id,
|
|
x.Code,
|
|
x.Name,
|
|
x.CollegeId,
|
|
CollegeName = x.College!.Name,
|
|
x.Credits,
|
|
x.Nature
|
|
})
|
|
.ToListAsync(cancellationToken)
|
|
});
|
|
}
|
|
|
|
[HttpPost("assign")]
|
|
[Authorize(Roles = ReviewRoles)]
|
|
public async Task<ActionResult> Assign(
|
|
TeacherCourseAssignmentRequest request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (!await db.AcademicTerms.AnyAsync(
|
|
x => x.Id == request.AcademicTermId && x.IsEnabled,
|
|
cancellationToken))
|
|
return ValidationProblem("所选学期不存在或已停用。");
|
|
if (!await db.Courses.AnyAsync(
|
|
x => x.Id == request.CourseId && x.IsEnabled,
|
|
cancellationToken))
|
|
return ValidationProblem("所选课程不存在或已停用。");
|
|
|
|
var teachers = db.Teachers.Where(x =>
|
|
x.Id == request.TeacherId &&
|
|
x.Status == TeacherStatus.Active);
|
|
var collegeId = currentUserDataScope.Current.RestrictedCollegeId;
|
|
if (collegeId.HasValue)
|
|
teachers = teachers.Where(x => x.CollegeId == collegeId.Value);
|
|
if (!await teachers.AnyAsync(cancellationToken))
|
|
return ValidationProblem("所选教师不存在、不在职或不属于当前学院。");
|
|
|
|
var application = await db.TeacherCourseApplications
|
|
.FirstOrDefaultAsync(x =>
|
|
x.AcademicTermId == request.AcademicTermId &&
|
|
x.TeacherId == request.TeacherId &&
|
|
x.CourseId == request.CourseId,
|
|
cancellationToken);
|
|
if (application?.Status == TeacherCourseApplicationStatus.Approved)
|
|
return ConflictProblem("该教师在所选学期已具备此课程的授课资格。");
|
|
|
|
if (application is null)
|
|
{
|
|
application = new TeacherCourseApplication
|
|
{
|
|
AcademicTermId = request.AcademicTermId,
|
|
TeacherId = request.TeacherId,
|
|
CourseId = request.CourseId,
|
|
SubmittedAt = DateTime.UtcNow
|
|
};
|
|
db.TeacherCourseApplications.Add(application);
|
|
}
|
|
|
|
application.Status = TeacherCourseApplicationStatus.Approved;
|
|
application.ReviewComment = Normalize(request.Comment) ?? "学院直接分配";
|
|
application.ReviewedAt = DateTime.UtcNow;
|
|
application.ReviewedByUserId = currentUserDataScope.Current.UserId;
|
|
await db.SaveChangesAsync(cancellationToken);
|
|
return Ok(new { application.Id });
|
|
}
|
|
|
|
[HttpPost("{id:guid}/review")]
|
|
[Authorize(Roles = ReviewRoles)]
|
|
public async Task<ActionResult> Review(
|
|
Guid id,
|
|
TeacherCourseReviewRequest request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (request.Status is not (
|
|
TeacherCourseApplicationStatus.Approved or
|
|
TeacherCourseApplicationStatus.Rejected))
|
|
return ValidationProblem("审核结论只能为通过或驳回。");
|
|
var application = await ScopedApplications()
|
|
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
|
if (application is null) return NotFound();
|
|
if (application.Status != TeacherCourseApplicationStatus.Pending)
|
|
return ConflictProblem("只有待审核申报可以审核。");
|
|
application.Status = request.Status;
|
|
application.ReviewComment = Normalize(request.ReviewComment);
|
|
application.ReviewedAt = DateTime.UtcNow;
|
|
application.ReviewedByUserId = currentUserDataScope.Current.UserId;
|
|
await db.SaveChangesAsync(cancellationToken);
|
|
return NoContent();
|
|
}
|
|
|
|
[HttpGet("eligible")]
|
|
[Authorize(Roles = ReviewRoles)]
|
|
public async Task<ActionResult> GetEligible(
|
|
Guid academicTermId,
|
|
Guid courseId,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var source = ScopedApplications().AsNoTracking()
|
|
.Where(x =>
|
|
x.AcademicTermId == academicTermId &&
|
|
x.CourseId == courseId &&
|
|
x.Status == TeacherCourseApplicationStatus.Approved &&
|
|
x.Teacher!.Status == TeacherStatus.Active);
|
|
return Ok(await source
|
|
.OrderBy(x => x.Teacher!.TeacherNumber)
|
|
.Select(x => new
|
|
{
|
|
x.TeacherId,
|
|
x.Teacher!.TeacherNumber,
|
|
x.Teacher.Name,
|
|
x.Teacher.Title,
|
|
CollegeName = x.Teacher.College!.Name
|
|
})
|
|
.ToListAsync(cancellationToken));
|
|
}
|
|
|
|
private IQueryable<TeacherCourseApplication> ScopedApplications()
|
|
{
|
|
var source = db.TeacherCourseApplications.AsQueryable();
|
|
var collegeId = currentUserDataScope.Current.RestrictedCollegeId;
|
|
return collegeId.HasValue
|
|
? source.Where(x => x.Teacher!.CollegeId == collegeId.Value)
|
|
: source;
|
|
}
|
|
|
|
private Task<Teacher?> CurrentTeacherAsync(CancellationToken cancellationToken)
|
|
{
|
|
var userId = currentUserDataScope.Current.UserId;
|
|
return db.Teachers.FirstOrDefaultAsync(
|
|
x => x.UserId == userId && x.Status == TeacherStatus.Active,
|
|
cancellationToken);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
public sealed record TeacherCourseApplicationRequest(
|
|
Guid AcademicTermId,
|
|
Guid CourseId,
|
|
[MaxLength(500)] string? Statement);
|
|
|
|
public sealed record TeacherCourseReviewRequest(
|
|
TeacherCourseApplicationStatus Status,
|
|
[MaxLength(500)] string? ReviewComment);
|
|
|
|
public sealed record TeacherCourseAssignmentRequest(
|
|
Guid AcademicTermId,
|
|
Guid TeacherId,
|
|
Guid CourseId,
|
|
[MaxLength(500)] string? Comment);
|