|
@@ -1347,13 +1347,138 @@ class TaskScheduler {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ extractFailureMessage(payload = {}) {
|
|
|
|
|
+ const result = payload.result && typeof payload.result === 'object' ? payload.result : {}
|
|
|
|
|
+ const parts = [
|
|
|
|
|
+ payload.message,
|
|
|
|
|
+ payload.error_msg,
|
|
|
|
|
+ payload.error,
|
|
|
|
|
+ result.message,
|
|
|
|
|
+ result.error,
|
|
|
|
|
+ result.error_msg,
|
|
|
|
|
+ typeof payload.result === 'string' ? payload.result : null
|
|
|
|
|
+ ]
|
|
|
|
|
+ return parts.filter(Boolean).map(item => String(item)).join(' ').trim()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ resolveFailureType(payload = {}) {
|
|
|
|
|
+ const result = payload.result && typeof payload.result === 'object' ? payload.result : {}
|
|
|
|
|
+ return String(payload.type || payload.event || result.type || result.event || '').trim()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ shouldRequeueTaskOnFailure(task, payload = {}) {
|
|
|
|
|
+ if (payload.success === true) {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ if (payload.requeue === true || payload.requeue === 1 || payload.requeue === '1') {
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const message = this.extractFailureMessage(payload)
|
|
|
|
|
+ const failureType = this.resolveFailureType(payload)
|
|
|
|
|
+ const terminalTypes = new Set(['already', 'dajia', 'not_open', 'not_in_time'])
|
|
|
|
|
+ if (terminalTypes.has(failureType)) {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const terminalPatterns = [
|
|
|
|
|
+ '已选择',
|
|
|
|
|
+ '冲突',
|
|
|
|
|
+ '超过',
|
|
|
|
|
+ '选课不开放',
|
|
|
|
|
+ '不在选课时间',
|
|
|
|
|
+ '用户名或密码',
|
|
|
|
|
+ '未匹配到目标课程',
|
|
|
|
|
+ '验证码验证失败次数',
|
|
|
|
|
+ '为避免账号被锁定'
|
|
|
|
|
+ ]
|
|
|
|
|
+ if (terminalPatterns.some(pattern => message.includes(pattern))) {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 旧版客户端 task_exit 上报:抢课任务异常结束,code=1
|
|
|
|
|
+ if (/抢课任务异常结束/.test(message) || /\bcode=1\b/.test(message)) {
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+ if (/抢课循环结束但未获得成功结果/.test(message)) {
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const autoRelogin = Number(task?.auto_relogin) === 1
|
|
|
|
|
+ if (autoRelogin) {
|
|
|
|
|
+ // 新版可传 type=login_expired;旧版仅 message / result.message
|
|
|
|
|
+ if (failureType === 'login_expired') {
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+ const loginPatterns = [
|
|
|
|
|
+ '别处登录',
|
|
|
|
|
+ '在其他地方登录',
|
|
|
|
|
+ '账号在别处',
|
|
|
|
|
+ '登录失效',
|
|
|
|
|
+ '登录已失效',
|
|
|
|
|
+ '请重新登录',
|
|
|
|
|
+ '按登录失效处理',
|
|
|
|
|
+ '空响应',
|
|
|
|
|
+ '未登录',
|
|
|
|
|
+ '登录失败',
|
|
|
|
|
+ '会话过期',
|
|
|
|
|
+ '会话失效'
|
|
|
|
|
+ ]
|
|
|
|
|
+ if (loginPatterns.some(pattern => message.includes(pattern))) {
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async requeueTaskAfterClientFailure(clientId, taskId, payload = {}) {
|
|
|
|
|
+ const now = Date.now()
|
|
|
|
|
+ const message = this.buildReportMessage(payload) || '任务异常结束,已重新进入待分配队列'
|
|
|
|
|
+ const result = await db.query(
|
|
|
|
|
+ `UPDATE qk_task SET status = ?, assigned_client_id = NULL, lease_expire_at = NULL,
|
|
|
|
|
+ assigned_at = NULL, result_json = NULL, error_msg = ?, finished_time = NULL, update_time = ?
|
|
|
|
|
+ WHERE id = ? AND assigned_client_id = ? AND status IN (?, ?)`,
|
|
|
|
|
+ [TASK_STATUS.PENDING, message, now, taskId, clientId, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
|
|
|
|
|
+ )
|
|
|
|
|
+ if (!result || result.affectedRows <= 0) {
|
|
|
|
|
+ throw new Error('任务不存在或不属于当前客户端')
|
|
|
|
|
+ }
|
|
|
|
|
+ await this.syncClientSlots(clientId, now)
|
|
|
|
|
+ await this.logTask(taskId, clientId, 'requeued', message, payload.result || payload)
|
|
|
|
|
+ this.logInfo('reportResult', '抢课任务异常结束,已重新入队', { taskId, clientId }, {
|
|
|
|
|
+ message,
|
|
|
|
|
+ requeued: true,
|
|
|
|
|
+ exit_reason: payload.exit_reason || payload.reason || null
|
|
|
|
|
+ })
|
|
|
|
|
+ return { task_id: taskId, status: TASK_STATUS.PENDING, requeued: true }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
async reportResult(clientId, clientSecret, payload = {}) {
|
|
async reportResult(clientId, clientSecret, payload = {}) {
|
|
|
|
|
+ await this.ensureSchema()
|
|
|
const client = await this.authenticateClient(clientId, clientSecret)
|
|
const client = await this.authenticateClient(clientId, clientSecret)
|
|
|
if (!client) {
|
|
if (!client) {
|
|
|
throw new Error('客户端凭证无效')
|
|
throw new Error('客户端凭证无效')
|
|
|
}
|
|
}
|
|
|
const taskId = Number(payload.task_id || payload.id)
|
|
const taskId = Number(payload.task_id || payload.id)
|
|
|
const success = payload.success === true || payload.status === TASK_STATUS.SUCCESS
|
|
const success = payload.success === true || payload.status === TASK_STATUS.SUCCESS
|
|
|
|
|
+ const taskRows = await db.query(
|
|
|
|
|
+ 'SELECT id, auto_relogin FROM qk_task WHERE id = ? AND assigned_client_id = ? AND status IN (?, ?)',
|
|
|
|
|
+ [taskId, clientId, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
|
|
|
|
|
+ )
|
|
|
|
|
+ if (!taskRows || taskRows.length === 0) {
|
|
|
|
|
+ this.logWarn('reportResult', '任务结果上报被拒绝:任务不存在或不属于当前客户端', { taskId, clientId }, {
|
|
|
|
|
+ success,
|
|
|
|
|
+ status: success ? TASK_STATUS.SUCCESS : TASK_STATUS.FAILED
|
|
|
|
|
+ })
|
|
|
|
|
+ throw new Error('任务不存在或不属于当前客户端')
|
|
|
|
|
+ }
|
|
|
|
|
+ const task = taskRows[0]
|
|
|
|
|
+
|
|
|
|
|
+ if (!success && this.shouldRequeueTaskOnFailure(task, payload)) {
|
|
|
|
|
+ return this.requeueTaskAfterClientFailure(clientId, taskId, payload)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const status = success ? TASK_STATUS.SUCCESS : TASK_STATUS.FAILED
|
|
const status = success ? TASK_STATUS.SUCCESS : TASK_STATUS.FAILED
|
|
|
const now = Date.now()
|
|
const now = Date.now()
|
|
|
const resultJson = payload.result ? JSON.stringify(payload.result) : JSON.stringify({
|
|
const resultJson = payload.result ? JSON.stringify(payload.result) : JSON.stringify({
|
|
@@ -1365,10 +1490,6 @@ class TaskScheduler {
|
|
|
[status, resultJson, success ? null : (payload.error_msg || payload.message || '抢课失败'), now, now, taskId, clientId, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
|
|
[status, resultJson, success ? null : (payload.error_msg || payload.message || '抢课失败'), now, now, taskId, clientId, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
|
|
|
)
|
|
)
|
|
|
if (!result || result.affectedRows <= 0) {
|
|
if (!result || result.affectedRows <= 0) {
|
|
|
- this.logWarn('reportResult', '任务结果上报被拒绝:任务不存在或不属于当前客户端', { taskId, clientId }, {
|
|
|
|
|
- success,
|
|
|
|
|
- status
|
|
|
|
|
- })
|
|
|
|
|
throw new Error('任务不存在或不属于当前客户端')
|
|
throw new Error('任务不存在或不属于当前客户端')
|
|
|
}
|
|
}
|
|
|
await this.syncClientSlots(clientId, now)
|
|
await this.syncClientSlots(clientId, now)
|