优化学业预警检测:

通知阶段移除逐条查询学生、规则、辅导员的 N+1 数据库访问。
不及格预警从“每个学生一次统计 + 一次查重”改为批量分组统计与一次性已有记录集合查询。
保持原有阈值、去重、通知内容与权限行为。
This commit is contained in:
2026-08-11 12:06:04 +08:00 Unverified
parent bf01dba72b
commit 08396a6e23
@@ -101,17 +101,23 @@ public sealed class WarningsController(AppDbContext db, ICurrentUserDataScope sc
db.WarningRecords.AddRange(generated);
await db.SaveChangesAsync(ct);
var studentsById = studentInfos.ToDictionary(student => student.Id);
var rulesByType = rules.ToDictionary(rule => rule.Type);
var classIds = studentInfos.Select(student => student.ClassId).Distinct().ToArray();
var counselorsByClass = await db.AdministrativeClasses.AsNoTracking()
.WhereIn(classIds, item => item.Id)
.Where(item => item.CounselorUserId != null)
.Select(item => new { item.Id, CounselorUserId = item.CounselorUserId!.Value })
.ToDictionaryAsync(item => item.Id, item => item.CounselorUserId, ct);
foreach (var w in generated)
{
var si = studentInfos.First(s => s.Id == w.StudentId);
var r = rules.First(r => r.Type == w.Type);
var si = studentsById[w.StudentId];
var r = rulesByType[w.Type];
if (r.NotifyStudent && si.UserId.HasValue)
await NotificationService.SendAsync(db, si.UserId.Value, "学业预警", w.Detail, "/warnings", ct, NotificationCategory.Warning);
if (r.NotifyCounselor)
if (r.NotifyCounselor && counselorsByClass.TryGetValue(si.ClassId, out var counselorId))
{
var counselorId = await db.AdministrativeClasses.Where(c => c.Id == si.ClassId && c.CounselorUserId != null).Select(c => c.CounselorUserId!.Value).FirstOrDefaultAsync(ct);
if (counselorId != default)
await NotificationService.SendAsync(db, counselorId, "学生学业预警", $"{si.Name}{w.Detail}", "/warnings", ct, NotificationCategory.Warning);
await NotificationService.SendAsync(db, counselorId, "学生学业预警", $"{si.Name}{w.Detail}", "/warnings", ct, NotificationCategory.Warning);
}
}
}
@@ -197,17 +203,31 @@ public sealed class WarningsController(AppDbContext db, ICurrentUserDataScope sc
// ═══════════ Detection logic ═══════════
private async Task<List<WarningRecord>> DetectFailedCredits(WarningRule rule, Guid termId, List<StudentInfo> students, CancellationToken ct)
{
var records = new List<WarningRecord>();
foreach (var s in students)
{
var failed = await db.GradeRecords.CountAsync(x => x.StudentId == s.Id && x.GradeSheet!.TeachingTask!.AcademicTermId == termId && x.GradeSheet.Status == GradeSheetStatus.Published && x.TotalScore < 60, ct);
if (failed >= rule.Threshold)
var studentIds = students.Select(student => student.Id).ToArray();
var existingStudentIds = await db.WarningRecords.AsNoTracking()
.Where(record => record.AcademicTermId == termId && record.Type == rule.Type)
.Select(record => record.StudentId)
.ToHashSetAsync(ct);
var failedByStudent = await db.GradeRecords.AsNoTracking()
.Where(record => studentIds.Contains(record.StudentId) &&
record.GradeSheet!.TeachingTask!.AcademicTermId == termId &&
record.GradeSheet.Status == GradeSheetStatus.Published &&
record.TotalScore < 60)
.GroupBy(record => record.StudentId)
.Select(group => new { StudentId = group.Key, Failed = group.Count() })
.ToListAsync(ct);
return failedByStudent
.Where(item => item.Failed >= rule.Threshold &&
!existingStudentIds.Contains(item.StudentId))
.Select(item => new WarningRecord
{
if (await db.WarningRecords.AnyAsync(x => x.StudentId == s.Id && x.AcademicTermId == termId && x.Type == WarningType.FailedCredits, ct)) continue;
records.Add(new WarningRecord { StudentId = s.Id, Type = rule.Type, TriggerValue = failed, Detail = $"不及格课程 {failed} 门,达到预警阈值 {rule.Threshold} 门。", AcademicTermId = termId });
}
}
return records;
StudentId = item.StudentId,
Type = rule.Type,
TriggerValue = item.Failed,
Detail = $"不及格课程 {item.Failed} 门,达到预警阈值 {rule.Threshold} 门。",
AcademicTermId = termId
})
.ToList();
}
private async Task<List<WarningRecord>> DetectLowGPA(WarningRule rule, Guid termId, List<StudentInfo> students, CancellationToken ct)