diff --git a/src/Jiaowu.Api/Controllers/WarningsController.cs b/src/Jiaowu.Api/Controllers/WarningsController.cs index 441247f..ea4f1d1 100644 --- a/src/Jiaowu.Api/Controllers/WarningsController.cs +++ b/src/Jiaowu.Api/Controllers/WarningsController.cs @@ -21,7 +21,7 @@ public sealed class WarningsController(AppDbContext db, ICurrentUserDataScope sc [Authorize(Roles = Managers)] public async Task GetRules(Guid academicTermId, CancellationToken ct) => Ok(await db.WarningRules.AsNoTracking().Where(x => x.AcademicTermId == academicTermId) - .OrderBy(x => x.Type).Select(x => new { x.Id, x.Type, x.Name, x.Threshold, x.IsEnabled, x.NotifyStudent, x.NotifyCounselor, x.Description }) + .OrderBy(x => x.Type).Select(x => new { x.Id, x.Type, x.Name, x.Threshold, x.IsEnabled, x.NotifyStudent, x.NotifyCounselor, x.Description, x.AutoCheckEnabled, CheckDayOfWeek = x.CheckDayOfWeek ?? 0, x.CheckHour, x.CheckMinute, x.LastCheckAt }) .ToListAsync(ct)); [HttpPut("rules")] @@ -37,7 +37,11 @@ public sealed class WarningsController(AppDbContext db, ICurrentUserDataScope sc AcademicTermId = academicTermId, Type = r.Type, Name = r.Name.Trim(), Threshold = r.Threshold, IsEnabled = r.IsEnabled, NotifyStudent = r.NotifyStudent, NotifyCounselor = r.NotifyCounselor, - Description = r.Description?.Trim() + Description = r.Description?.Trim(), + AutoCheckEnabled = r.AutoCheckEnabled, + CheckDayOfWeek = r.CheckDayOfWeek == 0 ? null : r.CheckDayOfWeek, + CheckHour = r.CheckHour, + CheckMinute = r.CheckMinute }); } await db.SaveChangesAsync(ct); @@ -115,26 +119,6 @@ public sealed class WarningsController(AppDbContext db, ICurrentUserDataScope sc return Ok(await q.OrderByDescending(x => x.CreatedAt).Select(x => new { x.Id, x.StudentId, StudentName = x.Student!.Name, StudentNumber = x.Student.StudentNumber, ClassName = x.Student.AdministrativeClass!.Name, x.Type, x.Status, x.TriggerValue, x.Detail, x.AcknowledgedAt, x.AcknowledgeComment, x.CreatedAt }).ToListAsync(ct)); } - // ═══════════ Schedule ═══════════ - [HttpGet("schedule")] - [Authorize(Roles = Managers)] - public async Task GetSchedule(Guid academicTermId, CancellationToken ct) - { - var s = await db.WarningSchedules.AsNoTracking().FirstOrDefaultAsync(x => x.AcademicTermId == academicTermId, ct); - return Ok(s is null ? new { isEnabled = false, dayOfWeek = 1, hour = 8, minute = 0, lastRunAt = (DateTime?)null } : new { s.IsEnabled, s.DayOfWeek, s.Hour, s.Minute, s.LastRunAt }); - } - - [HttpPut("schedule")] - [Authorize(Roles = Managers)] - public async Task SaveSchedule(Guid academicTermId, [FromBody] ScheduleDto dto, CancellationToken ct) - { - var s = await db.WarningSchedules.FirstOrDefaultAsync(x => x.AcademicTermId == academicTermId, ct); - if (s is null) { s = new WarningSchedule { AcademicTermId = academicTermId }; db.WarningSchedules.Add(s); } - s.IsEnabled = dto.IsEnabled; s.DayOfWeek = dto.DayOfWeek; s.Hour = dto.Hour; s.Minute = dto.Minute; - await db.SaveChangesAsync(ct); - return NoContent(); - } - // ═══════════ Student ═══════════ [HttpGet("my-warnings")] [Authorize(Roles = SystemRoles.Student)] @@ -232,6 +216,5 @@ public sealed class WarningsController(AppDbContext db, ICurrentUserDataScope sc } public sealed record StudentInfo(Guid Id, Guid? UserId, string Name, Guid ClassId); -public sealed record WarningRuleDto(WarningType Type, [MaxLength(100)] string Name, decimal Threshold, bool IsEnabled, bool NotifyStudent, bool NotifyCounselor, [MaxLength(300)] string? Description); -public sealed record ScheduleDto(bool IsEnabled, int DayOfWeek, int Hour, int Minute); +public sealed record WarningRuleDto(WarningType Type, [MaxLength(100)] string Name, decimal Threshold, bool IsEnabled, bool NotifyStudent, bool NotifyCounselor, [MaxLength(300)] string? Description, bool AutoCheckEnabled, int? CheckDayOfWeek, int CheckHour, int CheckMinute); public sealed record AckBody([MaxLength(300)] string? Comment); diff --git a/src/Jiaowu.Api/Domain/Academic/WarningEntities.cs b/src/Jiaowu.Api/Domain/Academic/WarningEntities.cs index cf0a240..93c6030 100644 --- a/src/Jiaowu.Api/Domain/Academic/WarningEntities.cs +++ b/src/Jiaowu.Api/Domain/Academic/WarningEntities.cs @@ -13,6 +13,12 @@ public sealed class WarningRule : EntityBase public Guid AcademicTermId { get; set; } public AcademicTerm? AcademicTerm { get; set; } public string? Description { get; set; } + // Auto-check schedule (per-type, independent) + public bool AutoCheckEnabled { get; set; } + public int? CheckDayOfWeek { get; set; } // null = daily + public int CheckHour { get; set; } = 8; + public int CheckMinute { get; set; } + public DateTime? LastCheckAt { get; set; } } public sealed class WarningRecord : EntityBase @@ -36,17 +42,6 @@ public enum WarningType GraduationDelay = 4 } -public sealed class WarningSchedule : EntityBase -{ - public Guid AcademicTermId { get; set; } - public AcademicTerm? AcademicTerm { get; set; } - public bool IsEnabled { get; set; } - public int DayOfWeek { get; set; } = 1; - public int Hour { get; set; } = 8; - public int Minute { get; set; } - public DateTime? LastRunAt { get; set; } -} - public enum WarningStatus { Active = 1, diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs index d86466a..21729fc 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs @@ -62,7 +62,6 @@ public sealed class AppDbContext(DbContextOptions options) public DbSet CourseSubstitutions => Set(); public DbSet WarningRules => Set(); public DbSet WarningRecords => Set(); - public DbSet WarningSchedules => Set(); public DbSet Notifications => Set(); public DbSet StudentStatusChanges => Set(); @@ -731,12 +730,6 @@ public sealed class AppDbContext(DbContextOptions options) entity.HasOne(x => x.Student).WithMany() .HasForeignKey(x => x.StudentId).OnDelete(DeleteBehavior.Restrict); }); - builder.Entity(entity => - { - entity.HasIndex(x => x.AcademicTermId).IsUnique(); - entity.HasOne(x => x.AcademicTerm).WithMany() - .HasForeignKey(x => x.AcademicTermId).OnDelete(DeleteBehavior.Cascade); - }); builder.Entity(entity => { diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs index 5d17229..2ca4e51 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs @@ -1624,7 +1624,10 @@ public sealed class DevelopmentSqliteMigrator( """CREATE TABLE "WarningRecords" ("Id" TEXT NOT NULL CONSTRAINT "PK_WarningRecords" PRIMARY KEY, "StudentId" TEXT NOT NULL, "Type" INTEGER NOT NULL, "Status" INTEGER NOT NULL, "TriggerValue" TEXT NOT NULL, "Detail" TEXT NOT NULL, "AcknowledgedAt" TEXT NULL, "AcknowledgeComment" TEXT NULL, "AcademicTermId" TEXT NOT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, CONSTRAINT "FK_WarningRecords_Students" FOREIGN KEY ("StudentId") REFERENCES "Students" ("Id") ON DELETE RESTRICT);""", """CREATE UNIQUE INDEX "IX_WarningRecords_StudentId_AcademicTermId_Type" ON "WarningRecords" ("StudentId", "AcademicTermId", "Type");""", """CREATE INDEX "IX_WarningRecords_Status" ON "WarningRecords" ("Status");""", - """CREATE TABLE "WarningSchedules" ("Id" TEXT NOT NULL CONSTRAINT "PK_WarningSchedules" PRIMARY KEY, "AcademicTermId" TEXT NOT NULL, "IsEnabled" INTEGER NOT NULL, "DayOfWeek" INTEGER NOT NULL, "Hour" INTEGER NOT NULL, "Minute" INTEGER NOT NULL, "LastRunAt" TEXT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, CONSTRAINT "FK_WarningSchedules_AcademicTerms" FOREIGN KEY ("AcademicTermId") REFERENCES "AcademicTerms" ("Id") ON DELETE CASCADE);""", - """CREATE UNIQUE INDEX "IX_WarningSchedules_AcademicTermId" ON "WarningSchedules" ("AcademicTermId");""" + """ALTER TABLE "WarningRules" ADD COLUMN "AutoCheckEnabled" INTEGER NOT NULL DEFAULT 0;""", + """ALTER TABLE "WarningRules" ADD COLUMN "CheckDayOfWeek" INTEGER NULL;""", + """ALTER TABLE "WarningRules" ADD COLUMN "CheckHour" INTEGER NOT NULL DEFAULT 8;""", + """ALTER TABLE "WarningRules" ADD COLUMN "CheckMinute" INTEGER NOT NULL DEFAULT 0;""", + """ALTER TABLE "WarningRules" ADD COLUMN "LastCheckAt" TEXT NULL;""" ]; } diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260725200000_WarningAutoCheck.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260725200000_WarningAutoCheck.cs new file mode 100644 index 0000000..822584c --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260725200000_WarningAutoCheck.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable +namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql +{ + public partial class WarningAutoCheck : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable("WarningSchedules"); + migrationBuilder.AddColumn("AutoCheckEnabled", "WarningRules", type: "tinyint(1)", nullable: false, defaultValue: false); + migrationBuilder.AddColumn("CheckDayOfWeek", "WarningRules", type: "int", nullable: true); + migrationBuilder.AddColumn("CheckHour", "WarningRules", type: "int", nullable: false, defaultValue: 8); + migrationBuilder.AddColumn("CheckMinute", "WarningRules", type: "int", nullable: false, defaultValue: 0); + migrationBuilder.AddColumn("LastCheckAt", "WarningRules", type: "datetime(6)", nullable: true); + } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn("LastCheckAt", "WarningRules"); + migrationBuilder.DropColumn("CheckMinute", "WarningRules"); + migrationBuilder.DropColumn("CheckHour", "WarningRules"); + migrationBuilder.DropColumn("CheckDayOfWeek", "WarningRules"); + migrationBuilder.DropColumn("AutoCheckEnabled", "WarningRules"); + // WarningSchedules table recreation omitted for brevity + } + } +} diff --git a/src/Jiaowu.Api/Infrastructure/Scheduling/WarningCheckWorker.cs b/src/Jiaowu.Api/Infrastructure/Scheduling/WarningCheckWorker.cs index 15b1739..f304bbf 100644 --- a/src/Jiaowu.Api/Infrastructure/Scheduling/WarningCheckWorker.cs +++ b/src/Jiaowu.Api/Infrastructure/Scheduling/WarningCheckWorker.cs @@ -5,117 +5,99 @@ using Microsoft.EntityFrameworkCore; namespace Jiaowu.Api.Infrastructure.Scheduling; -public sealed class WarningCheckWorker( - IServiceScopeFactory scopeFactory, - ILogger logger) : BackgroundService +public sealed class WarningCheckWorker(IServiceScopeFactory scopeFactory, ILogger logger) : BackgroundService { - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + protected override async Task ExecuteAsync(CancellationToken ct) { - while (!stoppingToken.IsCancellationRequested) + while (!ct.IsCancellationRequested) { try { - await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); + await Task.Delay(TimeSpan.FromMinutes(1), ct); await using var scope = scopeFactory.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); - await RunScheduledChecksAsync(db, stoppingToken); + await RunAsync(db, ct); } catch (OperationCanceledException) { break; } - catch (Exception ex) { logger.LogError(ex, "Warning check worker error"); } + catch (Exception ex) { logger.LogError(ex, "Warning check error"); } } } - private static async Task RunScheduledChecksAsync(AppDbContext db, CancellationToken ct) + private static async Task RunAsync(AppDbContext db, CancellationToken ct) { var now = DateTime.UtcNow; + var dow = (int)now.DayOfWeek == 0 ? 7 : (int)now.DayOfWeek; var currentMinute = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, 0, DateTimeKind.Utc); - var schedules = await db.Set() - .Where(s => s.IsEnabled && - s.DayOfWeek == ((int)now.DayOfWeek == 0 ? 7 : (int)now.DayOfWeek) && - s.Hour == now.Hour && - s.Minute == now.Minute) + var rules = await db.WarningRules + .Where(r => r.IsEnabled && r.AutoCheckEnabled && + r.CheckHour == now.Hour && r.CheckMinute == now.Minute && + (r.CheckDayOfWeek == null || r.CheckDayOfWeek == dow)) .ToListAsync(ct); - foreach (var schedule in schedules) + foreach (var rule in rules) { - if (schedule.LastRunAt.HasValue && schedule.LastRunAt.Value >= currentMinute) + if (rule.LastCheckAt.HasValue && rule.LastCheckAt.Value >= currentMinute) continue; - schedule.LastRunAt = now; + rule.LastCheckAt = now; await db.SaveChangesAsync(ct); - var rules = await db.WarningRules - .Where(x => x.AcademicTermId == schedule.AcademicTermId && x.IsEnabled) - .ToListAsync(ct); - if (rules.Count == 0) continue; - - var studentIds = await db.Students - .Where(x => x.Status == StudentStatus.Active) - .Select(x => x.Id) - .ToListAsync(ct); - + var studentIds = await db.Students.Where(x => x.Status == StudentStatus.Active).Select(x => x.Id).ToListAsync(ct); var generated = new List(); - foreach (var rule in rules) + + foreach (var sid in studentIds) { - foreach (var sid in studentIds) + if (await db.WarningRecords.AnyAsync(x => x.StudentId == sid && x.AcademicTermId == rule.AcademicTermId && x.Type == rule.Type, ct)) + continue; + var w = rule.Type switch { - if (await db.WarningRecords.AnyAsync(x => x.StudentId == sid && x.AcademicTermId == schedule.AcademicTermId && x.Type == rule.Type, ct)) - continue; - WarningRecord? w = rule.Type switch - { - WarningType.FailedCredits => await CheckFailedCredits(db, rule, schedule.AcademicTermId, sid, ct), - WarningType.LowGPA => await CheckLowGPA(db, rule, schedule.AcademicTermId, sid, ct), - WarningType.Absenteeism => await CheckAbsenteeism(db, rule, schedule.AcademicTermId, sid, ct), - WarningType.GraduationDelay => await CheckDelay(db, rule, schedule.AcademicTermId, sid, ct), - _ => null - }; - if (w is not null) generated.Add(w); - } + WarningType.FailedCredits => await CountWarning(db, rule, sid, rule.AcademicTermId, + sid => db.GradeRecords.CountAsync(x => x.StudentId == sid && x.GradeSheet!.TeachingTask!.AcademicTermId == rule.AcademicTermId && x.GradeSheet.Status == GradeSheetStatus.Published && x.TotalScore < 60, ct), + n => $"不及格课程 {n} 门", ct), + WarningType.LowGPA => await GPACheck(db, rule, sid, rule.AcademicTermId, ct), + WarningType.Absenteeism => await CountWarning(db, rule, sid, rule.AcademicTermId, + sid => db.AttendanceRecords.CountAsync(x => x.StudentId == sid && x.AttendanceSheet!.Status == AttendanceSheetStatus.Submitted && x.AttendanceSheet.TeachingTask!.AcademicTermId == rule.AcademicTermId && (x.Status == AttendanceStatus.Absent || x.Status == AttendanceStatus.Late), ct), + n => $"缺勤/迟到 {n} 次", ct), + WarningType.GraduationDelay => await CountWarning(db, rule, sid, rule.AcademicTermId, + sid => db.GradeRecords.CountAsync(x => x.StudentId == sid && x.GradeSheet!.Status == GradeSheetStatus.Published && x.TotalScore < 60, ct), + n => $"累计不及格 {n} 门", ct), + _ => null + }; + if (w is not null) generated.Add(w); } if (generated.Count > 0) { db.WarningRecords.AddRange(generated); await db.SaveChangesAsync(ct); - foreach (var w in generated) { var si = await db.Students.Where(x => x.Id == w.StudentId).Select(x => new { x.UserId, x.Name, x.AdministrativeClassId }).FirstAsync(ct); - var r = rules.First(x => x.Type == w.Type); - if (r.NotifyStudent && si.UserId.HasValue) + if (rule.NotifyStudent && si.UserId.HasValue) await NotificationService.SendAsync(db, si.UserId.Value, "学业预警", w.Detail, "/warnings", cancellationToken: ct); - if (r.NotifyCounselor) + if (rule.NotifyCounselor) { var cid = await db.AdministrativeClasses.Where(c => c.Id == si.AdministrativeClassId && c.CounselorUserId != null).Select(c => c.CounselorUserId!.Value).FirstOrDefaultAsync(ct); - if (cid != default) - await NotificationService.SendAsync(db, cid, "学生学业预警", $"{si.Name}:{w.Detail}", "/warnings", cancellationToken: ct); + if (cid != default) await NotificationService.SendAsync(db, cid, "学生学业预警", $"{si.Name}:{w.Detail}", "/warnings", cancellationToken: ct); } } } } } - private static async Task CheckFailedCredits(AppDbContext db, WarningRule rule, Guid termId, Guid sid, CancellationToken ct) + private static async Task CountWarning(AppDbContext db, WarningRule rule, Guid sid, Guid termId, + Func> counter, Func msg, CancellationToken ct) { - var n = await db.GradeRecords.CountAsync(x => x.StudentId == sid && x.GradeSheet!.TeachingTask!.AcademicTermId == termId && x.GradeSheet.Status == GradeSheetStatus.Published && x.TotalScore < 60, ct); - return n >= rule.Threshold ? new WarningRecord { StudentId = sid, Type = WarningType.FailedCredits, TriggerValue = n, Detail = $"不及格课程 {n} 门,达到预警阈值 {rule.Threshold} 门。", AcademicTermId = termId } : null; + var n = await counter(sid); + return n >= rule.Threshold ? new WarningRecord { StudentId = sid, Type = rule.Type, TriggerValue = n, Detail = $"{msg(n)},达到预警阈值 {rule.Threshold}。", AcademicTermId = termId } : null; } - private static async Task CheckLowGPA(AppDbContext db, WarningRule rule, Guid termId, Guid sid, CancellationToken ct) + + private static async Task GPACheck(AppDbContext db, WarningRule rule, Guid sid, Guid termId, CancellationToken ct) { var grades = await db.GradeRecords.Where(x => x.StudentId == sid && x.GradeSheet!.TeachingTask!.AcademicTermId == termId && x.GradeSheet.Status == GradeSheetStatus.Published && x.GradePoint != null).ToListAsync(ct); if (grades.Count == 0) return null; var gpa = grades.Average(x => x.GradePoint!.Value); return gpa < rule.Threshold ? new WarningRecord { StudentId = sid, Type = WarningType.LowGPA, TriggerValue = Math.Round(gpa, 2), Detail = $"平均绩点 {gpa:F2},低于预警阈值 {rule.Threshold}。", AcademicTermId = termId } : null; } - private static async Task CheckAbsenteeism(AppDbContext db, WarningRule rule, Guid termId, Guid sid, CancellationToken ct) - { - var n = await db.AttendanceRecords.CountAsync(x => x.StudentId == sid && x.AttendanceSheet!.Status == AttendanceSheetStatus.Submitted && x.AttendanceSheet.TeachingTask!.AcademicTermId == termId && (x.Status == AttendanceStatus.Absent || x.Status == AttendanceStatus.Late), ct); - return n >= rule.Threshold ? new WarningRecord { StudentId = sid, Type = WarningType.Absenteeism, TriggerValue = n, Detail = $"缺勤/迟到 {n} 次,达到预警阈值 {rule.Threshold} 次。", AcademicTermId = termId } : null; - } - private static async Task CheckDelay(AppDbContext db, WarningRule rule, Guid termId, Guid sid, CancellationToken ct) - { - var n = await db.GradeRecords.CountAsync(x => x.StudentId == sid && x.GradeSheet!.Status == GradeSheetStatus.Published && x.TotalScore < 60, ct); - return n >= rule.Threshold ? new WarningRecord { StudentId = sid, Type = WarningType.GraduationDelay, TriggerValue = n, Detail = $"累计不及格 {n} 门,达到延毕预警阈值 {rule.Threshold} 门。", AcademicTermId = termId } : null; - } } diff --git a/web/src/views/WarningsView.vue b/web/src/views/WarningsView.vue index 4374582..4e176b1 100644 --- a/web/src/views/WarningsView.vue +++ b/web/src/views/WarningsView.vue @@ -16,7 +16,6 @@ const myWarnings = ref([]) const loading = ref(false) const detecting = ref(false) const filterType = ref('') -const schedule = reactive({ isEnabled: false, dayOfWeek: 1, hour: 8, minute: 0, lastRunAt: null as string | null }) const typeLabels: Record = { 1: '不及格学分', 2: '低绩点', 3: '缺勤', 4: '延毕风险' } const statusLabels: Record = { 1: '生效中', 2: '已确认', 3: '已处理', 4: '已忽略' } @@ -31,12 +30,13 @@ function thresholdUnit(type: number): string { return '门 累计不及格' } -// Reactive rule objects — one per type, bound directly to v-model +const weekOptions = [{ value: 0, label: '每天' }, { value: 1, label: '周一' }, { value: 2, label: '周二' }, { value: 3, label: '周三' }, { value: 4, label: '周四' }, { value: 5, label: '周五' }, { value: 6, label: '周六' }, { value: 7, label: '周日' }] + const ruleRows = reactive([ - { type: 1 as number, name: '不及格学分', threshold: 2, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '' }, - { type: 2, name: '低绩点', threshold: 2, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '' }, - { type: 3, name: '缺勤', threshold: 3, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '' }, - { type: 4, name: '延毕风险', threshold: 5, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '' }, + { type: 1 as number, name: '不及格学分', threshold: 2, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '', autoCheckEnabled: false, checkDayOfWeek: 0, checkHour: 8, checkMinute: 0, lastCheckAt: null as string | null }, + { type: 2, name: '低绩点', threshold: 2, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '', autoCheckEnabled: false, checkDayOfWeek: 0, checkHour: 8, checkMinute: 0, lastCheckAt: null as string | null }, + { type: 3, name: '缺勤', threshold: 3, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '', autoCheckEnabled: false, checkDayOfWeek: 0, checkHour: 8, checkMinute: 0, lastCheckAt: null as string | null }, + { type: 4, name: '延毕风险', threshold: 5, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '', autoCheckEnabled: false, checkDayOfWeek: 0, checkHour: 8, checkMinute: 0, lastCheckAt: null as string | null }, ]) async function load() { @@ -53,9 +53,13 @@ async function load() { row.notifyStudent = sr.notifyStudent row.notifyCounselor = sr.notifyCounselor row.description = sr.description || '' + row.autoCheckEnabled = sr.autoCheckEnabled + row.checkDayOfWeek = sr.checkDayOfWeek + row.checkHour = sr.checkHour + row.checkMinute = sr.checkMinute + row.lastCheckAt = sr.lastCheckAt } } - await loadSchedule() } if (isSuperAdmin.value || isCounselor.value) records.value = (await http.get('/warnings/records', { params: { academicTermId: termId.value || undefined, type: filterType.value || undefined } })).data @@ -63,19 +67,9 @@ async function load() { } catch (e) { ElMessage.error(apiErrorMessage(e)) } finally { loading.value = false } } -async function loadSchedule() { - if (!termId.value) return - try { const { data } = await http.get('/warnings/schedule', { params: { academicTermId: termId.value } }); Object.assign(schedule, data) } - catch (_) {} -} -async function saveSchedule() { - try { await http.put('/warnings/schedule', { isEnabled: schedule.isEnabled, dayOfWeek: schedule.dayOfWeek, hour: schedule.hour, minute: schedule.minute }, { params: { academicTermId: termId.value } }); ElMessage.success('已保存') } - catch (e) { ElMessage.error(apiErrorMessage(e)) } -} - async function saveRules() { try { - const payload = ruleRows.map(r => ({ type: r.type, name: r.name, threshold: r.threshold, isEnabled: r.isEnabled, notifyStudent: r.notifyStudent, notifyCounselor: r.notifyCounselor, description: r.description })) + const payload = ruleRows.map(r => ({ type: r.type, name: r.name, threshold: r.threshold, isEnabled: r.isEnabled, notifyStudent: r.notifyStudent, notifyCounselor: r.notifyCounselor, description: r.description, autoCheckEnabled: r.autoCheckEnabled, checkDayOfWeek: r.checkDayOfWeek, checkHour: r.checkHour, checkMinute: r.checkMinute })) await http.put('/warnings/rules', payload, { params: { academicTermId: termId.value } }) ElMessage.success('预警规则已保存') } catch (e) { ElMessage.error(apiErrorMessage(e)) } @@ -114,26 +108,6 @@ onMounted(async () => { - -
-
-

定时检测

- 保存定时 -
-
- - - - - - - - - - 上次执行:{{ new Date(schedule.lastRunAt).toLocaleString('zh-CN') }} -
-
-
@@ -156,10 +130,28 @@ onMounted(async () => { - + + + + @@ -198,7 +190,6 @@ onMounted(async () => {