|
|
@@ -14,6 +14,13 @@ const TASK_STATUS = {
|
|
|
CANCELLED: 'cancelled'
|
|
|
}
|
|
|
|
|
|
+const REPORT_LOG_EVENTS = new Set([
|
|
|
+ 'request_result',
|
|
|
+ 'progress_snapshot',
|
|
|
+ 'grab_success',
|
|
|
+ 'grab_fail'
|
|
|
+])
|
|
|
+
|
|
|
class TaskScheduler {
|
|
|
constructor(options = {}) {
|
|
|
this.leaseMs = options.leaseMs || config.qk?.leaseMs || 90 * 1000
|
|
|
@@ -194,6 +201,90 @@ class TaskScheduler {
|
|
|
this.logInfo('taskLog', message || event, { taskId, clientId }, payload ? { event, ...this.sanitizeForLog(payload) } : { event })
|
|
|
}
|
|
|
|
|
|
+ serializeReportLog(row) {
|
|
|
+ const result = { ...row }
|
|
|
+ if (typeof result.payload_json === 'string' && result.payload_json) {
|
|
|
+ try {
|
|
|
+ result.payload_json = JSON.parse(result.payload_json)
|
|
|
+ } catch (_) {}
|
|
|
+ }
|
|
|
+ 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} 失败` : '请求失败'
|
|
|
+ }
|
|
|
+
|
|
|
+ async assertClientTaskAccess(clientId, taskId) {
|
|
|
+ const rows = await db.query(
|
|
|
+ 'SELECT id, status, assigned_client_id FROM qk_task WHERE id = ?',
|
|
|
+ [taskId]
|
|
|
+ )
|
|
|
+ if (!rows || rows.length === 0) {
|
|
|
+ throw new Error('任务不存在')
|
|
|
+ }
|
|
|
+ const task = rows[0]
|
|
|
+ if (task.assigned_client_id !== clientId) {
|
|
|
+ throw new Error('任务不属于当前客户端')
|
|
|
+ }
|
|
|
+ if (![TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING].includes(task.status)) {
|
|
|
+ throw new Error('任务当前状态不可上报')
|
|
|
+ }
|
|
|
+ return task
|
|
|
+ }
|
|
|
+
|
|
|
+ async reportProgress(clientId, clientSecret, payload = {}) {
|
|
|
+ const client = await this.authenticateClient(clientId, clientSecret)
|
|
|
+ if (!client) {
|
|
|
+ throw new Error('客户端凭证无效')
|
|
|
+ }
|
|
|
+ const taskId = Number(payload.task_id || payload.id)
|
|
|
+ if (!taskId) {
|
|
|
+ throw new Error('缺少任务 ID')
|
|
|
+ }
|
|
|
+ const event = String(payload.event || 'request_result')
|
|
|
+ if (!REPORT_LOG_EVENTS.has(event) || event === 'grab_success' || event === 'grab_fail') {
|
|
|
+ throw new Error('不支持的上报类型')
|
|
|
+ }
|
|
|
+ const task = await this.assertClientTaskAccess(clientId, taskId)
|
|
|
+ const message = this.buildReportMessage(payload)
|
|
|
+ const now = Date.now()
|
|
|
+ await this.logTask(taskId, clientId, event, message, payload)
|
|
|
+ const updates = ['update_time = ?']
|
|
|
+ const params = [now]
|
|
|
+ if (task.status === TASK_STATUS.ASSIGNED) {
|
|
|
+ updates.push('status = ?')
|
|
|
+ params.push(TASK_STATUS.RUNNING)
|
|
|
+ }
|
|
|
+ if (payload.success !== true && message) {
|
|
|
+ updates.push('error_msg = ?')
|
|
|
+ params.push(message)
|
|
|
+ }
|
|
|
+ params.push(taskId, clientId)
|
|
|
+ await db.query(
|
|
|
+ `UPDATE qk_task SET ${updates.join(', ')} WHERE id = ? AND assigned_client_id = ?`,
|
|
|
+ params
|
|
|
+ )
|
|
|
+ this.logInfo('reportProgress', '客户端上报抢课进度', { taskId, clientId }, {
|
|
|
+ event,
|
|
|
+ success: payload.success === true,
|
|
|
+ message
|
|
|
+ })
|
|
|
+ return { task_id: taskId, event, message }
|
|
|
+ }
|
|
|
+
|
|
|
async createTask(uuid, payload) {
|
|
|
const courses = this.normalizeArray(payload.courses || payload.COURSES)
|
|
|
const courseGroups = this.normalizeArray(payload.course_groups || payload.COURSE_GROUPS)
|
|
|
@@ -846,6 +937,86 @@ class TaskScheduler {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ async listAdminReports(filters = {}) {
|
|
|
+ const pagesize = Math.max(1, Math.min(100, Number(filters.pagesize || 20)))
|
|
|
+ const current = Math.max(1, Number(filters.current || 1))
|
|
|
+ const where = ['l.event IN (?, ?, ?, ?)']
|
|
|
+ const params = ['request_result', 'progress_snapshot', 'grab_success', 'grab_fail']
|
|
|
+ const countParams = ['request_result', 'progress_snapshot', 'grab_success', 'grab_fail']
|
|
|
+ if (filters.task_id) {
|
|
|
+ where.push('l.task_id = ?')
|
|
|
+ params.push(Number(filters.task_id))
|
|
|
+ countParams.push(Number(filters.task_id))
|
|
|
+ }
|
|
|
+ if (filters.client_id) {
|
|
|
+ where.push('l.client_id = ?')
|
|
|
+ params.push(String(filters.client_id))
|
|
|
+ countParams.push(String(filters.client_id))
|
|
|
+ }
|
|
|
+ if (filters.event) {
|
|
|
+ where.push('l.event = ?')
|
|
|
+ params.push(String(filters.event))
|
|
|
+ countParams.push(String(filters.event))
|
|
|
+ }
|
|
|
+ if (filters.start_time) {
|
|
|
+ where.push('l.create_time >= ?')
|
|
|
+ params.push(Number(filters.start_time))
|
|
|
+ countParams.push(Number(filters.start_time))
|
|
|
+ }
|
|
|
+ if (filters.end_time) {
|
|
|
+ where.push('l.create_time <= ?')
|
|
|
+ params.push(Number(filters.end_time))
|
|
|
+ countParams.push(Number(filters.end_time))
|
|
|
+ }
|
|
|
+ if (filters.student_num) {
|
|
|
+ where.push('t.student_num LIKE ?')
|
|
|
+ params.push(`%${filters.student_num}%`)
|
|
|
+ countParams.push(`%${filters.student_num}%`)
|
|
|
+ }
|
|
|
+ if (filters.name || filters.task_name) {
|
|
|
+ where.push('t.name LIKE ?')
|
|
|
+ params.push(`%${filters.name || filters.task_name}%`)
|
|
|
+ countParams.push(`%${filters.name || filters.task_name}%`)
|
|
|
+ }
|
|
|
+ if (filters.username) {
|
|
|
+ where.push('u.username COLLATE utf8mb4_general_ci LIKE (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
|
|
|
+ params.push(`%${filters.username}%`)
|
|
|
+ countParams.push(`%${filters.username}%`)
|
|
|
+ }
|
|
|
+ if (filters.client_label) {
|
|
|
+ where.push('c.label LIKE ?')
|
|
|
+ params.push(`%${filters.client_label}%`)
|
|
|
+ countParams.push(`%${filters.client_label}%`)
|
|
|
+ }
|
|
|
+ const whereSql = where.join(' AND ')
|
|
|
+ const offset = (current - 1) * pagesize
|
|
|
+ const joinSql = `
|
|
|
+ FROM qk_task_log l
|
|
|
+ LEFT JOIN qk_task t ON t.id = l.task_id
|
|
|
+ LEFT JOIN qk_client c ON c.client_id = l.client_id
|
|
|
+ LEFT JOIN users u ON u.uuid COLLATE utf8mb4_general_ci = t.create_user COLLATE utf8mb4_general_ci`
|
|
|
+ const countRows = await db.query(
|
|
|
+ `SELECT COUNT(*) AS total ${joinSql} WHERE ${whereSql}`,
|
|
|
+ countParams
|
|
|
+ )
|
|
|
+ const rows = await db.query(
|
|
|
+ `SELECT l.id, l.task_id, l.client_id, l.event, l.message, l.payload_json, l.create_time,
|
|
|
+ t.name AS task_name, t.student_num, t.status AS task_status,
|
|
|
+ c.label AS client_label, u.username, u.avatar
|
|
|
+ ${joinSql}
|
|
|
+ WHERE ${whereSql}
|
|
|
+ ORDER BY l.create_time DESC
|
|
|
+ LIMIT ${pagesize} OFFSET ${offset}`,
|
|
|
+ params
|
|
|
+ )
|
|
|
+ return {
|
|
|
+ list: (rows || []).map(row => this.serializeReportLog(row)),
|
|
|
+ total: countRows?.[0]?.total || 0,
|
|
|
+ current,
|
|
|
+ pagesize
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
async reassignStaleRunningTasks() {
|
|
|
const onlineCount = await this.countOnlineClients()
|
|
|
if (onlineCount <= 1) {
|