Browse Source

✨ feat: 新增次数记录等功能

Pchen0 1 week ago
parent
commit
de49c0e844

+ 25 - 0
apis/Goods/Admin/ApproveSendCountRequest.js

@@ -3,6 +3,7 @@ const db = require("../../../plugin/DataBase/db")
 const AccessControl = require("../../../lib/AccessControl")
 const AccessControl = require("../../../lib/AccessControl")
 const { BaseStdResponse } = require("../../../BaseStdResponse")
 const { BaseStdResponse } = require("../../../BaseStdResponse")
 const EmailTemplate = require("../../../plugin/Email/emailTemplate")
 const EmailTemplate = require("../../../plugin/Email/emailTemplate")
+const { insertLedgerRecord } = require("../../../lib/Lepao/CountLedger")
 
 
 class ApproveSendCountRequest extends API {
 class ApproveSendCountRequest extends API {
     constructor() {
     constructor() {
@@ -51,6 +52,17 @@ class ApproveSendCountRequest extends API {
                 return res.json({ ...BaseStdResponse.ERR, msg: "该申请已审核,请刷新后重试!" })
                 return res.json({ ...BaseStdResponse.ERR, msg: "该申请已审核,请刷新后重试!" })
             }
             }
 
 
+            const [receiverRows] = await conn.execute(
+                "SELECT uuid, lepao_count FROM users WHERE id = ? FOR UPDATE",
+                [request.receiver_user_id]
+            )
+            if (!receiverRows || receiverRows.length !== 1) {
+                await conn.rollback()
+                return res.json({ ...BaseStdResponse.ERR, msg: "接收用户不存在,审核通过失败!" })
+            }
+            const receiverUuid = receiverRows[0].uuid
+            const beforeCount = Number(receiverRows[0].lepao_count || 0)
+
             const [incResult] = await conn.execute(
             const [incResult] = await conn.execute(
                 "UPDATE users SET lepao_count = lepao_count + ? WHERE id = ?",
                 "UPDATE users SET lepao_count = lepao_count + ? WHERE id = ?",
                 [request.count, request.receiver_user_id]
                 [request.count, request.receiver_user_id]
@@ -71,6 +83,17 @@ class ApproveSendCountRequest extends API {
                 return res.json({ ...BaseStdResponse.ERR, msg: "更新审核状态失败,请稍后再试!" })
                 return res.json({ ...BaseStdResponse.ERR, msg: "更新审核状态失败,请稍后再试!" })
             }
             }
 
 
+            await insertLedgerRecord({
+                executor: conn,
+                userUuid: receiverUuid,
+                delta: Number(request.count || 0),
+                balanceBefore: beforeCount,
+                balanceAfter: beforeCount + Number(request.count || 0),
+                bizType: 'gift_receive',
+                bizId: `send_request:${id}`,
+                operatorUuid: uuid
+            })
+
             await conn.commit()
             await conn.commit()
             const requestId = request.id
             const requestId = request.id
             const reviewTime = new Date().getTime()
             const reviewTime = new Date().getTime()
@@ -109,6 +132,8 @@ class ApproveSendCountRequest extends API {
             try { await conn.rollback() } catch (_) { }
             try { await conn.rollback() } catch (_) { }
             this.logger.error(`审核通过赠送申请失败!${err.message || "未知错误"}`)
             this.logger.error(`审核通过赠送申请失败!${err.message || "未知错误"}`)
             return res.json({ ...BaseStdResponse.ERR, msg: "审核通过失败,请稍后再试!" })
             return res.json({ ...BaseStdResponse.ERR, msg: "审核通过失败,请稍后再试!" })
+        } finally {
+            conn.release()
         }
         }
     }
     }
 }
 }

+ 25 - 0
apis/Goods/Admin/RejectSendCountRequest.js

@@ -3,6 +3,7 @@ const db = require("../../../plugin/DataBase/db")
 const AccessControl = require("../../../lib/AccessControl")
 const AccessControl = require("../../../lib/AccessControl")
 const { BaseStdResponse } = require("../../../BaseStdResponse")
 const { BaseStdResponse } = require("../../../BaseStdResponse")
 const EmailTemplate = require("../../../plugin/Email/emailTemplate")
 const EmailTemplate = require("../../../plugin/Email/emailTemplate")
+const { insertLedgerRecord } = require("../../../lib/Lepao/CountLedger")
 
 
 class RejectSendCountRequest extends API {
 class RejectSendCountRequest extends API {
     constructor() {
     constructor() {
@@ -55,6 +56,16 @@ class RejectSendCountRequest extends API {
                 return res.json({ ...BaseStdResponse.ERR, msg: "该申请已审核,请刷新后重试!" })
                 return res.json({ ...BaseStdResponse.ERR, msg: "该申请已审核,请刷新后重试!" })
             }
             }
 
 
+            const [senderRows] = await conn.execute(
+                "SELECT lepao_count FROM users WHERE uuid = ? FOR UPDATE",
+                [request.sender_uuid]
+            )
+            if (!senderRows || senderRows.length !== 1) {
+                await conn.rollback()
+                return res.json({ ...BaseStdResponse.ERR, msg: "赠送人不存在,退回次数失败!" })
+            }
+            const beforeCount = Number(senderRows[0].lepao_count || 0)
+
             const [refundResult] = await conn.execute(
             const [refundResult] = await conn.execute(
                 "UPDATE users SET lepao_count = lepao_count + ? WHERE uuid = ?",
                 "UPDATE users SET lepao_count = lepao_count + ? WHERE uuid = ?",
                 [request.count, request.sender_uuid]
                 [request.count, request.sender_uuid]
@@ -75,6 +86,18 @@ class RejectSendCountRequest extends API {
                 return res.json({ ...BaseStdResponse.ERR, msg: "更新审核状态失败,请稍后再试!" })
                 return res.json({ ...BaseStdResponse.ERR, msg: "更新审核状态失败,请稍后再试!" })
             }
             }
 
 
+            await insertLedgerRecord({
+                executor: conn,
+                userUuid: request.sender_uuid,
+                delta: Number(request.count || 0),
+                balanceBefore: beforeCount,
+                balanceAfter: beforeCount + Number(request.count || 0),
+                bizType: 'gift_send_refund',
+                bizId: `send_request:${id}`,
+                operatorUuid: uuid,
+                remark: reject_reason || ''
+            })
+
             await conn.commit()
             await conn.commit()
             const requestId = request.id
             const requestId = request.id
             const reviewTime = new Date().getTime()
             const reviewTime = new Date().getTime()
@@ -114,6 +137,8 @@ class RejectSendCountRequest extends API {
             try { await conn.rollback() } catch (_) { }
             try { await conn.rollback() } catch (_) { }
             this.logger.error(`拒绝赠送申请失败!${err.message || "未知错误"}`)
             this.logger.error(`拒绝赠送申请失败!${err.message || "未知错误"}`)
             return res.json({ ...BaseStdResponse.ERR, msg: "拒绝申请失败,请稍后再试!" })
             return res.json({ ...BaseStdResponse.ERR, msg: "拒绝申请失败,请稍后再试!" })
+        } finally {
+            conn.release()
         }
         }
     }
     }
 }
 }

+ 13 - 1
apis/Goods/SendCount.js

@@ -3,6 +3,7 @@ const db = require("../../plugin/DataBase/db")
 const AccessControl = require("../../lib/AccessControl")
 const AccessControl = require("../../lib/AccessControl")
 const { BaseStdResponse } = require("../../BaseStdResponse")
 const { BaseStdResponse } = require("../../BaseStdResponse")
 const EmailTemplate = require("../../plugin/Email/emailTemplate")
 const EmailTemplate = require("../../plugin/Email/emailTemplate")
+const { insertLedgerRecord } = require("../../lib/Lepao/CountLedger")
 
 
 class SendCount extends API {
 class SendCount extends API {
     constructor() {
     constructor() {
@@ -72,8 +73,17 @@ class SendCount extends API {
                 return res.json({ ...BaseStdResponse.ERR, msg: "提交赠送审核失败,请稍后再试!" })
                 return res.json({ ...BaseStdResponse.ERR, msg: "提交赠送审核失败,请稍后再试!" })
             }
             }
 
 
-            await conn.commit()
             const requestId = insertResult.insertId
             const requestId = insertResult.insertId
+            await insertLedgerRecord({
+                executor: conn,
+                userUuid: uuid,
+                delta: -count,
+                balanceBefore: Number(senderRows[0].lepao_count || 0),
+                balanceAfter: Number(senderRows[0].lepao_count || 0) - count,
+                bizType: 'gift_send_lock',
+                bizId: `send_request:${requestId}`
+            })
+            await conn.commit()
             const createTime = new Date().getTime()
             const createTime = new Date().getTime()
 
 
             // 非阻塞通知管理员,不影响主业务流程
             // 非阻塞通知管理员,不影响主业务流程
@@ -116,6 +126,8 @@ class SendCount extends API {
                 ...BaseStdResponse.ERR,
                 ...BaseStdResponse.ERR,
                 msg: `赠送次数失败,请稍后再试!`
                 msg: `赠送次数失败,请稍后再试!`
             })
             })
+        } finally {
+            conn.release()
         }
         }
     }
     }
 }
 }

+ 51 - 3
apis/Lepao/Account/Admin/ChangeLepaoCount.js

@@ -2,6 +2,7 @@ const API = require("../../../../lib/API");
 const db = require("../../../../plugin/DataBase/db");
 const db = require("../../../../plugin/DataBase/db");
 const AccessControl = require("../../../../lib/AccessControl");
 const AccessControl = require("../../../../lib/AccessControl");
 const { BaseStdResponse } = require("../../../../BaseStdResponse");
 const { BaseStdResponse } = require("../../../../BaseStdResponse");
+const { insertLedgerRecord } = require("../../../../lib/Lepao/CountLedger");
 
 
 class ChangeLepaoCount extends API {
 class ChangeLepaoCount extends API {
     constructor() {
     constructor() {
@@ -37,12 +38,59 @@ class ChangeLepaoCount extends API {
                 ...BaseStdResponse.PERMISSION_DENIED
                 ...BaseStdResponse.PERMISSION_DENIED
             })
             })
 
 
-        let sql = 'UPDATE users SET lepao_count = ? WHERE uuid = ?'
-        let r = await db.query(sql, [lepao_count, userid])
-        if (!r || r.affectedRows !== 1)
+        const conn = await db.connect()
+        try {
+            await conn.beginTransaction()
+            const [userRows] = await conn.execute(
+                'SELECT lepao_count FROM users WHERE uuid = ? FOR UPDATE',
+                [userid]
+            )
+            if (!userRows || userRows.length !== 1) {
+                await conn.rollback()
+                return res.json({
+                    ...BaseStdResponse.MISSING_FILE,
+                    msg: '未找到用户信息'
+                })
+            }
+
+            const beforeCount = Number(userRows[0].lepao_count || 0)
+            const targetCount = Number(lepao_count)
+            const delta = targetCount - beforeCount
+            const [r] = await conn.execute(
+                'UPDATE users SET lepao_count = ? WHERE uuid = ?',
+                [targetCount, userid]
+            )
+            if (!r || r.affectedRows !== 1) {
+                await conn.rollback()
+                return res.json({
+                    ...BaseStdResponse.DATABASE_ERR
+                })
+            }
+
+            if (delta !== 0) {
+                await insertLedgerRecord({
+                    executor: conn,
+                    userUuid: userid,
+                    delta,
+                    balanceBefore: beforeCount,
+                    balanceAfter: targetCount,
+                    bizType: 'admin_adjust',
+                    bizId: `admin_adjust:${Date.now()}:${userid}`,
+                    operatorUuid: uuid,
+                    remark: '管理员手工调整次数'
+                })
+            }
+
+            await conn.commit()
+        } catch (error) {
+            try { await conn.rollback() } catch (_) { }
+            this.logger.error(`管理员调整次数失败: ${error.stack || error}`)
             return res.json({
             return res.json({
                 ...BaseStdResponse.DATABASE_ERR
                 ...BaseStdResponse.DATABASE_ERR
             })
             })
+        } finally {
+            conn.release()
+        }
 
 
         res.json({
         res.json({
             ...BaseStdResponse.OK
             ...BaseStdResponse.OK

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

@@ -0,0 +1,62 @@
+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
+            })
+        }
+
+        const permission = await AccessControl.getPermission(uuid)
+        if (!permission.includes('admin') && !permission.includes('service')) {
+            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

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

@@ -6,6 +6,7 @@ const mq = require('../../../../plugin/mq')
 const { mq: mqName } = require('../../../../plugin/mq/mqPrefix')
 const { mq: mqName } = require('../../../../plugin/mq/mqPrefix')
 const { BaseStdResponse } = require("../../../../BaseStdResponse.js")
 const { BaseStdResponse } = require("../../../../BaseStdResponse.js")
 const { dataDecrypt } = require('../../../../plugin/Lepao/Crypto')
 const { dataDecrypt } = require('../../../../plugin/Lepao/Crypto')
+const { enqueueLepaoSyncAccountInfo } = require('../../../../plugin/mq/enqueueLepaoSyncAccountInfo')
 
 
 // 客户端上传数据接口
 // 客户端上传数据接口
 class UpdateAccount extends API {
 class UpdateAccount extends API {
@@ -137,6 +138,7 @@ class UpdateAccount extends API {
                 if (findRows[0].auto_run === 1 && Array.isArray(findRows[0].auto_day) && findRows[0].auto_day.includes(new Date().getDay())) {
                 if (findRows[0].auto_run === 1 && Array.isArray(findRows[0].auto_day) && findRows[0].auto_day.includes(new Date().getDay())) {
                     enqueueLepaoStartRun(student_num, this.logger)
                     enqueueLepaoStartRun(student_num, this.logger)
                 }
                 }
+                enqueueLepaoSyncAccountInfo(student_num, this.logger)
             }
             }
 
 
             // 获取新加账号中存在的路径
             // 获取新加账号中存在的路径

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

@@ -6,6 +6,7 @@ const mq = require('../../../../plugin/mq')
 const { mq: mqName } = require('../../../../plugin/mq/mqPrefix')
 const { mq: mqName } = require('../../../../plugin/mq/mqPrefix')
 const { BaseStdResponse } = require("../../../../BaseStdResponse.js")
 const { BaseStdResponse } = require("../../../../BaseStdResponse.js")
 const { dataDecrypt } = require('../../../../plugin/Lepao/Crypto')
 const { dataDecrypt } = require('../../../../plugin/Lepao/Crypto')
+const { enqueueLepaoSyncAccountInfo } = require('../../../../plugin/mq/enqueueLepaoSyncAccountInfo')
 
 
 // 客户端上传数据接口
 // 客户端上传数据接口
 class UpdateAccountAndroidApp extends API {
 class UpdateAccountAndroidApp extends API {
@@ -139,6 +140,7 @@ class UpdateAccountAndroidApp extends API {
                 if (findRows[0].auto_run === 1 && Array.isArray(findRows[0].auto_day) && findRows[0].auto_day.includes(new Date().getDay())) {
                 if (findRows[0].auto_run === 1 && Array.isArray(findRows[0].auto_day) && findRows[0].auto_day.includes(new Date().getDay())) {
                     enqueueLepaoStartRun(student_num, this.logger)
                     enqueueLepaoStartRun(student_num, this.logger)
                 }
                 }
+                enqueueLepaoSyncAccountInfo(student_num, this.logger)
             }
             }
 
 
             // 获取新加账号中存在的路径
             // 获取新加账号中存在的路径

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

@@ -6,6 +6,7 @@ const mq = require('../../../../plugin/mq')
 const { mq: mqName } = require('../../../../plugin/mq/mqPrefix')
 const { mq: mqName } = require('../../../../plugin/mq/mqPrefix')
 const { BaseStdResponse } = require("../../../../BaseStdResponse.js")
 const { BaseStdResponse } = require("../../../../BaseStdResponse.js")
 const { dataDecrypt } = require('../../../../plugin/Lepao/Crypto')
 const { dataDecrypt } = require('../../../../plugin/Lepao/Crypto')
+const { enqueueLepaoSyncAccountInfo } = require('../../../../plugin/mq/enqueueLepaoSyncAccountInfo')
 
 
 // 客户端上传数据接口
 // 客户端上传数据接口
 class UpdateAccountiPhone extends API {
 class UpdateAccountiPhone extends API {
@@ -141,6 +142,7 @@ class UpdateAccountiPhone extends API {
                 if (findRows[0].auto_run === 1 && Array.isArray(findRows[0].auto_day) && findRows[0].auto_day.includes(new Date().getDay())) {
                 if (findRows[0].auto_run === 1 && Array.isArray(findRows[0].auto_day) && findRows[0].auto_day.includes(new Date().getDay())) {
                     enqueueLepaoStartRun(student_num, this.logger)
                     enqueueLepaoStartRun(student_num, this.logger)
                 }
                 }
+                enqueueLepaoSyncAccountInfo(student_num, this.logger)
             }
             }
 
 
             // 获取新加账号中存在的路径
             // 获取新加账号中存在的路径

+ 9 - 88
apis/Lepao/Account/UpdateSelfAccount.js

@@ -1,10 +1,7 @@
 const API = require("../../../lib/API.js")
 const API = require("../../../lib/API.js")
-const db = require("../../../plugin/DataBase/db.js")
-const axios = require("axios")
 const AccessControl = require("../../../lib/AccessControl.js")
 const AccessControl = require("../../../lib/AccessControl.js")
 const { BaseStdResponse } = require("../../../BaseStdResponse.js")
 const { BaseStdResponse } = require("../../../BaseStdResponse.js")
-const { dataEncrypt, dataDecrypt, dataSign } = require("../../../plugin/Lepao/Crypto")
-const { URLSearchParams } = require("url")
+const { syncAccountInfo } = require("../../../lib/Lepao/syncAccountInfo")
 
 
 class UpdateSelfAccount extends API {
 class UpdateSelfAccount extends API {
     constructor() {
     constructor() {
@@ -30,97 +27,21 @@ class UpdateSelfAccount extends API {
         }
         }
 
 
         try {
         try {
-            const rows = await db.query(
-                'SELECT uid, token, school_id, userAgent, state FROM lepao_account WHERE student_num = ? AND create_user = ?',
-                [student_num, uuid]
-            )
-            if (!rows || rows.length !== 1) {
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    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,
-                card_id: student_num,
-                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 || 'Mozilla/5.0 (Linux; Android 16; 2211133C Build/BP2A.250605.031.A3; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/138.0.7204.180 Mobile Safari/537.36 XWEB/1380347 MMWEBSDK/20250202 MMWEBID/1020 wxwork/5.0.6.66174 MicroMessenger/8.0.28.48(0x28001c30) MiniProgramEnv/android Luggage/3.0.2.95ef3f83 NetType/WIFI Language/zh_CN ABI/arm64'
-            }
-
-            const apiRes = await axios.post(
-                'https://lepao.ctbu.edu.cn/v3/api.php/Run2/beforeRunV260',
-                form,
-                { headers, proxy: false }
-            )
-
-            let result = apiRes.data
-            if (result?.data && result?.is_encrypt === 1) {
-                result.data = JSON.parse(dataDecrypt(result.data))
-            }
-
-            const info = result?.info || result?.msg || '系统繁忙,请稍后再试'
-            const updateTime = Date.now()
-
-            // 登录失效:更新 state=0
-            if (String(info).includes('重新登录') || Number(result?.status) === 101) {
-                await db.query('UPDATE lepao_account SET state = 0, update_time = ? WHERE student_num = ? AND create_user = ?', [updateTime, student_num, uuid])
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: info
-                })
-            }
-
-            if (!result || Number(result.status) !== 1 || !result.data) {
+            const syncResult = await syncAccountInfo({
+                studentNum: student_num,
+                createUser: uuid,
+                logger: this.logger
+            })
+            if (!syncResult.ok) {
                 return res.json({
                 return res.json({
                     ...BaseStdResponse.ERR,
                     ...BaseStdResponse.ERR,
-                    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 student_num = ? AND create_user = ?',
-                [term_num, total_num, updateTime, student_num, uuid]
-            )
-            if (!updateRows || updateRows.affectedRows !== 1) {
-                return res.json({
-                    ...BaseStdResponse.DATABASE_ERR
+                    msg: syncResult.msg || '同步失败,请稍后再试'
                 })
                 })
             }
             }
 
 
             return res.json({
             return res.json({
                 ...BaseStdResponse.OK,
                 ...BaseStdResponse.OK,
-                data: {
-                    term_num,
-                    total_num,
-                    state: 1
-                }
+                data: syncResult.data
             })
             })
         } catch (error) {
         } catch (error) {
             this.logger.error(`用户自助同步乐跑账号失败: ${error.stack || error}`)
             this.logger.error(`用户自助同步乐跑账号失败: ${error.stack || error}`)

+ 140 - 0
apis/Lepao/Count/Ledger/AdminList.js

@@ -0,0 +1,140 @@
+const API = require("../../../../lib/API")
+const db = require("../../../../plugin/DataBase/db")
+const AccessControl = require("../../../../lib/AccessControl")
+const { BaseStdResponse } = require("../../../../BaseStdResponse")
+
+class AdminList extends API {
+    constructor() {
+        super()
+        this.setPath('/Admin/Lepao/Count/Ledger/List')
+        this.setMethod('GET')
+    }
+
+    async onRequest(req, res) {
+        let {
+            uuid,
+            session,
+            current = 1,
+            pagesize = 20,
+            user_uuid,
+            username,
+            student_num,
+            biz_type,
+            operator_uuid,
+            start_time,
+            end_time
+        } = req.query
+
+        current = Number(current)
+        pagesize = Number(pagesize)
+        const startTimeNum = start_time === undefined || start_time === null || start_time === '' ? null : Number(start_time)
+        const endTimeNum = end_time === undefined || end_time === null || end_time === '' ? null : Number(end_time)
+
+        if ([uuid, session].some(v => v === '' || v === null || v === undefined)) {
+            return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
+        }
+        if (!Number.isInteger(current) || current < 1 || !Number.isInteger(pagesize) || pagesize < 1 || pagesize > 100) {
+            return res.json({ ...BaseStdResponse.ERR, msg: '参数错误' })
+        }
+        if ((startTimeNum !== null && !Number.isFinite(startTimeNum)) || (endTimeNum !== null && !Number.isFinite(endTimeNum))) {
+            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('service')) {
+            return res.json({ ...BaseStdResponse.PERMISSION_DENIED })
+        }
+
+        const where = ['1 = 1']
+        const params = []
+        const offset = (current - 1) * pagesize
+
+        if (user_uuid) {
+            where.push('l.user_uuid COLLATE utf8mb4_general_ci = ?')
+            params.push(user_uuid)
+        }
+        if (username) {
+            where.push('u.username LIKE ?')
+            params.push(`%${username}%`)
+        }
+        if (student_num) {
+            where.push('la.student_num LIKE ?')
+            params.push(`%${student_num}%`)
+        }
+        if (biz_type) {
+            where.push('l.biz_type COLLATE utf8mb4_general_ci = ?')
+            params.push(biz_type)
+        }
+        if (operator_uuid) {
+            where.push('l.operator_uuid COLLATE utf8mb4_general_ci = ?')
+            params.push(operator_uuid)
+        }
+        if (startTimeNum !== null) {
+            where.push('l.created_at >= FROM_UNIXTIME(? / 1000)')
+            params.push(startTimeNum)
+        }
+        if (endTimeNum !== null) {
+            where.push('l.created_at <= FROM_UNIXTIME(? / 1000)')
+            params.push(endTimeNum)
+        }
+
+        const whereSql = where.join(' AND ')
+        const listSql = `
+            SELECT
+                l.id,
+                l.user_uuid,
+                u.username,
+                la.student_num,
+                l.delta,
+                l.balance_before,
+                l.balance_after,
+                l.biz_type,
+                l.biz_id,
+                l.operator_uuid,
+                op.username AS operator_name,
+                l.remark,
+                UNIX_TIMESTAMP(l.created_at) * 1000 AS created_at
+            FROM lepao_count_ledger l
+            LEFT JOIN users u ON u.uuid = l.user_uuid COLLATE utf8mb4_general_ci
+            LEFT JOIN users op ON op.uuid = l.operator_uuid COLLATE utf8mb4_general_ci
+            LEFT JOIN (
+                SELECT create_user, MIN(student_num) AS student_num
+                FROM lepao_account
+                GROUP BY create_user
+            ) la ON la.create_user = l.user_uuid COLLATE utf8mb4_general_ci
+            WHERE ${whereSql}
+            ORDER BY l.id DESC
+            LIMIT ${pagesize} OFFSET ${offset}
+        `
+        const countSql = `
+            SELECT COUNT(*) AS total
+            FROM lepao_count_ledger l
+            LEFT JOIN users u ON u.uuid = l.user_uuid COLLATE utf8mb4_general_ci
+            LEFT JOIN (
+                SELECT create_user, MIN(student_num) AS student_num
+                FROM lepao_account
+                GROUP BY create_user
+            ) la ON la.create_user = l.user_uuid COLLATE utf8mb4_general_ci
+            WHERE ${whereSql}
+        `
+        const rows = await db.query(listSql, params)
+        const countRows = await db.query(countSql, params)
+        if (!rows || !countRows) {
+            return res.json({ ...BaseStdResponse.DATABASE_ERR })
+        }
+
+        return res.json({
+            ...BaseStdResponse.OK,
+            data: rows,
+            pagination: {
+                current,
+                pagesize,
+                total: Number(countRows[0]?.total || 0)
+            }
+        })
+    }
+}
+
+module.exports.AdminList = AdminList

+ 106 - 0
apis/Lepao/Count/Ledger/MyList.js

@@ -0,0 +1,106 @@
+const API = require("../../../../lib/API")
+const db = require("../../../../plugin/DataBase/db")
+const AccessControl = require("../../../../lib/AccessControl")
+const { BaseStdResponse } = require("../../../../BaseStdResponse")
+
+class MyList extends API {
+    constructor() {
+        super()
+        this.setPath('/Lepao/Count/Ledger/MyList')
+        this.setMethod('GET')
+    }
+
+    async onRequest(req, res) {
+        let { uuid, session, current = 1, pagesize = 20, biz_type, start_time, end_time } = req.query
+        current = Number(current)
+        pagesize = Number(pagesize)
+        const startTimeNum = start_time === undefined || start_time === null || start_time === '' ? null : Number(start_time)
+        const endTimeNum = end_time === undefined || end_time === null || end_time === '' ? null : Number(end_time)
+
+        if ([uuid, session].some(v => v === '' || v === null || v === undefined)) {
+            return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
+        }
+        if (!Number.isInteger(current) || current < 1 || !Number.isInteger(pagesize) || pagesize < 1 || pagesize > 100) {
+            return res.json({ ...BaseStdResponse.ERR, msg: '参数错误' })
+        }
+        if ((startTimeNum !== null && !Number.isFinite(startTimeNum)) || (endTimeNum !== null && !Number.isFinite(endTimeNum))) {
+            return res.json({ ...BaseStdResponse.ERR, msg: '时间参数错误' })
+        }
+        if (!await AccessControl.checkSession(uuid, session)) {
+            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
+        }
+
+        const where = ['l.user_uuid = ?']
+        const params = [uuid]
+        const sumWhere = ['user_uuid = ?']
+        const sumParams = [uuid]
+        const offset = (current - 1) * pagesize
+
+        if (biz_type) {
+            where.push('l.biz_type = ?')
+            params.push(biz_type)
+            sumWhere.push('biz_type = ?')
+            sumParams.push(biz_type)
+        }
+
+        if (startTimeNum !== null) {
+            where.push('l.created_at >= FROM_UNIXTIME(? / 1000)')
+            params.push(startTimeNum)
+            sumWhere.push('created_at >= FROM_UNIXTIME(? / 1000)')
+            sumParams.push(startTimeNum)
+        }
+        if (endTimeNum !== null) {
+            where.push('l.created_at <= FROM_UNIXTIME(? / 1000)')
+            params.push(endTimeNum)
+            sumWhere.push('created_at <= FROM_UNIXTIME(? / 1000)')
+            sumParams.push(endTimeNum)
+        }
+
+        const whereSql = where.join(' AND ')
+        const listSql = `
+            SELECT
+                l.id,
+                l.user_uuid,
+                l.delta,
+                l.balance_before,
+                l.balance_after,
+                l.biz_type,
+                l.biz_id,
+                l.operator_uuid,
+                l.remark,
+                UNIX_TIMESTAMP(l.created_at) * 1000 AS created_at
+            FROM lepao_count_ledger l
+            WHERE ${whereSql}
+            ORDER BY l.id DESC
+            LIMIT ${pagesize} OFFSET ${offset}
+        `
+        const countSql = `SELECT COUNT(*) AS total FROM lepao_count_ledger l WHERE ${whereSql}`
+        const summarySql = `
+            SELECT
+                COALESCE(SUM(delta), 0) AS total_delta,
+                COUNT(*) AS total_records
+            FROM lepao_count_ledger
+            WHERE ${sumWhere.join(' AND ')}
+        `
+
+        const rows = await db.query(listSql, params)
+        const countRows = await db.query(countSql, params)
+        const summaryRows = await db.query(summarySql, sumParams)
+        if (!rows || !countRows || !summaryRows) {
+            return res.json({ ...BaseStdResponse.DATABASE_ERR })
+        }
+
+        return res.json({
+            ...BaseStdResponse.OK,
+            data: rows,
+            summary: summaryRows[0] || { total_delta: 0, total_records: 0 },
+            pagination: {
+                current,
+                pagesize,
+                total: Number(countRows[0]?.total || 0)
+            }
+        })
+    }
+}
+
+module.exports.MyList = MyList

+ 23 - 0
apis/Order/CallBack.js

@@ -3,9 +3,31 @@ const db = require("../../plugin/DataBase/db.js")
 const { BaseStdResponse } = require("../../BaseStdResponse.js")
 const { BaseStdResponse } = require("../../BaseStdResponse.js")
 const config = require('../../config.json')
 const config = require('../../config.json')
 const crypto = require("crypto")
 const crypto = require("crypto")
+const { insertLedgerRecord } = require('../../lib/Lepao/CountLedger')
 
 
 const PAYMENT_KEY = config.pay.key
 const PAYMENT_KEY = config.pay.key
 
 
+async function writePurchaseLedger(orderId, userUuid, addCount, logger) {
+    const delta = Number(addCount || 0)
+    if (!orderId || !userUuid || delta === 0) return
+    try {
+        const userRows = await db.query('SELECT lepao_count FROM users WHERE uuid = ?', [userUuid])
+        if (!userRows || userRows.length !== 1) return
+        const afterCount = Number(userRows[0].lepao_count || 0)
+        const beforeCount = afterCount - delta
+        await insertLedgerRecord({
+            userUuid,
+            delta,
+            balanceBefore: beforeCount,
+            balanceAfter: afterCount,
+            bizType: 'purchase',
+            bizId: orderId
+        })
+    } catch (error) {
+        logger?.error?.(`写入购买次数流水失败 ${orderId}: ${error.stack || error}`)
+    }
+}
+
 class CallBack extends API {
 class CallBack extends API {
     constructor() {
     constructor() {
         super()
         super()
@@ -94,6 +116,7 @@ class CallBack extends API {
 
 
                 sql = 'UPDATE orders SET state = 2 WHERE orderId = ?'
                 sql = 'UPDATE orders SET state = 2 WHERE orderId = ?'
                 await db.query(sql, [out_trade_no])
                 await db.query(sql, [out_trade_no])
+                await writePurchaseLedger(out_trade_no, create_user, lepao_count, this.logger)
 
 
                 this.logger.info(`支付成功,订单处理完毕。订单号:${out_trade_no}`)
                 this.logger.info(`支付成功,订单处理完毕。订单号:${out_trade_no}`)
 
 

+ 26 - 0
apis/Order/CreateOrder.js

@@ -8,10 +8,35 @@ const axios = require('axios')
 const config = require('../../config.json')
 const config = require('../../config.json')
 const mq = require('../../plugin/mq')
 const mq = require('../../plugin/mq')
 const { mq: mqName } = require('../../plugin/mq/mqPrefix')
 const { mq: mqName } = require('../../plugin/mq/mqPrefix')
+const { insertLedgerRecord } = require('../../lib/Lepao/CountLedger')
 
 
 const ORDER_PAYMENT_QUEUE = mqName('order_payment_check')
 const ORDER_PAYMENT_QUEUE = mqName('order_payment_check')
 let orderPaymentWorkerStarted = false
 let orderPaymentWorkerStarted = false
 
 
+async function writePurchaseLedger(orderId, userUuid, addCount, logger) {
+    const delta = Number(addCount || 0)
+    if (!orderId || !userUuid || delta === 0) return
+    try {
+        const userRows = await db.query(
+            'SELECT lepao_count FROM users WHERE uuid = ?',
+            [userUuid]
+        )
+        if (!userRows || userRows.length !== 1) return
+        const afterCount = Number(userRows[0].lepao_count || 0)
+        const beforeCount = afterCount - delta
+        await insertLedgerRecord({
+            userUuid,
+            delta,
+            balanceBefore: beforeCount,
+            balanceAfter: afterCount,
+            bizType: 'purchase',
+            bizId: orderId
+        })
+    } catch (error) {
+        logger?.error?.(`写入购买次数流水失败 ${orderId}: ${error.stack || error}`)
+    }
+}
+
 async function startOrderPaymentWorker(logger) {
 async function startOrderPaymentWorker(logger) {
     if (orderPaymentWorkerStarted) {
     if (orderPaymentWorkerStarted) {
         return
         return
@@ -120,6 +145,7 @@ async function pollOrderPaymentStatus(orderId, logger) {
 
 
                     sql = 'UPDATE orders SET state = 2 WHERE orderId = ?'
                     sql = 'UPDATE orders SET state = 2 WHERE orderId = ?'
                     await db.query(sql, [out_trade_no])
                     await db.query(sql, [out_trade_no])
+                    await writePurchaseLedger(out_trade_no, create_user, lepao_count, logger)
 
 
                     logger.info(`订单处理成功:${out_trade_no}`)
                     logger.info(`订单处理成功:${out_trade_no}`)
                     return
                     return

+ 49 - 0
lib/Lepao/CountLedger.js

@@ -0,0 +1,49 @@
+const db = require('../../plugin/DataBase/db')
+
+function normalizeResult(rawResult) {
+    if (Array.isArray(rawResult)) return rawResult[0]
+    return rawResult
+}
+
+async function executeSql(executor, sql, params) {
+    if (executor && typeof executor.execute === 'function') {
+        return executor.execute(sql, params)
+    }
+    return db.query(sql, params)
+}
+
+async function insertLedgerRecord({
+    executor,
+    userUuid,
+    delta,
+    balanceBefore,
+    balanceAfter,
+    bizType,
+    bizId,
+    operatorUuid = null,
+    remark = ''
+}) {
+    if (!userUuid || !bizType) return false
+
+    const result = await executeSql(
+        executor,
+        `INSERT IGNORE INTO lepao_count_ledger
+        (user_uuid, delta, balance_before, balance_after, biz_type, biz_id, operator_uuid, remark, created_at)
+        VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())`,
+        [
+            userUuid,
+            Number(delta || 0),
+            Number(balanceBefore || 0),
+            Number(balanceAfter || 0),
+            bizType,
+            String(bizId || ''),
+            operatorUuid,
+            String(remark || '')
+        ]
+    )
+
+    const rows = normalizeResult(result)
+    return !!rows && Number(rows.affectedRows || 0) > 0
+}
+
+module.exports = { insertLedgerRecord }

+ 104 - 8
lib/Lepao/Worker.js

@@ -19,6 +19,8 @@ const {
     dataSign
     dataSign
 } = require('../../plugin/Lepao/Crypto')
 } = require('../../plugin/Lepao/Crypto')
 const generateGyrFromPath = require('../../plugin/Lepao/generateGyrFromPath')
 const generateGyrFromPath = require('../../plugin/Lepao/generateGyrFromPath')
+const { syncAccountInfo } = require('./syncAccountInfo')
+const { insertLedgerRecord } = require('./CountLedger')
 
 
 const Logger = require('../Logger')
 const Logger = require('../Logger')
 
 
@@ -423,6 +425,23 @@ class Worker {
     /* ================= 业务 ================= */
     /* ================= 业务 ================= */
 
 
     initHandlers() {
     initHandlers() {
+        this.register('lepao.syncAccountInfo', async (req) => {
+            const studentNum = req?.student_num
+            if (!studentNum) {
+                throw new Error('同步乐跑账号失败:缺少 student_num')
+            }
+            const syncResult = await syncAccountInfo({
+                studentNum,
+                logger: this.logger
+            })
+            if (!syncResult.ok) {
+                const err = new Error(syncResult.msg || '同步乐跑账号失败')
+                err.retryable = false
+                throw err
+            }
+            return syncResult.data
+        })
+
         /* ---------------- 开始乐跑 ---------------- */
         /* ---------------- 开始乐跑 ---------------- */
         this.register('lepao.startRun', async (req, ctx) => {
         this.register('lepao.startRun', async (req, ctx) => {
             const traceId = ctx.traceId
             const traceId = ctx.traceId
@@ -744,10 +763,51 @@ class Worker {
             }
             }
 
 
             this.logger.info(`${account || uuid}开始扣减乐跑次数`)
             this.logger.info(`${account || uuid}开始扣减乐跑次数`)
-            const useLepaoCountSql = 'UPDATE users SET lepao_count = lepao_count - 1 WHERE uuid  = ?'
-            const r = await db.query(useLepaoCountSql, [uuid])
-            if (!r || r.affectedRows !== 1) {
-                throw new Error('扣减乐跑次数失败:数据库更新失败')
+            const conn = await db.connect()
+            try {
+                await conn.beginTransaction()
+                const [userRows] = await conn.execute(
+                    'SELECT lepao_count FROM users WHERE uuid = ? FOR UPDATE',
+                    [uuid]
+                )
+                if (!userRows || userRows.length !== 1) {
+                    await conn.rollback()
+                    throw new Error('扣减乐跑次数失败:用户不存在')
+                }
+
+                const beforeCount = Number(userRows[0].lepao_count || 0)
+                if (beforeCount < 1) {
+                    await conn.rollback()
+                    throw new Error('用户乐跑次数不足,请购买乐跑次数后重试!')
+                }
+
+                const [r] = await conn.execute(
+                    'UPDATE users SET lepao_count = lepao_count - 1 WHERE uuid = ?',
+                    [uuid]
+                )
+                if (!r || r.affectedRows !== 1) {
+                    await conn.rollback()
+                    throw new Error('扣减乐跑次数失败:数据库更新失败')
+                }
+
+                await insertLedgerRecord({
+                    executor: conn,
+                    userUuid: uuid,
+                    delta: -1,
+                    balanceBefore: beforeCount,
+                    balanceAfter: beforeCount - 1,
+                    bizType: 'run_consume',
+                    bizId: consumeKey
+                })
+
+                await conn.commit()
+            } catch (error) {
+                try { await conn.rollback() } catch (_) { }
+                throw error
+            } finally {
+                if (conn?.connection && typeof conn.connection.release === 'function' && typeof conn?.release === 'function') {
+                    conn.release()
+                }
             }
             }
             this.logger.info(`${account || uuid}扣减乐跑次数完成`)
             this.logger.info(`${account || uuid}扣减乐跑次数完成`)
 
 
@@ -777,10 +837,46 @@ class Worker {
             }
             }
 
 
             this.logger.info(`${account || uuid}开始返还乐跑次数`)
             this.logger.info(`${account || uuid}开始返还乐跑次数`)
-            const sql = 'UPDATE users SET lepao_count = lepao_count + 1 WHERE uuid  = ?'
-            const r = await db.query(sql, [uuid])
-            if (!r || r.affectedRows !== 1) {
-                throw new Error('返还乐跑次数失败:数据库更新失败')
+            const conn = await db.connect()
+            try {
+                await conn.beginTransaction()
+                const [userRows] = await conn.execute(
+                    'SELECT lepao_count FROM users WHERE uuid = ? FOR UPDATE',
+                    [uuid]
+                )
+                if (!userRows || userRows.length !== 1) {
+                    await conn.rollback()
+                    throw new Error('返还乐跑次数失败:用户不存在')
+                }
+
+                const beforeCount = Number(userRows[0].lepao_count || 0)
+                const [r] = await conn.execute(
+                    'UPDATE users SET lepao_count = lepao_count + 1 WHERE uuid  = ?',
+                    [uuid]
+                )
+                if (!r || r.affectedRows !== 1) {
+                    await conn.rollback()
+                    throw new Error('返还乐跑次数失败:数据库更新失败')
+                }
+
+                await insertLedgerRecord({
+                    executor: conn,
+                    userUuid: uuid,
+                    delta: 1,
+                    balanceBefore: beforeCount,
+                    balanceAfter: beforeCount + 1,
+                    bizType: 'run_refund',
+                    bizId: refundKey
+                })
+
+                await conn.commit()
+            } catch (error) {
+                try { await conn.rollback() } catch (_) { }
+                throw error
+            } finally {
+                if (conn?.connection && typeof conn.connection.release === 'function' && typeof conn?.release === 'function') {
+                    conn.release()
+                }
             }
             }
             this.logger.info(`${account || uuid}返还乐跑次数完成`)
             this.logger.info(`${account || uuid}返还乐跑次数完成`)
             await Redis.set(refundKey, '1', { EX: 3600 })
             await Redis.set(refundKey, '1', { EX: 3600 })

+ 109 - 0
lib/Lepao/syncAccountInfo.js

@@ -0,0 +1,109 @@
+const axios = require('axios')
+const db = require('../../plugin/DataBase/db')
+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 axios.post(
+            'https://lepao.ctbu.edu.cn/v3/api.php/Run2/beforeRunV260',
+            form,
+            { headers, proxy: false }
+        )
+
+        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 }

+ 24 - 0
plugin/mq/enqueueLepaoSyncAccountInfo.js

@@ -0,0 +1,24 @@
+const mq = require('./index')
+const { assertRunforgeTaskIngress, publishRunforgeTask } = require('./runforgeTaskMq')
+
+async function enqueueLepaoSyncAccountInfo(studentNum, logger) {
+    if (!studentNum) return
+    try {
+        const channel = await mq.getChannel('lepao_account_sync')
+        await assertRunforgeTaskIngress(channel, logger)
+        const taskId = `lepao:sync-account:${Date.now()}:${studentNum}`
+        publishRunforgeTask(channel, {
+            id: taskId,
+            type: 'lepao.syncAccountInfo',
+            data: {
+                taskId,
+                student_num: studentNum
+            },
+            retry: 0
+        })
+    } catch (e) {
+        logger?.error?.(`投递乐跑账号同步任务失败 ${studentNum}: ${e.message || e}`)
+    }
+}
+
+module.exports = { enqueueLepaoSyncAccountInfo }