Browse Source

🐞 fix: 修复重工乐跑线路异常等问题

Pchen. 3 months ago
parent
commit
02c50c504f
4 changed files with 26 additions and 466 deletions
  1. 0 368
      lib/Lepao/Lepao.js
  2. 6 5
      lib/Lepao/cg_lepao.js
  3. 19 1
      lib/Lepao/generatePath.js
  4. 1 92
      plugin/Email/emailTemplate.js

+ 0 - 368
lib/Lepao/Lepao.js

@@ -1,368 +0,0 @@
-const axios = require('axios')
-const Redis = require('../../plugin/DataBase/Redis')
-const db = require('../../plugin/DataBase/db')
-const Logger = require('../Logger')
-const path = require('path')
-const EmailTemplate = require('../../plugin/Email/emailTemplate')
-const config = require('../../config.json')
-
-class Lepao {
-    constructor() {
-        this.logger = new Logger(path.join(__dirname, '../logs/Lepao.log'), 'INFO')
-        this.runpy = config.runpy
-    }
-
-    async getPath(account, vip) {
-        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 === 2) {
-            max = 2.10
-            min = 1.60
-        }
-
-        this.logger.info(`${account}路径参数: area=${area ?? '随机'}, max_distance=${max}, min_distance=${min}`)
-
-        let pathSql = 'SELECT id 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 randomPath.id
-    }
-
-    async getRecord(uid, token, school_id, student_id) {
-        try {
-            const reqData = { uid, token, school_id, student_id }
-            this.logger.info(`开始请求获取跑步次数 uid=${uid} student_id=${student_id}`)
-            const recordUrl = this.runpy + '/get_record'
-            let recordRes = await axios.post(recordUrl, reqData)
-            const { data } = recordRes
-            this.logger.info(`获取跑步次数返回结果: ${JSON.stringify(data)}`)
-            if (!data || data.status !== 1 || !data.data) {
-                this.logger.warn('获取剩余跑步次数失败,接口返回异常')
-                return
-            }
-            return data.data
-        } catch (error) {
-            this.logger.error(`获取跑步次数失败: ${error.stack || error.message}`)
-            return
-        }
-    }
-
-    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(`lepaoSuccess:${account}`, account, {
-                EX: exp
-            })
-        } catch (error) {
-            this.logger.error(`redis缓存乐跑记录失败: ${error.stack || '未知错误'}`)
-        }
-    }
-
-    async beginLepao(uuid, account, token, uid, school_id, state) {
-        try {
-            this.logger.info(`${account}开始执行乐跑流程`)
-
-            // 检查redis是否存在当天乐跑成功记录
-            const isSuccess = await Redis.get(`lepaoSuccess:${account}`)
-            if (isSuccess)
-                throw new Error('该账号当天已存在成功乐跑记录')
-            const isProgress = await Redis.get(`lepaoProgress:${account}`)
-            if (isProgress)
-                throw new Error('该账号已进入乐跑任务队列,请等待乐跑完成后再进行乐跑操作')
-
-            //已开始乐跑,存入Redis
-            await Redis.set(`lepaoProgress:${account}`, account, {
-                EX: 120
-            })
-
-            const userPermissionSql = 'SELECT vip, 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('用户乐跑次数不足,请购买乐跑套餐!')
-            }
-
-            if (state !== 1) {
-                this.logger.warn(`${account}登录状态异常 state=${state}`)
-                return this.sendFailEmail(account, '乐跑账号登录已过期,请尝试使用登录器重新登录')
-            }
-
-            // 获取路径 ID
-            const path_id = await this.getPath(account, userPermissionData[0].vip)
-
-            // 更换跑区
-            this.logger.info(`${account}开始更换跑区,path_id=${path_id}`)
-            const zoneUrl = this.runpy + '/set_zone'
-
-            // 晚上10点后提前
-            let run_end_time = Math.floor(Date.now() / 1000) - 300 // 提前5分钟
-            let hour = new Date().getHours()
-
-            if (hour < 7)
-                throw new Error('当前不在有效乐跑时间范围内。RunForge支持乐跑时间段为7:00~24:00')
-
-            if (hour >= 22) {
-                this.logger.info(`${account}当前时间为${hour}点,调整run_end_time提前5小时`)
-                run_end_time -= 18000
-            }
-
-            const ossData = { uid, token, school_id, student_id: account, random_id: path_id, run_end_time }
-
-            try {
-                const zoneRes = await axios.post(zoneUrl, ossData)
-                const { data } = zoneRes
-
-                this.logger.info(`${account}更换跑区返回结果: ${JSON.stringify(data)}`)
-
-                if (!data || data.status !== 1 || !data.data) {
-                    // 10.17更新,只有明确说明登录失效才会更新状态
-                    if (data && data.info && data.info.includes('请重新登录'))
-                        this.setStatusFail(account)
-                    throw new Error(data?.info || '系统繁忙,请联系客服或稍后再试')
-                }
-            } catch (error) {
-                this.logger.error(`${account}更换跑区失败: ${error.stack || error.message}`)
-                throw error
-            }
-
-            // 上传 OSS
-            this.logger.info(`${account}开始上传OSS记录`)
-            const ossUrl = this.runpy + '/upload_oss_file'
-            let oss_path, point_data
-
-            try {
-                const ossRes = await axios.post(ossUrl, ossData, {
-                    proxy: false
-                })
-
-                const { data } = ossRes
-                this.logger.info(`${account}上传OSS记录返回结果: ${JSON.stringify(data)}`)
-                if (!data || data.code !== 200 || !data.oss_path || !data.point_data) {
-                    if (data.code == -200) {
-                        this.logger.info(`${account}分配打卡点数量不足,重新执行乐跑流程`)
-                        return this.beginLepao(uuid, account, token, uid, school_id, state)
-                    }
-
-                    throw new Error('系统繁忙,请联系客服或稍后再试')
-                }
-                oss_path = data.oss_path
-                point_data = data.point_data
-                this.logger.info(`${account}上传OSS记录成功!oss_path:${oss_path}`)
-            } catch (error) {
-                // this.setStatusFail(account)
-                this.logger.error(`${account}上传OSS记录失败,请检查登录是否过期。${error.stack || error.message}`)
-                throw new Error('系统繁忙,请联系客服或稍后再试')
-            }
-
-            // 扣除乐跑次数
-            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 lepaoData = {
-                uid,
-                token,
-                school_id,
-                student_id: account,
-                random_id: path_id,
-                record_file: oss_path,
-                run_end_time,
-                point_data
-            }
-
-            this.logger.info(`${account}乐跑请求参数构造完成:`)
-            this.logger.info(JSON.stringify(lepaoData))
-
-            // 绑定乐跑数据
-            this.logger.info(`${account}开始绑定乐跑数据`)
-            const lepaoUrl = this.runpy + '/bind_data'
-            try {
-                const lepaoRes = await axios.post(lepaoUrl, lepaoData)
-                const { data } = lepaoRes
-                this.logger.info(`${account}绑定乐跑数据返回结果: ${JSON.stringify(data)}`)
-
-                if (!data || data.status !== 1 || !data.data) {
-                    // 10.17更新,只有明确说明登录失效才会更新状态
-                    if (data && data.info && data.info.includes('请重新登录'))
-                        this.setStatusFail(account)
-
-                    throw new Error(data?.info || '系统繁忙,请联系客服或稍后再试')
-                }
-
-                await this.addRecord(uuid, account, data.data, path_id, point_data)
-
-                // 获取剩余跑步次数
-                const recordData = await this.getRecord(uid, token, school_id, account)
-                this.logger.info(`${account}获取剩余跑步次数结果: ${JSON.stringify(recordData)}`)
-
-                let term_num = recordData?.term_num || 0
-                let total_num = recordData?.total_num || 30
-
-                if (data.data.record_failed_reason === '自动确认有效' || data.data.record_failed_reason === '') {
-                    // 成功记录存入Redis
-                    await this.writeRedis(account)
-                    await this.sendSuccessEmail(account, data.data, term_num, total_num)
-                } else {
-                    this.logger.warn(`${account}乐跑失败,原因: ${data.data.record_failed_reason}`)
-
-                    // 已存在记录也存redis
-                    if (data.data.record_failed_reason === '当天关联成绩次数已达到上限')
-                        await this.writeRedis(account)
-
-                    await this.sendFailEmail(account, data.data.record_failed_reason)
-                    await this.lepaoFail(uuid)
-                }
-
-                let recordSql = 'UPDATE lepao_account SET term_num = ?, total_num = ? WHERE student_num = ?'
-                let recordRows = await db.query(recordSql, [term_num, total_num, account])
-                if (!recordRows || recordRows.affectedRows !== 1)
-                    this.logger.warn(`${account}更新乐跑次数失败`)
-                else
-                    this.logger.info(`${account}更新乐跑次数成功 term_num=${term_num}, total_num=${total_num}`)
-            } catch (error) {
-                this.logger.error(`${account}绑定乐跑数据失败: ${error.stack || error.message}`)
-                await this.lepaoFail(uuid)
-                throw error
-            }
-        } catch (error) {
-            this.logger.error(`${account}乐跑流程异常: ${error.stack || error.message}`)
-            await this.sendFailEmail(account, error.message || '系统繁忙,请联系客服或稍后再试')
-        } finally {
-            await Redis.del(`lepaoProgress:${account}`)
-        }
-    }
-
-    async addRecord(uuid, account, result, path_id, point_data) {
-        try {
-            const time = Date.now()
-            this.logger.info(`${account}添加乐跑记录,path_id=${path_id}`)
-            const sql = 'INSERT INTO lepao_record (uuid, time, lepao_account, result, path_id, point_data) VALUES (?, ?, ?, ?, ?, ?)'
-            await db.query(sql, [uuid, time, account, result, path_id, point_data])
-            this.logger.info(`${account}添加乐跑记录成功`)
-        } catch (error) {
-            this.logger.error(`添加乐跑记录失败: ${error.stack || error.message}`)
-        }
-    }
-
-    async sendSuccessEmail(account, lepaoData, term_num, 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 === 'Request failed with status code 503' ? 'RunForge系统维护中,请稍后再试' : reason
-            }
-
-            await EmailTemplate.lepaoFail(rows[0].email, data)
-            this.logger.info(`${account}乐跑失败邮件发送完成`)
-        } catch (error) {
-            this.logger.error(`发送失败邮件失败: ${error.stack || error.message}`)
-        }
-    }
-
-    async lepaoFail(uuid) {
-        try {
-            this.logger.info(`返还用户 ${uuid} 乐跑次数`)
-            const 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 setStatusFail(account) {
-        try {
-            this.logger.info(`${account}设置账号为未启用`)
-            const sql = 'UPDATE lepao_account SET state = 0 WHERE student_num  = ?'
-            await db.query(sql, [account])
-            this.logger.info(`${account}账号状态设置为未启用成功`)
-        } catch (error) {
-            this.logger.error(`设置用户 ${account} state时出错: ${error.stack || error.message}`)
-        }
-    }
-}
-
-const lepao = new Lepao()
-module.exports.lepao = lepao

+ 6 - 5
lib/Lepao/cg_lepao.js

@@ -5,7 +5,7 @@ const mq = require('../../plugin/mq')
 const Logger = require('../Logger')
 const path = require('path')
 const EmailTemplate = require('../../plugin/Email/emailTemplate')
-const { offsetGeoPoints, geoPointsToString, addSeconds } = require('./generatePath')
+const { offsetGeoPoints, geoPointsToString, getCurrentTime } = require('./generatePath')
 const { sendStartLepao, sendStopLepao, lepaoUserInfo } = require('./lepaoAPI')
 
 class cgLepao {
@@ -33,7 +33,7 @@ class cgLepao {
             min = 1.60
         }
 
-        this.logger.info(`${account}路径参数: area=${area ?? '随机'}, max_distance=${max}, min_distance=${min}`)
+        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]
@@ -207,7 +207,7 @@ class cgLepao {
                 }
             )
 
-            this.logger.info(`已投递跑步任务 user=${account} 延迟=${delayMs}ms`)
+            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'
@@ -228,11 +228,12 @@ class cgLepao {
             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}`)
-                    ch.ack(msg)
                     throw new Error('乐跑路径数据获取失败,请联系客服或稍后再试')
                 }
                 let pathData = pathRows[0]
@@ -245,7 +246,7 @@ class cgLepao {
                     distance: Number(pathData.distance).toFixed(6),
                     distribution: pathData.speed,
                     duration: pathData.time,
-                    endTime: addSeconds(task.startTime, pathData.time),
+                    endTime: currentTime,
                     id: task.task_id,
                     maxDistribution: '0.00',
                     pathLine: pathLineStr,

+ 19 - 1
lib/Lepao/generatePath.js

@@ -37,7 +37,7 @@ export function geoPointsToString(points, fixed = 6) {
         .map(([lng, lat]) =>
             `${lat.toFixed(fixed)},${lng.toFixed(fixed)}`
         )
-        .join('') + ''
+        .join(';') + ';'
 }
 
 /**
@@ -77,3 +77,21 @@ export function addSeconds(startTime, secondsToAdd) {
     const pad = (n) => n.toString().padStart(2, '0')
     return `${year}-${pad(month)}-${pad(day)} ${pad(hour)}:${pad(minute)}:${pad(second)}`
 }
+
+/** 获取当前时间,格式 "YYYY-MM-DD HH:mm:ss"
+ * @returns {string}
+ */
+export function getCurrentTime() {
+    const now = new Date()
+
+    const pad = n => String(n).padStart(2, '0')
+
+    const year = now.getFullYear()
+    const month = pad(now.getMonth() + 1) // 月份从0开始
+    const day = pad(now.getDate())
+    const hour = pad(now.getHours())
+    const minute = pad(now.getMinutes())
+    const second = pad(now.getSeconds())
+
+    return `${year}-${month}-${day} ${hour}:${minute}:${second}`
+}

+ 1 - 92
plugin/Email/emailTemplate.js

@@ -456,7 +456,7 @@ class emailTemplate {
                 <p><strong>平均配速:</strong> ${this.calculatePace(data.time, data.distance)} 🐇</p>
                 <p><strong>跑步距离:</strong> ${Number(data.distance).toFixed(2)} Km 💕</p>
                 <p><strong>累计次数:</strong> ${data.total_num} 次 ✨</p>
-                <p><strong>剩余次数:</strong> ${data.term_num - data.total_num >= 0 ? (data.term_num - data.total_num) : '已完成'} 次 🎯</p>
+                <p><strong>剩余次数:</strong> ${data.term_num === data.total_num ? (data.term_num - data.total_num) : '已完成'} 次 🎯</p>
                 </div>
 
                 <p class="important">如果宝宝开启了自动乐跑,要记得不要在其他设备上登录“智慧体育”小程序哦 🚫📱,不然登录就会失效,要重新来一次啦~</p>
@@ -647,97 +647,6 @@ class emailTemplate {
         )
     }
 
-    async powerCheck(email, data) {
-        await sendEmail(email, '宿舍电费提醒',
-            `<html lang="zh-CN">
-            <head>
-                <meta charset="UTF-8">
-                <meta name="viewport" content="width=device-width, initial-scale=1.0">
-                <title>宿舍电费提醒</title>
-                <style>
-                    body {
-                        font-family: Arial, sans-serif;
-                        background-color: #f4f4f4;
-                        margin: 0;
-                        padding: 0;
-                    }
-
-                    .container {
-                        width: 80%;
-                        margin: 20px auto;
-                        background-color: #fff;
-                        padding: 20px;
-                        border-radius: 8px;
-                        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
-                    }
-
-                    .head {
-                        display: flex;
-                        justify-content: center;
-                        align-items: center;
-                        gap: 10px;
-                        color: #2c3e50;
-                    }
-
-                    p {
-                        font-size: 16px;
-                        color: #34495e;
-                        line-height: 1.6;
-                        text-indent: 2em;
-                    }
-
-                    .info {
-                        background-color: #ecf0f1;
-                        padding: 15px;
-                        border-radius: 5px;
-                        margin: 20px 0;
-                    }
-
-                    .info p {
-                        margin: 5px 0;
-                    }
-
-                    .important {
-                        color: #e74c3c;
-                        font-weight: bold;
-                    }
-
-                    .footer {
-                        font-size: 14px;
-                        text-align: center;
-                        color: #7f8c8d;
-                        margin-top: 50px;
-                    }
-                </style>
-            </head>
-
-            <body>
-            <div class="container">
-                <div class="head">
-                <h2>宿舍电费提醒</h2>
-                </div>
-
-                <p>尊敬的用户:</p>
-                <p>${data.building}${data.room}宿舍电费已低于预设提醒阈值,请留意:</p>
-
-                <div class="info">
-                <p><strong>校区:</strong> ${data.area}</p>
-                <p><strong>楼栋:</strong> ${data.building}</p>
-                <p><strong>寝室号:</strong> ${data.room}</p>
-                <p><strong>当前余额:</strong> ¥${data.now_balance}</p>
-                <p><strong>提醒阈值:</strong> ¥${data.lowest}</p>
-                <p><strong>扣费时间:</strong> ${data.now_change_time}</p>
-                </div>
-
-                <p class="important">当前电费已低于预设提醒阈值${Number(data.lowest - data.now_balance).toFixed(2)}元,请及时充值</p>
-                <p class="footer">Copyright © 2025 RunForge ✨</p>
-            </div>
-            </body>
-
-            </html>`
-        )
-    }
-
     // 时长计算
     formatSecondsToMinSec(totalSeconds) {
         const minutes = Math.floor(totalSeconds / 60);