Browse Source

✨ feat: 新增账号绑定解绑审计能力

记录平台与机器人绑定/解绑行为,并提供用户侧和管理员侧(按账号/全局筛选)审计查询,提升账号操作可追溯性。

Made-with: Cursor
Pchen0 6 days ago
parent
commit
76f3dc9b3b

+ 18 - 0
apis/Lepao/Account/AddAccount.js

@@ -2,6 +2,7 @@ const API = require("../../../lib/API.js");
 const db = require("../../../plugin/DataBase/db.js");
 const db = require("../../../plugin/DataBase/db.js");
 const { BaseStdResponse } = require("../../../BaseStdResponse.js");
 const { BaseStdResponse } = require("../../../BaseStdResponse.js");
 const AccessControl = require("../../../lib/AccessControl.js");
 const AccessControl = require("../../../lib/AccessControl.js");
+const { insertBindAudit, BindAuditAction, BindAuditSource } = require("../../../lib/Lepao/BindAudit.js");
 
 
 class AddAccount extends API {
 class AddAccount extends API {
     constructor() {
     constructor() {
@@ -98,6 +99,8 @@ class AddAccount extends API {
         }
         }
 
 
         const time = new Date().getTime()
         const time = new Date().getTime()
+        const previousOwner = countRows.length !== 0 ? countRows[0].create_user : null
+        const shouldRecordBind = !id && previousOwner !== uuid
 
 
         let sql, r
         let sql, r
 
 
@@ -149,6 +152,21 @@ class AddAccount extends API {
                         bot_account: selectRows.length !== 0 ? selectRows[0].bot_account : undefined
                         bot_account: selectRows.length !== 0 ? selectRows[0].bot_account : undefined
                     }
                     }
                 })
                 })
+
+                if (shouldRecordBind) {
+                    const auditOk = await insertBindAudit({
+                        studentNum: student_num,
+                        ownerUuid: uuid,
+                        action: BindAuditAction.PLATFORM_BIND,
+                        source: BindAuditSource.USER_API,
+                        operatorUuid: uuid,
+                        detail: { via: 'AddAccount' },
+                        createdAt: time
+                    })
+                    if (!auditOk) {
+                        this.logger.warn(`绑定审计写入失败 student_num=${student_num}`)
+                    }
+                }
             } else {
             } else {
                 return res.json({ ...BaseStdResponse.ERR, msg: '添加乐跑账号失败!数据库错误' })
                 return res.json({ ...BaseStdResponse.ERR, msg: '添加乐跑账号失败!数据库错误' })
             }
             }

+ 16 - 1
apis/Lepao/Account/DeleteAccount.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 { insertBindAudit, BindAuditAction, BindAuditSource } = require("../../../lib/Lepao/BindAudit");
 
 
 class DeleteAccount extends API {
 class DeleteAccount extends API {
     constructor() {
     constructor() {
@@ -25,7 +26,7 @@ class DeleteAccount extends API {
                 ...BaseStdResponse.ACCESS_DENIED
                 ...BaseStdResponse.ACCESS_DENIED
             })
             })
 
 
-        let selectSql = 'SELECT create_user FROM lepao_account WHERE id = ?'
+        let selectSql = 'SELECT student_num, create_user FROM lepao_account WHERE id = ?'
         let selectRows = await db.query(selectSql, [id])
         let selectRows = await db.query(selectSql, [id])
         if (!selectRows || selectRows.length === 0)
         if (!selectRows || selectRows.length === 0)
             return res.json({
             return res.json({
@@ -33,6 +34,7 @@ class DeleteAccount extends API {
                 msg: '解绑账号失败!未找到账户信息'
                 msg: '解绑账号失败!未找到账户信息'
             })
             })
 
 
+        let source = BindAuditSource.USER_API
         if (selectRows[0].create_user !== uuid) {
         if (selectRows[0].create_user !== uuid) {
             let permission = await AccessControl.getPermission(uuid)
             let permission = await AccessControl.getPermission(uuid)
             if (!permission.includes("admin") && !permission.includes("service"))
             if (!permission.includes("admin") && !permission.includes("service"))
@@ -40,6 +42,7 @@ class DeleteAccount extends API {
                     ...BaseStdResponse.ERR,
                     ...BaseStdResponse.ERR,
                     msg: '解绑账号失败!未找到账户信息'
                     msg: '解绑账号失败!未找到账户信息'
                 })
                 })
+            source = permission.includes("admin") ? BindAuditSource.ADMIN_API : BindAuditSource.SERVICE_API
         }
         }
 
 
         let sql = 'UPDATE lepao_account SET create_user = NULL, auto_run = 0 WHERE id = ?'
         let sql = 'UPDATE lepao_account SET create_user = NULL, auto_run = 0 WHERE id = ?'
@@ -47,6 +50,18 @@ class DeleteAccount extends API {
 
 
         try {
         try {
             if (r && r.affectedRows > 0) {
             if (r && r.affectedRows > 0) {
+                const auditOk = await insertBindAudit({
+                    studentNum: selectRows[0].student_num,
+                    ownerUuid: selectRows[0].create_user,
+                    action: BindAuditAction.PLATFORM_UNBIND,
+                    source,
+                    operatorUuid: uuid,
+                    detail: { via: 'DeleteAccount' },
+                    createdAt: Date.now()
+                })
+                if (!auditOk) {
+                    this.logger.warn(`解绑审计写入失败 student_num=${selectRows[0].student_num}`)
+                }
                 res.json({
                 res.json({
                     ...BaseStdResponse.OK
                     ...BaseStdResponse.OK
                 })
                 })

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

@@ -0,0 +1,98 @@
+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

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

@@ -0,0 +1,134 @@
+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

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

@@ -0,0 +1,106 @@
+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

+ 50 - 0
lib/Lepao/BindAudit.js

@@ -0,0 +1,50 @@
+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
+}

+ 37 - 1
lib/Lepao/Mcp.js

@@ -4,6 +4,7 @@ const Logger = require('../Logger')
 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 EmailTemplate = require('../../plugin/Email/emailTemplate')
 const EmailTemplate = require('../../plugin/Email/emailTemplate')
+const { insertBindAudit, BindAuditAction, BindAuditSource } = require('./BindAudit')
 
 
 class Mcp {
 class Mcp {
     constructor() {
     constructor() {
@@ -59,7 +60,7 @@ class Mcp {
             let umo = `QQ2749761853:FriendMessage:${sender}`
             let umo = `QQ2749761853:FriendMessage:${sender}`
             let sql = `
             let sql = `
                 SELECT 
                 SELECT 
-                    f.student_num, a.name, f.bot_account
+                    f.student_num, a.name, a.create_user, f.bot_account
                 FROM 
                 FROM 
                     lepao_extra f
                     lepao_extra f
                 LEFT JOIN 
                 LEFT JOIN 
@@ -83,6 +84,19 @@ class Mcp {
             let insertRows = await db.query(insertSql, [sender, umo, bind_code])
             let insertRows = await db.query(insertSql, [sender, umo, bind_code])
             if (!insertRows || insertRows.affectedRows !== 1)
             if (!insertRows || insertRows.affectedRows !== 1)
                 return '系统出错,请稍后再试'
                 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}`
             return `绑定成功,姓名:${rows[0].name ?? '未更新,请使用乐跑登录器更新账号信息'},学号:${rows[0].student_num}`
         } catch (error) {
         } catch (error) {
             this.logger.error(`MCP绑定账号出错:${error.stack}`)
             this.logger.error(`MCP绑定账号出错:${error.stack}`)
@@ -158,12 +172,34 @@ class Mcp {
                 return '缺少参数'
                 return '缺少参数'
 
 
             this.logger.info(`MCP接收解绑账号请求:${sender}`)
             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 = `
             let insertSql = `
                 UPDATE lepao_extra SET bot_account = NULL, bot_umo = NULL WHERE bot_account = ?
                 UPDATE lepao_extra SET bot_account = NULL, bot_umo = NULL WHERE bot_account = ?
             `
             `
             let insertRows = await db.query(insertSql, [sender])
             let insertRows = await db.query(insertSql, [sender])
             if (!insertRows || insertRows.affectedRows !== 1)
             if (!insertRows || insertRows.affectedRows !== 1)
                 return '系统出错,请稍后再试'
                 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 `解绑成功`
             return `解绑成功`
         } catch (error) {
         } catch (error) {
             this.logger.error(`MCP解绑账号出错:${error.stack}`)
             this.logger.error(`MCP解绑账号出错:${error.stack}`)

+ 14 - 0
lib/Lepao/WorkOrderMcp.js

@@ -5,6 +5,7 @@ const mq = require('../../plugin/mq')
 const { mq: mqName } = require('../../plugin/mq/mqPrefix')
 const { mq: mqName } = require('../../plugin/mq/mqPrefix')
 const Redis = require('../../plugin/DataBase/Redis')
 const Redis = require('../../plugin/DataBase/Redis')
 const EmailTemplate = require('../../plugin/Email/emailTemplate')
 const EmailTemplate = require('../../plugin/Email/emailTemplate')
+const { insertBindAudit, BindAuditAction, BindAuditSource } = require('./BindAudit')
 
 
 class WorkOrderMcp {
 class WorkOrderMcp {
     constructor() {
     constructor() {
@@ -152,6 +153,19 @@ class WorkOrderMcp {
             if (!updateRows || updateRows.affectedRows !== 1)
             if (!updateRows || updateRows.affectedRows !== 1)
                 return '系统出错,请稍后再试'
                 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)
             const latestUnbindCount = await Redis.incr(dailyUnbindKey)
             if (latestUnbindCount === 1) {
             if (latestUnbindCount === 1) {
                 await Redis.expire(dailyUnbindKey, this.getSecondsToDayEnd())
                 await Redis.expire(dailyUnbindKey, this.getSecondsToDayEnd())