优化毕业审核、学位审核
This commit is contained in:
1 parent
71ee21b063
commit
5dc1ae4e3a
19 files changed
+9645
-57
No files matched your search
Submodule Academic-Affairs-System.wiki updated: 4dafcd42a2...36984e720f.
@@ -4,6 +4,10 @@ using Jiaowu.Api.Domain.Academic;
|
|||||||
using Jiaowu.Api.Domain.Identity;
|
using Jiaowu.Api.Domain.Identity;
|
||||||
using Jiaowu.Api.Infrastructure.Auth;
|
using Jiaowu.Api.Infrastructure.Auth;
|
||||||
using Jiaowu.Api.Infrastructure.Graduation;
|
using Jiaowu.Api.Infrastructure.Graduation;
|
||||||
|
using Jiaowu.Api.Infrastructure.Excel;
|
||||||
|
using Jiaowu.Api.Infrastructure.Files;
|
||||||
|
using Jiaowu.Api.Infrastructure.BackgroundJobs;
|
||||||
|
using Jiaowu.Api.Domain.System;
|
||||||
using Jiaowu.Api.Infrastructure.Persistence;
|
using Jiaowu.Api.Infrastructure.Persistence;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -35,11 +39,14 @@ public sealed class DegreeAwardsController(
|
|||||||
x.Id,
|
x.Id,
|
||||||
x.Name,
|
x.Name,
|
||||||
x.GraduationYear,
|
x.GraduationYear,
|
||||||
|
x.EnrollmentYear,
|
||||||
x.DegreeName,
|
x.DegreeName,
|
||||||
x.MinimumGradePoint,
|
x.MinimumGradePoint,
|
||||||
x.Status,
|
x.Status,
|
||||||
x.Notes,
|
x.Notes,
|
||||||
x.CalculatedAt,
|
x.CalculatedAt,
|
||||||
|
x.CalculationStatus,
|
||||||
|
x.CalculationError,
|
||||||
x.PublishedAt,
|
x.PublishedAt,
|
||||||
ResultCount = x.Results.Count(result =>
|
ResultCount = x.Results.Count(result =>
|
||||||
!collegeId.HasValue ||
|
!collegeId.HasValue ||
|
||||||
@@ -67,6 +74,8 @@ public sealed class DegreeAwardsController(
|
|||||||
[FromQuery] int pageSize = 20,
|
[FromQuery] int pageSize = 20,
|
||||||
[FromQuery] string? keyword = null,
|
[FromQuery] string? keyword = null,
|
||||||
[FromQuery] DegreeAwardConclusion? conclusion = null,
|
[FromQuery] DegreeAwardConclusion? conclusion = null,
|
||||||
|
[FromQuery] Guid? majorId = null,
|
||||||
|
[FromQuery] Guid? administrativeClassId = null,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
pageSize = Math.Clamp(pageSize, 10, 100);
|
pageSize = Math.Clamp(pageSize, 10, 100);
|
||||||
@@ -81,6 +90,10 @@ public sealed class DegreeAwardsController(
|
|||||||
if (collegeId.HasValue)
|
if (collegeId.HasValue)
|
||||||
source = source.Where(x =>
|
source = source.Where(x =>
|
||||||
x.Student!.AdministrativeClass!.Major!.CollegeId == collegeId);
|
x.Student!.AdministrativeClass!.Major!.CollegeId == collegeId);
|
||||||
|
if (majorId.HasValue)
|
||||||
|
source = source.Where(x => x.Student!.AdministrativeClass!.MajorId == majorId);
|
||||||
|
if (administrativeClassId.HasValue)
|
||||||
|
source = source.Where(x => x.Student!.AdministrativeClassId == administrativeClassId);
|
||||||
|
|
||||||
var summary = await source
|
var summary = await source
|
||||||
.GroupBy(_ => 1)
|
.GroupBy(_ => 1)
|
||||||
@@ -139,11 +152,14 @@ public sealed class DegreeAwardsController(
|
|||||||
batch.Id,
|
batch.Id,
|
||||||
batch.Name,
|
batch.Name,
|
||||||
batch.GraduationYear,
|
batch.GraduationYear,
|
||||||
|
batch.EnrollmentYear,
|
||||||
batch.DegreeName,
|
batch.DegreeName,
|
||||||
batch.MinimumGradePoint,
|
batch.MinimumGradePoint,
|
||||||
batch.Status,
|
batch.Status,
|
||||||
batch.Notes,
|
batch.Notes,
|
||||||
batch.CalculatedAt,
|
batch.CalculatedAt,
|
||||||
|
batch.CalculationStatus,
|
||||||
|
batch.CalculationError,
|
||||||
batch.PublishedAt,
|
batch.PublishedAt,
|
||||||
ResultCount = summary?.ResultCount ?? 0,
|
ResultCount = summary?.ResultCount ?? 0,
|
||||||
GrantedCount = summary?.GrantedCount ?? 0,
|
GrantedCount = summary?.GrantedCount ?? 0,
|
||||||
@@ -173,6 +189,7 @@ public sealed class DegreeAwardsController(
|
|||||||
{
|
{
|
||||||
Name = request.Name.Trim(),
|
Name = request.Name.Trim(),
|
||||||
GraduationYear = request.GraduationYear,
|
GraduationYear = request.GraduationYear,
|
||||||
|
EnrollmentYear = request.EnrollmentYear,
|
||||||
DegreeName = request.DegreeName.Trim(),
|
DegreeName = request.DegreeName.Trim(),
|
||||||
MinimumGradePoint = request.MinimumGradePoint,
|
MinimumGradePoint = request.MinimumGradePoint,
|
||||||
Notes = request.Notes?.Trim()
|
Notes = request.Notes?.Trim()
|
||||||
@@ -184,7 +201,33 @@ public sealed class DegreeAwardsController(
|
|||||||
|
|
||||||
[HttpPost("batches/{id:guid}/calculate")]
|
[HttpPost("batches/{id:guid}/calculate")]
|
||||||
[Authorize(Roles = Managers)]
|
[Authorize(Roles = Managers)]
|
||||||
public async Task<ActionResult> Calculate(Guid id, CancellationToken token)
|
public async Task<ActionResult> QueueCalculation(Guid id, CancellationToken token)
|
||||||
|
{
|
||||||
|
var batch = await db.DegreeAwardBatches.FirstOrDefaultAsync(x => x.Id == id, token);
|
||||||
|
if (batch is null) return NotFound();
|
||||||
|
if (batch.Status != DegreeAwardBatchStatus.Draft) return ConflictProblem("已发布批次不能重新计算。");
|
||||||
|
if (batch.CalculationStatus is AuditCalculationStatus.Queued or AuditCalculationStatus.Running)
|
||||||
|
return ConflictProblem("该批次正在后台计算,请稍候。");
|
||||||
|
batch.CalculationStatus = AuditCalculationStatus.Queued;
|
||||||
|
batch.CalculationError = null;
|
||||||
|
var outbox = await db.BackgroundJobOutboxMessages.FirstOrDefaultAsync(x =>
|
||||||
|
x.JobKind == BackgroundJobKind.DegreeAwardCalculation && x.JobId == id, token);
|
||||||
|
if (outbox is null)
|
||||||
|
db.BackgroundJobOutboxMessages.Add(BackgroundJobOutboxMessage.Create(BackgroundJobKind.DegreeAwardCalculation, id));
|
||||||
|
else
|
||||||
|
{
|
||||||
|
outbox.State = BackgroundJobOutboxState.Pending;
|
||||||
|
outbox.CompletedAt = null;
|
||||||
|
outbox.ProcessingToken = null;
|
||||||
|
outbox.LeaseExpiresAt = null;
|
||||||
|
outbox.LastError = null;
|
||||||
|
}
|
||||||
|
await db.SaveChangesAsync(token);
|
||||||
|
return Accepted(new { batch.Id, batch.CalculationStatus });
|
||||||
|
}
|
||||||
|
|
||||||
|
[NonAction]
|
||||||
|
public async Task<ActionResult> CalculateNow(Guid id, CancellationToken token)
|
||||||
{
|
{
|
||||||
var batch = await db.DegreeAwardBatches
|
var batch = await db.DegreeAwardBatches
|
||||||
.FirstOrDefaultAsync(x => x.Id == id, token);
|
.FirstOrDefaultAsync(x => x.Id == id, token);
|
||||||
@@ -192,31 +235,31 @@ public sealed class DegreeAwardsController(
|
|||||||
if (batch.Status != DegreeAwardBatchStatus.Draft)
|
if (batch.Status != DegreeAwardBatchStatus.Draft)
|
||||||
return ConflictProblem("已发布批次不能重新计算。");
|
return ConflictProblem("已发布批次不能重新计算。");
|
||||||
|
|
||||||
var auditCandidates = await db.GraduationAuditResults.AsNoTracking()
|
var students = await db.Students.AsNoTracking()
|
||||||
.Where(x =>
|
.Where(x => x.EnrollmentYear == batch.EnrollmentYear &&
|
||||||
x.GraduationAuditBatch!.GraduationYear == batch.GraduationYear &&
|
(x.Status == StudentStatus.Active || x.Status == StudentStatus.Suspended ||
|
||||||
x.GraduationAuditBatch.Status == GraduationAuditBatchStatus.Published &&
|
x.Status == StudentStatus.Graduated))
|
||||||
x.Conclusion == GraduationAuditConclusion.Eligible)
|
.OrderBy(x => x.StudentNumber)
|
||||||
.OrderByDescending(x => x.GraduationAuditBatch!.PublishedAt)
|
|
||||||
.Select(x => new
|
.Select(x => new
|
||||||
{
|
{
|
||||||
x.Id,
|
x.Id,
|
||||||
x.StudentId,
|
x.Status
|
||||||
StudentStatus = x.Student!.Status
|
|
||||||
})
|
})
|
||||||
.ToListAsync(token);
|
.ToListAsync(token);
|
||||||
var audits = auditCandidates
|
var auditCandidates = await db.GraduationAuditResults.AsNoTracking()
|
||||||
|
.Where(x => x.GraduationAuditBatch!.GraduationYear == batch.GraduationYear &&
|
||||||
|
x.GraduationAuditBatch.EnrollmentYear == batch.EnrollmentYear)
|
||||||
|
.OrderByDescending(x => x.GraduationAuditBatch!.CalculatedAt)
|
||||||
|
.Select(x => new { x.Id, x.StudentId, x.Conclusion, x.MissingCourseNames })
|
||||||
|
.ToListAsync(token);
|
||||||
|
var auditsByStudent = auditCandidates
|
||||||
.GroupBy(x => x.StudentId)
|
.GroupBy(x => x.StudentId)
|
||||||
.Select(x => x.First())
|
.Select(x => x.First())
|
||||||
.ToList();
|
.ToDictionary(x => x.StudentId);
|
||||||
var gradePoints = await db.GradeRecords.AsNoTracking()
|
var gradePoints = await db.GradeRecords.AsNoTracking()
|
||||||
.Where(x => x.GradeSheet!.Status == GradeSheetStatus.Published &&
|
.Where(x => x.GradeSheet!.Status == GradeSheetStatus.Published &&
|
||||||
x.GradePoint.HasValue)
|
x.GradePoint.HasValue)
|
||||||
.Where(x => db.GraduationAuditResults.Any(audit =>
|
.Where(x => x.Student!.EnrollmentYear == batch.EnrollmentYear)
|
||||||
audit.StudentId == x.StudentId &&
|
|
||||||
audit.GraduationAuditBatch!.GraduationYear == batch.GraduationYear &&
|
|
||||||
audit.GraduationAuditBatch.Status == GraduationAuditBatchStatus.Published &&
|
|
||||||
audit.Conclusion == GraduationAuditConclusion.Eligible))
|
|
||||||
.Select(x => new
|
.Select(x => new
|
||||||
{
|
{
|
||||||
x.StudentId,
|
x.StudentId,
|
||||||
@@ -232,9 +275,10 @@ public sealed class DegreeAwardsController(
|
|||||||
.Where(x => x.DegreeAwardBatchId == id)
|
.Where(x => x.DegreeAwardBatchId == id)
|
||||||
.ExecuteDeleteAsync(token);
|
.ExecuteDeleteAsync(token);
|
||||||
|
|
||||||
foreach (var audit in audits)
|
foreach (var student in students)
|
||||||
{
|
{
|
||||||
var grades = gradePointsByStudent.GetValueOrDefault(audit.StudentId) ?? [];
|
var audit = auditsByStudent.GetValueOrDefault(student.Id);
|
||||||
|
var grades = gradePointsByStudent.GetValueOrDefault(student.Id) ?? [];
|
||||||
var credits = grades.Sum(x => x.Credits);
|
var credits = grades.Sum(x => x.Credits);
|
||||||
var average = credits == 0
|
var average = credits == 0
|
||||||
? 0
|
? 0
|
||||||
@@ -243,17 +287,20 @@ public sealed class DegreeAwardsController(
|
|||||||
2,
|
2,
|
||||||
MidpointRounding.AwayFromZero);
|
MidpointRounding.AwayFromZero);
|
||||||
var conclusion = DegreeAwardRules.Evaluate(
|
var conclusion = DegreeAwardRules.Evaluate(
|
||||||
true, audit.StudentStatus, average, batch.MinimumGradePoint);
|
audit?.Conclusion == GraduationAuditConclusion.Eligible,
|
||||||
var reason = audit.StudentStatus != StudentStatus.Graduated
|
average, batch.MinimumGradePoint);
|
||||||
? "学籍状态尚未转为毕业"
|
var reason = audit is null
|
||||||
|
? "尚未生成对应的毕业资格审核结果"
|
||||||
|
: audit.Conclusion != GraduationAuditConclusion.Eligible
|
||||||
|
? $"毕业资格审核暂不符合:{audit.MissingCourseNames ?? "请核对毕业审核结果"}"
|
||||||
: average < batch.MinimumGradePoint
|
: average < batch.MinimumGradePoint
|
||||||
? $"平均绩点 {average:0.00},低于批次要求 {batch.MinimumGradePoint:0.00}"
|
? $"平均绩点 {average:0.00},低于批次要求 {batch.MinimumGradePoint:0.00}"
|
||||||
: string.Empty;
|
: string.Empty;
|
||||||
db.DegreeAwardResults.Add(new DegreeAwardResult
|
db.DegreeAwardResults.Add(new DegreeAwardResult
|
||||||
{
|
{
|
||||||
DegreeAwardBatchId = batch.Id,
|
DegreeAwardBatchId = batch.Id,
|
||||||
StudentId = audit.StudentId,
|
StudentId = student.Id,
|
||||||
GraduationAuditResultId = audit.Id,
|
GraduationAuditResultId = audit?.Id,
|
||||||
AverageGradePoint = average,
|
AverageGradePoint = average,
|
||||||
CalculatedConclusion = conclusion,
|
CalculatedConclusion = conclusion,
|
||||||
Conclusion = conclusion,
|
Conclusion = conclusion,
|
||||||
@@ -262,7 +309,7 @@ public sealed class DegreeAwardsController(
|
|||||||
}
|
}
|
||||||
batch.CalculatedAt = DateTime.UtcNow;
|
batch.CalculatedAt = DateTime.UtcNow;
|
||||||
await db.SaveChangesAsync(token);
|
await db.SaveChangesAsync(token);
|
||||||
return Ok(new { ResultCount = audits.Count });
|
return Ok(new { ResultCount = students.Count });
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("results/{id:guid}")]
|
[HttpPut("results/{id:guid}")]
|
||||||
@@ -311,6 +358,54 @@ public sealed class DegreeAwardsController(
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpDelete("batches/{id:guid}")]
|
||||||
|
[Authorize(Roles = Managers)]
|
||||||
|
public async Task<ActionResult> Delete(Guid id, CancellationToken token)
|
||||||
|
{
|
||||||
|
var batch = await db.DegreeAwardBatches.FirstOrDefaultAsync(x => x.Id == id, token);
|
||||||
|
if (batch is null) return NotFound();
|
||||||
|
if (batch.Status != DegreeAwardBatchStatus.Draft)
|
||||||
|
return ConflictProblem("已发布批次不能删除。");
|
||||||
|
db.DegreeAwardBatches.Remove(batch);
|
||||||
|
await db.SaveChangesAsync(token);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("batches/{id:guid}/export.xlsx")]
|
||||||
|
[Authorize(Roles = Reviewers)]
|
||||||
|
public async Task<ActionResult> Export(
|
||||||
|
Guid id,
|
||||||
|
[FromQuery] string? keyword,
|
||||||
|
[FromQuery] DegreeAwardConclusion? conclusion,
|
||||||
|
[FromQuery] Guid? majorId,
|
||||||
|
[FromQuery] Guid? administrativeClassId,
|
||||||
|
CancellationToken token)
|
||||||
|
{
|
||||||
|
var batch = await db.DegreeAwardBatches.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, token);
|
||||||
|
if (batch is null) return NotFound();
|
||||||
|
var results = db.DegreeAwardResults.AsNoTracking().Where(x => x.DegreeAwardBatchId == id);
|
||||||
|
var collegeId = RestrictedCollegeId();
|
||||||
|
if (collegeId.HasValue) results = results.Where(x => x.Student!.AdministrativeClass!.Major!.CollegeId == collegeId);
|
||||||
|
if (majorId.HasValue) results = results.Where(x => x.Student!.AdministrativeClass!.MajorId == majorId);
|
||||||
|
if (administrativeClassId.HasValue) results = results.Where(x => x.Student!.AdministrativeClassId == administrativeClassId);
|
||||||
|
var normalizedKeyword = keyword?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedKeyword)) results = results.Where(x => x.Student!.StudentNumber.Contains(normalizedKeyword) || x.Student.Name.Contains(normalizedKeyword));
|
||||||
|
if (conclusion.HasValue) results = results.Where(x => x.Conclusion == conclusion.Value);
|
||||||
|
var rows = await results.OrderBy(x => x.Student!.StudentNumber).Select(x => new
|
||||||
|
{
|
||||||
|
x.Student!.StudentNumber, x.Student.Name,
|
||||||
|
College = x.Student.AdministrativeClass!.Major!.College!.Name,
|
||||||
|
Major = x.Student.AdministrativeClass.Major!.Name, Class = x.Student.AdministrativeClass.Name,
|
||||||
|
x.AverageGradePoint, x.ExceptionReason, x.Conclusion, x.IsOverridden, x.ReviewComment
|
||||||
|
}).ToListAsync(token);
|
||||||
|
var bytes = ExcelWorkbookHelper.Create("学位授予审核",
|
||||||
|
["学号", "姓名", "学院", "专业", "班级", "平均绩点", "结论", "规则说明", "人工复核", "复核意见"],
|
||||||
|
rows.Select(x => (IReadOnlyList<object?>)[x.StudentNumber, x.Name, x.College, x.Major, x.Class,
|
||||||
|
x.AverageGradePoint, x.Conclusion == DegreeAwardConclusion.Granted ? "建议授予" : "暂不授予",
|
||||||
|
x.ExceptionReason, x.IsOverridden ? "是" : "否", x.ReviewComment]).ToList());
|
||||||
|
return File(bytes, ExcelWorkbookHelper.ContentType, DownloadFileName.Export("学位授予审核", ".xlsx", batch.GraduationYear.ToString(), batch.EnrollmentYear.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("my-result")]
|
[HttpGet("my-result")]
|
||||||
[Authorize(Roles = SystemRoles.Student)]
|
[Authorize(Roles = SystemRoles.Student)]
|
||||||
public async Task<ActionResult> GetMyResult(CancellationToken token)
|
public async Task<ActionResult> GetMyResult(CancellationToken token)
|
||||||
@@ -360,6 +455,7 @@ public sealed class DegreeAwardsController(
|
|||||||
public sealed record DegreeAwardBatchRequest(
|
public sealed record DegreeAwardBatchRequest(
|
||||||
[Required, MinLength(3), MaxLength(120)] string Name,
|
[Required, MinLength(3), MaxLength(120)] string Name,
|
||||||
[Range(2000, 2200)] int GraduationYear,
|
[Range(2000, 2200)] int GraduationYear,
|
||||||
|
[Range(2000, 2200)] int EnrollmentYear,
|
||||||
[Required, MinLength(2), MaxLength(80)] string DegreeName,
|
[Required, MinLength(2), MaxLength(80)] string DegreeName,
|
||||||
[Range(typeof(decimal), "0", "5")] decimal MinimumGradePoint,
|
[Range(typeof(decimal), "0", "5")] decimal MinimumGradePoint,
|
||||||
[MaxLength(500)] string? Notes);
|
[MaxLength(500)] string? Notes);
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Jiaowu.Api.Contracts;
|
||||||
using Jiaowu.Api.Domain.Academic;
|
using Jiaowu.Api.Domain.Academic;
|
||||||
using Jiaowu.Api.Domain.Identity;
|
using Jiaowu.Api.Domain.Identity;
|
||||||
using Jiaowu.Api.Infrastructure.Auth;
|
using Jiaowu.Api.Infrastructure.Auth;
|
||||||
using Jiaowu.Api.Infrastructure.Graduation;
|
using Jiaowu.Api.Infrastructure.Graduation;
|
||||||
|
using Jiaowu.Api.Infrastructure.Excel;
|
||||||
|
using Jiaowu.Api.Infrastructure.Files;
|
||||||
|
using Jiaowu.Api.Infrastructure.BackgroundJobs;
|
||||||
|
using Jiaowu.Api.Domain.System;
|
||||||
using Jiaowu.Api.Infrastructure.Persistence;
|
using Jiaowu.Api.Infrastructure.Persistence;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -39,6 +44,8 @@ public sealed class GraduationAuditsController(
|
|||||||
x.Status,
|
x.Status,
|
||||||
x.Notes,
|
x.Notes,
|
||||||
x.CalculatedAt,
|
x.CalculatedAt,
|
||||||
|
x.CalculationStatus,
|
||||||
|
x.CalculationError,
|
||||||
x.PublishedAt,
|
x.PublishedAt,
|
||||||
x.CreatedAt,
|
x.CreatedAt,
|
||||||
ResultCount = x.Results.Count(result =>
|
ResultCount = x.Results.Count(result =>
|
||||||
@@ -67,6 +74,8 @@ public sealed class GraduationAuditsController(
|
|||||||
[FromQuery] int pageSize = 20,
|
[FromQuery] int pageSize = 20,
|
||||||
[FromQuery] string? keyword = null,
|
[FromQuery] string? keyword = null,
|
||||||
[FromQuery] GraduationAuditConclusion? conclusion = null,
|
[FromQuery] GraduationAuditConclusion? conclusion = null,
|
||||||
|
[FromQuery] Guid? majorId = null,
|
||||||
|
[FromQuery] Guid? administrativeClassId = null,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
page = Math.Max(page, 1);
|
page = Math.Max(page, 1);
|
||||||
@@ -81,6 +90,10 @@ public sealed class GraduationAuditsController(
|
|||||||
if (collegeId.HasValue)
|
if (collegeId.HasValue)
|
||||||
results = results.Where(x =>
|
results = results.Where(x =>
|
||||||
x.Student!.AdministrativeClass!.Major!.CollegeId == collegeId);
|
x.Student!.AdministrativeClass!.Major!.CollegeId == collegeId);
|
||||||
|
if (majorId.HasValue)
|
||||||
|
results = results.Where(x => x.Student!.AdministrativeClass!.MajorId == majorId);
|
||||||
|
if (administrativeClassId.HasValue)
|
||||||
|
results = results.Where(x => x.Student!.AdministrativeClassId == administrativeClassId);
|
||||||
|
|
||||||
var summary = await results
|
var summary = await results
|
||||||
.GroupBy(_ => 1)
|
.GroupBy(_ => 1)
|
||||||
@@ -143,6 +156,8 @@ public sealed class GraduationAuditsController(
|
|||||||
batch.Status,
|
batch.Status,
|
||||||
batch.Notes,
|
batch.Notes,
|
||||||
batch.CalculatedAt,
|
batch.CalculatedAt,
|
||||||
|
batch.CalculationStatus,
|
||||||
|
batch.CalculationError,
|
||||||
batch.PublishedAt,
|
batch.PublishedAt,
|
||||||
ResultCount = summary?.ResultCount ?? 0,
|
ResultCount = summary?.ResultCount ?? 0,
|
||||||
EligibleCount = summary?.EligibleCount ?? 0,
|
EligibleCount = summary?.EligibleCount ?? 0,
|
||||||
@@ -180,7 +195,33 @@ public sealed class GraduationAuditsController(
|
|||||||
|
|
||||||
[HttpPost("batches/{id:guid}/calculate")]
|
[HttpPost("batches/{id:guid}/calculate")]
|
||||||
[Authorize(Roles = Managers)]
|
[Authorize(Roles = Managers)]
|
||||||
public async Task<ActionResult> Calculate(Guid id, CancellationToken token)
|
public async Task<ActionResult> QueueCalculation(Guid id, CancellationToken token)
|
||||||
|
{
|
||||||
|
var batch = await db.GraduationAuditBatches.FirstOrDefaultAsync(x => x.Id == id, token);
|
||||||
|
if (batch is null) return NotFound();
|
||||||
|
if (batch.Status != GraduationAuditBatchStatus.Draft) return ConflictProblem("已发布批次不能重新计算。");
|
||||||
|
if (batch.CalculationStatus is AuditCalculationStatus.Queued or AuditCalculationStatus.Running)
|
||||||
|
return ConflictProblem("该批次正在后台计算,请稍候。");
|
||||||
|
batch.CalculationStatus = AuditCalculationStatus.Queued;
|
||||||
|
batch.CalculationError = null;
|
||||||
|
var outbox = await db.BackgroundJobOutboxMessages.FirstOrDefaultAsync(x =>
|
||||||
|
x.JobKind == BackgroundJobKind.GraduationAuditCalculation && x.JobId == id, token);
|
||||||
|
if (outbox is null)
|
||||||
|
db.BackgroundJobOutboxMessages.Add(BackgroundJobOutboxMessage.Create(BackgroundJobKind.GraduationAuditCalculation, id));
|
||||||
|
else
|
||||||
|
{
|
||||||
|
outbox.State = BackgroundJobOutboxState.Pending;
|
||||||
|
outbox.CompletedAt = null;
|
||||||
|
outbox.ProcessingToken = null;
|
||||||
|
outbox.LeaseExpiresAt = null;
|
||||||
|
outbox.LastError = null;
|
||||||
|
}
|
||||||
|
await db.SaveChangesAsync(token);
|
||||||
|
return Accepted(new { batch.Id, batch.CalculationStatus });
|
||||||
|
}
|
||||||
|
|
||||||
|
[NonAction]
|
||||||
|
public async Task<ActionResult> CalculateNow(Guid id, CancellationToken token)
|
||||||
{
|
{
|
||||||
var batch = await db.GraduationAuditBatches
|
var batch = await db.GraduationAuditBatches
|
||||||
.FirstOrDefaultAsync(x => x.Id == id, token);
|
.FirstOrDefaultAsync(x => x.Id == id, token);
|
||||||
@@ -348,6 +389,60 @@ public sealed class GraduationAuditsController(
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpDelete("batches/{id:guid}")]
|
||||||
|
[Authorize(Roles = Managers)]
|
||||||
|
public async Task<ActionResult> Delete(Guid id, CancellationToken token)
|
||||||
|
{
|
||||||
|
var batch = await db.GraduationAuditBatches.FirstOrDefaultAsync(x => x.Id == id, token);
|
||||||
|
if (batch is null) return NotFound();
|
||||||
|
if (batch.Status != GraduationAuditBatchStatus.Draft)
|
||||||
|
return ConflictProblem("已发布批次不能删除。");
|
||||||
|
db.GraduationAuditBatches.Remove(batch);
|
||||||
|
await db.SaveChangesAsync(token);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("batches/{id:guid}/export.xlsx")]
|
||||||
|
[Authorize(Roles = Reviewers)]
|
||||||
|
public async Task<ActionResult> Export(
|
||||||
|
Guid id,
|
||||||
|
[FromQuery] string? keyword,
|
||||||
|
[FromQuery] GraduationAuditConclusion? conclusion,
|
||||||
|
[FromQuery] Guid? majorId,
|
||||||
|
[FromQuery] Guid? administrativeClassId,
|
||||||
|
CancellationToken token)
|
||||||
|
{
|
||||||
|
var batch = await db.GraduationAuditBatches.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id, token);
|
||||||
|
if (batch is null) return NotFound();
|
||||||
|
var results = db.GraduationAuditResults.AsNoTracking()
|
||||||
|
.Where(x => x.GraduationAuditBatchId == id);
|
||||||
|
var collegeId = RestrictedCollegeId();
|
||||||
|
if (collegeId.HasValue)
|
||||||
|
results = results.Where(x => x.Student!.AdministrativeClass!.Major!.CollegeId == collegeId);
|
||||||
|
if (majorId.HasValue) results = results.Where(x => x.Student!.AdministrativeClass!.MajorId == majorId);
|
||||||
|
if (administrativeClassId.HasValue) results = results.Where(x => x.Student!.AdministrativeClassId == administrativeClassId);
|
||||||
|
var normalizedKeyword = keyword?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedKeyword))
|
||||||
|
results = results.Where(x => x.Student!.StudentNumber.Contains(normalizedKeyword) || x.Student.Name.Contains(normalizedKeyword));
|
||||||
|
if (conclusion.HasValue) results = results.Where(x => x.Conclusion == conclusion.Value);
|
||||||
|
var rows = await results.OrderBy(x => x.Student!.StudentNumber).Select(x => new
|
||||||
|
{
|
||||||
|
x.Student!.StudentNumber, x.Student.Name,
|
||||||
|
College = x.Student.AdministrativeClass!.Major!.College!.Name,
|
||||||
|
Major = x.Student.AdministrativeClass.Major!.Name, Class = x.Student.AdministrativeClass.Name,
|
||||||
|
x.RequiredCredits, x.EarnedCredits, x.RequiredCourseCount, x.PassedRequiredCourseCount,
|
||||||
|
x.FailedCourseCount, x.MissingCourseNames, x.Conclusion, x.IsOverridden, x.ReviewComment
|
||||||
|
}).ToListAsync(token);
|
||||||
|
var bytes = ExcelWorkbookHelper.Create("毕业资格审核",
|
||||||
|
["学号", "姓名", "学院", "专业", "班级", "应修学分", "已获学分", "必修完成", "未解决不及格", "未完成课程", "结论", "人工复核", "复核意见"],
|
||||||
|
rows.Select(x => (IReadOnlyList<object?>)[x.StudentNumber, x.Name, x.College, x.Major, x.Class,
|
||||||
|
x.RequiredCredits, x.EarnedCredits, $"{x.PassedRequiredCourseCount}/{x.RequiredCourseCount}", x.FailedCourseCount,
|
||||||
|
x.MissingCourseNames, x.Conclusion == GraduationAuditConclusion.Eligible ? "符合毕业条件" : "暂不符合",
|
||||||
|
x.IsOverridden ? "是" : "否", x.ReviewComment]).ToList());
|
||||||
|
return File(bytes, ExcelWorkbookHelper.ContentType, DownloadFileName.Export("毕业资格审核", ".xlsx", batch.GraduationYear.ToString(), batch.EnrollmentYear.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("my-result")]
|
[HttpGet("my-result")]
|
||||||
[Authorize(Roles = SystemRoles.Student)]
|
[Authorize(Roles = SystemRoles.Student)]
|
||||||
public async Task<ActionResult> GetMyResult(CancellationToken token)
|
public async Task<ActionResult> GetMyResult(CancellationToken token)
|
||||||
|
|||||||
Loaded 3 of 19 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user