Browse Source

✨ feat: 全面移除旧版本乐跑

Pchen. 1 month ago
parent
commit
3bbd4fe636

+ 18 - 16
apis/Corn/UpdateState.js

@@ -1,8 +1,11 @@
 const API = require("../../lib/API.js")
 const db = require('../../plugin/DataBase/db.js')
-const axios = require('axios')
-const config = require('../../config.json')
 const { BaseStdResponse } = require("../../BaseStdResponse.js")
+const {
+    probeSetZone,
+    isProbeSetZoneOk,
+    getProbeFailMessage
+} = require('../../plugin/Lepao/runforgeSetZoneProbe')
 
 const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
 
@@ -13,8 +16,6 @@ class UpdateState extends API {
         this.noEncrypt()
         this.setPath('/Corn/UpdateState')
         this.setMethod('GET')
-
-        this.runpy = config.runpy
     }
 
     async onRequest(req, res) {
@@ -25,41 +26,42 @@ class UpdateState extends API {
 
             this.logger.info('开始更新乐跑账号登录状态')
 
-            const zoneUrl = this.runpy + '/set_zone'
-
-            let sql = `SELECT id, uid, token, school_id, name, student_num FROM lepao_account WHERE state = 0 AND auto_run = 1 AND token IS NOT NULL`
+            let sql = `SELECT id, uid, token, school_id, name, student_num, userAgent FROM lepao_account WHERE state = 0 AND auto_run = 1 AND token IS NOT NULL`
 
             let r = await db.query(sql)
             if (!r)
                 return this.logger.error('更新乐跑账号登录状态失败!')
 
             for (const item of r) {
-                const { name, student_num, token, uid, school_id } = item
+                const { name, student_num, token, uid, school_id, userAgent } = item
                 this.logger.info(`${name}(${student_num})开始更新乐跑登录状态`)
 
                 await sleep(2000)
 
                 try {
-                    const ossData = { uid, token, school_id, student_id: student_num, random_id: 1, run_end_time: 1 }
-                    const zoneRes = await axios.post(zoneUrl, ossData)
-                    const { data } = zoneRes
+                    const data = await probeSetZone({
+                        uid,
+                        token,
+                        school_id,
+                        student_num,
+                        random_id: 1,
+                        userAgent
+                    })
 
                     this.logger.info(`${student_num}更新乐跑登录状态返回结果: ${JSON.stringify(data)}`)
 
-                    if (!data || data.status != 1 || !data.data) {
+                    if (!isProbeSetZoneOk(data)) {
                         this.logger.info(`${name}(${student_num})数据获取失败,不更新`)
                         continue
                     }
 
-                    const sql = 'UPDATE lepao_account SET state = 1 WHERE student_num  = ?'
-                    await db.query(sql, [student_num])
+                    const updateSql = 'UPDATE lepao_account SET state = 1 WHERE student_num  = ?'
+                    await db.query(updateSql, [student_num])
 
                     this.logger.info(`${name}(${student_num})数据获取成果,状态更新为正常`)
                 } catch (err) {
                     this.logger.error(`${name}(${student_num})更新乐跑登录状态失败:${err.message || err}`)
                 }
-
-                
             }
 
             this.logger.info('更新乐跑账号登录状态完成')

+ 24 - 21
apis/Corn/UpdateStateAll.js

@@ -1,8 +1,11 @@
 const API = require("../../lib/API.js")
 const db = require('../../plugin/DataBase/db.js')
-const axios = require('axios')
-const config = require('../../config.json')
 const { BaseStdResponse } = require("../../BaseStdResponse.js")
+const {
+    probeSetZone,
+    isProbeSetZoneOk,
+    getProbeFailMessage
+} = require('../../plugin/Lepao/runforgeSetZoneProbe')
 
 const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
 
@@ -13,8 +16,6 @@ class UpdateStateAll extends API {
         this.noEncrypt()
         this.setPath('/Corn/UpdateStateAll')
         this.setMethod('GET')
-
-        this.runpy = config.runpy
     }
 
     async onRequest(req, res) {
@@ -25,48 +26,50 @@ class UpdateStateAll extends API {
 
             this.logger.info('开始更新乐跑账号登录状态')
 
-            const zoneUrl = this.runpy + '/set_zone'
-
-            let sql = `SELECT id, uid, token, school_id, name, student_num FROM lepao_account WHERE token IS NOT NULL`
+            let sql = `SELECT id, uid, token, school_id, name, student_num, userAgent FROM lepao_account WHERE token IS NOT NULL`
 
             let r = await db.query(sql)
             if (!r)
                 return this.logger.error('更新乐跑账号登录状态失败!')
 
             for (const item of r) {
-                const { name, student_num, token, uid, school_id } = item
+                const { name, student_num, token, uid, school_id, userAgent } = item
                 this.logger.info(`${name}(${student_num})开始更新乐跑登录状态`)
 
                 await sleep(2000)
 
                 try {
-                    const ossData = { uid, token, school_id, student_id: student_num, random_id: 1, run_end_time: 1 }
-                    const zoneRes = await axios.post(zoneUrl, ossData)
-                    const { data } = zoneRes
+                    const data = await probeSetZone({
+                        uid,
+                        token,
+                        school_id,
+                        student_num,
+                        random_id: 1,
+                        userAgent
+                    })
 
                     this.logger.info(`${student_num}更新乐跑登录状态返回结果: ${JSON.stringify(data)}`)
 
-                    if (!data || data.status != 1 || !data.data) {
-                        if (data?.info.includes('重新登录')) {
-                            const sql = 'UPDATE lepao_account SET state = 0 WHERE student_num  = ?'
-                            await db.query(sql, [student_num])
+                    if (!isProbeSetZoneOk(data)) {
+                        const msg = getProbeFailMessage(data)
+                        if (msg && msg.includes('重新登录')) {
+                            const failSql = 'UPDATE lepao_account SET state = 0 WHERE student_num  = ?'
+                            await db.query(failSql, [student_num])
                             this.logger.info(`${name}(${student_num})状态更新为待登录`)
+                        } else {
+                            this.logger.info(`${name}(${student_num})数据获取失败`)
                         }
 
-                        else this.logger.info(`${name}(${student_num})数据获取失败`)
-
                         continue
                     }
 
-                    const sql = 'UPDATE lepao_account SET state = 1 WHERE student_num  = ?'
-                    await db.query(sql, [student_num])
+                    const okSql = 'UPDATE lepao_account SET state = 1 WHERE student_num  = ?'
+                    await db.query(okSql, [student_num])
 
                     this.logger.info(`${name}(${student_num})状态更新为正常`)
                 } catch (err) {
                     this.logger.error(`${name}(${student_num})更新乐跑登录状态失败:${err.message || err}`)
                 }
-
-
             }
 
             this.logger.info('更新乐跑账号登录状态完成')

+ 2 - 2
apis/Lepao/Account/UpdateAccount/UpdateAccount.js

@@ -1,7 +1,7 @@
 const API = require("../../../../lib/API.js")
 const db = require("../../../../plugin/DataBase/db.js")
 const EmailTemplate = require('../../../../plugin/Email/emailTemplate.js')
-const lepao = require("../../../../lib/Lepao/Lepao.js").lepao
+const { enqueueLepaoStartRun } = require('../../../../plugin/mq/enqueueLepaoStartRun')
 const mq = require('../../../../plugin/mq')
 const { BaseStdResponse } = require("../../../../BaseStdResponse.js")
 const { dataDecrypt } = require('../../../../plugin/Lepao/Crypto')
@@ -134,7 +134,7 @@ class UpdateAccount extends API {
                 }
 
                 if (findRows[0].auto_run === 1 && Array.isArray(findRows[0].auto_day) && findRows[0].auto_day.includes(new Date().getDay())) {
-                    lepao.beginLepao(findRows[0].create_user, student_num, token, uid, school_id, 1)
+                    enqueueLepaoStartRun(student_num, this.logger)
                 }
             }
 

+ 2 - 2
apis/Lepao/Account/UpdateAccount/UpdateAccountAndroidApp.js

@@ -1,7 +1,7 @@
 const API = require("../../../../lib/API.js")
 const db = require("../../../../plugin/DataBase/db.js")
 const EmailTemplate = require('../../../../plugin/Email/emailTemplate.js')
-const lepao = require("../../../../lib/Lepao/Lepao.js").lepao
+const { enqueueLepaoStartRun } = require('../../../../plugin/mq/enqueueLepaoStartRun')
 const mq = require('../../../../plugin/mq')
 const { BaseStdResponse } = require("../../../../BaseStdResponse.js")
 const { dataDecrypt } = require('../../../../plugin/Lepao/Crypto')
@@ -136,7 +136,7 @@ class UpdateAccountAndroidApp extends API {
                 }
 
                 if (findRows[0].auto_run === 1 && Array.isArray(findRows[0].auto_day) && findRows[0].auto_day.includes(new Date().getDay())) {
-                    lepao.beginLepao(findRows[0].create_user, student_num, token, uid, school_id, 1)
+                    enqueueLepaoStartRun(student_num, this.logger)
                 }
             }
 

+ 2 - 2
apis/Lepao/Account/UpdateAccount/UpdateAccountiPhone.js

@@ -1,7 +1,7 @@
 const API = require("../../../../lib/API.js")
 const db = require("../../../../plugin/DataBase/db.js")
 const EmailTemplate = require('../../../../plugin/Email/emailTemplate.js')
-const lepao = require("../../../../lib/Lepao/Lepao.js").lepao
+const { enqueueLepaoStartRun } = require('../../../../plugin/mq/enqueueLepaoStartRun')
 const mq = require('../../../../plugin/mq')
 const { BaseStdResponse } = require("../../../../BaseStdResponse.js")
 const { dataDecrypt } = require('../../../../plugin/Lepao/Crypto')
@@ -138,7 +138,7 @@ class UpdateAccountiPhone extends API {
                 }
 
                 if (findRows[0].auto_run === 1 && Array.isArray(findRows[0].auto_day) && findRows[0].auto_day.includes(new Date().getDay())) {
-                    lepao.beginLepao(findRows[0].create_user, student_num, token, uid, school_id, 1)
+                    enqueueLepaoStartRun(student_num, this.logger)
                 }
             }
 

+ 0 - 3
apis/Lepao/Face/BeginFaceReco.js

@@ -3,15 +3,12 @@ const db = require("../../../plugin/DataBase/db.js")
 const axios = require('axios')
 const { v4: uuidv4 } = require('uuid')
 const Redis = require('../../../plugin/DataBase/Redis')
-const config = require('../../../config.json')
 const { BaseStdResponse } = require("../../../BaseStdResponse.js")
 
 class BeginFaceReco extends API {
     constructor() {
         super()
 
-        this.runpy = config.runpy
-
         this.setPath('/Face/BeginFaceReco')
         this.setMethod('POST')
     }

+ 0 - 512
lib/Lepao/Lepao.js

@@ -1,512 +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')
-const mq = require('../../plugin/mq')
-
-class Lepao {
-    constructor() {
-        this.logger = new Logger(path.join(__dirname, '../logs/Lepao.log'), 'INFO')
-        this.runpy = config.runpy
-
-        this.messageQueue = 'runforge_message_queue'
-    }
-
-    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.00
-            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, userAgent) {
-        try {
-            const reqData = { uid, token, school_id, student_id, userAgent }
-            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
-            }
-
-            let phoneSql = 'SELECT userAgent, deviceModel FROM lepao_account WHERE student_num = ?'
-            let phoneData = await db.query(phoneSql, [account])
-            let userAgent = (phoneData && phoneData.length > 0 && phoneData[0].userAgent) ? phoneData[0].userAgent : 'Mozilla/5.0 (Linux; Android 16; 2211133C Build/BP2A.250605.031.A3; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/138.0.7204.180 Mobile Safari/537.36 XWEB/1380347 MMWEBSDK/20250202 MMWEBID/1020 wxwork/5.0.6.66174 MicroMessenger/8.0.28.48(0x28001c30) MiniProgramEnv/android Luggage/3.0.2.95ef3f83 NetType/5G Language/zh_CN ABI/arm64'
-            let deviceModel = (phoneData && phoneData.length > 0 && phoneData[0].deviceModel) ? phoneData[0].deviceModel : '2211133C'
-
-            const ossData = { uid, token, school_id, student_id: account, random_id: path_id, run_end_time, userAgent }
-
-            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
-            }
-
-            // 临时修复3.12~3.18期间未入库记录,后续可删除
-            // await this.fixRecords(uuid, ossData)
-
-            // 上传 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,
-                userAgent,
-                deviceModel
-            }
-
-            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, userAgent)
-                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 
-                    a.name, 
-                    a.email, 
-                    a.target_count,
-                    a.notice_type,
-                    e.bot_umo 
-                FROM 
-                    lepao_account a
-                LEFT JOIN
-                    lepao_extra e
-                ON 
-                    a.student_num = e.student_num
-                WHERE
-                    a.student_num = ?
-            `
-
-            const rows = await db.query(emailSql, [account])
-            if (!rows || rows.length === 0) {
-                this.logger.error(`${account}查找用户信息失败`)
-                throw new Error('查找用户信息失败')
-            }
-
-            let data = {
-                ...lepaoData,
-                type: 'lepao_success',
-                umo: rows[0].bot_umo,
-                term_num: rows[0].target_count,
-                total_num,
-                name: rows[0].name,
-                account
-            }
-
-            if (rows[0].notice_type === 'bot' && rows[0].bot_umo) {
-                this.logger.info(`${account}发送乐跑成功Bot通知,UMO=${rows[0].bot_umo}`)
-                const ch = await mq.getChannel(this.messageQueue)
-
-                await ch.assertQueue(this.messageQueue, {
-                    durable: true
-                })
-
-                ch.sendToQueue(
-                    this.messageQueue,
-                    Buffer.from(JSON.stringify(data)),
-                    {
-                        persistent: true,
-                        contentType: 'application/json'
-                    }
-                )
-
-                this.logger.info(`${account}乐跑成功Bot通知发送完成`)
-            } else if (rows[0].notice_type === 'email' && rows[0].email) {
-                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}乐跑目标完成,发送乐跑结束通知并关闭自动乐跑`)
-
-                if (rows[0].notice_type === 'bot' && rows[0].bot_umo) {
-                    this.logger.info(`${account}发送乐跑完成Bot通知,UMO=${rows[0].bot_umo}`)
-                    const ch = await mq.getChannel(this.messageQueue)
-
-                    await ch.assertQueue(this.messageQueue, {
-                        durable: true
-                    })
-
-                    data.type = 'lepao_over'
-
-                    ch.sendToQueue(
-                        this.messageQueue,
-                        Buffer.from(JSON.stringify(data)),
-                        {
-                            persistent: true,
-                            contentType: 'application/json'
-                        }
-                    )
-
-                    this.logger.info(`${account}乐跑完成Bot通知发送完成`)
-                } else if (rows[0].notice_type === 'email' && rows[0].email) {
-                    await EmailTemplate.lepaoOver(rows[0].email, data)
-                    this.logger.info(`${account}乐跑完成邮件发送完成`)
-                }
-
-                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 
-                    a.name, 
-                    a.email, 
-                    a.target_count,
-                    a.notice_type,
-                    e.bot_umo 
-                FROM 
-                    lepao_account a
-                LEFT JOIN
-                    lepao_extra e
-                ON 
-                    a.student_num = e.student_num
-                WHERE
-                    a.student_num = ?
-            `
-            const rows = await db.query(emailSql, [account])
-            if (!rows || rows.length == 0) {
-                this.logger.error(`${account}查找用户邮箱失败`)
-                throw new Error('查找用户邮箱失败')
-            }
-
-            const data = {
-                type: 'lepao_fail',
-                umo: rows[0].bot_umo,
-                name: rows[0].name,
-                account,
-                reason: (reason === 'Request failed with status code 503' || reason === 'Request failed with status code 500') ? '系统繁忙,请稍后再试' : reason
-            }
-
-            if (rows[0].notice_type === 'bot' && rows[0].bot_umo) {
-                this.logger.info(`${account}发送乐跑失败Bot通知,UMO=${rows[0].bot_umo}`)
-                const ch = await mq.getChannel(this.messageQueue)
-
-                await ch.assertQueue(this.messageQueue, {
-                    durable: true
-                })
-
-                ch.sendToQueue(
-                    this.messageQueue,
-                    Buffer.from(JSON.stringify(data)),
-                    {
-                        persistent: true,
-                        contentType: 'application/json'
-                    }
-                )
-
-                this.logger.info(`${account}Bot通知发送完成`)
-            } else if (rows[0].notice_type === 'email' && rows[0].email) {
-                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}`)
-        }
-    }
-
-    // 修复3.12~3.18期间未入库记录
-    async fixRecords(uuid, reqData) {
-        try {
-            //已开始乐跑,存入Redis
-            const isFix = await Redis.get(`fixRecord:${reqData.student_id}`)
-            if (isFix) {
-                this.logger.info(`${reqData.student_id}已存在修复记录,跳过修复流程`)
-                return
-            }
-
-            await Redis.set(`fixRecord:${reqData.student_id}`, reqData.student_id)
-
-            const countUrl = this.runpy + '/get_term_record'
-
-            const recordRes = await axios.post(countUrl, reqData)
-            const { data } = recordRes
-            if (!data || !data.counts) {
-                this.logger.warn(`修复乐跑记录失败,接口返回异常`)
-                return
-            }
-
-            const count = data.counts || 0
-            let fixSql = 'UPDATE users SET lepao_count = lepao_count - ? WHERE uuid  = ?'
-            await db.query(fixSql, [count, uuid])
-            this.logger.info(`修复乐跑记录成功,扣除用户 ${uuid} 乐跑次数 ${count} 次`)
-
-        } catch (error) {
-            this.logger.error(`修复乐跑记录失败: ${error.stack || error.message}`)
-        }
-    }
-}
-
-const lepao = new Lepao()
-module.exports.lepao = lepao

+ 115 - 0
plugin/Lepao/runforgeSetZoneProbe.js

@@ -0,0 +1,115 @@
+/**
+ * 直连 RunForge 切换跑区(与 Worker lepao.setZone 一致),用于 token 探活,不经过 runpy。
+ */
+const axios = require('axios')
+const { URLSearchParams } = require('url')
+const db = require('../DataBase/db')
+const { dataEncrypt, dataDecrypt, dataSign } = require('./Crypto')
+
+const BASE_URL = 'https://lepao.ctbu.edu.cn/v3/api.php'
+
+const DEFAULT_UA =
+    'Mozilla/5.0 (Linux; Android 16; 2211133C Build/BP2A.250605.031.A3; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/138.0.7204.180 Mobile Safari/537.36 XWEB/1380347 MMWEBSDK/20250202 MMWEBID/1020 wxwork/5.0.6.66174 MicroMessenger/8.0.28.48(0x28001c30) MiniProgramEnv/android Luggage/3.0.2.95ef3f83 NetType/WIFI Language/zh_CN ABI/arm64'
+
+const runZoneMap = {
+    兰花湖校区跑区: 2,
+    主校区北跑区: 3,
+    主校区南跑区: 5,
+    重庆工商大学茶园校区: 6
+}
+
+function lepaoTimestamp() {
+    return Number((Date.now() / 1000).toFixed(3))
+}
+
+/**
+ * @param {object} p
+ * @param {string} p.uid
+ * @param {string} p.token
+ * @param {string|number} p.school_id
+ * @param {string} p.student_num
+ * @param {number} [p.random_id=1] path_data.id,需存在且 run_zone_name 可映射
+ * @param {string} [p.userAgent]
+ * @returns {Promise<object>} RunForge 响应体(与 Worker request 解密后形态一致)
+ */
+async function probeSetZone(p) {
+    const { uid, token, school_id, student_num, random_id = 1, userAgent } = p
+
+    const record = await db.query('SELECT run_zone_name FROM path_data WHERE id = ?', [random_id])
+    if (!record || record.length === 0) {
+        throw new Error('跑区不存在')
+    }
+
+    const runZoneId = runZoneMap[record[0].run_zone_name]
+    if (!runZoneId) throw new Error('跑区不存在')
+
+    const raw = {
+        uid,
+        token,
+        school_id,
+        term_id: 0,
+        course_id: 0,
+        class_id: 0,
+        student_num,
+        card_id: student_num,
+        timestamp: lepaoTimestamp(),
+        version: 1,
+        nonce: String(Math.floor(Math.random() * 900000 + 100000)),
+        ostype: 5,
+        run_zone_id: String(runZoneId)
+    }
+    raw.sign = dataSign(raw)
+
+    const form = new URLSearchParams()
+    form.append('ostype', '5')
+    form.append('data', dataEncrypt(JSON.stringify(raw)))
+
+    const res = await axios.post(`${BASE_URL}/Run/setRunZone`, form, {
+        headers: {
+            'Content-Type': 'application/x-www-form-urlencoded',
+            Accept: '*/*',
+            'Accept-Language': 'zh-CN,zh-Hans;q=0.9',
+            'User-Agent': userAgent || DEFAULT_UA,
+            Referer: 'https://servicewechat.com/wxf94c4ddb63d87ede/32/page-frame.html',
+            charset: 'utf-8'
+        },
+        timeout: 20000,
+        proxy: false
+    })
+
+    let result = res.data
+    if (result?.data && result?.is_encrypt === 1) {
+        result = { ...result, data: JSON.parse(dataDecrypt(result.data)) }
+    }
+    return result
+}
+
+function isProbeSetZoneOk(result) {
+    if (!result) return false
+    const hasData = result.data != null && result.data !== ''
+    if (!hasData) return false
+    if (Number(result.status) === 1) return true
+    const cd = Number(result.code)
+    if (cd === 1 || cd === 200) return true
+    return false
+}
+
+function getProbeFailMessage(result) {
+    if (!result) return ''
+    return (
+        result.info ||
+        result.msg ||
+        result.message ||
+        (typeof result.data === 'object' && result.data
+            ? result.data.info || result.data.msg || result.data.message
+            : '') ||
+        ''
+    )
+}
+
+module.exports = {
+    probeSetZone,
+    isProbeSetZoneOk,
+    getProbeFailMessage,
+    BASE_URL
+}

+ 26 - 0
plugin/mq/enqueueLepaoStartRun.js

@@ -0,0 +1,26 @@
+const mq = require('./index')
+const { assertRunforgeTaskIngress, publishRunforgeTask } = require('./runforgeTaskMq')
+
+/**
+ * 将乐跑任务写入 MQ,由 Worker 执行(与 SingleRun 一致)
+ */
+async function enqueueLepaoStartRun(studentNum, logger) {
+    try {
+        const channel = await mq.getChannel('lepao_account_autorun')
+        await assertRunforgeTaskIngress(channel, logger)
+        const taskId = `lepao:account:${Date.now()}:${studentNum}`
+        publishRunforgeTask(channel, {
+            id: taskId,
+            type: 'lepao.startRun',
+            data: {
+                taskId,
+                account: studentNum
+            },
+            retry: 0
+        })
+    } catch (e) {
+        logger?.error?.(`投递自动乐跑任务失败 ${studentNum}: ${e.message || e}`)
+    }
+}
+
+module.exports = { enqueueLepaoStartRun }