“教学班成绩分析”页面新增“定时刷新设置”,支持启停、刷新间隔、单次处理上限、查看上次/下次扫描时间。

配置保存在独立数据库表 CourseGradeStatisticsRefreshSettings,不是修改 appsettings.json。
后台每 10 秒读取配置,仅到期扫描;成绩录入、导入、审批时不再立即创建统计任务。
扫描发现过期数据后,仍通过持久任务、Outbox 和 RabbitMQ 执行;Redis统计缓存由处理任务统一刷新。
This commit is contained in:
biss committed 2026-08-09 20:51:57 +08:00
1 parent e8261714da
commit cd073885b5
17 files changed
+7661 -46

No files matched your search

+8 -7
View File
@@ -6,7 +6,7 @@ import { useAuthStore } from '../stores/auth'
import { academicTermLabel, academicTermOptionClass, defaultAcademicTermId } from '../utils/academicTerms'
const auth = useAuthStore()
const isSuperAdmin = computed(() => auth.user?.roles.includes('SuperAdmin'))
const isWarningManager = computed(() => auth.user?.roles.some(r => ['SuperAdmin', 'AcademicAdmin'].includes(r)) ?? false)
const isCounselor = computed(() => auth.user?.roles.includes('Counselor') && !auth.user?.roles.some(r => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(r)))
const isStudent = computed(() => auth.user?.roles.includes('Student') && !auth.user?.roles.some(r => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor'].includes(r)))
@@ -43,11 +43,11 @@ const ruleRows = reactive([
async function load() {
loading.value = true
try {
if (isSuperAdmin.value && termId.value) {
if (isWarningManager.value && termId.value) {
const serverRules = (await http.get('/warnings/rules', { params: { academicTermId: termId.value } })).data
// Merge server values into reactive rows
for (const row of ruleRows) {
const sr = serverRules.find((r: any) => r.type === row.type)
const sr = serverRules.find((r: any) => Number(r.type) === row.type)
if (sr) {
row.threshold = sr.threshold
row.isEnabled = sr.isEnabled
@@ -62,7 +62,7 @@ async function load() {
}
}
}
if (isSuperAdmin.value || isCounselor.value)
if (isWarningManager.value || isCounselor.value)
records.value = (await http.get('/warnings/records', { params: { academicTermId: termId.value || undefined, type: filterType.value || undefined } })).data
if (isStudent.value) myWarnings.value = (await http.get('/warnings/my-warnings')).data
} catch (e) { ElMessage.error(apiErrorMessage(e)) } finally { loading.value = false }
@@ -72,6 +72,7 @@ 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, autoCheckEnabled: r.autoCheckEnabled, checkDayOfWeek: r.checkDayOfWeek, checkHour: r.checkHour, checkMinute: r.checkMinute }))
await http.put('/warnings/rules', payload, { params: { academicTermId: termId.value } })
await load()
ElMessage.success('预警规则已保存')
} catch (e) { ElMessage.error(apiErrorMessage(e)) }
}
@@ -102,7 +103,7 @@ onMounted(async () => {
<template>
<div class="page-stack warn-page">
<section class="page-intro">
<div><span class="section-kicker">ACADEMIC WARNING</span><h2>{{ isStudent ? '我的预警' : '学业预警' }}</h2><p>{{ isSuperAdmin ? '配置预警规则,执行检测,查看预警记录。' : isCounselor ? '查看所管学生的学业预警情况。' : '查看并确认您的学业预警通知。' }}</p></div>
<div><span class="section-kicker">ACADEMIC WARNING</span><h2>{{ isStudent ? '我的预警' : '学业预警' }}</h2><p>{{ isWarningManager ? '配置预警规则,执行检测,查看预警记录。' : isCounselor ? '查看所管学生的学业预警情况。' : '查看并确认您的学业预警通知。' }}</p></div>
<div style="display:flex;gap:8px;align-items:center">
<el-select v-model="termId" clearable @change="load" style="width:240px"><el-option v-for="t in terms" :key="t.id" :label="academicTermLabel(t)" :value="t.id" :class="academicTermOptionClass(t)" /></el-select>
<el-button :icon="Refresh" @click="load">刷新</el-button>
@@ -110,7 +111,7 @@ onMounted(async () => {
</section>
<!-- Admin: Rule config -->
<section v-if="isSuperAdmin && termId" class="warn-rules">
<section v-if="isWarningManager && termId" class="warn-rules">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<h3 style="margin:0">预警规则配置</h3>
<div>
@@ -160,7 +161,7 @@ onMounted(async () => {
</section>
<!-- Records -->
<section v-if="isSuperAdmin || isCounselor" v-loading="loading" class="warn-records">
<section v-if="isWarningManager || isCounselor" v-loading="loading" class="warn-records">
<div style="display:flex;gap:12px;align-items:center;margin-bottom:12px">
<h3 style="margin:0">预警记录</h3>
<el-select v-model="filterType" clearable placeholder="全部类型" @change="load" style="width:140px"><el-option v-for="(v,k) in typeLabels" :key="k" :label="v" :value="Number(k)" /></el-select>