全部完成。定时检测系统概览:
新增组件
┌────────────────────────────────┬─────────────────────────────────────────────────────────┐
│ 组件 │ 说明 │
├────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ WarningSchedule 实体 │ 定时配置:学期、星期几、时、分、启用/禁用、上次执行时间 │
├────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ WarningCheckWorker │ BackgroundService,每分钟轮询一次,匹配到时间就执行检测 │
├────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ GET/PUT /api/warnings/schedule │ 读取/保存定时配置 │
└────────────────────────────────┴─────────────────────────────────────────────────────────┘
页面配置
在学业预警页面顶部新增「定时检测」区域:
- 启用开关:一键开/关自动检测
- 星期选择:周一至周日
- 时分选择:精确到分钟
- 上次执行时间:自动显示
工作流程
每分钟 Worker 轮询
→ 查询 WarningSchedules 表,匹配当前星期+时+分
→ 检查是否已在本分钟执行过(防止重复)
→ 加载该学期全部启用规则 + 在籍学生
→ 逐条规则、逐个学生检测
→ 已存在的同型预警跳过,新预警写入 + 通知学生/辅导员
→ 更新 LastRunAt
管理员配置"每周一 08:00 自动检测",系统就会在每周一早上 8 点整自动执行一次完整的学业预警检测。
This commit is contained in:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user