diff --git a/src/Jiaowu.Api/Controllers/WarningsController.cs b/src/Jiaowu.Api/Controllers/WarningsController.cs index a73e97a..2257c55 100644 --- a/src/Jiaowu.Api/Controllers/WarningsController.cs +++ b/src/Jiaowu.Api/Controllers/WarningsController.cs @@ -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> DetectFailedCredits(WarningRule rule, Guid termId, List students, CancellationToken ct) { - var records = new List(); - 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> DetectLowGPA(WarningRule rule, Guid termId, List students, CancellationToken ct)