Browse Source

🎈 perf: 优化健康二师乐跑

Pchen. 1 month ago
parent
commit
0f160c63bb
100 changed files with 721 additions and 10847 deletions
  1. 1 1
      apis/Article/GetArticle.js
  2. 1 1
      apis/Article/GetArticleList.js
  3. 24 42
      apis/Corn/StartAutoLepao.js
  4. 8 35
      apis/Corn/StartLepao.js
  5. 6 22
      apis/Goods/Admin/AddGoods.js
  6. 0 3
      apis/Goods/Admin/GetGoods.js
  7. 0 1
      apis/Goods/Admin/GetGoodsList.js
  8. 5 2
      apis/Goods/GetCount.js
  9. 10 8
      apis/Goods/SendCount.js
  10. 0 34
      apis/JW/AdminGetAccount.js
  11. 2 1
      apis/Kefu/Order/Admin/GetOrderDetail.js
  12. 2 1
      apis/Kefu/Order/Admin/GetOrderList.js
  13. 2 14
      apis/Kefu/Order/Admin/ReplyOrder.js
  14. 0 64
      apis/Kefu/Order/Bot/AIReply.js
  15. 5 26
      apis/Kefu/Order/CreateOrder.js
  16. 14 9
      apis/Lepao/Account/AddAccount.js
  17. 2 2
      apis/Lepao/Account/Admin/ChangeLepaoCount.js
  18. 7 14
      apis/Lepao/Account/Admin/GetAccountList.js
  19. 0 61
      apis/Lepao/Account/Admin/UpdateAccountInfo.js
  20. 0 98
      apis/Lepao/BindAudit/Admin/ByAccount.js
  21. 0 134
      apis/Lepao/BindAudit/Admin/List.js
  22. 0 106
      apis/Lepao/BindAudit/MyList.js
  23. 2 1
      apis/Lepao/ChangeAutoRun.js
  24. 11 0
      apis/Lepao/GetJkesRunOverview.js
  25. 1 2
      apis/Lepao/Path/GetPathDetail.js
  26. 5 3
      apis/Lepao/Record/Admin/GetLepaoRecords.js
  27. 0 72
      apis/Lepao/Record/Admin/GetOfficialTermRecords.js
  28. 27 13
      apis/Lepao/Record/Admin/GetRecordDetail.js
  29. 4 2
      apis/Lepao/Record/GetLepaoRecords.js
  30. 0 72
      apis/Lepao/Record/GetOfficialTermRecords.js
  31. 26 10
      apis/Lepao/Record/GetRecordDetail.js
  32. 12 1
      apis/Lepao/SingleRun.js
  33. 0 17
      apis/MCP/userMcp/GetMcp.js
  34. 0 269
      apis/MCP/userMcp/McpRPC.js
  35. 0 17
      apis/MCP/workorderMcp/GetMcp.js
  36. 0 124
      apis/MCP/workorderMcp/McpRPC.js
  37. 4 7
      apis/Order/CallBack.js
  38. 1 1
      apis/Order/CreateOrder.js
  39. 0 42
      apis/Power/Admin/GetChangeRecord.js
  40. 0 129
      apis/Power/Admin/List.js
  41. 4 4
      apis/Public/GetAndroidAppVersion.js
  42. 2 2
      apis/Public/GetAppVersion.js
  43. 0 314
      apis/QK/Admin/Admin.js
  44. 0 30
      apis/QK/Batch/Batch.js
  45. 0 173
      apis/QK/Client/Client.js
  46. 0 162
      apis/QK/Task/Task.js
  47. 0 72
      apis/QuXuanShu/Admin/Detail.js
  48. 0 148
      apis/QuXuanShu/Admin/List.js
  49. 0 233
      apis/QuXuanShu/GetBookList.js
  50. 0 115
      apis/Upload/UploadFaceVideo.js
  51. 6 15
      apis/User/Admin/GetQueueTasks.js
  52. 4 41
      apis/User/Admin/GetUserList.js
  53. 2 2
      apis/User/BindEmail.js
  54. 1 8
      apis/User/GetPermissions.js
  55. 1 7
      apis/User/GetUserInfo.js
  56. 1 23
      apis/User/Login.js
  57. 0 6
      apis/User/qqLogin/qqLoginStep2.js
  58. 0 90
      apis/User/uniLogin/BindSocial.js
  59. 3 42
      apis/User/uniLogin/GetLoginUrl.js
  60. 47 65
      apis/User/uniLogin/Login.js
  61. 0 86
      apis/User/uniLogin/UnbindSocial.js
  62. 16 55
      config.example.json
  63. 0 65
      docs/lepao_feature_contract_20260430.md
  64. 2 41
      lib/API.js
  65. 5 257
      lib/AccessControl.js
  66. 0 50
      lib/Lepao/BindAudit.js
  67. 0 515
      lib/Lepao/Mcp.js
  68. 0 203
      lib/Lepao/WorkOrderMcp.js
  69. 109 31
      lib/Lepao/Worker.js
  70. 23 0
      lib/Lepao/billing.js
  71. 0 111
      lib/Lepao/lepaoOfficialApi.js
  72. 0 42
      lib/Lepao/lepaoSchoolHttp.js
  73. 0 121
      lib/Lepao/officialAccountAccess.js
  74. 0 311
      lib/Lepao/qgOssPut.js
  75. 0 66
      lib/Lepao/qgOutboundAxios.js
  76. 0 109
      lib/Lepao/syncAccountInfo.js
  77. 3 3
      lib/OrderRefundService.js
  78. 0 50
      lib/QK/LeaseWatcher.js
  79. 0 1843
      lib/QK/TaskScheduler.js
  80. 48 116
      lib/Server.js
  81. 0 40
      lib/UniLoginClient.js
  82. 0 100
      lib/UserSocialBinding.js
  83. 0 35
      lib/serverRole.js
  84. 0 2766
      package-lock.json
  85. 2 4
      package.json
  86. 8 14
      plugin/DataBase/Redis.js
  87. 37 32
      plugin/Email/Email.js
  88. 2 2
      plugin/Email/emailLayout.js
  89. 38 25
      plugin/Email/emailTemplate.js
  90. 0 228
      plugin/Lepao/generateGyrFromPath.js
  91. 0 31
      plugin/Lepao/toCoordinatePairs.js
  92. 0 443
      plugin/OneBot/OneBotV11.js
  93. 21 0
      plugin/jkes/autoRunAccountOptions.js
  94. 15 3
      plugin/jkes/formatRecordRow.js
  95. 44 0
      plugin/jkes/jkesHttp.js
  96. 0 1
      plugin/jkes/jkesSettings.js
  97. 13 4
      plugin/jkes/monthPolicy.js
  98. 1 1
      plugin/jkes/paceUtils.js
  99. 72 0
      plugin/jkes/prepareAutoRunTask.js
  100. 9 30
      plugin/jkes/request.js

+ 1 - 1
apis/Article/GetArticle.js

@@ -36,7 +36,7 @@ class GetArticle extends API {
                     a.time,
                     u.username AS author
                 FROM article a
-                LEFT JOIN users u ON a.author = u.uuid
+                LEFT JOIN users u ON a.author COLLATE utf8mb4_general_ci = u.uuid COLLATE utf8mb4_general_ci
                 WHERE a.state = 1 AND a.slug = ?
             `;
 

+ 1 - 1
apis/Article/GetArticleList.js

@@ -39,7 +39,7 @@ class GetArticleList extends API {
                 a.time,
                 u.username AS author
             FROM article a
-            LEFT JOIN users u ON a.author = u.uuid
+            LEFT JOIN users u ON a.author COLLATE utf8mb4_general_ci = u.uuid COLLATE utf8mb4_general_ci
             WHERE a.state = 1 AND a.type = ?
             ORDER BY a.id DESC
             LIMIT ? OFFSET ?

+ 24 - 42
apis/Corn/StartAutoLepao.js

@@ -1,14 +1,12 @@
 const API = require("../../lib/API");
 const db = require('../../plugin/DataBase/db')
-const Redis = require('../../plugin/DataBase/Redis')
 const mq = require('../../plugin/mq')
 const { assertRunforgeTaskIngress, publishRunforgeTask } = require('../../plugin/mq/runforgeTaskMq')
 const { scheduleDelayedRunforgeTask } = require('../../plugin/mq/lepaoAutoScheduleRedis')
-const jkesRedisKeys = require('../../plugin/jkes/redisKeys')
+const mqNames = require('../../plugin/mq/jkesMqNames')
 
 const { BaseStdResponse } = require("../../BaseStdResponse");
-const { planJkesAutoRun } = require('../../plugin/jkes/monthPolicy')
-const { buildAutoRunTaskExtrasFromAccountRow } = require('../../plugin/jkes/autoRunAccountOptions')
+const { prepareAutoRunTask, buildStartRunPayload } = require('../../plugin/jkes/prepareAutoRunTask')
 
 class StartAutoLepao extends API {
     constructor() {
@@ -19,6 +17,16 @@ class StartAutoLepao extends API {
         this.setMethod('GET');
     }
 
+    async publishAutoRun(channel, item, payload, logLabel) {
+        const { name, student_num } = item
+        try {
+            publishRunforgeTask(channel, payload)
+            this.logger.info(`${name}(${student_num})${logLabel}`)
+        } catch (err) {
+            this.logger.error(`${name}(${student_num})乐跑投递失败:${err.message || err}`)
+        }
+    }
+
     async onRequest(req, res) {
         try {
             res.json({
@@ -32,7 +40,7 @@ class StartAutoLepao extends API {
                         SELECT name, student_num, auto_day, token, target_count,
                                auto_run_distance_min_km, auto_run_distance_max_km, pace_min_sec_per_km, pace_max_sec_per_km
                         FROM lepao_account
-                        WHERE auto_run = 1
+                        WHERE auto_run = 1 AND state = 1
                             AND (auto_time = ? OR (auto_time = -1 AND today_auto_time = ?))
                             AND JSON_CONTAINS(auto_day, CAST(? AS JSON))
                         `
@@ -47,7 +55,7 @@ class StartAutoLepao extends API {
 
             let channel
             try {
-                channel = await mq.getChannel('lepao_corn')
+                channel = await mq.getChannel(mqNames.channelLepaoCorn)
                 await assertRunforgeTaskIngress(channel, this.logger)
             } catch (err) {
                 this.logger.error(`自动乐跑:连接 MQ 或声明拓扑失败:${err.message || err}`)
@@ -55,40 +63,18 @@ class StartAutoLepao extends API {
             }
 
             for (const item of r) {
-                const { name, student_num, auto_day, token, target_count } = item
-                const runExtras = buildAutoRunTaskExtrasFromAccountRow(item)
-
-                const isSuccess = await Redis.get(jkesRedisKeys.lepaoSuccess(student_num))
-                if (isSuccess) {
-                    this.logger.info(`${name}(${student_num})当天已乐跑成功,不执行自动乐跑,如仍需跑步请手动发起`)
-                    continue
-                }
-
-                const plan = await planJkesAutoRun(student_num, auto_day, token, {
-                    monthTargetKm: target_count,
-                    stopAfterMinimum: true
-                })
-                if (!plan.run) {
-                    this.logger.info(`${name}(${student_num}) JKES 自动乐跑跳过:${plan.reason}`)
+                const { name, student_num } = item
+                const prepared = await prepareAutoRunTask(item)
+                if (!prepared.run) {
+                    this.logger.info(`${name}(${student_num}) JKES 自动乐跑跳过:${prepared.reason}`)
                     continue
                 }
 
                 const delayMs = spreadWindowMs > 0 ? Math.floor(Math.random() * spreadWindowMs) : 0
                 const fireAt = nowMs + delayMs
-                const taskId = `lepao:auto:${fireAt}:${student_num}`
-                const payload = {
-                    id: taskId,
-                    type: 'lepao.startRun',
-                    data: {
-                        taskId,
-                        account: student_num,
-                        targetKm: runExtras.targetKm,
-                        autoDoubleSlot: runExtras.autoDoubleSlot,
-                        paceRandomMinSecPerKm: runExtras.paceRandomMinSecPerKm,
-                        paceRandomMaxSecPerKm: runExtras.paceRandomMaxSecPerKm
-                    },
-                    retry: 0
-                }
+                const payload = buildStartRunPayload(student_num, prepared.task, {
+                    taskId: `lepao:auto:${fireAt}:${student_num}`
+                })
 
                 if (delayMs > 0) {
                     try {
@@ -101,17 +87,13 @@ class StartAutoLepao extends API {
                             `${name}(${student_num})已写入 Redis 调度(约 ${Math.round(delayMs / 1000)}s 后进 MQ)`
                         )
                     } catch (err) {
-                        this.logger.error(`${name}(${student_num})Redis 调度失败:${err.message || err}`)
+                        this.logger.error(`${name}(${student_num})Redis 调度失败,改为立即投递:${err.message || err}`)
+                        await this.publishAutoRun(channel, item, payload, 'Redis 调度失败后已立即投递自动乐跑任务')
                     }
                     continue
                 }
 
-                try {
-                    publishRunforgeTask(channel, payload)
-                    this.logger.info(`${name}(${student_num})已投递自动乐跑任务`)
-                } catch (err) {
-                    this.logger.error(`${name}(${student_num})乐跑投递失败:${err.message || err}`)
-                }
+                await this.publishAutoRun(channel, item, payload, '已投递自动乐跑任务')
             }
         } catch (error) {
             this.logger.error(error)

+ 8 - 35
apis/Corn/StartLepao.js

@@ -1,15 +1,13 @@
 const API = require("../../lib/API");
 const db = require('../../plugin/DataBase/db')
-const Redis = require('../../plugin/DataBase/Redis')
 const mq = require('../../plugin/mq')
 const {
     assertRunforgeTaskIngress,
     publishRunforgeTask
 } = require('../../plugin/mq/runforgeTaskMq')
+const mqNames = require('../../plugin/mq/jkesMqNames')
 const { BaseStdResponse } = require("../../BaseStdResponse")
-const { planJkesAutoRun } = require('../../plugin/jkes/monthPolicy')
-const { buildAutoRunTaskExtrasFromAccountRow } = require('../../plugin/jkes/autoRunAccountOptions')
-const jkesRedisKeys = require('../../plugin/jkes/redisKeys')
+const { prepareAutoRunTask, buildStartRunPayload } = require('../../plugin/jkes/prepareAutoRunTask')
 
 class StartLepao extends API {
     constructor() {
@@ -49,44 +47,19 @@ class StartLepao extends API {
                 return this.logger.error('获取补充乐跑账号失败!')
 
             for (const item of r) {
-                const { name, student_num, auto_day, token, target_count } = item
-                const runExtras = buildAutoRunTaskExtrasFromAccountRow(item)
+                const { name, student_num } = item
                 this.logger.info(`${name}(${student_num})开始补充乐跑`)
 
-                const isSuccess = await Redis.get(jkesRedisKeys.lepaoSuccess(student_num))
-                if (isSuccess) {
-                    this.logger.info(`${name}(${student_num})当天已乐跑成功,不执行补充乐跑`)
-                    continue
-                }
-
-                const plan = await planJkesAutoRun(student_num, auto_day, token, {
-                    monthTargetKm: target_count,
-                    stopAfterMinimum: true
-                })
-                if (!plan.run) {
-                    this.logger.info(`${name}(${student_num}) JKES 补充乐跑跳过:${plan.reason}`)
+                const prepared = await prepareAutoRunTask(item)
+                if (!prepared.run) {
+                    this.logger.info(`${name}(${student_num}) JKES 补充乐跑跳过:${prepared.reason}`)
                     continue
                 }
 
                 try {
-                    const channel = await mq.getChannel('lepao_corn')
+                    const channel = await mq.getChannel(mqNames.channelLepaoCorn)
                     await assertRunforgeTaskIngress(channel, this.logger)
-
-                    const taskId = `lepao:repair:${Date.now()}:${student_num}`
-                    const payload = {
-                        id: taskId,
-                        type: 'lepao.startRun',
-                        data: {
-                            taskId,
-                            account: student_num,
-                            targetKm: runExtras.targetKm,
-                            autoDoubleSlot: runExtras.autoDoubleSlot,
-                            paceRandomMinSecPerKm: runExtras.paceRandomMinSecPerKm,
-                            paceRandomMaxSecPerKm: runExtras.paceRandomMaxSecPerKm
-                        },
-                        retry: 0
-                    }
-
+                    const payload = buildStartRunPayload(student_num, prepared.task, { prefix: 'lepao:repair' })
                     publishRunforgeTask(channel, payload)
                     this.logger.info(`${name}(${student_num})已投递补充乐跑任务`)
                 } catch (err) {

+ 6 - 22
apis/Goods/Admin/AddGoods.js

@@ -24,26 +24,10 @@ class AddProduct extends API {
             price,
             num,
             lepao_count,
-            ic_count,
-            icon,
-            description,
-            features
+            ic_count = 0
         } = req.body
 
-        const goodsIcon = (icon && String(icon).trim()) ? String(icon).trim().slice(0, 16) : '🏃'
-        const goodsDesc = description != null ? String(description).trim().slice(0, 200) : ''
-        let goodsFeatures = '[]'
-        if (features != null && features !== '') {
-            const raw = typeof features === 'string' ? features : JSON.stringify(features)
-            try {
-                const arr = JSON.parse(raw)
-                goodsFeatures = JSON.stringify(Array.isArray(arr) ? arr.map(s => String(s).trim()).filter(Boolean).slice(0, 6) : [])
-            } catch {
-                goodsFeatures = '[]'
-            }
-        }
-
-        if ([uuid, session, name, state, content, price, num, lepao_count, ic_count].some(value => value === '' || value === null || value === undefined))
+        if ([uuid, session, name, state, content, price, num, lepao_count].some(value => value === '' || value === null || value === undefined))
             return res.json({
                 ...BaseStdResponse.MISSING_PARAMETER
             })
@@ -65,11 +49,11 @@ class AddProduct extends API {
         const time = new Date().getTime()
 
         if (!id) {
-            sql = 'INSERT INTO goods (name, create_user, create_time, update_time, state, content, price, lepao_count, ic_count, num, icon, description, features) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
-            r = await db.query(sql, [name, uuid, time, time, state, content, price, lepao_count, ic_count, num, goodsIcon, goodsDesc, goodsFeatures])
+            sql = 'INSERT INTO goods (name, create_user, create_time, update_time, state, content, price, lepao_count, ic_count, num) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
+            r = await db.query(sql, [name, uuid, time, time, state, content, price, lepao_count, ic_count, num])
         } else {
-            sql = 'UPDATE goods SET name = ?, update_user = ?, update_time = ?, state = ?, content = ?, price = ?, lepao_count = ?, ic_count = ?, num = ?, icon = ?, description = ?, features = ? WHERE id = ?'
-            r = await db.query(sql, [name, uuid, time, state, content, price, lepao_count, ic_count, num, goodsIcon, goodsDesc, goodsFeatures, id])
+            sql = 'UPDATE goods SET name = ?, update_user = ?, update_time = ?, state = ?,  content = ?, price = ?, lepao_count = ?, ic_count = ?, num = ? WHERE id = ?'
+            r = await db.query(sql, [name, uuid, time, state, content, price, lepao_count, ic_count, num, id])
         }
 
         try {

+ 0 - 3
apis/Goods/Admin/GetGoods.js

@@ -48,9 +48,6 @@ class GetGoods extends API {
                 a.num,
                 a.ic_count,
                 a.lepao_count,
-                a.icon,
-                a.description,
-                a.features,
                 a.create_time,
                 a.update_time,
                 a.limit,

+ 0 - 1
apis/Goods/Admin/GetGoodsList.js

@@ -52,7 +52,6 @@ class GetGoodsList extends API {
                 a.num,
                 a.ic_count,
                 a.lepao_count,
-                a.icon,
                 a.views,
                 a.create_time,
                 a.update_time,

+ 5 - 2
apis/Goods/GetCount.js

@@ -28,7 +28,7 @@ class GetCount extends API {
                 ...BaseStdResponse.ACCESS_DENIED
             })
 
-        let sql = 'SELECT ic_count, lepao_count, vip FROM users WHERE uuid = ? '
+        let sql = 'SELECT lepao_count FROM users WHERE uuid = ? '
         let rows = await db.query(sql, [uuid])
 
         if (!rows || rows.length === 0)
@@ -37,9 +37,12 @@ class GetCount extends API {
                 msg: '获取用户信息失败!'
             })
 
+        const row = rows[0]
         res.json({
             ...BaseStdResponse.OK,
-            data: rows[0]
+            data: {
+                lepao_count: Number(row.lepao_count) || 0
+            }
         })
     }
 }

+ 10 - 8
apis/Goods/SendCount.js

@@ -17,11 +17,13 @@ class SendCount extends API {
         username = typeof username === "string" ? username.trim() : username
         count = Number(count)
 
+        count = Math.round(count * 100) / 100
+
         if ([uuid, session, username, count].some(v => v == null || v === "" || Number.isNaN(count)))
             return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
 
-        if (!Number.isInteger(count) || count < 1 || count > 9999)
-            return res.json({ ...BaseStdResponse.ERR, msg: "超出赠送的次数范围,请重新选择赠送次数" })
+        if (count <= 0 || count > 9999)
+            return res.json({ ...BaseStdResponse.ERR, msg: "超出赠送的里程范围,请重新选择赠送公里数" })
 
         if (!(await AccessControl.checkSession(uuid, session)))
             return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
@@ -50,7 +52,7 @@ class SendCount extends API {
             }
             if (targetRows[0].uuid === uuid) {
                 await conn.rollback()
-                return res.json({ ...BaseStdResponse.ERR, msg: "不能给自己赠送次数!" })
+                return res.json({ ...BaseStdResponse.ERR, msg: "不能给自己赠送里程!" })
             }
 
             const [decResult] = await conn.execute(
@@ -59,7 +61,7 @@ class SendCount extends API {
             )
             if (decResult.affectedRows !== 1) {
                 await conn.rollback()
-                return res.json({ ...BaseStdResponse.ERR, msg: "剩余乐跑次数不足,请购买后再赠送!" })
+                return res.json({ ...BaseStdResponse.ERR, msg: "剩余乐跑里程不足,请购买后再赠送!" })
             }
 
             const senderLepaoBefore = Number(senderRows[0].lepao_count || 0)
@@ -106,7 +108,7 @@ class SendCount extends API {
                     balanceAfter: senderLepaoBefore - count,
                     bizType: "gift_send_lock",
                     bizId: `send_request:${requestId}`,
-                    remark: `向${username}赠送${count}`
+                    remark: `向${username}赠送${count}公里`
                 })
                 await insertLedgerRecord({
                     executor: conn,
@@ -117,7 +119,7 @@ class SendCount extends API {
                     bizType: "gift_receive",
                     bizId: `send_request:${requestId}`,
                     operatorUuid: null,
-                    remark: `${senderRows[0].username}赠送${count}`
+                    remark: `${senderRows[0].username}赠送${count}公里`
                 })
 
                 await conn.commit()
@@ -209,10 +211,10 @@ class SendCount extends API {
 
         } catch (err) {
             try { await conn.rollback() } catch (_) { }
-            this.logger.error(`赠送乐跑次数失败!${err.message || "未知错误"}`)
+            this.logger.error(`赠送乐跑里程失败!${err.message || "未知错误"}`)
             return res.json({
                 ...BaseStdResponse.ERR,
-                msg: `赠送次数失败,请稍后再试!`
+                msg: `赠送里程失败,请稍后再试!`
             })
         } finally {
             if (conn?.connection && typeof conn.connection.release === 'function' && typeof conn?.release === 'function') {

+ 0 - 34
apis/JW/AdminGetAccount.js

@@ -1,34 +0,0 @@
-const API = require('../../lib/API.js')
-const db = require('../../plugin/DataBase/db.js')
-const { BaseStdResponse } = require('../../BaseStdResponse.js')
-const AccessControl = require('../../lib/AccessControl')
-
-class AdminGetAccount extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/JW/Account')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        const { uuid, session, create_user } = req.query
-
-        if ([uuid, session, create_user].some(value => value === '' || value === null || value === undefined)) {
-            return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-        }
-
-        if (!await AccessControl.checkSession(uuid, session)) {
-            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
-        }
-
-        try {
-            const rows = await AccessControl.listVerifiedJwAccounts(create_user)
-            return res.json({ ...BaseStdResponse.OK, data: rows })
-        } catch (err) {
-            this.logger.error(`管理员获取统一身份认证账号失败:${err.stack || err}`)
-            return res.json({ ...BaseStdResponse.ERR, msg: '获取统一身份认证账号失败' })
-        }
-    }
-}
-
-module.exports.AdminGetAccount = AdminGetAccount

+ 2 - 1
apis/Kefu/Order/Admin/GetOrderDetail.js

@@ -25,7 +25,8 @@ class GetOrderDetail extends API {
             })
 
         // 检查权限
-        if (!await AccessControl.canAccess(uuid, ['page.admin.service.orderList']))
+        let permission = await AccessControl.getPermission(uuid)
+        if (!permission.includes("admin") && !permission.includes("service"))
             return res.json({
                 ...BaseStdResponse.PERMISSION_DENIED
             })

+ 2 - 1
apis/Kefu/Order/Admin/GetOrderList.js

@@ -41,7 +41,8 @@ class GetOrderList extends API {
             })
 
         // 检查权限
-        if (!await AccessControl.canAccess(uuid, ['page.admin.service.orderList']))
+        let permission = await AccessControl.getPermission(uuid)
+        if (!permission.includes("admin") && !permission.includes("service"))
             return res.json({
                 ...BaseStdResponse.PERMISSION_DENIED
             })

+ 2 - 14
apis/Kefu/Order/Admin/ReplyOrder.js

@@ -3,7 +3,6 @@ const db = require("../../../../plugin/DataBase/db.js");
 const { BaseStdResponse } = require("../../../../BaseStdResponse.js");
 const AccessControl = require("../../../../lib/AccessControl.js");
 const EmailTemplate = require('../../../../plugin/Email/emailTemplate')
-const OneBotV11 = require('../../../../plugin/OneBot/OneBotV11')
 
 class ReplyOrder extends API {
     constructor() {
@@ -27,7 +26,8 @@ class ReplyOrder extends API {
             })
 
         // 检查权限
-        if (!await AccessControl.canAccess(uuid, ['action.service.replyOrder']))
+        let permission = await AccessControl.getPermission(uuid)
+        if (!permission.includes("admin") && !permission.includes("service"))
             return res.json({
                 ...BaseStdResponse.PERMISSION_DENIED
             })
@@ -67,18 +67,6 @@ class ReplyOrder extends API {
                 if (selectRows[0].email) {
                     await EmailTemplate.orderNewReply(selectRows[0].email, { id, content, files })
                 }
-
-                // try {
-                //     await OneBotV11.sendOrderMessage({
-                //         orderId: id,
-                //         role: '客服',
-                //         senderUuid: uuid,
-                //         content,
-                //         files: files ?? []
-                //     })
-                // } catch (botErr) {
-                //     this.logger.error(`OneBot v11 转发客服回复失败:${botErr.stack}`)
-                // }
             } else {
                 return res.json({ ...BaseStdResponse.ERR, msg: '回复工单失败!数据库错误' })
             }

+ 0 - 64
apis/Kefu/Order/Bot/AIReply.js

@@ -1,64 +0,0 @@
-const API = require("../../../../lib/API.js");
-const db = require("../../../../plugin/DataBase/db.js");
-const { BaseStdResponse } = require("../../../../BaseStdResponse.js");
-const OneBotV11 = require("../../../../plugin/OneBot/OneBotV11.js");
-
-class AIReply extends API {
-    constructor() {
-        super();
-        this.setPath('/Kefu/Order/Bot/AIReply')
-        this.setMethod('POST')
-        this.noEncrypt()
-    }
-
-    async onRequest(req, res) {
-        const onebot = OneBotV11.getOneBotConfig()
-        if (!onebot.enabled) {
-            return res.json({ ...BaseStdResponse.ERR, msg: 'OneBot v11 未启用' })
-        }
-
-        const token = req.headers['x-onebot-access-token'] || req.headers['authorization'] || ''
-        if (onebot.callbackToken && !String(token).includes(onebot.callbackToken)) {
-            return res.status(401).json({
-                ...BaseStdResponse.ACCESS_DENIED,
-                msg: 'AI 回调 token 校验失败'
-            })
-        }
-
-        const { id, content } = req.body || {}
-        if ([id, content].some(value => value === '' || value === null || value === undefined)) {
-            return res.json({
-                ...BaseStdResponse.MISSING_PARAMETER,
-                msg: '缺少 id 或 content'
-            })
-        }
-
-        const selectSql = 'SELECT msg, state FROM work_order WHERE id = ?'
-        const rows = await db.query(selectSql, [id])
-        if (!rows || rows.length !== 1 || rows[0].state === 2) {
-            return res.json({
-                ...BaseStdResponse.ERR,
-                msg: '工单不存在或已关闭'
-            })
-        }
-
-        const now = Date.now()
-        const msg = rows[0].msg || []
-        msg.push({
-            time: now,
-            content: String(content).trim(),
-            uuid: 'e4fe0277-0b1a-41a1-b25f-8b6e4cec3281',
-            type: 'ai',
-            files: []
-        })
-
-        const updateSql = 'UPDATE work_order SET msg = ?, update_time = ?, state = 3 WHERE id = ?'
-        await db.query(updateSql, [msg, now, id])
-
-        return res.json({
-            ...BaseStdResponse.OK
-        })
-    }
-}
-
-module.exports.AIReply = AIReply

+ 5 - 26
apis/Kefu/Order/CreateOrder.js

@@ -3,7 +3,6 @@ const db = require("../../../plugin/DataBase/db.js");
 const { BaseStdResponse } = require("../../../BaseStdResponse.js");
 const AccessControl = require("../../../lib/AccessControl.js");
 const EmailTemplate = require('../../../plugin/Email/emailTemplate')
-const OneBotV11 = require('../../../plugin/OneBot/OneBotV11')
 
 class CreateOrder extends API {
     constructor() {
@@ -16,17 +15,15 @@ class CreateOrder extends API {
     async onRequest(req, res) {
         let { uuid, session, id, title, content, files, email } = req.body
 
-        if ([uuid, session, content].some(value => value === '' || value === null || value === undefined)) {
+        if ([uuid, session, content].some(value => value === '' || value === null || value === undefined))
             return res.json({
                 ...BaseStdResponse.MISSING_PARAMETER
             })
-        }
 
-        if (!await AccessControl.checkSession(uuid, session)) {
+        if (!await AccessControl.checkSession(uuid, session))
             return res.status(401).json({
                 ...BaseStdResponse.ACCESS_DENIED
             })
-        }
 
         const time = new Date().getTime()
 
@@ -39,7 +36,7 @@ class CreateOrder extends API {
             type: 'user'
         }
 
-        let sql, r, need_manual = 0
+        let sql, r
 
         if (!id) {
             if ([title].some(value => value === '' || value === null || value === undefined))
@@ -59,7 +56,7 @@ class CreateOrder extends API {
             sql = 'INSERT INTO work_order (title, email, msg, create_user, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?)'
             r = await db.query(sql, [title, email, msg, uuid, time, time])
         } else {
-            const selectSql = 'SELECT msg, state, email, need_manual FROM work_order WHERE id = ? AND create_user = ?'
+            const selectSql = 'SELECT msg, state, email FROM work_order WHERE id = ? AND create_user = ?'
             const selectRows = await db.query(selectSql, [id, uuid])
             if (!selectRows || selectRows.length !== 1 || selectRows[0].state === 2)
                 return res.json({
@@ -72,7 +69,6 @@ class CreateOrder extends API {
 
             sql = 'UPDATE work_order SET msg = ?, update_time = ?, state = 0 WHERE id = ? AND create_user = ?'
             r = await db.query(sql, [msg, time, id, uuid])
-            need_manual = selectRows[0].need_manual
         }
 
         try {
@@ -82,23 +78,6 @@ class CreateOrder extends API {
                     data: r.insertId
                 })
 
-                try {
-                    if (need_manual == 0) {
-                        this.logger.info(`工单转发OneBot开始:orderId=${r.insertId || id}`)
-                        await OneBotV11.sendOrderMessage({
-                            orderId: r.insertId || id,
-                            title,
-                            role: '用户',
-                            senderUuid: uuid,
-                            content,
-                            files: files ?? []
-                        })
-                        this.logger.info(`工单转发OneBot结束:orderId=${r.insertId || id}`)
-                    }
-                } catch (botErr) {
-                    this.logger.error(`OneBot v11 转发工单消息失败:${botErr.stack}`)
-                }
-
                 let kefuSql = `
                     SELECT email FROM users
                     WHERE JSON_CONTAINS(permission, '"admin"') OR JSON_CONTAINS(permission, '"service"')
@@ -109,7 +88,7 @@ class CreateOrder extends API {
                 let emails = [...new Set(kefuRows.map(row => row.email))]
 
                 for (const email of emails) {
-                    if (!email) break
+                    if(!email) break
                     await EmailTemplate.orderNewReply(email, { id: r.insertId || id, content, files })
                 }
             } else {

+ 14 - 9
apis/Lepao/Account/AddAccount.js

@@ -3,10 +3,12 @@ const db = require("../../../plugin/DataBase/db.js");
 const { BaseStdResponse } = require("../../../BaseStdResponse.js");
 const AccessControl = require("../../../lib/AccessControl.js");
 const { getJkesSettings } = require("../../../plugin/jkes/jkesSettings.js");
+const { isJkesRunZone, normalizeJkesRunZone } = require("../../../plugin/jkes/runZones.js");
 const {
     DEFAULT_AUTO_RUN_DISTANCE_KM,
     validateAutoRunPresetForSave
 } = require("../../../plugin/jkes/autoRunAccountOptions.js");
+const { resolveQQAvatarFromEmail } = require("../../../plugin/jkes/resolveQQAvatar.js");
 
 function pickBodyOrDb(bodyVal, dbVal, fallback) {
     if (bodyVal !== undefined && bodyVal !== null && bodyVal !== "") return bodyVal;
@@ -25,11 +27,6 @@ class AddAccount extends API {
         this.banEmailList = ['icloud.com']
     }
 
-    isQQ(str) {
-        const reg = /^[1-9][0-9]{4,10}$/
-        return reg.test(str)
-    }
-
     // 生成 6 位数字 + 字母混合码
     async generateCode() {
         try {
@@ -85,6 +82,14 @@ class AddAccount extends API {
             })
         }
 
+        if (area !== undefined && area !== null && area !== '' && !isJkesRunZone(area)) {
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '跑区无效,请选择学府大道校区或南山校区'
+            })
+        }
+        area = normalizeJkesRunZone(area)
+
         if (notice_type === 'email') {
             if (!this.emailRegex.test(email)) {
                 return res.json({
@@ -215,9 +220,8 @@ class AddAccount extends API {
         let sql, r, user_avatar
 
         // 生成用户头像
-        if (email && email.split('@')[1].toLowerCase() === 'qq.com' && this.isQQ(email.split('@')[0])) {
-            user_avatar = `https://q2.qlogo.cn/headimg_dl?dst_uin=${email}&spec=640`
-        } else {
+        user_avatar = resolveQQAvatarFromEmail(email)
+        if (!user_avatar) {
             let userSex = 2;
             const urows = await db.query("SELECT sex FROM users WHERE uuid = ? LIMIT 1", [uuid]);
             if (urows?.[0]?.sex === 1) userSex = 1;
@@ -279,10 +283,11 @@ class AddAccount extends API {
                     return res.json({ ...BaseStdResponse.ERR, msg: '添加乐跑账号失败!数据库错误' })
             }
         } else {
-            sql = "UPDATE lepao_account SET student_num = ?, email = ?, area = ?, auto_time = ?, target_count = ?, auto_run = ?, notes = ?, auto_day = ?, update_time = ?, notice_type = ?, auto_run_distance_min_km = ?, auto_run_distance_max_km = ?, pace_min_sec_per_km = ?, pace_max_sec_per_km = ? WHERE id = ? AND create_user = ?";
+            sql = "UPDATE lepao_account SET student_num = ?, email = ?, user_avatar = ?, area = ?, auto_time = ?, target_count = ?, auto_run = ?, notes = ?, auto_day = ?, update_time = ?, notice_type = ?, auto_run_distance_min_km = ?, auto_run_distance_max_km = ?, pace_min_sec_per_km = ?, pace_max_sec_per_km = ? WHERE id = ? AND create_user = ?";
             r = await db.query(sql, [
                 student_num,
                 email ?? "",
+                user_avatar ?? "",
                 area,
                 auto_time,
                 targetKm,

+ 2 - 2
apis/Lepao/Account/Admin/ChangeLepaoCount.js

@@ -83,14 +83,14 @@ class ChangeLepaoCount extends API {
                     bizType: 'admin_adjust',
                     bizId: `admin_adjust:${Date.now()}:${userid}`,
                     operatorUuid: uuid,
-                    remark: remark || '系统调整次数'
+                    remark: remark || '系统调整里程'
                 })
             }
 
             await conn.commit()
         } catch (error) {
             try { await conn.rollback() } catch (_) { }
-            this.logger.error(`管理员调整次数失败: ${error.stack || error}`)
+            this.logger.error(`管理员调整里程失败: ${error.stack || error}`)
             return res.json({
                 ...BaseStdResponse.DATABASE_ERR
             })

+ 7 - 14
apis/Lepao/Account/Admin/GetAccountList.js

@@ -12,7 +12,7 @@ class GetAccountList extends API {
     }
 
     async onRequest(req, res) {
-        let { uuid, session, email, area, user_uuid, username, student_num, notes, state, auto_time, queryTime, bind_code, bot_account, pagesize, current } = req.query
+        let { uuid, session, email, area, user_uuid, username, student_num, state, auto_time, queryTime, bind_code, bot_account, pagesize, current } = req.query
 
         if ([uuid, session, pagesize, current].some(value => value === '' || value === null || value === undefined))
             return res.json({
@@ -41,7 +41,8 @@ class GetAccountList extends API {
             })
 
         // 检查权限
-        if (!await AccessControl.canAccess(uuid, ['page.admin.lepaoAccount']))
+        let permission = await AccessControl.getPermission(uuid)
+        if (!permission.includes("admin") && !permission.includes("service"))
             return res.json({
                 ...BaseStdResponse.PERMISSION_DENIED
             })
@@ -87,7 +88,6 @@ class GetAccountList extends API {
                     l.pace_min_sec_per_km,
                     l.pace_max_sec_per_km,
                     l.notice_type,
-                    l.update_type,
                     f.bind_code,
                     f.bot_account,
                     f.bot_umo,
@@ -161,13 +161,6 @@ class GetAccountList extends API {
             countParams.push(`%${username}%`)
         }
 
-        if (notes) {
-            sql += ` AND l.notes LIKE ?`
-            countSql += ` AND l.notes LIKE ?`
-            params.push(`%${notes}%`)
-            countParams.push(`%${notes}%`)
-        }
-
         if (bind_code) {
             sql += ` AND f.bind_code = ?`
             countSql += ` AND f.bind_code = ?`
@@ -183,10 +176,10 @@ class GetAccountList extends API {
         }
 
         if (auto_time !== 0) {
-            sql += ` AND (l.auto_time = ? OR l.auto_time = -1 AND l.today_auto_time = ?)`
-            countSql += ` AND (l.auto_time = ? OR l.auto_time = -1 AND l.today_auto_time = ?)`
-            params.push(auto_time, auto_time)
-            countParams.push(auto_time, auto_time)
+            sql += ` AND l.auto_time = ?`
+            countSql += ` AND l.auto_time = ?`
+            params.push(auto_time)
+            countParams.push(auto_time)
         }
 
         if (state !== -1) {

+ 0 - 61
apis/Lepao/Account/Admin/UpdateAccountInfo.js

@@ -1,61 +0,0 @@
-const API = require("../../../../lib/API")
-const AccessControl = require("../../../../lib/AccessControl")
-const { BaseStdResponse } = require("../../../../BaseStdResponse")
-const { syncAccountInfo } = require("../../../../lib/Lepao/syncAccountInfo")
-
-class UpdateAccountInfo extends API {
-    constructor() {
-        super()
-
-        this.setPath('/Admin/Lepao/Account/UpdateAccountInfo')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        const { uuid, session, student_num } = req.body
-
-        if ([uuid, session, student_num].some(v => v === '' || v === null || v === undefined)) {
-            return res.json({
-                ...BaseStdResponse.MISSING_PARAMETER
-            })
-        }
-
-        if (!await AccessControl.checkSession(uuid, session)) {
-            return res.status(401).json({
-                ...BaseStdResponse.ACCESS_DENIED
-            })
-        }
-
-        if (!await AccessControl.canAccess(uuid, ['action.lepao.admin.updateAccount'])) {
-            return res.json({
-                ...BaseStdResponse.PERMISSION_DENIED
-            })
-        }
-
-        try {
-            const syncResult = await syncAccountInfo({
-                studentNum: student_num,
-                logger: this.logger
-            })
-            if (!syncResult.ok) {
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: syncResult.msg || '同步失败,请稍后再试'
-                })
-            }
-
-            return res.json({
-                ...BaseStdResponse.OK,
-                data: syncResult.data
-            })
-        } catch (error) {
-            this.logger.error(`管理员同步乐跑账号失败: ${error.stack || error}`)
-            return res.json({
-                ...BaseStdResponse.ERR,
-                msg: '同步失败,请稍后再试'
-            })
-        }
-    }
-}
-
-module.exports.UpdateAccountInfo = UpdateAccountInfo

+ 0 - 98
apis/Lepao/BindAudit/Admin/ByAccount.js

@@ -1,98 +0,0 @@
-const API = require("../../../../lib/API.js")
-const db = require("../../../../plugin/DataBase/db.js")
-const { BaseStdResponse } = require("../../../../BaseStdResponse.js")
-const AccessControl = require("../../../../lib/AccessControl.js")
-
-function parseDetail(value) {
-    if (value === null || value === undefined || value === '') return null
-    if (typeof value === 'object') return value
-    try {
-        return JSON.parse(value)
-    } catch {
-        return null
-    }
-}
-
-class AdminBindAuditByAccount extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/Lepao/BindAudit/ByAccount')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        let { uuid, session, student_num, pagesize, current, queryTime } = req.query
-
-        if ([uuid, session, student_num, pagesize, current].some(v => v === '' || v === null || v === undefined))
-            return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-        if (isNaN(pagesize) || Number(pagesize) <= 0 || isNaN(current) || Number(current) <= 0)
-            return res.json({ ...BaseStdResponse.ERR, msg: '参数错误' })
-
-        if (!await AccessControl.checkSession(uuid, session))
-            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
-
-        const permission = await AccessControl.getPermission(uuid)
-        if (!permission.includes("admin") && !permission.includes("server") && !permission.includes("service"))
-            return res.json({ ...BaseStdResponse.PERMISSION_DENIED })
-
-        const offset = (Number(current) - 1) * Number(pagesize)
-        const where = ['lba.student_num COLLATE utf8mb4_general_ci = (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)']
-        const params = [student_num]
-        const countParams = [student_num]
-
-        if (Array.isArray(queryTime) && queryTime.length === 2) {
-            where.push('lba.created_at >= ? AND lba.created_at < ?')
-            params.push(queryTime[0], queryTime[1])
-            countParams.push(queryTime[0], queryTime[1])
-        }
-
-        const whereSql = where.join(' AND ')
-        const listSql = `
-            SELECT
-                lba.id,
-                lba.student_num,
-                lba.owner_uuid,
-                lba.action,
-                lba.source,
-                lba.operator_uuid,
-                lba.detail_json,
-                lba.created_at,
-                la.name AS lepao_name,
-                la.user_avatar AS lepao_avatar,
-                owner_u.username AS owner_username,
-                owner_u.avatar AS owner_avatar,
-                op_u.username AS operator_username,
-                op_u.avatar AS operator_avatar
-            FROM lepao_bind_audit lba
-            LEFT JOIN lepao_account la ON la.student_num COLLATE utf8mb4_general_ci = lba.student_num COLLATE utf8mb4_general_ci
-            LEFT JOIN users owner_u ON owner_u.uuid COLLATE utf8mb4_general_ci = lba.owner_uuid COLLATE utf8mb4_general_ci
-            LEFT JOIN users op_u ON op_u.uuid COLLATE utf8mb4_general_ci = lba.operator_uuid COLLATE utf8mb4_general_ci
-            WHERE ${whereSql}
-            ORDER BY lba.id DESC
-            LIMIT ? OFFSET ?
-        `
-        const countSql = `SELECT COUNT(*) AS total FROM lepao_bind_audit lba WHERE ${whereSql}`
-        params.push(String(pagesize), String(offset))
-
-        const rows = await db.query(listSql, params)
-        const countRows = await db.query(countSql, countParams)
-        if (!rows || !countRows) return res.json({ ...BaseStdResponse.DATABASE_ERR })
-
-        const data = rows.map(item => ({
-            ...item,
-            detail_json: parseDetail(item.detail_json)
-        }))
-
-        return res.json({
-            ...BaseStdResponse.OK,
-            data,
-            pagination: {
-                current: Number(current),
-                pagesize: Number(pagesize),
-                total: countRows[0]?.total || 0
-            }
-        })
-    }
-}
-
-module.exports.AdminBindAuditByAccount = AdminBindAuditByAccount

+ 0 - 134
apis/Lepao/BindAudit/Admin/List.js

@@ -1,134 +0,0 @@
-const API = require("../../../../lib/API.js")
-const db = require("../../../../plugin/DataBase/db.js")
-const { BaseStdResponse } = require("../../../../BaseStdResponse.js")
-const AccessControl = require("../../../../lib/AccessControl.js")
-
-function parseDetail(value) {
-    if (value === null || value === undefined || value === '') return null
-    if (typeof value === 'object') return value
-    try {
-        return JSON.parse(value)
-    } catch {
-        return null
-    }
-}
-
-class AdminBindAuditList extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/Lepao/BindAudit/List')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        let {
-            uuid,
-            session,
-            student_num,
-            owner_uuid,
-            operator_uuid,
-            action,
-            source,
-            queryTime,
-            pagesize,
-            current
-        } = req.query
-
-        if ([uuid, session, pagesize, current].some(v => v === '' || v === null || v === undefined))
-            return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-        if (isNaN(pagesize) || Number(pagesize) <= 0 || isNaN(current) || Number(current) <= 0)
-            return res.json({ ...BaseStdResponse.ERR, msg: '参数错误' })
-
-        if (!await AccessControl.checkSession(uuid, session))
-            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
-
-        const permission = await AccessControl.getPermission(uuid)
-        if (!permission.includes("admin") && !permission.includes("server") && !permission.includes("service"))
-            return res.json({ ...BaseStdResponse.PERMISSION_DENIED })
-
-        const offset = (Number(current) - 1) * Number(pagesize)
-        const where = ['1 = 1']
-        const params = []
-        const countParams = []
-
-        if (student_num) {
-            where.push('lba.student_num COLLATE utf8mb4_general_ci LIKE (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(`%${student_num}%`)
-            countParams.push(`%${student_num}%`)
-        }
-        if (owner_uuid) {
-            where.push('lba.owner_uuid COLLATE utf8mb4_general_ci = (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(owner_uuid)
-            countParams.push(owner_uuid)
-        }
-        if (operator_uuid) {
-            where.push('lba.operator_uuid COLLATE utf8mb4_general_ci = (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(operator_uuid)
-            countParams.push(operator_uuid)
-        }
-        if (action) {
-            where.push('lba.action COLLATE utf8mb4_general_ci = (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(action)
-            countParams.push(action)
-        }
-        if (source) {
-            where.push('lba.source COLLATE utf8mb4_general_ci = (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(source)
-            countParams.push(source)
-        }
-        if (Array.isArray(queryTime) && queryTime.length === 2) {
-            where.push('lba.created_at >= ? AND lba.created_at < ?')
-            params.push(queryTime[0], queryTime[1])
-            countParams.push(queryTime[0], queryTime[1])
-        }
-
-        const whereSql = where.join(' AND ')
-        const listSql = `
-            SELECT
-                lba.id,
-                lba.student_num,
-                lba.owner_uuid,
-                lba.action,
-                lba.source,
-                lba.operator_uuid,
-                lba.detail_json,
-                lba.created_at,
-                la.name AS lepao_name,
-                la.user_avatar AS lepao_avatar,
-                owner_u.username AS owner_username,
-                owner_u.avatar AS owner_avatar,
-                op_u.username AS operator_username,
-                op_u.avatar AS operator_avatar
-            FROM lepao_bind_audit lba
-            LEFT JOIN lepao_account la ON la.student_num COLLATE utf8mb4_general_ci = lba.student_num COLLATE utf8mb4_general_ci
-            LEFT JOIN users owner_u ON owner_u.uuid COLLATE utf8mb4_general_ci = lba.owner_uuid COLLATE utf8mb4_general_ci
-            LEFT JOIN users op_u ON op_u.uuid COLLATE utf8mb4_general_ci = lba.operator_uuid COLLATE utf8mb4_general_ci
-            WHERE ${whereSql}
-            ORDER BY lba.id DESC
-            LIMIT ? OFFSET ?
-        `
-        const countSql = `SELECT COUNT(*) AS total FROM lepao_bind_audit lba WHERE ${whereSql}`
-        params.push(String(pagesize), String(offset))
-
-        const rows = await db.query(listSql, params)
-        const countRows = await db.query(countSql, countParams)
-        if (!rows || !countRows) return res.json({ ...BaseStdResponse.DATABASE_ERR })
-
-        const data = rows.map(item => ({
-            ...item,
-            detail_json: parseDetail(item.detail_json)
-        }))
-
-        return res.json({
-            ...BaseStdResponse.OK,
-            data,
-            pagination: {
-                current: Number(current),
-                pagesize: Number(pagesize),
-                total: countRows[0]?.total || 0
-            }
-        })
-    }
-}
-
-module.exports.AdminBindAuditList = AdminBindAuditList

+ 0 - 106
apis/Lepao/BindAudit/MyList.js

@@ -1,106 +0,0 @@
-const API = require("../../../lib/API.js")
-const db = require("../../../plugin/DataBase/db.js")
-const { BaseStdResponse } = require("../../../BaseStdResponse.js")
-const AccessControl = require("../../../lib/AccessControl.js")
-
-const SYSTEM_UNBIND_SOURCES = new Set(['admin_api', 'service_api', 'mcp_work_order'])
-
-function mapUserAuditItem(item) {
-    const base = {
-        student_num: item.student_num,
-        created_at: item.created_at
-    }
-
-    if (item.action === 'platform_unbind' && SYSTEM_UNBIND_SOURCES.has(item.source)) {
-        return {
-            ...base,
-            action: 'system_unbind',
-            action_label: '系统解绑'
-        }
-    }
-
-    const labels = {
-        platform_bind: '绑定',
-        platform_unbind: '解绑',
-        bot_bind: '绑定',
-        bot_unbind: '解绑'
-    }
-    return {
-        ...base,
-        action: item.action,
-        action_label: labels[item.action] || item.action
-    }
-}
-
-class MyBindAuditList extends API {
-    constructor() {
-        super()
-        this.setPath('/Lepao/BindAudit/List')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        let { uuid, session, student_num, pagesize, current, queryTime } = req.query
-
-        if ([uuid, session, pagesize, current].some(v => v === '' || v === null || v === undefined))
-            return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-
-        if (isNaN(pagesize) || Number(pagesize) <= 0 || isNaN(current) || Number(current) <= 0) {
-            return res.json({ ...BaseStdResponse.ERR, msg: '参数错误' })
-        }
-
-        if (!await AccessControl.checkSession(uuid, session))
-            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
-
-        const offset = (Number(current) - 1) * Number(pagesize)
-        const where = ['lba.owner_uuid COLLATE utf8mb4_general_ci = (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)']
-        const params = [uuid]
-        const countParams = [uuid]
-
-        if (student_num) {
-            where.push('lba.student_num COLLATE utf8mb4_general_ci LIKE (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(`%${student_num}%`)
-            countParams.push(`%${student_num}%`)
-        }
-
-        if (Array.isArray(queryTime) && queryTime.length === 2) {
-            where.push('lba.created_at >= ? AND lba.created_at < ?')
-            params.push(queryTime[0], queryTime[1])
-            countParams.push(queryTime[0], queryTime[1])
-        }
-
-        const whereSql = where.join(' AND ')
-        const listSql = `
-            SELECT
-                lba.student_num,
-                lba.action,
-                lba.source,
-                lba.created_at,
-                la.name AS lepao_name,
-                la.user_avatar AS lepao_avatar
-            FROM lepao_bind_audit lba
-            LEFT JOIN lepao_account la ON la.student_num = lba.student_num
-            WHERE ${whereSql}
-            ORDER BY lba.id DESC
-            LIMIT ? OFFSET ?
-        `
-        const countSql = `SELECT COUNT(*) AS total FROM lepao_bind_audit lba WHERE ${whereSql}`
-        params.push(String(pagesize), String(offset))
-
-        const rows = await db.query(listSql, params)
-        const countRows = await db.query(countSql, countParams)
-        if (!rows || !countRows) return res.json({ ...BaseStdResponse.DATABASE_ERR })
-
-        return res.json({
-            ...BaseStdResponse.OK,
-            data: rows.map(mapUserAuditItem),
-            pagination: {
-                current: Number(current),
-                pagesize: Number(pagesize),
-                total: countRows[0]?.total || 0
-            }
-        })
-    }
-}
-
-module.exports.MyBindAuditList = MyBindAuditList

+ 2 - 1
apis/Lepao/ChangeAutoRun.js

@@ -33,7 +33,8 @@ class ChangeAutoRun extends API {
             })
 
         if (selectRows[0].create_user !== uuid) {
-            if (!await AccessControl.canAccess(uuid, ['action.lepao.changeAutoRun']))
+            let permission = await AccessControl.getPermission(uuid)
+            if (!permission.includes("admin") && !permission.includes("service"))
                 return res.json({
                     ...BaseStdResponse.ERR,
                     msg: '切换自动乐跑状态失败!未找到账户信息'

+ 11 - 0
apis/Lepao/GetJkesRunOverview.js

@@ -9,6 +9,10 @@ const {
     refreshMonthKmFromApi,
     normalizeMonthTargetKm
 } = require('../../plugin/jkes/monthPolicy')
+const {
+    markLoginExpiredByStudentNum,
+    LOGIN_EXPIRED_MSG
+} = require('../../plugin/jkes/syncLepaoAccountFromToken')
 /**
  * 从 JKES 官方接口拉取本月/累计里程,并返回本地月度缓存。
  * monthTargetKm 为学校要求的「本月最低」参考,非可跑里程上限。
@@ -88,6 +92,13 @@ class GetJkesRunOverview extends API {
             })
         } catch (e) {
             this.logger.error(`JkesRunOverview ${e.stack || e}`)
+            if (e?.loginExpired) {
+                await markLoginExpiredByStudentNum(student_num)
+                return res.json({
+                    ...BaseStdResponse.ERR,
+                    msg: e.message || LOGIN_EXPIRED_MSG
+                })
+            }
             return res.json({
                 ...BaseStdResponse.ERR,
                 msg: '获取 JKES 跑步数据失败'

+ 1 - 2
apis/Lepao/Path/GetPathDetail.js

@@ -2,7 +2,6 @@ const API = require("../../../lib/API")
 const db = require("../../../plugin/DataBase/db")
 const AccessControl = require("../../../lib/AccessControl")
 const { BaseStdResponse } = require("../../../BaseStdResponse")
-const { toCoordinatePairs } = require("../../../plugin/Lepao/toCoordinatePairs")
 
 class GetPathDetail extends API {
     constructor() {
@@ -68,7 +67,7 @@ class GetPathDetail extends API {
             }) 
 
         let data = rows[0]
-        data.data = toCoordinatePairs(data.data)
+        data.data = data.data.map(point => [point.o, point.a])
 
         res.json({
             ...BaseStdResponse.OK,

+ 5 - 3
apis/Lepao/Record/Admin/GetLepaoRecords.js

@@ -2,7 +2,7 @@ const API = require("../../../../lib/API.js");
 const db = require("../../../../plugin/DataBase/db.js");
 const { BaseStdResponse } = require("../../../../BaseStdResponse.js");
 const AccessControl = require("../../../../lib/AccessControl.js");
-const { enrichLepaoRecordRow } = require("../../../../plugin/jkes/formatRecordRow.js");
+const { enrichLepaoRecordRowForApi } = require("../../../../plugin/jkes/formatRecordRow.js");
 
 class AdminGetLepaoRecords extends API {
     constructor() {
@@ -43,7 +43,7 @@ class AdminGetLepaoRecords extends API {
 
         // 检查权限
         let permission = await AccessControl.getPermission(uuid)
-        if (!permission.includes("admin") && !permission.includes("server"))
+        if (!permission.includes("admin") && !permission.includes("service"))
             return res.json({
                 ...BaseStdResponse.PERMISSION_DENIED
             })
@@ -54,9 +54,11 @@ class AdminGetLepaoRecords extends API {
         let sql = `
                 SELECT 
                     r.id,
+                    r.public_id,
                     r.time,
                     r.result,
                     r.state,
+                    r.run_mode,
                     r.lepao_account,
                     r.path_id,
                     a.name,
@@ -147,7 +149,7 @@ class AdminGetLepaoRecords extends API {
 
         res.json({
             ...BaseStdResponse.OK,
-            data: await Promise.all(rows.map(enrichLepaoRecordRow)),
+            data: await Promise.all(rows.map(enrichLepaoRecordRowForApi)),
             pagination: {
                 current,
                 pagesize,

+ 0 - 72
apis/Lepao/Record/Admin/GetOfficialTermRecords.js

@@ -1,72 +0,0 @@
-const API = require('../../../../lib/API')
-const AccessControl = require('../../../../lib/AccessControl')
-const { BaseStdResponse } = require('../../../../BaseStdResponse')
-const { getCurrentTerm, getTermRunRecord } = require('../../../../lib/Lepao/lepaoOfficialApi')
-const {
-    isMissing,
-    loadAdminOfficialAccount,
-    mapOfficialError,
-    syncOfficialTotalNum
-} = require('../../../../lib/Lepao/officialAccountAccess')
-
-class AdminGetOfficialTermRecords extends API {
-    constructor() {
-        super()
-
-        this.setPath('/Admin/Lepao/OfficialTermRecords')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        const { uuid, session, student_num } = req.query
-        const page = Number(req.query.page || 1)
-
-        if ([uuid, session, student_num].some(isMissing)) {
-            return res.json({
-                ...BaseStdResponse.MISSING_PARAMETER
-            })
-        }
-
-        if (!Number.isFinite(page) || page <= 0) {
-            return res.json({
-                ...BaseStdResponse.ERR,
-                msg: '参数错误'
-            })
-        }
-
-        if (!await AccessControl.checkSession(uuid, session)) {
-            return res.status(401).json({
-                ...BaseStdResponse.ACCESS_DENIED
-            })
-        }
-
-        try {
-            const { account, error } = await loadAdminOfficialAccount(uuid, student_num)
-            if (error) return res.json(error)
-
-            const currentTerm = await getCurrentTerm(account, { logger: this.logger })
-            const recordResult = await getTermRunRecord(
-                account,
-                { termId: currentTerm.term_id, page },
-                { logger: this.logger }
-            )
-            const recordData = recordResult.data || {}
-            await syncOfficialTotalNum(student_num, recordData, this.logger)
-
-            return res.json({
-                ...BaseStdResponse.OK,
-                data: {
-                    term_id: currentTerm.term_id,
-                    term_name: currentTerm.term_name,
-                    page,
-                    ...recordData
-                }
-            })
-        } catch (error) {
-            this.logger.error(`管理员获取官方学期乐跑记录失败 ${student_num}: ${error.stack || error}`)
-            return res.json(await mapOfficialError(error, student_num, this.logger))
-        }
-    }
-}
-
-module.exports.AdminGetOfficialTermRecords = AdminGetOfficialTermRecords

+ 27 - 13
apis/Lepao/Record/Admin/GetRecordDetail.js

@@ -2,7 +2,7 @@ const API = require("../../../../lib/API")
 const db = require("../../../../plugin/DataBase/db")
 const AccessControl = require("../../../../lib/AccessControl")
 const { BaseStdResponse } = require("../../../../BaseStdResponse")
-const { enrichLepaoRecordRow } = require("../../../../plugin/jkes/formatRecordRow.js")
+const { enrichLepaoRecordRow, stripInternalRecordId } = require("../../../../plugin/jkes/formatRecordRow.js")
 
 class AdminGetRecordDetail extends API {
     constructor() {
@@ -16,28 +16,25 @@ class AdminGetRecordDetail extends API {
         let {
             uuid,
             session,
-            id
+            public_id
         } = req.query
 
-        if ([uuid, session, id].some(value => value === '' || value === null || value === undefined))
+        if ([uuid, session, public_id].some(value => value === '' || value === null || value === undefined))
             return res.json({
                 ...BaseStdResponse.MISSING_PARAMETER
             })
 
-        // 检查 session
         if (!await AccessControl.checkSession(uuid, session))
             return res.status(401).json({
                 ...BaseStdResponse.ACCESS_DENIED
             })
 
-        // 检查权限
         let permission = await AccessControl.getPermission(uuid)
-        if (!permission.includes("admin") && !permission.includes("server"))
+        if (!permission.includes("admin") && !permission.includes("service"))
             return res.json({
                 ...BaseStdResponse.PERMISSION_DENIED
             })
 
-
         const baseWhere = `
             FROM 
                 lepao_record r
@@ -50,15 +47,18 @@ class AdminGetRecordDetail extends API {
             ON 
                 r.path_id = p.id
             WHERE 
-                r.id = ?
+                r.public_id = ?
         `
         let rows
         try {
             const sql = `
                 SELECT 
+                    r.id,
+                    r.public_id,
                     r.time,
                     r.result,
                     r.state,
+                    r.run_mode,
                     r.lepao_account,
                     r.path_id,
                     r.path_data AS run_path_data,
@@ -69,14 +69,17 @@ class AdminGetRecordDetail extends API {
                     NULL AS path_distance
                 ${baseWhere}
             `
-            rows = await db.query(sql, [id])
+            rows = await db.query(sql, [public_id])
         } catch (e) {
             if (!(e?.message || '').includes("Unknown column 'r.path_data'")) throw e
             const sql = `
                 SELECT 
+                    r.id,
+                    r.public_id,
                     r.time,
                     r.result,
                     r.state,
+                    r.run_mode,
                     r.lepao_account,
                     r.path_id,
                     r.point_data AS run_path_data,
@@ -87,7 +90,7 @@ class AdminGetRecordDetail extends API {
                     NULL AS path_distance
                 ${baseWhere}
             `
-            rows = await db.query(sql, [id])
+            rows = await db.query(sql, [public_id])
         }
 
         if (!rows)
@@ -138,13 +141,24 @@ class AdminGetRecordDetail extends API {
         delete data.data
         delete data.run_path_data
 
-        data.jkes_record = enrichLepaoRecordRow({ result: data.result }).jkes_record
+        const enriched = await enrichLepaoRecordRow({
+            id: data.id,
+            state: data.state,
+            result: data.result
+        })
+        data.jkes_record = enriched.jkes_record
+        if (enriched.lepao_schedule != null) {
+            data.lepao_schedule = enriched.lepao_schedule
+        }
+        if (enriched.schedule != null) {
+            data.schedule = enriched.schedule
+        }
 
         res.json({
             ...BaseStdResponse.OK,
-            data
+            data: stripInternalRecordId(data)
         })
     }
 }
 
-module.exports.AdminGetRecordDetail = AdminGetRecordDetail
+module.exports.AdminGetRecordDetail = AdminGetRecordDetail

+ 4 - 2
apis/Lepao/Record/GetLepaoRecords.js

@@ -2,7 +2,7 @@ const API = require("../../../lib/API.js");
 const db = require("../../../plugin/DataBase/db.js");
 const { BaseStdResponse } = require("../../../BaseStdResponse.js");
 const AccessControl = require("../../../lib/AccessControl.js");
-const { enrichLepaoRecordRow } = require("../../../plugin/jkes/formatRecordRow.js");
+const { enrichLepaoRecordRowForApi } = require("../../../plugin/jkes/formatRecordRow.js");
 
 class GetLepaoRecords extends API {
     constructor() {
@@ -46,10 +46,12 @@ class GetLepaoRecords extends API {
         let sql = `
                 SELECT 
                     r.id,
+                    r.public_id,
                     r.uuid,
                     r.time,
                     r.result,
                     r.state,
+                    r.run_mode,
                     r.lepao_account,
                     r.path_id,
                     a.name,
@@ -127,7 +129,7 @@ class GetLepaoRecords extends API {
 
         res.json({
             ...BaseStdResponse.OK,
-            data: await Promise.all(rows.map(enrichLepaoRecordRow)),
+            data: await Promise.all(rows.map(enrichLepaoRecordRowForApi)),
             pagination: {
                 current,
                 pagesize,

+ 0 - 72
apis/Lepao/Record/GetOfficialTermRecords.js

@@ -1,72 +0,0 @@
-const API = require('../../../lib/API')
-const AccessControl = require('../../../lib/AccessControl')
-const { BaseStdResponse } = require('../../../BaseStdResponse')
-const { getCurrentTerm, getTermRunRecord } = require('../../../lib/Lepao/lepaoOfficialApi')
-const {
-    isMissing,
-    loadUserOfficialAccount,
-    mapOfficialError,
-    syncOfficialTotalNum
-} = require('../../../lib/Lepao/officialAccountAccess')
-
-class GetOfficialTermRecords extends API {
-    constructor() {
-        super()
-
-        this.setPath('/Lepao/OfficialTermRecords')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        const { uuid, session, student_num } = req.query
-        const page = Number(req.query.page || 1)
-
-        if ([uuid, session, student_num].some(isMissing)) {
-            return res.json({
-                ...BaseStdResponse.MISSING_PARAMETER
-            })
-        }
-
-        if (!Number.isFinite(page) || page <= 0) {
-            return res.json({
-                ...BaseStdResponse.ERR,
-                msg: '参数错误'
-            })
-        }
-
-        if (!await AccessControl.checkSession(uuid, session)) {
-            return res.status(401).json({
-                ...BaseStdResponse.ACCESS_DENIED
-            })
-        }
-
-        try {
-            const { account, error } = await loadUserOfficialAccount(uuid, student_num)
-            if (error) return res.json(error)
-
-            const currentTerm = await getCurrentTerm(account, { logger: this.logger })
-            const recordResult = await getTermRunRecord(
-                account,
-                { termId: currentTerm.term_id, page },
-                { logger: this.logger }
-            )
-            const recordData = recordResult.data || {}
-            await syncOfficialTotalNum(student_num, recordData, this.logger)
-
-            return res.json({
-                ...BaseStdResponse.OK,
-                data: {
-                    term_id: currentTerm.term_id,
-                    term_name: currentTerm.term_name,
-                    page,
-                    ...recordData
-                }
-            })
-        } catch (error) {
-            this.logger.error(`获取官方学期乐跑记录失败 ${student_num}: ${error.stack || error}`)
-            return res.json(await mapOfficialError(error, student_num, this.logger))
-        }
-    }
-}
-
-module.exports.GetOfficialTermRecords = GetOfficialTermRecords

+ 26 - 10
apis/Lepao/Record/GetRecordDetail.js

@@ -2,7 +2,7 @@ const API = require("../../../lib/API")
 const db = require("../../../plugin/DataBase/db")
 const AccessControl = require("../../../lib/AccessControl")
 const { BaseStdResponse } = require("../../../BaseStdResponse")
-const { enrichLepaoRecordRow } = require("../../../plugin/jkes/formatRecordRow.js")
+const { enrichLepaoRecordRow, stripInternalRecordId } = require("../../../plugin/jkes/formatRecordRow.js")
 
 class GetRecordDetail extends API {
     constructor() {
@@ -16,15 +16,14 @@ class GetRecordDetail extends API {
         let {
             uuid,
             session,
-            id
+            public_id
         } = req.query
 
-        if ([uuid, session, id].some(value => value === '' || value === null || value === undefined))
+        if ([uuid, session, public_id].some(value => value === '' || value === null || value === undefined))
             return res.json({
                 ...BaseStdResponse.MISSING_PARAMETER
             })
 
-        // 检查 session
         if (!await AccessControl.checkSession(uuid, session))
             return res.status(401).json({
                 ...BaseStdResponse.ACCESS_DENIED
@@ -42,15 +41,18 @@ class GetRecordDetail extends API {
             ON 
                 r.path_id = p.id
             WHERE 
-                (r.uuid = ? OR (a.create_user IS NOT NULL AND a.create_user = ?)) AND r.id = ?
+                (r.uuid = ? OR (a.create_user IS NOT NULL AND a.create_user = ?)) AND r.public_id = ?
         `
         let rows
         try {
             const sql = `
                 SELECT 
+                    r.id,
+                    r.public_id,
                     r.time,
                     r.result,
                     r.state,
+                    r.run_mode,
                     r.lepao_account,
                     r.path_id,
                     r.path_data AS run_path_data,
@@ -61,14 +63,17 @@ class GetRecordDetail extends API {
                     NULL AS path_distance
                 ${baseWhere}
             `
-            rows = await db.query(sql, [uuid, uuid, id])
+            rows = await db.query(sql, [uuid, uuid, public_id])
         } catch (e) {
             if (!(e?.message || '').includes("Unknown column 'r.path_data'")) throw e
             const sql = `
                 SELECT 
+                    r.id,
+                    r.public_id,
                     r.time,
                     r.result,
                     r.state,
+                    r.run_mode,
                     r.lepao_account,
                     r.path_id,
                     r.point_data AS run_path_data,
@@ -79,7 +84,7 @@ class GetRecordDetail extends API {
                     NULL AS path_distance
                 ${baseWhere}
             `
-            rows = await db.query(sql, [uuid, uuid, id])
+            rows = await db.query(sql, [uuid, uuid, public_id])
         }
 
         if (!rows)
@@ -130,13 +135,24 @@ class GetRecordDetail extends API {
         delete data.data
         delete data.run_path_data
 
-        data.jkes_record = enrichLepaoRecordRow({ result: data.result }).jkes_record
+        const enriched = await enrichLepaoRecordRow({
+            id: data.id,
+            state: data.state,
+            result: data.result
+        })
+        data.jkes_record = enriched.jkes_record
+        if (enriched.lepao_schedule != null) {
+            data.lepao_schedule = enriched.lepao_schedule
+        }
+        if (enriched.schedule != null) {
+            data.schedule = enriched.schedule
+        }
 
         res.json({
             ...BaseStdResponse.OK,
-            data
+            data: stripInternalRecordId(data)
         })
     }
 }
 
-module.exports.GetRecordDetail = GetRecordDetail
+module.exports.GetRecordDetail = GetRecordDetail

+ 12 - 1
apis/Lepao/SingleRun.js

@@ -9,6 +9,10 @@ const {
     publishRunforgeTask
 } = require('../../plugin/mq/runforgeTaskMq')
 const { planJkesManualRun } = require('../../plugin/jkes/monthPolicy')
+const {
+    markLoginExpiredByStudentNum,
+    LOGIN_EXPIRED_MSG
+} = require('../../plugin/jkes/syncLepaoAccountFromToken')
 const { parsePaceToSecPerKm, clampManualPaceSec } = require('../../plugin/jkes/paceUtils')
 const jkesRedisKeys = require('../../plugin/jkes/redisKeys')
 const mqNames = require('../../plugin/mq/jkesMqNames')
@@ -134,9 +138,16 @@ class SingleRun extends API {
 
         } catch (err) {
             this.logger.error(`手动乐跑失败!${err.stack}`)
+            if (err?.loginExpired) {
+                await markLoginExpiredByStudentNum(student_num)
+                return res.json({
+                    ...BaseStdResponse.ERR,
+                    msg: err.message || LOGIN_EXPIRED_MSG
+                })
+            }
             return res.json({
                 ...BaseStdResponse.ERR,
-                msg: "乐跑失败!数据库异常"
+                msg: '乐跑失败!请稍后再试'
             })
         }
     }

+ 0 - 17
apis/MCP/userMcp/GetMcp.js

@@ -1,17 +0,0 @@
-const API = require("../../../lib/API")
-
-class GetMcp extends API {
-    constructor() {
-        super()
-
-        this.noEncrypt()
-        this.setPath('/mcp')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        res.json({"status": "ok"})
-    }
-}
-
-module.exports.GetMcp = GetMcp

+ 0 - 269
apis/MCP/userMcp/McpRPC.js

@@ -1,269 +0,0 @@
-const API = require("../../../lib/API.js")
-const MCP = require("../../../lib/Lepao/Mcp.js").MCP
-
-class McpRpc extends API {
-    constructor() {
-        super()
-
-        this.noEncrypt()
-        this.setPath('/mcp')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        const { method, params = {}, id: req_id } = req.body
-
-        try {
-            let result
-
-            if (method === 'initialize') {
-                result = {
-                    protocolVersion: "2024-11-05",
-                    capabilities: { "tools": {} },
-                    serverInfo: { name: "runforge", version: "1.0" }
-                }
-            } else if (method === 'tools/list') {
-                result = {
-                    "tools": [
-                        {
-                            "name": "bind_account",
-                            "description": "Bind a user account when the user provides a bind code",
-                            "inputSchema": {
-                                "type": "object",
-                                "properties": {
-                                    "sender": {
-                                        "type": "string",
-                                        "description": "Unique user identifier from the chat platform"
-                                    },
-                                    "bind_code": {
-                                        "type": "string",
-                                        "description": "Binding code provided by the user"
-                                    }
-                                },
-                                "required": ["sender", "bind_code"]
-                            }
-                        },
-                        {
-                            "name": "unbind_account",
-                            "description": "Unbind the currently linked user account",
-                            "inputSchema": {
-                                "type": "object",
-                                "properties": {
-                                    "sender": {
-                                        "type": "string",
-                                        "description": "Unique user identifier from the chat platform"
-                                    }
-                                },
-                                "required": ["sender"]
-                            }
-                        },
-                        {
-                            "name": "get_account_info",
-                            "description": "Retrieve user account information such as name, student ID, account status, and running statistics",
-                            "inputSchema": {
-                                "type": "object",
-                                "properties": {
-                                    "sender": {
-                                        "type": "string",
-                                        "description": "Unique user identifier from the chat platform"
-                                    }
-                                },
-                                "required": ["sender"]
-                            }
-                        },
-                        {
-                            "name": "set_notification",
-                            "description": "Set notification preference when the user wants to enable or disable notifications",
-                            "inputSchema": {
-                                "type": "object",
-                                "properties": {
-                                    "sender": {
-                                        "type": "string",
-                                        "description": "Unique user identifier from the chat platform"
-                                    },
-                                    "mode": {
-                                        "type": "string",
-                                        "enum": ["bot", "email", "none"],
-                                        "description": "Notification mode: bot (chat bot), email (email notification), none (disable notifications)"
-                                    }
-                                },
-                                "required": ["sender", "mode"]
-                            }
-                        },
-                        {
-                            "name": "change_email",
-                            "description": "Update the user's notification email address",
-                            "inputSchema": {
-                                "type": "object",
-                                "properties": {
-                                    "sender": {
-                                        "type": "string",
-                                        "description": "Unique user identifier from the chat platform"
-                                    },
-                                    "email": {
-                                        "type": "string",
-                                        "format": "email",
-                                        "description": "New email address for notifications"
-                                    }
-                                },
-                                "required": ["sender", "email"]
-                            }
-                        },
-                        {
-                            "name": "create_work_order",
-                            "description": "Create a customer support ticket when the user reports a problem or suggestion",
-                            "inputSchema": {
-                                "type": "object",
-                                "properties": {
-                                    "sender": {
-                                        "type": "string",
-                                        "description": "Unique user identifier from the chat platform"
-                                    },
-                                    "email": {
-                                        "type": "string",
-                                        "format": "email",
-                                        "description": "User contact email"
-                                    },
-                                    "title": {
-                                        "type": "string",
-                                        "maxLength": 20,
-                                        "description": "Short title of the issue (max 20 characters)"
-                                    },
-                                    "content": {
-                                        "type": "string",
-                                        "maxLength": 100,
-                                        "description": "Detailed description of the issue (max 100 characters)"
-                                    }
-                                },
-                                "required": ["sender", "email", "title", "content"]
-                            }
-                        },
-                        {
-                            "name": "change_area",
-                            "description": "Set the running area for the user. If the user requests a random area, use '随机分配'",
-                            "inputSchema": {
-                                "type": "object",
-                                "properties": {
-                                    "sender": {
-                                        "type": "string",
-                                        "description": "Unique user identifier from the chat platform"
-                                    },
-                                    "area": {
-                                        "type": "string",
-                                        "enum": [
-                                            "兰花湖校区跑区",
-                                            "主校区北跑区",
-                                            "主校区南跑区",
-                                            "重庆工商大学茶园校区",
-                                            "随机分配"
-                                        ],
-                                        "description": "Running area. Must be one of the predefined areas, or '随机分配' for random assignment"
-                                    }
-                                },
-                                "required": ["sender", "area"]
-                            }
-                        },
-                        {
-                            "name": "change_auto_time",
-                            "description": "Set the automatic running start hour. Use -1 if the user wants a random time assigned",
-                            "inputSchema": {
-                                "type": "object",
-                                "properties": {
-                                    "sender": {
-                                        "type": "string",
-                                        "description": "Unique user identifier from the chat platform"
-                                    },
-                                    "auto_time": {
-                                        "type": "integer",
-                                        "minimum": -1,
-                                        "maximum": 23,
-                                        "description": "Hour of day to start running (7-23), or -1 to assign a random time"
-                                    }
-                                },
-                                "required": ["sender", "auto_time"]
-                            }
-                        },
-                        {
-                            "name": "change_auto_day",
-                            "description": "Set the days of the week for automatic running",
-                            "inputSchema": {
-                                "type": "object",
-                                "properties": {
-                                    "sender": {
-                                        "type": "string",
-                                        "description": "Unique user identifier from the chat platform"
-                                    },
-                                    "auto_day": {
-                                        "type": "array",
-                                        "items": {
-                                            "type": "integer",
-                                            "minimum": 0,
-                                            "maximum": 6
-                                        },
-                                        "description": "Days of week to run (0=Sunday, 6=Saturday)"
-                                    }
-                                },
-                                "required": ["sender", "auto_day"]
-                            }
-                        }
-                    ]
-                }
-            } else if (method === 'tools/call') {
-                const { name, arguments: args = {} } = params
-
-                let output
-
-                switch (name) {
-                    case "bind_account":
-                        output = await MCP.bind_account(args)
-                        break
-                    case "get_account_info":
-                        output = await MCP.get_account_info(args)
-                        break
-                    case "unbind_account":
-                        output = await MCP.unbind_account(args)
-                        break
-                    case "set_notification":
-                        output = await MCP.set_notification(args)
-                        break
-                    case "change_email":
-                        output = await MCP.change_email(args)
-                        break
-                    case "create_work_order":
-                        output = await MCP.create_work_order(args)
-                        break
-                    case "change_area":
-                        output = await MCP.change_area(args)
-                        break
-                    case "change_auto_time":
-                        output = await MCP.change_auto_time(args)
-                        break
-                    case "change_auto_day":
-                        output = await MCP.change_auto_day(args)
-                        break
-                    default:
-                        output = "未知工具"
-                }
-
-                result = { content: [{ type: "text", text: output }] }
-            } else {
-                result = { error: `未知方法: ${method}` }
-            }
-
-            // 返回标准 JSON-RPC 响应
-            return res.json({
-                jsonrpc: "2.0",
-                id: req_id ?? null,
-                result
-            })
-        } catch (e) {
-            return res.json({
-                jsonrpc: "2.0",
-                id: req_id ?? null,
-                error: { code: -32000, message: e.message }
-            })
-        }
-    }
-}
-
-module.exports.McpRpc = McpRpc

+ 0 - 17
apis/MCP/workorderMcp/GetMcp.js

@@ -1,17 +0,0 @@
-const API = require("../../../lib/API")
-
-class GetMcp extends API {
-    constructor() {
-        super()
-
-        this.noEncrypt()
-        this.setPath('/workorderMcp')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        res.json({"status": "ok"})
-    }
-}
-
-module.exports.GetMcp = GetMcp

+ 0 - 124
apis/MCP/workorderMcp/McpRPC.js

@@ -1,124 +0,0 @@
-const API = require("../../../lib/API.js")
-const MCP = require("../../../lib/Lepao/WorkOrderMcp.js").WORKORDERMCP
-
-class McpRpc extends API {
-    constructor() {
-        super()
-
-        this.noEncrypt()
-        this.setPath('/workorderMcp')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        const { method, params = {}, id: req_id } = req.body
-
-        try {
-            let result
-
-            if (method === 'initialize') {
-                result = {
-                    protocolVersion: "2024-11-05",
-                    capabilities: { "tools": {} },
-                    serverInfo: { name: "runforgeWorkOrder", version: "1.0" }
-                }
-            } else if (method === 'tools/list') {
-                result = {
-                    "tools": [
-                        {
-                            "name": "need_manual",
-                            "description": "提交转人工服务请求",
-                            "inputSchema": {
-                                "type": "object",
-                                "properties": {
-                                    "sender": {
-                                        "type": "string",
-                                        "description": "Unique user identifier from the chat platform"
-                                    },
-                                    "order_id": {
-                                        "type": "integer",
-                                        "description": "Order ID of the work order to submit"
-                                    }
-                                },
-                                "required": ["sender", "order_id"]
-                            }
-                        },
-                        {
-                            "name": "unbind_account",
-                            "description": "解绑被他人绑定的乐跑账号",
-                            "inputSchema": {
-                                "type": "object",
-                                "properties": {
-                                    "sender": {
-                                        "type": "string",
-                                        "description": "Unique user identifier from the chat platform"
-                                    },
-                                    "student_num": {
-                                        "type": "integer",
-                                        "description": "Student number of the account to unbind"
-                                    }
-                                },
-                                "required": ["sender", "student_num"]
-                            }
-                        },
-                        {
-                            "name": "get_account_info",
-                            "description": "按学号获取乐跑账号信息",
-                            "inputSchema": {
-                                "type": "object",
-                                "properties": {
-                                    "sender": {
-                                        "type": "string",
-                                        "description": "Unique user identifier from the chat platform"
-                                    },
-                                    "student_num": {
-                                        "type": "integer",
-                                        "description": "Student number of the account to get info"
-                                    }
-                                },
-                                "required": ["sender", "student_num"]
-                            }
-                        }
-                    ]
-                }
-            } else if (method === 'tools/call') {
-                const { name, arguments: args = {} } = params
-
-                let output
-
-                switch (name) {
-                    case "need_manual":
-                        output = await MCP.need_manual(args)
-                        break
-                    case "get_account_info":
-                        output = await MCP.get_account_info(args)
-                        break
-                    case "unbind_account":
-                        output = await MCP.unbind_account(args)
-                        break
-                    default:
-                        output = "未知工具"
-                }
-
-                result = { content: [{ type: "text", text: output }] }
-            } else {
-                result = { error: `未知方法: ${method}` }
-            }
-
-            // 返回标准 JSON-RPC 响应
-            return res.json({
-                jsonrpc: "2.0",
-                id: req_id ?? null,
-                result
-            })
-        } catch (e) {
-            return res.json({
-                jsonrpc: "2.0",
-                id: req_id ?? null,
-                error: { code: -32000, message: e.message }
-            })
-        }
-    }
-}
-
-module.exports.McpRpc = McpRpc

+ 4 - 7
apis/Order/CallBack.js

@@ -26,7 +26,7 @@ async function writePurchaseLedger(orderId, userUuid, addCount, logger) {
             remark: `订单号:${orderId}`
         })
     } catch (error) {
-        logger?.error?.(`写入购买次数流水失败 ${orderId}: ${error.stack || error}`)
+        logger?.error?.(`写入购买里程流水失败 ${orderId}: ${error.stack || error}`)
     }
 }
 
@@ -89,8 +89,6 @@ class CallBack extends API {
                 sql = `
                     SELECT 
                         g.lepao_count,
-                        g.ic_count,
-                        g.vip,
                         a.create_user
                     FROM 
                         orders a
@@ -108,11 +106,10 @@ class CallBack extends API {
                     return res.send('fail')
                 }
 
-                const { lepao_count, ic_count, vip, create_user } = rows[0]
+                const { lepao_count, create_user } = rows[0]
 
-                // 更新用户剩余次数
-                sql = 'UPDATE users SET lepao_count = lepao_count + ?, ic_count = ic_count + ?, vip = ? WHERE uuid = ?'
-                const updateUser = await db.query(sql, [lepao_count, ic_count, vip, create_user])
+                sql = 'UPDATE users SET lepao_count = lepao_count + ? WHERE uuid = ?'
+                const updateUser = await db.query(sql, [lepao_count, create_user])
                 if (!updateUser || updateUser.affectedRows !== 1) {
                     this.logger.error(`更新用户失败,UUID: ${create_user}`)
                     await db.query('UPDATE orders SET state = 4 WHERE orderId = ?', [out_trade_no])

+ 1 - 1
apis/Order/CreateOrder.js

@@ -172,7 +172,7 @@ class CreateOrder extends API {
 
                 const deviceType = req.headers['device-type'] ?? '浏览器'
                 let return_url
-                if (deviceType === 'RunForge Uniapp Client')
+                if (deviceType === 'RunForge Uniapp Client' || deviceType === '沐晨乐跑 Uniapp Client')
                     return_url = paymentConfig.uni_return_url + orderId
                 else
                     return_url = paymentConfig.return_url + orderId

+ 0 - 42
apis/Power/Admin/GetChangeRecord.js

@@ -1,42 +0,0 @@
-const API = require("../../../lib/API.js")
-const db = require("../../../plugin/DataBase/db.js")
-const { BaseStdResponse } = require("../../../BaseStdResponse.js")
-const AccessControl = require("../../../lib/AccessControl.js")
-
-class AdminPowerGetChangeRecord extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/Power/Task/GetChangeRecord')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        let { uuid, session, id } = req.query
-
-        if ([uuid, session, id].some(v => v === '' || v === null || v === undefined))
-            return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-
-        if (!await AccessControl.checkSession(uuid, session))
-            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
-
-        const permission = await AccessControl.getPermission(uuid)
-        if (!permission.includes('admin') && !permission.includes('server') && !permission.includes('service'))
-            return res.json({ ...BaseStdResponse.PERMISSION_DENIED })
-
-        const sql = `
-            SELECT *
-            FROM power_change_record
-            WHERE task_id = ?
-            ORDER BY id DESC
-        `
-        const rows = await db.query(sql, [id])
-        if (!rows) return res.json({ ...BaseStdResponse.DATABASE_ERR })
-
-        return res.json({
-            ...BaseStdResponse.OK,
-            data: rows
-        })
-    }
-}
-
-module.exports.AdminPowerGetChangeRecord = AdminPowerGetChangeRecord

+ 0 - 129
apis/Power/Admin/List.js

@@ -1,129 +0,0 @@
-const API = require("../../../lib/API.js")
-const db = require("../../../plugin/DataBase/db.js")
-const { BaseStdResponse } = require("../../../BaseStdResponse.js")
-const AccessControl = require("../../../lib/AccessControl.js")
-
-class AdminPowerTaskList extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/Power/Task/List')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        let {
-            uuid,
-            session,
-            create_user,
-            username,
-            email,
-            area,
-            building,
-            room,
-            queryTime,
-            pagesize,
-            current
-        } = req.query
-
-        if ([uuid, session, pagesize, current].some(v => v === '' || v === null || v === undefined))
-            return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-        if (isNaN(pagesize) || Number(pagesize) <= 0 || isNaN(current) || Number(current) <= 0)
-            return res.json({ ...BaseStdResponse.ERR, msg: '参数错误' })
-
-        if (!await AccessControl.checkSession(uuid, session))
-            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
-
-        const permission = await AccessControl.getPermission(uuid)
-        if (!permission.includes('admin') && !permission.includes('server') && !permission.includes('service'))
-            return res.json({ ...BaseStdResponse.PERMISSION_DENIED })
-
-        const offset = (Number(current) - 1) * Number(pagesize)
-        const where = ['1 = 1']
-        const params = []
-        const countParams = []
-
-        if (create_user) {
-            where.push('pt.create_user COLLATE utf8mb4_general_ci = (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(create_user)
-            countParams.push(create_user)
-        }
-        if (username) {
-            where.push('u.username COLLATE utf8mb4_general_ci LIKE (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(`%${username}%`)
-            countParams.push(`%${username}%`)
-        }
-        if (email) {
-            where.push('pt.email COLLATE utf8mb4_general_ci LIKE (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(`%${email}%`)
-            countParams.push(`%${email}%`)
-        }
-        if (area) {
-            where.push('pt.area COLLATE utf8mb4_general_ci = (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(area)
-            countParams.push(area)
-        }
-        if (building) {
-            where.push('pt.building COLLATE utf8mb4_general_ci LIKE (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(`%${building}%`)
-            countParams.push(`%${building}%`)
-        }
-        if (room) {
-            where.push('pt.room COLLATE utf8mb4_general_ci LIKE (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(`%${room}%`)
-            countParams.push(`%${room}%`)
-        }
-        if (Array.isArray(queryTime) && queryTime.length === 2) {
-            where.push('pt.update_time >= ? AND pt.update_time < ?')
-            params.push(queryTime[0], queryTime[1])
-            countParams.push(queryTime[0], queryTime[1])
-        }
-
-        const whereSql = where.join(' AND ')
-        const listSql = `
-            SELECT
-                pt.id,
-                pt.create_user,
-                pt.create_time,
-                pt.update_time,
-                pt.area,
-                pt.building,
-                pt.room,
-                pt.email,
-                pt.lowest,
-                pt.notes,
-                pt.balance,
-                pt.koufei_date,
-                pt.is_notice,
-                u.username,
-                u.avatar
-            FROM power_task pt
-            LEFT JOIN users u ON u.uuid COLLATE utf8mb4_general_ci = pt.create_user COLLATE utf8mb4_general_ci
-            WHERE ${whereSql}
-            ORDER BY pt.id DESC
-            LIMIT ? OFFSET ?
-        `
-        const countSql = `
-            SELECT COUNT(*) AS total
-            FROM power_task pt
-            LEFT JOIN users u ON u.uuid COLLATE utf8mb4_general_ci = pt.create_user COLLATE utf8mb4_general_ci
-            WHERE ${whereSql}
-        `
-        params.push(String(pagesize), String(offset))
-
-        const rows = await db.query(listSql, params)
-        const countRows = await db.query(countSql, countParams)
-        if (!rows || !countRows) return res.json({ ...BaseStdResponse.DATABASE_ERR })
-
-        return res.json({
-            ...BaseStdResponse.OK,
-            data: rows,
-            pagination: {
-                current: Number(current),
-                pagesize: Number(pagesize),
-                total: countRows[0]?.total || 0
-            }
-        })
-    }
-}
-
-module.exports.AdminPowerTaskList = AdminPowerTaskList

+ 4 - 4
apis/Public/GetAndroidAppVersion.js

@@ -11,13 +11,13 @@ class GetAndroidAppVersion extends API {
 
     async onRequest(req, res) {
         const data = {
-            describe: '<p>1.修复无法登录乐跑小程序的问题</p> <p>2、本次升级为强制整包升级,请在更新包下载完毕后点击安装</p><p><small style="color: red">更新过程中请勿关闭程序或切到后台,以免导致升级失败</small></p>',
-            edition_url: 'https://lepao-cloud.xxoo365.top/down.php/1c1538a08b791cebc1aa4462ed051fad.apk', //apk、wgt包下载地址或者应用市场地址  安卓应用市场 market://details?id=xxxx 苹果store itms-apps://itunes.apple.com/cn/app/xxxxxx
+            describe: '<p>1、优化应用内核,加快响应速度</p> <p>2、修复已知问题</p><p><small style="color: red">更新过程中请勿关闭程序或切到后台,以免导致升级失败</small></p>',
+            edition_url: 'https://lepao-cloud.xxoo365.top/down.php/3f437e526e37b47d84b539457cb3d49f.apk', //apk、wgt包下载地址或者应用市场地址  安卓应用市场 market://details?id=xxxx 苹果store itms-apps://itunes.apple.com/cn/app/xxxxxx
             edition_force: 1, //是否强制更新 0代表否 1代表是
             package_type: 0, //0是整包升级(apk或者appstore或者安卓应用市场) 1是wgt升级
             edition_issue: 1, //是否发行  0否 1是 为了控制上架应用市场审核时不能弹出热更新框
-            edition_number: 131, //版本号 最重要的manifest里的版本号 (检查更新主要以服务器返回的edition_number版本号是否大于当前app的版本号来实现是否更新)
-            edition_name: '1.3.1', // 版本名称 manifest里的版本名称
+            edition_number: 121, //版本号 最重要的manifest里的版本号 (检查更新主要以服务器返回的edition_number版本号是否大于当前app的版本号来实现是否更新)
+            edition_name: '1.2.1', // 版本名称 manifest里的版本名称
             edition_silence: 0, // 是否静默更新 0代表否 1代表是
             log: '1、优化应用内核,加快响应速度; 2、修复已知问题' //系统设置内日志
         }

+ 2 - 2
apis/Public/GetAppVersion.js

@@ -13,8 +13,8 @@ class GetAppVersion extends API {
         res.json({
             ...BaseStdResponse.OK,
             data: {
-                version: '3.2',
-                msg: '\n更新内容:增强在线下单功能,支持自助退款'
+                version: '2.0',
+                msg: '\n更新内容:\n重构乐跑逻辑 优化乐跑任务调度功能'
             }
         })
     }

+ 0 - 314
apis/QK/Admin/Admin.js

@@ -1,314 +0,0 @@
-const API = require('../../../lib/API')
-const { BaseStdResponse } = require('../../../BaseStdResponse')
-const { TaskScheduler } = require('../../../lib/QK/TaskScheduler')
-
-const scheduler = new TaskScheduler()
-
-function fail(res, err, fallback = '后台抢课管理操作失败') {
-    return res.json({
-        ...BaseStdResponse.ERR,
-        msg: err?.message || fallback
-    })
-}
-
-class ListClient extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/QK/Client/List')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const list = await scheduler.listClients()
-            return res.json({ ...BaseStdResponse.OK, data: list })
-        } catch (err) {
-            this.logger.error(`获取抢课客户端列表失败:${err.stack || err}`)
-            return fail(res, err, '获取抢课客户端列表失败')
-        }
-    }
-}
-
-class DeleteClient extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/QK/Client/Delete')
-        this.setMethod('DELETE')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { client_id } = req.body
-            if (!client_id) {
-                return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-            }
-            await scheduler.deleteClient(client_id)
-            this.logger.info(`[QK][API][DeleteClient] 已禁用客户端 clientId=${client_id}`)
-            return res.json({ ...BaseStdResponse.OK })
-        } catch (err) {
-            this.logger.error(`禁用抢课客户端失败:${err.stack || err}`)
-            return fail(res, err, '禁用抢课客户端失败')
-        }
-    }
-}
-
-class EnableClient extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/QK/Client/Enable')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { client_id } = req.body
-            if (!client_id) {
-                return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-            }
-            await scheduler.enableClient(client_id)
-            this.logger.info(`[QK][API][EnableClient] 已解禁客户端 clientId=${client_id}`)
-            return res.json({ ...BaseStdResponse.OK })
-        } catch (err) {
-            this.logger.error(`解禁抢课客户端失败:${err.stack || err}`)
-            return fail(res, err, '解禁抢课客户端失败')
-        }
-    }
-}
-
-class ListTask extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/QK/Task/List')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const result = await scheduler.listAdminTasks(req.query)
-            return res.json({ ...BaseStdResponse.OK, data: result })
-        } catch (err) {
-            this.logger.error(`获取抢课任务列表失败:${err.stack || err}`)
-            return fail(res, err, '获取抢课任务列表失败')
-        }
-    }
-}
-
-class GetTask extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/QK/Task/Detail')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { id } = req.query
-            if (!id) {
-                return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-            }
-            const task = await scheduler.getAdminTask(id)
-            if (!task) {
-                return res.json({ ...BaseStdResponse.ERR, msg: '任务不存在' })
-            }
-            return res.json({ ...BaseStdResponse.OK, data: task })
-        } catch (err) {
-            this.logger.error(`获取抢课任务详情失败:${err.stack || err}`)
-            return fail(res, err, '获取抢课任务详情失败')
-        }
-    }
-}
-
-class UpdateTask extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/QK/Task/Update')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { id } = req.body
-            if (!id) {
-                return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-            }
-            await scheduler.adminUpdateTask(id, req.body)
-            this.logger.info(`[QK][API][AdminUpdateTask] 管理员更新任务 taskId=${id}`)
-            return res.json({ ...BaseStdResponse.OK })
-        } catch (err) {
-            this.logger.error(`管理员更新抢课任务失败:${err.stack || err}`)
-            return fail(res, err, '更新抢课任务失败')
-        }
-    }
-}
-
-class CancelTask extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/QK/Task/Cancel')
-        this.setMethod('DELETE')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { id } = req.body
-            if (!id) {
-                return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-            }
-            await scheduler.adminCancelTask(id)
-            this.logger.info(`[QK][API][AdminCancelTask] 管理员取消任务 taskId=${id}`)
-            return res.json({ ...BaseStdResponse.OK })
-        } catch (err) {
-            this.logger.error(`管理员取消抢课任务失败:${err.stack || err}`)
-            return fail(res, err, '取消抢课任务失败')
-        }
-    }
-}
-
-class RetryTask extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/QK/Task/Retry')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { id } = req.body
-            if (!id) {
-                return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-            }
-            await scheduler.adminRetryTask(id)
-            this.logger.info(`[QK][API][AdminRetryTask] 管理员重试任务 taskId=${id}`)
-            return res.json({ ...BaseStdResponse.OK })
-        } catch (err) {
-            this.logger.error(`管理员重试抢课任务失败:${err.stack || err}`)
-            return fail(res, err, '重试抢课任务失败')
-        }
-    }
-}
-
-class StartTask extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/QK/Task/Start')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { id } = req.body
-            if (!id) {
-                return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-            }
-            await scheduler.adminStartTask(id)
-            this.logger.info(`[QK][API][AdminStartTask] 管理员开启任务 taskId=${id}`)
-            return res.json({ ...BaseStdResponse.OK })
-        } catch (err) {
-            this.logger.error(`管理员开启抢课任务失败:${err.stack || err}`)
-            return fail(res, err, '开启抢课任务失败')
-        }
-    }
-}
-
-class PauseTask extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/QK/Task/Pause')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { id } = req.body
-            if (!id) {
-                return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-            }
-            await scheduler.adminPauseTask(id)
-            this.logger.info(`[QK][API][AdminPauseTask] 管理员暂停任务 taskId=${id}`)
-            return res.json({ ...BaseStdResponse.OK })
-        } catch (err) {
-            this.logger.error(`管理员暂停抢课任务失败:${err.stack || err}`)
-            return fail(res, err, '暂停抢课任务失败')
-        }
-    }
-}
-
-class ListReport extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/QK/Report/List')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const result = await scheduler.listAdminReports(req.query)
-            return res.json({ ...BaseStdResponse.OK, data: result })
-        } catch (err) {
-            this.logger.error(`获取抢课回报记录失败:${err.stack || err}`)
-            return fail(res, err, '获取抢课回报记录失败')
-        }
-    }
-}
-
-class ListBatch extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/QK/Batch/List')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const list = await scheduler.listBatches()
-            return res.json({ ...BaseStdResponse.OK, data: list })
-        } catch (err) {
-            this.logger.error(`获取抢课批次列表失败:${err.stack || err}`)
-            return fail(res, err, '获取抢课批次列表失败')
-        }
-    }
-}
-
-class SaveBatch extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/QK/Batch/Save')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { id, name, jx0502zbid, enabled } = req.body
-            if (!name || !jx0502zbid) {
-                return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-            }
-            if (id) {
-                await scheduler.updateBatch(id, req.body)
-                this.logger.info(`[QK][API][SaveBatch] 更新批次 batchId=${id}`)
-                return res.json({ ...BaseStdResponse.OK, data: { id } })
-            }
-            const batchId = await scheduler.createBatch({ name, jx0502zbid, enabled })
-            this.logger.info(`[QK][API][SaveBatch] 创建批次 batchId=${batchId}`)
-            return res.json({ ...BaseStdResponse.OK, data: { id: batchId } })
-        } catch (err) {
-            this.logger.error(`保存抢课批次失败:${err.stack || err}`)
-            return fail(res, err, '保存抢课批次失败')
-        }
-    }
-}
-
-module.exports = {
-    ListClient,
-    DeleteClient,
-    EnableClient,
-    ListTask,
-    GetTask,
-    UpdateTask,
-    CancelTask,
-    RetryTask,
-    StartTask,
-    PauseTask,
-    ListReport,
-    ListBatch,
-    SaveBatch
-}

+ 0 - 30
apis/QK/Batch/Batch.js

@@ -1,30 +0,0 @@
-const API = require('../../../lib/API')
-const { BaseStdResponse } = require('../../../BaseStdResponse')
-const { TaskScheduler } = require('../../../lib/QK/TaskScheduler')
-
-const scheduler = new TaskScheduler()
-
-class ListBatch extends API {
-    constructor() {
-        super()
-        this.setPath('/QK/Batch')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const list = await scheduler.listBatches({ enabledOnly: true })
-            return res.json({ ...BaseStdResponse.OK, data: list })
-        } catch (err) {
-            this.logger.error(`获取抢课批次失败:${err.stack || err}`)
-            return res.json({
-                ...BaseStdResponse.ERR,
-                msg: err?.message || '获取抢课批次失败'
-            })
-        }
-    }
-}
-
-module.exports = {
-    ListBatch
-}

+ 0 - 173
apis/QK/Client/Client.js

@@ -1,173 +0,0 @@
-const API = require('../../../lib/API')
-const { BaseStdResponse } = require('../../../BaseStdResponse')
-const { TaskScheduler } = require('../../../lib/QK/TaskScheduler')
-
-const scheduler = new TaskScheduler()
-
-function getClientCredentials(body) {
-    return {
-        clientId: body.client_id,
-        clientSecret: body.client_secret
-    }
-}
-
-function fail(res, err, fallback = '客户端请求失败') {
-    return res.json({
-        ...BaseStdResponse.ERR,
-        msg: err?.message || fallback
-    })
-}
-
-class RegisterClient extends API {
-    constructor() {
-        super()
-        this.setPath('/QK/Client/Register')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { clientId, clientSecret } = getClientCredentials(req.body)
-            const result = await scheduler.registerClient(clientId, clientSecret, req.body)
-            this.logger.info(`[QK][API][Register] 客户端注册成功 clientId=${result.client_id} max_slots=${result.max_slots}`)
-            return res.json({ ...BaseStdResponse.OK, data: result })
-        } catch (err) {
-            this.logger.error(`抢课客户端注册失败:${err.stack || err}`)
-            return fail(res, err, '抢课客户端注册失败')
-        }
-    }
-}
-
-class HeartbeatClient extends API {
-    constructor() {
-        super()
-        this.setPath('/QK/Client/Heartbeat')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { clientId, clientSecret } = getClientCredentials(req.body)
-            const result = await scheduler.heartbeat(clientId, clientSecret, req.body)
-            return res.json({ ...BaseStdResponse.OK, data: result })
-        } catch (err) {
-            this.logger.error(`抢课客户端心跳失败:${err.stack || err}`)
-            return fail(res, err, '抢课客户端心跳失败')
-        }
-    }
-}
-
-class PullTasks extends API {
-    constructor() {
-        super()
-        this.setPath('/QK/Client/PullTasks')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { clientId, clientSecret } = getClientCredentials(req.body)
-            const tasks = await scheduler.pullTasks(clientId, clientSecret, req.body.count)
-            if (tasks.length > 0) {
-                this.logger.info(`[QK][API][PullTasks] clientId=${clientId} 拉取到 ${tasks.length} 个任务 taskIds=${tasks.map(t => t.id).join(',')}`)
-            }
-            return res.json({ ...BaseStdResponse.OK, data: tasks })
-        } catch (err) {
-            this.logger.error(`抢课客户端拉取任务失败:${err.stack || err}`)
-            return fail(res, err, '抢课客户端拉取任务失败')
-        }
-    }
-}
-
-class ReclaimTasks extends API {
-    constructor() {
-        super()
-        this.setPath('/QK/Client/ReclaimTasks')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { clientId, clientSecret } = getClientCredentials(req.body)
-            const tasks = await scheduler.reclaimTasks(clientId, clientSecret)
-            if (tasks.length > 0) {
-                this.logger.info(`[QK][API][ReclaimTasks] clientId=${clientId} 回收 ${tasks.length} 个任务 taskIds=${tasks.map(t => t.id).join(',')}`)
-            }
-            return res.json({ ...BaseStdResponse.OK, data: tasks })
-        } catch (err) {
-            this.logger.error(`抢课客户端回收任务失败:${err.stack || err}`)
-            return fail(res, err, '抢课客户端回收任务失败')
-        }
-    }
-}
-
-class ReleaseTasks extends API {
-    constructor() {
-        super()
-        this.setPath('/QK/Client/ReleaseTasks')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { clientId, clientSecret } = getClientCredentials(req.body)
-            const result = await scheduler.releaseTasks(clientId, clientSecret, req.body)
-            if (result.released > 0) {
-                this.logger.info(`[QK][API][ReleaseTasks] clientId=${clientId} 释放 ${result.released} 个任务 taskIds=${result.task_ids.join(',')}`)
-            }
-            return res.json({ ...BaseStdResponse.OK, data: result })
-        } catch (err) {
-            this.logger.error(`抢课客户端释放任务失败:${err.stack || err}`)
-            return fail(res, err, '抢课客户端释放任务失败')
-        }
-    }
-}
-
-class ReportResult extends API {
-    constructor() {
-        super()
-        this.setPath('/QK/Client/ReportResult')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { clientId, clientSecret } = getClientCredentials(req.body)
-            const result = await scheduler.reportResult(clientId, clientSecret, req.body)
-            this.logger.info(`[QK][API][ReportResult] clientId=${clientId} taskId=${result.task_id} status=${result.status}`)
-            return res.json({ ...BaseStdResponse.OK, data: result })
-        } catch (err) {
-            this.logger.error(`抢课客户端上报结果失败:${err.stack || err}`)
-            return fail(res, err, '抢课客户端上报结果失败')
-        }
-    }
-}
-
-class ReportProgress extends API {
-    constructor() {
-        super()
-        this.setPath('/QK/Client/ReportProgress')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { clientId, clientSecret } = getClientCredentials(req.body)
-            const result = await scheduler.reportProgress(clientId, clientSecret, req.body)
-            return res.json({ ...BaseStdResponse.OK, data: result })
-        } catch (err) {
-            this.logger.error(`抢课客户端上报进度失败:${err.stack || err}`)
-            return fail(res, err, '抢课客户端上报进度失败')
-        }
-    }
-}
-
-module.exports = {
-    RegisterClient,
-    HeartbeatClient,
-    PullTasks,
-    ReclaimTasks,
-    ReleaseTasks,
-    ReportResult,
-    ReportProgress
-}

+ 0 - 162
apis/QK/Task/Task.js

@@ -1,162 +0,0 @@
-const API = require('../../../lib/API')
-const { BaseStdResponse } = require('../../../BaseStdResponse')
-const { TaskScheduler } = require('../../../lib/QK/TaskScheduler')
-
-const scheduler = new TaskScheduler()
-
-function fail(res, err, fallback = '抢课任务操作失败') {
-    return res.json({
-        ...BaseStdResponse.ERR,
-        msg: err?.message || fallback
-    })
-}
-
-class GetTask extends API {
-    constructor() {
-        super()
-        this.setPath('/QK/Task')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { uuid, status, student_num, name, batch_id, course_name, course } = req.query
-            const list = await scheduler.listUserTasks(uuid, { status, student_num, name, batch_id, course_name, course })
-            return res.json({ ...BaseStdResponse.OK, data: list })
-        } catch (err) {
-            this.logger.error(`获取抢课任务失败:${err.stack || err}`)
-            return fail(res, err, '获取抢课任务失败')
-        }
-    }
-}
-
-class GetTaskDetail extends API {
-    constructor() {
-        super()
-        this.setPath('/QK/Task/Detail')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { uuid, id } = req.query
-            if (!id) {
-                return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-            }
-            const detail = await scheduler.getTaskDetail(uuid, id)
-            if (!detail) {
-                return res.json({ ...BaseStdResponse.ERR, msg: '任务不存在' })
-            }
-            return res.json({ ...BaseStdResponse.OK, data: detail })
-        } catch (err) {
-            this.logger.error(`获取抢课任务详情失败:${err.stack || err}`)
-            return fail(res, err, '获取抢课任务详情失败')
-        }
-    }
-}
-
-class SaveTask extends API {
-    constructor() {
-        super()
-        this.setPath('/QK/Task')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { uuid, id, batch_id, jw_account_id } = req.body
-            if ([uuid, batch_id, jw_account_id].some(value => value === '' || value === null || value === undefined)) {
-                return res.json({ ...BaseStdResponse.MISSING_PARAMETER, msg: '请选择选课批次和统一身份认证账号' })
-            }
-            if (id) {
-                await scheduler.updateTask(uuid, id, req.body)
-                this.logger.info(`[QK][API][SaveTask] 用户更新任务 uuid=${uuid} taskId=${id}`)
-                return res.json({ ...BaseStdResponse.OK })
-            }
-            const taskId = await scheduler.createTask(uuid, req.body)
-            this.logger.info(`[QK][API][SaveTask] 用户创建任务 uuid=${uuid} taskId=${taskId} jw_account_id=${jw_account_id}`)
-            return res.json({ ...BaseStdResponse.OK, data: { id: taskId } })
-        } catch (err) {
-            this.logger.error(`保存抢课任务失败:${err.stack || err}`)
-            return fail(res, err, '保存抢课任务失败')
-        }
-    }
-}
-
-class DeleteTask extends API {
-    constructor() {
-        super()
-        this.setPath('/QK/Task')
-        this.setMethod('DELETE')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { uuid, id } = req.body
-            if (!id) {
-                return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-            }
-            await scheduler.cancelTask(uuid, id)
-            this.logger.info(`[QK][API][DeleteTask] 用户取消任务 uuid=${uuid} taskId=${id}`)
-            return res.json({ ...BaseStdResponse.OK })
-        } catch (err) {
-            this.logger.error(`取消抢课任务失败:${err.stack || err}`)
-            return fail(res, err, '取消抢课任务失败')
-        }
-    }
-}
-
-class StartTask extends API {
-    constructor() {
-        super()
-        this.setPath('/QK/Task/Start')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { uuid, id } = req.body
-            if (!id) {
-                return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-            }
-            await scheduler.startTask(uuid, id)
-            this.logger.info(`[QK][API][StartTask] 用户开启任务 uuid=${uuid} taskId=${id}`)
-            return res.json({ ...BaseStdResponse.OK })
-        } catch (err) {
-            this.logger.error(`开启抢课任务失败:${err.stack || err}`)
-            return fail(res, err, '开启抢课任务失败')
-        }
-    }
-}
-
-class PauseTask extends API {
-    constructor() {
-        super()
-        this.setPath('/QK/Task/Pause')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        try {
-            const { uuid, id } = req.body
-            if (!id) {
-                return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-            }
-            await scheduler.pauseTask(uuid, id)
-            this.logger.info(`[QK][API][PauseTask] 用户暂停任务 uuid=${uuid} taskId=${id}`)
-            return res.json({ ...BaseStdResponse.OK })
-        } catch (err) {
-            this.logger.error(`暂停抢课任务失败:${err.stack || err}`)
-            return fail(res, err, '暂停抢课任务失败')
-        }
-    }
-}
-
-module.exports = {
-    GetTask,
-    GetTaskDetail,
-    SaveTask,
-    DeleteTask,
-    StartTask,
-    PauseTask
-}

+ 0 - 72
apis/QuXuanShu/Admin/Detail.js

@@ -1,72 +0,0 @@
-const API = require("../../../lib/API.js")
-const db = require("../../../plugin/DataBase/db.js")
-const { BaseStdResponse } = require("../../../BaseStdResponse.js")
-const AccessControl = require("../../../lib/AccessControl.js")
-
-function parseBookList(value) {
-    if (value === null || value === undefined || value === '') return []
-    if (Array.isArray(value)) return value
-    try {
-        const parsed = JSON.parse(value)
-        return Array.isArray(parsed) ? parsed : []
-    } catch {
-        return []
-    }
-}
-
-function extractClassName(bookList) {
-    if (!Array.isArray(bookList) || bookList.length === 0) return ''
-    const book = bookList.find(b => b && b.className)
-    return book?.className ?? ''
-}
-
-class AdminQxsAccountDetail extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/QXS/Account/Detail')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        let { uuid, session, id } = req.query
-
-        if ([uuid, session, id].some(v => v === '' || v === null || v === undefined))
-            return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-
-        if (!await AccessControl.checkSession(uuid, session))
-            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
-
-        const permission = await AccessControl.getPermission(uuid)
-        if (!permission.includes('admin') && !permission.includes('server') && !permission.includes('service'))
-            return res.json({ ...BaseStdResponse.PERMISSION_DENIED })
-
-        const sql = `
-            SELECT id, username, create_time, realname, email, mobile, termName, book_list
-            FROM qsx_account
-            WHERE id = ?
-            LIMIT 1
-        `
-        const rows = await db.query(sql, [id])
-        if (!rows) return res.json({ ...BaseStdResponse.DATABASE_ERR })
-        if (!rows.length) return res.json({ ...BaseStdResponse.ERR, msg: '记录不存在' })
-
-        const row = rows[0]
-        const bookList = parseBookList(row.book_list)
-        return res.json({
-            ...BaseStdResponse.OK,
-            data: {
-                id: row.id,
-                username: row.username,
-                create_time: row.create_time,
-                realname: row.realname,
-                email: row.email,
-                mobile: row.mobile,
-                termName: row.termName,
-                className: extractClassName(bookList),
-                bookList
-            }
-        })
-    }
-}
-
-module.exports.AdminQxsAccountDetail = AdminQxsAccountDetail

+ 0 - 148
apis/QuXuanShu/Admin/List.js

@@ -1,148 +0,0 @@
-const API = require("../../../lib/API.js")
-const db = require("../../../plugin/DataBase/db.js")
-const { BaseStdResponse } = require("../../../BaseStdResponse.js")
-const AccessControl = require("../../../lib/AccessControl.js")
-
-function parseBookList(value) {
-    if (value === null || value === undefined || value === '') return []
-    if (Array.isArray(value)) return value
-    try {
-        const parsed = JSON.parse(value)
-        return Array.isArray(parsed) ? parsed : []
-    } catch {
-        return []
-    }
-}
-
-function extractClassName(bookList) {
-    if (!Array.isArray(bookList) || bookList.length === 0) return ''
-    const book = bookList.find(b => b && b.className)
-    return book?.className ?? ''
-}
-
-class AdminQxsAccountList extends API {
-    constructor() {
-        super()
-        this.setPath('/Admin/QXS/Account/List')
-        this.setMethod('GET')
-    }
-
-    async onRequest(req, res) {
-        let {
-            uuid,
-            session,
-            username,
-            realname,
-            email,
-            mobile,
-            termName,
-            className,
-            queryTime,
-            pagesize,
-            current
-        } = req.query
-
-        if ([uuid, session, pagesize, current].some(v => v === '' || v === null || v === undefined))
-            return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-        if (isNaN(pagesize) || Number(pagesize) <= 0 || isNaN(current) || Number(current) <= 0)
-            return res.json({ ...BaseStdResponse.ERR, msg: '参数错误' })
-
-        if (!await AccessControl.checkSession(uuid, session))
-            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
-
-        const permission = await AccessControl.getPermission(uuid)
-        if (!permission.includes('admin') && !permission.includes('server') && !permission.includes('service'))
-            return res.json({ ...BaseStdResponse.PERMISSION_DENIED })
-
-        const offset = (Number(current) - 1) * Number(pagesize)
-        const where = ['1 = 1']
-        const params = []
-        const countParams = []
-
-        if (username) {
-            where.push('qa.username COLLATE utf8mb4_general_ci LIKE (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(`%${username}%`)
-            countParams.push(`%${username}%`)
-        }
-        if (realname) {
-            where.push('qa.realname COLLATE utf8mb4_general_ci LIKE (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(`%${realname}%`)
-            countParams.push(`%${realname}%`)
-        }
-        if (email) {
-            where.push('qa.email COLLATE utf8mb4_general_ci LIKE (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(`%${email}%`)
-            countParams.push(`%${email}%`)
-        }
-        if (mobile) {
-            where.push('qa.mobile COLLATE utf8mb4_general_ci LIKE (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(`%${mobile}%`)
-            countParams.push(`%${mobile}%`)
-        }
-        if (termName) {
-            where.push('qa.termName COLLATE utf8mb4_general_ci LIKE (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(`%${termName}%`)
-            countParams.push(`%${termName}%`)
-        }
-        if (className) {
-            where.push('qa.book_list COLLATE utf8mb4_general_ci LIKE (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(`%"className"%${className}%`)
-            countParams.push(`%"className"%${className}%`)
-        }
-        if (Array.isArray(queryTime) && queryTime.length === 2) {
-            where.push('qa.create_time >= ? AND qa.create_time < ?')
-            params.push(queryTime[0], queryTime[1])
-            countParams.push(queryTime[0], queryTime[1])
-        }
-
-        const whereSql = where.join(' AND ')
-        const listSql = `
-            SELECT
-                qa.id,
-                qa.username,
-                qa.create_time,
-                qa.realname,
-                qa.email,
-                qa.mobile,
-                qa.termName,
-                qa.book_list
-            FROM qsx_account qa
-            WHERE ${whereSql}
-            ORDER BY qa.id DESC
-            LIMIT ? OFFSET ?
-        `
-        const countSql = `SELECT COUNT(*) AS total FROM qsx_account qa WHERE ${whereSql}`
-        params.push(String(pagesize), String(offset))
-
-        const rows = await db.query(listSql, params)
-        const countRows = await db.query(countSql, countParams)
-        if (!rows || !countRows) return res.json({ ...BaseStdResponse.DATABASE_ERR })
-
-        const data = rows.map(item => {
-            const bookList = parseBookList(item.book_list)
-            return {
-                id: item.id,
-                username: item.username,
-                create_time: item.create_time,
-                realname: item.realname,
-                email: item.email,
-                mobile: item.mobile,
-                termName: item.termName,
-                className: extractClassName(bookList),
-                book_count: bookList.length
-            }
-        })
-
-        return res.json({
-            ...BaseStdResponse.OK,
-            data,
-            pagination: {
-                current: Number(current),
-                pagesize: Number(pagesize),
-                total: countRows[0]?.total || 0
-            }
-        })
-    }
-}
-
-module.exports.AdminQxsAccountList = AdminQxsAccountList

+ 0 - 233
apis/QuXuanShu/GetBookList.js

@@ -1,233 +0,0 @@
-const API = require("../../lib/API.js")
-const db = require("../../plugin/DataBase/db.js")
-const { axiosWithQgOutbound } = require("../../lib/Lepao/qgOutboundAxios")
-const OSS = require("ali-oss")
-const { BaseStdResponse } = require("../../BaseStdResponse.js")
-
-class GetBookList extends API {
-    constructor() {
-        super()
-
-        this.setPath("/QXS/GetBookList")
-        this.setMethod("POST")
-
-        this.UserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090a13) UnifiedPCWindowsWechat(0xf254100e) XWEB/16283'
-        this.Refer = 'https://servicewechat.com/wxeadc3e8bceec3d03/177/page-frame.html'
-    }
-
-    async qxsLogin(username, password) {
-        const endpoint = "https://api.quxuanshu.com/pass/login"
-        const reqData = {
-            account: username,
-            password,
-            loginMethod: 0,
-            isAutoLogin: "0",
-            multiUserType: "0,1,2,3,4,5"
-        }
-
-        const res = await axiosWithQgOutbound({
-            method: 'post',
-            url: endpoint,
-            data: reqData,
-            headers: {
-                "User-Agent": this.UserAgent,
-                "Referer": this.Refer
-            },
-            timeout: 15000,
-            logger: this.logger,
-            scene: 'QXSLogin'
-        })
-
-        const data = res.data
-        if (!data || data.code !== 0 || !data.data?.accessToken) {
-            throw new Error(data?.msg ?? "请稍后再试")
-        }
-        return { accessToken: data.data.accessToken, userId: data.data.userId }
-    }
-
-    async qsxUserInfo(accessToken) {
-        const endpoint = "https://api.quxuanshu.com/pass/loginInfo"
-        const res = await axiosWithQgOutbound({
-            method: 'get',
-            url: endpoint,
-            headers: {
-                accessToken,
-                "User-Agent": this.UserAgent,
-                "Referer": this.Refer,
-            },
-            timeout: 15000,
-            logger: this.logger,
-            scene: 'QXSUserInfo'
-        })
-        const data = res.data
-        if (!data || data.code !== 0 || !data.data) {
-            throw new Error(data?.msg ?? "请稍后再试")
-        }
-        return { userInfo: data.data }
-    }
-
-    async qsxGetList(accessToken, termCode) {
-        const endpoint = "https://api.quxuanshu.com/student/order/toOrder/list"
-        const res = await axiosWithQgOutbound({
-            method: 'post',
-            url: endpoint,
-            data: { termCode },
-            headers: {
-                accessToken,
-                "User-Agent": this.UserAgent,
-                "Referer": this.Refer
-            },
-            timeout: 20000,
-            logger: this.logger,
-            scene: 'QXSOrderList'
-        })
-        const data = res.data
-        if (!data || data.code !== 0 || !data.data) {
-            if(data.msg === 'success') {
-                throw new Error("书单未出或无订购数据,请稍后再试")
-            }
-
-            if (data.msg === '统一订购模式下无权限查看订购数据!') {
-                const endpoint = "https://api.quxuanshu.com/student/order/orderInfo/list"
-                const res = await axiosWithQgOutbound({
-                    method: 'post',
-                    url: endpoint,
-                    data: { type: 0, termCode },
-                    headers: {
-                        accessToken,
-                        "User-Agent": this.UserAgent,
-                        "Referer": this.Refer
-                    },
-                    timeout: 15000,
-                    logger: this.logger,
-                    scene: 'QXSOrderInfoList'
-                })
-                const data = res.data
-                if (!data || data.code !== 0 || !data.data || !data.data.list[0] || !data.data.list[0].orderItem) {
-                    throw new Error(data?.msg ?? "请稍后再试")
-                }
-
-                return { bookList: data.data.list[0].orderItem ?? [] }
-            }
-
-            throw new Error(data?.msg ?? "请稍后再试")
-        }
-        return { bookList: data.data.list ?? [] }
-    }
-
-    async qsxGetBookDetail(accessToken, termCode, title, isbn) {
-        const endpoint = "https://u.quxuanshu.com/api/teacher/textbook/textbookInfo/search"
-        const queryData = {
-            termCode,
-            searchKey: title,
-            platType: "3",
-            curPage: 1,
-            pageSize: 15,
-            checkCode: "",
-            awardTypeList: []
-        }
-        const res = await axiosWithQgOutbound({
-            method: 'post',
-            url: endpoint,
-            data: queryData,
-            headers: {
-                "AccessToken": accessToken,
-                "User-Agent": this.UserAgent,
-                "Referer": this.Refer
-            },
-            timeout: 15000,
-            logger: this.logger,
-            scene: 'QXSTextbookSearch'
-        })
-        let data
-        if (!res.data || !res.data.data || !res.data.data.list)
-            return null
-        else data = res.data.data.list
-        const book = data.find(b => b.isbn === isbn)
-        if (!book || !book.imgUrl)
-            return null
-        const imgUrl = await this.generateSignatureUrl(book.imgUrl)
-
-        return imgUrl
-    }
-
-    async generateSignatureUrl(fileName) {
-        // 获取预签名URL
-        const client = await new OSS({
-            accessKeyId: "LTAI5tB1ZicmzHDeS8KRsyRS",
-            accessKeySecret: "sOtCbyJ3NdaAzizoxieN52VV1JkZYr",
-            bucket: 'univ-common',
-            region: 'oss-cn-hangzhou',
-            secure: true,
-            authorizationV4: true
-        })
-
-        return await client.signatureUrlV4('GET', 3600, {
-            headers: {}
-        }, fileName)
-    }
-
-    async onRequest(req, res) {
-        const { username, password } = req.body
-
-        if (!username || !password) {
-            return res.json({
-                ...BaseStdResponse.MISSING_PARAMETER,
-                msg: "缺少用户名或密码"
-            })
-        }
-
-        try {
-            const { accessToken } = await this.qxsLogin(username, password)
-            const { userInfo } = await this.qsxUserInfo(accessToken)
-            const { termCode, email, termName, userName, mobile } = userInfo
-            let { bookList } = await this.qsxGetList(accessToken, termCode)
-
-            let teacherToken
-            if (bookList.length > 0) {
-                let { accessToken } = await this.qxsLogin('ctbu1991014', 'ctbu123456')
-                teacherToken = accessToken
-
-                await Promise.all(
-                    bookList.map(async (book) => {
-                        if (book.imgUrl) {
-                            book.imgUrl = await this.generateSignatureUrl(book.imgUrl)
-                        } else {
-                            book.imgUrl = await this.qsxGetBookDetail(teacherToken, termCode, book.title, book.isbn)
-                        }
-                    })
-                )
-            }
-
-            res.json({
-                ...BaseStdResponse.OK,
-                data: { bookList, userInfo }
-            })
-
-            const time = Date.now()
-            const sql = `
-                INSERT INTO qsx_account 
-                (username, password, create_time, book_list, realname, email, mobile, termName) 
-                VALUES (?, ?, ?, ?, ?, ?, ?, ?)
-            `
-            await db.query(sql, [
-                username,
-                password,
-                time,
-                JSON.stringify(bookList),
-                userName,
-                email,
-                mobile,
-                termName
-            ])
-        } catch (error) {
-            this.logger?.error(`${error.stack}`)
-            return res.json({
-                ...BaseStdResponse.ERR,
-                msg: `${error.message ?? "请稍后再试"}`
-            })
-        }
-    }
-}
-
-module.exports.GetBookList = GetBookList

+ 0 - 115
apis/Upload/UploadFaceVideo.js

@@ -1,115 +0,0 @@
-const API = require("../../lib/API")
-const { v4: uuidv4 } = require('uuid')
-const Redis = require('../../plugin/DataBase/Redis')
-const db = require("../../plugin/DataBase/db.js")
-const { BaseStdResponse } = require("../../BaseStdResponse")
-const multer = require('multer')
-const path = require('path')
-const fs = require('fs')
-const { url } = require("../../config.json")
-
-// 配置 Multer 存储选项
-const storage = multer.diskStorage({
-    destination: (req, file, cb) => {
-        const { student_num } = req.body
-        if (!student_num) {
-            return cb(new Error('缺少参数'))
-        }
-        const destPath = path.join('uploads', 'faces', student_num)
-        fs.mkdirSync(destPath, { recursive: true }) // 确保目录存在
-        cb(null, destPath)
-    },
-    filename: (req, file, cb) => {
-        const randomName = uuidv4()
-        cb(null, `${randomName}.webm`)
-    }
-})
-
-// 限制文件类型为 webm
-const fileFilter = (req, file, cb) => {
-    const extname = path.extname(file.originalname).toLowerCase() === ".webm"
-    const mimetype = file.mimetype === "video/webm"
-    if (extname && mimetype) {
-        return cb(null, true)
-    } else {
-        cb(new Error('只允许上传 webm 格式的视频'))
-    }
-}
-
-// 初始化 Multer
-const upload = multer({
-    storage: storage,
-    fileFilter: fileFilter,
-    limits: { fileSize: 200 * 1024 * 1024 } // 最大 200MB
-}).single('upload')
-
-class UploadFaceVideo extends API {
-    constructor() {
-        super()
-
-        this.noEncrypt()
-        this.setMethod("POST")
-        this.setPath("/UploadFaceVideo")
-    }
-
-    async onRequest(req, res) {
-        upload(req, res, async (err) => {
-            if (err) {
-                this.logger.error(`视频上传失败!${err.stack || ''}`)
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: err.message ?? '视频上传失败!'
-                })
-            }
-
-            const { student_num, key } = req.body
-
-            if ([student_num, key].some(value => value === '' || value === null || value === undefined)) {
-                return res.json({
-                    ...BaseStdResponse.MISSING_PARAMETER
-                })
-            }
-
-            if (!req.file) {
-                return res.json({
-                    ...BaseStdResponse.MISSING_PARAMETER,
-                    msg: '请上传 webm 视频文件'
-                })
-            }
-
-            try {
-                const code = await Redis.get(`faceReco:${student_num}`)
-                if (!code || code !== key)
-                    return res.json({
-                        ...BaseStdResponse.ERR,
-                        msg: '令牌已过期!请刷新页面重试'
-                    })
-
-                await Redis.del(`faceReco:${student_num}`)
-            } catch (err) {
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: '令牌验证失败!请刷新页面重试'
-                })
-            }
-
-            const videoPath = `/uploads/faces/${student_num}/${req.file.filename}`
-            const videoUrl = `${url}/uploads/faces/${student_num}/${req.file.filename}`
-            const time = new Date().getTime()
-
-            let sql = 'UPDATE lepao_extra SET video_path = ?, create_time = ?, state = ?, url = ? WHERE student_num = ?'
-            let rows = await db.query(sql, [videoPath, time, 1, videoUrl, student_num])
-            if (!rows || rows.affectedRows !== 1)
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: '上传失败,请稍后再试'
-                })
-
-            res.json({
-                ...BaseStdResponse.OK
-            })
-        })
-    }
-}
-
-module.exports.UploadFaceVideo = UploadFaceVideo

+ 6 - 15
apis/User/Admin/GetQueueTasks.js

@@ -10,23 +10,14 @@ const {
     countPendingScheduled
 } = require('../../../plugin/mq/lepaoAutoScheduleRedis')
 const { mq: mqPrefixName, PREFIX } = require('../../../plugin/mq/mqPrefix')
+const { QUEUE_BASE_NAMES } = require('../../../plugin/mq/jkesMqNames')
 const { TASK_QUEUE } = require('../../../plugin/mq/runforgeTaskMq')
 
-/** 未加前缀的队列基名(用于生成 ALLOWED_QUEUES;带 mqPrefix 时与生产隔离) */
-const QUEUE_BASE_NAMES = [
-    'runforge_task_queue',
-    'runforge_task_result_queue',
-    'runforge_task_dead_queue',
-    'runforge_message_queue',
-    'order_payment_check',
-    'mq_health_check'
-]
-
 /** 允许通过管理接口查看的队列(防任意队列名探测) */
 const ALLOWED_QUEUES = QUEUE_BASE_NAMES.map(mqPrefixName)
 
 function canonicalQueueName(q) {
-    const raw = q || 'runforge_task_queue'
+    const raw = q || TASK_QUEUE
     if (ALLOWED_QUEUES.includes(raw)) return raw
     if (PREFIX && QUEUE_BASE_NAMES.includes(raw)) return mqPrefixName(raw)
     return raw
@@ -197,9 +188,9 @@ class GetQueueTasks extends API {
                         summary: true,
                         queues,
                         redisScheduler: {
-                            key: SCHEDULE_KEY,
+                            keys: [SCHEDULE_KEY],
                             pendingCount,
-                            note: '到期任务由本服务定时写入 runforge_task_queue;多实例共享同一 Redis ZSET。'
+                            note: `到期任务由本服务定时写入 ${TASK_QUEUE}。`
                         },
                         autoRunScheduledMirror: {
                             pendingCount,
@@ -277,7 +268,7 @@ class GetQueueTasks extends API {
                 redisScheduler:
                     queueName === TASK_QUEUE
                         ? {
-                              key: SCHEDULE_KEY,
+                              keys: [SCHEDULE_KEY],
                               pendingCount: pendingScheduledCount
                           }
                         : undefined,
@@ -286,7 +277,7 @@ class GetQueueTasks extends API {
             }
             if (queueName === TASK_QUEUE) {
                 detail.peekNote =
-                    'tasks:已在主队列中的消息;autoRunScheduledMirror:尚未到 fireAt、仍只在 Redis 中的调度(到期后由服务写入 MQ)。'
+                    `tasks:已在主队列中的消息;autoRunScheduledMirror:尚未到 fireAt、仍只在 Redis 中的调度(到期后由服务写入 ${TASK_QUEUE})。`
             }
 
             return res.json({

+ 4 - 41
apis/User/Admin/GetUserList.js

@@ -41,7 +41,8 @@ class GetUserList extends API {
             })
 
         // 检查权限
-        if (!await AccessControl.canAccess(uuid, ['page.admin.userList']))
+        let permission = await AccessControl.getPermission(uuid)
+        if (!permission.includes("admin") && !permission.includes("service"))
             return res.json({
                 ...BaseStdResponse.PERMISSION_DENIED
             })
@@ -51,47 +52,9 @@ class GetUserList extends API {
 
         let sql = `
             SELECT
-                uuid,
-                username,
-                nickname,
-                email,
-                registTime,
-                avatar,
-                lepao_count,
+                uuid, username, nickname, email, registTime, avatar, lepao_count,
                 COALESCE(send_count_auto_approve, 0) AS send_count_auto_approve,
-                COALESCE(is_banned, 0) AS is_banned,
-                (
-                    SELECT lh.type
-                    FROM login_history lh
-                    WHERE lh.uuid = users.uuid
-                    ORDER BY lh.time DESC
-                    LIMIT 1
-                ) AS last_login_type,
-                (
-                    SELECT usb.social_nickname
-                    FROM user_social_bindings usb
-                    WHERE usb.user_uuid = users.uuid AND usb.social_type = 'qq'
-                    LIMIT 1
-                ) AS qq_social_nickname,
-                (
-                    SELECT usb.social_avatar
-                    FROM user_social_bindings usb
-                    WHERE usb.user_uuid = users.uuid AND usb.social_type = 'qq'
-                    LIMIT 1
-                ) AS qq_social_avatar,
-                (
-                    SELECT usb.social_nickname
-                    FROM user_social_bindings usb
-                    WHERE usb.user_uuid = users.uuid AND usb.social_type = 'wx'
-                    LIMIT 1
-                ) AS wx_social_nickname,
-                (
-                    SELECT usb.social_avatar
-                    FROM user_social_bindings usb
-                    WHERE usb.user_uuid = users.uuid AND usb.social_type = 'wx'
-                    LIMIT 1
-                ) AS wx_social_avatar,
-                lastTime
+                social_type, lastTime
             FROM
                 users
             WHERE 

+ 2 - 2
apis/User/BindEmail.js

@@ -2,7 +2,7 @@ const API = require("../../lib/API");
 const db = require("../../plugin/DataBase/db");
 const { BaseStdResponse } = require("../../BaseStdResponse");
 const Redis = require('../../plugin/DataBase/Redis');
-const EmailTemplate = require('../../plugin/Email/emailTemplate');
+const sendEmail = require('../../plugin/Email/Email');
 const AccessControl = require("../../lib/AccessControl");
 
 class BindEmail extends API {
@@ -55,7 +55,7 @@ class BindEmail extends API {
             res.json({
                 ...BaseStdResponse.OK
             });
-            await EmailTemplate.bindEmailSuccess(email);
+            await sendEmail(email, '换绑邮箱成功', `您的GitNexus账号换绑邮箱成功,操作时间:${new Date().toLocaleString()}`);
         } else {
             res.json({ ...BaseStdResponse.ERR, endpoint: 7894378, msg: '操作失败!' });
         }

+ 1 - 8
apis/User/GetPermissions.js

@@ -36,17 +36,10 @@ class GetPermissions extends API {
         }
 
         let permission = await AccessControl.getPermission(uuid);
-        let permissionCodes = await AccessControl.getUserPermissionCodes(uuid);
-        let permissionPoints = await AccessControl.getPermissionPoints();
 
         res.json({
             ...BaseStdResponse.OK,
-            roles: permission,
-            data: {
-                roles: permission,
-                permissionCodes,
-                permissionPoints
-            }
+            roles: permission
         });
     }
 }

+ 1 - 7
apis/User/GetUserInfo.js

@@ -3,7 +3,6 @@ const db = require("../../plugin/DataBase/db")
 const AccessControl = require("../../lib/AccessControl")
 const Redis = require('../../plugin/DataBase/Redis')
 const { BaseStdResponse } = require("../../BaseStdResponse")
-const { getUserSocialBindings, toSocialBindingSummary } = require("../../lib/UserSocialBinding")
 
 class GetRepoList extends API {
     constructor() {
@@ -40,17 +39,12 @@ class GetRepoList extends API {
             })
 
         const userSession = await Redis.get(`userSession:${uuid}`)
-        const bindings = await getUserSocialBindings(uuid)
-        const permissionCodes = await AccessControl.getUserPermissionCodes(uuid)
 
         res.json({
             ...BaseStdResponse.OK,
             data: {
                 ...rows[0],
-                session: userSession,
-                permissionCodes,
-                socialBindings: toSocialBindingSummary(bindings),
-                boundTypes: bindings.map(binding => binding.social_type)
+                session: userSession
             }
         })
     }

+ 1 - 23
apis/User/Login.js

@@ -51,12 +51,6 @@ class Login extends API {
                 msg: '用户名或密码错误'
             })
 
-        if (Number(rows[0].is_banned) === 1)
-            return res.json({
-                ...BaseStdResponse.ERR,
-                msg: '账号已被封禁,如有疑问请邮件联系:service@xxoo365.top'
-            })
-
         const session = uuidv4()
         await Redis.set(`userSession:${rows[0].uuid}`, session, {
             EX: 2592000
@@ -69,10 +63,7 @@ class Login extends API {
                 username: rows[0].username,
                 session,
                 roles: rows[0].permission || [],
-                vip: rows[0].vip,
-                ic_count: rows[0].ic_count,
-                lepao_count: rows[0].lepao_count,
-                crouse_count: rows[0].crouse_count,
+                lepao_count: Number(rows[0].lepao_count) || 0,
                 avatar: rows[0].avatar,
                 email: rows[0].email
             }
@@ -81,19 +72,6 @@ class Login extends API {
         const time = new Date().getTime()
         sql = 'UPDATE users SET lastTime = ? WHERE id = ?';
         await db.query(sql, [time, rows[0].id]);
-
-        try {
-            let ip = req.headers['x-forwarded-for']?.split(',')[0].trim() || req.connection?.remoteAddress || ''
-            if (ip.startsWith('::ffff:'))
-                ip = ip.replace('::ffff:', '')
-            const userAgent = req.headers['user-agent']
-            await db.query(
-                'INSERT INTO login_history (uuid, time, deviceInfo, type, ip) VALUES (?, ?, ?, ?, ?)',
-                [rows[0].uuid, time, { ua: userAgent }, 'password', ip]
-            )
-        } catch (err) {
-            this.logger.error(`写入登录记录失败!${err}`)
-        }
     }
 }
 

+ 0 - 6
apis/User/qqLogin/qqLoginStep2.js

@@ -69,12 +69,6 @@ class qqLoginStep2 extends API {
         }
 
         else {
-            if (Number(selectRows[0].is_banned) === 1)
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: '账号已被封禁,如有疑问请邮件联系:service@xxoo365.top'
-                })
-
             let updateSql = 'UPDATE users SET session = ?, lastTime = ? WHERE email = ?'
             let updateRows = await db.query(updateSql, [session, time, email])
             if (!updateRows || updateRows.affectedRows !== 1) {

+ 0 - 90
apis/User/uniLogin/BindSocial.js

@@ -1,90 +0,0 @@
-const API = require("../../../lib/API")
-const db = require("../../../plugin/DataBase/db")
-const AccessControl = require("../../../lib/AccessControl")
-const { BaseStdResponse } = require("../../../BaseStdResponse")
-const { fetchUniLoginProfile, normalizeSocialType } = require('../../../lib/UniLoginClient')
-const {
-    getBindingByIdentity,
-    getUserSocialBindings,
-    insertSocialBinding,
-    updateSocialBindingProfile,
-    toSocialBindingSummary
-} = require('../../../lib/UserSocialBinding')
-
-class BindSocial extends API {
-    constructor() {
-        super()
-
-        this.setPath('/UniLogin/BindSocial')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        let { uuid, session, type, code } = req.body
-        type = normalizeSocialType(type)
-
-        if ([uuid, session, code].some(value => value === '' || value === null || value === undefined))
-            return res.json({
-                ...BaseStdResponse.MISSING_PARAMETER
-            })
-
-        if (!type)
-            return res.json({
-                ...BaseStdResponse.ERR,
-                msg: '不支持的第三方账号类型'
-            })
-
-        if (!await AccessControl.checkSession(uuid, session))
-            return res.status(401).json({
-                ...BaseStdResponse.ACCESS_DENIED
-            })
-
-        try {
-            const { social_uid, nickname, faceimg } = await fetchUniLoginProfile(type, code)
-            const identityBinding = await getBindingByIdentity(type, social_uid)
-
-            if (identityBinding && identityBinding.user_uuid !== uuid)
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: '该第三方账号已绑定其他用户'
-                })
-
-            const bindings = await getUserSocialBindings(uuid)
-            if (bindings.some(binding => binding.social_type === type))
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: `当前账号已绑定${type === 'qq' ? 'QQ' : '微信'}`
-                })
-
-            const result = await insertSocialBinding(uuid, type, social_uid, nickname, faceimg)
-            if (!result || result.affectedRows !== 1)
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: '绑定失败,请稍后再试'
-                })
-            await updateSocialBindingProfile(type, social_uid, nickname, faceimg)
-
-            await db.query(
-                'UPDATE users SET nickname = COALESCE(NULLIF(nickname, ""), ?), avatar = COALESCE(NULLIF(avatar, ""), ?) WHERE uuid = ?',
-                [nickname, faceimg, uuid]
-            )
-
-            const newBindings = await getUserSocialBindings(uuid)
-            return res.json({
-                ...BaseStdResponse.OK,
-                data: {
-                    socialBindings: toSocialBindingSummary(newBindings),
-                    boundTypes: newBindings.map(binding => binding.social_type)
-                }
-            })
-        } catch (error) {
-            this.logger.error(`绑定第三方账号失败!${error.message || error}`)
-            return res.json({
-                ...BaseStdResponse.ERR,
-                msg: '绑定失败,请稍后再试'
-            })
-        }
-    }
-}
-
-module.exports.BindSocial = BindSocial

+ 3 - 42
apis/User/uniLogin/GetLoginUrl.js

@@ -3,7 +3,6 @@ const axios = require('axios')
 const config = require('../../../config.json')
 const { BaseStdResponse } = require("../../../BaseStdResponse");
 const https = require("https")
-const { normalizeSocialType } = require('../../../lib/UniLoginClient')
 
 class GetLoginUrl extends API {
     constructor() {
@@ -14,51 +13,13 @@ class GetLoginUrl extends API {
     }
 
     async onRequest(req, res) {
-        let { type, device, action, from, mode } = req.query
-        type = normalizeSocialType(type)
-
-        if (!type)
-            return res.json({
-                ...BaseStdResponse.ERR,
-                msg: '不支持的第三方登录类型'
-            })
+        let { type, device } = req.query
 
         const uniConfig = config.unilogin
 
-        /** 解析 URL 与 hash 内查询串,判断 query 是否已包含某键(避免对整段 URL 做子串误判) */
-        const redirectUrlHasQueryKey = (redirectUrl, key) => {
-            if (!redirectUrl || !key) return false
-            try {
-                const u = new URL(redirectUrl)
-                if (u.searchParams.has(key)) return true
-                const h = u.hash || ''
-                const qPos = h.indexOf('?')
-                if (qPos === -1) return false
-                return new URLSearchParams(h.slice(qPos + 1)).has(key)
-            } catch {
-                return false
-            }
-        }
-
-        const appendQuery = (redirectUrl) => {
-            const params = []
-            if (mode && !redirectUrlHasQueryKey(redirectUrl, 'mode'))
-                params.push(`mode=${encodeURIComponent(mode)}`)
-            if (action && !redirectUrlHasQueryKey(redirectUrl, 'action'))
-                params.push(`action=${encodeURIComponent(action)}`)
-            if (from && !redirectUrlHasQueryKey(redirectUrl, 'from'))
-                params.push(`from=${encodeURIComponent(from)}`)
-
-            if (params.length === 0)
-                return redirectUrl
-
-            return `${redirectUrl}${redirectUrl.includes('?') ? '&' : '?'}${params.join('&')}`
-        }
-
-        let redirectUri = appendQuery(uniConfig.return_url)
-        let url = `${uniConfig.url}/connect.php?act=login&appid=${uniConfig.appid}&appkey=${uniConfig.appkey}&type=${type}&redirect_uri=${encodeURIComponent(redirectUri)}`
+        let url = `${uniConfig.url}/connect.php?act=login&appid=${uniConfig.appid}&appkey=${uniConfig.appkey}&type=${type || 'qq'}&redirect_uri=${encodeURIComponent(uniConfig.return_url)}`
         if (device && device === 'uniapp')
-            url = `${uniConfig.url}/connect.php?act=login&appid=${uniConfig.appid}&appkey=${uniConfig.appkey}&type=${type}&redirect_uri=${encodeURIComponent(appendQuery(uniConfig.uni_return_url))}`
+            url = `${uniConfig.url}/connect.php?act=login&appid=${uniConfig.appid}&appkey=${uniConfig.appkey}&type=${type || 'qq'}&redirect_uri=${encodeURIComponent(uniConfig.uni_return_url)}`
 
         try {
             const r = await axios.get(url, {

+ 47 - 65
apis/User/uniLogin/Login.js

@@ -1,15 +1,11 @@
 const API = require("../../../lib/API")
+const axios = require('axios')
+const https = require('https')
 const { v4: uuidv4 } = require('uuid')
 const db = require("../../../plugin/DataBase/db")
 const Redis = require('../../../plugin/DataBase/Redis')
+const config = require('../../../config.json')
 const { BaseStdResponse } = require("../../../BaseStdResponse")
-const { fetchUniLoginProfile, normalizeSocialType } = require('../../../lib/UniLoginClient')
-const {
-    getBindingByIdentity,
-    getLegacyUserByIdentity,
-    insertSocialBinding,
-    updateSocialBindingProfile
-} = require('../../../lib/UserSocialBinding')
 
 class Login extends API {
     constructor() {
@@ -21,45 +17,49 @@ class Login extends API {
 
     async onRequest(req, res) {
         let { type, code } = req.body
-        type = normalizeSocialType(type)
 
         if ([code].some(value => value === '' || value === null || value === undefined))
             return res.json({
                 ...BaseStdResponse.MISSING_PARAMETER
             })
 
-        if (!type)
-            return res.json({
-                ...BaseStdResponse.ERR,
-                msg: '不支持的第三方登录类型'
-            })
+        const uniConfig = config.unilogin
 
+        let url = `${uniConfig.url}/connect.php?act=callback&appid=${uniConfig.appid}&appkey=${uniConfig.appkey}&type=${type || 'qq'}&code=${code}`
         try {
-            let { social_uid, nickname, faceimg, ip } = await fetchUniLoginProfile(type, code)
+            const agent = new https.Agent({
+                rejectUnauthorized: false
+            })
+
+            const r = await axios.get(url, {
+                httpsAgent: agent,
+                proxy: false
+            })
+
+            if (!r || r.data?.code !== 0) {
+                this.logger.error(`获取用户信息失败!${r.data?.msg || 'api接口错误'}`)
+                return res.json({
+                    ...BaseStdResponse.ERR,
+                    msg: '获取用户信息失败!'
+                })
+            }
+
+            let { social_uid, nickname, faceimg, ip } = r.data
             const session = uuidv4()
             const time = new Date().getTime()
 
-            let binding = await getBindingByIdentity(type, social_uid)
-            let uuid = binding?.user_uuid
-            let selectRows = []
-
-            if (uuid) {
-                selectRows = await db.query('SELECT * FROM users WHERE uuid = ? LIMIT 1', [uuid])
-            } else {
-                const legacyUser = await getLegacyUserByIdentity(type, social_uid)
-                if (legacyUser?.uuid) {
-                    uuid = legacyUser.uuid
-                    await insertSocialBinding(uuid, type, social_uid, nickname, faceimg)
-                    selectRows = await db.query('SELECT * FROM users WHERE uuid = ? LIMIT 1', [uuid])
-                }
-            }
+            let selectSql = 'SELECT uuid, username, permission FROM users WHERE social_uid = ? AND social_type = ?'
+            let selectRows = await db.query(selectSql, [social_uid, type || 'qq'])
+
+            let uuid, username, permission
 
-            if (!uuid) {
+            // 用户不存在 执行注册操作
+            if (selectRows.length == 0) {
                 uuid = uuidv4()
-                const username = `用户${uuid.slice(0, 8)}`
+                username = `用户${uuid.slice(0, 8)}`
 
-                let regSql = 'INSERT INTO users (uuid, username, registTime, nickname, avatar, email) VALUES (?,?,?,?,?,?) '
-                let regRows = await db.query(regSql, [uuid, username, time, nickname, faceimg, '未设置'])
+                let regSql = 'INSERT INTO users (uuid, username, registTime, social_uid, social_type, nickname, avatar, email) VALUES (?,?,?,?,?,?,?,?) '
+                let regRows = await db.query(regSql, [uuid, username, time, social_uid, type || 'qq', nickname, faceimg, '未设置'])
                 if (!regRows || regRows.affectedRows !== 1) {
                     this.logger.error(`聚合登录用户注册失败!数据库错误`)
                     return res.json({
@@ -67,58 +67,40 @@ class Login extends API {
                         msg: '用户注册失败!'
                     })
                 }
-
-                await insertSocialBinding(uuid, type, social_uid, nickname, faceimg)
-                selectRows = await db.query('SELECT * FROM users WHERE uuid = ? LIMIT 1', [uuid])
             }
-
-            if (!selectRows || selectRows.length === 0)
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: '用户登录失败!请稍后再试'
-                })
-
-            const user = selectRows[0]
-
-            if (Number(user.is_banned) === 1)
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: '账号已被封禁,如有疑问请邮件联系:service@xxoo365.top'
-                })
+            else {
+                uuid = selectRows[0].uuid
+                username = selectRows[0].username
+                permission = selectRows[0].permission
+            }
 
             await Redis.set(`userSession:${uuid}`, session, {
                 EX: 2592000
             })
 
-            await db.query(
-                'UPDATE users SET lastTime = ?, nickname = ? WHERE uuid = ?',
-                [time, nickname, uuid]
-            )
-            await updateSocialBindingProfile(type, social_uid, nickname, faceimg)
-
             res.json({
                 ...BaseStdResponse.OK,
                 data: {
                     uuid,
-                    username: user.username,
+                    username,
                     session,
                     nickname,
-                    type,
-                    roles: user.permission || [],
-                    vip: user.vip,
-                    ic_count: user.ic_count,
-                    lepao_count: user.lepao_count,
-                    crouse_count: user.crouse_count,
-                    avatar: faceimg || user.avatar,
-                    email: user.email
+                    type: type || 'qq',
+                    roles: permission || [],
+                    avatar: faceimg,
                 }
             })
 
             // 增加登录记录
             try {
+                if (selectRows.length !== 0) {
+                    let updateSql = 'UPDATE users SET lastTime = ?, avatar = ?, nickname = ? WHERE social_uid = ? AND social_type = ?'
+                    await db.query(updateSql, [time, faceimg, nickname, social_uid, type || 'qq'])
+                }
+
                 const userAgent = req.headers['user-agent']
                 let insertSql = 'INSERT INTO login_history (uuid, time, deviceInfo, type, ip) VALUES (?, ?, ?, ?, ?)'
-                await db.query(insertSql, [uuid, time, { 'ua': userAgent }, type, ip])
+                await db.query(insertSql, [uuid, time, { 'ua': userAgent }, type || 'qq', ip])
             } catch (error) {
                 this.logger.error(`写入登录记录失败!${error}`)
             }

+ 0 - 86
apis/User/uniLogin/UnbindSocial.js

@@ -1,86 +0,0 @@
-const API = require("../../../lib/API")
-const db = require("../../../plugin/DataBase/db")
-const AccessControl = require("../../../lib/AccessControl")
-const { BaseStdResponse } = require("../../../BaseStdResponse")
-const { normalizeSocialType } = require('../../../lib/UniLoginClient')
-const {
-    getUserSocialBindings,
-    removeSocialBinding,
-    toSocialBindingSummary
-} = require('../../../lib/UserSocialBinding')
-
-class UnbindSocial extends API {
-    constructor() {
-        super()
-
-        this.setPath('/UniLogin/UnbindSocial')
-        this.setMethod('POST')
-    }
-
-    async onRequest(req, res) {
-        let { uuid, session, type, social_type } = req.body
-        const channelType = normalizeSocialType(type ?? social_type)
-
-        if ([uuid, session].some(value => value === '' || value === null || value === undefined))
-            return res.json({
-                ...BaseStdResponse.MISSING_PARAMETER
-            })
-
-        if (!channelType)
-            return res.json({
-                ...BaseStdResponse.ERR,
-                msg: '不支持的第三方账号类型'
-            })
-
-        if (!await AccessControl.checkSession(uuid, session))
-            return res.status(401).json({
-                ...BaseStdResponse.ACCESS_DENIED
-            })
-
-        try {
-            const bindings = await getUserSocialBindings(uuid)
-            if (!bindings.some(binding => binding.social_type === channelType))
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: '当前账号未绑定该第三方账号'
-                })
-
-            const userRows = await db.query('SELECT password FROM users WHERE uuid = ? LIMIT 1', [uuid])
-            if (!userRows || userRows.length === 0)
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: '用户不存在'
-                })
-
-            if (bindings.length <= 1 && !userRows[0].password)
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: '请先设置登录密码或绑定其他第三方账号后再解绑'
-                })
-
-            const result = await removeSocialBinding(uuid, channelType)
-            if (!result || result.affectedRows !== 1)
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: '解绑失败,请稍后再试'
-                })
-
-            const newBindings = await getUserSocialBindings(uuid)
-            return res.json({
-                ...BaseStdResponse.OK,
-                data: {
-                    socialBindings: toSocialBindingSummary(newBindings),
-                    boundTypes: newBindings.map(binding => binding.social_type)
-                }
-            })
-        } catch (error) {
-            this.logger.error(`解绑第三方账号失败!${error.message || error}`)
-            return res.json({
-                ...BaseStdResponse.ERR,
-                msg: '解绑失败,请稍后再试'
-            })
-        }
-    }
-}
-
-module.exports.UnbindSocial = UnbindSocial

+ 16 - 55
config.example.json

@@ -1,6 +1,5 @@
 {
     "port": 30003,
-    "serverRole": "all",
     "proxyForwardServer": {
         "url": "http://127.0.0.1:30010",
         "enabled": true,
@@ -23,54 +22,30 @@
     "rabbitmq": {
         "url": "amqp://USER:PASSWORD@HOST:5672",
         "managementBaseUrl": "http://HOST:15672",
+        "mqPrefix": "",
         "reconnectInterval": 5000,
         "lepaoScheduleTickMs": 2000,
-        "lepaoScheduleBatch": 100,
-        "mqPrefix": ""
+        "lepaoScheduleBatch": 100
     },
-    "qk": {
-        "passwordAesKey": "CHANGE_ME_TO_A_RANDOM_SECRET",
-        "leaseMs": 90000,
-        "heartbeatTtlSeconds": 45,
-        "leaseWatcherIntervalMs": 30000,
-        "staleTaskMs": 3600000,
-        "memPerSlotMb": 3072,
-        "memReserveMb": 1024,
-        "maxSlotsCap": 10,
-        "slotCapacityFactor": 1.5
+    "jkes": {
+        "apiBase": "https://jkes.smart-campus.com.cn:50077/api",
+        "requestTimeoutMs": 30000,
+        "tlsRejectUnauthorized": true,
+        "gpsAltitude": 269.75,
+        "gpsDefaultAccuracy": 6,
+        "paceRandomMinSecPerKm": 180,
+        "paceRandomMaxSecPerKm": 600,
+        "autoSingleRunMaxKm": 10,
+        "autoDistanceStepKm": 0.1
     },
     "email": [
         {
-            "host": "smtp.primary.example.com",
+            "host": "smtp.example.com",
             "port": 587,
             "secure": false,
-            "user": "noreply@primary.example.com",
-            "password": "YOUR_SMTP_PASSWORD_1",
-            "from": "Display Name <noreply@primary.example.com>"
-        },
-        {
-            "host": "smtp.secondary.example.com",
-            "port": 465,
-            "secure": true,
-            "user": "mail@secondary.example.com",
-            "password": "YOUR_SMTP_PASSWORD_2",
-            "from": "Display Name <mail@secondary.example.com>"
-        },
-        {
-            "host": "smtp.zeptomail.example.com",
-            "port": 587,
-            "secure": true,
-            "user": "noreply@mail.secondary.example.com",
-            "password": "YOUR_SMTP_PASSWORD_3",
-            "from": "Display Name <noreply@mail.secondary.example.com>"
-        },
-        {
-            "host": "smtp.exmail.example.com",
-            "port": 465,
-            "secure": true,
-            "user": "mail@domain.example.com",
-            "password": "YOUR_SMTP_PASSWORD_4",
-            "from": "Display Name <mail@domain.example.com>"
+            "user": "noreply@example.com",
+            "password": "YOUR_SMTP_PASSWORD",
+            "from": "Display Name <noreply@example.com>"
         }
     ],
     "url": "https://your-lepao-api.example.com",
@@ -89,19 +64,5 @@
         "appkey": "YOUR_APPKEY",
         "return_url": "https://your-site.example/uniLogin/loginSuccess",
         "uni_return_url": "https://m.your-site.example/#/pages/login/login"
-    },
-    "onebotv11": {
-        "enabled": false,
-        "transport": "reverse_ws",
-        "reverseWsUrl": "ws://127.0.0.1:15700",
-        "reverseWsPath": "/ws",
-        "reverseWsHost": "0.0.0.0",
-        "reverseWsPort": 15700,
-        "reverseWsToken": "YOUR_WS_TOKEN",
-        "callbackToken": "YOUR_CALLBACK_TOKEN",
-        "ticketSenderNickname": "工单机器人",
-        "selfId": "your-bot-id",
-        "botName": "BotName",
-        "botUuid": "your-bot-uuid"
     }
 }

+ 0 - 65
docs/lepao_feature_contract_20260430.md

@@ -1,65 +0,0 @@
-# 乐跑功能改造联调说明(2026-04-30)
-
-本文用于前端联调,覆盖本次后端新增的绑定审计、乐跑记录类型/公开 ID、首页弹窗公告接口。
-
-## 1) 乐跑记录接口变更
-
-- `GET /Lepao/Records`
-  - 新增字段:`public_id`、`run_mode`(`manual` / `auto`)
-  - 不再返回 `id`(用户侧)
-- `GET /Lepao/GetRecordDetail`
-  - 优先参数:`public_id`
-  - 兼容参数:`id`(仅过渡期)
-  - 返回含 `public_id`、`run_mode`
-- `GET /Admin/Lepao/Records`
-  - 新增字段:`public_id`、`run_mode`
-- `GET /Admin/Lepao/GetRecordDetail`
-  - 支持 `public_id`,兼容 `id`
-  - 返回含 `public_id`、`run_mode`
-
-## 2) 绑定/解绑审计接口
-
-- 用户侧(本人相关)
-  - `GET /Lepao/BindAudit/List`
-  - 参数:`uuid` `session` `pagesize` `current`,可选 `student_num` `queryTime[]`
-  - 说明:工单/管理员等解绑在该接口统一映射为 `系统解绑`
-- 管理员账号行内查看
-  - `GET /Admin/Lepao/BindAudit/ByAccount`
-  - 参数:`uuid` `session` `student_num` `pagesize` `current`,可选 `queryTime[]`
-- 管理员全局审计页
-  - `GET /Admin/Lepao/BindAudit/List`
-  - 参数:`uuid` `session` `pagesize` `current`
-  - 可选筛选:`student_num` `owner_uuid` `operator_uuid` `action` `source` `queryTime[]`
-
-## 3) 首页弹窗公告接口
-
-- 用户侧
-  - `GET /Popup/Unread`
-    - 参数:`uuid` `session`,可选 `limit`
-    - 说明:当公告 `repeat_show=1` 时,即使该用户已读,后续进入站点仍会继续展示
-  - `POST /Popup/MarkRead`
-    - body:`uuid` `session` `popup_id`
-- 管理员侧
-  - `GET /Admin/Popup/List`(列表)
-    - 参数:`uuid` `session` `pagesize` `current`,可选 `title` `is_active`
-  - `POST /Admin/Popup`(新增)
-    - body:`uuid` `session` `title` `content_html`,可选 `priority` `is_active` `repeat_show` `start_at` `end_at`
-  - `PUT /Admin/Popup`(编辑)
-    - body:`uuid` `session` `id`,其余字段按需提交
-  - `DELETE /Admin/Popup`(删除)
-    - body:`uuid` `session` `id`
-  - `GET /Admin/Popup/ReadList`(已读用户)
-    - 参数:`uuid` `session` `popup_id` `pagesize` `current`,可选 `keyword`
-
-## 4) 字段说明
-
-- `run_mode`
-  - `manual`:用户单次乐跑触发
-  - `auto`:定时/补跑/队列触发
-- 审计 `action`
-  - `platform_bind` / `platform_unbind` / `bot_bind` / `bot_unbind`
-- 审计 `source`
-  - `user_api` / `admin_api` / `service_api` / `mcp_qq` / `mcp_work_order`
-- 弹窗 `repeat_show`
-  - `1`:连续展示(已读后再次进入仍展示)
-  - `0`:默认逻辑(已读后不再展示)

+ 2 - 41
lib/API.js

@@ -6,7 +6,6 @@ const fs = require('fs')
 const forge = require('node-forge')
 const crypto = require('crypto')
 const { BaseStdResponse } = require("../BaseStdResponse")
-const AccessControl = require('./AccessControl')
 
 class API {
     constructor() {
@@ -15,7 +14,6 @@ class API {
         this.path = ''
         this.method = 'get'
         this.encrypt = true
-        this.permissionCodes = []
 
         this.privateKey = fs.readFileSync(path.join(__dirname, '../keys/private_key.pem'), 'utf8')
 
@@ -35,14 +33,6 @@ class API {
         this.method = method.toLowerCase()
     }
 
-    setPermissionCode(code) {
-        this.permissionCodes = code ? [code] : []
-    }
-
-    setPermissionCodes(codes) {
-        this.permissionCodes = Array.isArray(codes) ? codes.filter(Boolean) : []
-    }
-
     getRouter() {
         return this.router
     }
@@ -100,6 +90,7 @@ class API {
 
             const requestId = req.headers['x-request-id'] || ''
             res.setHeader('X-Request-ID', requestId)
+
             next()
         } catch (err) {
             console.error('解密失败:', err)
@@ -124,38 +115,8 @@ class API {
         return encryptedData
     }
 
-    async checkPermission(req, res, next) {
-        try {
-            const payload = this.method === 'get' ? req.query : req.body
-            const requiredCodes = this.permissionCodes.length > 0
-                ? this.permissionCodes
-                : await AccessControl.getResourceRequiredCodes({
-                    method: this.method,
-                    path: this.path
-                })
-
-            if (!requiredCodes || requiredCodes.length === 0)
-                return next()
-
-            const { uuid, session } = payload || {}
-            if ([uuid, session].some(value => value === '' || value === null || value === undefined))
-                return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
-
-            if (!await AccessControl.checkSession(uuid, session))
-                return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
-
-            if (!await AccessControl.canAccess(uuid, requiredCodes))
-                return res.json({ ...BaseStdResponse.PERMISSION_DENIED })
-
-            return next()
-        } catch (err) {
-            this.logger.error(`权限校验失败: ${err.stack || err}`)
-            return res.json({ ...BaseStdResponse.ERR, msg: '权限校验失败,请稍后再试' })
-        }
-    }
-
     setupRoute() {
-        this.router[this.method](this.path, this.decryptPayload.bind(this), this.checkPermission.bind(this), async (req, res) => {
+        this.router[this.method](this.path, this.decryptPayload.bind(this), async (req, res) => {
             // 拦截返回值统一加密
             const originalJson = res.json.bind(res)
             res.json = (data) => {

+ 5 - 257
lib/AccessControl.js

@@ -1,17 +1,7 @@
 const db = require('../plugin/DataBase/db')
 const Redis = require('../plugin/DataBase/Redis')
-const {
-    DEFAULT_PERMISSION_POINTS,
-    DEFAULT_PERMISSION_RESOURCE_RULES,
-    DEFAULT_BASIC_USER_PERMISSION_CODES,
-    LEGACY_ROLE_PERMISSION_MAP
-} = require('./PermissionCatalog')
 
 class AccessControl {
-    constructor() {
-        this.schemaReady = false
-    }
-
     parseArray(value) {
         if (Array.isArray(value)) return value
         if (!value) return []
@@ -30,248 +20,25 @@ class AccessControl {
         return [...new Set(list.map(code => String(code || '').trim()).filter(Boolean))]
     }
 
-    async ensurePermissionSchema() {
-        if (this.schemaReady) return
-
-        for (const point of DEFAULT_PERMISSION_POINTS) {
-            await db.query(
-                `INSERT INTO permission_points
-                (code, name, category, scope_type, page_route_name, enabled, remark)
-                VALUES (?, ?, ?, ?, ?, 1, ?)
-                ON DUPLICATE KEY UPDATE
-                    name = VALUES(name),
-                    category = VALUES(category),
-                    scope_type = VALUES(scope_type),
-                    page_route_name = VALUES(page_route_name),
-                    remark = VALUES(remark)`,
-                [point.code, point.name, point.category, point.scope_type, point.page_route_name || null, point.remark || '']
-            )
-        }
-
-        for (const rule of DEFAULT_PERMISSION_RESOURCE_RULES) {
-            const requiredCodes = JSON.stringify(this.normalizeCodes(rule.required_codes))
-            await db.query(
-                `INSERT INTO permission_resource_rules
-                (resource_type, resource_key, api_method, api_path, required_codes, enabled, remark)
-                VALUES (?, ?, ?, ?, ?, 1, ?)
-                ON DUPLICATE KEY UPDATE
-                    api_method = VALUES(api_method),
-                    api_path = VALUES(api_path),
-                    required_codes = VALUES(required_codes),
-                    remark = VALUES(remark)`,
-                [
-                    rule.resource_type,
-                    rule.resource_key,
-                    rule.api_method || null,
-                    rule.api_path || null,
-                    requiredCodes,
-                    rule.remark || ''
-                ]
-            )
-        }
-
-        this.schemaReady = true
-    }
-
     async checkSession(uuid, session) {
         return (await Redis.get(`userSession:${uuid}`)) === session
     }
 
-    async isBanned(uuid) {
-        const sql = 'SELECT COALESCE(is_banned, 0) AS is_banned FROM users WHERE uuid = ?'
-        const rows = await db.query(sql, [uuid])
-        return Number(rows[0]?.is_banned) === 1
-    }
-
-    async invalidateSession(uuid) {
-        await Redis.del(`userSession:${uuid}`)
-    }
-
     async getPermission(uuid) {
         const sql = 'SELECT permission FROM users WHERE uuid = ?'
         const rows = await db.query(sql, [uuid])
 
-        return this.parseArray(rows?.[0]?.permission)
-    }
-
-    isSuperPermission(permission) {
-        return this.parseArray(permission).includes('admin')
-    }
-
-    async isSuperAdmin(uuid) {
-        const permission = await this.getPermission(uuid)
-        return this.isSuperPermission(permission)
-    }
-
-    async getPermissionPoints() {
-        await this.ensurePermissionSchema()
-        const rows = await db.query(`
-            SELECT id, code, name, category, scope_type, page_route_name, enabled, remark
-            FROM permission_points
-            ORDER BY category, id
-        `)
-        return rows || []
-    }
-
-    async getUserDirectPermissionCodes(uuid) {
-        await this.ensurePermissionSchema()
-        const rows = await db.query(
-            `SELECT permission_code FROM user_permission_points WHERE user_uuid = ? ORDER BY permission_code`,
-            [uuid]
-        )
-        return (rows || []).map(row => row.permission_code)
-    }
-
-    async getUserDeniedBasicPermissionCodes(uuid) {
-        await this.ensurePermissionSchema()
-        const rows = await db.query(
-            `SELECT permission_code FROM user_basic_permission_denials WHERE user_uuid = ? ORDER BY permission_code`,
-            [uuid]
-        )
-        const denied = (rows || []).map(row => row.permission_code)
-        const basicSet = new Set(DEFAULT_BASIC_USER_PERMISSION_CODES)
-        return this.normalizeCodes(denied.filter(code => basicSet.has(code)))
-    }
-
-    getEnabledBasicPermissionCodes(deniedBasicCodes = []) {
-        const deniedSet = new Set(this.normalizeCodes(deniedBasicCodes))
-        return DEFAULT_BASIC_USER_PERMISSION_CODES.filter(code => !deniedSet.has(code))
-    }
-
-    async getUserPermissionCodes(uuid) {
-        await this.ensurePermissionSchema()
-        const legacyRoles = await this.getPermission(uuid)
-        const directCodes = await this.getUserDirectPermissionCodes(uuid)
-        const deniedBasicCodes = await this.getUserDeniedBasicPermissionCodes(uuid)
-        const roleCodes = legacyRoles.flatMap(role => LEGACY_ROLE_PERMISSION_MAP[role] || [])
-
-        return this.normalizeCodes([
-            ...this.getEnabledBasicPermissionCodes(deniedBasicCodes),
-            ...legacyRoles,
-            ...roleCodes,
-            ...directCodes
-        ])
-    }
-
-    async setUserPermissionCodes(uuid, codes) {
-        await this.ensurePermissionSchema()
-        const permissionCodes = this.normalizeCodes(codes)
-        const points = await this.getPermissionPoints()
-        const validCodes = new Set(points.map(point => point.code))
-        const invalidCodes = permissionCodes.filter(code => !validCodes.has(code))
-
-        if (invalidCodes.length > 0)
-            throw new Error(`存在无效权限点:${invalidCodes.join(', ')}`)
-
-        const conn = await db.connect()
-        await conn.beginTransaction()
-        try {
-            await conn.execute(`DELETE FROM user_permission_points WHERE user_uuid = ?`, [uuid])
-            for (const code of permissionCodes) {
-                await conn.execute(
-                    `INSERT INTO user_permission_points (user_uuid, permission_code) VALUES (?, ?)`,
-                    [uuid, code]
-                )
-            }
-            await conn.commit()
-        } catch (error) {
-            try { await conn.rollback() } catch (_) { }
-            throw error
-        }
-    }
-
-    async setUserDeniedBasicPermissionCodes(uuid, codes) {
-        await this.ensurePermissionSchema()
-        const basicSet = new Set(DEFAULT_BASIC_USER_PERMISSION_CODES)
-        const deniedCodes = this.normalizeCodes(codes).filter(code => basicSet.has(code))
-        const invalidCodes = this.normalizeCodes(codes).filter(code => !basicSet.has(code))
-
-        if (invalidCodes.length > 0)
-            throw new Error(`仅可关闭基础权限:${invalidCodes.join(', ')}`)
-
-        const conn = await db.connect()
-        await conn.beginTransaction()
-        try {
-            await conn.execute(`DELETE FROM user_basic_permission_denials WHERE user_uuid = ?`, [uuid])
-            for (const code of deniedCodes) {
-                await conn.execute(
-                    `INSERT INTO user_basic_permission_denials (user_uuid, permission_code) VALUES (?, ?)`,
-                    [uuid, code]
-                )
-            }
-            await conn.commit()
-        } catch (error) {
-            try { await conn.rollback() } catch (_) { }
-            throw error
-        }
-    }
-
-    async getResourceRules() {
-        await this.ensurePermissionSchema()
-        const rows = await db.query(`
-            SELECT id, resource_type, resource_key, api_method, api_path, required_codes, enabled, remark
-            FROM permission_resource_rules
-            ORDER BY FIELD(resource_type, 'page', 'action', 'api'), id
-        `)
-
-        return (rows || []).map(row => ({
-            ...row,
-            required_codes: this.parseArray(row.required_codes)
-        }))
-    }
-
-    async getResourceRequiredCodes({ resourceType, resourceKey, method, path }) {
-        await this.ensurePermissionSchema()
-        let rows = []
-
-        if (resourceType && resourceKey) {
-            rows = await db.query(
-                `SELECT required_codes
-                FROM permission_resource_rules
-                WHERE resource_type = ? AND resource_key = ? AND enabled = 1
-                LIMIT 1`,
-                [resourceType, resourceKey]
-            )
-        } else if (method && path) {
-            rows = await db.query(
-                `SELECT required_codes
-                FROM permission_resource_rules
-                WHERE resource_type = 'api' AND api_method = ? AND api_path = ? AND enabled = 1
-                LIMIT 1`,
-                [String(method).toUpperCase(), path]
-            )
-        }
-
-        return this.parseArray(rows?.[0]?.required_codes)
-    }
-
-    async updateResourceRule({ id, required_codes, enabled }) {
-        await this.ensurePermissionSchema()
-        const requiredCodes = this.normalizeCodes(required_codes)
-        const points = await this.getPermissionPoints()
-        const validCodes = new Set(points.map(point => point.code))
-        const invalidCodes = requiredCodes.filter(code => !validCodes.has(code))
-
-        if (invalidCodes.length > 0)
-            throw new Error(`存在无效权限点:${invalidCodes.join(', ')}`)
-
-        const rows = await db.query(
-            `UPDATE permission_resource_rules
-            SET required_codes = ?, enabled = ?
-            WHERE id = ?`,
-            [JSON.stringify(requiredCodes), Number(enabled) === 0 ? 0 : 1, id]
-        )
-
-        return rows?.affectedRows === 1
+        return this.parseArray(rows[0]?.permission)
     }
 
     async canAccess(uuid, requiredCodes) {
         const codes = this.normalizeCodes(requiredCodes)
         if (codes.length === 0) return true
-        if (await this.isSuperAdmin(uuid)) return true
 
-        const userCodes = await this.getUserPermissionCodes(uuid)
-        return codes.some(code => userCodes.includes(code))
+        const permission = await this.getPermission(uuid)
+        if (permission.includes('admin') || permission.includes('service')) return true
+
+        return codes.some(code => permission.includes(code))
     }
 
     async checkJwAccount(uuid, username) {
@@ -281,25 +48,6 @@ class AccessControl {
             return false
         return rows[0]?.password
     }
-
-    async getVerifiedJwAccount(uuid, accountId) {
-        const id = Number(accountId)
-        if (!id) return null
-        const rows = await db.query(
-            'SELECT id, username, password, realname FROM jw_account WHERE id = ? AND create_user = ? AND state = 1',
-            [id, uuid]
-        )
-        if (!rows?.length) return null
-        return rows[0]
-    }
-
-    async listVerifiedJwAccounts(uuid) {
-        const rows = await db.query(
-            'SELECT id, username, realname, deptName, className FROM jw_account WHERE create_user = ? AND state = 1 ORDER BY create_time DESC',
-            [uuid]
-        )
-        return rows || []
-    }
 }
 
 module.exports = new AccessControl();

+ 0 - 50
lib/Lepao/BindAudit.js

@@ -1,50 +0,0 @@
-const db = require('../../plugin/DataBase/db')
-
-const BindAuditAction = Object.freeze({
-    PLATFORM_BIND: 'platform_bind',
-    PLATFORM_UNBIND: 'platform_unbind',
-    BOT_BIND: 'bot_bind',
-    BOT_UNBIND: 'bot_unbind'
-})
-
-const BindAuditSource = Object.freeze({
-    USER_API: 'user_api',
-    ADMIN_API: 'admin_api',
-    SERVICE_API: 'service_api',
-    MCP_QQ: 'mcp_qq',
-    MCP_WORK_ORDER: 'mcp_work_order'
-})
-
-async function insertBindAudit({
-    studentNum,
-    ownerUuid = null,
-    action,
-    source,
-    operatorUuid = null,
-    detail = null,
-    createdAt = Date.now()
-}) {
-    if (!studentNum || !action || !source) return false
-
-    const sql = `
-        INSERT INTO lepao_bind_audit
-            (student_num, owner_uuid, action, source, operator_uuid, detail_json, created_at)
-        VALUES (?, ?, ?, ?, ?, ?, ?)
-    `
-    const rows = await db.query(sql, [
-        String(studentNum),
-        ownerUuid || null,
-        action,
-        source,
-        operatorUuid || null,
-        detail ? JSON.stringify(detail) : null,
-        Number(createdAt) || Date.now()
-    ])
-    return !!rows && rows.affectedRows === 1
-}
-
-module.exports = {
-    BindAuditAction,
-    BindAuditSource,
-    insertBindAudit
-}

+ 0 - 515
lib/Lepao/Mcp.js

@@ -1,515 +0,0 @@
-const db = require('../../plugin/DataBase/db')
-const path = require('path')
-const Logger = require('../Logger')
-const mq = require('../../plugin/mq')
-const { mq: mqName } = require('../../plugin/mq/mqPrefix')
-const EmailTemplate = require('../../plugin/Email/emailTemplate')
-const { insertBindAudit, BindAuditAction, BindAuditSource } = require('./BindAudit')
-
-class Mcp {
-    constructor() {
-        this.logger = new Logger(path.join(__dirname, '../logs/MCP.log'), 'INFO')
-
-        this.messageQueue = mqName('runforge_message_queue')
-
-        this.auto_day = [
-            { label: '周一', value: 1 },
-            { label: '周二', value: 2 },
-            { label: '周三', value: 3 },
-            { label: '周四', value: 4 },
-            { label: '周五', value: 5 },
-            { label: '周六', value: 6 },
-            { label: '周日', value: 0 }
-        ]
-
-        this.area = ["兰花湖校区跑区", "主校区北跑区", "主校区南跑区", "重庆工商大学茶园校区", "随机分配"]
-
-        this.auto_time = [
-            { label: '随机分配', value: -1 },
-            ...Array.from({ length: 17 }, (_, i) => {
-                const hour = i + 7
-                return { label: `${hour} ~ ${hour + 1}时`, value: hour }
-            })
-        ]
-
-        this.autoTimeLabel = (record) => {
-            if (record.auto_time === -1) {
-                if (record.today_auto_time)
-                    return `随机-今日${record.today_auto_time}时`
-                return '随机-待分配'
-            }
-            const match = this.auto_time.find(item => item.value === record.auto_time)
-            return match ? match.label : '-'
-        }
-
-        this.emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
-        this.banEmailList = ['icloud.com']
-    }
-
-    async bind_account({ sender, bind_code }) {
-        try {
-            if ([sender, bind_code].some(value => value === '' || value === null || value === undefined))
-                return '缺少参数'
-
-            this.logger.info(`MCP接收绑定账号请求:${sender},绑定码:${bind_code}`)
-            let selectSql = 'SELECT bot_account FROM lepao_extra WHERE bot_account = ?'
-            let selectRows = await db.query(selectSql, [sender])
-            if (!selectRows) return '系统出错,请稍后再试'
-            if (selectRows.length !== 0) return '您已绑定其他账号,请先解绑'
-
-            let umo = `QQ2749761853:FriendMessage:${sender}`
-            let sql = `
-                SELECT 
-                    f.student_num, a.name, a.create_user, f.bot_account
-                FROM 
-                    lepao_extra f
-                LEFT JOIN 
-                    lepao_account a
-                ON 
-                    f.student_num = a.student_num
-                WHERE 
-                    f.bind_code = ?
-            `
-            const rows = await db.query(sql, [bind_code])
-            if (!rows || rows.length === 0) return '系统出错,请稍后再试'
-
-            if (rows[0].bot_account !== null && rows[0].bot_account !== '') {
-                if (rows[0].bot_account !== sender) return '该账号已被他人绑定,请联系客服解绑'
-                return '该账号您已绑定'
-            }
-
-            let insertSql = `
-                UPDATE lepao_extra SET bot_account = ?, bot_umo = ? WHERE bind_code = ?
-            `
-            let insertRows = await db.query(insertSql, [sender, umo, bind_code])
-            if (!insertRows || insertRows.affectedRows !== 1)
-                return '系统出错,请稍后再试'
-
-            const auditOk = await insertBindAudit({
-                studentNum: rows[0].student_num,
-                ownerUuid: rows[0].create_user || null,
-                action: BindAuditAction.BOT_BIND,
-                source: BindAuditSource.MCP_QQ,
-                operatorUuid: null,
-                detail: { sender },
-                createdAt: Date.now()
-            })
-            if (!auditOk) {
-                this.logger.warn(`MCP绑定审计写入失败 student_num=${rows[0].student_num}`)
-            }
-            return `绑定成功,姓名:${rows[0].name ?? '未更新,请使用乐跑登录器更新账号信息'},学号:${rows[0].student_num}`
-        } catch (error) {
-            this.logger.error(`MCP绑定账号出错:${error.stack}`)
-            return '系统出错,请稍后再试'
-        }
-    }
-
-    async get_account_info({ sender }) {
-        try {
-            if ([sender].some(value => value === '' || value === null || value === undefined))
-                return '缺少参数'
-
-            this.logger.info(`MCP接收获取账号信息请求:${sender}`)
-            let sql = `
-                SELECT 
-                    l.name,
-                    l.student_num,
-                    l.state,
-                    l.area,
-                    l.auto_time,
-                    l.total_num,
-                    l.term_num,
-                    l.academy_name,
-                    l.sex,
-                    l.grade_id,
-                    l.email,
-                    l.auto_run,
-                    l.today_auto_time,
-                    l.target_count,
-                    l.auto_day,
-                    l.notice_type
-                FROM 
-                    lepao_account l
-                LEFT JOIN 
-                    lepao_extra f 
-                ON
-                    l.student_num = f.student_num
-                WHERE 
-                    f.bot_account = ?
-            `
-            const rows = await db.query(sql, [sender])
-            if (!rows || rows.length == 0) return '您尚未绑定乐跑账号,请先绑定'
-
-            let data = rows[0]
-            let returnMsg = `
-                姓名:${data.name ?? '未更新,请使用乐跑登录器更新账号信息'};学号:${data.student_num};账号状态:${data.state === 1 ? '正常' : '需使用乐跑登录器更新账号信息'};乐跑跑区:${data.area == '' ? "随机分配" : data.area};自动乐跑状态:${data.auto_run === 1 ? '开启' : '关闭'}
-                `
-            if (data.auto_run === 1) {
-                returnMsg += `自动乐跑时间:${this.autoTimeLabel(data)};`
-                returnMsg += `自动乐跑星期:${data.auto_day.slice().sort((a, b) => {
-                    if (a === 0) return 1; if (b === 0) return -1; return a - b;
-                }).map(day => this.auto_day.find(item => item.value === day)?.label).join(',')};`
-            }
-            if (data.sex) returnMsg += `性别:${data.sex === 1 ? '男' : '女'};`
-            if (data.email) returnMsg += `邮箱:${data.email};`
-            if (data.grade) returnMsg += `邮箱:${data.grade};`
-            if (data.academy_name) returnMsg += `学院:${data.academy_name};`
-            if (data.grade_id) returnMsg += `年级:${data.grade_id}级;`
-            if (data.target_count) returnMsg += `目标乐跑次数:${data.target_count};`
-            if (data.total_num) returnMsg += `累计乐跑次数:${data.total_num};`
-            if (data.notice_type) returnMsg += `通知方式:${data.notice_type};`
-
-            return returnMsg
-        } catch (error) {
-            this.logger.error(`MCP查询账号信息出错:${error.stack}`)
-            return '系统出错,请稍后再试'
-        }
-    }
-
-    async unbind_account({ sender }) {
-        try {
-            if ([sender].some(value => value === '' || value === null || value === undefined))
-                return '缺少参数'
-
-            this.logger.info(`MCP接收解绑账号请求:${sender}`)
-            const selectSql = `
-                SELECT e.student_num, a.create_user
-                FROM lepao_extra e
-                LEFT JOIN lepao_account a ON a.student_num = e.student_num
-                WHERE e.bot_account = ?
-            `
-            const selectRows = await db.query(selectSql, [sender])
-            if (!selectRows || selectRows.length === 0) return '您尚未绑定乐跑账号,请先绑定'
-
-            let insertSql = `
-                UPDATE lepao_extra SET bot_account = NULL, bot_umo = NULL WHERE bot_account = ?
-            `
-            let insertRows = await db.query(insertSql, [sender])
-            if (!insertRows || insertRows.affectedRows !== 1)
-                return '系统出错,请稍后再试'
-
-            const auditOk = await insertBindAudit({
-                studentNum: selectRows[0].student_num,
-                ownerUuid: selectRows[0].create_user || null,
-                action: BindAuditAction.BOT_UNBIND,
-                source: BindAuditSource.MCP_QQ,
-                operatorUuid: null,
-                detail: { sender },
-                createdAt: Date.now()
-            })
-            if (!auditOk) {
-                this.logger.warn(`MCP解绑审计写入失败 student_num=${selectRows[0].student_num}`)
-            }
-            return `解绑成功`
-        } catch (error) {
-            this.logger.error(`MCP解绑账号出错:${error.stack}`)
-            return '系统出错,请稍后再试'
-        }
-    }
-
-    async change_email({ sender, email }) {
-        try {
-            if ([sender, email].some(value => value === '' || value === null || value === undefined))
-                return '缺少参数'
-
-            this.logger.info(`MCP接收更换邮箱请求:${sender}`)
-
-            if (!this.emailRegex.test(email))
-                return '请检查邮箱格式是否正确'
-
-            const emailDomain = email.split('@')[1].toLowerCase()
-            if (this.banEmailList.includes(emailDomain))
-                return `暂不支持使用 ${emailDomain} 域名的邮箱,请更换其他邮箱后重试`
-
-            let insertSql = `
-                UPDATE 
-                    lepao_account la
-                JOIN 
-                    lepao_extra le 
-                ON 
-                    la.student_num = le.student_num
-                SET 
-                    la.email = ?
-                WHERE 
-                    le.bot_account = ?
-            `
-
-            let insertRows = await db.query(insertSql, [email, sender])
-            if (!insertRows || insertRows.affectedRows === 0)
-                return '您尚未绑定乐跑账号,请先绑定'
-            return `更换成功`
-        } catch (error) {
-            this.logger.error(`MCP更换邮箱出错:${error.stack}`)
-            return '系统出错,请稍后再试'
-        }
-    }
-
-    async set_notification({ sender, mode }) {
-        try {
-            if ([sender, mode].some(value => value === '' || value === null || value === undefined))
-                return '缺少参数'
-
-            this.logger.info(`MCP接收设置通知请求:${sender},mode:${mode}`)
-            if (mode !== 'email' && mode !== 'bot' && mode !== 'none') return '通知type不合法,仅支持 email, bot, none三种模式'
-
-            let sql = `
-                SELECT 
-                    a.notice_type, a.email
-                FROM 
-                    lepao_extra f
-                LEFT JOIN 
-                    lepao_account a
-                ON 
-                    f.student_num = a.student_num
-                WHERE 
-                    f.bot_account = ?
-            `
-            const rows = await db.query(sql, [sender])
-            if (!rows || rows.length == 0) return '您尚未绑定乐跑账号,请先绑定'
-
-            if (!rows[0].email) return '该账号还未设置邮箱,请先使用change_email设置邮箱'
-            if (rows[0].notice_type === mode) return `当前已是${mode}通知方式,无需修改`
-
-            let insertSql = `
-                UPDATE 
-                    lepao_account la
-                JOIN 
-                    lepao_extra le 
-                ON 
-                    la.student_num = le.student_num
-                SET 
-                    la.notice_type = ?
-                WHERE 
-                    le.bot_account = ?
-            `
-            let insertRows = await db.query(insertSql, [mode, sender])
-            if (!insertRows || insertRows.affectedRows !== 1)
-                return '系统出错,请稍后再试'
-            return `操作成功`
-        } catch (error) {
-            this.logger.error(`MCP更换通知方式出错:${error.stack}`)
-            return '系统出错,请稍后再试'
-        }
-    }
-
-    async change_area({ sender, area }) {
-        try {
-            if ([sender, area].some(value => value === '' || value === null || value === undefined))
-                return '缺少参数'
-
-            this.logger.info(`MCP接收更换跑区请求:${sender},area:${area}`)
-
-            if (!this.area.includes(area)) return '系统无该跑区,无法设置'
-
-            let insertSql = `
-                UPDATE 
-                    lepao_account la
-                JOIN 
-                    lepao_extra le 
-                ON 
-                    la.student_num = le.student_num
-                SET 
-                    la.area = ?
-                WHERE 
-                    le.bot_account = ?
-            `
-            let insertRows = await db.query(insertSql, [area === "随机分配" ? '' : area, sender])
-            if (!insertRows || insertRows.affectedRows === 0)
-                return '您尚未绑定乐跑账号,请先绑定'
-
-            return `操作成功`
-        } catch (error) {
-            this.logger.error(`MCP更换跑区出错:${error.stack}`)
-            return '系统出错,请稍后再试'
-        }
-    }
-
-    async change_auto_time({ sender, auto_time }) {
-        try {
-            if ([sender, auto_time].some(value => value === '' || value === null || value === undefined))
-                return '缺少参数'
-
-            this.logger.info(`MCP接收设置自动乐跑时间请求:${sender},auto_time:${auto_time}`)
-            if (!(Number.isInteger(auto_time) && ((auto_time >= 7 && auto_time <= 23) || auto_time === -1))) return '乐跑时间不合法,auto_time应为7~23或-1的整数'
-
-            let sql = `
-                SELECT 
-                    a.auto_run, a.auto_time, a.auto_day
-                FROM 
-                    lepao_extra f
-                LEFT JOIN 
-                    lepao_account a
-                ON 
-                    f.student_num = a.student_num
-                WHERE 
-                    f.bot_account = ?
-            `
-            const rows = await db.query(sql, [sender])
-            if (!rows || rows.length == 0) return '您尚未绑定乐跑账号,请先绑定'
-
-            if (rows[0].auto_run !== 1) return '该账号未开启自动乐跑,请前往哪吒乐跑系统开启自动乐跑后再设置乐跑时间'
-            if (rows[0].auto_time === auto_time) return `乐跑时间与现有时间一致,无需修改`
-
-            let insertSql = `
-                UPDATE 
-                    lepao_account la
-                JOIN 
-                    lepao_extra le 
-                ON 
-                    la.student_num = le.student_num
-                SET 
-                    la.auto_time = ?
-                WHERE 
-                    le.bot_account = ?
-            `
-            let insertRows = await db.query(insertSql, [auto_time, sender])
-            if (!insertRows || insertRows.affectedRows === 0)
-                return '系统出错,请稍后再试'
-
-            const data = {
-                auto_time, auto_day: rows[0].auto_day
-            }
-
-            return `操作成功,现在自动乐跑时间为:${data.auto_day.slice().sort((a, b) => {
-                if (a === 0) return 1; if (b === 0) return -1; return a - b;
-            }).map(day => this.auto_day.find(item => item.value === day)?.label).join(',')} ${auto_time === -1 ? '随机分配时段' : `${auto_time}~${auto_time + 1}时`}`
-        } catch (error) {
-            this.logger.error(`MCP更换设置自动乐跑时间出错:${error.stack}`)
-            return '系统出错,请稍后再试'
-        }
-    }
-
-    async change_auto_day({ sender, auto_day }) {
-        try {
-            if ([sender, auto_day].some(value => value === '' || value === null || value === undefined))
-                return '缺少参数'
-
-            this.logger.info(`MCP接收设置自动乐跑请求:${sender},auto_day:${auto_day}`)
-            if (!Array.isArray(auto_day) || !auto_day.every(v => Number.isInteger(v) && v >= 0 && v <= 6))
-                return '乐跑星期不合法,auto_day应为仅包含整数0~6的数组'
-
-            let sql = `
-                SELECT 
-                    a.auto_run, a.auto_time, a.auto_day
-                FROM 
-                    lepao_extra f
-                LEFT JOIN 
-                    lepao_account a
-                ON 
-                    f.student_num = a.student_num
-                WHERE 
-                    f.bot_account = ?
-            `
-            const rows = await db.query(sql, [sender])
-            if (!rows || rows.length == 0) return '您尚未绑定乐跑账号,请先绑定'
-
-            if (rows[0].auto_run !== 1) return '该账号未开启自动乐跑,请前往哪吒乐跑系统开启自动乐跑后再设置乐跑时间'
-
-            let insertSql = `
-                UPDATE 
-                    lepao_account la
-                JOIN 
-                    lepao_extra le 
-                ON 
-                    la.student_num = le.student_num
-                SET 
-                    la.auto_day = ?
-                WHERE 
-                    le.bot_account = ?
-            `
-            let insertRows = await db.query(insertSql, [JSON.stringify(auto_day), sender])
-            if (!insertRows || insertRows.affectedRows === 0)
-                return '系统出错,请稍后再试'
-
-            const auto_time = rows[0].auto_time
-            const data = {
-                auto_time, auto_day
-            }
-
-            return `操作成功,现在自动乐跑时间为:${data.auto_day.slice().sort((a, b) => {
-                if (a === 0) return 1; if (b === 0) return -1; return a - b;
-            }).map(day => this.auto_day.find(item => item.value === day)?.label).join(',')} ${auto_time === -1 ? '随机分配时段' : `${auto_time}~${auto_time + 1}时`}`
-        } catch (error) {
-            this.logger.error(`MCP更换设置自动乐跑时间出错:${error.stack}`)
-            return '系统出错,请稍后再试'
-        }
-    }
-
-    async create_work_order({ sender, email, title, content }) {
-        try {
-            if ([sender, email, title, content].some(value => value === '' || value === null || value === undefined))
-                return '缺少参数'
-
-            this.logger.info(`MCP接收工单创建请求:${sender},Email:${email}`)
-            if (!this.emailRegex.test(email))
-                return '请检查邮箱格式是否正确'
-
-            const emailDomain = email.split('@')[1].toLowerCase()
-            if (this.banEmailList.includes(emailDomain))
-                return `暂不支持使用 ${emailDomain} 域名的邮箱,请更换其他邮箱后重试`
-
-            let selectSql = `
-                SELECT 
-                    l.create_user
-                FROM 
-                    lepao_account l
-                LEFT JOIN
-                    lepao_extra e
-                ON 
-                    l.student_num = e.student_num
-                WHERE 
-                    e.bot_account = ?
-            `
-
-            let selectRows = await db.query(selectSql, [sender])
-            if (!selectRows || selectRows.length === 0) return '您还未绑定乐跑账号哦~请绑定后再试'
-
-            const time = new Date().getTime()
-            const uuid = selectRows[0].create_user
-
-            let msg = []
-            let message = {
-                time,
-                content,
-                files: [],
-                uuid,
-                type: 'user'
-            }
-            msg.push(message)
-            const systemMsg = {
-                time,
-                content: `该问题由用户${sender}通过小妍助理自动生成并提交~我们会尽快处理,请耐心等待`,
-                uuid: 'e4fe0277-0b1a-41a1-b25f-8b6e4cec3281',
-                type: 'system'
-            }
-            msg.push(systemMsg)
-            let sql = 'INSERT INTO work_order (title, email, msg, create_user, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?)'
-            let r = await db.query(sql, [title, email, msg, uuid, time, time])
-
-            if (!r || r.affectedRows !== 1)
-                return '系统出错,请稍后再试'
-
-            let kefuSql = `
-                    SELECT email FROM users
-                    WHERE JSON_CONTAINS(permission, '"admin"') OR JSON_CONTAINS(permission, '"service"')
-                `
-
-            let kefuRows = await db.query(kefuSql);
-
-            let emails = [...new Set(kefuRows.map(row => row.email))]
-
-            for (const email of emails) {
-                if (!email) break
-                EmailTemplate.orderNewReply(email, { id: r.insertId || id, content, files: [] })
-            }
-
-            return `提交成功`
-        } catch (error) {
-            this.logger.error(`MCP工单创建出错:${error.stack}`)
-            return '系统出错,请稍后再试'
-        }
-    }
-}
-
-const MCP = new Mcp()
-module.exports.MCP = MCP

+ 0 - 203
lib/Lepao/WorkOrderMcp.js

@@ -1,203 +0,0 @@
-const db = require('../../plugin/DataBase/db')
-const path = require('path')
-const Logger = require('../Logger')
-const mq = require('../../plugin/mq')
-const { mq: mqName } = require('../../plugin/mq/mqPrefix')
-const Redis = require('../../plugin/DataBase/Redis')
-const EmailTemplate = require('../../plugin/Email/emailTemplate')
-const { insertBindAudit, BindAuditAction, BindAuditSource } = require('./BindAudit')
-
-class WorkOrderMcp {
-    constructor() {
-        this.logger = new Logger(path.join(__dirname, '../logs/MCP.log'), 'INFO')
-
-        this.messageQueue = mqName('runforge_message_queue')
-
-        this.auto_day = [
-            { label: '周一', value: 1 },
-            { label: '周二', value: 2 },
-            { label: '周三', value: 3 },
-            { label: '周四', value: 4 },
-            { label: '周五', value: 5 },
-            { label: '周六', value: 6 },
-            { label: '周日', value: 0 }
-        ]
-
-        this.area = ["兰花湖校区跑区", "主校区北跑区", "主校区南跑区", "重庆工商大学茶园校区", "随机分配"]
-
-        this.auto_time = [
-            { label: '随机分配', value: -1 },
-            ...Array.from({ length: 17 }, (_, i) => {
-                const hour = i + 7
-                return { label: `${hour} ~ ${hour + 1}时`, value: hour }
-            })
-        ]
-
-        this.autoTimeLabel = (record) => {
-            if (record.auto_time === -1) {
-                if (record.today_auto_time)
-                    return `随机-今日${record.today_auto_time}时`
-                return '随机-待分配'
-            }
-            const match = this.auto_time.find(item => item.value === record.auto_time)
-            return match ? match.label : '-'
-        }
-
-        this.emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
-        this.banEmailList = ['icloud.com']
-        this.dailyUnbindLimit = 10
-    }
-
-
-    async get_account_info({ sender, student_num }) {
-        try {
-            if ([sender, student_num].some(value => value === '' || value === null || value === undefined))
-                return '缺少参数'
-
-            this.logger.info(`MCP接收获取账号信息请求:${sender},student_num:${student_num}`)
-            let sql = `
-                SELECT 
-                    l.name,
-                    l.student_num,
-                    l.state,
-                    l.area,
-                    l.auto_time,
-                    l.total_num,
-                    l.term_num,
-                    l.academy_name,
-                    l.sex,
-                    l.grade_id,
-                    l.email,
-                    l.auto_run,
-                    l.today_auto_time,
-                    l.target_count,
-                    l.auto_day,
-                    l.notice_type
-                FROM 
-                    lepao_account l
-                WHERE 
-                    l.student_num = ? AND l.create_user = ?
-            `
-            const rows = await db.query(sql, [student_num, sender])
-            if (!rows || rows.length == 0) return '该账号未被您绑定,无法查看'
-
-            let data = rows[0]
-            let returnMsg = `
-                姓名:${data.name ?? '未更新,请使用乐跑登录器更新账号信息'};学号:${data.student_num};账号状态:${data.state === 1 ? '正常' : '需使用乐跑登录器更新账号信息'};乐跑跑区:${data.area == '' ? "随机分配" : data.area};自动乐跑状态:${data.auto_run === 1 ? '开启' : '关闭'}
-                `
-            if (data.auto_run === 1) {
-                returnMsg += `自动乐跑时间:${this.autoTimeLabel(data)};`
-                returnMsg += `自动乐跑星期:${data.auto_day.slice().sort((a, b) => {
-                    if (a === 0) return 1; if (b === 0) return -1; return a - b;
-                }).map(day => this.auto_day.find(item => item.value === day)?.label).join(',')};`
-            }
-            if (data.sex) returnMsg += `性别:${data.sex === 1 ? '男' : '女'};`
-            if (data.email) returnMsg += `邮箱:${data.email};`
-            if (data.grade) returnMsg += `邮箱:${data.grade};`
-            if (data.academy_name) returnMsg += `学院:${data.academy_name};`
-            if (data.grade_id) returnMsg += `年级:${data.grade_id}级;`
-            if (data.target_count) returnMsg += `目标乐跑次数:${data.target_count};`
-            if (data.total_num) returnMsg += `累计乐跑次数:${data.total_num};`
-            if (data.notice_type) returnMsg += `通知方式:${data.notice_type};`
-
-            return returnMsg
-        } catch (error) {
-            this.logger.error(`MCP查询账号信息出错:${error.stack}`)
-            return '系统出错,请稍后再试'
-        }
-    }
-
-    getDailyUnbindRedisKey(sender) {
-        const now = new Date()
-        const year = now.getFullYear()
-        const month = `${now.getMonth() + 1}`.padStart(2, '0')
-        const day = `${now.getDate()}`.padStart(2, '0')
-        return `mcp:unbind:daily:${sender}:${year}${month}${day}`
-    }
-
-    getSecondsToDayEnd() {
-        const now = new Date()
-        const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0, 0)
-        return Math.max(1, Math.floor((tomorrow.getTime() - now.getTime()) / 1000))
-    }
-
-    async unbind_account({ sender, student_num }) {
-        try {
-            if ([sender, student_num].some(value => value === '' || value === null || value === undefined))
-                return '缺少参数'
-
-            this.logger.info(`工单MCP接收解绑账号请求:${sender}`)
-            const dailyUnbindKey = this.getDailyUnbindRedisKey(sender)
-            const dailyUnbindCount = Number(await Redis.get(dailyUnbindKey) || 0)
-            if (dailyUnbindCount >= this.dailyUnbindLimit) return '今日通过MCP解绑次数已超过限制,只能人工处理'
-
-            let selectSql = `SELECT create_user, auto_run FROM lepao_account WHERE student_num = ? AND create_user IS NOT NULL`
-            let selectRows = await db.query(selectSql, [student_num])
-            if (!selectRows || selectRows.length === 0 || !selectRows[0]?.create_user) return '该账号未绑定,无需解绑'
-
-            if (selectRows[0].auto_run === 1) return '该账号已开启自动乐跑,请用原绑定账号关闭自动乐跑后再解绑或联系人工客服处理'
-            let updateSql = `UPDATE lepao_account SET create_user = NULL, auto_run = 0, update_time = ? WHERE student_num = ?`
-            let updateRows = await db.query(updateSql, [Date.now(), student_num])
-            if (!updateRows || updateRows.affectedRows !== 1)
-                return '系统出错,请稍后再试'
-
-            const auditOk = await insertBindAudit({
-                studentNum: student_num,
-                ownerUuid: selectRows[0].create_user || null,
-                action: BindAuditAction.PLATFORM_UNBIND,
-                source: BindAuditSource.MCP_WORK_ORDER,
-                operatorUuid: sender || null,
-                detail: { sender },
-                createdAt: Date.now()
-            })
-            if (!auditOk) {
-                this.logger.warn(`工单解绑审计写入失败 student_num=${student_num}`)
-            }
-
-            const latestUnbindCount = await Redis.incr(dailyUnbindKey)
-            if (latestUnbindCount === 1) {
-                await Redis.expire(dailyUnbindKey, this.getSecondsToDayEnd())
-            }
-            return `解绑成功,您现在可绑定该账号`
-        } catch (error) {
-            this.logger.error(`工单MCP解绑账号出错:${error.stack}`)
-            return '系统出错,请稍后再试'
-        }
-    }
-
-    async need_manual({ sender, order_id }) {
-        try {
-            if ([sender, order_id].some(value => value === '' || value === null || value === undefined))
-                return '缺少参数'
-
-            this.logger.info(`MCP接收转人工服务请求:${order_id}`)
-
-            const selectSql = 'SELECT msg, state, email, need_manual FROM work_order WHERE id = ? AND create_user = ?'
-            const selectRows = await db.query(selectSql, [order_id, sender])
-            if (!selectRows || selectRows.length !== 1)
-                return '未找到你创建的工单,请核对工单ID'
-            if (selectRows[0].state === 2) return '工单已关闭,无法提交转人工服务,请提交新工单'
-            if (selectRows[0].need_manual === 1) return '该工单已提交转人工服务,无需重复提交'
-            
-            let msg = selectRows[0].msg
-            msg.push({
-                time: Date.now(),
-                content: '人工处理请求已提交,请耐心等待人工客服处理',
-                uuid: 'e4fe0277-0b1a-41a1-b25f-8b6e4cec3281',
-                type: 'system'
-            })
-
-            let updateSql = `UPDATE work_order SET msg = ?, need_manual = 1, update_time = ? WHERE id = ? AND create_user = ?`
-            let updateRows = await db.query(updateSql, [msg, Date.now(), order_id, sender])
-            if (!updateRows || updateRows.affectedRows !== 1) return '系统出错,请稍后再试'
-            
-            return `转人工服务请求已提交,后续消息AI助理小妍将不再回复`
-        } catch (error) {
-            this.logger.error(`MCP转人工服务出错:${error.stack}`)
-            return '系统出错,请稍后再试'
-        }
-    }
-}
-
-const WORKORDERMCP = new WorkOrderMcp()
-module.exports.WORKORDERMCP = WORKORDERMCP

+ 109 - 31
lib/Lepao/Worker.js

@@ -1,12 +1,17 @@
 const path = require('path')
 const mq = require('../../plugin/mq')
 const { assertRunforgeTaskIngress, TASK_QUEUE } = require('../../plugin/mq/runforgeTaskMq')
+const { RESULT_QUEUE, DEAD_QUEUE, MESSAGE_QUEUE } = require('../../plugin/mq/jkesMqNames')
 const db = require('../../plugin/DataBase/db')
 const Redis = require('../../plugin/DataBase/Redis')
 const EmailTemplate = require('../../plugin/Email/emailTemplate')
 const jkesRedisKeys = require('../../plugin/jkes/redisKeys')
 
 const Logger = require('../Logger')
+const { isProxyForwardEnabled } = require('../ProxyForwardClient')
+const { insertLedgerRecord } = require('./CountLedger')
+const { resolveLepaoBilling } = require('./billing')
+const { v4: uuidv4 } = require('uuid')
 
 class Worker {
     constructor() {
@@ -21,14 +26,15 @@ class Worker {
         this._consumeTag = null
 
         this.taskQueue = TASK_QUEUE
-        this.resultQueue = 'runforge_task_result_queue'
-        this.deadQueue = 'runforge_task_dead_queue'
-        this.noticeQueue = 'runforge_message_queue'
+        this.resultQueue = RESULT_QUEUE
+        this.deadQueue = DEAD_QUEUE
+        this.noticeQueue = MESSAGE_QUEUE
 
         this.channelName = 'lepao_worker'
 
         this.maxRetry = 3
         this.timeout = 15000
+        this.maxQueueLength = 2000
         this._lepaoRecordPathColumn = null
         // 预扣标记需覆盖“跑步+同步+重试”窗口,避免结算返还时标记已过期
         this.lepaoBalanceMarkerTtlSec = 24 * 3600
@@ -82,6 +88,25 @@ class Worker {
         }
     }
 
+    lepaoProgressTtlSec() {
+        const { getJkesSettings } = require('../../plugin/jkes/jkesSettings')
+        const { MANUAL_PACE_MAX_SEC } = require('../../plugin/jkes/paceUtils')
+        const cfg = getJkesSettings()
+        const maxKm = Math.max(2, Number(cfg.autoSingleRunMaxKm) || 10)
+        const maxPace = MANUAL_PACE_MAX_SEC
+        // 最长跑步时长 + finalize 轮询(40×90s)+ 10 分钟缓冲
+        return Math.ceil(maxKm * maxPace + 40 * 90 + 600)
+    }
+
+    async clearLepaoProgress(account) {
+        if (!account) return
+        try {
+            await Redis.del(jkesRedisKeys.lepaoProgress(account))
+        } catch (error) {
+            this.logger.error(`清除乐跑进度锁失败: ${error.stack || error}`)
+        }
+    }
+
     async getLepaoRecordPathColumn() {
         if (this._lepaoRecordPathColumn) return this._lepaoRecordPathColumn
         try {
@@ -97,18 +122,30 @@ class Worker {
         return this._lepaoRecordPathColumn
     }
 
-    async createLepaoRecord({ uuid, account, result = {}, pathId = null, pathData = [], state = 0 }) {
+    async createLepaoRecord({
+        uuid,
+        account,
+        result = {},
+        pathId = null,
+        pathData = [],
+        state = 0,
+        runMode = 'auto'
+    }) {
         if (!uuid || !account) return null
+        const publicId = uuidv4()
+        const safeRunMode = runMode === 'manual' ? 'manual' : 'auto'
         const pathCol = await this.getLepaoRecordPathColumn()
-        const sql = `INSERT INTO lepao_record (uuid, time, lepao_account, result, path_id, ${pathCol}, state) VALUES (?, ?, ?, ?, ?, ?, ?)`
+        const sql = `INSERT INTO lepao_record (public_id, uuid, time, lepao_account, result, path_id, ${pathCol}, state, run_mode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
         const r = await db.query(sql, [
+            publicId,
             uuid,
             Date.now(),
             account,
             JSON.stringify(result || {}),
             pathId,
             JSON.stringify(pathData || []),
-            state
+            state,
+            safeRunMode
         ])
         return r?.insertId || null
     }
@@ -299,10 +336,11 @@ class Worker {
             let userData = null
             let recordDbId = null
             let deductedKm = 0
+            let runCompleted = false
 
             try {
-                // const isSuccess = await Redis.get(jkesRedisKeys.lepaoSuccess(req.account))
-                // if (isSuccess) throw new Error('该账号当天已乐跑成功!请勿重复乐跑')
+                const isSuccess = await Redis.get(jkesRedisKeys.lepaoSuccess(req.account))
+                if (isSuccess) throw new Error('该账号当天已乐跑成功!请勿重复乐跑')
 
                 userData = await this.handlers['lepao.getUserData'](req, ctx)
 
@@ -317,11 +355,11 @@ class Worker {
                 if (inProgress) {
                     throw new Error('该账号已进入乐跑任务队列,请等待乐跑完成后再进行乐跑操作')
                 }
-                await Redis.set(progressKey, req.account, { EX: 1800 })
+                await Redis.set(progressKey, req.account, { EX: this.lepaoProgressTtlSec() })
 
                 const hour = new Date().getHours()
                 // if (hour < 7) {
-                //     throw new Error('当前不在有效乐跑时间范围内。哪吒乐跑支持乐跑时间段为7:00~24:00')
+                //     throw new Error('当前不在有效乐跑时间范围内。沐晨乐跑支持乐跑时间段为7:00~24:00')
                 // }
 
                 const { runJkesRecord } = require('../../plugin/jkes/runRecord')
@@ -400,7 +438,8 @@ class Worker {
                                     planned_km: targetKm,
                                     pace_sec_per_km: pace
                                 },
-                                state: 0
+                                state: 0,
+                                runMode: manual ? 'manual' : 'auto'
                             })
                         }
                         jkesEnd = await runJkesRecord({
@@ -409,6 +448,9 @@ class Worker {
                             pathPoints: rawData,
                             distanceM,
                             paceSecPerKm: pace,
+                            outboundMode: ctx?.outboundMode || 'auto',
+                            traceId,
+                            taskId: ctx?.taskId,
                             log: (msg) => this.logger.info(`[${traceId}] ${msg}`)
                         })
                         break
@@ -442,7 +484,8 @@ class Worker {
                         pathId: jkesPathId,
                         pathData: jkesEnd.uploadedPayloadPoints || [],
                         result: infoWithMeta || jkesEnd.endJson?.data || {},
-                        state: 1
+                        state: 1,
+                        runMode: manual ? 'manual' : 'auto'
                     })
                 } else {
                     await this.updateLepaoRecord(recordDbId, {
@@ -459,7 +502,7 @@ class Worker {
                     throw new Error(reason)
                 }
 
-                await this.writeSuccessRedis(req.account)
+                runCompleted = true
                 const autoDoubleSlot =
                     !manual && (req.autoDoubleSlot === true || req.autoDoubleSlot === 'true')
                 const finalizeTask = {
@@ -526,8 +569,8 @@ class Worker {
 
                 throw err
             } finally {
-                if (req?.account) {
-                    await Redis.del(jkesRedisKeys.lepaoProgress(req.account))
+                if (!runCompleted && req?.account) {
+                    await this.clearLepaoProgress(req.account)
                 }
             }
         })
@@ -593,6 +636,7 @@ class Worker {
                         traceId
                     }, { id: `${traceId}:notice:fail:finalize` })
                 }
+                await this.clearLepaoProgress(account)
                 return { traceId, finalized: false, refunded: true }
             }
             if (!synced) {
@@ -605,10 +649,9 @@ class Worker {
                 throw err
             }
 
-            const officialKmRaw = this.roundKm(recordDistanceKm(latest))
-            const officialKm = officialKmRaw > 0 ? officialKmRaw : this.roundKm(targetKm || 0)
-            const preDeduct = this.roundKm(deductedKm || targetKm || 0)
-            const refundKm = this.roundKm(Math.max(0, preDeduct - officialKm))
+            const plannedKm = this.roundKm(deductedKm || targetKm || 0)
+            const actualKm = this.roundKm(recordDistanceKm(latest))
+            const { billableKm, refundKm } = resolveLepaoBilling(plannedKm, actualKm)
 
             if (refundKm > 0) {
                 await this.handlers['lepao.refundCount'](
@@ -625,14 +668,15 @@ class Worker {
             await this.updateLepaoRecord(recordDbId, {
                 result: {
                     ...(latest || {}),
-                    planned_km: this.roundKm(targetKm || 0),
-                    deducted_km: preDeduct,
-                    official_km: officialKm,
+                    planned_km: plannedKm,
+                    deducted_km: plannedKm,
+                    actual_km: actualKm,
+                    official_km: billableKm,
                     refunded_km: refundKm
                 },
                 state: 2
             })
-            await recordSuccess(account, officialKm, { autoDoubleSlot: officialKm >= 2 && !!autoDoubleSlot })
+            await recordSuccess(account, actualKm, { autoDoubleSlot: actualKm >= 2 && !!autoDoubleSlot })
             await this.syncRunCount({ account, student_id: account, token })
 
             // 达成本月预设目标后:关闭自动乐跑并发送目标完成邮件(target_count=0 表示不限制)
@@ -661,12 +705,16 @@ class Worker {
                 this.logger.warn(`[${traceId}] 目标达成处理失败:${e.message || e}`)
             }
 
+            await this.writeSuccessRedis(account)
+            await this.clearLepaoProgress(account)
+
             if (ctx.channel) {
                 await this.enqueueTask(ctx.channel, 'lepao.sendNotice', {
                     account,
                     success: true,
                     data: {
-                        distance: officialKm,
+                        distance: actualKm,
+                        billedKm: billableKm,
                         time: Number(latest.useTime) || 0,
                         record_failed_reason: '',
                         refundedKm: refundKm
@@ -674,7 +722,7 @@ class Worker {
                     traceId
                 }, { id: `${traceId}:notice:success:finalize` })
             }
-            return { traceId, finalized: true, officialKm, refundKm }
+            return { traceId, finalized: true, actualKm, billableKm, refundKm }
         })
 
         this.register('lepao.sendNotice', async (req, ctx) => {
@@ -801,12 +849,23 @@ class Worker {
             }
 
             this.logger.info(`${account || uuid}开始扣减乐跑公里 ${amountKm}km`)
+            const userRows = await db.query('SELECT lepao_count FROM users WHERE uuid = ?', [uuid])
+            const beforeCount = Number(userRows?.[0]?.lepao_count || 0)
             const useLepaoCountSql =
                 'UPDATE users SET lepao_count = ROUND(lepao_count - ?, 2) WHERE uuid = ? AND lepao_count >= ?'
             const r = await db.query(useLepaoCountSql, [amountKm, uuid, amountKm])
             if (!r || r.affectedRows !== 1) {
                 throw new Error(`扣减乐跑公里失败:余额不足(需 ${amountKm}km)`)
             }
+            await insertLedgerRecord({
+                userUuid: uuid,
+                delta: -amountKm,
+                balanceBefore: beforeCount,
+                balanceAfter: beforeCount - amountKm,
+                bizType: 'run_consume',
+                bizId: consumeKey,
+                remark: `${account || uuid}乐跑扣除`
+            })
             this.logger.info(`${account || uuid}扣减乐跑公里完成`)
 
             await Redis.set(consumeKey, '1', { EX: this.lepaoBalanceMarkerTtlSec })
@@ -836,11 +895,22 @@ class Worker {
             }
 
             this.logger.info(`${account || uuid}开始返还乐跑公里 ${amountKm}km`)
+            const userRows = await db.query('SELECT lepao_count FROM users WHERE uuid = ?', [uuid])
+            const beforeCount = Number(userRows?.[0]?.lepao_count || 0)
             const sql = 'UPDATE users SET lepao_count = ROUND(lepao_count + ?, 2) WHERE uuid = ?'
             const r = await db.query(sql, [amountKm, uuid])
             if (!r || r.affectedRows !== 1) {
                 throw new Error('返还乐跑公里失败:数据库更新失败')
             }
+            await insertLedgerRecord({
+                userUuid: uuid,
+                delta: amountKm,
+                balanceBefore: beforeCount,
+                balanceAfter: beforeCount + amountKm,
+                bizType: 'run_refund',
+                bizId: refundKey,
+                remark: `${account || uuid}乐跑失败返还`
+            })
             this.logger.info(`${account || uuid}返还乐跑公里完成`)
             await Redis.set(refundKey, '1', { EX: this.lepaoBalanceMarkerTtlSec })
             return true
@@ -923,9 +993,9 @@ class Worker {
 
             await this.startConsumeLoop()
 
-            this.logger.info('哪吒乐跑 Worker 启动成功(JKES)')
+            this.logger.info('沐晨乐跑 Worker 启动成功(JKES)')
         } catch (err) {
-            this.logger.error('哪吒乐跑 Worker 启动失败: ' + (err.stack || err))
+            this.logger.error('沐晨乐跑 Worker 启动失败: ' + (err.stack || err))
         }
     }
 
@@ -951,8 +1021,14 @@ class Worker {
         await channel.prefetch(5)
 
         await assertRunforgeTaskIngress(channel, this.logger)
-        await channel.assertQueue(this.resultQueue, { durable: true })
-        await channel.assertQueue(this.deadQueue, { durable: true })
+        await channel.assertQueue(this.resultQueue, {
+            durable: true,
+            arguments: { 'x-max-length': this.maxQueueLength }
+        })
+        await channel.assertQueue(this.deadQueue, {
+            durable: true,
+            arguments: { 'x-max-length': this.maxQueueLength }
+        })
 
         const handleTaskMessage = async (msg) => {
                 if (!msg) return
@@ -1004,8 +1080,10 @@ class Worker {
                     if (needEarlyAck) {
                         safeAck()
                     }
+                    const proxyEnabled = isProxyForwardEnabled()
+                    const outboundMode = proxyEnabled ? 'proxy' : 'direct'
                     const result = await this.withTimeout(
-                        handler(data, { traceId, channel, taskId: id }),
+                        handler(data, { traceId, channel, taskId: id, outboundMode }),
                         type,
                         runMs
                     )
@@ -1096,7 +1174,7 @@ class Worker {
     async stop() {
         this.running = false
         await mq.close()
-        this.logger.info('哪吒乐跑 Worker 已停止')
+        this.logger.info('沐晨乐跑 Worker 已停止')
     }
 }
 

+ 23 - 0
lib/Lepao/billing.js

@@ -0,0 +1,23 @@
+function roundKm(value) {
+    const n = Number(value)
+    if (!Number.isFinite(n)) return 0
+    return Math.round(n * 100) / 100
+}
+
+/**
+ * 乐跑扣费:跑前按预计距离预扣,结算时按 min(预计, 实际) 计费。
+ * 多扣退还差额,少扣不补收。
+ * @returns {{ plannedKm: number, actualKm: number, billableKm: number, refundKm: number }}
+ */
+function resolveLepaoBilling(plannedKm, actualKm) {
+    const planned = roundKm(plannedKm)
+    const actual = roundKm(actualKm)
+    const billableKm = roundKm(Math.min(planned, actual))
+    const refundKm = roundKm(Math.max(0, planned - actual))
+    return { plannedKm: planned, actualKm: actual, billableKm, refundKm }
+}
+
+module.exports = {
+    roundKm,
+    resolveLepaoBilling
+}

+ 0 - 111
lib/Lepao/lepaoOfficialApi.js

@@ -1,111 +0,0 @@
-const { URLSearchParams } = require('url')
-const { dataEncrypt, dataDecrypt, dataSign } = require('../../plugin/Lepao/Crypto')
-const { postLepaoSchool } = require('./lepaoSchoolHttp')
-
-const BASE_URL = 'https://lepao.ctbu.edu.cn/v3/api.php'
-const DEFAULT_USER_AGENT = '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'
-
-function lepaoTimestamp() {
-    return Number((Date.now() / 1000).toFixed(3))
-}
-
-function lepaoNonce() {
-    return String(Math.floor(Math.random() * 900000 + 100000))
-}
-
-function buildSignedRaw(account, extraFields = {}) {
-    const studentNum = account.student_num || account.student_id
-    const raw = {
-        school_id: account.school_id,
-        term_id: 0,
-        course_id: 0,
-        class_id: 0,
-        student_num: studentNum,
-        card_id: studentNum,
-        uid: account.uid,
-        token: account.token,
-        timestamp: lepaoTimestamp(),
-        version: 1,
-        nonce: lepaoNonce(),
-        ostype: 5,
-        ...extraFields
-    }
-    raw.sign = dataSign(raw)
-    return raw
-}
-
-function unwrapLepaoResponse(result) {
-    if (result?.data && Number(result.is_encrypt) === 1) {
-        const decrypted = dataDecrypt(result.data)
-        if (!decrypted) {
-            const err = new Error('官方乐跑接口解密失败')
-            err.code = 'LEPAO_DECRYPT_FAILED'
-            throw err
-        }
-        result.data = JSON.parse(decrypted)
-    }
-
-    const failedByStatus = Object.prototype.hasOwnProperty.call(result || {}, 'status') && Number(result.status) !== 1
-    const failedByCode = Object.prototype.hasOwnProperty.call(result || {}, 'code') && Number(result.code) !== 1 && Number(result.code) !== 200
-    if (failedByStatus || failedByCode) {
-        const msg = result?.info || result?.msg || '官方乐跑接口请求失败'
-        const err = new Error(msg)
-        err.code = Number(result?.status) === 101 ? 'LEPAO_LOGIN_EXPIRED' : 'LEPAO_OFFICIAL_FAILED'
-        err.result = result
-        throw err
-    }
-
-    return result
-}
-
-async function postLepaoApi(pathSuffix, account, extraFields = {}, options = {}) {
-    const raw = buildSignedRaw(account, extraFields)
-    const form = new URLSearchParams()
-    form.append('ostype', '5')
-    form.append('data', dataEncrypt(JSON.stringify(raw)))
-
-    const res = await postLepaoSchool(`${BASE_URL}${pathSuffix}`, form, {
-        headers: {
-            'Content-Type': 'application/x-www-form-urlencoded',
-            'Accept': '*/*',
-            'Accept-Language': 'zh-CN,zh-Hans;q=0.9',
-            'Accept-Encoding': 'gzip, deflate, br',
-            'Referer': 'https://servicewechat.com/wxf94c4ddb63d87ede/32/page-frame.html',
-            'User-Agent': account.userAgent || DEFAULT_USER_AGENT
-        },
-        timeout: options.timeout || 20000,
-        logger: options.logger
-    })
-
-    return unwrapLepaoResponse(res.data)
-}
-
-async function getCurrentTerm(account, options = {}) {
-    const result = await postLepaoApi('/WpRun/getTermList', account, {}, options)
-    const list = Array.isArray(result.data) ? result.data : []
-    const current = list.find(item => String(item.status) === '1')
-    if (!current?.term_id) {
-        const err = new Error('未找到当前学期')
-        err.code = 'LEPAO_TERM_NOT_FOUND'
-        throw err
-    }
-    return current
-}
-
-async function getTermRunRecord(account, { termId, page = 1 } = {}, options = {}) {
-    const currentPage = Number(page)
-    return postLepaoApi(
-        '/WpRun/getTermRunRecord',
-        account,
-        {
-            term_id: String(termId),
-            page: Number.isFinite(currentPage) && currentPage > 0 ? currentPage : 1
-        },
-        options
-    )
-}
-
-module.exports = {
-    getCurrentTerm,
-    getTermRunRecord
-}

+ 0 - 42
lib/Lepao/lepaoSchoolHttp.js

@@ -1,42 +0,0 @@
-const { forwardRequest, briefUrlPath } = require('../ProxyForwardClient')
-
-function lepaoHttpLogLabel(traceId, mqTaskId) {
-    let s = ''
-    if (traceId) s += `[${traceId}] `
-    s += '[lepaoSchoolHttp]'
-    if (mqTaskId) s += ` [${mqTaskId}]`
-    return s
-}
-
-/**
- * 对 lepao 学校 API 的 POST,经 RunForge-ProxyServer 转发(失败时由代理服务自动直连回退)。
- */
-async function postLepaoSchool(url, data, options = {}) {
-    const {
-        headers = {},
-        timeout = 15000,
-        logger = null,
-        outboundMode = 'auto',
-        mqTaskId = null,
-        traceId = null
-    } = options
-
-    const logPrefix = lepaoHttpLogLabel(traceId, mqTaskId)
-    logger?.info?.(`${logPrefix} POST ${briefUrlPath(url)} mode=${outboundMode}`)
-
-    return forwardRequest({
-        method: 'post',
-        url,
-        data,
-        headers,
-        timeout,
-        outboundMode,
-        logger,
-        logPrefix,
-        scene: 'lepao_school'
-    })
-}
-
-module.exports = {
-    postLepaoSchool
-}

+ 0 - 121
lib/Lepao/officialAccountAccess.js

@@ -1,121 +0,0 @@
-const db = require('../../plugin/DataBase/db')
-const AccessControl = require('../AccessControl')
-const { BaseStdResponse } = require('../../BaseStdResponse')
-
-function isMissing(value) {
-    return value === '' || value === null || value === undefined
-}
-
-function errorResponse(base, msg) {
-    return { ...base, msg }
-}
-
-async function queryAccount(studentNum) {
-    const rows = await db.query(
-        `SELECT uid, token, school_id, student_num, userAgent, state, create_user
-         FROM lepao_account
-         WHERE student_num = ?`,
-        [studentNum]
-    )
-    return rows?.[0] || null
-}
-
-function validateRunnableAccount(account) {
-    if (!account) {
-        return errorResponse(BaseStdResponse.ERR, '未找到该乐跑账号')
-    }
-    if (account.state !== 1) {
-        return errorResponse(BaseStdResponse.ERR, '仅状态为正常的账号可查看官方记录')
-    }
-    if (!account.uid || !account.token || !account.school_id) {
-        return errorResponse(BaseStdResponse.ERR, '乐跑账号登录信息不完整,请使用登录器更新账号')
-    }
-    return null
-}
-
-async function loadUserOfficialAccount(uuid, studentNum) {
-    const account = await queryAccount(studentNum)
-    if (!account || account.create_user !== uuid) {
-        return {
-            error: errorResponse(BaseStdResponse.ERR, '未找到该乐跑账号或无权限操作')
-        }
-    }
-
-    const invalid = validateRunnableAccount(account)
-    if (invalid) return { error: invalid }
-
-    return { account }
-}
-
-async function loadAdminOfficialAccount(uuid, studentNum) {
-    const permission = await AccessControl.getPermission(uuid)
-    if (!permission.includes('admin') && !permission.includes('service') && !permission.includes('server')) {
-        return {
-            error: BaseStdResponse.PERMISSION_DENIED
-        }
-    }
-
-    const account = await queryAccount(studentNum)
-    const invalid = validateRunnableAccount(account)
-    if (invalid) return { error: invalid }
-
-    return { account }
-}
-
-function isLoginExpiredError(error) {
-    const msg = error?.message || ''
-    return (
-        error?.code === 'LEPAO_LOGIN_EXPIRED' ||
-        String(msg).includes('重新登录') ||
-        String(msg).includes('登录已过期') ||
-        String(msg).includes('登录失效')
-    )
-}
-
-async function markAccountLoginExpired(studentNum, logger) {
-    if (!studentNum) return
-    try {
-        await db.query('UPDATE lepao_account SET state = 0, update_time = ? WHERE student_num = ?', [
-            Date.now(),
-            studentNum
-        ])
-        logger?.warn?.(`${studentNum} 官方乐跑登录已失效,账号状态已更新为未登录`)
-    } catch (updateError) {
-        logger?.error?.(`更新乐跑账号登录状态失败 ${studentNum}: ${updateError.stack || updateError}`)
-    }
-}
-
-async function mapOfficialError(error, studentNum, logger) {
-    if (isLoginExpiredError(error)) {
-        await markAccountLoginExpired(studentNum, logger)
-        return errorResponse(BaseStdResponse.ERR, '乐跑登录已过期,请使用登录器更新账号')
-    }
-    const msg = error?.message || '获取官方乐跑记录失败'
-    return errorResponse(BaseStdResponse.ERR, msg)
-}
-
-/** 官方学期记录接口返回的 total_score_num 同步到 lepao_account.total_num */
-async function syncOfficialTotalNum(studentNum, officialData, logger) {
-    if (!studentNum || !officialData) return
-    const totalScoreNum = Number(officialData.total_score_num)
-    if (!Number.isFinite(totalScoreNum)) return
-
-    try {
-        const rows = await db.query(
-            'UPDATE lepao_account SET total_num = ? WHERE student_num = ?',
-            [totalScoreNum, studentNum]
-        )
-    } catch (error) {
-        logger?.error?.(`同步官方 total_num 失败 ${studentNum}: ${error.stack || error}`)
-    }
-}
-
-module.exports = {
-    isMissing,
-    loadUserOfficialAccount,
-    loadAdminOfficialAccount,
-    isLoginExpiredError,
-    markAccountLoginExpired,
-    mapOfficialError,
-    syncOfficialTotalNum
-}

+ 0 - 311
lib/Lepao/qgOssPut.js

@@ -1,311 +0,0 @@
-/**
-
- * OSS 上传:直连阿里云(ali-oss SDK 自行签名);乐跑学校 API 仍经 RunForge 代理。
-
- */
-
-const OSS = require('ali-oss')
-
-const { forwardRequest } = require('../ProxyForwardClient')
-
-
-
-function ossLogLabel(traceId, taskId) {
-
-    let s = ''
-
-    if (traceId) s += `[${traceId}] `
-
-    s += '[ossPut]'
-
-    if (taskId) s += ` [${taskId}]`
-
-    return s
-
-}
-
-
-
-function buildOssClient(sts, timeoutMs) {
-
-    return new OSS({
-
-        bucket: sts.bucket,
-
-        region: sts.region || 'oss-cn-hangzhou',
-
-        accessKeyId: sts.AccessKeyId,
-
-        accessKeySecret: sts.AccessKeySecret,
-
-        stsToken: sts.SecurityToken,
-
-        secure: true,
-
-        timeout: timeoutMs
-
-    })
-
-}
-
-
-
-/** ali-oss urllib 期望 { status, statusCode, headers, data, res } */
-
-function toUrllibResponse(resp) {
-
-    const status = resp?.status ?? resp?.statusCode
-
-    if (status == null) {
-
-        const err = new Error('代理转发未返回有效 HTTP status')
-
-        err.code = 'PROXY_FORWARD_BAD_RESPONSE'
-
-        throw err
-
-    }
-
-
-
-    let data = resp.data
-
-    if (data == null || data === '') {
-
-        data = Buffer.alloc(0)
-
-    } else if (!Buffer.isBuffer(data)) {
-
-        data = Buffer.from(typeof data === 'string' ? data : JSON.stringify(data))
-
-    }
-
-
-
-    const headers = resp.headers || {}
-
-    return {
-
-        status,
-
-        statusCode: status,
-
-        headers,
-
-        data,
-
-        res: { statusCode: status, headers }
-
-    }
-
-}
-
-
-
-function patchOssClientForForward(client, { outboundMode, timeout, logger, logPrefix, scene, ossPath }) {
-
-    const urllibMod = client.urllib
-
-    const originalRequest = urllibMod.request.bind(urllibMod)
-
-
-
-    async function runForward(reqUrl, reqOpt) {
-
-        const method = (reqOpt.method || 'GET').toUpperCase()
-
-        const headers = reqOpt.headers || {}
-
-        const body = reqOpt.content ?? reqOpt.body
-
-
-
-        if (outboundMode === 'direct') {
-
-            return originalRequest(reqUrl, reqOpt)
-
-        }
-
-
-
-        try {
-
-            const resp = await forwardRequest({
-
-                method,
-
-                url: reqUrl,
-
-                data: body,
-
-                headers,
-
-                timeout: reqOpt.timeout || timeout,
-
-                outboundMode,
-
-                logger,
-
-                logPrefix,
-
-                scene: scene || 'oss_put',
-
-                responseType: 'arraybuffer',
-
-                validateStatus: () => true
-
-            })
-
-
-
-            return toUrllibResponse(resp)
-
-        } catch (e) {
-
-            if (outboundMode === 'proxy') throw e
-
-            logger?.warn?.(`${logPrefix} OSS 经代理失败,改 SDK 直连: ${e.message || e}`)
-
-            return originalRequest(reqUrl, reqOpt)
-
-        }
-
-    }
-
-
-
-    // 兼容 urllib 的 callback / Promise 双模式,避免 ali-oss 收到 undefined status
-
-    urllibMod.request = function patchedRequest(url, args, callback) {
-
-        if (arguments.length === 2 && typeof args === 'function') {
-
-            callback = args
-
-            args = null
-
-        }
-
-        args = args || {}
-
-
-
-        if (typeof callback === 'function') {
-
-            runForward(url, args).then(
-
-                (result) => {
-
-                    if (!result || result.status == null) {
-
-                        callback(new Error('OSS HTTP 响应缺少 status'))
-
-                        return
-
-                    }
-
-                    callback(null, result.data, result.res)
-
-                },
-
-                (err) => callback(err)
-
-            )
-
-            return
-
-        }
-
-
-
-        return runForward(url, args)
-
-    }
-
-
-
-    logger?.info?.(`${logPrefix} PUT ${ossPath} mode=${outboundMode}`)
-
-}
-
-
-
-/**
-
- * @param {object} sts OSS STS 凭证
-
- * @param {string} ossPath 对象 key
-
- * @param {Buffer|string} content
-
- * @param {{ logger?: object, traceId?: string, taskId?: string, outboundMode?: 'auto'|'direct'|'proxy', timeout?: number }} options
-
- */
-
-async function putOssWithQgOutbound(sts, ossPath, content, options = {}) {
-
-    const {
-
-        logger = null,
-
-        traceId = null,
-
-        taskId = null,
-
-        outboundMode = 'direct',
-
-        timeout = 60000
-
-    } = options
-
-
-
-    const logPrefix = () => ossLogLabel(traceId, taskId)
-
-
-
-    if (outboundMode === 'direct') {
-
-        logger?.info?.(`${logPrefix()} PUT 直连 ${ossPath}`)
-
-        const client = buildOssClient(sts, timeout)
-
-        return client.put(ossPath, content)
-
-    }
-
-
-
-    const client = buildOssClient(sts, timeout)
-
-    patchOssClientForForward(client, {
-
-        outboundMode,
-
-        timeout,
-
-        logger,
-
-        logPrefix: logPrefix(),
-
-        scene: 'oss_put',
-
-        ossPath
-
-    })
-
-
-
-    return client.put(ossPath, content)
-
-}
-
-
-
-module.exports = {
-
-    putOssWithQgOutbound
-
-}
-
-

+ 0 - 66
lib/Lepao/qgOutboundAxios.js

@@ -1,66 +0,0 @@
-/**
- * 通用 HTTP 出站:经 RunForge-ProxyServer 转发。
- */
-const { forwardRequest } = require('../ProxyForwardClient')
-
-function logLabel(traceId, mqTaskId) {
-    let s = ''
-    if (traceId) s += `[${traceId}] `
-    s += '[outboundAxios]'
-    if (mqTaskId) s += ` [${mqTaskId}]`
-    return s
-}
-
-/**
- * @param {{
- *   method?: 'get'|'post'|'put'|'patch'|'delete'|'head'|'options'
- *   url: string
- *   data?: any
- *   headers?: object
- *   timeout?: number
- *   outboundMode?: 'auto'|'direct'|'proxy'
- *   logger?: object
- *   traceId?: string|null
- *   mqTaskId?: string|null
- *   scene?: string
- *   validateStatus?: (status:number)=>boolean
- *   responseType?: string
- *   transformResponse?: Function[]
- * }} opts
- */
-async function axiosWithQgOutbound(opts) {
-    const {
-        method = 'get',
-        url,
-        data,
-        headers = {},
-        timeout = 15000,
-        outboundMode = 'auto',
-        logger = null,
-        traceId = null,
-        mqTaskId = null,
-        scene = 'outbound',
-        validateStatus,
-        responseType,
-        transformResponse
-    } = opts
-
-    return forwardRequest({
-        method,
-        url,
-        data,
-        headers,
-        timeout,
-        outboundMode,
-        validateStatus,
-        responseType,
-        transformResponse,
-        logger,
-        logPrefix: logLabel(traceId, mqTaskId),
-        scene
-    })
-}
-
-module.exports = {
-    axiosWithQgOutbound
-}

+ 0 - 109
lib/Lepao/syncAccountInfo.js

@@ -1,109 +0,0 @@
-const db = require('../../plugin/DataBase/db')
-const { postLepaoSchool } = require('./lepaoSchoolHttp')
-const { URLSearchParams } = require('url')
-const { dataEncrypt, dataDecrypt, dataSign } = require('../../plugin/Lepao/Crypto')
-
-const DEFAULT_USER_AGENT = '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'
-
-async function syncAccountInfo({ studentNum, createUser, logger }) {
-    if (!studentNum) {
-        return { ok: false, msg: '缺少学号参数' }
-    }
-
-    const conditionSql = createUser
-        ? 'student_num = ? AND create_user = ?'
-        : 'student_num = ?'
-    const queryParams = createUser ? [studentNum, createUser] : [studentNum]
-    const rows = await db.query(
-        `SELECT uid, token, school_id, userAgent FROM lepao_account WHERE ${conditionSql}`,
-        queryParams
-    )
-    if (!rows || rows.length !== 1) {
-        return { ok: false, msg: '未找到该乐跑账号或无权限操作' }
-    }
-
-    const account = rows[0]
-    const raw = {
-        uid: account.uid,
-        token: account.token,
-        school_id: account.school_id,
-        term_id: 0,
-        course_id: 0,
-        class_id: 0,
-        student_num: studentNum,
-        card_id: studentNum,
-        timestamp: Number((Date.now() / 1000).toFixed(3)),
-        version: 1,
-        nonce: String(Math.floor(Math.random() * 900000 + 100000)),
-        ostype: 5
-    }
-    raw.sign = dataSign(raw)
-
-    const form = new URLSearchParams()
-    form.append('ostype', '5')
-    form.append('data', dataEncrypt(JSON.stringify(raw)))
-
-    const headers = {
-        'Content-Type': 'application/x-www-form-urlencoded',
-        'Accept': '*/*',
-        'Accept-Language': 'zh-CN,zh-Hans;q=0.9',
-        'Accept-Encoding': 'gzip, deflate, br',
-        'Referer': 'https://servicewechat.com/wxf94c4ddb63d87ede/32/page-frame.html',
-        'User-Agent': account.userAgent || DEFAULT_USER_AGENT
-    }
-
-    let result
-    try {
-        const apiRes = await postLepaoSchool(
-            'https://lepao.ctbu.edu.cn/v3/api.php/Run2/beforeRunV260',
-            form,
-            { headers, timeout: 20000, logger }
-        )
-
-        result = apiRes.data
-        if (result?.data && result?.is_encrypt === 1) {
-            result.data = JSON.parse(dataDecrypt(result.data))
-        }
-    } catch (error) {
-        logger?.error?.(`同步乐跑账号远端请求失败 ${studentNum}: ${error.stack || error}`)
-        return { ok: false, msg: '同步失败,请稍后再试' }
-    }
-
-    const info = result?.info || result?.msg || '系统繁忙,请稍后再试'
-    const updateTime = Date.now()
-
-    // 登录失效时,仅更新状态并返回失败信息
-    if (String(info).includes('重新登录') || Number(result?.status) === 101) {
-        await db.query(
-            `UPDATE lepao_account SET state = 0, update_time = ? WHERE ${conditionSql}`,
-            [updateTime, ...queryParams]
-        )
-        return { ok: false, msg: info, loginExpired: true }
-    }
-
-    if (!result || Number(result.status) !== 1 || !result.data) {
-        return { ok: false, msg: info }
-    }
-
-    const term_num = Number(result.data.term_num ?? 0)
-    const total_num = Number(result.data.total_num ?? 30)
-    const updateRows = await db.query(
-        `UPDATE lepao_account SET term_num = ?, total_num = ?, state = 1, update_time = ? WHERE ${conditionSql}`,
-        [term_num, total_num, updateTime, ...queryParams]
-    )
-    if (!updateRows || updateRows.affectedRows !== 1) {
-        return { ok: false, msg: '数据库更新失败' }
-    }
-
-    return {
-        ok: true,
-        data: {
-            student_num: studentNum,
-            term_num,
-            total_num,
-            state: 1
-        }
-    }
-}
-
-module.exports = { syncAccountInfo }

+ 3 - 3
lib/OrderRefundService.js

@@ -29,7 +29,7 @@ function evaluateRefundEligibility({
     const purchasedCount = Number(goodsLepaoCount || 0)
     const remainingCount = Number(userLepaoCount || 0)
     if (purchasedCount > 0 && remainingCount <= purchasedCount) {
-        return { canRefund: false, reason: '账户剩余次数需大于订单购买次数才可退款' }
+        return { canRefund: false, reason: '账户剩余里程需大于订单购买里程才可退款' }
     }
 
     return { canRefund: true, reason: '' }
@@ -95,7 +95,7 @@ async function executeOrderRefund({
         const deductLepao = Number(order.lepao_count || 0)
         const deductIc = Number(order.ic_count || 0)
         if (deductLepao > 0 && Number(user.lepao_count || 0) < deductLepao) {
-            return { ok: false, msg: '账户乐跑次数不足,无法完成退款' }
+            return { ok: false, msg: '账户乐跑里程不足,无法完成退款' }
         }
 
         await requestPaymentRefund({
@@ -151,7 +151,7 @@ async function executeOrderRefund({
             if (lockedDeductLepao > 0 && beforeLepao < lockedDeductLepao) {
                 await conn.rollback()
                 logger?.error?.(`退款支付已成功但扣次失败,需人工处理,订单号:${orderId}`)
-                return { ok: false, msg: '支付已退款但扣减次数失败,请联系客服处理' }
+                return { ok: false, msg: '支付已退款但扣减里程失败,请联系客服处理' }
             }
 
             const afterLepao = beforeLepao - lockedDeductLepao

+ 0 - 50
lib/QK/LeaseWatcher.js

@@ -1,50 +0,0 @@
-const Logger = require('../Logger')
-const { TaskScheduler } = require('./TaskScheduler')
-
-class LeaseWatcher {
-    constructor(options = {}) {
-        this.intervalMs = options.intervalMs || 30 * 1000
-        this.logger = options.logger || new Logger()
-        this.scheduler = options.scheduler || new TaskScheduler()
-        this.timer = null
-        this.running = false
-    }
-
-    async tick() {
-        if (this.running) {
-            this.logger.warn('[QK][LeaseWatcher] 上一轮巡检尚未结束,跳过本次执行')
-            return
-        }
-        this.running = true
-        try {
-            const count = await this.scheduler.requeueExpiredTasks()
-            if (count > 0) {
-                this.logger.info(`[QK][LeaseWatcher] 已将 ${count} 个失联任务重新入队`)
-            }
-        } catch (err) {
-            this.logger.error(`[QK][LeaseWatcher] 执行失败:${err.stack || err}`)
-        } finally {
-            this.running = false
-        }
-    }
-
-    start() {
-        if (this.timer) {
-            return this.timer
-        }
-        this.logger.info(`[QK][LeaseWatcher] 已启动,巡检间隔 ${this.intervalMs}ms`)
-        this.tick()
-        this.timer = setInterval(() => this.tick(), this.intervalMs)
-        return this.timer
-    }
-
-    stop() {
-        if (this.timer) {
-            clearInterval(this.timer)
-            this.timer = null
-            this.logger.info('[QK][LeaseWatcher] 已停止')
-        }
-    }
-}
-
-module.exports = LeaseWatcher

+ 0 - 1843
lib/QK/TaskScheduler.js

@@ -1,1843 +0,0 @@
-const crypto = require('crypto')
-const bcryptjs = require('bcryptjs')
-const config = require('../../config.json')
-const db = require('../../plugin/DataBase/db')
-const Redis = require('../../plugin/DataBase/Redis')
-const Logger = require('../Logger')
-const AccessControl = require('../AccessControl')
-
-const TASK_STATUS = {
-    PAUSED: 'paused',
-    PENDING: 'pending',
-    ASSIGNED: 'assigned',
-    RUNNING: 'running',
-    SUCCESS: 'success',
-    FAILED: 'failed',
-    CANCELLED: 'cancelled'
-}
-
-const REPORT_LOG_EVENTS = new Set([
-    'request_result',
-    'progress_snapshot',
-    'grab_success',
-    'grab_fail'
-])
-
-class TaskScheduler {
-    static _schemaReady = false
-
-    constructor(options = {}) {
-        this.leaseMs = options.leaseMs || config.qk?.leaseMs || 90 * 1000
-        this.heartbeatTtlSeconds = options.heartbeatTtlSeconds || config.qk?.heartbeatTtlSeconds || 45
-        this.pullLockTtlSeconds = options.pullLockTtlSeconds || 5
-        this.staleTaskMs = options.staleTaskMs || config.qk?.staleTaskMs || 60 * 60 * 1000
-        this.memPerSlotMb = options.memPerSlotMb || config.qk?.memPerSlotMb || 3072
-        this.memReserveMb = options.memReserveMb || config.qk?.memReserveMb || 1024
-        this.maxSlotsCap = options.maxSlotsCap || config.qk?.maxSlotsCap || 10
-        this.slotCapacityFactor = options.slotCapacityFactor || config.qk?.slotCapacityFactor || 1.5
-        this.logger = options.logger || new Logger()
-    }
-
-    async ensureSchema() {
-        if (TaskScheduler._schemaReady) {
-            return
-        }
-        try {
-            const rows = await db.query("SHOW COLUMNS FROM qk_task LIKE 'auto_relogin'")
-            if (!rows || rows.length === 0) {
-                await db.query(
-                    'ALTER TABLE qk_task ADD COLUMN auto_relogin TINYINT(1) NOT NULL DEFAULT 0 AFTER enable_ggxxk'
-                )
-                this.logInfo('ensureSchema', '已添加 qk_task.auto_relogin 字段')
-            }
-            const jwRows = await db.query("SHOW COLUMNS FROM qk_task LIKE 'jw_account_id'")
-            if (!jwRows || jwRows.length === 0) {
-                await db.query(
-                    'ALTER TABLE qk_task ADD COLUMN jw_account_id INT NULL DEFAULT NULL AFTER student_num'
-                )
-                this.logInfo('ensureSchema', '已添加 qk_task.jw_account_id 字段')
-            }
-        } catch (err) {
-            this.logWarn('ensureSchema', '抢课任务表结构检查失败', {}, err)
-        }
-        TaskScheduler._schemaReady = true
-    }
-
-    parseAutoRelogin(payload = {}) {
-        if (payload.auto_relogin !== undefined) {
-            return payload.auto_relogin === true || Number(payload.auto_relogin) === 1
-        }
-        if (payload.AUTO_RELOGIN !== undefined) {
-            return payload.AUTO_RELOGIN === true || Number(payload.AUTO_RELOGIN) === 1
-        }
-        return false
-    }
-
-    extractReportCourseName(payload = {}) {
-        if (!payload || typeof payload !== 'object') {
-            return ''
-        }
-        if (payload.course_name) {
-            return String(payload.course_name)
-        }
-        if (payload.course) {
-            return String(payload.course)
-        }
-        if (payload.label) {
-            const label = String(payload.label)
-            const at = label.indexOf('@')
-            if (at > 0) {
-                return label.slice(0, at)
-            }
-            return label
-        }
-        if (Array.isArray(payload.courses) && payload.courses.length > 0) {
-            return payload.courses.join('、')
-        }
-        return ''
-    }
-
-    formatReportMessage(message = '', payload = {}) {
-        const courseName = this.extractReportCourseName(payload)
-        const text = String(message || '').trim()
-        if (!courseName) {
-            return text
-        }
-        if (!text) {
-            return `[${courseName}]`
-        }
-        if (text.includes(`[${courseName}]`) || text.startsWith(`${courseName}:`)) {
-            return text
-        }
-        return `[${courseName}] ${text}`
-    }
-
-    calculateMaxSlots(profile = {}) {
-        const freeMb = Math.max(0, Number(profile.free_mem_mb) || 0)
-        const totalMb = Math.max(0, Number(profile.total_mem_mb) || 0)
-        const threads = Math.max(1, Number(profile.cpu_threads) || 1)
-        const allocatableFreeMb = Math.max(0, freeMb - this.memReserveMb)
-        const allocatableTotalMb = Math.max(0, totalMb - this.memReserveMb)
-        const byFreeMem = Math.floor(allocatableFreeMb / this.memPerSlotMb)
-        const byTotalMem = Math.floor(allocatableTotalMb / this.memPerSlotMb)
-        const byCpu = Math.floor(threads * 0.8)
-        const fallbackMem = totalMb > 0 ? byTotalMem : 1
-        const byMem = freeMb > 0 ? Math.min(byFreeMem, byTotalMem) : fallbackMem
-        const base = Math.max(1, Math.min(byMem, byCpu))
-        const scaled = Math.max(1, Math.floor(base * this.slotCapacityFactor))
-        const cap = Math.max(1, Math.floor(this.maxSlotsCap * this.slotCapacityFactor))
-        return Math.max(1, Math.min(50, cap, scaled))
-    }
-
-    resolveClientMaxSlots(client, payload = {}) {
-        const fromPayload = this.calculateMaxSlots({
-            free_mem_mb: payload.free_mem_mb ?? client.free_mem_mb,
-            total_mem_mb: payload.total_mem_mb ?? client.total_mem_mb,
-            cpu_threads: payload.cpu_threads ?? client.cpu_threads
-        })
-        const reported = Number(payload.max_slots || client.max_slots || 0)
-        if (!reported) return fromPayload
-        return Math.max(1, Math.min(reported, fromPayload))
-    }
-
-    getClientAvailableSlots(client, payload = {}) {
-        const maxSlots = this.resolveClientMaxSlots(client, payload)
-        const currentSlots = Math.max(0, Number(client.current_slots ?? 0))
-        return Math.max(0, maxSlots - currentSlots)
-    }
-
-    async countClientActiveTasks(clientId) {
-        if (!clientId) return 0
-        const rows = await db.query(
-            'SELECT COUNT(*) AS cnt FROM qk_task WHERE assigned_client_id = ? AND status IN (?, ?)',
-            [clientId, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-        )
-        return Math.max(0, Number(rows?.[0]?.cnt || 0))
-    }
-
-    async syncClientSlots(clientId, now = Date.now()) {
-        if (!clientId) return 0
-        const count = await this.countClientActiveTasks(clientId)
-        await db.query(
-            'UPDATE qk_client SET current_slots = ?, update_time = ? WHERE client_id = ?',
-            [count, now, clientId]
-        )
-        try {
-            await Redis.set(`qk:client:slots:${clientId}`, String(count), { EX: this.heartbeatTtlSeconds })
-        } catch (_) {}
-        return count
-    }
-
-    async findRevokedClientTasks(clientId, runningTaskIds = []) {
-        const ids = (runningTaskIds || []).map(Number).filter(Boolean)
-        if (!ids.length) return []
-        const placeholders = ids.map(() => '?').join(',')
-        const rows = await db.query(
-            `SELECT id FROM qk_task WHERE id IN (${placeholders})
-            AND (assigned_client_id IS NULL OR assigned_client_id <> ? OR status NOT IN (?, ?))`,
-            [...ids, clientId, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-        )
-        return (rows || []).map(row => row.id)
-    }
-
-    async getClientAvailableSlotsAsync(client, payload = {}) {
-        const maxSlots = this.resolveClientMaxSlots(client, payload)
-        const activeCount = await this.countClientActiveTasks(client.client_id)
-        return Math.max(0, maxSlots - activeCount)
-    }
-
-    getInFlightTaskStatuses() {
-        return [TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-    }
-
-    /** 同一学号在单次拉取中只保留最早创建的一条待分配任务 */
-    pickPullableTasks(rows, limit) {
-        const picked = []
-        const seenStudentNums = new Set()
-        for (const row of rows || []) {
-            const studentNum = String(row.student_num || '').trim()
-            if (!studentNum || seenStudentNums.has(studentNum)) {
-                continue
-            }
-            seenStudentNums.add(studentNum)
-            picked.push(row)
-            if (picked.length >= limit) {
-                break
-            }
-        }
-        return picked
-    }
-
-    async countOnlineClients() {
-        const now = Date.now()
-        const threshold = now - this.heartbeatTtlSeconds * 1000
-        const rows = await db.query(
-            `SELECT COUNT(*) AS total FROM qk_client
-            WHERE enabled = 1 AND online = 1
-            AND last_heartbeat_at IS NOT NULL AND last_heartbeat_at >= ?`,
-            [threshold]
-        )
-        return Number(rows?.[0]?.total || 0)
-    }
-
-    safeStringify(obj) {
-        const seen = new WeakSet()
-        return JSON.stringify(obj, (key, value) => {
-            if (typeof value === 'object' && value !== null) {
-                if (seen.has(value)) return '[Circular]'
-                seen.add(value)
-            }
-            return value
-        })
-    }
-
-    sanitizeForLog(payload) {
-        if (!payload || typeof payload !== 'object') {
-            return payload
-        }
-        const copy = Array.isArray(payload) ? [...payload] : { ...payload }
-        for (const key of ['password', 'pass', 'password_enc', 'client_secret']) {
-            if (key in copy) {
-                copy[key] = '***'
-            }
-        }
-        return copy
-    }
-
-    buildLogPrefix(tag, ctx = {}) {
-        const parts = ['[QK]', `[${tag}]`]
-        if (ctx.taskId) parts.push(`[taskId=${ctx.taskId}]`)
-        if (ctx.clientId) parts.push(`[clientId=${ctx.clientId}]`)
-        if (ctx.uuid) parts.push(`[uuid=${ctx.uuid}]`)
-        return parts.join('')
-    }
-
-    logInfo(tag, message, ctx = {}, data = null) {
-        const prefix = this.buildLogPrefix(tag, ctx)
-        const suffix = data != null ? ` ${this.safeStringify(this.sanitizeForLog(data))}` : ''
-        this.logger.info(`${prefix} ${message}${suffix}`)
-    }
-
-    logWarn(tag, message, ctx = {}, data = null) {
-        const prefix = this.buildLogPrefix(tag, ctx)
-        const suffix = data != null ? ` ${this.safeStringify(this.sanitizeForLog(data))}` : ''
-        this.logger.warn(`${prefix} ${message}${suffix}`)
-    }
-
-    logError(tag, message, ctx = {}, err = null) {
-        const prefix = this.buildLogPrefix(tag, ctx)
-        const suffix = err ? ` ${err.stack || err}` : ''
-        this.logger.error(`${prefix} ${message}${suffix}`)
-    }
-
-    getPasswordKey() {
-        const source = process.env.QK_PASSWORD_KEY || config.qk?.passwordAesKey || config.database?.password || 'runforge-qk-default-key'
-        return crypto.createHash('sha256').update(String(source)).digest()
-    }
-
-    encryptPassword(password) {
-        const iv = crypto.randomBytes(16)
-        const cipher = crypto.createCipheriv('aes-256-cbc', this.getPasswordKey(), iv)
-        let encrypted = cipher.update(String(password), 'utf8', 'base64')
-        encrypted += cipher.final('base64')
-        return `${iv.toString('base64')}:${encrypted}`
-    }
-
-    decryptPassword(encrypted) {
-        const [ivText, payload] = String(encrypted || '').split(':')
-        if (!ivText || !payload) {
-            return ''
-        }
-        const decipher = crypto.createDecipheriv('aes-256-cbc', this.getPasswordKey(), Buffer.from(ivText, 'base64'))
-        let decrypted = decipher.update(payload, 'base64', 'utf8')
-        decrypted += decipher.final('utf8')
-        return decrypted
-    }
-
-    normalizeArray(value) {
-        if (Array.isArray(value)) {
-            return value.map(item => String(item).trim()).filter(Boolean)
-        }
-        if (typeof value === 'string') {
-            const trimmed = value.trim()
-            if (!trimmed) {
-                return []
-            }
-            try {
-                const parsed = JSON.parse(trimmed)
-                if (Array.isArray(parsed)) {
-                    return parsed.map(item => String(item).trim()).filter(Boolean)
-                }
-            } catch (_) {
-                return trimmed.split(/[\n,,]/).map(item => item.trim()).filter(Boolean)
-            }
-        }
-        return []
-    }
-
-    countTaskTargets(courses = [], courseGroups = []) {
-        return this.normalizeArray(courses).length + this.normalizeArray(courseGroups).length
-    }
-
-    getMinTaskInterval(totalCount) {
-        return totalCount >= 5 ? 500 : 200
-    }
-
-    validateTaskCoursesAndInterval(courses, courseGroups, intervalMs) {
-        const normalizedCourses = this.normalizeArray(courses)
-        const normalizedGroups = this.normalizeArray(courseGroups)
-        const totalCount = normalizedCourses.length + normalizedGroups.length
-        if (totalCount <= 0) {
-            throw new Error('至少需要填写一门课程或一个课程分组')
-        }
-        if (totalCount > 10) {
-            throw new Error('每个任务最多选择10门课程或分组')
-        }
-        const minInterval = this.getMinTaskInterval(totalCount)
-        const safeInterval = Number(intervalMs)
-        if (!Number.isFinite(safeInterval) || safeInterval < minInterval || safeInterval > 10000) {
-            throw new Error(`当前共 ${totalCount} 个抢课目标,间隔需在 ${minInterval}-10000ms 之间`)
-        }
-        return {
-            courses: normalizedCourses,
-            courseGroups: normalizedGroups,
-            intervalMs: safeInterval,
-            totalCount,
-            minInterval
-        }
-    }
-
-    async resolveTaskAccount(ownerUuid, payload, { requireAccount = true } = {}) {
-        const jwAccountId = Number(payload.jw_account_id || payload.jwAccountId)
-        if (jwAccountId) {
-            const account = await AccessControl.getVerifiedJwAccount(ownerUuid, jwAccountId)
-            if (!account) {
-                throw new Error('统一身份认证账号不存在或未验证')
-            }
-            return {
-                student_num: account.username,
-                password: account.password,
-                jw_account_id: account.id
-            }
-        }
-        const studentNum = String(payload.student_num || payload.user || '').trim()
-        const password = payload.password || payload.pass
-        if (studentNum && password) {
-            return {
-                student_num: studentNum,
-                password,
-                jw_account_id: payload.jw_account_id || null
-            }
-        }
-        if (requireAccount) {
-            throw new Error('请选择统一身份认证账号')
-        }
-        return null
-    }
-
-    async inferJwAccountId(ownerUuid, studentNum, currentId) {
-        if (currentId) {
-            return Number(currentId)
-        }
-        const username = String(studentNum || '').trim()
-        if (!username) {
-            return null
-        }
-        const rows = await db.query(
-            'SELECT id FROM jw_account WHERE create_user = ? AND username = ? AND state = 1 LIMIT 1',
-            [ownerUuid, username]
-        )
-        return rows?.[0]?.id || null
-    }
-
-    serializeTask(row, includeSecret = false) {
-        const result = { ...row }
-        result.enable_ggxxk = Number(result.enable_ggxxk) === 1
-        result.auto_relogin = Number(result.auto_relogin) === 1
-        result.courses = this.normalizeArray(result.courses)
-        result.course_groups = this.normalizeArray(result.course_groups)
-        if (typeof result.result_json === 'string' && result.result_json) {
-            try {
-                result.result_json = JSON.parse(result.result_json)
-            } catch (_) {}
-        }
-        result.jx0502zbid = row.batch_jx0502zbid || row.jx0502zbid || ''
-        result.batch_name = row.batch_name || ''
-        if (row.batch_enabled !== undefined) {
-            result.batch_enabled = Number(row.batch_enabled) === 1
-        }
-        delete result.batch_jx0502zbid
-        if (includeSecret) {
-            result.password = this.decryptPassword(result.password_enc)
-        }
-        delete result.password_enc
-        return result
-    }
-
-    taskBatchJoinSql(alias = 't') {
-        return `LEFT JOIN qk_batch b ON b.id = ${alias}.batch_id`
-    }
-
-    taskBatchSelectSql(alias = 't') {
-        return `, b.name AS batch_name, b.jx0502zbid AS batch_jx0502zbid, b.enabled AS batch_enabled`
-    }
-
-    async getBatchById(batchId) {
-        const rows = await db.query('SELECT * FROM qk_batch WHERE id = ?', [batchId])
-        if (!rows || rows.length === 0) {
-            throw new Error('抢课批次不存在')
-        }
-        return rows[0]
-    }
-
-    async requireEnabledBatch(batchId) {
-        const batch = await this.getBatchById(batchId)
-        if (!Number(batch.enabled)) {
-            throw new Error('所选抢课批次未启用')
-        }
-        return batch
-    }
-
-    resolveTaskBatchId(payload = {}) {
-        const batchId = Number(payload.batch_id || payload.batchId)
-        if (!batchId) {
-            throw new Error('请选择抢课批次')
-        }
-        return batchId
-    }
-
-    async releaseAssignedTask(task) {
-        return !!(task && [TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING].includes(task.status))
-    }
-
-    async pauseTasksByBatch(batchId, reason = '批次已停用,任务已收回') {
-        const rows = await db.query(
-            'SELECT id, status, assigned_client_id FROM qk_task WHERE batch_id = ? AND status IN (?, ?, ?)',
-            [batchId, TASK_STATUS.PENDING, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-        )
-        const now = Date.now()
-        const affectedClients = new Set()
-        let count = 0
-        for (const row of rows || []) {
-            if (row.assigned_client_id) {
-                affectedClients.add(row.assigned_client_id)
-            }
-            const result = await db.query(
-                `UPDATE qk_task SET status = ?, assigned_client_id = NULL, lease_expire_at = NULL,
-                assigned_at = NULL, exclude_client_id = NULL, update_time = ? WHERE id = ? AND status IN (?, ?, ?)`,
-                [TASK_STATUS.PAUSED, now, row.id, TASK_STATUS.PENDING, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-            )
-            if (result && result.affectedRows > 0) {
-                count += 1
-                await this.logTask(row.id, row.assigned_client_id, 'batch_paused', reason)
-            }
-        }
-        for (const clientId of affectedClients) {
-            await this.syncClientSlots(clientId, now)
-        }
-        if (count > 0) {
-            this.logInfo('pauseTasksByBatch', reason, { batchId }, { affected: count })
-        }
-        return count
-    }
-
-    async redispatchTasksByBatch(batchId, reason = '批次 ID 已变更,任务已重新进入分配队列') {
-        const rows = await db.query(
-            'SELECT id, status, assigned_client_id FROM qk_task WHERE batch_id = ? AND status IN (?, ?, ?)',
-            [batchId, TASK_STATUS.PENDING, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-        )
-        const now = Date.now()
-        let count = 0
-        for (const row of rows || []) {
-            if ([TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING].includes(row.status)) {
-                await this.releaseAssignedTask(row, now)
-                const result = await db.query(
-                    `UPDATE qk_task SET status = ?, assigned_client_id = NULL, lease_expire_at = NULL,
-                    assigned_at = NULL, exclude_client_id = NULL, update_time = ? WHERE id = ? AND status IN (?, ?)`,
-                    [TASK_STATUS.PENDING, now, row.id, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-                )
-                if (result && result.affectedRows > 0) {
-                    count += 1
-                    await this.logTask(row.id, row.assigned_client_id, 'batch_redispatch', reason)
-                }
-            }
-        }
-        if (count > 0) {
-            this.logInfo('redispatchTasksByBatch', reason, { batchId }, { affected: count })
-        }
-        return count
-    }
-
-    serializeBatch(row) {
-        return {
-            id: row.id,
-            name: row.name,
-            jx0502zbid: row.jx0502zbid,
-            enabled: Number(row.enabled) === 1,
-            create_time: row.create_time,
-            update_time: row.update_time
-        }
-    }
-
-    async listBatches(options = {}) {
-        const enabledOnly = !!options.enabledOnly
-        const where = enabledOnly ? 'WHERE enabled = 1' : ''
-        const rows = await db.query(`SELECT * FROM qk_batch ${where} ORDER BY update_time DESC, id DESC`)
-        return (rows || []).map(row => this.serializeBatch(row))
-    }
-
-    async createBatch(payload = {}) {
-        const name = String(payload.name || '').trim()
-        const jx0502zbid = String(payload.jx0502zbid || '').trim()
-        if (!name || !jx0502zbid) {
-            throw new Error('批次名称和批次 ID 不能为空')
-        }
-        const now = Date.now()
-        const result = await db.query(
-            'INSERT INTO qk_batch (name, jx0502zbid, enabled, create_time, update_time) VALUES (?, ?, ?, ?, ?)',
-            [name, jx0502zbid, payload.enabled === false ? 0 : 1, now, now]
-        )
-        if (!result || result.affectedRows <= 0) {
-            throw new Error('创建抢课批次失败')
-        }
-        this.logInfo('createBatch', '抢课批次已创建', { batchId: result.insertId }, { name, jx0502zbid })
-        return result.insertId
-    }
-
-    async updateBatch(batchId, payload = {}) {
-        const existing = await this.getBatchById(batchId)
-        const name = payload.name !== undefined ? String(payload.name || '').trim() : existing.name
-        const jx0502zbid = payload.jx0502zbid !== undefined ? String(payload.jx0502zbid || '').trim() : existing.jx0502zbid
-        const enabled = payload.enabled !== undefined ? (payload.enabled ? 1 : 0) : Number(existing.enabled)
-        if (!name || !jx0502zbid) {
-            throw new Error('批次名称和批次 ID 不能为空')
-        }
-        const now = Date.now()
-        const result = await db.query(
-            'UPDATE qk_batch SET name = ?, jx0502zbid = ?, enabled = ?, update_time = ? WHERE id = ?',
-            [name, jx0502zbid, enabled, now, batchId]
-        )
-        if (!result || result.affectedRows <= 0) {
-            throw new Error('更新抢课批次失败')
-        }
-        const disabledNow = Number(existing.enabled) === 1 && enabled === 0
-        const idChanged = String(existing.jx0502zbid) !== jx0502zbid
-        if (disabledNow) {
-            await this.pauseTasksByBatch(batchId)
-        } else if (idChanged && enabled === 1) {
-            await this.redispatchTasksByBatch(batchId)
-        }
-        this.logInfo('updateBatch', '抢课批次已更新', { batchId }, {
-            name,
-            jx0502zbid,
-            enabled: enabled === 1,
-            disabledNow,
-            idChanged
-        })
-    }
-
-    async logTask(taskId, clientId, event, message = '', payload = null) {
-        const sql = 'INSERT INTO qk_task_log (task_id, client_id, event, message, payload_json, create_time) VALUES (?, ?, ?, ?, ?, ?)'
-        await db.query(sql, [
-            taskId,
-            clientId || null,
-            event,
-            message || '',
-            payload ? JSON.stringify(payload) : null,
-            Date.now()
-        ])
-        this.logInfo('taskLog', message || event, { taskId, clientId }, payload ? { event, ...this.sanitizeForLog(payload) } : { event })
-    }
-
-    serializeReportLog(row) {
-        const result = { ...row }
-        if (typeof result.payload_json === 'string' && result.payload_json) {
-            try {
-                result.payload_json = JSON.parse(result.payload_json)
-            } catch (_) {}
-        }
-        const payload = result.payload_json && typeof result.payload_json === 'object'
-            ? result.payload_json
-            : {}
-        result.course_name = this.extractReportCourseName(payload)
-        result.display_message = this.formatReportMessage(result.message, payload)
-        return result
-    }
-
-    buildReportMessage(payload = {}) {
-        const base = (() => {
-            if (payload.message) {
-                return String(payload.message)
-            }
-            if (payload.error_msg) {
-                return String(payload.error_msg)
-            }
-            if (payload.error) {
-                return String(payload.error)
-            }
-            if (payload.success === true) {
-                return payload.label ? `${payload.label} 成功` : '请求成功'
-            }
-            return payload.label ? `${payload.label} 失败` : '请求失败'
-        })()
-        return this.formatReportMessage(base, payload)
-    }
-
-    async assertClientTaskAccess(clientId, taskId) {
-        const rows = await db.query(
-            'SELECT id, status, assigned_client_id FROM qk_task WHERE id = ?',
-            [taskId]
-        )
-        if (!rows || rows.length === 0) {
-            throw new Error('任务不存在')
-        }
-        const task = rows[0]
-        if (task.assigned_client_id !== clientId) {
-            throw new Error('任务不属于当前客户端')
-        }
-        if (![TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING].includes(task.status)) {
-            throw new Error('任务当前状态不可上报')
-        }
-        return task
-    }
-
-    async reportProgress(clientId, clientSecret, payload = {}) {
-        const client = await this.authenticateClient(clientId, clientSecret)
-        if (!client) {
-            throw new Error('客户端凭证无效')
-        }
-        const taskId = Number(payload.task_id || payload.id)
-        if (!taskId) {
-            throw new Error('缺少任务 ID')
-        }
-        const event = String(payload.event || 'request_result')
-        if (!REPORT_LOG_EVENTS.has(event) || event === 'grab_success' || event === 'grab_fail') {
-            throw new Error('不支持的上报类型')
-        }
-        const task = await this.assertClientTaskAccess(clientId, taskId)
-        const message = this.buildReportMessage(payload)
-        const now = Date.now()
-        await this.logTask(taskId, clientId, event, message, payload)
-        const updates = ['update_time = ?']
-        const params = [now]
-        if (task.status === TASK_STATUS.ASSIGNED) {
-            updates.push('status = ?')
-            params.push(TASK_STATUS.RUNNING)
-        }
-        if (payload.success !== true && message) {
-            updates.push('error_msg = ?')
-            params.push(message)
-        }
-        params.push(taskId, clientId)
-        await db.query(
-            `UPDATE qk_task SET ${updates.join(', ')} WHERE id = ? AND assigned_client_id = ?`,
-            params
-        )
-        this.logInfo('reportProgress', '客户端上报抢课进度', { taskId, clientId }, {
-            event,
-            success: payload.success === true,
-            message
-        })
-        return { task_id: taskId, event, message }
-    }
-
-    async createTask(uuid, payload) {
-        await this.ensureSchema()
-        const validated = this.validateTaskCoursesAndInterval(
-            payload.courses || payload.COURSES,
-            payload.course_groups || payload.COURSE_GROUPS,
-            payload.interval_ms || payload.INTERVAL_MS || 500
-        )
-        const { courses, courseGroups, intervalMs } = validated
-        const batchId = this.resolveTaskBatchId(payload)
-        await this.requireEnabledBatch(batchId)
-        const account = await this.resolveTaskAccount(uuid, payload, { requireAccount: true })
-        const time = Date.now()
-        const sql = `INSERT INTO qk_task
-            (create_user, name, batch_id, jx0502zbid, student_num, jw_account_id, password_enc, courses, course_groups, enable_ggxxk, auto_relogin, interval_ms, status, create_time, update_time)
-            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
-        const result = await db.query(sql, [
-            uuid,
-            payload.name,
-            batchId,
-            '',
-            account.student_num,
-            account.jw_account_id,
-            this.encryptPassword(account.password),
-            JSON.stringify(courses),
-            JSON.stringify(courseGroups),
-            payload.enable_ggxxk || payload.ENABLE_GGXXK ? 1 : 0,
-            this.parseAutoRelogin(payload) ? 1 : 0,
-            intervalMs,
-            TASK_STATUS.PAUSED,
-            time,
-            time
-        ])
-        if (!result || result.affectedRows <= 0) {
-            throw new Error('创建抢课任务失败')
-        }
-        await this.logTask(result.insertId, null, 'created', '用户提交抢课任务(未开始)')
-        this.logInfo('createTask', '抢课任务已创建', { taskId: result.insertId, uuid }, {
-            name: payload.name,
-            batch_id: batchId,
-            student_num: account.student_num,
-            jw_account_id: account.jw_account_id,
-            courses_count: courses.length,
-            course_groups_count: courseGroups.length,
-            interval_ms: intervalMs,
-            enable_ggxxk: !!(payload.enable_ggxxk || payload.ENABLE_GGXXK),
-            auto_relogin: this.parseAutoRelogin(payload)
-        })
-        return result.insertId
-    }
-
-    async updateTask(uuid, taskId, payload) {
-        await this.ensureSchema()
-        const rows = await db.query('SELECT status FROM qk_task WHERE id = ? AND create_user = ?', [taskId, uuid])
-        if (!rows || rows.length === 0) {
-            throw new Error('任务不存在')
-        }
-        if (![TASK_STATUS.PAUSED, TASK_STATUS.FAILED, TASK_STATUS.CANCELLED].includes(rows[0].status)) {
-            throw new Error('任务进行中或已分配,请先暂停后再修改')
-        }
-        const validated = this.validateTaskCoursesAndInterval(
-            payload.courses || payload.COURSES,
-            payload.course_groups || payload.COURSE_GROUPS,
-            payload.interval_ms || payload.INTERVAL_MS || 500
-        )
-        const { courses, courseGroups, intervalMs } = validated
-        const batchId = this.resolveTaskBatchId(payload)
-        await this.requireEnabledBatch(batchId)
-        const account = await this.resolveTaskAccount(uuid, payload, { requireAccount: true })
-        const params = [
-            payload.name,
-            batchId,
-            account.student_num,
-            account.jw_account_id,
-            this.encryptPassword(account.password),
-            JSON.stringify(courses),
-            JSON.stringify(courseGroups),
-            payload.enable_ggxxk || payload.ENABLE_GGXXK ? 1 : 0,
-            this.parseAutoRelogin(payload) ? 1 : 0,
-            intervalMs,
-            TASK_STATUS.PAUSED,
-            Date.now()
-        ]
-        params.push(taskId, uuid)
-        const sql = `UPDATE qk_task SET name = ?, batch_id = ?, student_num = ?, jw_account_id = ?, password_enc = ?, courses = ?, course_groups = ?, enable_ggxxk = ?, auto_relogin = ?, interval_ms = ?, status = ?, jx0502zbid = '', update_time = ?, assigned_client_id = NULL, lease_expire_at = NULL, result_json = NULL, error_msg = NULL, finished_time = NULL WHERE id = ? AND create_user = ?`
-        const result = await db.query(sql, params)
-        if (!result || result.affectedRows <= 0) {
-            throw new Error('更新抢课任务失败')
-        }
-        await this.logTask(taskId, null, 'updated', '用户更新抢课任务')
-        this.logInfo('updateTask', '抢课任务已更新(未开始)', { taskId, uuid }, {
-            name: payload.name,
-            student_num: account.student_num,
-            jw_account_id: account.jw_account_id,
-            courses_count: courses.length,
-            course_groups_count: courseGroups.length,
-            interval_ms: intervalMs,
-            account_changed: true
-        })
-    }
-
-    async listUserTasks(uuid, filters = {}) {
-        const where = ['t.create_user = ?']
-        const params = [uuid]
-        if (filters.status) {
-            where.push('t.status = ?')
-            params.push(filters.status)
-        }
-        if (filters.student_num) {
-            where.push('t.student_num LIKE ?')
-            params.push(`%${filters.student_num}%`)
-        }
-        if (filters.name) {
-            where.push('t.name LIKE ?')
-            params.push(`%${filters.name}%`)
-        }
-        const courseName = String(filters.course_name || filters.course || '').trim()
-        if (courseName) {
-            const keyword = `%${courseName}%`
-            where.push('(t.courses LIKE ? OR t.course_groups LIKE ?)')
-            params.push(keyword, keyword)
-        }
-        if (filters.batch_id) {
-            where.push('t.batch_id = ?')
-            params.push(Number(filters.batch_id))
-        }
-        const whereSql = where.join(' AND ')
-        const rows = await db.query(
-            `SELECT t.*${this.taskBatchSelectSql('t')}
-            FROM qk_task t
-            ${this.taskBatchJoinSql('t')}
-            WHERE ${whereSql}
-            ORDER BY t.create_time DESC`,
-            params
-        )
-        return (rows || []).map(row => this.serializeTask(row))
-    }
-
-    async getTaskDetail(uuid, taskId) {
-        const rows = await db.query(
-            `SELECT t.*${this.taskBatchSelectSql('t')}
-            FROM qk_task t
-            ${this.taskBatchJoinSql('t')}
-            WHERE t.id = ? AND t.create_user = ?`,
-            [taskId, uuid]
-        )
-        if (!rows || rows.length === 0) {
-            return null
-        }
-        const task = this.serializeTask(rows[0], false)
-        task.jw_account_id = await this.inferJwAccountId(uuid, task.student_num, task.jw_account_id)
-        const logs = await db.query('SELECT client_id, event, message, payload_json, create_time FROM qk_task_log WHERE task_id = ? ORDER BY create_time DESC LIMIT 100', [taskId])
-        return {
-            task,
-            logs: (logs || []).map(log => {
-                if (typeof log.payload_json === 'string' && log.payload_json) {
-                    try {
-                        log.payload_json = JSON.parse(log.payload_json)
-                    } catch (_) {}
-                }
-                return log
-            })
-        }
-    }
-
-    async cancelTask(uuid, taskId) {
-        const result = await db.query(
-            'UPDATE qk_task SET status = ?, update_time = ?, finished_time = ? WHERE id = ? AND create_user = ? AND status IN (?, ?, ?)',
-            [TASK_STATUS.CANCELLED, Date.now(), Date.now(), taskId, uuid, TASK_STATUS.PAUSED, TASK_STATUS.PENDING, TASK_STATUS.FAILED]
-        )
-        if (!result || result.affectedRows <= 0) {
-            throw new Error('任务不存在或当前状态不可取消')
-        }
-        await this.logTask(taskId, null, 'cancelled', '用户取消抢课任务')
-        this.logInfo('cancelTask', '用户已取消抢课任务', { taskId, uuid })
-    }
-
-    async startTask(uuid, taskId) {
-        const rows = await db.query('SELECT * FROM qk_task WHERE id = ? AND create_user = ?', [taskId, uuid])
-        if (!rows || rows.length === 0) {
-            throw new Error('任务不存在')
-        }
-        const task = rows[0]
-        if (![TASK_STATUS.PAUSED, TASK_STATUS.FAILED].includes(task.status)) {
-            throw new Error('仅未开始或失败的任务可开启')
-        }
-        if (task.batch_id) {
-            await this.requireEnabledBatch(task.batch_id)
-        }
-        const now = Date.now()
-        const result = await db.query(
-            `UPDATE qk_task SET status = ?, update_time = ?, assigned_client_id = NULL, lease_expire_at = NULL,
-            assigned_at = NULL, exclude_client_id = NULL, result_json = NULL, error_msg = NULL, finished_time = NULL
-            WHERE id = ? AND create_user = ? AND status IN (?, ?)`,
-            [TASK_STATUS.PENDING, now, taskId, uuid, TASK_STATUS.PAUSED, TASK_STATUS.FAILED]
-        )
-        if (!result || result.affectedRows <= 0) {
-            throw new Error('开启抢课任务失败')
-        }
-        await this.logTask(taskId, null, 'started', '用户开启抢课任务,等待分配')
-        this.logInfo('startTask', '用户已开启抢课任务', { taskId, uuid })
-    }
-
-    async pauseTask(uuid, taskId) {
-        const rows = await db.query('SELECT * FROM qk_task WHERE id = ? AND create_user = ?', [taskId, uuid])
-        if (!rows || rows.length === 0) {
-            throw new Error('任务不存在')
-        }
-        const task = rows[0]
-        if (![TASK_STATUS.PENDING, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING].includes(task.status)) {
-            throw new Error('当前状态不可暂停')
-        }
-        const now = Date.now()
-        const wasAssigned = [TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING].includes(task.status)
-        const releasedClientId = wasAssigned ? task.assigned_client_id : null
-        const result = await db.query(
-            `UPDATE qk_task SET status = ?, update_time = ?, assigned_client_id = NULL, lease_expire_at = NULL,
-            assigned_at = NULL, exclude_client_id = NULL
-            WHERE id = ? AND create_user = ? AND status IN (?, ?, ?)`,
-            [TASK_STATUS.PAUSED, now, taskId, uuid, TASK_STATUS.PENDING, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-        )
-        if (!result || result.affectedRows <= 0) {
-            throw new Error('暂停抢课任务失败')
-        }
-        if (releasedClientId) {
-            await this.syncClientSlots(releasedClientId, now)
-        }
-        await this.logTask(taskId, task.assigned_client_id, 'paused', wasAssigned ? '用户暂停任务,已收回客户端' : '用户暂停抢课任务')
-        this.logInfo('pauseTask', '用户已暂停抢课任务', { taskId, uuid }, {
-            released_from_client: wasAssigned ? task.assigned_client_id : null
-        })
-    }
-
-    async getAdminTask(taskId) {
-        const rows = await db.query(
-            `SELECT t.*, u.username, u.avatar${this.taskBatchSelectSql('t')}
-            FROM qk_task t
-            LEFT JOIN users u ON u.uuid COLLATE utf8mb4_general_ci = t.create_user COLLATE utf8mb4_general_ci
-            ${this.taskBatchJoinSql('t')}
-            WHERE t.id = ?`,
-            [taskId]
-        )
-        if (!rows || rows.length === 0) {
-            return null
-        }
-        const task = this.serializeTask(rows[0], false)
-        task.jw_account_id = await this.inferJwAccountId(rows[0].create_user, task.student_num, task.jw_account_id)
-        return task
-    }
-
-    async adminUpdateTask(taskId, payload) {
-        await this.ensureSchema()
-        const rows = await db.query('SELECT * FROM qk_task WHERE id = ?', [taskId])
-        if (!rows || rows.length === 0) {
-            throw new Error('任务不存在')
-        }
-        const task = rows[0]
-        if (task.status === TASK_STATUS.SUCCESS) {
-            throw new Error('已成功的任务不可编辑')
-        }
-        const validated = this.validateTaskCoursesAndInterval(
-            payload.courses || payload.COURSES,
-            payload.course_groups || payload.COURSE_GROUPS,
-            payload.interval_ms || payload.INTERVAL_MS || task.interval_ms || 500
-        )
-        const { courses, courseGroups, intervalMs } = validated
-        const now = Date.now()
-        const wasAssigned = [TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING].includes(task.status)
-        const releasedClientId = wasAssigned ? task.assigned_client_id : null
-        const batchId = this.resolveTaskBatchId(payload)
-        const account = await this.resolveTaskAccount(task.create_user, payload, { requireAccount: true })
-        const params = [
-            payload.name,
-            batchId,
-            account.student_num,
-            account.jw_account_id,
-            this.encryptPassword(account.password),
-            JSON.stringify(courses),
-            JSON.stringify(courseGroups),
-            payload.enable_ggxxk || payload.ENABLE_GGXXK ? 1 : 0,
-            this.parseAutoRelogin(payload) ? 1 : 0,
-            intervalMs,
-            TASK_STATUS.PAUSED,
-            now,
-            taskId
-        ]
-        const sql = `UPDATE qk_task SET name = ?, batch_id = ?, student_num = ?, jw_account_id = ?, password_enc = ?, courses = ?, course_groups = ?, enable_ggxxk = ?, auto_relogin = ?, interval_ms = ?, status = ?, jx0502zbid = '', update_time = ?, assigned_client_id = NULL, lease_expire_at = NULL, assigned_at = NULL, exclude_client_id = NULL, result_json = NULL, error_msg = NULL, finished_time = NULL WHERE id = ?`
-        const result = await db.query(sql, params)
-        if (!result || result.affectedRows <= 0) {
-            throw new Error('更新抢课任务失败')
-        }
-        if (releasedClientId) {
-            await this.syncClientSlots(releasedClientId, now)
-        }
-        await this.logTask(taskId, task.assigned_client_id, 'admin_updated', wasAssigned ? '管理员更新任务并收回(未开始)' : '管理员更新抢课任务')
-        this.logInfo('adminUpdateTask', '管理员已更新抢课任务', { taskId }, {
-            name: payload.name,
-            student_num: account.student_num,
-            jw_account_id: account.jw_account_id,
-            released_from_client: wasAssigned ? task.assigned_client_id : null
-        })
-    }
-
-    async adminCancelTask(taskId) {
-        const rows = await db.query('SELECT id, status, assigned_client_id FROM qk_task WHERE id = ?', [taskId])
-        if (!rows || rows.length === 0) {
-            throw new Error('任务不存在')
-        }
-        const task = rows[0]
-        if (![TASK_STATUS.PAUSED, TASK_STATUS.PENDING, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING, TASK_STATUS.FAILED].includes(task.status)) {
-            throw new Error('当前状态不可取消')
-        }
-        const now = Date.now()
-        const releasedClientId = [TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING].includes(task.status)
-            ? task.assigned_client_id
-            : null
-        const result = await db.query(
-            `UPDATE qk_task SET status = ?, assigned_client_id = NULL, lease_expire_at = NULL, assigned_at = NULL,
-            exclude_client_id = NULL, update_time = ?, finished_time = ? WHERE id = ? AND status IN (?, ?, ?, ?, ?)`,
-            [TASK_STATUS.CANCELLED, now, now, taskId, TASK_STATUS.PAUSED, TASK_STATUS.PENDING, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING, TASK_STATUS.FAILED]
-        )
-        if (!result || result.affectedRows <= 0) {
-            throw new Error('取消抢课任务失败')
-        }
-        if (releasedClientId) {
-            await this.syncClientSlots(releasedClientId, now)
-        }
-        await this.logTask(taskId, task.assigned_client_id, 'admin_cancelled', '管理员取消抢课任务')
-        this.logInfo('adminCancelTask', '管理员已取消抢课任务', { taskId })
-    }
-
-    async adminRetryTask(taskId) {
-        const rows = await db.query('SELECT id, status FROM qk_task WHERE id = ?', [taskId])
-        if (!rows || rows.length === 0) {
-            throw new Error('任务不存在')
-        }
-        if (![TASK_STATUS.FAILED, TASK_STATUS.CANCELLED].includes(rows[0].status)) {
-            throw new Error('仅失败或已取消的任务可重试')
-        }
-        const now = Date.now()
-        const result = await db.query(
-            `UPDATE qk_task SET status = ?, assigned_client_id = NULL, lease_expire_at = NULL, assigned_at = NULL,
-            exclude_client_id = NULL, result_json = NULL, error_msg = NULL, finished_time = NULL, update_time = ?
-            WHERE id = ? AND status IN (?, ?)`,
-            [TASK_STATUS.PENDING, now, taskId, TASK_STATUS.FAILED, TASK_STATUS.CANCELLED]
-        )
-        if (!result || result.affectedRows <= 0) {
-            throw new Error('重试抢课任务失败')
-        }
-        await this.logTask(taskId, null, 'admin_retry', '管理员将任务重新加入待分配队列')
-        this.logInfo('adminRetryTask', '管理员已重试抢课任务', { taskId })
-    }
-
-    async adminStartTask(taskId) {
-        const rows = await db.query('SELECT * FROM qk_task WHERE id = ?', [taskId])
-        if (!rows || rows.length === 0) {
-            throw new Error('任务不存在')
-        }
-        const task = rows[0]
-        if (![TASK_STATUS.PAUSED, TASK_STATUS.FAILED].includes(task.status)) {
-            throw new Error('仅未开始或失败的任务可开启')
-        }
-        if (task.batch_id) {
-            await this.requireEnabledBatch(task.batch_id)
-        }
-        const now = Date.now()
-        const result = await db.query(
-            `UPDATE qk_task SET status = ?, update_time = ?, assigned_client_id = NULL, lease_expire_at = NULL,
-            assigned_at = NULL, exclude_client_id = NULL, result_json = NULL, error_msg = NULL, finished_time = NULL
-            WHERE id = ? AND status IN (?, ?)`,
-            [TASK_STATUS.PENDING, now, taskId, TASK_STATUS.PAUSED, TASK_STATUS.FAILED]
-        )
-        if (!result || result.affectedRows <= 0) {
-            throw new Error('开启抢课任务失败')
-        }
-        await this.logTask(taskId, null, 'admin_started', '管理员开启抢课任务,等待分配')
-        this.logInfo('adminStartTask', '管理员已开启抢课任务', { taskId })
-    }
-
-    async adminPauseTask(taskId) {
-        const rows = await db.query('SELECT * FROM qk_task WHERE id = ?', [taskId])
-        if (!rows || rows.length === 0) {
-            throw new Error('任务不存在')
-        }
-        const task = rows[0]
-        if (![TASK_STATUS.PENDING, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING].includes(task.status)) {
-            throw new Error('当前状态不可暂停')
-        }
-        const now = Date.now()
-        const wasAssigned = [TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING].includes(task.status)
-        const releasedClientId = wasAssigned ? task.assigned_client_id : null
-        const result = await db.query(
-            `UPDATE qk_task SET status = ?, update_time = ?, assigned_client_id = NULL, lease_expire_at = NULL,
-            assigned_at = NULL, exclude_client_id = NULL
-            WHERE id = ? AND status IN (?, ?, ?)`,
-            [TASK_STATUS.PAUSED, now, taskId, TASK_STATUS.PENDING, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-        )
-        if (!result || result.affectedRows <= 0) {
-            throw new Error('暂停抢课任务失败')
-        }
-        if (releasedClientId) {
-            await this.syncClientSlots(releasedClientId, now)
-        }
-        await this.logTask(taskId, task.assigned_client_id, 'admin_paused', wasAssigned ? '管理员暂停任务,已收回客户端' : '管理员暂停抢课任务')
-        this.logInfo('adminPauseTask', '管理员已暂停抢课任务', { taskId }, {
-            released_from_client: wasAssigned ? task.assigned_client_id : null
-        })
-    }
-
-    async authenticateClient(clientId, clientSecret) {
-        if (!clientId || !clientSecret) {
-            this.logWarn('authClient', '客户端认证失败:缺少凭证', { clientId: clientId || 'unknown' })
-            return null
-        }
-        const rows = await db.query('SELECT * FROM qk_client WHERE client_id = ? AND enabled = 1', [clientId])
-        if (!rows || rows.length !== 1) {
-            this.logWarn('authClient', '客户端认证失败:客户端不存在或已禁用', { clientId })
-            return null
-        }
-        if (!bcryptjs.compareSync(String(clientSecret), rows[0].client_secret_hash)) {
-            this.logWarn('authClient', '客户端认证失败:密钥不匹配', { clientId })
-            return null
-        }
-        return rows[0]
-    }
-
-    async enrollOrAuthenticateClient(clientId, clientSecret, payload = {}) {
-        const existing = await this.authenticateClient(clientId, clientSecret)
-        if (existing) {
-            return existing
-        }
-        if (!clientId || !clientSecret || !String(clientId).startsWith('qk-cli-')) {
-            throw new Error('客户端凭证无效')
-        }
-        const rows = await db.query('SELECT * FROM qk_client WHERE client_id = ?', [clientId])
-        if (rows && rows.length > 0) {
-            throw new Error('客户端凭证无效')
-        }
-        const time = Date.now()
-        const label = payload.label || payload.hostname || `auto-${clientId}`
-        try {
-            const result = await db.query(
-                'INSERT INTO qk_client (client_id, client_secret_hash, label, create_time, update_time) VALUES (?, ?, ?, ?, ?)',
-                [clientId, bcryptjs.hashSync(String(clientSecret), 10), label, time, time]
-            )
-            if (!result || result.affectedRows <= 0) {
-                throw new Error('客户端自动注册失败')
-            }
-        } catch (err) {
-            if (err?.code === 'ER_DUP_ENTRY') {
-                const raced = await this.authenticateClient(clientId, clientSecret)
-                if (raced) {
-                    return raced
-                }
-            }
-            throw err
-        }
-        const created = await db.query('SELECT * FROM qk_client WHERE client_id = ? AND enabled = 1', [clientId])
-        if (!created || created.length !== 1) {
-            throw new Error('客户端自动注册失败')
-        }
-        this.logInfo('enrollClient', '抢课客户端已自动注册', { clientId }, { label })
-        return created[0]
-    }
-
-    async registerClient(clientId, clientSecret, payload = {}) {
-        const client = await this.enrollOrAuthenticateClient(clientId, clientSecret, payload)
-        const maxSlots = this.resolveClientMaxSlots(client, payload)
-        const time = Date.now()
-        await db.query(
-            'UPDATE qk_client SET label = COALESCE(NULLIF(?, \'\'), label), max_slots = ?, hostname = ?, os_username = ?, cpu_model = ?, cpu_threads = ?, total_mem_mb = ?, free_mem_mb = ?, platform = ?, last_heartbeat_at = ?, online = 1, update_time = ? WHERE client_id = ?',
-            [
-                payload.label || '',
-                maxSlots,
-                payload.hostname || null,
-                payload.os_username || null,
-                payload.cpu_model || null,
-                payload.cpu_threads || null,
-                payload.total_mem_mb || null,
-                payload.free_mem_mb || null,
-                payload.platform || null,
-                time,
-                time,
-                clientId
-            ]
-        )
-        await Redis.set(`qk:client:hb:${clientId}`, String(time), { EX: this.heartbeatTtlSeconds })
-        const currentSlots = await this.syncClientSlots(clientId, time)
-        this.logInfo('registerClient', '抢课客户端已注册/上线', { clientId }, {
-            label: payload.label || client.label,
-            max_slots: maxSlots,
-            current_slots: currentSlots,
-            hostname: payload.hostname,
-            platform: payload.platform,
-            cpu_threads: payload.cpu_threads,
-            total_mem_mb: payload.total_mem_mb,
-            free_mem_mb: payload.free_mem_mb
-        })
-        return { client_id: clientId, max_slots: maxSlots, current_slots: currentSlots }
-    }
-
-    async heartbeat(clientId, clientSecret, payload = {}) {
-        const client = await this.authenticateClient(clientId, clientSecret)
-        if (!client) {
-            throw new Error('客户端凭证无效')
-        }
-        const runningTasks = Array.isArray(payload.running_tasks) ? payload.running_tasks.map(Number).filter(Boolean) : []
-        const revokedTaskIds = await this.findRevokedClientTasks(clientId, runningTasks)
-        const activeRunningTasks = runningTasks.filter(id => !revokedTaskIds.includes(Number(id)))
-        const maxSlots = this.resolveClientMaxSlots(client, payload)
-        const now = Date.now()
-        await db.query(
-            'UPDATE qk_client SET max_slots = ?, hostname = ?, os_username = ?, cpu_model = ?, cpu_threads = ?, total_mem_mb = ?, free_mem_mb = ?, platform = ?, last_heartbeat_at = ?, online = 1, update_time = ? WHERE client_id = ?',
-            [
-                maxSlots,
-                payload.hostname || null,
-                payload.os_username || null,
-                payload.cpu_model || null,
-                payload.cpu_threads || null,
-                payload.total_mem_mb || null,
-                payload.free_mem_mb || null,
-                payload.platform || null,
-                now,
-                now,
-                clientId
-            ]
-        )
-        await Redis.set(`qk:client:hb:${clientId}`, String(now), { EX: this.heartbeatTtlSeconds })
-        if (activeRunningTasks.length > 0) {
-            const leaseExpireAt = now + this.leaseMs
-            const placeholders = activeRunningTasks.map(() => '?').join(',')
-            await db.query(
-                `UPDATE qk_task SET status = ?, lease_expire_at = ?, update_time = ? WHERE assigned_client_id = ? AND id IN (${placeholders}) AND status IN (?, ?)`,
-                [TASK_STATUS.RUNNING, leaseExpireAt, now, clientId, ...activeRunningTasks, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-            )
-            this.logInfo('heartbeat', '客户端心跳续租运行中任务', { clientId }, {
-                running_tasks: activeRunningTasks,
-                revoked_task_ids: revokedTaskIds,
-                lease_expire_at: leaseExpireAt,
-                max_slots: maxSlots
-            })
-        } else if (revokedTaskIds.length > 0) {
-            this.logInfo('heartbeat', '客户端上报任务已被服务端收回', { clientId }, {
-                revoked_task_ids: revokedTaskIds
-            })
-        }
-        await this.releaseOrphanedClientTasks(clientId, activeRunningTasks, now)
-        const currentSlots = await this.syncClientSlots(clientId, now)
-        return { client_id: clientId, current_slots: currentSlots, max_slots: maxSlots, revoked_task_ids: revokedTaskIds }
-    }
-
-    async releaseOrphanedClientTasks(clientId, runningTasks, now = Date.now()) {
-        const assigned = await db.query(
-            'SELECT id FROM qk_task WHERE assigned_client_id = ? AND status IN (?, ?)',
-            [clientId, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-        )
-        const runningSet = new Set((runningTasks || []).map(Number).filter(Boolean))
-        let released = 0
-        for (const row of assigned || []) {
-            if (runningSet.has(row.id)) continue
-            const result = await db.query(
-                'UPDATE qk_task SET status = ?, assigned_client_id = NULL, lease_expire_at = NULL, assigned_at = NULL, update_time = ? WHERE id = ? AND assigned_client_id = ? AND status IN (?, ?)',
-                [TASK_STATUS.PENDING, now, row.id, clientId, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-            )
-            if (result && result.affectedRows > 0) {
-                released += 1
-                await this.logTask(row.id, clientId, 'released', '客户端未继续执行,任务已释放回队列')
-                this.logInfo('releaseOrphaned', '释放未在运行的已分配任务', { taskId: row.id, clientId })
-            }
-        }
-        if (released > 0) {
-            await this.syncClientSlots(clientId, now)
-        }
-        return released
-    }
-
-    async reclaimTasks(clientId, clientSecret) {
-        const client = await this.authenticateClient(clientId, clientSecret)
-        if (!client) {
-            throw new Error('客户端凭证无效')
-        }
-        const now = Date.now()
-        const leaseExpireAt = now + this.leaseMs
-        const rows = await db.query(
-            `SELECT t.*, b.jx0502zbid AS batch_jx0502zbid, b.name AS batch_name, b.enabled AS batch_enabled
-            FROM qk_task t
-            LEFT JOIN qk_batch b ON b.id = t.batch_id
-            WHERE t.assigned_client_id = ? AND t.status IN (?, ?)
-            ORDER BY t.create_time ASC`,
-            [clientId, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-        )
-        if (!rows || rows.length === 0) {
-            return []
-        }
-        for (const row of rows) {
-            await db.query(
-                'UPDATE qk_task SET status = ?, lease_expire_at = ?, update_time = ? WHERE id = ? AND assigned_client_id = ? AND status IN (?, ?)',
-                [TASK_STATUS.RUNNING, leaseExpireAt, now, row.id, clientId, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-            )
-            await this.logTask(row.id, clientId, 'reclaimed', '客户端重连后回收任务继续执行')
-        }
-        this.logInfo('reclaimTasks', `客户端回收 ${rows.length} 个进行中任务`, { clientId }, {
-            task_ids: rows.map(row => row.id),
-            lease_expire_at: leaseExpireAt
-        })
-        return rows.map(row => this.serializeTask({
-            ...row,
-            assigned_client_id: clientId,
-            lease_expire_at: leaseExpireAt,
-            status: TASK_STATUS.RUNNING
-        }, true))
-    }
-
-    async releaseTasks(clientId, clientSecret, payload = {}) {
-        const client = await this.authenticateClient(clientId, clientSecret)
-        if (!client) {
-            throw new Error('客户端凭证无效')
-        }
-        const now = Date.now()
-        const taskIds = Array.isArray(payload.task_ids)
-            ? payload.task_ids.map(Number).filter(Boolean)
-            : []
-        let rows = []
-        if (taskIds.length > 0) {
-            const placeholders = taskIds.map(() => '?').join(',')
-            rows = await db.query(
-                `SELECT id FROM qk_task WHERE assigned_client_id = ? AND id IN (${placeholders}) AND status IN (?, ?)`,
-                [clientId, ...taskIds, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-            )
-        } else {
-            rows = await db.query(
-                'SELECT id FROM qk_task WHERE assigned_client_id = ? AND status IN (?, ?)',
-                [clientId, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-            )
-        }
-        let released = 0
-        for (const row of rows || []) {
-            const result = await db.query(
-                'UPDATE qk_task SET status = ?, assigned_client_id = NULL, lease_expire_at = NULL, assigned_at = NULL, update_time = ? WHERE id = ? AND assigned_client_id = ? AND status IN (?, ?)',
-                [TASK_STATUS.PENDING, now, row.id, clientId, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-            )
-            if (result && result.affectedRows > 0) {
-                released += 1
-                await this.logTask(row.id, clientId, 'released', '客户端主动释放任务')
-            }
-        }
-        if (released > 0) {
-            await this.syncClientSlots(clientId, now)
-            this.logInfo('releaseTasks', `客户端释放 ${released} 个任务`, { clientId }, {
-                task_ids: (rows || []).map(row => row.id)
-            })
-        }
-        return { released, task_ids: (rows || []).map(row => row.id) }
-    }
-
-    async pullTasks(clientId, clientSecret, count) {
-        const client = await this.authenticateClient(clientId, clientSecret)
-        if (!client) {
-            throw new Error('客户端凭证无效')
-        }
-        const availableSlots = await this.getClientAvailableSlotsAsync(client)
-        const safeCount = Math.max(0, Math.min(50, Number(count || 0), availableSlots))
-        if (safeCount <= 0) {
-            this.logWarn('pullTasks', '客户端无可用槽位或拉取数量无效,已忽略', { clientId }, {
-                count,
-                available_slots: availableSlots,
-                max_slots: this.resolveClientMaxSlots(client),
-                current_slots: client.current_slots,
-                free_mem_mb: client.free_mem_mb
-            })
-            return []
-        }
-        const lockKey = `qk:pull:lock:${clientId}`
-        const locked = await Redis.set(lockKey, '1', { NX: true, EX: this.pullLockTtlSeconds })
-        if (!locked) {
-            this.logWarn('pullTasks', '拉取任务被并发锁拦截,本次跳过', { clientId }, { count: safeCount })
-            return []
-        }
-        const conn = await db.connect()
-        try {
-            await conn.beginTransaction()
-            const inFlightStatuses = this.getInFlightTaskStatuses()
-            const candidateLimit = Math.min(50, Math.max(safeCount, safeCount * 5))
-            const [candidateRows] = await conn.execute(
-                `SELECT t.*, b.jx0502zbid AS batch_jx0502zbid, b.name AS batch_name, b.enabled AS batch_enabled
-                FROM qk_task t
-                LEFT JOIN qk_batch b ON b.id = t.batch_id
-                WHERE t.status = ?
-                AND (t.batch_id IS NULL OR b.enabled = 1)
-                AND (t.exclude_client_id IS NULL OR t.exclude_client_id <> ?)
-                AND NOT EXISTS (
-                    SELECT 1 FROM qk_task active
-                    WHERE active.student_num = t.student_num
-                    AND active.status IN (?, ?)
-                )
-                ORDER BY t.create_time ASC LIMIT ${candidateLimit} FOR UPDATE`,
-                [TASK_STATUS.PENDING, clientId, ...inFlightStatuses]
-            )
-            const rows = this.pickPullableTasks(candidateRows, safeCount)
-            const now = Date.now()
-            const leaseExpireAt = now + this.leaseMs
-            for (const row of rows) {
-                await conn.execute(
-                    'UPDATE qk_task SET status = ?, assigned_client_id = ?, lease_expire_at = ?, assigned_at = ?, exclude_client_id = NULL, update_time = ? WHERE id = ?',
-                    [TASK_STATUS.ASSIGNED, clientId, leaseExpireAt, now, now, row.id]
-                )
-            }
-            await conn.commit()
-            if (rows.length > 0) {
-                await this.syncClientSlots(clientId, now)
-            }
-            for (const row of rows) {
-                await this.logTask(row.id, clientId, 'assigned', '任务已分配给客户端')
-            }
-            if (rows.length > 0) {
-                this.logInfo('pullTasks', `已分配 ${rows.length} 个抢课任务`, { clientId }, {
-                    task_ids: rows.map(row => row.id),
-                    lease_expire_at: leaseExpireAt,
-                    requested_count: safeCount
-                })
-            }
-            return rows.map(row => this.serializeTask({ ...row, assigned_client_id: clientId, lease_expire_at: leaseExpireAt, status: TASK_STATUS.ASSIGNED }, true))
-        } catch (err) {
-            await conn.rollback()
-            this.logError('pullTasks', '拉取并分配任务失败,事务已回滚', { clientId }, err)
-            throw err
-        } finally {
-            await Redis.del(lockKey)
-        }
-    }
-
-    extractFailureMessage(payload = {}) {
-        const result = payload.result && typeof payload.result === 'object' ? payload.result : {}
-        const parts = [
-            payload.message,
-            payload.error_msg,
-            payload.error,
-            result.message,
-            result.error,
-            result.error_msg,
-            typeof payload.result === 'string' ? payload.result : null
-        ]
-        return parts.filter(Boolean).map(item => String(item)).join(' ').trim()
-    }
-
-    resolveFailureType(payload = {}) {
-        const result = payload.result && typeof payload.result === 'object' ? payload.result : {}
-        return String(payload.type || payload.event || result.type || result.event || '').trim()
-    }
-
-    shouldRequeueTaskOnFailure(task, payload = {}) {
-        if (payload.success === true) {
-            return false
-        }
-        if (payload.requeue === true || payload.requeue === 1 || payload.requeue === '1') {
-            return true
-        }
-
-        const message = this.extractFailureMessage(payload)
-        const failureType = this.resolveFailureType(payload)
-        const terminalTypes = new Set(['already', 'dajia', 'not_open', 'not_in_time'])
-        if (terminalTypes.has(failureType)) {
-            return false
-        }
-
-        const terminalPatterns = [
-            '已选择',
-            '冲突',
-            '超过',
-            '选课不开放',
-            '不在选课时间',
-            '用户名或密码',
-            '未匹配到目标课程',
-            '验证码验证失败次数',
-            '为避免账号被锁定'
-        ]
-        if (terminalPatterns.some(pattern => message.includes(pattern))) {
-            return false
-        }
-
-        // 旧版客户端 task_exit 上报:抢课任务异常结束,code=1
-        if (/抢课任务异常结束/.test(message) || /\bcode=1\b/.test(message)) {
-            return true
-        }
-        if (/抢课循环结束但未获得成功结果/.test(message)) {
-            return true
-        }
-
-        // 客户端被收回槽位、窗口关闭等导致的被动停止,应重新分配而非标为失败
-        const exitReason = String(payload.exit_reason || payload.reason || '').trim()
-        if (failureType === 'stopped' || exitReason === 'stopped' || message.includes('任务已停止')) {
-            return true
-        }
-
-        const autoRelogin = Number(task?.auto_relogin) === 1
-        if (autoRelogin) {
-            // 新版可传 type=login_expired;旧版仅 message / result.message
-            if (failureType === 'login_expired') {
-                return true
-            }
-            const loginPatterns = [
-                '别处登录',
-                '在其他地方登录',
-                '账号在别处',
-                '登录失效',
-                '登录已失效',
-                '请重新登录',
-                '按登录失效处理',
-                '空响应',
-                '未登录',
-                '登录失败',
-                '会话过期',
-                '会话失效'
-            ]
-            if (loginPatterns.some(pattern => message.includes(pattern))) {
-                return true
-            }
-        }
-
-        return false
-    }
-
-    async requeueTaskAfterClientFailure(clientId, taskId, payload = {}) {
-        const now = Date.now()
-        const message = this.buildReportMessage(payload) || '任务异常结束,已重新进入待分配队列'
-        const result = await db.query(
-            `UPDATE qk_task SET status = ?, assigned_client_id = NULL, lease_expire_at = NULL,
-            assigned_at = NULL, result_json = NULL, error_msg = ?, finished_time = NULL, update_time = ?
-            WHERE id = ? AND assigned_client_id = ? AND status IN (?, ?)`,
-            [TASK_STATUS.PENDING, message, now, taskId, clientId, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-        )
-        if (!result || result.affectedRows <= 0) {
-            throw new Error('任务不存在或不属于当前客户端')
-        }
-        await this.syncClientSlots(clientId, now)
-        await this.logTask(taskId, clientId, 'requeued', message, payload.result || payload)
-        this.logInfo('reportResult', '抢课任务异常结束,已重新入队', { taskId, clientId }, {
-            message,
-            requeued: true,
-            exit_reason: payload.exit_reason || payload.reason || null
-        })
-        return { task_id: taskId, status: TASK_STATUS.PENDING, requeued: true }
-    }
-
-    async reportResult(clientId, clientSecret, payload = {}) {
-        await this.ensureSchema()
-        const client = await this.authenticateClient(clientId, clientSecret)
-        if (!client) {
-            throw new Error('客户端凭证无效')
-        }
-        const taskId = Number(payload.task_id || payload.id)
-        const success = payload.success === true || payload.status === TASK_STATUS.SUCCESS
-        const taskRows = await db.query(
-            'SELECT id, auto_relogin FROM qk_task WHERE id = ? AND assigned_client_id = ? AND status IN (?, ?)',
-            [taskId, clientId, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-        )
-        if (!taskRows || taskRows.length === 0) {
-            this.logWarn('reportResult', '任务结果上报被拒绝:任务不存在或不属于当前客户端', { taskId, clientId }, {
-                success,
-                status: success ? TASK_STATUS.SUCCESS : TASK_STATUS.FAILED
-            })
-            throw new Error('任务不存在或不属于当前客户端')
-        }
-        const task = taskRows[0]
-
-        if (!success && this.shouldRequeueTaskOnFailure(task, payload)) {
-            return this.requeueTaskAfterClientFailure(clientId, taskId, payload)
-        }
-
-        const status = success ? TASK_STATUS.SUCCESS : TASK_STATUS.FAILED
-        const now = Date.now()
-        const resultJson = payload.result ? JSON.stringify(payload.result) : JSON.stringify({
-            course: payload.course || '',
-            message: payload.message || ''
-        })
-        const result = await db.query(
-            'UPDATE qk_task SET status = ?, result_json = ?, error_msg = ?, update_time = ?, finished_time = ?, lease_expire_at = NULL WHERE id = ? AND assigned_client_id = ? AND status IN (?, ?)',
-            [status, resultJson, success ? null : (payload.error_msg || payload.message || '抢课失败'), now, now, taskId, clientId, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-        )
-        if (!result || result.affectedRows <= 0) {
-            throw new Error('任务不存在或不属于当前客户端')
-        }
-        await this.syncClientSlots(clientId, now)
-        await db.query(
-            `UPDATE qk_client SET total_completed = total_completed + 1, total_success = total_success + ?, update_time = ? WHERE client_id = ?`,
-            [success ? 1 : 0, now, clientId]
-        )
-        await this.logTask(taskId, clientId, success ? 'grab_success' : 'grab_fail', payload.message || payload.error_msg || '', payload.result || payload)
-        this.logInfo('reportResult', success ? '抢课成功' : '抢课失败', { taskId, clientId }, {
-            success,
-            message: payload.message || payload.error_msg || '',
-            course: payload.course || payload.result?.course || '',
-            result: payload.result || null
-        })
-        return { task_id: taskId, status }
-    }
-
-    async listClients() {
-        const rows = await db.query('SELECT id, client_id, label, enabled, max_slots, current_slots, hostname, os_username, cpu_model, cpu_threads, total_mem_mb, free_mem_mb, platform, last_heartbeat_at, online, total_completed, total_success, create_time, update_time FROM qk_client ORDER BY update_time DESC')
-        return rows || []
-    }
-
-    async deleteClient(clientId) {
-        const result = await db.query('UPDATE qk_client SET enabled = 0, online = 0, update_time = ? WHERE client_id = ?', [Date.now(), clientId])
-        if (!result || result.affectedRows <= 0) {
-            throw new Error('客户端不存在')
-        }
-        this.logInfo('deleteClient', '抢课客户端已禁用', { clientId })
-    }
-
-    async enableClient(clientId) {
-        const rows = await db.query('SELECT client_id, enabled FROM qk_client WHERE client_id = ?', [clientId])
-        if (!rows || rows.length === 0) {
-            throw new Error('客户端不存在')
-        }
-        if (Number(rows[0].enabled) === 1) {
-            throw new Error('客户端已处于启用状态')
-        }
-        const now = Date.now()
-        await db.query('UPDATE qk_client SET enabled = 1, update_time = ? WHERE client_id = ?', [now, clientId])
-        this.logInfo('enableClient', '抢课客户端已解禁', { clientId })
-    }
-
-    async listAdminTasks(filters = {}) {
-        const pagesize = Math.max(1, Math.min(100, Number(filters.pagesize || 20)))
-        const current = Math.max(1, Number(filters.current || 1))
-        const where = ['1 = 1']
-        const params = []
-        const countParams = []
-        if (filters.status) {
-            where.push('t.status = ?')
-            params.push(filters.status)
-            countParams.push(filters.status)
-        }
-        if (filters.client_id) {
-            where.push('t.assigned_client_id = ?')
-            params.push(filters.client_id)
-            countParams.push(filters.client_id)
-        }
-        if (filters.student_num) {
-            where.push('t.student_num LIKE ?')
-            params.push(`%${filters.student_num}%`)
-            countParams.push(`%${filters.student_num}%`)
-        }
-        if (filters.username) {
-            where.push('u.username COLLATE utf8mb4_general_ci LIKE (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(`%${filters.username}%`)
-            countParams.push(`%${filters.username}%`)
-        }
-        const courseName = String(filters.course_name || filters.course || '').trim()
-        if (courseName) {
-            const keyword = `%${courseName}%`
-            where.push('(t.courses LIKE ? OR t.course_groups LIKE ?)')
-            params.push(keyword, keyword)
-            countParams.push(keyword, keyword)
-        }
-        if (filters.batch_id) {
-            where.push('t.batch_id = ?')
-            params.push(Number(filters.batch_id))
-            countParams.push(Number(filters.batch_id))
-        }
-        const whereSql = where.join(' AND ')
-        const offset = (current - 1) * pagesize
-        const countRows = await db.query(
-            `SELECT COUNT(*) AS total FROM qk_task t
-            LEFT JOIN users u ON u.uuid COLLATE utf8mb4_general_ci = t.create_user COLLATE utf8mb4_general_ci
-            WHERE ${whereSql}`,
-            countParams
-        )
-        const rows = await db.query(
-            `SELECT t.*, u.username, u.avatar${this.taskBatchSelectSql('t')}
-            FROM qk_task t
-            LEFT JOIN users u ON u.uuid COLLATE utf8mb4_general_ci = t.create_user COLLATE utf8mb4_general_ci
-            ${this.taskBatchJoinSql('t')}
-            WHERE ${whereSql}
-            ORDER BY t.create_time DESC
-            LIMIT ${pagesize} OFFSET ${offset}`,
-            params
-        )
-        return {
-            list: (rows || []).map(row => this.serializeTask(row, true)),
-            total: countRows?.[0]?.total || 0,
-            current,
-            pagesize
-        }
-    }
-
-    async listAdminReports(filters = {}) {
-        const pagesize = Math.max(1, Math.min(100, Number(filters.pagesize || 20)))
-        const current = Math.max(1, Number(filters.current || 1))
-        const where = ['l.event IN (?, ?, ?, ?)']
-        const params = ['request_result', 'progress_snapshot', 'grab_success', 'grab_fail']
-        const countParams = ['request_result', 'progress_snapshot', 'grab_success', 'grab_fail']
-        if (filters.task_id) {
-            where.push('l.task_id = ?')
-            params.push(Number(filters.task_id))
-            countParams.push(Number(filters.task_id))
-        }
-        if (filters.client_id) {
-            where.push('l.client_id = ?')
-            params.push(String(filters.client_id))
-            countParams.push(String(filters.client_id))
-        }
-        if (filters.event) {
-            where.push('l.event = ?')
-            params.push(String(filters.event))
-            countParams.push(String(filters.event))
-        }
-        if (filters.start_time) {
-            where.push('l.create_time >= ?')
-            params.push(Number(filters.start_time))
-            countParams.push(Number(filters.start_time))
-        }
-        if (filters.end_time) {
-            where.push('l.create_time <= ?')
-            params.push(Number(filters.end_time))
-            countParams.push(Number(filters.end_time))
-        }
-        if (filters.student_num) {
-            where.push('t.student_num LIKE ?')
-            params.push(`%${filters.student_num}%`)
-            countParams.push(`%${filters.student_num}%`)
-        }
-        if (filters.name || filters.task_name) {
-            where.push('t.name LIKE ?')
-            params.push(`%${filters.name || filters.task_name}%`)
-            countParams.push(`%${filters.name || filters.task_name}%`)
-        }
-        if (filters.username) {
-            where.push('u.username COLLATE utf8mb4_general_ci LIKE (CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci)')
-            params.push(`%${filters.username}%`)
-            countParams.push(`%${filters.username}%`)
-        }
-        if (filters.client_label) {
-            where.push('c.label LIKE ?')
-            params.push(`%${filters.client_label}%`)
-            countParams.push(`%${filters.client_label}%`)
-        }
-        const whereSql = where.join(' AND ')
-        const offset = (current - 1) * pagesize
-        const joinSql = `
-            FROM qk_task_log l
-            LEFT JOIN qk_task t ON t.id = l.task_id
-            LEFT JOIN qk_client c ON c.client_id = l.client_id
-            LEFT JOIN users u ON u.uuid COLLATE utf8mb4_general_ci = t.create_user COLLATE utf8mb4_general_ci`
-        const countRows = await db.query(
-            `SELECT COUNT(*) AS total ${joinSql} WHERE ${whereSql}`,
-            countParams
-        )
-        const rows = await db.query(
-            `SELECT l.id, l.task_id, l.client_id, l.event, l.message, l.payload_json, l.create_time,
-            t.name AS task_name, t.student_num, t.status AS task_status,
-            c.label AS client_label, u.username, u.avatar
-            ${joinSql}
-            WHERE ${whereSql}
-            ORDER BY l.create_time DESC
-            LIMIT ${pagesize} OFFSET ${offset}`,
-            params
-        )
-        return {
-            list: (rows || []).map(row => this.serializeReportLog(row)),
-            total: countRows?.[0]?.total || 0,
-            current,
-            pagesize
-        }
-    }
-
-    async reassignStaleRunningTasks() {
-        const onlineCount = await this.countOnlineClients()
-        if (onlineCount <= 1) {
-            return 0
-        }
-
-        const now = Date.now()
-        const staleBefore = now - this.staleTaskMs
-        const rows = await db.query(
-            `SELECT id, assigned_client_id FROM qk_task
-            WHERE status IN (?, ?)
-            AND assigned_at IS NOT NULL
-            AND assigned_at < ?`,
-            [TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING, staleBefore]
-        )
-
-        let count = 0
-        const affectedClients = new Set()
-        for (const row of rows || []) {
-            const result = await db.query(
-                `UPDATE qk_task
-                SET status = ?, assigned_client_id = NULL, lease_expire_at = NULL,
-                    assigned_at = NULL, exclude_client_id = ?, update_time = ?
-                WHERE id = ? AND status IN (?, ?)`,
-                [TASK_STATUS.PENDING, row.assigned_client_id, now, row.id, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-            )
-            if (result && result.affectedRows > 0) {
-                count += 1
-                if (row.assigned_client_id) {
-                    affectedClients.add(row.assigned_client_id)
-                }
-                await this.logTask(row.id, row.assigned_client_id, 'reassigned', '任务超过1小时未成功,已收回并等待分配给其他客户端')
-                this.logWarn('reassignStale', '长时间未成功任务已收回', {
-                    taskId: row.id,
-                    clientId: row.assigned_client_id
-                }, { online_clients: onlineCount })
-            }
-        }
-        for (const clientId of affectedClients) {
-            await this.syncClientSlots(clientId, now)
-        }
-        return count
-    }
-
-    async requeueExpiredTasks() {
-        const now = Date.now()
-        const rows = await db.query(
-            'SELECT id, assigned_client_id FROM qk_task WHERE status IN (?, ?) AND lease_expire_at IS NOT NULL AND lease_expire_at < ?',
-            [TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING, now]
-        )
-        let count = 0
-        const affectedClients = new Set()
-        for (const row of rows || []) {
-            const result = await db.query(
-                'UPDATE qk_task SET status = ?, assigned_client_id = NULL, lease_expire_at = NULL, assigned_at = NULL, update_time = ? WHERE id = ? AND status IN (?, ?)',
-                [TASK_STATUS.PENDING, now, row.id, TASK_STATUS.ASSIGNED, TASK_STATUS.RUNNING]
-            )
-            if (result && result.affectedRows > 0) {
-                count++
-                if (row.assigned_client_id) {
-                    affectedClients.add(row.assigned_client_id)
-                }
-                await this.logTask(row.id, row.assigned_client_id, 'reassigned', '租约过期,任务已重新进入待分配队列')
-                this.logWarn('requeueExpired', '租约过期,任务已重新入队', {
-                    taskId: row.id,
-                    clientId: row.assigned_client_id
-                })
-            }
-        }
-        for (const clientId of affectedClients) {
-            await this.syncClientSlots(clientId, now)
-        }
-        const offlineResult = await db.query(
-            'UPDATE qk_client SET online = 0, current_slots = 0, update_time = ? WHERE online = 1 AND (last_heartbeat_at IS NULL OR last_heartbeat_at < ?)',
-            [now, now - this.heartbeatTtlSeconds * 1000]
-        )
-        const offlineCount = offlineResult?.affectedRows || 0
-        const staleCount = await this.reassignStaleRunningTasks()
-        if (count > 0 || offlineCount > 0 || staleCount > 0) {
-            this.logInfo('requeueExpired', '租约巡检完成', {}, {
-                expired_tasks: (rows || []).length,
-                requeued: count,
-                stale_reassigned: staleCount,
-                clients_marked_offline: offlineCount
-            })
-        }
-        return count + staleCount
-    }
-}
-
-module.exports = {
-    TaskScheduler,
-    TASK_STATUS
-}

+ 48 - 116
lib/Server.js

@@ -7,42 +7,34 @@ const Logger = require('./Logger')
 const MySQL = require('../plugin/DataBase/MySQL')
 const Worker = require('./Lepao/Worker')
 const mq = require('../plugin/mq')
-const { mq: mqName } = require('../plugin/mq/mqPrefix')
+const { HEALTH_CHECK_QUEUE } = require('../plugin/mq/jkesMqNames')
 const { startLepaoSchedulePublisher } = require('../plugin/mq/lepaoSchedulePublisher')
-const OneBotV11 = require('../plugin/OneBot/OneBotV11')
-const AccessControl = require('./AccessControl')
-const LeaseWatcher = require('./QK/LeaseWatcher')
-const { TaskScheduler } = require('./QK/TaskScheduler')
-const {
-    resolveServerRole,
-    shouldServeApi,
-    shouldRunLepaoWorker,
-    shouldRunOrderPaymentWorker
-} = require('./serverRole')
-const { TASK_QUEUE } = require('../plugin/mq/runforgeTaskMq')
-const { PREFIX } = require('../plugin/mq/mqPrefix')
 
 class SERVER {
     constructor() {
-        this.serverRole = resolveServerRole()
+        this.app = express()
         this.port = config.port || 3000
-        this.apiDirectory = path.join(__dirname, '../apis')
+        this.apiDirectory = path.join(__dirname, '../apis') // API 文件存放目录
 
-        const logName = this.serverRole === 'worker' ? 'WorkerServer.log' : 'Server.log'
-        this.logger = new Logger(path.join(__dirname, '../logs', logName), 'INFO')
+        this.logger = new Logger(path.join(__dirname, '../logs/Server.log'), 'INFO')
 
+        // 解析 JSON 请求体
+        this.app.use(express.json())
+
+        //解决cors跨域
+        this.app.use(cors())
+
+        //使用静态资源
+        this.app.use('/uploads', express.static('./uploads'))
+
+        // 初始化数据库连接
         this.db = new MySQL()
 
-        if (shouldServeApi(this.serverRole)) {
-            this.app = express()
-            this.app.use(express.json())
-            this.app.use(cors())
-            this.app.use('/uploads', express.static('./uploads'))
-            this.app.use('/models', express.static('./models'))
-            this.loadAPIs(this.apiDirectory)
-        }
+        // 加载 API 路由
+        this.loadAPIs(this.apiDirectory)
     }
 
+    // 测试数据库连接
     async initDB() {
         try {
             this.logger.info('正在测试数据库连接')
@@ -54,43 +46,29 @@ class SERVER {
         }
     }
 
-    async startLepaoWorker() {
-        const worker = new Worker()
-        await worker.start()
-        this.logger.info('哪吒乐跑 Worker 已启动,正在监听 MQ 任务...')
-        startLepaoSchedulePublisher({
-            logger: this.logger,
-            intervalMs: config.rabbitmq?.lepaoScheduleTickMs ?? 2000,
-            batch: config.rabbitmq?.lepaoScheduleBatch ?? 100
-        })
-    }
-
+    // 测试MQ连接
     async initMQ() {
         try {
             await mq.init()
             const ch = await mq.getChannel('health')
 
-            await ch.assertQueue(mqName('mq_health_check'), { durable: false })
+            await ch.assertQueue(HEALTH_CHECK_QUEUE, { durable: false })
             this.logger.info('✅ RabbitMQ 初始化 & 测试成功')
 
-            if (shouldRunLepaoWorker(this.serverRole)) {
-                try {
-                    await this.startLepaoWorker()
-                } catch (err) {
-                    console.error('哪吒乐跑 Worker 启动失败:', err)
-                    process.exit(1)
-                }
-            } else {
-                this.logger.info(
-                    `serverRole=api,跳过乐跑 Worker;乐跑任务将投递到 MQ 队列「${TASK_QUEUE}」(mqPrefix=${JSON.stringify(PREFIX)})`
-                )
-            }
-
-            if (shouldRunOrderPaymentWorker(this.serverRole)) {
+            const worker = new Worker()
+            try {
+                await worker.start()
+                this.logger.info('沐晨乐跑 Worker 已启动,正在监听 MQ 任务...')
+                startLepaoSchedulePublisher({
+                    logger: this.logger,
+                    intervalMs: config.rabbitmq?.lepaoScheduleTickMs ?? 2000,
+                    batch: config.rabbitmq?.lepaoScheduleBatch ?? 100
+                })
                 const { startOrderPaymentWorker } = require('../plugin/mq/orderPaymentWorker')
                 await startOrderPaymentWorker(this.logger)
-            } else if (shouldServeApi(this.serverRole)) {
-                this.logger.info('serverRole=api,跳过订单支付 MQ 消费者(由 all 进程消费)')
+            } catch (err) {
+                console.error('沐晨乐跑 Worker 启动失败:', err)
+                process.exit(1)
             }
         } catch (e) {
             this.logger.error('❌ RabbitMQ 初始化失败')
@@ -98,23 +76,6 @@ class SERVER {
         }
     }
 
-    async initOneBot() {
-        try {
-            const ok = await OneBotV11.initOneBotWs()
-            if (ok) {
-                this.logger.info('OneBot v11 ws 初始化成功,已开始监听消息')
-            } else {
-                this.logger.info('OneBot v11 ws 未初始化(可能未启用)')
-            }
-        } catch (err) {
-            this.logger.error(`OneBot v11 ws 初始化失败: ${err.message}`)
-        }
-    }
-
-    async initAccessControlSchema() {
-        await AccessControl.ensurePermissionSchema()
-    }
-
     loadAPIs(directory) {
         const items = fs.readdirSync(directory)
 
@@ -123,13 +84,16 @@ class SERVER {
             const stats = fs.statSync(itemPath)
 
             if (stats.isDirectory()) {
+                // 如果是目录,递归调用
                 this.loadAPIs(itemPath)
             } else if (stats.isFile() && itemPath.endsWith('.js')) {
+                // 如果是文件且是 JavaScript 文件
                 this.loadAPIFile(itemPath)
             }
         })
     }
 
+    // 加载单个 API 文件
     loadAPIFile(filePath) {
         try {
             const APIClass = require(filePath)
@@ -147,54 +111,22 @@ class SERVER {
         }
     }
 
-    listenHttp() {
-        this.app.listen(this.port, () => {
-            this.logger.info(`==========服务器正在 ${this.port} 端口上运行 (role=${this.serverRole})==========`)
-        })
-    }
-
-    async startApiServices() {
-        try {
-            await this.initAccessControlSchema()
-        } catch (err) {
-            this.logger.error(`权限模型初始化异常: ${err.message}`)
-        }
-
-        try {
-            await this.initOneBot()
-        } catch (err) {
-            this.logger.error(`OneBot 初始化异常: ${err.message}`)
-        }
-
-        this.qkLeaseWatcher = new LeaseWatcher({
-            logger: this.logger,
-            intervalMs: config.qk?.leaseWatcherIntervalMs || 30 * 1000,
-            scheduler: new TaskScheduler({ logger: this.logger })
-        })
-        this.qkLeaseWatcher.start()
-
-        this.listenHttp()
-    }
-
     start() {
-        this.logger.info(
-            `============正在启动 (role=${this.serverRole}, lepaoQueue=${TASK_QUEUE}, mqPrefix=${JSON.stringify(PREFIX)})============`
-        )
-
-        this.initDB()
-            .then(() => this.initMQ())
-            .then(async () => {
-                if (!shouldServeApi(this.serverRole)) {
-                    this.logger.info('serverRole=worker,未启动 HTTP API,进程将保持运行以消费乐跑任务')
-                    return
-                }
-                await this.startApiServices()
-            })
-            .catch((err) => {
-                this.logger.error(`启动失败: ${err.message || err}`)
-                if (err.stack) this.logger.error(err.stack)
-                process.exit(1)
+        this.logger.info('============正在启动服务器============')
+
+        // 初始化数据库连接
+        this.initDB().then(() => {
+            this.initMQ().then(() => {
+                this.app.listen(this.port, () => {
+                    this.logger.info(`==========服务器正在 ${this.port} 端口上运行==========`)
+                })
+            }).catch(err => {
+                this.logger.error(`启动服务器失败: ${err.message}`)
             })
+        }).catch(err => {
+            this.logger.error(`启动服务器失败: ${err.message}`)
+            process.exit(1) // 启动失败时退出进程
+        })
     }
 }
 

+ 0 - 40
lib/UniLoginClient.js

@@ -1,40 +0,0 @@
-const axios = require('axios')
-const https = require('https')
-const config = require('../config.json')
-
-const VALID_SOCIAL_TYPES = ['qq', 'wx']
-
-function normalizeSocialType(type) {
-    const socialType = type || 'qq'
-    return VALID_SOCIAL_TYPES.includes(socialType) ? socialType : null
-}
-
-async function fetchUniLoginProfile(type, code) {
-    const socialType = normalizeSocialType(type)
-    if (!socialType)
-        throw new Error('不支持的第三方登录类型')
-
-    const uniConfig = config.unilogin
-    const url = `${uniConfig.url}/connect.php?act=callback&appid=${uniConfig.appid}&appkey=${uniConfig.appkey}&type=${socialType}&code=${code}`
-
-    const r = await axios.get(url, {
-        httpsAgent: new https.Agent({
-            rejectUnauthorized: false
-        }),
-        proxy: false
-    })
-
-    if (!r || r.data?.code !== 0)
-        throw new Error(r.data?.msg || 'api接口错误')
-
-    return {
-        ...r.data,
-        social_type: socialType
-    }
-}
-
-module.exports = {
-    VALID_SOCIAL_TYPES,
-    normalizeSocialType,
-    fetchUniLoginProfile
-}

+ 0 - 100
lib/UserSocialBinding.js

@@ -1,100 +0,0 @@
-const db = require('../plugin/DataBase/db')
-const { normalizeSocialType } = require('./UniLoginClient')
-
-async function getBindingByIdentity(socialType, socialUid) {
-    const type = normalizeSocialType(socialType)
-    if (!type || !socialUid)
-        return null
-
-    const rows = await db.query(
-        'SELECT user_uuid, social_type, social_uid, social_nickname, social_avatar FROM user_social_bindings WHERE social_type = ? AND social_uid = ? LIMIT 1',
-        [type, socialUid]
-    )
-
-    return rows?.[0] || null
-}
-
-async function getLegacyUserByIdentity(socialType, socialUid) {
-    const type = normalizeSocialType(socialType)
-    if (!type || !socialUid)
-        return null
-
-    try {
-        const rows = await db.query(
-            'SELECT uuid, username, permission, avatar, nickname FROM users WHERE social_uid = ? AND social_type = ? LIMIT 1',
-            [socialUid, type]
-        )
-        return rows?.[0] || null
-    } catch (error) {
-        if (error?.code === 'ER_BAD_FIELD_ERROR')
-            return null
-        throw error
-    }
-}
-
-async function getUserSocialBindings(userUuid) {
-    const rows = await db.query(
-        'SELECT social_type, social_uid, social_nickname, social_avatar FROM user_social_bindings WHERE user_uuid = ? ORDER BY FIELD(social_type, "qq", "wx")',
-        [userUuid]
-    )
-
-    return rows || []
-}
-
-async function insertSocialBinding(userUuid, socialType, socialUid, socialNickname = null, socialAvatar = null) {
-    const type = normalizeSocialType(socialType)
-    if (!type || !socialUid)
-        return null
-
-    const now = new Date().getTime()
-    return await db.query(
-        'INSERT IGNORE INTO user_social_bindings (user_uuid, social_type, social_uid, social_nickname, social_avatar, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
-        [userUuid, type, socialUid, socialNickname, socialAvatar, now, now]
-    )
-}
-
-async function updateSocialBindingProfile(socialType, socialUid, socialNickname = null, socialAvatar = null) {
-    const type = normalizeSocialType(socialType)
-    if (!type || !socialUid)
-        return null
-
-    const now = new Date().getTime()
-    return await db.query(
-        'UPDATE user_social_bindings SET social_nickname = ?, social_avatar = ?, updated_at = ? WHERE social_type = ? AND social_uid = ?',
-        [socialNickname, socialAvatar, now, type, socialUid]
-    )
-}
-
-async function removeSocialBinding(userUuid, socialType) {
-    const type = normalizeSocialType(socialType)
-    if (!type)
-        return null
-
-    return await db.query(
-        'DELETE FROM user_social_bindings WHERE user_uuid = ? AND social_type = ?',
-        [userUuid, type]
-    )
-}
-
-function toSocialBindingSummary(bindings = []) {
-    return ['qq', 'wx'].map(type => {
-        const binding = bindings.find(item => item.social_type === type)
-        return {
-            type,
-            bound: !!binding,
-            social_uid: binding?.social_uid || '',
-            nickname: binding?.social_nickname || '',
-            avatar: binding?.social_avatar || ''
-        }
-    })
-}
-
-module.exports = {
-    getBindingByIdentity,
-    getLegacyUserByIdentity,
-    getUserSocialBindings,
-    insertSocialBinding,
-    updateSocialBindingProfile,
-    removeSocialBinding,
-    toSocialBindingSummary
-}

+ 0 - 35
lib/serverRole.js

@@ -1,35 +0,0 @@
-const config = require('../config.json')
-
-const VALID_ROLES = new Set(['all', 'api', 'worker'])
-
-/**
- * 进程角色:all=API+乐跑Worker一体;api=仅HTTP入队;worker=仅消费乐跑任务
- * 环境变量 RUNFORGE_SERVER_ROLE 优先于 config.serverRole
- */
-function resolveServerRole() {
-    const raw = process.env.RUNFORGE_SERVER_ROLE || config.serverRole || 'all'
-    const role = String(raw).toLowerCase()
-    if (VALID_ROLES.has(role)) return role
-    console.warn(`[serverRole] 未知值 "${raw}",回退为 all`)
-    return 'all'
-}
-
-function shouldServeApi(role) {
-    return role === 'all' || role === 'api'
-}
-
-function shouldRunLepaoWorker(role) {
-    return role === 'all' || role === 'worker'
-}
-
-/** 订单支付 MQ 消费者仅在 all 进程运行;api 只入队不消费 */
-function shouldRunOrderPaymentWorker(role) {
-    return role === 'all'
-}
-
-module.exports = {
-    resolveServerRole,
-    shouldServeApi,
-    shouldRunLepaoWorker,
-    shouldRunOrderPaymentWorker
-}

+ 0 - 2766
package-lock.json

@@ -1,2766 +0,0 @@
-{
-  "name": "ic-ctbu-backend",
-  "version": "1.0.0",
-  "lockfileVersion": 3,
-  "requires": true,
-  "packages": {
-    "": {
-      "name": "ic-ctbu-backend",
-      "version": "1.0.0",
-      "license": "ISC",
-      "dependencies": {
-        "ali-oss": "^6.x",
-        "amqplib": "^0.10.9",
-        "axios": "^1.7.4",
-        "bcryptjs": "^2.4.3",
-        "body-parser": "^1.20.2",
-        "chalk": "^4.1.2",
-        "cors": "^2.8.5",
-        "express": "^4.21.1",
-        "https-proxy-agent": "^7.0.6",
-        "multer": "^1.4.5-lts.1",
-        "mysql2": "^3.11.0",
-        "node-forge": "^1.3.1",
-        "nodemailer": "^6.9.14",
-        "puppeteer": "^23.9.0",
-        "redis": "^4.7.0",
-        "svg-captcha": "^1.4.0",
-        "ua-parser-js": "^2.0.4",
-        "uuid": "^11.0.3",
-        "ws": "^8.20.0"
-      }
-    },
-    "node_modules/@babel/code-frame": {
-      "version": "7.29.0",
-      "license": "MIT",
-      "dependencies": {
-        "@babel/helper-validator-identifier": "^7.28.5",
-        "js-tokens": "^4.0.0",
-        "picocolors": "^1.1.1"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/helper-validator-identifier": {
-      "version": "7.28.5",
-      "license": "MIT",
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@puppeteer/browsers": {
-      "version": "2.6.1",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "debug": "^4.4.0",
-        "extract-zip": "^2.0.1",
-        "progress": "^2.0.3",
-        "proxy-agent": "^6.5.0",
-        "semver": "^7.6.3",
-        "tar-fs": "^3.0.6",
-        "unbzip2-stream": "^1.4.3",
-        "yargs": "^17.7.2"
-      },
-      "bin": {
-        "browsers": "lib/cjs/main-cli.js"
-      },
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@puppeteer/browsers/node_modules/data-uri-to-buffer": {
-      "version": "6.0.2",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 14"
-      }
-    },
-    "node_modules/@puppeteer/browsers/node_modules/degenerator": {
-      "version": "5.0.1",
-      "license": "MIT",
-      "dependencies": {
-        "ast-types": "^0.13.4",
-        "escodegen": "^2.1.0",
-        "esprima": "^4.0.1"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
-    },
-    "node_modules/@puppeteer/browsers/node_modules/escodegen": {
-      "version": "2.1.0",
-      "license": "BSD-2-Clause",
-      "dependencies": {
-        "esprima": "^4.0.1",
-        "estraverse": "^5.2.0",
-        "esutils": "^2.0.2"
-      },
-      "bin": {
-        "escodegen": "bin/escodegen.js",
-        "esgenerate": "bin/esgenerate.js"
-      },
-      "engines": {
-        "node": ">=6.0"
-      },
-      "optionalDependencies": {
-        "source-map": "~0.6.1"
-      }
-    },
-    "node_modules/@puppeteer/browsers/node_modules/estraverse": {
-      "version": "5.3.0",
-      "license": "BSD-2-Clause",
-      "engines": {
-        "node": ">=4.0"
-      }
-    },
-    "node_modules/@puppeteer/browsers/node_modules/get-uri": {
-      "version": "6.0.5",
-      "license": "MIT",
-      "dependencies": {
-        "basic-ftp": "^5.0.2",
-        "data-uri-to-buffer": "^6.0.2",
-        "debug": "^4.3.4"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
-    },
-    "node_modules/@puppeteer/browsers/node_modules/http-proxy-agent": {
-      "version": "7.0.2",
-      "license": "MIT",
-      "dependencies": {
-        "agent-base": "^7.1.0",
-        "debug": "^4.3.4"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
-    },
-    "node_modules/@puppeteer/browsers/node_modules/lru-cache": {
-      "version": "7.18.3",
-      "license": "ISC",
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/@puppeteer/browsers/node_modules/pac-proxy-agent": {
-      "version": "7.2.0",
-      "license": "MIT",
-      "dependencies": {
-        "@tootallnate/quickjs-emscripten": "^0.23.0",
-        "agent-base": "^7.1.2",
-        "debug": "^4.3.4",
-        "get-uri": "^6.0.1",
-        "http-proxy-agent": "^7.0.0",
-        "https-proxy-agent": "^7.0.6",
-        "pac-resolver": "^7.0.1",
-        "socks-proxy-agent": "^8.0.5"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
-    },
-    "node_modules/@puppeteer/browsers/node_modules/pac-resolver": {
-      "version": "7.0.1",
-      "license": "MIT",
-      "dependencies": {
-        "degenerator": "^5.0.0",
-        "netmask": "^2.0.2"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
-    },
-    "node_modules/@puppeteer/browsers/node_modules/proxy-agent": {
-      "version": "6.5.0",
-      "license": "MIT",
-      "dependencies": {
-        "agent-base": "^7.1.2",
-        "debug": "^4.3.4",
-        "http-proxy-agent": "^7.0.1",
-        "https-proxy-agent": "^7.0.6",
-        "lru-cache": "^7.14.1",
-        "pac-proxy-agent": "^7.1.0",
-        "proxy-from-env": "^1.1.0",
-        "socks-proxy-agent": "^8.0.5"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
-    },
-    "node_modules/@puppeteer/browsers/node_modules/proxy-from-env": {
-      "version": "1.1.0",
-      "license": "MIT"
-    },
-    "node_modules/@puppeteer/browsers/node_modules/socks-proxy-agent": {
-      "version": "8.0.5",
-      "license": "MIT",
-      "dependencies": {
-        "agent-base": "^7.1.2",
-        "debug": "^4.3.4",
-        "socks": "^2.8.3"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
-    },
-    "node_modules/@redis/bloom": {
-      "version": "1.2.0",
-      "license": "MIT",
-      "peerDependencies": {
-        "@redis/client": "^1.0.0"
-      }
-    },
-    "node_modules/@redis/client": {
-      "version": "1.6.1",
-      "license": "MIT",
-      "dependencies": {
-        "cluster-key-slot": "1.1.2",
-        "generic-pool": "3.9.0",
-        "yallist": "4.0.0"
-      },
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@redis/graph": {
-      "version": "1.1.1",
-      "license": "MIT",
-      "peerDependencies": {
-        "@redis/client": "^1.0.0"
-      }
-    },
-    "node_modules/@redis/json": {
-      "version": "1.0.7",
-      "license": "MIT",
-      "peerDependencies": {
-        "@redis/client": "^1.0.0"
-      }
-    },
-    "node_modules/@redis/search": {
-      "version": "1.2.0",
-      "license": "MIT",
-      "peerDependencies": {
-        "@redis/client": "^1.0.0"
-      }
-    },
-    "node_modules/@redis/time-series": {
-      "version": "1.1.0",
-      "license": "MIT",
-      "peerDependencies": {
-        "@redis/client": "^1.0.0"
-      }
-    },
-    "node_modules/@tootallnate/quickjs-emscripten": {
-      "version": "0.23.0",
-      "license": "MIT"
-    },
-    "node_modules/@types/node": {
-      "version": "25.5.2",
-      "license": "MIT",
-      "dependencies": {
-        "undici-types": "~7.18.0"
-      }
-    },
-    "node_modules/@types/yauzl": {
-      "version": "2.10.3",
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "@types/node": "*"
-      }
-    },
-    "node_modules/accepts": {
-      "version": "1.3.8",
-      "license": "MIT",
-      "dependencies": {
-        "mime-types": "~2.1.34",
-        "negotiator": "0.6.3"
-      },
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/address": {
-      "version": "1.2.2",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 10.0.0"
-      }
-    },
-    "node_modules/agent-base": {
-      "version": "7.1.4",
-      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
-      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 14"
-      }
-    },
-    "node_modules/agentkeepalive": {
-      "version": "3.5.3",
-      "license": "MIT",
-      "dependencies": {
-        "humanize-ms": "^1.2.1"
-      },
-      "engines": {
-        "node": ">= 4.0.0"
-      }
-    },
-    "node_modules/ali-oss": {
-      "version": "6.23.0",
-      "license": "MIT",
-      "dependencies": {
-        "address": "^1.2.2",
-        "agentkeepalive": "^3.4.1",
-        "bowser": "^1.6.0",
-        "copy-to": "^2.0.1",
-        "dateformat": "^2.0.0",
-        "debug": "^4.3.4",
-        "destroy": "^1.0.4",
-        "end-or-error": "^1.0.1",
-        "get-ready": "^1.0.0",
-        "humanize-ms": "^1.2.0",
-        "is-type-of": "^1.4.0",
-        "js-base64": "^2.5.2",
-        "jstoxml": "^2.0.0",
-        "lodash": "^4.17.21",
-        "merge-descriptors": "^1.0.1",
-        "mime": "^2.4.5",
-        "platform": "^1.3.1",
-        "pump": "^3.0.0",
-        "qs": "^6.4.0",
-        "sdk-base": "^2.0.1",
-        "stream-http": "2.8.2",
-        "stream-wormhole": "^1.0.4",
-        "urllib": "^2.44.0",
-        "utility": "^1.18.0",
-        "xml2js": "^0.6.2"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/amqplib": {
-      "version": "0.10.9",
-      "license": "MIT",
-      "dependencies": {
-        "buffer-more-ints": "~1.0.0",
-        "url-parse": "~1.5.10"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/ansi-regex": {
-      "version": "5.0.1",
-      "license": "MIT",
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/ansi-styles": {
-      "version": "4.3.0",
-      "license": "MIT",
-      "dependencies": {
-        "color-convert": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-      }
-    },
-    "node_modules/any-promise": {
-      "version": "1.3.0",
-      "license": "MIT"
-    },
-    "node_modules/append-field": {
-      "version": "1.0.0",
-      "license": "MIT"
-    },
-    "node_modules/argparse": {
-      "version": "2.0.1",
-      "license": "Python-2.0"
-    },
-    "node_modules/array-flatten": {
-      "version": "1.1.1",
-      "license": "MIT"
-    },
-    "node_modules/ast-types": {
-      "version": "0.13.4",
-      "license": "MIT",
-      "dependencies": {
-        "tslib": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/asynckit": {
-      "version": "0.4.0",
-      "license": "MIT"
-    },
-    "node_modules/aws-ssl-profiles": {
-      "version": "1.1.2",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 6.0.0"
-      }
-    },
-    "node_modules/axios": {
-      "version": "1.14.0",
-      "license": "MIT",
-      "dependencies": {
-        "follow-redirects": "^1.15.11",
-        "form-data": "^4.0.5",
-        "proxy-from-env": "^2.1.0"
-      }
-    },
-    "node_modules/b4a": {
-      "version": "1.8.0",
-      "license": "Apache-2.0",
-      "peerDependencies": {
-        "react-native-b4a": "*"
-      },
-      "peerDependenciesMeta": {
-        "react-native-b4a": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/bare-events": {
-      "version": "2.8.2",
-      "license": "Apache-2.0",
-      "peerDependencies": {
-        "bare-abort-controller": "*"
-      },
-      "peerDependenciesMeta": {
-        "bare-abort-controller": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/bare-fs": {
-      "version": "4.6.0",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "bare-events": "^2.5.4",
-        "bare-path": "^3.0.0",
-        "bare-stream": "^2.6.4",
-        "bare-url": "^2.2.2",
-        "fast-fifo": "^1.3.2"
-      },
-      "engines": {
-        "bare": ">=1.16.0"
-      },
-      "peerDependencies": {
-        "bare-buffer": "*"
-      },
-      "peerDependenciesMeta": {
-        "bare-buffer": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/bare-os": {
-      "version": "3.8.7",
-      "license": "Apache-2.0",
-      "engines": {
-        "bare": ">=1.14.0"
-      }
-    },
-    "node_modules/bare-path": {
-      "version": "3.0.0",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "bare-os": "^3.0.1"
-      }
-    },
-    "node_modules/bare-stream": {
-      "version": "2.12.0",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "streamx": "^2.25.0",
-        "teex": "^1.0.1"
-      },
-      "peerDependencies": {
-        "bare-abort-controller": "*",
-        "bare-buffer": "*",
-        "bare-events": "*"
-      },
-      "peerDependenciesMeta": {
-        "bare-abort-controller": {
-          "optional": true
-        },
-        "bare-buffer": {
-          "optional": true
-        },
-        "bare-events": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/bare-url": {
-      "version": "2.4.0",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "bare-path": "^3.0.0"
-      }
-    },
-    "node_modules/base64-js": {
-      "version": "1.5.1",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "license": "MIT"
-    },
-    "node_modules/basic-ftp": {
-      "version": "5.2.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">=10.0.0"
-      }
-    },
-    "node_modules/bcryptjs": {
-      "version": "2.4.3",
-      "license": "MIT"
-    },
-    "node_modules/body-parser": {
-      "version": "1.20.4",
-      "license": "MIT",
-      "dependencies": {
-        "bytes": "~3.1.2",
-        "content-type": "~1.0.5",
-        "debug": "2.6.9",
-        "depd": "2.0.0",
-        "destroy": "~1.2.0",
-        "http-errors": "~2.0.1",
-        "iconv-lite": "~0.4.24",
-        "on-finished": "~2.4.1",
-        "qs": "~6.14.0",
-        "raw-body": "~2.5.3",
-        "type-is": "~1.6.18",
-        "unpipe": "~1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.8",
-        "npm": "1.2.8000 || >= 1.4.16"
-      }
-    },
-    "node_modules/body-parser/node_modules/debug": {
-      "version": "2.6.9",
-      "license": "MIT",
-      "dependencies": {
-        "ms": "2.0.0"
-      }
-    },
-    "node_modules/body-parser/node_modules/ms": {
-      "version": "2.0.0",
-      "license": "MIT"
-    },
-    "node_modules/body-parser/node_modules/qs": {
-      "version": "6.14.2",
-      "license": "BSD-3-Clause",
-      "dependencies": {
-        "side-channel": "^1.1.0"
-      },
-      "engines": {
-        "node": ">=0.6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/bowser": {
-      "version": "1.9.4",
-      "license": "MIT"
-    },
-    "node_modules/buffer": {
-      "version": "5.7.1",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "license": "MIT",
-      "dependencies": {
-        "base64-js": "^1.3.1",
-        "ieee754": "^1.1.13"
-      }
-    },
-    "node_modules/buffer-crc32": {
-      "version": "0.2.13",
-      "license": "MIT",
-      "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/buffer-from": {
-      "version": "1.1.2",
-      "license": "MIT"
-    },
-    "node_modules/buffer-more-ints": {
-      "version": "1.0.0",
-      "license": "MIT"
-    },
-    "node_modules/builtin-status-codes": {
-      "version": "3.0.0",
-      "license": "MIT"
-    },
-    "node_modules/busboy": {
-      "version": "1.6.0",
-      "dependencies": {
-        "streamsearch": "^1.1.0"
-      },
-      "engines": {
-        "node": ">=10.16.0"
-      }
-    },
-    "node_modules/bytes": {
-      "version": "3.1.2",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/call-bind-apply-helpers": {
-      "version": "1.0.2",
-      "license": "MIT",
-      "dependencies": {
-        "es-errors": "^1.3.0",
-        "function-bind": "^1.1.2"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/call-bound": {
-      "version": "1.0.4",
-      "license": "MIT",
-      "dependencies": {
-        "call-bind-apply-helpers": "^1.0.2",
-        "get-intrinsic": "^1.3.0"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/callsites": {
-      "version": "3.1.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/chalk": {
-      "version": "4.1.2",
-      "license": "MIT",
-      "dependencies": {
-        "ansi-styles": "^4.1.0",
-        "supports-color": "^7.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/chalk?sponsor=1"
-      }
-    },
-    "node_modules/chromium-bidi": {
-      "version": "0.11.0",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "mitt": "3.0.1",
-        "zod": "3.23.8"
-      },
-      "peerDependencies": {
-        "devtools-protocol": "*"
-      }
-    },
-    "node_modules/cliui": {
-      "version": "8.0.1",
-      "license": "ISC",
-      "dependencies": {
-        "string-width": "^4.2.0",
-        "strip-ansi": "^6.0.1",
-        "wrap-ansi": "^7.0.0"
-      },
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/cluster-key-slot": {
-      "version": "1.1.2",
-      "license": "Apache-2.0",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/color-convert": {
-      "version": "2.0.1",
-      "license": "MIT",
-      "dependencies": {
-        "color-name": "~1.1.4"
-      },
-      "engines": {
-        "node": ">=7.0.0"
-      }
-    },
-    "node_modules/color-name": {
-      "version": "1.1.4",
-      "license": "MIT"
-    },
-    "node_modules/combined-stream": {
-      "version": "1.0.8",
-      "license": "MIT",
-      "dependencies": {
-        "delayed-stream": "~1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/concat-stream": {
-      "version": "1.6.2",
-      "engines": [
-        "node >= 0.8"
-      ],
-      "license": "MIT",
-      "dependencies": {
-        "buffer-from": "^1.0.0",
-        "inherits": "^2.0.3",
-        "readable-stream": "^2.2.2",
-        "typedarray": "^0.0.6"
-      }
-    },
-    "node_modules/content-disposition": {
-      "version": "0.5.4",
-      "license": "MIT",
-      "dependencies": {
-        "safe-buffer": "5.2.1"
-      },
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/content-type": {
-      "version": "1.0.5",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/cookie": {
-      "version": "0.7.2",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/cookie-signature": {
-      "version": "1.0.7",
-      "license": "MIT"
-    },
-    "node_modules/copy-to": {
-      "version": "2.0.1",
-      "license": "MIT"
-    },
-    "node_modules/core-util-is": {
-      "version": "1.0.3",
-      "license": "MIT"
-    },
-    "node_modules/cors": {
-      "version": "2.8.6",
-      "license": "MIT",
-      "dependencies": {
-        "object-assign": "^4",
-        "vary": "^1"
-      },
-      "engines": {
-        "node": ">= 0.10"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/express"
-      }
-    },
-    "node_modules/cosmiconfig": {
-      "version": "9.0.1",
-      "license": "MIT",
-      "dependencies": {
-        "env-paths": "^2.2.1",
-        "import-fresh": "^3.3.0",
-        "js-yaml": "^4.1.0",
-        "parse-json": "^5.2.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/d-fischer"
-      },
-      "peerDependencies": {
-        "typescript": ">=4.9.5"
-      },
-      "peerDependenciesMeta": {
-        "typescript": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/dateformat": {
-      "version": "2.2.0",
-      "license": "MIT",
-      "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/debug": {
-      "version": "4.4.3",
-      "license": "MIT",
-      "dependencies": {
-        "ms": "^2.1.3"
-      },
-      "engines": {
-        "node": ">=6.0"
-      },
-      "peerDependenciesMeta": {
-        "supports-color": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/default-user-agent": {
-      "version": "1.0.0",
-      "license": "MIT",
-      "dependencies": {
-        "os-name": "~1.0.3"
-      },
-      "engines": {
-        "node": ">= 0.10.0"
-      }
-    },
-    "node_modules/delayed-stream": {
-      "version": "1.0.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">=0.4.0"
-      }
-    },
-    "node_modules/denque": {
-      "version": "2.1.0",
-      "license": "Apache-2.0",
-      "engines": {
-        "node": ">=0.10"
-      }
-    },
-    "node_modules/depd": {
-      "version": "2.0.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/destroy": {
-      "version": "1.2.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.8",
-        "npm": "1.2.8000 || >= 1.4.16"
-      }
-    },
-    "node_modules/detect-europe-js": {
-      "version": "0.1.2",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/faisalman"
-        },
-        {
-          "type": "opencollective",
-          "url": "https://opencollective.com/ua-parser-js"
-        },
-        {
-          "type": "paypal",
-          "url": "https://paypal.me/faisalman"
-        }
-      ],
-      "license": "MIT"
-    },
-    "node_modules/devtools-protocol": {
-      "version": "0.0.1367902",
-      "license": "BSD-3-Clause"
-    },
-    "node_modules/digest-header": {
-      "version": "1.1.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 8.0.0"
-      }
-    },
-    "node_modules/dunder-proto": {
-      "version": "1.0.1",
-      "license": "MIT",
-      "dependencies": {
-        "call-bind-apply-helpers": "^1.0.1",
-        "es-errors": "^1.3.0",
-        "gopd": "^1.2.0"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/ee-first": {
-      "version": "1.1.1",
-      "license": "MIT"
-    },
-    "node_modules/emoji-regex": {
-      "version": "8.0.0",
-      "license": "MIT"
-    },
-    "node_modules/encodeurl": {
-      "version": "2.0.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/end-of-stream": {
-      "version": "1.4.5",
-      "license": "MIT",
-      "dependencies": {
-        "once": "^1.4.0"
-      }
-    },
-    "node_modules/end-or-error": {
-      "version": "1.0.1",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.11.14"
-      }
-    },
-    "node_modules/env-paths": {
-      "version": "2.2.1",
-      "license": "MIT",
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/error-ex": {
-      "version": "1.3.4",
-      "license": "MIT",
-      "dependencies": {
-        "is-arrayish": "^0.2.1"
-      }
-    },
-    "node_modules/es-define-property": {
-      "version": "1.0.1",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/es-errors": {
-      "version": "1.3.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/es-object-atoms": {
-      "version": "1.1.1",
-      "license": "MIT",
-      "dependencies": {
-        "es-errors": "^1.3.0"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/es-set-tostringtag": {
-      "version": "2.1.0",
-      "license": "MIT",
-      "dependencies": {
-        "es-errors": "^1.3.0",
-        "get-intrinsic": "^1.2.6",
-        "has-tostringtag": "^1.0.2",
-        "hasown": "^2.0.2"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/escalade": {
-      "version": "3.2.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/escape-html": {
-      "version": "1.0.3",
-      "license": "MIT"
-    },
-    "node_modules/esprima": {
-      "version": "4.0.1",
-      "license": "BSD-2-Clause",
-      "bin": {
-        "esparse": "bin/esparse.js",
-        "esvalidate": "bin/esvalidate.js"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/esutils": {
-      "version": "2.0.3",
-      "license": "BSD-2-Clause",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/etag": {
-      "version": "1.8.1",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/events-universal": {
-      "version": "1.0.1",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "bare-events": "^2.7.0"
-      }
-    },
-    "node_modules/express": {
-      "version": "4.22.1",
-      "license": "MIT",
-      "dependencies": {
-        "accepts": "~1.3.8",
-        "array-flatten": "1.1.1",
-        "body-parser": "~1.20.3",
-        "content-disposition": "~0.5.4",
-        "content-type": "~1.0.4",
-        "cookie": "~0.7.1",
-        "cookie-signature": "~1.0.6",
-        "debug": "2.6.9",
-        "depd": "2.0.0",
-        "encodeurl": "~2.0.0",
-        "escape-html": "~1.0.3",
-        "etag": "~1.8.1",
-        "finalhandler": "~1.3.1",
-        "fresh": "~0.5.2",
-        "http-errors": "~2.0.0",
-        "merge-descriptors": "1.0.3",
-        "methods": "~1.1.2",
-        "on-finished": "~2.4.1",
-        "parseurl": "~1.3.3",
-        "path-to-regexp": "~0.1.12",
-        "proxy-addr": "~2.0.7",
-        "qs": "~6.14.0",
-        "range-parser": "~1.2.1",
-        "safe-buffer": "5.2.1",
-        "send": "~0.19.0",
-        "serve-static": "~1.16.2",
-        "setprototypeof": "1.2.0",
-        "statuses": "~2.0.1",
-        "type-is": "~1.6.18",
-        "utils-merge": "1.0.1",
-        "vary": "~1.1.2"
-      },
-      "engines": {
-        "node": ">= 0.10.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/express"
-      }
-    },
-    "node_modules/express/node_modules/debug": {
-      "version": "2.6.9",
-      "license": "MIT",
-      "dependencies": {
-        "ms": "2.0.0"
-      }
-    },
-    "node_modules/express/node_modules/ms": {
-      "version": "2.0.0",
-      "license": "MIT"
-    },
-    "node_modules/express/node_modules/qs": {
-      "version": "6.14.2",
-      "license": "BSD-3-Clause",
-      "dependencies": {
-        "side-channel": "^1.1.0"
-      },
-      "engines": {
-        "node": ">=0.6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/extend-shallow": {
-      "version": "2.0.1",
-      "license": "MIT",
-      "dependencies": {
-        "is-extendable": "^0.1.0"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/extract-zip": {
-      "version": "2.0.1",
-      "license": "BSD-2-Clause",
-      "dependencies": {
-        "debug": "^4.1.1",
-        "get-stream": "^5.1.0",
-        "yauzl": "^2.10.0"
-      },
-      "bin": {
-        "extract-zip": "cli.js"
-      },
-      "engines": {
-        "node": ">= 10.17.0"
-      },
-      "optionalDependencies": {
-        "@types/yauzl": "^2.9.1"
-      }
-    },
-    "node_modules/fast-fifo": {
-      "version": "1.3.2",
-      "license": "MIT"
-    },
-    "node_modules/fd-slicer": {
-      "version": "1.1.0",
-      "license": "MIT",
-      "dependencies": {
-        "pend": "~1.2.0"
-      }
-    },
-    "node_modules/finalhandler": {
-      "version": "1.3.2",
-      "license": "MIT",
-      "dependencies": {
-        "debug": "2.6.9",
-        "encodeurl": "~2.0.0",
-        "escape-html": "~1.0.3",
-        "on-finished": "~2.4.1",
-        "parseurl": "~1.3.3",
-        "statuses": "~2.0.2",
-        "unpipe": "~1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/finalhandler/node_modules/debug": {
-      "version": "2.6.9",
-      "license": "MIT",
-      "dependencies": {
-        "ms": "2.0.0"
-      }
-    },
-    "node_modules/finalhandler/node_modules/ms": {
-      "version": "2.0.0",
-      "license": "MIT"
-    },
-    "node_modules/follow-redirects": {
-      "version": "1.15.11",
-      "funding": [
-        {
-          "type": "individual",
-          "url": "https://github.com/sponsors/RubenVerborgh"
-        }
-      ],
-      "license": "MIT",
-      "engines": {
-        "node": ">=4.0"
-      },
-      "peerDependenciesMeta": {
-        "debug": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/form-data": {
-      "version": "4.0.5",
-      "license": "MIT",
-      "dependencies": {
-        "asynckit": "^0.4.0",
-        "combined-stream": "^1.0.8",
-        "es-set-tostringtag": "^2.1.0",
-        "hasown": "^2.0.2",
-        "mime-types": "^2.1.12"
-      },
-      "engines": {
-        "node": ">= 6"
-      }
-    },
-    "node_modules/formstream": {
-      "version": "1.5.2",
-      "license": "MIT",
-      "dependencies": {
-        "destroy": "^1.0.4",
-        "mime": "^2.5.2",
-        "node-hex": "^1.0.1",
-        "pause-stream": "~0.0.11"
-      }
-    },
-    "node_modules/forwarded": {
-      "version": "0.2.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/fresh": {
-      "version": "0.5.2",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/function-bind": {
-      "version": "1.1.2",
-      "license": "MIT",
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/generate-function": {
-      "version": "2.3.1",
-      "license": "MIT",
-      "dependencies": {
-        "is-property": "^1.0.2"
-      }
-    },
-    "node_modules/generic-pool": {
-      "version": "3.9.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 4"
-      }
-    },
-    "node_modules/get-caller-file": {
-      "version": "2.0.5",
-      "license": "ISC",
-      "engines": {
-        "node": "6.* || 8.* || >= 10.*"
-      }
-    },
-    "node_modules/get-intrinsic": {
-      "version": "1.3.0",
-      "license": "MIT",
-      "dependencies": {
-        "call-bind-apply-helpers": "^1.0.2",
-        "es-define-property": "^1.0.1",
-        "es-errors": "^1.3.0",
-        "es-object-atoms": "^1.1.1",
-        "function-bind": "^1.1.2",
-        "get-proto": "^1.0.1",
-        "gopd": "^1.2.0",
-        "has-symbols": "^1.1.0",
-        "hasown": "^2.0.2",
-        "math-intrinsics": "^1.1.0"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/get-proto": {
-      "version": "1.0.1",
-      "license": "MIT",
-      "dependencies": {
-        "dunder-proto": "^1.0.1",
-        "es-object-atoms": "^1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/get-ready": {
-      "version": "1.0.0",
-      "license": "MIT"
-    },
-    "node_modules/get-stream": {
-      "version": "5.2.0",
-      "license": "MIT",
-      "dependencies": {
-        "pump": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/gopd": {
-      "version": "1.2.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/has-flag": {
-      "version": "4.0.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/has-symbols": {
-      "version": "1.1.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/has-tostringtag": {
-      "version": "1.0.2",
-      "license": "MIT",
-      "dependencies": {
-        "has-symbols": "^1.0.3"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/hasown": {
-      "version": "2.0.2",
-      "license": "MIT",
-      "dependencies": {
-        "function-bind": "^1.1.2"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/http-errors": {
-      "version": "2.0.1",
-      "license": "MIT",
-      "dependencies": {
-        "depd": "~2.0.0",
-        "inherits": "~2.0.4",
-        "setprototypeof": "~1.2.0",
-        "statuses": "~2.0.2",
-        "toidentifier": "~1.0.1"
-      },
-      "engines": {
-        "node": ">= 0.8"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/express"
-      }
-    },
-    "node_modules/https-proxy-agent": {
-      "version": "7.0.6",
-      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
-      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
-      "license": "MIT",
-      "dependencies": {
-        "agent-base": "^7.1.2",
-        "debug": "4"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
-    },
-    "node_modules/humanize-ms": {
-      "version": "1.2.1",
-      "license": "MIT",
-      "dependencies": {
-        "ms": "^2.0.0"
-      }
-    },
-    "node_modules/iconv-lite": {
-      "version": "0.4.24",
-      "license": "MIT",
-      "dependencies": {
-        "safer-buffer": ">= 2.1.2 < 3"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/ieee754": {
-      "version": "1.2.1",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "license": "BSD-3-Clause"
-    },
-    "node_modules/import-fresh": {
-      "version": "3.3.1",
-      "license": "MIT",
-      "dependencies": {
-        "parent-module": "^1.0.0",
-        "resolve-from": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/inherits": {
-      "version": "2.0.4",
-      "license": "ISC"
-    },
-    "node_modules/ip-address": {
-      "version": "10.1.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 12"
-      }
-    },
-    "node_modules/ipaddr.js": {
-      "version": "1.9.1",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.10"
-      }
-    },
-    "node_modules/is-arrayish": {
-      "version": "0.2.1",
-      "license": "MIT"
-    },
-    "node_modules/is-class-hotfix": {
-      "version": "0.0.6",
-      "license": "MIT"
-    },
-    "node_modules/is-extendable": {
-      "version": "0.1.1",
-      "license": "MIT",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/is-fullwidth-code-point": {
-      "version": "3.0.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/is-property": {
-      "version": "1.0.2",
-      "license": "MIT"
-    },
-    "node_modules/is-standalone-pwa": {
-      "version": "0.1.1",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/faisalman"
-        },
-        {
-          "type": "opencollective",
-          "url": "https://opencollective.com/ua-parser-js"
-        },
-        {
-          "type": "paypal",
-          "url": "https://paypal.me/faisalman"
-        }
-      ],
-      "license": "MIT"
-    },
-    "node_modules/is-type-of": {
-      "version": "1.4.0",
-      "license": "MIT",
-      "dependencies": {
-        "core-util-is": "^1.0.2",
-        "is-class-hotfix": "~0.0.6",
-        "isstream": "~0.1.2"
-      }
-    },
-    "node_modules/isarray": {
-      "version": "1.0.0",
-      "license": "MIT"
-    },
-    "node_modules/isstream": {
-      "version": "0.1.2",
-      "license": "MIT"
-    },
-    "node_modules/js-base64": {
-      "version": "2.6.4",
-      "license": "BSD-3-Clause"
-    },
-    "node_modules/js-tokens": {
-      "version": "4.0.0",
-      "license": "MIT"
-    },
-    "node_modules/js-yaml": {
-      "version": "4.1.1",
-      "license": "MIT",
-      "dependencies": {
-        "argparse": "^2.0.1"
-      },
-      "bin": {
-        "js-yaml": "bin/js-yaml.js"
-      }
-    },
-    "node_modules/json-parse-even-better-errors": {
-      "version": "2.3.1",
-      "license": "MIT"
-    },
-    "node_modules/jstoxml": {
-      "version": "2.2.9",
-      "license": "MIT"
-    },
-    "node_modules/lines-and-columns": {
-      "version": "1.2.4",
-      "license": "MIT"
-    },
-    "node_modules/lodash": {
-      "version": "4.18.1",
-      "license": "MIT"
-    },
-    "node_modules/long": {
-      "version": "5.3.2",
-      "license": "Apache-2.0"
-    },
-    "node_modules/lru.min": {
-      "version": "1.1.4",
-      "license": "MIT",
-      "engines": {
-        "bun": ">=1.0.0",
-        "deno": ">=1.30.0",
-        "node": ">=8.0.0"
-      },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/wellwelwel"
-      }
-    },
-    "node_modules/math-intrinsics": {
-      "version": "1.1.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/media-typer": {
-      "version": "0.3.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/merge-descriptors": {
-      "version": "1.0.3",
-      "license": "MIT",
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/methods": {
-      "version": "1.1.2",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/mime": {
-      "version": "2.6.0",
-      "license": "MIT",
-      "bin": {
-        "mime": "cli.js"
-      },
-      "engines": {
-        "node": ">=4.0.0"
-      }
-    },
-    "node_modules/mime-db": {
-      "version": "1.52.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/mime-types": {
-      "version": "2.1.35",
-      "license": "MIT",
-      "dependencies": {
-        "mime-db": "1.52.0"
-      },
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/minimist": {
-      "version": "1.2.8",
-      "license": "MIT",
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/mitt": {
-      "version": "3.0.1",
-      "license": "MIT"
-    },
-    "node_modules/mkdirp": {
-      "version": "0.5.6",
-      "license": "MIT",
-      "dependencies": {
-        "minimist": "^1.2.6"
-      },
-      "bin": {
-        "mkdirp": "bin/cmd.js"
-      }
-    },
-    "node_modules/ms": {
-      "version": "2.1.3",
-      "license": "MIT"
-    },
-    "node_modules/multer": {
-      "version": "1.4.5-lts.2",
-      "license": "MIT",
-      "dependencies": {
-        "append-field": "^1.0.0",
-        "busboy": "^1.0.0",
-        "concat-stream": "^1.5.2",
-        "mkdirp": "^0.5.4",
-        "object-assign": "^4.1.1",
-        "type-is": "^1.6.4",
-        "xtend": "^4.0.0"
-      },
-      "engines": {
-        "node": ">= 6.0.0"
-      }
-    },
-    "node_modules/mysql2": {
-      "version": "3.20.0",
-      "license": "MIT",
-      "dependencies": {
-        "aws-ssl-profiles": "^1.1.2",
-        "denque": "^2.1.0",
-        "generate-function": "^2.3.1",
-        "iconv-lite": "^0.7.2",
-        "long": "^5.3.2",
-        "lru.min": "^1.1.4",
-        "named-placeholders": "^1.1.6",
-        "sql-escaper": "^1.3.3"
-      },
-      "engines": {
-        "node": ">= 8.0"
-      },
-      "peerDependencies": {
-        "@types/node": ">= 8"
-      }
-    },
-    "node_modules/mysql2/node_modules/iconv-lite": {
-      "version": "0.7.2",
-      "license": "MIT",
-      "dependencies": {
-        "safer-buffer": ">= 2.1.2 < 3.0.0"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/express"
-      }
-    },
-    "node_modules/mz": {
-      "version": "2.7.0",
-      "license": "MIT",
-      "dependencies": {
-        "any-promise": "^1.0.0",
-        "object-assign": "^4.0.1",
-        "thenify-all": "^1.0.0"
-      }
-    },
-    "node_modules/named-placeholders": {
-      "version": "1.1.6",
-      "license": "MIT",
-      "dependencies": {
-        "lru.min": "^1.1.0"
-      },
-      "engines": {
-        "node": ">=8.0.0"
-      }
-    },
-    "node_modules/negotiator": {
-      "version": "0.6.3",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/netmask": {
-      "version": "2.1.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.4.0"
-      }
-    },
-    "node_modules/node-forge": {
-      "version": "1.4.0",
-      "license": "(BSD-3-Clause OR GPL-2.0)",
-      "engines": {
-        "node": ">= 6.13.0"
-      }
-    },
-    "node_modules/node-hex": {
-      "version": "1.0.1",
-      "license": "MIT",
-      "engines": {
-        "node": ">=8.0.0"
-      }
-    },
-    "node_modules/nodemailer": {
-      "version": "6.10.1",
-      "license": "MIT-0",
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
-    "node_modules/object-assign": {
-      "version": "4.1.1",
-      "license": "MIT",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/object-inspect": {
-      "version": "1.13.4",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/on-finished": {
-      "version": "2.4.1",
-      "license": "MIT",
-      "dependencies": {
-        "ee-first": "1.1.1"
-      },
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/once": {
-      "version": "1.4.0",
-      "license": "ISC",
-      "dependencies": {
-        "wrappy": "1"
-      }
-    },
-    "node_modules/opentype.js": {
-      "version": "0.7.3",
-      "license": "MIT",
-      "dependencies": {
-        "tiny-inflate": "^1.0.2"
-      },
-      "bin": {
-        "ot": "bin/ot"
-      }
-    },
-    "node_modules/os-name": {
-      "version": "1.0.3",
-      "license": "MIT",
-      "dependencies": {
-        "osx-release": "^1.0.0",
-        "win-release": "^1.0.0"
-      },
-      "bin": {
-        "os-name": "cli.js"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/osx-release": {
-      "version": "1.1.0",
-      "license": "MIT",
-      "dependencies": {
-        "minimist": "^1.1.0"
-      },
-      "bin": {
-        "osx-release": "cli.js"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/parent-module": {
-      "version": "1.0.1",
-      "license": "MIT",
-      "dependencies": {
-        "callsites": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/parse-json": {
-      "version": "5.2.0",
-      "license": "MIT",
-      "dependencies": {
-        "@babel/code-frame": "^7.0.0",
-        "error-ex": "^1.3.1",
-        "json-parse-even-better-errors": "^2.3.0",
-        "lines-and-columns": "^1.1.6"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/parseurl": {
-      "version": "1.3.3",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/path-to-regexp": {
-      "version": "0.1.13",
-      "license": "MIT"
-    },
-    "node_modules/pause-stream": {
-      "version": "0.0.11",
-      "license": [
-        "MIT",
-        "Apache2"
-      ],
-      "dependencies": {
-        "through": "~2.3"
-      }
-    },
-    "node_modules/pend": {
-      "version": "1.2.0",
-      "license": "MIT"
-    },
-    "node_modules/picocolors": {
-      "version": "1.1.1",
-      "license": "ISC"
-    },
-    "node_modules/platform": {
-      "version": "1.3.6",
-      "license": "MIT"
-    },
-    "node_modules/process-nextick-args": {
-      "version": "2.0.1",
-      "license": "MIT"
-    },
-    "node_modules/progress": {
-      "version": "2.0.3",
-      "license": "MIT",
-      "engines": {
-        "node": ">=0.4.0"
-      }
-    },
-    "node_modules/proxy-addr": {
-      "version": "2.0.7",
-      "license": "MIT",
-      "dependencies": {
-        "forwarded": "0.2.0",
-        "ipaddr.js": "1.9.1"
-      },
-      "engines": {
-        "node": ">= 0.10"
-      }
-    },
-    "node_modules/proxy-from-env": {
-      "version": "2.1.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/pump": {
-      "version": "3.0.4",
-      "license": "MIT",
-      "dependencies": {
-        "end-of-stream": "^1.1.0",
-        "once": "^1.3.1"
-      }
-    },
-    "node_modules/puppeteer": {
-      "version": "23.11.1",
-      "hasInstallScript": true,
-      "license": "Apache-2.0",
-      "dependencies": {
-        "@puppeteer/browsers": "2.6.1",
-        "chromium-bidi": "0.11.0",
-        "cosmiconfig": "^9.0.0",
-        "devtools-protocol": "0.0.1367902",
-        "puppeteer-core": "23.11.1",
-        "typed-query-selector": "^2.12.0"
-      },
-      "bin": {
-        "puppeteer": "lib/cjs/puppeteer/node/cli.js"
-      },
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/puppeteer-core": {
-      "version": "23.11.1",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "@puppeteer/browsers": "2.6.1",
-        "chromium-bidi": "0.11.0",
-        "debug": "^4.4.0",
-        "devtools-protocol": "0.0.1367902",
-        "typed-query-selector": "^2.12.0",
-        "ws": "^8.18.0"
-      },
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/qs": {
-      "version": "6.15.0",
-      "license": "BSD-3-Clause",
-      "dependencies": {
-        "side-channel": "^1.1.0"
-      },
-      "engines": {
-        "node": ">=0.6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/querystringify": {
-      "version": "2.2.0",
-      "license": "MIT"
-    },
-    "node_modules/range-parser": {
-      "version": "1.2.1",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/raw-body": {
-      "version": "2.5.3",
-      "license": "MIT",
-      "dependencies": {
-        "bytes": "~3.1.2",
-        "http-errors": "~2.0.1",
-        "iconv-lite": "~0.4.24",
-        "unpipe": "~1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/readable-stream": {
-      "version": "2.3.8",
-      "license": "MIT",
-      "dependencies": {
-        "core-util-is": "~1.0.0",
-        "inherits": "~2.0.3",
-        "isarray": "~1.0.0",
-        "process-nextick-args": "~2.0.0",
-        "safe-buffer": "~5.1.1",
-        "string_decoder": "~1.1.1",
-        "util-deprecate": "~1.0.1"
-      }
-    },
-    "node_modules/readable-stream/node_modules/safe-buffer": {
-      "version": "5.1.2",
-      "license": "MIT"
-    },
-    "node_modules/redis": {
-      "version": "4.7.1",
-      "license": "MIT",
-      "workspaces": [
-        "./packages/*"
-      ],
-      "dependencies": {
-        "@redis/bloom": "1.2.0",
-        "@redis/client": "1.6.1",
-        "@redis/graph": "1.1.1",
-        "@redis/json": "1.0.7",
-        "@redis/search": "1.2.0",
-        "@redis/time-series": "1.1.0"
-      }
-    },
-    "node_modules/require-directory": {
-      "version": "2.1.1",
-      "license": "MIT",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/requires-port": {
-      "version": "1.0.0",
-      "license": "MIT"
-    },
-    "node_modules/resolve-from": {
-      "version": "4.0.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/safe-buffer": {
-      "version": "5.2.1",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "license": "MIT"
-    },
-    "node_modules/safer-buffer": {
-      "version": "2.1.2",
-      "license": "MIT"
-    },
-    "node_modules/sax": {
-      "version": "1.6.0",
-      "license": "BlueOak-1.0.0",
-      "engines": {
-        "node": ">=11.0.0"
-      }
-    },
-    "node_modules/sdk-base": {
-      "version": "2.0.1",
-      "license": "MIT",
-      "dependencies": {
-        "get-ready": "~1.0.0"
-      }
-    },
-    "node_modules/semver": {
-      "version": "7.7.4",
-      "license": "ISC",
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/send": {
-      "version": "0.19.2",
-      "license": "MIT",
-      "dependencies": {
-        "debug": "2.6.9",
-        "depd": "2.0.0",
-        "destroy": "1.2.0",
-        "encodeurl": "~2.0.0",
-        "escape-html": "~1.0.3",
-        "etag": "~1.8.1",
-        "fresh": "~0.5.2",
-        "http-errors": "~2.0.1",
-        "mime": "1.6.0",
-        "ms": "2.1.3",
-        "on-finished": "~2.4.1",
-        "range-parser": "~1.2.1",
-        "statuses": "~2.0.2"
-      },
-      "engines": {
-        "node": ">= 0.8.0"
-      }
-    },
-    "node_modules/send/node_modules/debug": {
-      "version": "2.6.9",
-      "license": "MIT",
-      "dependencies": {
-        "ms": "2.0.0"
-      }
-    },
-    "node_modules/send/node_modules/debug/node_modules/ms": {
-      "version": "2.0.0",
-      "license": "MIT"
-    },
-    "node_modules/send/node_modules/mime": {
-      "version": "1.6.0",
-      "license": "MIT",
-      "bin": {
-        "mime": "cli.js"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/serve-static": {
-      "version": "1.16.3",
-      "license": "MIT",
-      "dependencies": {
-        "encodeurl": "~2.0.0",
-        "escape-html": "~1.0.3",
-        "parseurl": "~1.3.3",
-        "send": "~0.19.1"
-      },
-      "engines": {
-        "node": ">= 0.8.0"
-      }
-    },
-    "node_modules/setprototypeof": {
-      "version": "1.2.0",
-      "license": "ISC"
-    },
-    "node_modules/side-channel": {
-      "version": "1.1.0",
-      "license": "MIT",
-      "dependencies": {
-        "es-errors": "^1.3.0",
-        "object-inspect": "^1.13.3",
-        "side-channel-list": "^1.0.0",
-        "side-channel-map": "^1.0.1",
-        "side-channel-weakmap": "^1.0.2"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/side-channel-list": {
-      "version": "1.0.0",
-      "license": "MIT",
-      "dependencies": {
-        "es-errors": "^1.3.0",
-        "object-inspect": "^1.13.3"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/side-channel-map": {
-      "version": "1.0.1",
-      "license": "MIT",
-      "dependencies": {
-        "call-bound": "^1.0.2",
-        "es-errors": "^1.3.0",
-        "get-intrinsic": "^1.2.5",
-        "object-inspect": "^1.13.3"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/side-channel-weakmap": {
-      "version": "1.0.2",
-      "license": "MIT",
-      "dependencies": {
-        "call-bound": "^1.0.2",
-        "es-errors": "^1.3.0",
-        "get-intrinsic": "^1.2.5",
-        "object-inspect": "^1.13.3",
-        "side-channel-map": "^1.0.1"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/smart-buffer": {
-      "version": "4.2.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 6.0.0",
-        "npm": ">= 3.0.0"
-      }
-    },
-    "node_modules/socks": {
-      "version": "2.8.7",
-      "license": "MIT",
-      "dependencies": {
-        "ip-address": "^10.0.1",
-        "smart-buffer": "^4.2.0"
-      },
-      "engines": {
-        "node": ">= 10.0.0",
-        "npm": ">= 3.0.0"
-      }
-    },
-    "node_modules/source-map": {
-      "version": "0.6.1",
-      "license": "BSD-3-Clause",
-      "optional": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/sql-escaper": {
-      "version": "1.3.3",
-      "license": "MIT",
-      "engines": {
-        "bun": ">=1.0.0",
-        "deno": ">=2.0.0",
-        "node": ">=12.0.0"
-      },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
-      }
-    },
-    "node_modules/statuses": {
-      "version": "2.0.2",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/stream-http": {
-      "version": "2.8.2",
-      "license": "MIT",
-      "dependencies": {
-        "builtin-status-codes": "^3.0.0",
-        "inherits": "^2.0.1",
-        "readable-stream": "^2.3.6",
-        "to-arraybuffer": "^1.0.0",
-        "xtend": "^4.0.0"
-      }
-    },
-    "node_modules/stream-wormhole": {
-      "version": "1.1.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">=4.0.0"
-      }
-    },
-    "node_modules/streamsearch": {
-      "version": "1.1.0",
-      "engines": {
-        "node": ">=10.0.0"
-      }
-    },
-    "node_modules/streamx": {
-      "version": "2.25.0",
-      "license": "MIT",
-      "dependencies": {
-        "events-universal": "^1.0.0",
-        "fast-fifo": "^1.3.2",
-        "text-decoder": "^1.1.0"
-      }
-    },
-    "node_modules/string_decoder": {
-      "version": "1.1.1",
-      "license": "MIT",
-      "dependencies": {
-        "safe-buffer": "~5.1.0"
-      }
-    },
-    "node_modules/string_decoder/node_modules/safe-buffer": {
-      "version": "5.1.2",
-      "license": "MIT"
-    },
-    "node_modules/string-width": {
-      "version": "4.2.3",
-      "license": "MIT",
-      "dependencies": {
-        "emoji-regex": "^8.0.0",
-        "is-fullwidth-code-point": "^3.0.0",
-        "strip-ansi": "^6.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/strip-ansi": {
-      "version": "6.0.1",
-      "license": "MIT",
-      "dependencies": {
-        "ansi-regex": "^5.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/supports-color": {
-      "version": "7.2.0",
-      "license": "MIT",
-      "dependencies": {
-        "has-flag": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/svg-captcha": {
-      "version": "1.4.0",
-      "license": "MIT",
-      "dependencies": {
-        "opentype.js": "^0.7.3"
-      },
-      "engines": {
-        "node": ">=4.x"
-      }
-    },
-    "node_modules/tar-fs": {
-      "version": "3.1.2",
-      "license": "MIT",
-      "dependencies": {
-        "pump": "^3.0.0",
-        "tar-stream": "^3.1.5"
-      },
-      "optionalDependencies": {
-        "bare-fs": "^4.0.1",
-        "bare-path": "^3.0.0"
-      }
-    },
-    "node_modules/tar-stream": {
-      "version": "3.1.8",
-      "license": "MIT",
-      "dependencies": {
-        "b4a": "^1.6.4",
-        "bare-fs": "^4.5.5",
-        "fast-fifo": "^1.2.0",
-        "streamx": "^2.15.0"
-      }
-    },
-    "node_modules/teex": {
-      "version": "1.0.1",
-      "license": "MIT",
-      "dependencies": {
-        "streamx": "^2.12.5"
-      }
-    },
-    "node_modules/text-decoder": {
-      "version": "1.2.7",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "b4a": "^1.6.4"
-      }
-    },
-    "node_modules/thenify": {
-      "version": "3.3.1",
-      "license": "MIT",
-      "dependencies": {
-        "any-promise": "^1.0.0"
-      }
-    },
-    "node_modules/thenify-all": {
-      "version": "1.6.0",
-      "license": "MIT",
-      "dependencies": {
-        "thenify": ">= 3.1.0 < 4"
-      },
-      "engines": {
-        "node": ">=0.8"
-      }
-    },
-    "node_modules/through": {
-      "version": "2.3.8",
-      "license": "MIT"
-    },
-    "node_modules/tiny-inflate": {
-      "version": "1.0.3",
-      "license": "MIT"
-    },
-    "node_modules/to-arraybuffer": {
-      "version": "1.0.1",
-      "license": "MIT"
-    },
-    "node_modules/toidentifier": {
-      "version": "1.0.1",
-      "license": "MIT",
-      "engines": {
-        "node": ">=0.6"
-      }
-    },
-    "node_modules/tslib": {
-      "version": "2.8.1",
-      "license": "0BSD"
-    },
-    "node_modules/type-is": {
-      "version": "1.6.18",
-      "license": "MIT",
-      "dependencies": {
-        "media-typer": "0.3.0",
-        "mime-types": "~2.1.24"
-      },
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/typed-query-selector": {
-      "version": "2.12.1",
-      "license": "MIT"
-    },
-    "node_modules/typedarray": {
-      "version": "0.0.6",
-      "license": "MIT"
-    },
-    "node_modules/ua-is-frozen": {
-      "version": "0.1.2",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/faisalman"
-        },
-        {
-          "type": "opencollective",
-          "url": "https://opencollective.com/ua-parser-js"
-        },
-        {
-          "type": "paypal",
-          "url": "https://paypal.me/faisalman"
-        }
-      ],
-      "license": "MIT"
-    },
-    "node_modules/ua-parser-js": {
-      "version": "2.0.9",
-      "funding": [
-        {
-          "type": "opencollective",
-          "url": "https://opencollective.com/ua-parser-js"
-        },
-        {
-          "type": "paypal",
-          "url": "https://paypal.me/faisalman"
-        },
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/faisalman"
-        }
-      ],
-      "license": "AGPL-3.0-or-later",
-      "dependencies": {
-        "detect-europe-js": "^0.1.2",
-        "is-standalone-pwa": "^0.1.1",
-        "ua-is-frozen": "^0.1.2"
-      },
-      "bin": {
-        "ua-parser-js": "script/cli.js"
-      },
-      "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/unbzip2-stream": {
-      "version": "1.4.3",
-      "license": "MIT",
-      "dependencies": {
-        "buffer": "^5.2.1",
-        "through": "^2.3.8"
-      }
-    },
-    "node_modules/undici-types": {
-      "version": "7.18.2",
-      "license": "MIT"
-    },
-    "node_modules/unescape": {
-      "version": "1.0.1",
-      "license": "MIT",
-      "dependencies": {
-        "extend-shallow": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/unpipe": {
-      "version": "1.0.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/url-parse": {
-      "version": "1.5.10",
-      "license": "MIT",
-      "dependencies": {
-        "querystringify": "^2.1.1",
-        "requires-port": "^1.0.0"
-      }
-    },
-    "node_modules/urllib": {
-      "version": "2.44.0",
-      "license": "MIT",
-      "dependencies": {
-        "any-promise": "^1.3.0",
-        "content-type": "^1.0.2",
-        "default-user-agent": "^1.0.0",
-        "digest-header": "^1.0.0",
-        "ee-first": "~1.1.1",
-        "formstream": "^1.1.0",
-        "humanize-ms": "^1.2.0",
-        "iconv-lite": "^0.6.3",
-        "pump": "^3.0.0",
-        "qs": "^6.4.0",
-        "statuses": "^1.3.1",
-        "utility": "^1.16.1"
-      },
-      "engines": {
-        "node": ">= 0.10.0"
-      },
-      "peerDependencies": {
-        "proxy-agent": "^5.0.0"
-      },
-      "peerDependenciesMeta": {
-        "proxy-agent": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/urllib/node_modules/iconv-lite": {
-      "version": "0.6.3",
-      "license": "MIT",
-      "dependencies": {
-        "safer-buffer": ">= 2.1.2 < 3.0.0"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/urllib/node_modules/statuses": {
-      "version": "1.5.0",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/util-deprecate": {
-      "version": "1.0.2",
-      "license": "MIT"
-    },
-    "node_modules/utility": {
-      "version": "1.18.0",
-      "license": "MIT",
-      "dependencies": {
-        "copy-to": "^2.0.1",
-        "escape-html": "^1.0.3",
-        "mkdirp": "^0.5.1",
-        "mz": "^2.7.0",
-        "unescape": "^1.0.1"
-      },
-      "engines": {
-        "node": ">= 0.12.0"
-      }
-    },
-    "node_modules/utils-merge": {
-      "version": "1.0.1",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.4.0"
-      }
-    },
-    "node_modules/uuid": {
-      "version": "11.1.0",
-      "funding": [
-        "https://github.com/sponsors/broofa",
-        "https://github.com/sponsors/ctavan"
-      ],
-      "license": "MIT",
-      "bin": {
-        "uuid": "dist/esm/bin/uuid"
-      }
-    },
-    "node_modules/vary": {
-      "version": "1.1.2",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/win-release": {
-      "version": "1.1.1",
-      "license": "MIT",
-      "dependencies": {
-        "semver": "^5.0.1"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/win-release/node_modules/semver": {
-      "version": "5.7.2",
-      "license": "ISC",
-      "bin": {
-        "semver": "bin/semver"
-      }
-    },
-    "node_modules/wrap-ansi": {
-      "version": "7.0.0",
-      "license": "MIT",
-      "dependencies": {
-        "ansi-styles": "^4.0.0",
-        "string-width": "^4.1.0",
-        "strip-ansi": "^6.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
-      }
-    },
-    "node_modules/wrappy": {
-      "version": "1.0.2",
-      "license": "ISC"
-    },
-    "node_modules/ws": {
-      "version": "8.20.0",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
-      "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=10.0.0"
-      },
-      "peerDependencies": {
-        "bufferutil": "^4.0.1",
-        "utf-8-validate": ">=5.0.2"
-      },
-      "peerDependenciesMeta": {
-        "bufferutil": {
-          "optional": true
-        },
-        "utf-8-validate": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/xml2js": {
-      "version": "0.6.2",
-      "license": "MIT",
-      "dependencies": {
-        "sax": ">=0.6.0",
-        "xmlbuilder": "~11.0.0"
-      },
-      "engines": {
-        "node": ">=4.0.0"
-      }
-    },
-    "node_modules/xmlbuilder": {
-      "version": "11.0.1",
-      "license": "MIT",
-      "engines": {
-        "node": ">=4.0"
-      }
-    },
-    "node_modules/xtend": {
-      "version": "4.0.2",
-      "license": "MIT",
-      "engines": {
-        "node": ">=0.4"
-      }
-    },
-    "node_modules/y18n": {
-      "version": "5.0.8",
-      "license": "ISC",
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/yallist": {
-      "version": "4.0.0",
-      "license": "ISC"
-    },
-    "node_modules/yargs": {
-      "version": "17.7.2",
-      "license": "MIT",
-      "dependencies": {
-        "cliui": "^8.0.1",
-        "escalade": "^3.1.1",
-        "get-caller-file": "^2.0.5",
-        "require-directory": "^2.1.1",
-        "string-width": "^4.2.3",
-        "y18n": "^5.0.5",
-        "yargs-parser": "^21.1.1"
-      },
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/yargs-parser": {
-      "version": "21.1.1",
-      "license": "ISC",
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/yauzl": {
-      "version": "2.10.0",
-      "license": "MIT",
-      "dependencies": {
-        "buffer-crc32": "~0.2.3",
-        "fd-slicer": "~1.1.0"
-      }
-    },
-    "node_modules/zod": {
-      "version": "3.23.8",
-      "license": "MIT",
-      "funding": {
-        "url": "https://github.com/sponsors/colinhacks"
-      }
-    }
-  }
-}

+ 2 - 4
package.json

@@ -4,8 +4,7 @@
   "description": "",
   "main": "index.js",
   "scripts": {
-    "start": "node index.js",
-    "start:worker": "node worker.js"
+    "start": "node index.js"
   },
   "author": "thc",
   "license": "ISC",
@@ -26,7 +25,6 @@
     "redis": "^4.7.0",
     "svg-captcha": "^1.4.0",
     "ua-parser-js": "^2.0.4",
-    "uuid": "^11.0.3",
-    "ws": "^8.20.0"
+    "uuid": "^11.0.3"
   }
 }

+ 8 - 14
plugin/DataBase/Redis.js

@@ -5,24 +5,18 @@ const path = require('path');
 
 class RedisClient {
     constructor() {
-        // node-redis v4 必须使用 socket 或 url,顶层 host/port 会被忽略并默认连 127.0.0.1:6379
-        const clientOptions = {
-            socket: {
-                host: config.redis.host,
-                port: config.redis.port
-            }
-        };
-        if (config.redis.password) {
-            clientOptions.password = config.redis.password;
-        }
-        this.client = redis.createClient(clientOptions);
+        // 创建 Redis 客户端实例
+        this.client = redis.createClient({
+            host: config.redis.host,
+            port: config.redis.port,
+            password: config.redis.password || null
+        });
 
         this.logger = new Logger(path.join(__dirname, '../../logs/Redis.log'), 'INFO');
-        this.logger.info(`Redis 目标:${config.redis.host}:${config.redis.port}`);
 
         // 处理连接错误
         this.client.on('error', (err) => {
-            this.logger.error(`Redis连接出错:${err?.message || err?.stack || err}`);
+            this.logger.error(`Redis连接出错:${err.stack}`);
         });
 
         // 确保连接成功
@@ -45,7 +39,7 @@ class RedisClient {
     getClient() {
         if (!this.client.isOpen) {
             this.client.connect().catch((err) => {
-                this.logger.error(`重新连接Redis时出错:${err?.message || err?.stack || err}`);
+                this.logger.error(`重新连接Redis时出错:${err.stack}`);
             });
         }
         return this.client;

+ 37 - 32
plugin/Email/Email.js

@@ -6,55 +6,60 @@ const Logger = require('../../lib/Logger')
 const logger = new Logger(path.join(__dirname, '../../logs/Email.log'), 'INFO')
 
 /**
- * 尝试用多个邮箱配置发送邮件
+ * 尝试用多个邮箱配置发送邮件;失败时仅记录日志并 resolve(false),不抛错、不 reject。
  * @param {string} email - 收件人邮箱
  * @param {string} subject - 邮件主题
  * @param {string} content - 邮件内容(支持 HTML)
+ * @returns {Promise<boolean>} 是否至少有一个账号发送成功
  */
 async function sendEmail(email, subject, content) {
-    return new Promise(async (resolve, reject) => {
-        const mail = {
-            to: email,
-            subject: subject,
-            html: content
-        }
+    const mail = {
+        to: email,
+        subject,
+        html: content
+    }
 
-        if (!Array.isArray(config.email) || config.email.length === 0) {
-            logger.error('邮件发送失败:未配置可用邮箱账号')
-            return resolve(false)
-        }
+    if (!Array.isArray(config.email) || config.email.length === 0) {
+        logger.error('邮件发送失败:未配置可用邮箱账号')
+        return false
+    }
 
-        // 遍历所有邮箱配置
-        for (let i = 0; i < config.email.length; i++) {
-            const currentConfig = config.email[i];
+    for (let i = 0; i < config.email.length; i++) {
+        const currentConfig = config.email[i]
+        if (!currentConfig?.host || !currentConfig?.user) {
+            logger.error(`邮件发送失败:第 ${i + 1} 个邮箱配置不完整,已跳过`)
+            continue
+        }
 
-            const transporter = nodemailer.createTransport({
+        let transporter
+        try {
+            transporter = nodemailer.createTransport({
                 host: currentConfig.host,
                 port: currentConfig.port,
+                secure: currentConfig.secure === true,
                 auth: {
                     user: currentConfig.user,
                     pass: currentConfig.password
                 }
             })
+        } catch (error) {
+            logger.error(`邮件传输器创建失败 (${currentConfig.user}):${error.stack || error.message || error}`)
+            continue
+        }
 
-            mail.from = currentConfig.from
-
-            try {
-                await transporter.sendMail(mail)
-                logger.info(`${email}邮件发送成功,使用账号: ${currentConfig.user}`)
-                return resolve()
-            } catch (error) {
-                logger.error(`邮件发送失败 (${currentConfig.user}),错误:`, error.stack)
-                if (i === config.email.length - 1) {
-                    // 最后一个配置也失败了
-                    logger.error(`邮件发送失败,所有邮箱配置均不可用`)
-                    return resolve(false)
-                }
-                // 否则继续下一个配置
-            }
+        mail.from = currentConfig.from || currentConfig.user
+
+        try {
+            await transporter.sendMail(mail)
+            logger.info(`${email} 邮件发送成功,使用账号: ${currentConfig.user}`)
+            return true
+        } catch (error) {
+            logger.error(`邮件发送失败 (${currentConfig.user}),错误: ${error.stack || error.message || error}`)
         }
-        return resolve(false)
-    })
+    }
+
+    logger.error('邮件发送失败,所有邮箱配置均不可用')
+    return false
 }
 
 module.exports = sendEmail

+ 2 - 2
plugin/Email/emailLayout.js

@@ -1,11 +1,11 @@
 'use strict'
 
-const BRAND = 'RunForge'
+const BRAND = '沐晨乐跑'
 
 /** 页脚固定说明(分段展示,避免单行过长) */
 const FOOTER_DISCLAIMER = {
     label: '本邮件由系统自动发送,请勿直接回复。',
-    helpPrefix: '如需帮助,请访问 RunForge 网站提交工单,或发送邮件至',
+    helpPrefix: '如需帮助,请访问沐晨乐跑网站提交工单,或发送邮件至',
     helpEmail: 'service@xxoo365.top'
 }
 

+ 38 - 25
plugin/Email/emailTemplate.js

@@ -41,7 +41,7 @@ class emailTemplate {
         const time = this.stramptoTime(new Date().getTime())
         const bodyHtml = [
             pEsc('尊敬的用户:'),
-            pEsc('您的 RunForge 账号绑定邮箱已成功更新。'),
+            pEsc('您的沐晨乐跑账号绑定邮箱已成功更新。'),
             panel(kv('操作时间', time)),
             pEsc('如非本人操作,请尽快联系客服处理。', { last: true })
         ].join('')
@@ -61,7 +61,7 @@ class emailTemplate {
         const time = new Date().getTime()
         const bodyHtml = [
             pEsc(`尊敬的 ${username}:`),
-            pEsc('您已成功注册 RunForge 账号,账号信息如下:'),
+            pEsc('您已成功注册沐晨乐跑账号,账号信息如下:'),
             panel([
                 kv('用户名', username),
                 kv('注册时间', this.stramptoTime(time))
@@ -83,7 +83,7 @@ class emailTemplate {
 
     async updateSuccess(email, data) {
         const autoNote = data.auto_run === 0
-            ? '当前未开启自动乐跑。如需跑步,请登录 RunForge 后手动发起。'
+            ? '当前未开启自动乐跑。如需跑步,请登录沐晨乐跑后手动发起。'
             : '已为您开启自动乐跑,系统将按计划代为完成跑步任务,请留意后续邮件提醒。'
 
         const bodyHtml = [
@@ -96,8 +96,8 @@ class emailTemplate {
                 kv('更新时间', this.stramptoTime(new Date().getTime()))
             ].join('')),
             notice(autoNote, 'neutral'),
-            notice('请在当前登录乐跑账号的设备上使用「智慧体育」小程序;请勿在其他设备登录该小程序,以免登录状态失效并需重新绑定。', 'neutral'),
-            pEsc('如需帮助,请联系 RunForge 客服。', { last: true })
+            notice('请勿在其他设备登录该乐跑账号,以免登录状态失效并需重新绑定。', 'neutral'),
+            pEsc('如需帮助,请联系沐晨乐跑客服。', { last: true })
         ].join('')
 
         await sendEmail(
@@ -113,11 +113,21 @@ class emailTemplate {
     }
 
     async lepaoOver(email, data) {
+        const targetKm = Number(data.target_km ?? data.target_count) || 0
+        const monthKm = Number(data.month_km ?? data.term_num) || 0
+        const progressPanel = targetKm > 0
+            ? panel([
+                kv('本月目标', `${targetKm} km`),
+                kv('本月已完成', `${monthKm} km`)
+            ].join(''))
+            : ''
+
         const bodyHtml = [
             pEsc(`尊敬的 ${data.name}:`),
             pEsc('您设定的乐跑目标已全部完成,系统已为您关闭自动乐跑功能。'),
-            pEsc('感谢您的配合。若仍需跑步,可在 RunForge 中按需重新开启相关功能。'),
-            notice('如有疑问,请通过 RunForge 工单反馈。', 'neutral'),
+            progressPanel,
+            pEsc('感谢您的配合。若仍需跑步,可在沐晨乐跑中按需重新开启相关功能。'),
+            notice('如有疑问,请通过沐晨乐跑工单反馈。', 'neutral'),
             pEsc('祝您学习生活愉快。', { last: true })
         ].join('')
 
@@ -134,11 +144,12 @@ class emailTemplate {
     }
 
     async lepaoSuccess(email, data) {
-        const target_count = Number(data.target_count) || 0
-        const total_num = Number(data.total_num) || 0
+        const targetKm = Number(data.target_km ?? data.target_count) || 0
+        const monthKm = Number(data.month_km ?? data.term_num) || 0
+        const totalKm = Number(data.total_km ?? data.total_num) || 0
         const timeSec = Number(data.time) || 0
         const distanceKm = Number(data.distance) || 0
-        const passTit = data.pass_tit != null && data.pass_tit !== '' ? data.pass_tit : '—'
+        const runZone = data.run_zone_name || data.pass_tit || '—'
         const paceStr =
             distanceKm > 0 && timeSec > 0
                 ? this.calculatePace(timeSec, distanceKm)
@@ -146,12 +157,14 @@ class emailTemplate {
         const timeStr = timeSec > 0 ? this.formatSecondsToMinSec(timeSec) : '—'
 
         let goalLines = ''
-        if (target_count === 0) {
-            goalLines = kv('累计次数', `${total_num} 次`)
+        if (targetKm === 0) {
+            const cumulative = totalKm || monthKm
+            goalLines = kv('累计里程', cumulative ? `${cumulative} km` : '—')
         } else {
             goalLines = [
-                kv('目标次数', `${target_count} 次`),
-                kv('累计次数', `${total_num} 次`)
+                kv('本月目标', `${targetKm} km`),
+                kv('本月已完成', `${monthKm} km`),
+                kv('累计里程', totalKm ? `${totalKm} km` : '—')
             ].join('')
         }
 
@@ -160,13 +173,13 @@ class emailTemplate {
             pEsc('系统已成功代您完成一次乐跑,摘要如下:'),
             panel([
                 kv('学号', data.account),
-                kv('跑区', passTit),
+                kv('跑区', runZone),
                 kv('用时', timeStr),
                 kv('平均配速', paceStr),
-                kv('距离', `${distanceKm || '—'} km`),
+                kv('距离', distanceKm ? `${distanceKm} km` : '—'),
                 goalLines
             ].join('')),
-            notice('若已开启自动乐跑,请勿在除更新乐跑账号信息以外的其他设备登录「智慧体育」小程序,以免登录状态失效。', 'neutral')
+            notice('若已开启自动乐跑,请勿在其他设备登录该乐跑账号,以免登录状态失效。', 'neutral')
         ].join('')
 
         await sendEmail(
@@ -193,8 +206,8 @@ class emailTemplate {
                 kv('时间', this.stramptoTime(time)),
                 kv('原因', data.reason)
             ].join('')),
-            notice('若为登录失效,请在 RunForge 乐跑登录器中重新完成登录后再试。', 'warning'),
-            pEsc('若问题持续存在,请联系 RunForge 客服并说明学号与失败时间,便于排查。', { last: true })
+            notice('若为登录失效,请在沐晨乐跑登录器中重新完成登录后再试。', 'warning'),
+            pEsc('若问题持续存在,请联系沐晨乐跑客服并说明学号与失败时间,便于排查。', { last: true })
         ].join('')
 
         await sendEmail(
@@ -212,7 +225,7 @@ class emailTemplate {
     async orderNewReply(email, data) {
         const files = Array.isArray(data.files) ? data.files : []
         const filesNote = files.length > 0
-            ? notice('本回复包含附件,请登录 RunForge 官网,在工单详情中查看。', 'neutral')
+            ? notice('本回复包含附件,请登录沐晨乐跑官网,在工单详情中查看。', 'neutral')
             : ''
 
         const bodyHtml = [
@@ -224,7 +237,7 @@ class emailTemplate {
                 kv('回复时间', this.stramptoTime(new Date().getTime()))
             ].join('')),
             filesNote,
-            notice('请登录 RunForge 官网,在工单详情中回复。', 'warning'),
+            notice('请登录沐晨乐跑官网,在工单详情中回复。', 'warning'),
             pEsc('感谢您的理解与配合。', { last: true })
         ].join('')
 
@@ -251,7 +264,7 @@ class emailTemplate {
                 kv('赠送次数', String(data.count)),
                 kv('申请时间', this.stramptoTime(data.createTime))
             ].join('')),
-            notice('请前往 RunForge 管理后台完成审核。', 'warning'),
+            notice('请前往沐晨乐跑管理后台完成审核。', 'warning'),
             pEsc('谢谢。', { last: true })
         ].join('')
 
@@ -309,7 +322,7 @@ class emailTemplate {
                 kv('到账次数', String(data.count)),
                 kv('审核时间', this.stramptoTime(data.reviewTime))
             ].join('')),
-            pEsc('您可在 RunForge 中查看账户余额与明细。', { last: true })
+            pEsc('您可在沐晨乐跑中查看账户余额与明细。', { last: true })
         ].join('')
 
         await sendEmail(
@@ -334,7 +347,7 @@ class emailTemplate {
                 kv('审核时间', this.stramptoTime(data.reviewTime)),
                 kv('拒绝原因', data.rejectReason)
             ].join('')),
-            notice('如有疑问,请通过 RunForge 联系客服处理。', 'neutral'),
+            notice('如有疑问,请通过沐晨乐跑联系客服处理。', 'neutral'),
             pEsc('感谢您的理解。', { last: true })
         ].join('')
 
@@ -371,7 +384,7 @@ class emailTemplate {
                     : '当前余额已低于提醒阈值,建议尽快充值以免影响用电。',
                 'warning'
             ),
-            pEsc('您可在 RunForge 中调整提醒阈值或管理电费订阅。', { last: true })
+            pEsc('您可在沐晨乐跑中调整提醒阈值或管理电费订阅。', { last: true })
         ].join('')
 
         await sendEmail(

+ 0 - 228
plugin/Lepao/generateGyrFromPath.js

@@ -1,228 +0,0 @@
-"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;
-};

+ 0 - 31
plugin/Lepao/toCoordinatePairs.js

@@ -1,31 +0,0 @@
-function parseMaybeJson(value) {
-    if (value == null) return null
-    if (Array.isArray(value)) return value
-    if (typeof value === 'string') {
-        try {
-            return JSON.parse(value)
-        } catch {
-            return null
-        }
-    }
-    return null
-}
-
-/**
- * 将 path_data.data 或 lepao_record.point_data 转为前端使用的 [经度, 纬度] 数组
- */
-function toCoordinatePairs(pathData, pointData) {
-    const path = parseMaybeJson(pathData)
-    if (Array.isArray(path) && path.length > 0 && path[0]?.o != null) {
-        return path.map(point => [point.o, point.a])
-    }
-
-    const points = parseMaybeJson(pointData)
-    if (Array.isArray(points) && points.length > 0) {
-        return points.map(p => [p.longitude ?? p.longtitude, p.latitude])
-    }
-
-    return []
-}
-
-module.exports = { toCoordinatePairs, parseMaybeJson }

+ 0 - 443
plugin/OneBot/OneBotV11.js

@@ -1,443 +0,0 @@
-const path = require('path')
-const WebSocket = require('ws')
-const config = require('../../config.json')
-const Logger = require('../../lib/Logger')
-const db = require('../DataBase/db.js')
-const EmailTemplate = require('../Email/emailTemplate')
-
-const logger = new Logger(path.join(__dirname, '../../logs/OneBotV11.log'), 'INFO')
-let wsClient = null
-let connectingPromise = null
-let reconnectTimer = null
-let wsApiClient = null
-let connectingApiPromise = null
-let apiReconnectTimer = null
-
-function stringifySafe(data) {
-    try {
-        return JSON.stringify(data)
-    } catch (_) {
-        return '[unserializable]'
-    }
-}
-
-function getOneBotConfig() {
-    const onebot = config.onebotv11 || {}
-    return {
-        enabled: onebot.enabled === true,
-        transport: onebot.transport || 'reverse_ws',
-        reverseWsUrl: onebot.reverseWsUrl || '',
-        reverseWsHost: onebot.reverseWsHost || '127.0.0.1',
-        reverseWsPort: Number(onebot.reverseWsPort || 15700),
-        /** AstrBot / aiocqhttp 反向 WS 常见路径为 /ws;根路径 / 常返回 405 */
-        reverseWsPath: onebot.reverseWsPath || '',
-        reverseWsToken: onebot.reverseWsToken || '',
-        callbackToken: onebot.callbackToken || '',
-        ticketSenderNickname: onebot.ticketSenderNickname || '工单系统',
-        selfId: onebot.selfId || '',
-        botName: onebot.botName || '小妍助理',
-        botUuid: onebot.botUuid || 'onebot-v11-xiaoyan-assistant'
-    }
-}
-
-function getReverseWsUrl(onebot) {
-    const defaultPath = '/ws'
-    const explicitPath = onebot.reverseWsPath
-        ? (onebot.reverseWsPath.startsWith('/') ? onebot.reverseWsPath : `/${onebot.reverseWsPath}`)
-        : null
-
-    if (onebot.reverseWsUrl) {
-        try {
-            const u = new URL(onebot.reverseWsUrl)
-            if (explicitPath) {
-                u.pathname = explicitPath
-                return u.href
-            }
-            if (u.pathname && u.pathname !== '/') return u.href
-            u.pathname = defaultPath
-            return u.href
-        } catch (_) {
-            return onebot.reverseWsUrl
-        }
-    }
-
-    const pathPart = explicitPath || defaultPath
-    return `ws://${onebot.reverseWsHost}:${onebot.reverseWsPort}${pathPart}`
-}
-
-function scheduleReconnect() {
-    if (reconnectTimer) return
-    logger.info('OneBot v11 ws 3秒后尝试重连')
-    reconnectTimer = setTimeout(() => {
-        reconnectTimer = null
-        connectReverseWs().catch(err => {
-            logger.error(`OneBot v11 重连失败:${err.message}`)
-        })
-    }, 3000)
-}
-
-function scheduleApiReconnect() {
-    if (apiReconnectTimer) return
-    logger.info('OneBot v11 API ws 3秒后尝试重连')
-    apiReconnectTimer = setTimeout(() => {
-        apiReconnectTimer = null
-        connectApiReverseWs().catch(err => {
-            logger.error(`OneBot v11 API ws 重连失败:${err.message}`)
-        })
-    }, 3000)
-}
-
-async function connectReverseWs() {
-    if (wsClient && wsClient.readyState === WebSocket.OPEN) return wsClient
-    if (connectingPromise) return connectingPromise
-
-    const onebot = getOneBotConfig()
-    if (!onebot.enabled) throw new Error('OneBot v11 未启用')
-    if (onebot.transport !== 'reverse_ws') throw new Error('仅支持 reverse_ws 传输模式')
-
-    const wsUrl = getReverseWsUrl(onebot)
-    logger.info(`OneBot v11 准备连接 reverse ws:${wsUrl}`)
-    if (!onebot.selfId) {
-        throw new Error('OneBot v11 未配置 selfId:握手头 X-Self-ID 必填')
-    }
-    const selfIdStr = String(onebot.selfId).trim()
-    if (!selfIdStr) {
-        throw new Error('OneBot v11 selfId 不能为空')
-    }
-    const headers = {
-        'User-Agent': 'runforge/1.0 (OneBot-v11-reverse-ws-client)',
-        'X-Client-Role': 'Universal',
-        'X-Self-ID': selfIdStr
-    }
-    if (onebot.reverseWsToken) headers['Authorization'] = `Bearer ${onebot.reverseWsToken}`
-
-    connectingPromise = new Promise((resolve, reject) => {
-        const client = new WebSocket(wsUrl, { headers })
-        let opened = false
-
-        const connTimeout = setTimeout(() => {
-            if (!opened) {
-                try { client.terminate() } catch (_) { }
-                reject(new Error(`连接 OneBot v11 超时:${wsUrl}`))
-            }
-        }, 5000)
-
-        client.on('unexpected-response', (_req, res) => {
-            const statusCode = res && res.statusCode
-            const statusMessage = res && res.statusMessage
-            const rh = res && typeof res.getHeaders === 'function' ? res.getHeaders() : {}
-            const rawPreview = res && Array.isArray(res.rawHeaders) ? res.rawHeaders.slice(0, 24) : []
-            logger.error(`OneBot v11 ws unexpected-response: ${statusCode} ${statusMessage}`)
-            logger.error(`OneBot v11 ws response headers: ${stringifySafe({ rh, rawPreview })}`)
-        })
-
-        client.on('open', () => {
-            clearTimeout(connTimeout)
-            opened = true
-            wsClient = client
-            logger.info(`OneBot v11 reverse ws 已连接:${wsUrl},selfId=${onebot.selfId || '未配置'}`)
-            const lifecycleConnect = buildLifecycleConnectEvent(onebot)
-            client.send(JSON.stringify(lifecycleConnect), (err) => {
-                if (err) {
-                    logger.error(`OneBot 生命周期事件发送失败:${err.message}`)
-                    return
-                }
-                logger.info('OneBot 生命周期事件已发送:meta_event.lifecycle.connect')
-            })
-            resolve(client)
-        })
-
-        client.on('error', (err) => {
-            clearTimeout(connTimeout)
-            if (!opened) reject(err)
-            logger.error(`OneBot v11 ws 异常:${err.message}`)
-        })
-
-        client.on('close', () => {
-            wsClient = null
-            connectingPromise = null
-            logger.error('OneBot v11 ws 连接已关闭')
-            if (onebot.enabled) scheduleReconnect()
-        })
-
-        client.on('message', (raw) => {
-            const text = typeof raw === 'string' ? raw : raw.toString('utf8')
-            logger.info(`OneBot v11 收到消息:${text.slice(0, 1000)}`)
-            handleWsIncomingFrame(text, client).catch((err) => {
-                logger.error(`OneBot v11 处理收到消息失败:${err.message}`)
-            })
-        })
-    })
-
-    try {
-        return await connectingPromise
-    } finally {
-        connectingPromise = null
-    }
-}
-
-async function connectApiReverseWs() {
-    if (wsApiClient && wsApiClient.readyState === WebSocket.OPEN) return wsApiClient
-    if (connectingApiPromise) return connectingApiPromise
-
-    const onebot = getOneBotConfig()
-    if (!onebot.enabled) throw new Error('OneBot v11 未启用')
-    if (onebot.transport !== 'reverse_ws') throw new Error('仅支持 reverse_ws 传输模式')
-
-    const wsUrl = getReverseWsUrl(onebot)
-    if (!onebot.selfId) throw new Error('API ws 缺少 selfId')
-    const selfIdStr = String(onebot.selfId).trim()
-    if (!selfIdStr) throw new Error('API ws selfId 不能为空')
-
-    const headers = {
-        'User-Agent': 'runforge/1.0 (OneBot-v11-reverse-ws-api-client)',
-        'X-Client-Role': 'Api',
-        'X-Self-ID': selfIdStr
-    }
-    if (onebot.reverseWsToken) headers['Authorization'] = `Bearer ${onebot.reverseWsToken}`
-
-    connectingApiPromise = new Promise((resolve, reject) => {
-        const client = new WebSocket(wsUrl, { headers })
-        let opened = false
-        const connTimeout = setTimeout(() => {
-            if (!opened) {
-                try { client.terminate() } catch (_) { }
-                reject(new Error(`连接 OneBot v11 API ws 超时:${wsUrl}`))
-            }
-        }, 5000)
-
-        client.on('open', () => {
-            clearTimeout(connTimeout)
-            opened = true
-            wsApiClient = client
-            logger.info(`OneBot v11 API ws 已连接:${wsUrl}`)
-            resolve(client)
-        })
-
-        client.on('error', (err) => {
-            clearTimeout(connTimeout)
-            if (!opened) reject(err)
-            logger.error(`OneBot v11 API ws 异常:${err.message}`)
-        })
-
-        client.on('close', () => {
-            wsApiClient = null
-            connectingApiPromise = null
-            logger.error('OneBot v11 API ws 连接已关闭')
-            if (onebot.enabled) scheduleApiReconnect()
-        })
-
-        client.on('message', (raw) => {
-            const text = typeof raw === 'string' ? raw : raw.toString('utf8')
-            logger.info(`OneBot v11 API ws 收到消息:${text.slice(0, 1000)}`)
-            handleWsIncomingFrame(text, client).catch((err) => {
-                logger.error(`OneBot v11 API ws 消息处理失败:${err.message}`)
-            })
-        })
-    })
-
-    try {
-        return await connectingApiPromise
-    } finally {
-        connectingApiPromise = null
-    }
-}
-
-function buildSyntheticPrivateMessageEvent(text, onebot, orderId) {
-    const now = Math.floor(Date.now() / 1000)
-    const selfId = String(onebot.selfId).trim()
-    const senderIdStr = String(orderId || '').trim()
-    if (!senderIdStr || !/^\d+$/.test(senderIdStr) || senderIdStr === selfId) {
-        throw new Error('orderId 无效,无法作为 OneBot user_id')
-    }
-    // AstrBot aiocqhttp 消息链路要求 message 为消息段数组(字符串会走失败分支)
-    const messageArr = [{ type: 'text', data: { text } }]
-    const messageId = Math.floor(Math.random() * 2000000000) + 1
-    return {
-        time: now,
-        self_id: selfId,
-        post_type: 'message',
-        message_type: 'private',
-        sub_type: 'friend',
-        message_id: messageId,
-        user_id: senderIdStr,
-        message: messageArr,
-        raw_message: text,
-        font: 0,
-        sender: {
-            user_id: senderIdStr,
-            nickname: onebot.ticketSenderNickname || '工单系统',
-            sex: 'unknown',
-            age: 0
-        }
-    }
-}
-
-function buildLifecycleConnectEvent(onebot) {
-    return {
-        time: Math.floor(Date.now() / 1000),
-        self_id: String(onebot.selfId).trim(),
-        post_type: 'meta_event',
-        meta_event_type: 'lifecycle',
-        sub_type: 'connect'
-    }
-}
-
-async function sendImplementationEvent(eventObj) {
-    const client = await connectReverseWs()
-    const line = stringifySafe(eventObj)
-    logger.info(`OneBot v11 上报事件 post_type=${eventObj.post_type} message_type=${eventObj.message_type} len=${line.length}`)
-    return new Promise((resolve, reject) => {
-        client.send(line, (err) => {
-            if (err) {
-                logger.error(`OneBot 事件发送失败:${err.message}`)
-                return reject(err)
-            }
-            logger.info('OneBot v11 事件已写入 WebSocket')
-            resolve(true)
-        })
-    })
-}
-
-function decodeMessageText(message) {
-    if (!message) return ''
-    if (typeof message === 'string') return message
-    if (Array.isArray(message)) {
-        return message.map(seg => {
-            if (typeof seg === 'string') return seg
-            if (seg && seg.type === 'text' && seg.data && typeof seg.data.text === 'string') return seg.data.text
-            return ''
-        }).join('')
-    }
-    return String(message)
-}
-
-async function persistAiReplyAsServerReply(orderId, plainText) {
-    const onebot = getOneBotConfig()
-    const parsedOrderId = Number(orderId)
-    if (!Number.isInteger(parsedOrderId) || parsedOrderId <= 0) return
-
-    const rows = await db.query('SELECT msg, state, email FROM work_order WHERE id = ?', [parsedOrderId])
-    if (!rows || rows.length !== 1 || rows[0].state === 2) return
-
-    const now = Date.now()
-    const msg = rows[0].msg || []
-    msg.push({
-        time: now,
-        content: plainText,
-        files: [],
-        uuid: 'e4fe0277-0b1a-41a1-b25f-8b6e4cec3281',
-        type: 'ai'
-    })
-
-    await db.query('UPDATE work_order SET msg = ?, update_time = ?, state = 3 WHERE id = ?', [msg, now, parsedOrderId])
-
-    if (rows[0].email) {
-        await EmailTemplate.orderNewReply(rows[0].email, { id: parsedOrderId, content: plainText, files: [] })
-    }
-}
-
-async function handleWsIncomingFrame(text, client) {
-    let j
-    try {
-        j = JSON.parse(text)
-    } catch (_) {
-        return
-    }
-    if (j == null || typeof j !== 'object') return
-
-    if (j.action === 'send_private_msg' && j.params) {
-        const plainText = decodeMessageText(j.params.message)
-        logger.info(`OneBot v11 收到中文消息(解码):${plainText}`)
-        await persistAiReplyAsServerReply(j.params.user_id, plainText)
-    }
-
-    /**
-     * 反向 WS 上「应用端 → 实现端」的 API 请求(aiocqhttp 使用 echo: { seq })。
-     * 若不回包,AstrBot 侧 ResultStore 会超时,事件处理卡住,表现为服务端「收不到」工单事件。
-     */
-    if (j.action !== undefined && j.echo !== undefined && typeof j.echo === 'object' && j.echo !== null && j.echo.seq !== undefined) {
-        const resp = {
-            status: 'ok',
-            retcode: 0,
-            data: null,
-            echo: j.echo
-        }
-        try {
-            client.send(JSON.stringify(resp))
-            logger.info(`OneBot v11 已应答应用端 API:action=${j.action} seq=${JSON.stringify(j.echo.seq)}`)
-        } catch (err) {
-            logger.error(`OneBot v11 应答 API 失败:${err.message}`)
-        }
-        return
-    }
-
-    if ('post_type' in j) return
-
-    if (j.echo !== undefined && j.status !== undefined) {
-        if (j.retcode !== 0 && j.retcode !== undefined) {
-            const wording = j.wording || j.message || stringifySafe(j.data)
-            logger.error(`OneBot API 返回失败 echo=${j.echo} retcode=${j.retcode} ${wording}`)
-        } else {
-            logger.info(`OneBot API 成功 echo=${j.echo}`)
-        }
-    }
-}
-
-function buildOrderText(payload) {
-    const {
-        orderId,
-        title,
-        role,
-        content,
-        files = [],
-        senderUuid
-    } = payload
-
-    const fileText = files.length > 0 ? `\n附件:\n${files.join('\n')}` : ''
-    return [
-        `[工单#${orderId}] ${title ? title : '工单消息更新'}`,
-        `发送方:${role === '用户' ? '用户' : '系统'}`,
-        `用户uuid:${senderUuid || '-'}`,
-        '消息内容:',
-        content
-    ].join('\n') + fileText
-}
-
-async function sendOrderMessage(payload) {
-    const onebot = getOneBotConfig()
-    if (!onebot.enabled) return false
-    if (onebot.transport !== 'reverse_ws') {
-        logger.error('OneBot v11 传输模式错误,仅支持 reverse_ws')
-        return false
-    }
-
-    const text = buildOrderText(payload)
-
-    const eventObj = buildSyntheticPrivateMessageEvent(text, onebot, payload.orderId)
-    await sendImplementationEvent(eventObj)
-    logger.info(`OneBot 工单事件已上报:orderId=${payload.orderId}, userId=${eventObj.user_id}, messageId=${eventObj.message_id}`)
-    return true
-}
-
-async function initOneBotWs() {
-    const onebot = getOneBotConfig()
-    if (!onebot.enabled) {
-        logger.info('OneBot v11 未启用,跳过 ws 初始化')
-        return false
-    }
-    if (onebot.transport !== 'reverse_ws') {
-        logger.error('OneBot v11 初始化失败:传输模式不是 reverse_ws')
-        return false
-    }
-
-    await connectReverseWs()
-    await connectApiReverseWs()
-    return true
-}
-
-module.exports = {
-    sendOrderMessage,
-    getOneBotConfig,
-    initOneBotWs
-}

+ 21 - 0
plugin/jkes/autoRunAccountOptions.js

@@ -182,10 +182,31 @@ function buildAutoRunTaskExtrasFromAccountRow(row) {
     }
 }
 
+/** 合并月度计划距离与账号随机区间,双倍日由 monthPolicy 决定 */
+function buildAutoRunTaskFromPlan(accountRow, plan) {
+    const cfg = getJkesSettings()
+    const maxCap = Math.max(2, Number(cfg.autoSingleRunMaxKm) || 10)
+    const step = Number(cfg.autoDistanceStepKm) || 0.1
+    const extras = buildAutoRunTaskExtrasFromAccountRow(accountRow)
+    const plannedKm = Number(plan?.targetKm) || 1
+
+    let targetKm = Math.min(maxCap, Math.max(plannedKm, extras.targetKm))
+    targetKm = roundToStepKm(targetKm, step)
+    targetKm = Math.min(maxCap, Math.max(1, Math.round(targetKm * 100) / 100))
+
+    return {
+        targetKm,
+        autoDoubleSlot: !!plan?.autoDoubleSlot,
+        paceRandomMinSecPerKm: extras.paceRandomMinSecPerKm,
+        paceRandomMaxSecPerKm: extras.paceRandomMaxSecPerKm
+    }
+}
+
 module.exports = {
     DEFAULT_AUTO_RUN_DISTANCE_KM,
     validateAutoRunPresetForSave,
     buildAutoRunTaskExtrasFromAccountRow,
+    buildAutoRunTaskFromPlan,
     randomAutoRunDistanceKmBetween,
     normalizePaceRangeSecPerKm
 }

+ 15 - 3
plugin/jkes/formatRecordRow.js

@@ -11,9 +11,10 @@ async function getLepaoSchedule(recordId) {
  */
 async function enrichLepaoRecordRow(row) {
     let out = { ...row }
-    if (row.id && row.state === 0) {
+    if (row.id != null && Number(row.state) === 0) {
         const schedule = await getLepaoSchedule(row.id)
-        out['lepao_schedule'] = schedule
+        out.lepao_schedule = schedule
+        out.schedule = schedule
     }
     let jkes = null
     try {
@@ -41,4 +42,15 @@ async function enrichLepaoRecordRow(row) {
     return out
 }
 
-module.exports = { enrichLepaoRecordRow }
+function stripInternalRecordId(row) {
+    if (!row || typeof row !== 'object') return row
+    const { id, ...rest } = row
+    return rest
+}
+
+async function enrichLepaoRecordRowForApi(row) {
+    const enriched = await enrichLepaoRecordRow(row)
+    return stripInternalRecordId(enriched)
+}
+
+module.exports = { enrichLepaoRecordRow, enrichLepaoRecordRowForApi, stripInternalRecordId }

+ 44 - 0
plugin/jkes/jkesHttp.js

@@ -0,0 +1,44 @@
+const { forwardRequest, briefUrlPath } = require('../../lib/ProxyForwardClient')
+
+function jkesHttpLogLabel(traceId, taskId) {
+    let s = ''
+    if (traceId) s += `[${traceId}] `
+    s += '[jkesHttp]'
+    if (taskId) s += ` [${taskId}]`
+    return s
+}
+
+/**
+ * 对 JKES API 的 POST,经 RunForge-ProxyServer 转发(失败时由代理服务自动直连回退)。
+ */
+async function postJkes(url, data, options = {}) {
+    const {
+        headers = {},
+        timeout = 30000,
+        logger = null,
+        outboundMode = 'auto',
+        taskId = null,
+        traceId = null,
+        validateStatus
+    } = options
+
+    const logPrefix = jkesHttpLogLabel(traceId, taskId)
+    logger?.info?.(`${logPrefix} POST ${briefUrlPath(url)} mode=${outboundMode}`)
+
+    return forwardRequest({
+        method: 'post',
+        url,
+        data,
+        headers,
+        timeout,
+        outboundMode,
+        validateStatus,
+        logger,
+        logPrefix,
+        scene: 'jkes'
+    })
+}
+
+module.exports = {
+    postJkes
+}

+ 0 - 1
plugin/jkes/jkesSettings.js

@@ -7,7 +7,6 @@ const defaultJkes = {
     referer: 'https://servicewechat.com/wxbd35ecd93e780082/11/page-frame.html',
     tlsRejectUnauthorized: true,
     requestTimeoutMs: 30000,
-    useSystemProxy: false,
     gpsAltitude: 269.75,
     gpsDefaultAccuracy: 6,
     /** 自动乐跑随机配速:每公里秒数 */

+ 13 - 4
plugin/jkes/monthPolicy.js

@@ -104,8 +104,16 @@ async function refreshMonthKmFromApi(account, token, d = new Date()) {
      * 仅在差距较小(<=5km)时才允许用本地值兜底。
      */
     const prevKm = Number(prev.km) || 0
-    const drift = prevKm - apiKm
-    const km = drift > 0 && drift <= 5 ? prevKm : apiKm
+    const apiKmNum = Number(apiKm) || 0
+    let km = apiKmNum
+    if (apiKmNum <= 0 && prevKm > 0) {
+        km = prevKm
+    } else {
+        const drift = prevKm - apiKmNum
+        if (drift > 0 && drift <= 5) {
+            km = prevKm
+        }
+    }
     await writeState(account, { km, doubles: prev.doubles }, d)
     return km
 }
@@ -140,7 +148,7 @@ function roundToStep(value, step = 0.5) {
 
 /**
  * 自动任务:在 auto_day 且当日可跑时投递;2 公里日最多占满 DOUBLE_RUNS_CAP 次,之后均为 1 公里。
- * 不因已达 30km 而停止(30km 为最低要求参考,非额度)
+ * stopAfterMinimum=true 时,达到 monthTargetKm 后不再自动跑
  */
 async function planJkesAutoRun(account, autoDayRaw, token, options = {}, d = new Date()) {
     const cfg = getJkesSettings()
@@ -180,7 +188,8 @@ async function planJkesAutoRun(account, autoDayRaw, token, options = {}, d = new
         targetKm = Math.max(targetKm, 2.5)
     }
     targetKm = Math.min(maxAutoSingleKm, Math.max(1, Math.round(targetKm * 100) / 100))
-    return { run: true, targetKm, monthTargetKm }
+    const autoDoubleSlot = mustDoubleToday || plannedDoubleToday
+    return { run: true, targetKm, monthTargetKm, autoDoubleSlot }
 }
 
 /**

+ 1 - 1
plugin/jkes/paceUtils.js

@@ -51,7 +51,7 @@ function clampManualPaceSec(sec) {
 
 /** 自动乐跑:闭区间随机秒/公里 */
 function randomPaceSecPerKm(minSec, maxSec) {
-    const lo = Math.max(120, Math.min(minSec, maxSec))
+    const lo = Math.max(MANUAL_PACE_MIN_SEC, Math.min(minSec, maxSec))
     const hi = Math.max(lo, Math.max(minSec, maxSec))
     return Math.round(lo + Math.random() * (hi - lo))
 }

+ 72 - 0
plugin/jkes/prepareAutoRunTask.js

@@ -0,0 +1,72 @@
+const Redis = require('../DataBase/Redis.js')
+const jkesRedisKeys = require('./redisKeys.js')
+const { planJkesAutoRun, parseAutoDay } = require('./monthPolicy.js')
+const { buildAutoRunTaskFromPlan } = require('./autoRunAccountOptions.js')
+
+async function checkAutoRunBlockedToday(studentNum) {
+    const isSuccess = await Redis.get(jkesRedisKeys.lepaoSuccess(studentNum))
+    if (isSuccess) {
+        return { blocked: true, reason: '当天已乐跑成功' }
+    }
+    const inProgress = await Redis.get(jkesRedisKeys.lepaoProgress(studentNum))
+    if (inProgress) {
+        return { blocked: true, reason: '乐跑进行中' }
+    }
+    return { blocked: false }
+}
+
+function isAutoRunDayToday(autoDayRaw) {
+    const days = parseAutoDay(autoDayRaw)
+    return days.length === 0 || days.includes(new Date().getDay())
+}
+
+/**
+ * 校验当日是否应投递自动乐跑,并合并月度计划距离与双倍日策略。
+ * @param {object} accountRow lepao_account 行(含 student_num / auto_day / token / target_count 等)
+ */
+async function prepareAutoRunTask(accountRow, options = {}) {
+    const studentNum = accountRow?.student_num
+    if (!studentNum) {
+        return { run: false, reason: '缺少学号' }
+    }
+
+    const blocked = await checkAutoRunBlockedToday(studentNum)
+    if (blocked.blocked) {
+        return { run: false, reason: blocked.reason }
+    }
+
+    const plan = await planJkesAutoRun(studentNum, accountRow.auto_day, accountRow.token, {
+        monthTargetKm: accountRow.target_count,
+        stopAfterMinimum: options.stopAfterMinimum !== false
+    })
+    if (!plan.run) {
+        return plan
+    }
+
+    const task = buildAutoRunTaskFromPlan(accountRow, plan)
+    return { run: true, plan, task }
+}
+
+function buildStartRunPayload(studentNum, task, options = {}) {
+    const taskId = options.taskId || `${options.prefix || 'lepao:auto'}:${Date.now()}:${studentNum}`
+    return {
+        id: taskId,
+        type: 'lepao.startRun',
+        data: {
+            taskId,
+            account: studentNum,
+            targetKm: task.targetKm,
+            autoDoubleSlot: task.autoDoubleSlot,
+            paceRandomMinSecPerKm: task.paceRandomMinSecPerKm,
+            paceRandomMaxSecPerKm: task.paceRandomMaxSecPerKm
+        },
+        retry: 0
+    }
+}
+
+module.exports = {
+    checkAutoRunBlockedToday,
+    isAutoRunDayToday,
+    prepareAutoRunTask,
+    buildStartRunPayload
+}

+ 9 - 30
plugin/jkes/request.js

@@ -1,24 +1,10 @@
-const axios = require('axios')
-const https = require('https')
 const path = require('path')
 const Logger = require('../../lib/Logger')
 const { getJkesSettings, normalizeApiBase } = require('./jkesSettings')
+const { postJkes } = require('./jkesHttp')
 
 const logger = new Logger(path.join(__dirname, '../../logs/JKES.log'), 'INFO')
 
-let jkesHttpsAgent
-function getJkesHttpsAgent() {
-    if (!jkesHttpsAgent) {
-        const s = getJkesSettings()
-        jkesHttpsAgent = new https.Agent({
-            keepAlive: true,
-            minVersion: 'TLSv1.2',
-            rejectUnauthorized: s.tlsRejectUnauthorized !== false
-        })
-    }
-    return jkesHttpsAgent
-}
-
 function buildJkesHeaders(token) {
     const tokenClean = String(token ?? '').trim()
     const s = getJkesSettings()
@@ -55,8 +41,10 @@ function makeJkesLoginExpiredError(payload) {
  * @param {string} url 如 /sys/user/getMyInfo
  * @param {object} [data] POST body
  * @param {string} token
+ * @param {object} [options]
+ * @param {'auto'|'direct'|'proxy'} [options.outboundMode='auto']
  */
-async function jkesRequest(url, data, token) {
+async function jkesRequest(url, data, token, options = {}) {
     const tokenClean = String(token ?? '').trim()
     if (!tokenClean) {
         logger.error('[JKES] 缺少 token')
@@ -67,6 +55,7 @@ async function jkesRequest(url, data, token) {
     const pathPart = url.startsWith('/') ? url : `/${url}`
     const fullUrl = `${normalizeApiBase(s.apiBase)}${pathPart}`
     const body = data === undefined || data === null ? {} : data
+    const { outboundMode = 'auto' } = options
 
     let parsed
     try {
@@ -82,23 +71,13 @@ async function jkesRequest(url, data, token) {
 
     try {
         logger.info(`[JKES] POST ${pathPart}`)
-        const axiosOpts = {
+        const res = await postJkes(fullUrl, body, {
             headers: buildJkesHeaders(tokenClean),
             timeout: Number(s.requestTimeoutMs) || 30000,
             validateStatus: () => true,
-            httpsAgent: getJkesHttpsAgent(),
-            beforeRedirect: (options) => {
-                if (options.protocol !== 'https:') {
-                    logger.error(`[JKES] 拒绝跟随非 HTTPS 重定向: ${options.href}`)
-                    throw new Error('JKES 重定向目标必须为 https')
-                }
-            }
-        }
-        if (!s.useSystemProxy) {
-            axiosOpts.proxy = false
-        }
-
-        const res = await axios.post(fullUrl, body, axiosOpts)
+            outboundMode,
+            logger
+        })
 
         const payload = res.data
         const payloadPreview =

Some files were not shown because too many files changed in this diff