|
|
@@ -23,6 +23,8 @@ const REPORT_LOG_EVENTS = new Set([
|
|
|
])
|
|
|
|
|
|
class TaskScheduler {
|
|
|
+ static _schemaReady = false
|
|
|
+
|
|
|
constructor(options = {}) {
|
|
|
this.leaseMs = options.leaseMs || config.qk?.leaseMs || 90 * 1000
|
|
|
this.heartbeatTtlSeconds = options.heartbeatTtlSeconds || config.qk?.heartbeatTtlSeconds || 45
|
|
|
@@ -35,6 +37,73 @@ class TaskScheduler {
|
|
|
this.logger = options.logger || new Logger()
|
|
|
}
|
|
|
|
|
|
+ async ensureSchema() {
|
|
|
+ if (TaskScheduler._schemaReady) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const rows = await db.query("SHOW COLUMNS FROM qk_task LIKE 'auto_relogin'")
|
|
|
+ if (!rows || rows.length === 0) {
|
|
|
+ await db.query(
|
|
|
+ 'ALTER TABLE qk_task ADD COLUMN auto_relogin TINYINT(1) NOT NULL DEFAULT 0 AFTER enable_ggxxk'
|
|
|
+ )
|
|
|
+ this.logInfo('ensureSchema', '已添加 qk_task.auto_relogin 字段')
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ this.logWarn('ensureSchema', '抢课任务表结构检查失败', {}, err)
|
|
|
+ }
|
|
|
+ TaskScheduler._schemaReady = true
|
|
|
+ }
|
|
|
+
|
|
|
+ parseAutoRelogin(payload = {}) {
|
|
|
+ if (payload.auto_relogin !== undefined) {
|
|
|
+ return payload.auto_relogin === true || Number(payload.auto_relogin) === 1
|
|
|
+ }
|
|
|
+ if (payload.AUTO_RELOGIN !== undefined) {
|
|
|
+ return payload.AUTO_RELOGIN === true || Number(payload.AUTO_RELOGIN) === 1
|
|
|
+ }
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ extractReportCourseName(payload = {}) {
|
|
|
+ if (!payload || typeof payload !== 'object') {
|
|
|
+ return ''
|
|
|
+ }
|
|
|
+ if (payload.course_name) {
|
|
|
+ return String(payload.course_name)
|
|
|
+ }
|
|
|
+ if (payload.course) {
|
|
|
+ return String(payload.course)
|
|
|
+ }
|
|
|
+ if (payload.label) {
|
|
|
+ const label = String(payload.label)
|
|
|
+ const at = label.indexOf('@')
|
|
|
+ if (at > 0) {
|
|
|
+ return label.slice(0, at)
|
|
|
+ }
|
|
|
+ return label
|
|
|
+ }
|
|
|
+ if (Array.isArray(payload.courses) && payload.courses.length > 0) {
|
|
|
+ return payload.courses.join('、')
|
|
|
+ }
|
|
|
+ return ''
|
|
|
+ }
|
|
|
+
|
|
|
+ formatReportMessage(message = '', payload = {}) {
|
|
|
+ const courseName = this.extractReportCourseName(payload)
|
|
|
+ const text = String(message || '').trim()
|
|
|
+ if (!courseName) {
|
|
|
+ return text
|
|
|
+ }
|
|
|
+ if (!text) {
|
|
|
+ return `[${courseName}]`
|
|
|
+ }
|
|
|
+ if (text.includes(`[${courseName}]`) || text.startsWith(`${courseName}:`)) {
|
|
|
+ return text
|
|
|
+ }
|
|
|
+ return `[${courseName}] ${text}`
|
|
|
+ }
|
|
|
+
|
|
|
calculateMaxSlots(profile = {}) {
|
|
|
const freeMb = Math.max(0, Number(profile.free_mem_mb) || 0)
|
|
|
const totalMb = Math.max(0, Number(profile.total_mem_mb) || 0)
|
|
|
@@ -273,6 +342,7 @@ class TaskScheduler {
|
|
|
serializeTask(row, includeSecret = false) {
|
|
|
const result = { ...row }
|
|
|
result.enable_ggxxk = Number(result.enable_ggxxk) === 1
|
|
|
+ result.auto_relogin = Number(result.auto_relogin) === 1
|
|
|
result.courses = this.normalizeArray(result.courses)
|
|
|
result.course_groups = this.normalizeArray(result.course_groups)
|
|
|
if (typeof result.result_json === 'string' && result.result_json) {
|
|
|
@@ -475,23 +545,31 @@ class TaskScheduler {
|
|
|
result.payload_json = JSON.parse(result.payload_json)
|
|
|
} catch (_) {}
|
|
|
}
|
|
|
+ const payload = result.payload_json && typeof result.payload_json === 'object'
|
|
|
+ ? result.payload_json
|
|
|
+ : {}
|
|
|
+ result.course_name = this.extractReportCourseName(payload)
|
|
|
+ result.display_message = this.formatReportMessage(result.message, payload)
|
|
|
return result
|
|
|
}
|
|
|
|
|
|
buildReportMessage(payload = {}) {
|
|
|
- if (payload.message) {
|
|
|
- return String(payload.message)
|
|
|
- }
|
|
|
- if (payload.error_msg) {
|
|
|
- return String(payload.error_msg)
|
|
|
- }
|
|
|
- if (payload.error) {
|
|
|
- return String(payload.error)
|
|
|
- }
|
|
|
- if (payload.success === true) {
|
|
|
- return payload.label ? `${payload.label} 成功` : '请求成功'
|
|
|
- }
|
|
|
- return payload.label ? `${payload.label} 失败` : '请求失败'
|
|
|
+ const base = (() => {
|
|
|
+ if (payload.message) {
|
|
|
+ return String(payload.message)
|
|
|
+ }
|
|
|
+ if (payload.error_msg) {
|
|
|
+ return String(payload.error_msg)
|
|
|
+ }
|
|
|
+ if (payload.error) {
|
|
|
+ return String(payload.error)
|
|
|
+ }
|
|
|
+ if (payload.success === true) {
|
|
|
+ return payload.label ? `${payload.label} 成功` : '请求成功'
|
|
|
+ }
|
|
|
+ return payload.label ? `${payload.label} 失败` : '请求失败'
|
|
|
+ })()
|
|
|
+ return this.formatReportMessage(base, payload)
|
|
|
}
|
|
|
|
|
|
async assertClientTaskAccess(clientId, taskId) {
|
|
|
@@ -553,6 +631,7 @@ class TaskScheduler {
|
|
|
}
|
|
|
|
|
|
async createTask(uuid, payload) {
|
|
|
+ await this.ensureSchema()
|
|
|
const validated = this.validateTaskCoursesAndInterval(
|
|
|
payload.courses || payload.COURSES,
|
|
|
payload.course_groups || payload.COURSE_GROUPS,
|
|
|
@@ -563,8 +642,8 @@ class TaskScheduler {
|
|
|
await this.requireEnabledBatch(batchId)
|
|
|
const time = Date.now()
|
|
|
const sql = `INSERT INTO qk_task
|
|
|
- (create_user, name, batch_id, jx0502zbid, student_num, password_enc, courses, course_groups, enable_ggxxk, interval_ms, status, create_time, update_time)
|
|
|
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
|
+ (create_user, name, batch_id, jx0502zbid, student_num, password_enc, courses, course_groups, enable_ggxxk, auto_relogin, interval_ms, status, create_time, update_time)
|
|
|
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
|
const result = await db.query(sql, [
|
|
|
uuid,
|
|
|
payload.name,
|
|
|
@@ -575,6 +654,7 @@ class TaskScheduler {
|
|
|
JSON.stringify(courses),
|
|
|
JSON.stringify(courseGroups),
|
|
|
payload.enable_ggxxk || payload.ENABLE_GGXXK ? 1 : 0,
|
|
|
+ this.parseAutoRelogin(payload) ? 1 : 0,
|
|
|
intervalMs,
|
|
|
TASK_STATUS.PAUSED,
|
|
|
time,
|
|
|
@@ -591,12 +671,14 @@ class TaskScheduler {
|
|
|
courses_count: courses.length,
|
|
|
course_groups_count: courseGroups.length,
|
|
|
interval_ms: intervalMs,
|
|
|
- enable_ggxxk: !!(payload.enable_ggxxk || payload.ENABLE_GGXXK)
|
|
|
+ enable_ggxxk: !!(payload.enable_ggxxk || payload.ENABLE_GGXXK),
|
|
|
+ auto_relogin: this.parseAutoRelogin(payload)
|
|
|
})
|
|
|
return result.insertId
|
|
|
}
|
|
|
|
|
|
async updateTask(uuid, taskId, payload) {
|
|
|
+ await this.ensureSchema()
|
|
|
const rows = await db.query('SELECT status FROM qk_task WHERE id = ? AND create_user = ?', [taskId, uuid])
|
|
|
if (!rows || rows.length === 0) {
|
|
|
throw new Error('任务不存在')
|
|
|
@@ -620,6 +702,7 @@ class TaskScheduler {
|
|
|
JSON.stringify(courses),
|
|
|
JSON.stringify(courseGroups),
|
|
|
payload.enable_ggxxk || payload.ENABLE_GGXXK ? 1 : 0,
|
|
|
+ this.parseAutoRelogin(payload) ? 1 : 0,
|
|
|
intervalMs,
|
|
|
TASK_STATUS.PAUSED,
|
|
|
Date.now()
|
|
|
@@ -628,7 +711,7 @@ class TaskScheduler {
|
|
|
params.splice(3, 0, this.encryptPassword(payload.password || payload.pass))
|
|
|
}
|
|
|
params.push(taskId, uuid)
|
|
|
- const sql = `UPDATE qk_task SET name = ?, batch_id = ?, student_num = ?${passwordSql}, courses = ?, course_groups = ?, enable_ggxxk = ?, interval_ms = ?, status = ?, jx0502zbid = '', update_time = ?, assigned_client_id = NULL, lease_expire_at = NULL, result_json = NULL, error_msg = NULL, finished_time = NULL WHERE id = ? AND create_user = ?`
|
|
|
+ const sql = `UPDATE qk_task SET name = ?, batch_id = ?, student_num = ?${passwordSql}, courses = ?, course_groups = ?, enable_ggxxk = ?, auto_relogin = ?, interval_ms = ?, status = ?, jx0502zbid = '', update_time = ?, assigned_client_id = NULL, lease_expire_at = NULL, result_json = NULL, error_msg = NULL, finished_time = NULL WHERE id = ? AND create_user = ?`
|
|
|
const result = await db.query(sql, params)
|
|
|
if (!result || result.affectedRows <= 0) {
|
|
|
throw new Error('更新抢课任务失败')
|
|
|
@@ -784,6 +867,7 @@ class TaskScheduler {
|
|
|
}
|
|
|
|
|
|
async adminUpdateTask(taskId, payload) {
|
|
|
+ await this.ensureSchema()
|
|
|
const rows = await db.query('SELECT * FROM qk_task WHERE id = ?', [taskId])
|
|
|
if (!rows || rows.length === 0) {
|
|
|
throw new Error('任务不存在')
|
|
|
@@ -810,6 +894,7 @@ class TaskScheduler {
|
|
|
JSON.stringify(courses),
|
|
|
JSON.stringify(courseGroups),
|
|
|
payload.enable_ggxxk || payload.ENABLE_GGXXK ? 1 : 0,
|
|
|
+ this.parseAutoRelogin(payload) ? 1 : 0,
|
|
|
intervalMs,
|
|
|
TASK_STATUS.PAUSED,
|
|
|
now
|
|
|
@@ -818,7 +903,7 @@ class TaskScheduler {
|
|
|
params.splice(3, 0, this.encryptPassword(payload.password || payload.pass))
|
|
|
}
|
|
|
params.push(taskId)
|
|
|
- const sql = `UPDATE qk_task SET name = ?, batch_id = ?, student_num = ?${passwordSql}, courses = ?, course_groups = ?, enable_ggxxk = ?, interval_ms = ?, status = ?, jx0502zbid = '', update_time = ?, assigned_client_id = NULL, lease_expire_at = NULL, assigned_at = NULL, exclude_client_id = NULL, result_json = NULL, error_msg = NULL, finished_time = NULL WHERE id = ?`
|
|
|
+ const sql = `UPDATE qk_task SET name = ?, batch_id = ?, student_num = ?${passwordSql}, courses = ?, course_groups = ?, enable_ggxxk = ?, auto_relogin = ?, interval_ms = ?, status = ?, jx0502zbid = '', update_time = ?, assigned_client_id = NULL, lease_expire_at = NULL, assigned_at = NULL, exclude_client_id = NULL, result_json = NULL, error_msg = NULL, finished_time = NULL WHERE id = ?`
|
|
|
const result = await db.query(sql, params)
|
|
|
if (!result || result.affectedRows <= 0) {
|
|
|
throw new Error('更新抢课任务失败')
|