From 34ebad316a30979727ba0e5cb1e9d7913888d30c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=88=90?= Date: Fri, 19 Dec 2025 11:40:25 +0800 Subject: [PATCH] 1 --- admin/src/api/profile/pla_account_server.js | 11 + .../src/views/account/pla_account_detail.vue | 109 +++++++--- admin/src/views/task/task_status.vue | 1 + api/controller_admin/pla_account.js | 29 +++ api/services/pla_account_service.js | 192 ++++++++++++++---- 5 files changed, 276 insertions(+), 66 deletions(-) diff --git a/admin/src/api/profile/pla_account_server.js b/admin/src/api/profile/pla_account_server.js index c3af2ca..34f8418 100644 --- a/admin/src/api/profile/pla_account_server.js +++ b/admin/src/api/profile/pla_account_server.js @@ -132,6 +132,17 @@ class PlaAccountServer { }) } + /** + * 重试指令 + * @param {Number|String} commandId - 指令ID + * @returns {Promise} + */ + retryCommand(commandId) { + return window.framework.http.post(`pla_account/retryCommand`, { + commandId + }) + } + /** * 停止账号的所有任务 * @param {Object} row - 账号数据(包含id和sn_code) diff --git a/admin/src/views/account/pla_account_detail.vue b/admin/src/views/account/pla_account_detail.vue index 3d8a22f..229bd40 100644 --- a/admin/src/views/account/pla_account_detail.vue +++ b/admin/src/views/account/pla_account_detail.vue @@ -122,7 +122,7 @@
过期时间: - + {{ getExpireDate(accountInfo) }}
@@ -176,7 +176,8 @@ {{ item.weight }}%
- 总权重:{{ totalWeight }}% + 总权重:{{ totalWeight + }}%
暂无配置
@@ -204,13 +205,15 @@
最低薪资(元): - {{ deliverConfig.min_salary || deliverConfig.min_salary === 0 ? deliverConfig.min_salary : '-' }} + {{ deliverConfig.min_salary || deliverConfig.min_salary === 0 ? + deliverConfig.min_salary : '-' }}
最高薪资(元): - {{ deliverConfig.max_salary || deliverConfig.max_salary === 0 ? deliverConfig.max_salary : '-' }} + {{ deliverConfig.max_salary || deliverConfig.max_salary === 0 ? + deliverConfig.max_salary : '-' }}
@@ -354,8 +357,8 @@
-
- +
@@ -364,8 +367,8 @@
-
- +
@@ -524,10 +527,10 @@ export default { return { accountInfo: {}, activeTab: 'tasks', - + // 职位类型选项 jobTypeOptions: [], - + // 配置数据 priorityList: [], deliverConfig: { @@ -778,17 +781,35 @@ export default { { title: '操作', key: 'action', - width: 120, + width: 180, render: (h, params) => { - return h('Button', { - props: { - type: 'primary', - size: 'small' - }, - on: { - click: () => this.showCommandDetail(params.row) - } - }, '详情') + const btns = [] + // 详情按钮 + btns.push({ + title: '详情', + type: 'primary', + click: () => this.showCommandDetail(params.row) + }) + // 重试按钮(只在失败状态时显示) + if (params.row.status === 'failed') { + btns.push({ + title: '重试', + type: 'warning', + click: () => this.retryCommand(params.row) + }) + } + return h('div', btns.map(btn => + h('Button', { + props: { + type: btn.type, + size: 'small' + }, + style: { marginRight: '5px' }, + on: { + click: btn.click + } + }, btn.title) + )) } } ] @@ -828,7 +849,7 @@ export default { this.accountInfo = {} } }, - + // 解析配置数据 parseConfigData(accountInfo) { // 解析排序优先级配置 @@ -847,7 +868,7 @@ export default { } else { this.priorityList = [] } - + // 解析自动投递配置 if (accountInfo.deliver_config) { const deliverConfig = typeof accountInfo.deliver_config === 'string' @@ -885,7 +906,7 @@ export default { deliver_workdays_only: 1 } } - + // 解析自动沟通配置 if (accountInfo.chat_strategy) { const chatStrategy = typeof accountInfo.chat_strategy === 'string' @@ -909,7 +930,7 @@ export default { chat_workdays_only: 1 } } - + // 解析自动活跃配置 if (accountInfo.active_actions) { const activeActions = typeof accountInfo.active_actions === 'string' @@ -928,7 +949,7 @@ export default { } } }, - + // 加载职位类型 async loadJobTypes() { try { @@ -943,14 +964,14 @@ export default { console.error('加载职位类型失败:', err) } }, - + // 获取职位类型名称 getJobTypeName(jobTypeId) { if (!jobTypeId) return '' const jobType = this.jobTypeOptions.find(item => item.value === jobTypeId) return jobType ? jobType.label : '' }, - + // 获取优先级标签 getPriorityLabel(key) { const labelMap = { @@ -974,8 +995,16 @@ export default { pageOption: this.tasksPageOption } const res = await plaAccountServer.getTasks(this.accountId, param) + + this.tasksData = res.data.rows || [] this.tasksPageOption.total = res.data.count || 0 + + + + setTimeout(() => { + this.$forceUpdate() + }, 0) } catch (error) { this.$Message.error('加载任务列表失败') this.tasksData = [] @@ -997,8 +1026,13 @@ export default { pageOption: this.commandsPageOption } const res = await plaAccountServer.getCommands(this.accountId, param) + this.commandsData = res.data.rows || [] this.commandsPageOption.total = res.data.count || 0 + + setTimeout(() => { + this.$forceUpdate() + }, 0) } catch (error) { this.$Message.error('加载指令列表失败') this.commandsData = [] @@ -1020,6 +1054,27 @@ export default { } }, + // 重试指令 + async retryCommand(command) { + this.$Modal.confirm({ + title: '确认重试', + content: `确定要重试指令"${command.command_name}"吗?`, + onOk: async () => { + try { + await plaAccountServer.retryCommand(command.id) + this.$Message.success('重试指令成功') + // 刷新指令列表 + setTimeout(() => { + this.queryCommands(this.commandsPageOption.page) + }, 1000) + } catch (error) { + console.error('重试指令失败:', error) + this.$Message.error(error.message || '重试指令失败') + } + } + }) + }, + // 取消任务 async cancelTask(task) { this.$Modal.confirm({ diff --git a/admin/src/views/task/task_status.vue b/admin/src/views/task/task_status.vue index 4de7a03..9a57a5d 100644 --- a/admin/src/views/task/task_status.vue +++ b/admin/src/views/task/task_status.vue @@ -284,6 +284,7 @@ export default { try { const res = await taskStatusServer.getCommands(row.id) + this.commandsModal.data = res.data || [] } catch (error) { this.$Message.error('获取指令列表失败') diff --git a/api/controller_admin/pla_account.js b/api/controller_admin/pla_account.js index 371f13d..2ea882a 100644 --- a/api/controller_admin/pla_account.js +++ b/api/controller_admin/pla_account.js @@ -445,6 +445,35 @@ module.exports = { return ctx.success(commandDetail); }, + /** + * @swagger + * /admin_api/pla_account/retryCommand: + * post: + * summary: 重试指令 + * description: 重新执行失败的指令 + * tags: [后台-账号管理] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - commandId + * properties: + * commandId: + * type: integer + * description: 指令ID + * responses: + * 200: + * description: 重试成功 + */ + 'POST /pla_account/retryCommand': async (ctx) => { + const body = ctx.getBody(); + const result = await plaAccountService.retryCommand(body); + return ctx.success(result); + }, + /** * @swagger * /admin_api/pla_account/parseLocation: diff --git a/api/services/pla_account_service.js b/api/services/pla_account_service.js index 919c6b3..2ba5980 100644 --- a/api/services/pla_account_service.js +++ b/api/services/pla_account_service.js @@ -252,9 +252,12 @@ class PlaAccountService { order: [['id', 'DESC']] }); + // 将 Sequelize 模型实例转换为普通对象 + const rows = result.rows.map(row => row.get({ plain: true })); + return { count: result.count, - rows: result.rows + rows: rows }; } @@ -266,7 +269,9 @@ class PlaAccountService { async getAccountCommands(params) { const pla_account = db.getModel('pla_account'); const task_commands = db.getModel('task_commands'); + const task_status = db.getModel('task_status'); const Sequelize = require('sequelize'); + const Op = Sequelize.Op; const { id, limit, offset } = params; @@ -280,51 +285,66 @@ class PlaAccountService { throw new Error('账号不存在'); } - // 获取 sequelize 实例 - const sequelize = task_commands.sequelize; + // 先查询所有匹配 sn_code 的任务 ID + const tasks = await task_status.findAll({ + where: { + sn_code: account.sn_code + }, + attributes: ['id'] + }); - // 使用原生 SQL JOIN 查询 - const countSql = ` - SELECT COUNT(DISTINCT tc.id) as count - FROM task_commands tc - INNER JOIN task_status ts ON tc.task_id = ts.id - WHERE ts.sn_code = :sn_code - `; + const taskIds = tasks.map(task => task.id); - const dataSql = ` - SELECT tc.* - FROM task_commands tc - INNER JOIN task_status ts ON tc.task_id = ts.id - WHERE ts.sn_code = :sn_code - ORDER BY tc.id DESC - LIMIT :limit OFFSET :offset - `; + // 如果没有任务,直接返回空结果 + if (taskIds.length === 0) { + return { + count: 0, + rows: [] + }; + } - // 并行执行查询和计数 - const [countResult, dataResult] = await Promise.all([ - sequelize.query(countSql, { - replacements: { sn_code: account.sn_code }, - type: Sequelize.QueryTypes.SELECT - }), - sequelize.query(dataSql, { - replacements: { - sn_code: account.sn_code, - limit: limit, - offset: offset - }, - type: Sequelize.QueryTypes.SELECT - }) - ]); + // 使用 Sequelize 模型查询指令列表 + const result = await task_commands.findAndCountAll({ + where: { + task_id: { + [Op.in]: taskIds + } + }, + limit, + offset, + order: [['id', 'DESC']] + }); - const count = countResult[0]?.count || 0; - - // 将原始数据转换为 Sequelize 模型实例 - const rows = dataResult.map(row => { - return task_commands.build(row, { isNewRecord: false }); + // 将 Sequelize 模型实例转换为普通对象 + const rows = result.rows.map(row => { + const plainRow = row.get({ plain: true }); + + // 添加 create_time 字段(使用 start_time 或 createdAt) + if (!plainRow.create_time) { + plainRow.create_time = plainRow.start_time || plainRow.createdAt || null; + } + + // 解析 JSON 字段 + if (plainRow.command_params && typeof plainRow.command_params === 'string') { + try { + plainRow.command_params = JSON.parse(plainRow.command_params); + } catch (e) { + // 解析失败保持原样 + } + } + if (plainRow.result && typeof plainRow.result === 'string') { + try { + plainRow.result = JSON.parse(plainRow.result); + } catch (e) { + // 解析失败保持原样 + } + } + + return plainRow; }); return { - count: parseInt(count), + count: result.count, rows: rows }; } @@ -451,6 +471,100 @@ class PlaAccountService { return commandDetail; } + /** + * 重试指令 + * @param {Object} params - 重试参数 + * @param {number} params.commandId - 指令ID + * @returns {Promise} 重试结果 + */ + async retryCommand(params) { + const task_commands = db.getModel('task_commands'); + const task_status = db.getModel('task_status'); + const scheduleManager = require('../middleware/schedule/index.js'); + + const { commandId } = params; + + if (!commandId) { + throw new Error('指令ID不能为空'); + } + + // 查询指令信息 + const command = await task_commands.findByPk(commandId); + if (!command) { + throw new Error('指令不存在'); + } + + // 检查指令状态 + if (command.status !== 'failed') { + throw new Error('只能重试失败的指令'); + } + + // 获取任务信息 + const task = await task_status.findByPk(command.task_id); + if (!task) { + throw new Error('任务不存在'); + } + + // 获取账号信息 + const pla_account = db.getModel('pla_account'); + const account = await pla_account.findOne({ where: { sn_code: task.sn_code } }); + if (!account) { + throw new Error('账号不存在'); + } + + // 检查授权状态 + const authCheck = await authorizationService.checkAuthorization(account.id, 'id'); + if (!authCheck.is_authorized) { + throw new Error(authCheck.message); + } + + // 检查 MQTT 客户端 + if (!scheduleManager.mqttClient) { + throw new Error('MQTT客户端未初始化'); + } + + // 重置指令状态 + await command.update({ + status: 'pending', + error_message: null, + error_stack: null, + retry_count: (command.retry_count || 0) + 1, + start_time: null, + end_time: null, + duration: null, + result: null + }); + + // 解析指令参数 + let commandParams = {}; + if (command.command_params) { + try { + commandParams = typeof command.command_params === 'string' + ? JSON.parse(command.command_params) + : command.command_params; + } catch (error) { + console.warn('解析指令参数失败:', error); + } + } + + // 构建指令对象 + const commandObj = { + command_type: command.command_type, + command_name: command.command_name, + command_params: JSON.stringify(commandParams) + }; + + // 执行指令 + const result = await scheduleManager.command.executeCommand(task.id, commandObj, scheduleManager.mqttClient); + + return { + success: true, + message: '指令重试成功', + commandId: command.id, + result: result + }; + } + /** * 执行账号任务(旧接口兼容) * @param {Object} params - 任务参数