Browse Source

✨ feat: 优化任务分配算法

Pchen. 1 month ago
parent
commit
2e3037c7c0
1 changed files with 125 additions and 4 deletions
  1. 125 4
      lib/QK/TaskScheduler.js

+ 125 - 4
lib/QK/TaskScheduler.js

@@ -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 = {}) {
+        await this.ensureSchema()
         const client = await this.authenticateClient(clientId, clientSecret)
         if (!client) {
             throw new Error('客户端凭证无效')
         }
         const taskId = Number(payload.task_id || payload.id)
         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 now = Date.now()
         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]
         )
         if (!result || result.affectedRows <= 0) {
-            this.logWarn('reportResult', '任务结果上报被拒绝:任务不存在或不属于当前客户端', { taskId, clientId }, {
-                success,
-                status
-            })
             throw new Error('任务不存在或不属于当前客户端')
         }
         await this.syncClientSlots(clientId, now)