全部完成。定时检测系统概览:

新增组件

  ┌────────────────────────────────┬─────────────────────────────────────────────────────────┐
  │              组件              │                          说明                           │
  ├────────────────────────────────┼─────────────────────────────────────────────────────────┤
  │ WarningSchedule 实体           │ 定时配置:学期、星期几、时、分、启用/禁用、上次执行时间 │
  ├────────────────────────────────┼─────────────────────────────────────────────────────────┤
  │ WarningCheckWorker             │ BackgroundService,每分钟轮询一次,匹配到时间就执行检测 │
  ├────────────────────────────────┼─────────────────────────────────────────────────────────┤
  │ GET/PUT /api/warnings/schedule │ 读取/保存定时配置                                       │
  └────────────────────────────────┴─────────────────────────────────────────────────────────┘

  页面配置

  在学业预警页面顶部新增「定时检测」区域:
  - 启用开关:一键开/关自动检测
  - 星期选择:周一至周日
  - 时分选择:精确到分钟
  - 上次执行时间:自动显示

  工作流程

  每分钟 Worker 轮询
    → 查询 WarningSchedules 表,匹配当前星期+时+分
    → 检查是否已在本分钟执行过(防止重复)
    → 加载该学期全部启用规则 + 在籍学生
    → 逐条规则、逐个学生检测
    → 已存在的同型预警跳过,新预警写入 + 通知学生/辅导员
    → 更新 LastRunAt

  管理员配置"每周一 08:00 自动检测",系统就会在每周一早上 8 点整自动执行一次完整的学业预警检测。
This commit is contained in:
2026-07-25 17:17:05 +08:00 Unverified
parent db3a275601
commit d23b5156b4
7 changed files with 197 additions and 1 deletions
+33
View File
@@ -16,6 +16,7 @@ const myWarnings = ref<any[]>([])
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<number, string> = { 1: '不及格学分', 2: '低绩点', 3: '缺勤', 4: '延毕风险' }
const statusLabels: Record<number, string> = { 1: '生效中', 2: '已确认', 3: '已处理', 4: '已忽略' }
@@ -54,6 +55,7 @@ async function load() {
row.description = sr.description || ''
}
}
await loadSchedule()
}
if (isSuperAdmin.value || isCounselor.value)
records.value = (await http.get('/warnings/records', { params: { academicTermId: termId.value || undefined, type: filterType.value || undefined } })).data
@@ -61,6 +63,16 @@ 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 }))
@@ -102,6 +114,26 @@ onMounted(async () => {
</div>
</section>
<!-- Admin: Schedule config -->
<section v-if="isSuperAdmin && termId" class="warn-schedule">
<div style="display:flex;justify-content:space-between;align-items:center">
<h3 style="margin:0">定时检测</h3>
<el-button type="primary" size="small" @click="saveSchedule">保存定时</el-button>
</div>
<div style="display:flex;align-items:center;gap:12px;margin-top:10px;flex-wrap:wrap">
<el-switch v-model="schedule.isEnabled" active-text="启用自动检测" />
<span></span>
<el-select v-model="schedule.dayOfWeek" size="small" style="width:100px" :disabled="!schedule.isEnabled">
<el-option v-for="d in 7" :key="d" :label="['','周一','周二','周三','周四','周五','周六','周日'][d]" :value="d" />
</el-select>
<el-input-number v-model="schedule.hour" :min="0" :max="23" size="small" style="width:80px" :disabled="!schedule.isEnabled" />
<span></span>
<el-input-number v-model="schedule.minute" :min="0" :max="59" size="small" style="width:80px" :disabled="!schedule.isEnabled" />
<span></span>
<span v-if="schedule.lastRunAt" style="font-size:11px;color:var(--muted);margin-left:auto">上次执行{{ new Date(schedule.lastRunAt).toLocaleString('zh-CN') }}</span>
</div>
</section>
<!-- Admin: Rule config -->
<section v-if="isSuperAdmin && termId" class="warn-rules">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
@@ -166,6 +198,7 @@ onMounted(async () => {
</template>
<style scoped>
.warn-schedule { background:#f0f9eb; border:1px solid #c6e2ff; border-radius:8px; padding:14px 20px; margin-bottom:16px; }
.warn-rules { background:#fff; border:1px solid #e4e7ed; border-radius:8px; padding:16px 20px; margin-bottom:20px; }
.warn-records { background:#fff; border:1px solid #e4e7ed; border-radius:8px; padding:16px 20px; }
.warn-mine { display:grid; gap:10px; }