Add deep links for pending approval notifications

This commit is contained in:
biss committed 2026-08-17 20:50:56 +08:00
1 parent ce8982dba9
commit 4c95edb678
9 files changed
+191 -34

No files matched your search

@@ -93,6 +93,46 @@ public sealed class ApprovalsController(AppDbContext db, ICurrentUserDataScope s
return Ok(new PagedResult<ApprovalItem>(pageItems, total, page, pageSize));
}
[HttpGet("pending/{type}/{id:guid}")]
[Authorize(Roles = Managers)]
public async Task<ActionResult<ApprovalItem>> GetPendingItem(
string type,
Guid id,
CancellationToken ct)
{
ApprovalItem? item = type switch
{
"CourseExemption" => await Scoped<CourseExemption>(x =>
x.Id == id && x.Status == ApprovalStatus.Submitted)
.Select(x => new ApprovalItem(x.Id, "CourseExemption", "免修",
$"{x.Student!.Name} — 《{x.TeachingTask!.Course!.Name}》", x.Reason,
x.SubmittedAt, x.TeachingTask.Course!.College!.Name))
.FirstOrDefaultAsync(ct),
"DeferredExam" => await Scoped<DeferredExam>(x =>
x.Id == id && x.Status == ApprovalStatus.Submitted)
.Select(x => new ApprovalItem(x.Id, "DeferredExam", "缓考",
$"{x.Student!.Name} — 《{x.TeachingTask!.Course!.Name}》", x.Reason,
x.SubmittedAt, x.TeachingTask.Course!.College!.Name))
.FirstOrDefaultAsync(ct),
"CourseSubstitution" => await Scoped<CourseSubstitution>(x =>
x.Id == id && x.Status == ApprovalStatus.Submitted)
.Select(x => new ApprovalItem(x.Id, "CourseSubstitution", "课程替代",
$"{x.Student!.Name} — 《{x.SubstituteCourse!.Name}》→《{x.OriginalCourse!.Name}》",
x.Reason, x.SubmittedAt, x.Student.AdministrativeClass!.Major!.College!.Name))
.FirstOrDefaultAsync(ct),
"GradeModification" => await Scoped<GradeModification>(x =>
x.Id == id && (x.Status == GradeModificationStatus.TeacherSubmitted ||
x.Status == GradeModificationStatus.CollegeApproved))
.Select(x => new ApprovalItem(x.Id, "GradeModification", "成绩修改",
$"{x.GradeRecord!.Student!.Name} — 《{x.GradeRecord.GradeSheet!.TeachingTask!.Course!.Name}》",
$"{x.CurrentScore} → {x.RequestedScore}:{x.Reason}", x.SubmittedAt,
x.GradeRecord.GradeSheet.TeachingTask.Course!.College!.Name))
.FirstOrDefaultAsync(ct),
_ => null
};
return item is null ? NotFound() : Ok(item);
}
// ═══════════════ Course Exemption ═══════════════
[HttpGet("exemptions/mine")]
[Authorize(Roles = SystemRoles.Student)]
@@ -124,7 +164,7 @@ public sealed class ApprovalsController(AppDbContext db, ICurrentUserDataScope s
var info = await db.CourseExemptions.Where(x => x.Id == ex.Id)
.Select(x => new { StudentName = x.Student!.Name, CourseName = x.TeachingTask!.Course!.Name })
.FirstAsync(ct);
await NotifyManagers("免修申请", $"{info.StudentName} 申请免修《{info.CourseName}》", ct);
await NotifyManagers("CourseExemption", ex.Id, "免修申请", $"{info.StudentName} 申请免修《{info.CourseName}》", ct);
return Created("", new { ex.Id });
}
@@ -187,7 +227,7 @@ public sealed class ApprovalsController(AppDbContext db, ICurrentUserDataScope s
var info = await db.DeferredExams.Where(x => x.Id == d.Id)
.Select(x => new { StudentName = x.Student!.Name, CourseName = x.TeachingTask!.Course!.Name })
.FirstAsync(ct);
await NotifyManagers("缓考申请", $"{info.StudentName} 申请《{info.CourseName}》缓考", ct);
await NotifyManagers("DeferredExam", d.Id, "缓考申请", $"{info.StudentName} 申请《{info.CourseName}》缓考", ct);
return Created("", new { d.Id });
}
@@ -239,6 +279,7 @@ public sealed class ApprovalsController(AppDbContext db, ICurrentUserDataScope s
.FirstAsync(ct);
await NotificationService.SendToRoleAsync(db, SystemRoles.CollegeAdmin, "成绩修改待审核",
$"{gmInfo.StudentName} — 《{gmInfo.CourseName}》{gmInfo.CurrentScore}→{gmInfo.RequestedScore}",
linkUrl: $"/approvals?tab=pending&type=GradeModification&id={gm.Id}",
cancellationToken: ct,
category: NotificationCategory.Grade);
return Created("", new { gm.Id });
@@ -258,7 +299,7 @@ public sealed class ApprovalsController(AppDbContext db, ICurrentUserDataScope s
SystemRoles.AcademicAdmin,
"成绩修改待校级审核",
$"{gm.GradeRecord!.Student!.Name} — 《{gm.GradeRecord.GradeSheet!.TeachingTask!.Course!.Name}》",
linkUrl: "/approvals",
linkUrl: $"/approvals?tab=pending&type=GradeModification&id={gm.Id}",
cancellationToken: ct,
category: NotificationCategory.Grade);
return NoContent();
@@ -406,7 +447,7 @@ public sealed class ApprovalsController(AppDbContext db, ICurrentUserDataScope s
var info = await db.CourseSubstitutions.Where(x => x.Id == cs.Id)
.Select(x => new { StudentName = x.Student!.Name, SubName = x.SubstituteCourse!.Name, OrigName = x.OriginalCourse!.Name })
.FirstAsync(ct);
await NotifyManagers("课程替代申请", $"{info.StudentName} 申请用《{info.SubName}》替代《{info.OrigName}》", ct);
await NotifyManagers("CourseSubstitution", cs.Id, "课程替代申请", $"{info.StudentName} 申请用《{info.SubName}》替代《{info.OrigName}》", ct);
return Created("", new { cs.Id });
}
@@ -588,7 +629,7 @@ public sealed class ApprovalsController(AppDbContext db, ICurrentUserDataScope s
ct);
private ActionResult StudentNotFound() => Conflict(new ProblemDetails { Title = "未关联学生档案", Detail = "当前账号未关联有效学生档案。", Status = 409 });
private async Task NotifyManagers(string title, string content, CancellationToken ct) { await NotificationService.SendToRoleAsync(db, SystemRoles.CollegeAdmin, title, content, cancellationToken: ct, category: NotificationCategory.Approval); await NotificationService.SendToRoleAsync(db, SystemRoles.AcademicAdmin, title, content, cancellationToken: ct, category: NotificationCategory.Approval); }
private async Task NotifyManagers(string type, Guid id, string title, string content, CancellationToken ct) { var linkUrl = $"/approvals?tab=pending&type={type}&id={id}"; await NotificationService.SendToRoleAsync(db, SystemRoles.CollegeAdmin, title, content, linkUrl: linkUrl, cancellationToken: ct, category: NotificationCategory.Approval); await NotificationService.SendToRoleAsync(db, SystemRoles.AcademicAdmin, title, content, linkUrl: linkUrl, cancellationToken: ct, category: NotificationCategory.Approval); }
private async Task NotifyStudent(Guid sid, string title, string content, CancellationToken ct) { var uid = await db.Students.Where(s => s.Id == sid).Select(s => s.UserId).FirstOrDefaultAsync(ct); if (uid.HasValue) await NotificationService.SendAsync(db, uid.Value, title, content, "/approvals", ct, NotificationCategory.Approval); }
private async Task NotifyCollege(GradeModification gm, CancellationToken ct) { await NotificationService.SendToRoleAsync(db, SystemRoles.CollegeAdmin, "成绩修改待审核", $"{gm.GradeRecord!.Student!.Name} — 《{gm.GradeRecord.GradeSheet!.TeachingTask!.Course!.Name}》{gm.CurrentScore}→{gm.RequestedScore}", cancellationToken: ct, category: NotificationCategory.Grade); }
private async Task NotifyRole(string role, string title, string content, CancellationToken ct) { await NotificationService.SendToRoleAsync(db, role, title, content, cancellationToken: ct, category: NotificationCategory.Approval); }