|
|
@@ -2,10 +2,11 @@ const axios = require('axios')
|
|
|
const Redis = require('../../plugin/DataBase/Redis')
|
|
|
const db = require('../../plugin/DataBase/db')
|
|
|
const mq = require('../../plugin/mq')
|
|
|
-const path = require('path')
|
|
|
const Logger = require('../Logger')
|
|
|
const path = require('path')
|
|
|
const EmailTemplate = require('../../plugin/Email/emailTemplate')
|
|
|
+const { offsetGeoPoints, geoPointsToString, addSeconds } = require('./generatePath')
|
|
|
+const { sendStartLepao, sendStopLepao, lepaoUserInfo } = require('./lepaoAPI')
|
|
|
|
|
|
class cgLepao {
|
|
|
constructor() {
|
|
|
@@ -13,8 +14,297 @@ class cgLepao {
|
|
|
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 ?? '随机'}, max_distance=${max}, min_distance=${min}`)
|
|
|
+
|
|
|
+ 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 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) {
|
|
|
+ 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} 延迟=${delayMs}ms`)
|
|
|
+ } 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 {
|
|
|
+ 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}`)
|
|
|
+ ch.ack(msg)
|
|
|
+ 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: addSeconds(task.startTime, pathData.time),
|
|
|
+ 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
|
|
|
+
|
|
|
+ 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 cgLepao = new cgLepao()
|
|
|
-module.exports.cgLepao = cgLepao
|
|
|
+const Lepao = new cgLepao()
|
|
|
+module.exports.Lepao = Lepao
|