Browse Source

✨ feat: 新增陀螺仪数据模拟

Pchen. 1 month ago
parent
commit
e96263e1c2

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

@@ -3,6 +3,7 @@ const db = require("../../../../plugin/DataBase/db.js")
 const EmailTemplate = require('../../../../plugin/Email/emailTemplate.js')
 const EmailTemplate = require('../../../../plugin/Email/emailTemplate.js')
 const { enqueueLepaoStartRun } = require('../../../../plugin/mq/enqueueLepaoStartRun')
 const { enqueueLepaoStartRun } = require('../../../../plugin/mq/enqueueLepaoStartRun')
 const mq = require('../../../../plugin/mq')
 const mq = require('../../../../plugin/mq')
+const { mq: mqName } = require('../../../../plugin/mq/mqPrefix')
 const { BaseStdResponse } = require("../../../../BaseStdResponse.js")
 const { BaseStdResponse } = require("../../../../BaseStdResponse.js")
 const { dataDecrypt } = require('../../../../plugin/Lepao/Crypto')
 const { dataDecrypt } = require('../../../../plugin/Lepao/Crypto')
 
 
@@ -11,7 +12,7 @@ class UpdateAccount extends API {
     constructor() {
     constructor() {
         super()
         super()
 
 
-        this.messageQueue = 'runforge_message_queue'
+        this.messageQueue = mqName('runforge_message_queue')
 
 
         this.noEncrypt()
         this.noEncrypt()
         this.setPath('/Lepao/UpdateAccount')
         this.setPath('/Lepao/UpdateAccount')
@@ -111,7 +112,7 @@ class UpdateAccount extends API {
                 }
                 }
 
 
                 if (findRows[0].notice_type === 'bot' && findRows[0].bot_umo) {
                 if (findRows[0].notice_type === 'bot' && findRows[0].bot_umo) {
-                        this.logger.info(`${student_num}发送乐跑更新Bot通知,UMO=${rows[0].bot_umo}`)
+                    this.logger.info(`${student_num}发送乐跑更新Bot通知,UMO=${findRows[0].bot_umo}`)
                     const ch = await mq.getChannel(this.messageQueue)
                     const ch = await mq.getChannel(this.messageQueue)
 
 
                     await ch.assertQueue(this.messageQueue, {
                     await ch.assertQueue(this.messageQueue, {

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

@@ -3,6 +3,7 @@ const db = require("../../../../plugin/DataBase/db.js")
 const EmailTemplate = require('../../../../plugin/Email/emailTemplate.js')
 const EmailTemplate = require('../../../../plugin/Email/emailTemplate.js')
 const { enqueueLepaoStartRun } = require('../../../../plugin/mq/enqueueLepaoStartRun')
 const { enqueueLepaoStartRun } = require('../../../../plugin/mq/enqueueLepaoStartRun')
 const mq = require('../../../../plugin/mq')
 const mq = require('../../../../plugin/mq')
+const { mq: mqName } = require('../../../../plugin/mq/mqPrefix')
 const { BaseStdResponse } = require("../../../../BaseStdResponse.js")
 const { BaseStdResponse } = require("../../../../BaseStdResponse.js")
 const { dataDecrypt } = require('../../../../plugin/Lepao/Crypto')
 const { dataDecrypt } = require('../../../../plugin/Lepao/Crypto')
 
 
@@ -11,7 +12,7 @@ class UpdateAccountAndroidApp extends API {
     constructor() {
     constructor() {
         super()
         super()
 
 
-        this.messageQueue = 'runforge_message_queue'
+        this.messageQueue = mqName('runforge_message_queue')
 
 
         this.setPath('/Lepao/UpdateAccountAndroidApp')
         this.setPath('/Lepao/UpdateAccountAndroidApp')
         this.setMethod('POST')
         this.setMethod('POST')

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

@@ -3,6 +3,7 @@ const db = require("../../../../plugin/DataBase/db.js")
 const EmailTemplate = require('../../../../plugin/Email/emailTemplate.js')
 const EmailTemplate = require('../../../../plugin/Email/emailTemplate.js')
 const { enqueueLepaoStartRun } = require('../../../../plugin/mq/enqueueLepaoStartRun')
 const { enqueueLepaoStartRun } = require('../../../../plugin/mq/enqueueLepaoStartRun')
 const mq = require('../../../../plugin/mq')
 const mq = require('../../../../plugin/mq')
+const { mq: mqName } = require('../../../../plugin/mq/mqPrefix')
 const { BaseStdResponse } = require("../../../../BaseStdResponse.js")
 const { BaseStdResponse } = require("../../../../BaseStdResponse.js")
 const { dataDecrypt } = require('../../../../plugin/Lepao/Crypto')
 const { dataDecrypt } = require('../../../../plugin/Lepao/Crypto')
 
 
@@ -11,7 +12,7 @@ class UpdateAccountiPhone extends API {
     constructor() {
     constructor() {
         super()
         super()
 
 
-        this.messageQueue = 'runforge_message_queue'
+        this.messageQueue = mqName('runforge_message_queue')
         this.noEncrypt()
         this.noEncrypt()
         this.setPath('/Lepao/UpdateAccountiPhone')
         this.setPath('/Lepao/UpdateAccountiPhone')
         this.setMethod('POST')
         this.setMethod('POST')

+ 4 - 3
apis/Order/CreateOrder.js

@@ -7,8 +7,9 @@ const crypto = require('crypto')
 const axios = require('axios')
 const axios = require('axios')
 const config = require('../../config.json')
 const config = require('../../config.json')
 const mq = require('../../plugin/mq')
 const mq = require('../../plugin/mq')
+const { mq: mqName } = require('../../plugin/mq/mqPrefix')
 
 
-const ORDER_PAYMENT_QUEUE = 'order_payment_check'
+const ORDER_PAYMENT_QUEUE = mqName('order_payment_check')
 let orderPaymentWorkerStarted = false
 let orderPaymentWorkerStarted = false
 
 
 async function startOrderPaymentWorker(logger) {
 async function startOrderPaymentWorker(logger) {
@@ -251,14 +252,14 @@ class CreateOrder extends API {
                 // 下单成功后推送到订单支付检查队列,由当前模块的消费者进行轮询及超时取消
                 // 下单成功后推送到订单支付检查队列,由当前模块的消费者进行轮询及超时取消
                 try {
                 try {
                     const ch = await mq.getChannel('order_payment')
                     const ch = await mq.getChannel('order_payment')
-                    await ch.assertQueue('order_payment_check', {
+                    await ch.assertQueue(ORDER_PAYMENT_QUEUE, {
                         durable: true
                         durable: true
                     })
                     })
                     const msg = {
                     const msg = {
                         orderId,
                         orderId,
                         enqueueTime: Date.now()
                         enqueueTime: Date.now()
                     }
                     }
-                    ch.sendToQueue('order_payment_check', Buffer.from(JSON.stringify(msg)), {
+                    ch.sendToQueue(ORDER_PAYMENT_QUEUE, Buffer.from(JSON.stringify(msg)), {
                         persistent: true
                         persistent: true
                     })
                     })
                 } catch (error) {
                 } catch (error) {

+ 19 - 7
apis/User/Admin/GetQueueTasks.js

@@ -9,9 +9,11 @@ const {
     listPendingScheduledForAdmin,
     listPendingScheduledForAdmin,
     countPendingScheduled
     countPendingScheduled
 } = require('../../../plugin/mq/lepaoAutoScheduleRedis')
 } = require('../../../plugin/mq/lepaoAutoScheduleRedis')
+const { mq: mqPrefixName, PREFIX } = require('../../../plugin/mq/mqPrefix')
+const { TASK_QUEUE } = require('../../../plugin/mq/runforgeTaskMq')
 
 
-/** 允许通过管理接口查看的队列(防任意队列名探测) */
-const ALLOWED_QUEUES = [
+/** 未加前缀的队列基名(用于生成 ALLOWED_QUEUES;带 mqPrefix 时与生产隔离) */
+const QUEUE_BASE_NAMES = [
     'runforge_task_queue',
     'runforge_task_queue',
     'runforge_task_result_queue',
     'runforge_task_result_queue',
     'runforge_task_dead_queue',
     'runforge_task_dead_queue',
@@ -20,6 +22,16 @@ const ALLOWED_QUEUES = [
     'mq_health_check'
     'mq_health_check'
 ]
 ]
 
 
+/** 允许通过管理接口查看的队列(防任意队列名探测) */
+const ALLOWED_QUEUES = QUEUE_BASE_NAMES.map(mqPrefixName)
+
+function canonicalQueueName(q) {
+    const raw = q || 'runforge_task_queue'
+    if (ALLOWED_QUEUES.includes(raw)) return raw
+    if (PREFIX && QUEUE_BASE_NAMES.includes(raw)) return mqPrefixName(raw)
+    return raw
+}
+
 function parseAmqpHttpBase(amqpUrl) {
 function parseAmqpHttpBase(amqpUrl) {
     const u = new URL(String(amqpUrl).replace(/^amqp:/, 'http:'))
     const u = new URL(String(amqpUrl).replace(/^amqp:/, 'http:'))
     return {
     return {
@@ -199,7 +211,7 @@ class GetQueueTasks extends API {
                 })
                 })
             }
             }
 
 
-            const queueName = queue || 'runforge_task_queue'
+            const queueName = canonicalQueueName(queue)
             if (!ALLOWED_QUEUES.includes(queueName))
             if (!ALLOWED_QUEUES.includes(queueName))
                 return res.json({
                 return res.json({
                     ...BaseStdResponse.ERR,
                     ...BaseStdResponse.ERR,
@@ -240,11 +252,11 @@ class GetQueueTasks extends API {
             const wantScheduled =
             const wantScheduled =
                 includeScheduled !== '0' &&
                 includeScheduled !== '0' &&
                 includeScheduled !== 'false' &&
                 includeScheduled !== 'false' &&
-                queueName === 'runforge_task_queue'
+                queueName === TASK_QUEUE
 
 
             let autoRunScheduledMirror = null
             let autoRunScheduledMirror = null
             let pendingScheduledCount = null
             let pendingScheduledCount = null
-            if (queueName === 'runforge_task_queue') {
+            if (queueName === TASK_QUEUE) {
                 pendingScheduledCount = await countPendingScheduled(Date.now())
                 pendingScheduledCount = await countPendingScheduled(Date.now())
                 if (wantScheduled) {
                 if (wantScheduled) {
                     const slimit = Math.min(
                     const slimit = Math.min(
@@ -263,7 +275,7 @@ class GetQueueTasks extends API {
                 tasks,
                 tasks,
                 managementError,
                 managementError,
                 redisScheduler:
                 redisScheduler:
-                    queueName === 'runforge_task_queue'
+                    queueName === TASK_QUEUE
                         ? {
                         ? {
                               key: SCHEDULE_KEY,
                               key: SCHEDULE_KEY,
                               pendingCount: pendingScheduledCount
                               pendingCount: pendingScheduledCount
@@ -272,7 +284,7 @@ class GetQueueTasks extends API {
                 autoRunScheduledMirror,
                 autoRunScheduledMirror,
                 fetchedAt: Date.now()
                 fetchedAt: Date.now()
             }
             }
-            if (queueName === 'runforge_task_queue') {
+            if (queueName === TASK_QUEUE) {
                 detail.peekNote =
                 detail.peekNote =
                     'tasks:已在主队列中的消息;autoRunScheduledMirror:尚未到 fireAt、仍只在 Redis 中的调度(到期后由服务写入 MQ)。'
                     'tasks:已在主队列中的消息;autoRunScheduledMirror:尚未到 fireAt、仍只在 Redis 中的调度(到期后由服务写入 MQ)。'
             }
             }

+ 3 - 2
lib/Lepao/Mcp.js

@@ -2,13 +2,14 @@ const db = require('../../plugin/DataBase/db')
 const path = require('path')
 const path = require('path')
 const Logger = require('../Logger')
 const Logger = require('../Logger')
 const mq = require('../../plugin/mq')
 const mq = require('../../plugin/mq')
+const { mq: mqName } = require('../../plugin/mq/mqPrefix')
 const EmailTemplate = require('../../plugin/Email/emailTemplate')
 const EmailTemplate = require('../../plugin/Email/emailTemplate')
 
 
 class Mcp {
 class Mcp {
     constructor() {
     constructor() {
         this.logger = new Logger(path.join(__dirname, '../logs/MCP.log'), 'INFO')
         this.logger = new Logger(path.join(__dirname, '../logs/MCP.log'), 'INFO')
 
 
-        this.messageQueue = 'runforge_message_queue'
+        this.messageQueue = mqName('runforge_message_queue')
 
 
         this.auto_day = [
         this.auto_day = [
             { label: '周一', value: 1 },
             { label: '周一', value: 1 },
@@ -231,7 +232,7 @@ class Mcp {
             if (!rows || rows.length == 0) return '您尚未绑定乐跑账号,请先绑定'
             if (!rows || rows.length == 0) return '您尚未绑定乐跑账号,请先绑定'
 
 
             if (!rows[0].email) return '该账号还未设置邮箱,请先使用change_email设置邮箱'
             if (!rows[0].email) return '该账号还未设置邮箱,请先使用change_email设置邮箱'
-            if (rows[0].notice_type === 'mode') return `当前已是${mode}通知方式,无需修改`
+            if (rows[0].notice_type === mode) return `当前已是${mode}通知方式,无需修改`
 
 
             let insertSql = `
             let insertSql = `
                 UPDATE 
                 UPDATE 

+ 96 - 57
lib/Lepao/Worker.js

@@ -2,7 +2,8 @@ const path = require('path')
 const axios = require('axios')
 const axios = require('axios')
 const OSS = require('ali-oss')
 const OSS = require('ali-oss')
 const mq = require('../../plugin/mq')
 const mq = require('../../plugin/mq')
-const { assertRunforgeTaskIngress } = require('../../plugin/mq/runforgeTaskMq')
+const { mq: mqName } = require('../../plugin/mq/mqPrefix')
+const { assertRunforgeTaskIngress, TASK_QUEUE } = require('../../plugin/mq/runforgeTaskMq')
 const db = require('../../plugin/DataBase/db')
 const db = require('../../plugin/DataBase/db')
 const Redis = require('../../plugin/DataBase/Redis')
 const Redis = require('../../plugin/DataBase/Redis')
 const EmailTemplate = require('../../plugin/Email/emailTemplate')
 const EmailTemplate = require('../../plugin/Email/emailTemplate')
@@ -17,6 +18,7 @@ const {
     dataDecrypt,
     dataDecrypt,
     dataSign
     dataSign
 } = require('../../plugin/Lepao/Crypto')
 } = require('../../plugin/Lepao/Crypto')
+const generateGyrFromPath = require('../../plugin/Lepao/generateGyrFromPath')
 
 
 const Logger = require('../Logger')
 const Logger = require('../Logger')
 
 
@@ -32,10 +34,10 @@ class Worker {
 
 
         this.baseUrl = 'https://lepao.ctbu.edu.cn/v3/api.php'
         this.baseUrl = 'https://lepao.ctbu.edu.cn/v3/api.php'
 
 
-        this.taskQueue = 'runforge_task_queue'
-        this.resultQueue = 'runforge_task_result_queue'
-        this.deadQueue = 'runforge_task_dead_queue'
-        this.noticeQueue = 'runforge_message_queue'
+        this.taskQueue = TASK_QUEUE
+        this.resultQueue = mqName('runforge_task_result_queue')
+        this.deadQueue = mqName('runforge_task_dead_queue')
+        this.noticeQueue = mqName('runforge_message_queue')
 
 
         this.channelName = 'lepao_worker'
         this.channelName = 'lepao_worker'
 
 
@@ -426,7 +428,10 @@ class Worker {
             const maxPathRetry = 20  // 自动获取路径失败最大重试次数
             const maxPathRetry = 20  // 自动获取路径失败最大重试次数
             let pathRetry = 0
             let pathRetry = 0
             let pointData = null
             let pointData = null
+            let pathData = null
+            let newPathData = null
             let ossPath = null
             let ossPath = null
+            let ossSts = null
             let userData = null
             let userData = null
             let pathId = null
             let pathId = null
             let runZoneId = 0
             let runZoneId = 0
@@ -440,6 +445,7 @@ class Worker {
 
 
                 userData = await this.handlers['lepao.getUserData'](req, ctx)
                 userData = await this.handlers['lepao.getUserData'](req, ctx)
 
 
+
                 // 立刻合并账号凭证,保证后续任意 throw 时 finally 里 syncRunCount 不会用空 token 调 getRecord
                 // 立刻合并账号凭证,保证后续任意 throw 时 finally 里 syncRunCount 不会用空 token 调 getRecord
                 req = {
                 req = {
                     ...req,
                     ...req,
@@ -455,6 +461,16 @@ class Worker {
                 }
                 }
                 await Redis.set(progressKey, req.account, { EX: 1800 })
                 await Redis.set(progressKey, req.account, { EX: 1800 })
 
 
+                ossSts = await this.handlers['lepao.getOssSts'](req, ctx)
+                if (!ossSts?.bucket || !ossSts?.AccessKeyId || !ossSts?.AccessKeySecret || !ossSts?.SecurityToken) {
+                    throw new Error('获取 OSS 凭证失败,请联系客服或稍后再试')
+                }
+
+                req = {
+                    ...req,
+                    ossSts
+                }
+
                 // 晚上10点后提前
                 // 晚上10点后提前
                 let run_end_time = Math.floor(Date.now() / 1000) - 300 // 提前5分钟
                 let run_end_time = Math.floor(Date.now() / 1000) - 300 // 提前5分钟
                 let hour = new Date().getHours()
                 let hour = new Date().getHours()
@@ -482,14 +498,16 @@ class Worker {
                     try {
                     try {
                         // 2️⃣ 获取路径(仅路径选择失败时重试)
                         // 2️⃣ 获取路径(仅路径选择失败时重试)
                         const pathRes = await this.handlers['lepao.getPath'](req, ctx)
                         const pathRes = await this.handlers['lepao.getPath'](req, ctx)
-                        pathId = pathRes.path_id
+                        pathData = pathRes.pathData
+                        pathId = pathData?.id || null
+                        newPathData = getPathData(pathData.data, req.run_end_time, pathData.time)
 
 
                         // 3️⃣ 切换跑区
                         // 3️⃣ 切换跑区
-                        const zoneRes = await this.handlers['lepao.setZone']({ ...req, random_id: pathId }, ctx)
+                        const zoneRes = await this.handlers['lepao.setZone']({ ...req, pathData }, ctx)
                         runZoneId = zoneRes?.run_zone_id || 0
                         runZoneId = zoneRes?.run_zone_id || 0
 
 
                         // 4️⃣ 上传 OSS 文件、生成打卡点
                         // 4️⃣ 上传 OSS 文件、生成打卡点
-                        const uploadRes = await this.handlers['lepao.uploadOssFile']({ ...req, random_id: pathId }, ctx)
+                        const uploadRes = await this.handlers['lepao.uploadOssFile']({ ...req, pathData, newPathData }, ctx)
                         ossPath = uploadRes.oss_path
                         ossPath = uploadRes.oss_path
                         pointData = uploadRes.point_data
                         pointData = uploadRes.point_data
 
 
@@ -518,7 +536,7 @@ class Worker {
                 // 5️⃣ 提交跑步数据
                 // 5️⃣ 提交跑步数据
                 bindRes = await this.handlers['lepao.bindData']({
                 bindRes = await this.handlers['lepao.bindData']({
                     ...req,
                     ...req,
-                    random_id: pathId,
+                    pathData,
                     run_zone_id: runZoneId,
                     run_zone_id: runZoneId,
                     record_file: ossPath,
                     record_file: ossPath,
                     point_data: pointData
                     point_data: pointData
@@ -536,6 +554,14 @@ class Worker {
                 if (!runResult.ok) {
                 if (!runResult.ok) {
                     throw new Error(runResult.reason)
                     throw new Error(runResult.reason)
                 }
                 }
+                if (bindRes && bindRes.data && bindRes.data.record_id) {
+                    const gyrRes = await this.handlers['lepao.uploadGyrOssFile']({ ...req, newPathData, record_id: bindRes.data.record_id }, ctx)
+                    if (gyrRes?.status === 1) {
+                        this.logger.info(`${req.account}上传加速度数据成功!`)
+                    } else {
+                        this.logger.error(`${req.account}上传加速度数据失败!原因:${gyrRes.info || '未知错误'}`)
+                    }
+                }
 
 
                 // 同步乐跑次数(通知里要带 total_num / term_num,与 getRecord 一致)
                 // 同步乐跑次数(通知里要带 total_num / term_num,与 getRecord 一致)
                 const syncResult = await this.syncRunCount(req, ctx)
                 const syncResult = await this.syncRunCount(req, ctx)
@@ -585,19 +611,6 @@ class Worker {
                     }, { id: `${traceId}:notice:fail` })
                     }, { id: `${traceId}:notice:fail` })
                 }
                 }
 
 
-                // 将失败消息发送到结果队列或死信队列
-                if (ctx.channel) {
-                    await this.sendResult(ctx.channel, {
-                        id: req.taskId,
-                        success: false,
-                        error: err.message
-                    })
-                    await ctx.channel.sendToQueue(
-                        this.deadQueue,
-                        Buffer.from(JSON.stringify({ ...req, error: err.message })),
-                        { persistent: true }
-                    )
-                }
                 throw err
                 throw err
             } finally {
             } finally {
                 await Redis.del(`lepaoProgress:${req.account}`)
                 await Redis.del(`lepaoProgress:${req.account}`)
@@ -860,7 +873,7 @@ class Worker {
 
 
             this.logger.info(`${account}路径参数: area=${area ?? '随机'}, max_distance=${max}, min_distance=${min}`)
             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 > ? '
+            let pathSql = 'SELECT * FROM path_data WHERE state = 1 AND distance < ? AND distance > ? '
             const pathParams = [max, min]
             const pathParams = [max, min]
 
 
             if (area) {
             if (area) {
@@ -886,7 +899,7 @@ class Worker {
 
 
             this.logger.info(`${account}路径选中id=${randomPath.id},计数加1成功`)
             this.logger.info(`${account}路径选中id=${randomPath.id},计数加1成功`)
 
 
-            return { path_id: randomPath.id }
+            return { pathData: randomPath }
         })
         })
 
 
         /* ---------------- 获取跑步记录 ---------------- */
         /* ---------------- 获取跑步记录 ---------------- */
@@ -930,15 +943,9 @@ class Worker {
                 '重庆工商大学茶园校区': 6
                 '重庆工商大学茶园校区': 6
             }
             }
 
 
-            const record = await db.query(
-                'SELECT run_zone_name FROM path_data WHERE id = ?',
-                [req.random_id]
-            )
-            if (!record || record.length === 0) {
-                throw new Error('跑区不存在')
-            }
+            const { pathData } = req
 
 
-            const runZoneId = runZoneMap[record[0].run_zone_name]
+            const runZoneId = runZoneMap[pathData.run_zone_name]
             if (!runZoneId) throw new Error('跑区不存在')
             if (!runZoneId) throw new Error('跑区不存在')
 
 
             const raw = {
             const raw = {
@@ -997,17 +1004,9 @@ class Worker {
 
 
         /* ---------------- 上传 OSS 文件 ---------------- */
         /* ---------------- 上传 OSS 文件 ---------------- */
         this.register('lepao.uploadOssFile', async (req, ctx) => {
         this.register('lepao.uploadOssFile', async (req, ctx) => {
-            const pathRow = await db.query(
-                'SELECT * FROM path_data WHERE id=?',
-                [req.random_id]
-            )
-            if (!pathRow || pathRow.length === 0) {
-                throw new Error('路径数据不存在')
-            }
-            const pathData = pathRow[0]
+            const { pathData, newPathData, ossSts: sts } = req
 
 
             // 处理跑步路径
             // 处理跑步路径
-            const newPathData = getPathData(pathData.data, req.run_end_time, pathData.time)
             const pathResult = dataEncrypt(JSON.stringify(newPathData))
             const pathResult = dataEncrypt(JSON.stringify(newPathData))
 
 
             // 获取跑步规则参数
             // 获取跑步规则参数
@@ -1034,11 +1033,6 @@ class Worker {
                 throw err
                 throw err
             }
             }
 
 
-            const sts = await this.handlers['lepao.getOssSts'](req, ctx)
-            if (!sts?.bucket || !sts?.AccessKeyId || !sts?.AccessKeySecret || !sts?.SecurityToken) {
-                throw new Error('获取 OSS STS 失败')
-            }
-
             const now = new Date()
             const now = new Date()
             const yyyy = now.getFullYear()
             const yyyy = now.getFullYear()
             const mm = String(now.getMonth() + 1).padStart(2, '0')
             const mm = String(now.getMonth() + 1).padStart(2, '0')
@@ -1060,20 +1054,65 @@ class Worker {
             return { oss_path: ossPath, point_data: point_data }
             return { oss_path: ossPath, point_data: point_data }
         })
         })
 
 
-        /* ---------------- 提交跑步数据 ---------------- */
-        this.register('lepao.bindData', async (req, ctx) => {
-            if (req?.random_id === undefined || req?.random_id === null || req?.random_id === '') {
-                throw new Error('提交跑步数据失败:缺少 random_id')
+        this.register('lepao.uploadGyrOssFile', async (req, ctx) => {
+            const { newPathData, ossSts: sts, record_id } = req
+
+            // 生成加速度数据
+            const gyrData = generateGyrFromPath(newPathData)
+            if (!Array.isArray(gyrData) || gyrData.length === 0) {
+                this.logger.error('生成加速度数据失败')
+                return { status: 0, info: '生成加速度数据失败' }
             }
             }
 
 
-            const pathRow = await db.query(
-                'SELECT * FROM path_data WHERE id=?',
-                [req.random_id]
-            )
-            if (!pathRow || pathRow.length === 0) {
-                throw new Error(`提交跑步数据失败:未找到路径数据(random_id=${req.random_id})`)
+            const now = new Date()
+            const yyyy = now.getFullYear()
+            const mm = String(now.getMonth() + 1).padStart(2, '0')
+            const dd = String(now.getDate()).padStart(2, '0')
+            const formattedToday = `${yyyy}-${mm}-${dd}`
+            const boundary = String(Date.now())
+            const timestamp = String(Date.now())
+            const ossPath = `Public/Upload/file/run_gyroscope/${boundary.slice(-3)}/${formattedToday}/${timestamp}-${Math.floor(Math.random() * 150)}.txt`
+            const client = new OSS({
+                bucket: sts.bucket,
+                region: sts.region || 'oss-cn-hangzhou',
+                accessKeyId: sts.AccessKeyId,
+                accessKeySecret: sts.AccessKeySecret,
+                stsToken: sts.SecurityToken,
+                secure: true
+            })
+
+            await client.put(ossPath, Buffer.from(JSON.stringify(gyrData), 'utf-8'))
+
+            const data = {
+                uid: req.uid,
+                token: req.token,
+                school_id: req.school_id,
+                term_id: 0,
+                course_id: 0,
+                class_id: 0,
+                student_num: req.student_id,
+                card_id: req.student_id,
+                timestamp: this.lepaoTimestamp(),
+                version: 1,
+                nonce: String(Math.floor(Math.random() * 900000 + 100000)),
+                ostype: 5,
+                record_id: record_id,
+                gyroscope_file: ossPath
             }
             }
-            const pathData = pathRow[0]
+
+            data.sign = dataSign(data)
+
+            return this.request(
+                ctx.traceId,
+                'bindData',
+                this.api('/Run2/gyroscope'),
+                data
+            )
+        })
+
+        /* ---------------- 提交跑步数据 ---------------- */
+        this.register('lepao.bindData', async (req, ctx) => {
+            const { pathData } = req
 
 
             const distance = Number(Number(pathData.distance || 0).toFixed(2))
             const distance = Number(Number(pathData.distance || 0).toFixed(2))
             const stepData = generateCadence(distance, pathData.time)
             const stepData = generateCadence(distance, pathData.time)

+ 2 - 1
lib/Server.js

@@ -7,6 +7,7 @@ const Logger = require('./Logger')
 const MySQL = require('../plugin/DataBase/MySQL')
 const MySQL = require('../plugin/DataBase/MySQL')
 const Worker = require('./Lepao/Worker')
 const Worker = require('./Lepao/Worker')
 const mq = require('../plugin/mq')
 const mq = require('../plugin/mq')
+const { mq: mqName } = require('../plugin/mq/mqPrefix')
 const { startLepaoSchedulePublisher } = require('../plugin/mq/lepaoSchedulePublisher')
 const { startLepaoSchedulePublisher } = require('../plugin/mq/lepaoSchedulePublisher')
 
 
 class SERVER {
 class SERVER {
@@ -52,7 +53,7 @@ class SERVER {
             await mq.init()
             await mq.init()
             const ch = await mq.getChannel('health')
             const ch = await mq.getChannel('health')
 
 
-            await ch.assertQueue('mq_health_check', { durable: false })
+            await ch.assertQueue(mqName('mq_health_check'), { durable: false })
             this.logger.info('✅ RabbitMQ 初始化 & 测试成功')
             this.logger.info('✅ RabbitMQ 初始化 & 测试成功')
 
 
             const worker = new Worker()
             const worker = new Worker()

+ 228 - 0
plugin/Lepao/generateGyrFromPath.js

@@ -0,0 +1,228 @@
+"use strict";
+
+/**
+ * 传入 path_test.json 同类路径点数组,返回模拟真机上传的 IMU 采样数组(与小程序一致:仅最后一段;间隔约 60–70ms;各轴 float32 量化)。
+ * 单参数、无文件读写。
+ */
+module.exports = function generateGyrFromPath(pathRows) {
+  const R0 = 6371000;
+  const D2R = Math.PI / 180;
+
+  function parseRow(row) {
+    if (!row || typeof row.d !== "string") return null;
+    const [ts] = row.d.trim().split(/\s+/);
+    const t = parseInt(ts, 10);
+    if (!Number.isFinite(t)) return null;
+    const lat = row.a;
+    const lng = row.o;
+    if (typeof lat !== "number" || typeof lng !== "number") return null;
+    const speed = typeof row.s === "number" ? row.s : 0;
+    return { t, lat, lng, speed };
+  }
+
+  function sortPath(points) {
+    const list = points.map(parseRow).filter(Boolean);
+    list.sort((a, b) => a.t - b.t);
+    if (list.length < 2) return list;
+    const d = [list[0]];
+    for (let i = 1; i < list.length; i++) {
+      if (list[i].t !== list[i - 1].t) d.push(list[i]);
+    }
+    return d;
+  }
+
+  function hav(lat1, lng1, lat2, lng2) {
+    const φ1 = lat1 * D2R;
+    const φ2 = lat2 * D2R;
+    const dφ = (lat2 - lat1) * D2R;
+    const dλ = (lng2 - lng1) * D2R;
+    const s =
+      Math.sin(dφ / 2) ** 2 +
+      Math.cos(φ1) * Math.cos(φ2) * Math.sin(dλ / 2) ** 2;
+    return 2 * R0 * Math.asin(Math.min(1, Math.sqrt(s)));
+  }
+
+  function bear(lat1, lng1, lat2, lng2) {
+    const φ1 = lat1 * D2R;
+    const φ2 = lat2 * D2R;
+    const dλ = (lng2 - lng1) * D2R;
+    const y = Math.sin(dλ) * Math.cos(φ2);
+    const x =
+      Math.cos(φ1) * Math.sin(φ2) -
+      Math.sin(φ1) * Math.cos(φ2) * Math.cos(dλ);
+    return Math.atan2(y, x);
+  }
+
+  function unwrap(prev, next) {
+    let d = next - prev;
+    while (d > Math.PI) d -= 2 * Math.PI;
+    while (d < -Math.PI) d += 2 * Math.PI;
+    return prev + d;
+  }
+
+  function lerpPath(path, t, key) {
+    if (t <= path[0].t) return path[0][key];
+    const L = path[path.length - 1];
+    if (t >= L.t) return L[key];
+    let i = 0;
+    while (i < path.length - 1 && path[i + 1].t < t) i++;
+    const p0 = path[i];
+    const p1 = path[i + 1];
+    const f = (t - p0.t) / (p1.t - p0.t);
+    return p0[key] + f * (p1[key] - p0[key]);
+  }
+
+  function ll(path, t) {
+    return { lat: lerpPath(path, t, "lat"), lng: lerpPath(path, t, "lng") };
+  }
+
+  function segmentStartMs(t0, tEnd) {
+    const sec = Math.max(0, tEnd - t0) / 1000;
+    return t0 + Math.floor(sec / 60) * 60 * 1000;
+  }
+
+  /** 与真机常见的 60–72ms 间隔起伏一致(含 70→60 这类相邻差) */
+  function gapMs(i) {
+    const g = [70, 60, 65, 70, 60, 62, 68, 60, 72, 61];
+    return g[i % g.length];
+  }
+
+  function f32(x) {
+    return Math.fround(x);
+  }
+
+  function rngFactory(path) {
+    let h = 2166136261 >>> 0;
+    for (let i = 0; i < path.length; i++) {
+      const p = path[i];
+      h ^= p.t >>> 0;
+      h = Math.imul(h, 16777619);
+      const x = ((p.lat * 1e6) ^ (p.lng * 1e6)) | 0;
+      h ^= x;
+      h = Math.imul(h, 16777619);
+    }
+    let s = h || 0x9e3779b9;
+    return function () {
+      s = (s + 0x6d2b79f5) >>> 0;
+      let t = Math.imul(s ^ (s >>> 15), 1 | s);
+      t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
+      return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
+    };
+  }
+
+  if (!Array.isArray(pathRows) || pathRows.length < 2) return [];
+
+  const path = sortPath(pathRows);
+  if (path.length < 2) return [];
+
+  const rand = rngFactory(path);
+  const t0 = path[0].t;
+  const t1 = path[path.length - 1].t;
+  const tSeg = segmentStartMs(t0, t1);
+  const rngHalf = 35;
+
+  const pA0 = ll(path, tSeg);
+  const pA1 = ll(path, Math.min(t1, tSeg + Math.max(rngHalf, 1)));
+  let prevU = bear(pA0.lat, pA0.lng, pA1.lat, pA1.lng);
+  let prevV = lerpPath(path, tSeg, "speed");
+
+  const out = [];
+  let tCur = tSeg;
+  let idx = 0;
+  let lastT = tCur - gapMs(0);
+
+  /** 贴近真机样例的加计零偏量级(持机姿态下重力分摊 + 跑动微扰,非 9.8 单列) */
+  const baseAx = -1.08;
+  const baseAy = 0.9;
+  const baseAz = -0.06;
+  const amp = 0.36;
+
+  // 一阶低通,避免数值跳变过于“脚本化”
+  const lp = 0.82; // 越大越平滑
+  let lpAx = baseAx;
+  let lpAy = baseAy;
+  let lpAz = baseAz;
+  let lpGx = 0.43;
+  let lpGy = 0.42;
+  let lpGz = -1.78;
+
+  function clamp(x, lo, hi) {
+    return x < lo ? lo : x > hi ? hi : x;
+  }
+
+  while (tCur <= t1) {
+    const dtMs = tCur - lastT;
+    const dtSec = Math.max(dtMs, 1) / 1000;
+
+    const pb = ll(path, Math.max(t0, tCur - rngHalf));
+    const pf = ll(path, Math.min(t1, tCur + rngHalf));
+    const vPath = lerpPath(path, tCur, "speed");
+    const dist = hav(pb.lat, pb.lng, pf.lat, pf.lng);
+    const vTrack =
+      (2 * rngHalf) > 0 ? dist / ((2 * rngHalf) / 1000) : vPath;
+    const v = 0.62 * vPath + 0.38 * Math.max(0, vTrack);
+
+    const br = bear(pb.lat, pb.lng, pf.lat, pf.lng);
+    const uW = unwrap(prevU, br);
+    const yaw = (uW - prevU) / dtSec;
+    const aT = (v - prevV) / dtSec;
+    const aL = v * yaw;
+
+    prevU = uW;
+    prevV = v;
+    lastT = tCur;
+
+    const co = Math.cos(0.31);
+    const si = Math.sin(0.31);
+    const jx = aL * co - aT * si;
+    const jy = aL * si + aT * co;
+    const runSec = (tCur - t0) / 1000;
+    const ph = 2 * Math.PI * 1.82 * runSec;
+
+    let ax =
+      baseAx +
+      amp * jx +
+      0.2 * Math.sin(ph) +
+      0.1 * (rand() - 0.5);
+    let ay =
+      baseAy +
+      amp * jy +
+      0.17 * Math.cos(ph * 0.97) +
+      0.1 * (rand() - 0.5);
+    let az =
+      baseAz +
+      0.28 * Math.sin(2 * ph) +
+      0.18 * Math.cos(ph) +
+      0.08 * (rand() - 0.5);
+
+    let gx = 0.43 + 0.12 * Math.cos(ph) + 0.07 * (rand() - 0.5);
+    let gy = 0.42 + 0.26 * Math.sin(ph * 1.07) + 0.1 * (rand() - 0.5);
+    let gz = -1.78 + 0.8 * yaw + 0.14 * (rand() - 0.5);
+
+    // 去除极端尖峰注入,改为限幅 + 平滑,更贴近正常跑步统计特征
+    gx = clamp(gx, -2.2, 2.2);
+    gy = clamp(gy, -2.2, 2.2);
+    gz = clamp(gz, -3.2, 1.2);
+
+    lpAx = lp * lpAx + (1 - lp) * ax;
+    lpAy = lp * lpAy + (1 - lp) * ay;
+    lpAz = lp * lpAz + (1 - lp) * az;
+    lpGx = lp * lpGx + (1 - lp) * gx;
+    lpGy = lp * lpGy + (1 - lp) * gy;
+    lpGz = lp * lpGz + (1 - lp) * gz;
+
+    out.push({
+      t: tCur,
+      ax: f32(lpAx),
+      ay: f32(lpAy),
+      az: f32(lpAz),
+      gx: f32(lpGx),
+      gy: f32(lpGy),
+      gz: f32(lpGz),
+    });
+
+    tCur += gapMs(idx++);
+  }
+
+  return out;
+};

+ 3 - 2
plugin/mq/Worker.js

@@ -2,6 +2,7 @@ const db = require('../DataBase/db')
 const path = require('path')
 const path = require('path')
 const Logger = require('../../lib/Logger')
 const Logger = require('../../lib/Logger')
 const mq = require('.')
 const mq = require('.')
+const { mq: mqName } = require('./mqPrefix')
 
 
 class Worker {
 class Worker {
     constructor() {
     constructor() {
@@ -14,8 +15,8 @@ class Worker {
         this.running = false
         this.running = false
 
 
         // 队列名
         // 队列名
-        this.taskQueue = 'task_queue'
-        this.resultQueue = 'task_result_queue'
+        this.taskQueue = mqName('task_queue')
+        this.resultQueue = mqName('task_result_queue')
 
 
         // channel 名称(避免和别的模块冲突)
         // channel 名称(避免和别的模块冲突)
         this.channelName = 'worker_channel'
         this.channelName = 'worker_channel'

+ 7 - 5
plugin/mq/index.js

@@ -2,6 +2,7 @@ const amqp = require('amqplib')
 const path = require('path')
 const path = require('path')
 const config = require('../../config.json')
 const config = require('../../config.json')
 const Logger = require('../../lib/Logger')
 const Logger = require('../../lib/Logger')
+const { mq } = require('./mqPrefix')
 
 
 class MQManager {
 class MQManager {
     constructor() {
     constructor() {
@@ -57,16 +58,17 @@ class MQManager {
             await this.init()
             await this.init()
         }
         }
 
 
-        if (this.channels.has(name)) {
-            return this.channels.get(name)
+        const key = mq(name)
+        if (this.channels.has(key)) {
+            return this.channels.get(key)
         }
         }
 
 
         const channel = await this.connection.createChannel()
         const channel = await this.connection.createChannel()
-        this.channels.set(name, channel)
+        this.channels.set(key, channel)
 
 
         channel.on('close', () => {
         channel.on('close', () => {
-            this.logger.warn(`Channel [${name}] 已关闭`)
-            this.channels.delete(name)
+            this.logger.warn(`Channel [${key}] 已关闭`)
+            this.channels.delete(key)
         })
         })
 
 
         return channel
         return channel

+ 17 - 0
plugin/mq/mqPrefix.js

@@ -0,0 +1,17 @@
+const config = require('../../config.json')
+
+/**
+ * 与生产环境共享同一 RabbitMQ 时,可用此前缀隔离“连接侧 channel 缓存名”和真实队列名。
+ * 临时测试:在 config.json 的 rabbitmq.mqPrefix 设为 "test_";恢复生产设为空字符串或删除该字段。
+ */
+const PREFIX = String((config.rabbitmq && config.rabbitmq.mqPrefix) || '')
+
+function mq(name) {
+    if (name == null || name === '') return name
+    const s = String(name)
+    if (!PREFIX) return s
+    if (s.startsWith(PREFIX)) return s
+    return PREFIX + s
+}
+
+module.exports = { mq, PREFIX }