const axios = require('axios') const Redis = require('../../plugin/DataBase/Redis') const db = require('../../plugin/DataBase/db') const mq = require('../../plugin/mq') const Logger = require('../Logger') const path = require('path') const EmailTemplate = require('../../plugin/Email/emailTemplate') const { offsetGeoPoints, geoPointsToString, getCurrentTime } = require('./generatePath') const { sendStartLepao, sendStopLepao, lepaoUserInfo } = require('./lepaoAPI') class cgLepao { constructor() { this.logger = new Logger(path.join(__dirname, '../logs/cgLepao.log'), 'INFO') } // 获取跑步线路 async getPath(account) { this.logger.info(`${account}开始获取路径`) const accountSql = 'SELECT area, sex FROM lepao_account WHERE student_num = ?' const rows = await db.query(accountSql, [account]) if (!rows || rows.length === 0) { this.logger.error(`${account}无法获取账号数据`) throw new Error('无法获取账号数据') } const { area, sex } = rows[0] let max = 4.00 let min = 2.00 if (sex === 1) { max = 2.10 min = 1.60 } this.logger.info(`${account}路径参数: area=${area ?? '随机'}`) let pathSql = 'SELECT id, time FROM path_data WHERE state = 1 AND distance < ? AND distance > ? ' const pathParams = [max, min] if (area) { pathSql += ' AND run_zone_name = ?' pathParams.push(area) } pathSql += ' ORDER BY count ASC LIMIT 1' const paths = await db.query(pathSql, pathParams) if (!paths || paths.length === 0) { this.logger.error(`${account}未找到符合条件的路线`) throw new Error('未找到符合条件的路线,请改变路径选择条件') } const randomPath = paths[0] const updateSql = 'UPDATE path_data SET count = count + 1 WHERE id = ?' await db.query(updateSql, [randomPath.id]) this.logger.info(`${account}路径选中id=${randomPath.id},计数加1成功`) return { path_id: randomPath.id, path_time: randomPath.time, area } } async writeRedis(account) { try { // 计算至明日0时过期的秒数 const now = new Date() const tomorrow = new Date().setHours(24, 0, 0, 0) const exp = Math.floor((tomorrow - now) / 1000) await Redis.set(`cgLepaoSuccess:${account}`, account, { EX: exp }) } catch (error) { this.logger.error(`redis缓存乐跑记录失败: ${error.stack || '未知错误'}`) } } async lepaoFail(uuid) { try { this.logger.info(`返还用户 ${uuid} 乐跑次数`) let sql = 'UPDATE users SET lepao_count = lepao_count + 1 WHERE uuid = ?' await db.query(sql, [uuid]) this.logger.info(`返还用户 ${uuid} 乐跑次数成功`) } catch (error) { this.logger.error(`返还用户 ${uuid} 乐跑次数时出错: ${error.stack || error.message}`) } } async sendSuccessEmail(account, lepaoData, total_num) { try { this.logger.info(`${account}发送乐跑成功邮件`) const emailSql = 'SELECT name, email, target_count, auto_run FROM lepao_account WHERE student_num = ?' const rows = await db.query(emailSql, [account]) if (!rows || rows.length === 0) { this.logger.error(`${account}查找用户邮箱失败`) throw new Error('查找用户邮箱失败') } const data = { ...lepaoData, term_num: rows[0].target_count, total_num, name: rows[0].name, account } await EmailTemplate.lepaoSuccess(rows[0].email, data) this.logger.info(`${account}乐跑成功邮件发送完成`) if (rows[0].target_count !== 0 && total_num >= rows[0].target_count && rows[0].auto_run === 1) { this.logger.info(`${account}乐跑目标完成,发送乐跑结束邮件并关闭自动乐跑`) await EmailTemplate.lepaoOver(rows[0].email, data) let overSql = 'UPDATE lepao_account SET auto_run = 0 WHERE student_num = ?' let overRows = await db.query(overSql, [account]) if (!overRows || overRows.affectedRows !== 1) this.logger.warn(`${account}乐跑结束后关闭自动乐跑失败`) else this.logger.info(`${account}自动乐跑关闭成功`) } } catch (error) { this.logger.error(`发送成功邮件失败: ${error.stack || error.message}`) } } async sendFailEmail(account, reason) { try { this.logger.info(`${account}发送乐跑失败邮件,原因: ${reason}`) const emailSql = 'SELECT name, email FROM lepao_account WHERE student_num = ?' const rows = await db.query(emailSql, [account]) if (!rows || rows.length == 0) { this.logger.error(`${account}查找用户邮箱失败`) throw new Error('查找用户邮箱失败') } const data = { name: rows[0].name, account, reason: reason || '系统繁忙,请联系客服或稍后再试' } await EmailTemplate.lepaoFail(rows[0].email, data) this.logger.info(`${account}乐跑失败邮件发送完成`) } catch (error) { this.logger.error(`发送失败邮件失败: ${error.stack || error.message}`) } } async beginLepao(uuid, account) { try { this.logger.info(`${account}开始执行乐跑流程`) // 检查redis是否存在当天乐跑成功记录 const isSuccess = await Redis.get(`cgLepaoSuccess:${account}`) if (isSuccess) throw new Error('该账号当天已存在成功乐跑记录') const isProgress = await Redis.get(`cgLepaoProgress:${account}`) if (isProgress) throw new Error('该账号已进入乐跑任务队列,请等待乐跑完成后再进行乐跑操作') //已开始乐跑,存入Redis await Redis.set(`cgLepaoProgress:${account}`, account) // 扣除乐跑次数 this.logger.info(`${account}开始扣减乐跑次数`) const useLepaoCountSql = 'UPDATE users SET lepao_count = lepao_count - 1 WHERE uuid = ?' await db.query(useLepaoCountSql, [uuid]) this.logger.info(`${account}扣减乐跑次数完成`) const userPermissionSql = 'SELECT lepao_count FROM users WHERE uuid = ?' const userPermissionData = await db.query(userPermissionSql, [uuid]) if (!userPermissionData || userPermissionData.length !== 1) { this.logger.error(`${account}无法获取用户信息`) throw new Error('无法获取用户信息,请重试或联系RunForge客服') } if (userPermissionData[0].lepao_count < 1) { this.logger.warn(`${account}乐跑次数不足`) throw new Error('用户乐跑次数不足,请购买乐跑套餐!') } // 获取路径 ID const { path_id, path_time, area } = await this.getPath(account) // 提交乐跑请求 const { task_id, startTime } = await sendStartLepao(account) let taskSql = 'INSERT INTO lepao_record (uuid, lepao_account, task_id, path_id, startTime, time, area) VALUES (?, ?, ?, ?, ?, ?, ?)' let taskRows = await db.query(taskSql, [uuid, account, task_id, path_id, startTime, path_time, area]) if (!taskRows || taskRows.affectedRows === 0) { this.logger.error(`${account}乐跑任务入库失败`) throw new Error('乐跑任务发起失败,请联系客服或稍后再试') } let task = { uuid, account, task_id, path_id, path_time, startTime } let delayMs = path_time * 1000 const ch = await mq.getChannel('cg_run_task') ch.sendToQueue( 'cg_run_delay_queue', Buffer.from(JSON.stringify(task)), { expiration: delayMs.toString(), persistent: true } ) this.logger.info(`已投递跑步任务 user=${account} 延迟=${path_time}s`) } catch (error) { await this.lepaoFail(uuid) let failSql = 'UPDATE lepao_records SET state = 2 WHERE lepao_account = ? AND state = 0 ORDER BY id DESC LIMIT 1' await db.query(failSql, [account]) await this.sendFailEmail(account, error.message) await Redis.del(`cgLepaoProgress:${account}`) this.logger.error(`乐跑流程失败: ${error.stack || '未知错误'}`) } } async lepaoFinish() { const ch = await mq.getChannel('cg_run_finish') ch.prefetch(1) // 控制并发 await ch.consume('cg_run_finish_queue', async (msg) => { if (!msg) return const task = JSON.parse(msg.content.toString()) this.logger.info(`开始处理乐跑结束任务 user=${task.account} task_id=${task.task_id}`) try { const currentTime = getCurrentTime() let pathSql = 'SELECT * FROM path_data WHERE id = ?' const pathRows = await db.query(pathSql, [task.path_id]) if (!pathRows || pathRows.length === 0) { this.logger.error(`${task.account}乐跑路径数据获取失败 id=${task.path_id}`) throw new Error('乐跑路径数据获取失败,请联系客服或稍后再试') } let pathData = pathRows[0] let pathLine = offsetGeoPoints(pathData.data) let pathLineStr = geoPointsToString(pathLine) let lepaoData = { account: task.account, calorie: Number(pathData.calorie).toFixed(2), distance: Number(pathData.distance).toFixed(6), distribution: pathData.speed, duration: pathData.time, endTime: currentTime, id: task.task_id, maxDistribution: '0.00', pathLine: pathLineStr, startTime: task.startTime, } this.logger.info(`${task.account}乐跑数据构造结束:`) console.log(lepaoData) let lepaoResult = await sendStopLepao(lepaoData) if (lepaoResult.status !== 1) { this.logger.error(`${task.account}乐跑失败!`) let updateSql = 'UPDATE lepao_record SET startTime = ?, endTime = ?, frequency = ?, distance = ?, path_data = ?, state = 2 WHERE task_id = ?' await db.query(updateSql, [ lepaoResult.startTime, lepaoResult.endTime, lepaoResult.frequency, pathData.distance, pathLine, task.task_id ]) throw new Error('乐跑失败,请联系客服或稍后再试') } let updateSql = 'UPDATE lepao_record SET startTime = ?, endTime = ?, frequency = ?, distance = ?, path_data = ?, state = 1 WHERE task_id = ?' await db.query(updateSql, [ lepaoResult.startTime, lepaoResult.endTime, lepaoResult.frequency, pathData.distance, pathLine, task.task_id ]) this.logger.info(`${task.account}乐跑成功,获取账号信息`) let userInfo = await lepaoUserInfo(task.account) const { frequency } = userInfo lepaoResult.time = pathData.time lepaoResult.distance = Number(pathData.distance).toFixed(2) lepaoResult.area = pathData.run_zone_name await this.writeRedis(task.account) this.sendSuccessEmail(task.account, lepaoResult, frequency) } catch (error) { await this.lepaoFail(task.uuid) await this.sendFailEmail(task.account, error.message) this.logger.error(`乐跑结束处理失败: ${error.stack || '未知错误'}`) } finally { await Redis.del(`cgLepaoProgress:${task.account}`) ch.ack(msg) } }) this.logger.info('乐跑任务结束消费者启动完成') } } const Lepao = new cgLepao() module.exports.Lepao = Lepao