From 08396a6e237497ad130c8eeb60c72933abbd7d5a Mon Sep 17 00:00:00 2001 From: biss Date: Tue, 11 Aug 2026 12:06:04 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AD=A6=E4=B8=9A=E9=A2=84?= =?UTF-8?q?=E8=AD=A6=E6=A3=80=E6=B5=8B=EF=BC=9A=20=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E9=98=B6=E6=AE=B5=E7=A7=BB=E9=99=A4=E9=80=90=E6=9D=A1=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E5=AD=A6=E7=94=9F=E3=80=81=E8=A7=84=E5=88=99=E3=80=81?= =?UTF-8?q?=E8=BE=85=E5=AF=BC=E5=91=98=E7=9A=84=20N+1=20=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E8=AE=BF=E9=97=AE=E3=80=82=20=E4=B8=8D=E5=8F=8A?= =?UTF-8?q?=E6=A0=BC=E9=A2=84=E8=AD=A6=E4=BB=8E=E2=80=9C=E6=AF=8F=E4=B8=AA?= =?UTF-8?q?=E5=AD=A6=E7=94=9F=E4=B8=80=E6=AC=A1=E7=BB=9F=E8=AE=A1=20+=20?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=E6=9F=A5=E9=87=8D=E2=80=9D=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E5=88=86=E7=BB=84=E7=BB=9F=E8=AE=A1=E4=B8=8E?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=E6=80=A7=E5=B7=B2=E6=9C=89=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E9=9B=86=E5=90=88=E6=9F=A5=E8=AF=A2=E3=80=82=20=E4=BF=9D?= =?UTF-8?q?=E6=8C=81=E5=8E=9F=E6=9C=89=E9=98=88=E5=80=BC=E3=80=81=E5=8E=BB?= =?UTF-8?q?=E9=87=8D=E3=80=81=E9=80=9A=E7=9F=A5=E5=86=85=E5=AE=B9=E4=B8=8E?= =?UTF-8?q?=E6=9D=83=E9=99=90=E8=A1=8C=E4=B8=BA=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/WarningsController.cs | 52 +++++++++++++------ 1 file changed, 36 insertions(+), 16 deletions(-) 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)