Implement requested workflow and UI updates
This commit is contained in:
1 parent
5959585864
commit
390d0b123f
18 files changed
+7800
-35
No files matched your search
@@ -446,6 +446,15 @@ public sealed class AcademicPlanningController(
|
||||
.Select(x => x.CourseId)
|
||||
.Distinct()
|
||||
.ToHashSet();
|
||||
var approvedSubstitutions = await db.CourseSubstitutions.AsNoTracking()
|
||||
.Where(x => x.StudentId == student.Id && x.Status == ApprovalStatus.Approved)
|
||||
.Select(x => new { x.OriginalCourseId, x.SubstituteCourseId })
|
||||
.ToListAsync(cancellationToken);
|
||||
foreach (var substitution in approvedSubstitutions)
|
||||
{
|
||||
if (passedCourseIds.Contains(substitution.SubstituteCourseId))
|
||||
passedCourseIds.Add(substitution.OriginalCourseId);
|
||||
}
|
||||
var failedCourseIds = gradeAttempts
|
||||
.GroupBy(x => x.CourseId)
|
||||
.Where(group => !group.Any(x =>
|
||||
|
||||
@@ -439,9 +439,22 @@ public sealed class ApprovalsController(AppDbContext db, ICurrentUserDataScope s
|
||||
ct);
|
||||
if (!hasPassedSubstitute)
|
||||
return ConflictProblem("替代课程必须具有已发布且及格的成绩。");
|
||||
var applicant = await db.Students.AsNoTracking().Where(x => x.Id == sid.Value)
|
||||
.Select(x => new { MajorId = x.AdministrativeClass!.MajorId, x.EnrollmentYear })
|
||||
.FirstAsync(ct);
|
||||
var rule = await db.CourseEquivalencyRules.AsNoTracking()
|
||||
.Where(x => x.IsEnabled && x.OriginalCourseId == req.OriginalCourseId &&
|
||||
x.SubstituteCourseId == req.SubstituteCourseId)
|
||||
.Where(x => !x.MajorId.HasValue || x.MajorId == applicant.MajorId)
|
||||
.Where(x => !x.EffectiveGrade.HasValue || x.EffectiveGrade == applicant.EnrollmentYear)
|
||||
.OrderByDescending(x => x.MajorId.HasValue)
|
||||
.ThenByDescending(x => x.EffectiveGrade.HasValue)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
if (rule is null)
|
||||
return ConflictProblem("该课程组合未配置适用的等价课程规则,暂不能申请替代。");
|
||||
if (await db.CourseSubstitutions.AnyAsync(x => x.StudentId == sid && x.OriginalCourseId == req.OriginalCourseId && x.Status == ApprovalStatus.Submitted, ct))
|
||||
return ConflictProblem("该课程已有替代申请在审核中。");
|
||||
var cs = new CourseSubstitution { StudentId = sid.Value, OriginalCourseId = req.OriginalCourseId, SubstituteCourseId = req.SubstituteCourseId, Reason = req.Reason.Trim() };
|
||||
var cs = new CourseSubstitution { StudentId = sid.Value, OriginalCourseId = req.OriginalCourseId, SubstituteCourseId = req.SubstituteCourseId, CourseEquivalencyRuleId = rule.Id, Reason = req.Reason.Trim() };
|
||||
db.CourseSubstitutions.Add(cs);
|
||||
await db.SaveChangesAsync(ct);
|
||||
var info = await db.CourseSubstitutions.Where(x => x.Id == cs.Id)
|
||||
@@ -458,35 +471,8 @@ public sealed class ApprovalsController(AppDbContext db, ICurrentUserDataScope s
|
||||
var cs = await db.CourseSubstitutions.Include(x => x.Student).Include(x => x.OriginalCourse).Include(x => x.SubstituteCourse).FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
if (cs is null) return NotFound();
|
||||
cs.Status = ApprovalStatus.Approved; cs.ReviewedAt = DateTime.UtcNow; cs.ReviewedByUserId = scope.Current.UserId;
|
||||
// Auto-apply: copy passed grade to original course
|
||||
var currentTerm = await db.AcademicTerms.FirstOrDefaultAsync(x => x.IsCurrent, ct);
|
||||
if (currentTerm is not null)
|
||||
{
|
||||
var passedRecord = await db.GradeRecords
|
||||
.Where(x => x.StudentId == cs.StudentId && x.GradeSheet!.TeachingTask!.CourseId == cs.SubstituteCourseId && x.TotalScore >= 60)
|
||||
.OrderByDescending(x => x.GradeSheet!.TeachingTask!.AcademicTerm!.StartDate)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
if (passedRecord is not null)
|
||||
{
|
||||
// Find or create grade record for original course
|
||||
var origTask = await db.TeachingTasks.FirstOrDefaultAsync(x => x.CourseId == cs.OriginalCourseId && x.AcademicTermId == currentTerm.Id, ct);
|
||||
if (origTask is not null)
|
||||
{
|
||||
var origSheet = await db.GradeSheets.FirstOrDefaultAsync(x => x.TeachingTaskId == origTask.Id, ct);
|
||||
if (origSheet is not null)
|
||||
{
|
||||
var origRecord = await db.GradeRecords.FirstOrDefaultAsync(x => x.GradeSheetId == origSheet.Id && x.StudentId == cs.StudentId, ct);
|
||||
if (origRecord is not null)
|
||||
{
|
||||
origRecord.TotalScore = passedRecord.TotalScore;
|
||||
origRecord.GradePoint = passedRecord.GradePoint;
|
||||
origRecord.Notes =
|
||||
$"课程替代:{cs.SubstituteCourse!.Code} {cs.SubstituteCourse.Name}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Do not fabricate or edit a grade record. Formal curriculum and
|
||||
// graduation calculations resolve this approved rule from source data.
|
||||
await db.SaveChangesAsync(ct);
|
||||
await NotifyStudent(cs.StudentId, "课程替代申请已通过", $"《{cs.SubstituteCourse!.Name}》替代《{cs.OriginalCourse!.Name}》已生效。", ct);
|
||||
return NoContent();
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
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(Roles = SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin + "," + SystemRoles.CollegeAdmin)]
|
||||
[Route("api/course-equivalency-rules")]
|
||||
public sealed class CourseEquivalencyRulesController(
|
||||
AppDbContext db,
|
||||
ICurrentUserDataScope scope) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult> Get(CancellationToken token)
|
||||
{
|
||||
var collegeId = scope.Current.RestrictedCollegeId;
|
||||
return Ok(await Scoped(collegeId).AsNoTracking()
|
||||
.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.OriginalCourse!.Code)
|
||||
.Select(x => new { x.Id, x.OriginalCourseId, OriginalCourseCode = x.OriginalCourse!.Code,
|
||||
OriginalCourseName = x.OriginalCourse.Name, x.SubstituteCourseId,
|
||||
SubstituteCourseCode = x.SubstituteCourse!.Code, SubstituteCourseName = x.SubstituteCourse.Name,
|
||||
x.MajorId, MajorName = x.Major == null ? null : x.Major.Name, x.EffectiveGrade, x.IsEnabled, x.Notes, x.UpdatedAt })
|
||||
.ToListAsync(token));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> Create(CourseEquivalencyRuleRequest request, CancellationToken token)
|
||||
{
|
||||
if (request.OriginalCourseId == request.SubstituteCourseId)
|
||||
return ValidationProblem("被替代课程和替代课程不能相同。");
|
||||
var collegeId = scope.Current.RestrictedCollegeId;
|
||||
if (!await ScopedCourses(collegeId).AnyAsync(x => x.Id == request.OriginalCourseId, token) ||
|
||||
!await db.Courses.AnyAsync(x => x.Id == request.SubstituteCourseId, token))
|
||||
return Forbid();
|
||||
if (request.MajorId.HasValue && !await db.Majors.AnyAsync(x => x.Id == request.MajorId && (!collegeId.HasValue || x.CollegeId == collegeId), token))
|
||||
return ValidationProblem("适用专业不在当前维护范围内。");
|
||||
var rule = new CourseEquivalencyRule { OriginalCourseId = request.OriginalCourseId, SubstituteCourseId = request.SubstituteCourseId,
|
||||
MajorId = request.MajorId, EffectiveGrade = request.EffectiveGrade, IsEnabled = request.IsEnabled, Notes = request.Notes?.Trim() };
|
||||
db.CourseEquivalencyRules.Add(rule);
|
||||
try { await db.SaveChangesAsync(token); }
|
||||
catch (DbUpdateException) { return Conflict(new ProblemDetails { Title = "相同适用范围的等价课程规则已存在。", Status = StatusCodes.Status409Conflict }); }
|
||||
return Created($"api/course-equivalency-rules/{rule.Id}", new { rule.Id });
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<ActionResult> Update(Guid id, CourseEquivalencyRuleRequest request, CancellationToken token)
|
||||
{
|
||||
var rule = await Scoped(scope.Current.RestrictedCollegeId).FirstOrDefaultAsync(x => x.Id == id, token);
|
||||
if (rule is null) return NotFound();
|
||||
if (request.OriginalCourseId != rule.OriginalCourseId || request.SubstituteCourseId != rule.SubstituteCourseId)
|
||||
return ValidationProblem("为保证申请留痕,已建立的规则不能改写课程关系;请停用后新建规则。");
|
||||
var collegeId = scope.Current.RestrictedCollegeId;
|
||||
if (request.MajorId.HasValue && !await db.Majors.AnyAsync(x => x.Id == request.MajorId && (!collegeId.HasValue || x.CollegeId == collegeId), token))
|
||||
return ValidationProblem("适用专业不在当前维护范围内。");
|
||||
rule.MajorId = request.MajorId; rule.EffectiveGrade = request.EffectiveGrade; rule.IsEnabled = request.IsEnabled; rule.Notes = request.Notes?.Trim();
|
||||
await db.SaveChangesAsync(token);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private IQueryable<CourseEquivalencyRule> Scoped(Guid? collegeId) => collegeId.HasValue
|
||||
? db.CourseEquivalencyRules.Where(x => x.OriginalCourse!.CollegeId == collegeId.Value)
|
||||
: db.CourseEquivalencyRules;
|
||||
private IQueryable<Course> ScopedCourses(Guid? collegeId) => collegeId.HasValue
|
||||
? db.Courses.Where(x => x.CollegeId == collegeId.Value) : db.Courses;
|
||||
}
|
||||
|
||||
public sealed record CourseEquivalencyRuleRequest(Guid OriginalCourseId, Guid SubstituteCourseId, Guid? MajorId, int? EffectiveGrade, bool IsEnabled, [MaxLength(500)] string? Notes);
|
||||
Loaded 3 of 18 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user