|
|
@@ -1,9 +1,10 @@
|
|
|
const db = require('../../plugin/DataBase/db')
|
|
|
const { axiosWithQgOutbound } = require('../Lepao/qgOutboundAxios')
|
|
|
const Logger = require('../Logger')
|
|
|
-const path = require('path')
|
|
|
-const QxsBookList = require('../../apis/QuXuanShu/GetBookList').GetBookList
|
|
|
-const EmailTemplate = require('../../plugin/Email/emailTemplate')
|
|
|
+const path = require('path')
|
|
|
+const QxsBookList = require('../../apis/QuXuanShu/GetBookList').GetBookList
|
|
|
+const EmailTemplate = require('../../plugin/Email/emailTemplate')
|
|
|
+const { insertLedgerRecord } = require('../Lepao/CountLedger')
|
|
|
|
|
|
const ASSISTANT_UUID = 'e4fe0277-0b1a-41a1-b25f-8b6e4cec3281'
|
|
|
|
|
|
@@ -168,6 +169,33 @@ function mapPowerRecord(row) {
|
|
|
change_time: formatBeijingTime(row.change_time)
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+function mapSendCountRequest(row) {
|
|
|
+ const { id, sender_uuid, receiver_user_id, ...item } = row
|
|
|
+ return {
|
|
|
+ request_id: id,
|
|
|
+ ...item,
|
|
|
+ created_at: formatBeijingTime(item.created_at),
|
|
|
+ reviewed_at: formatBeijingTime(item.reviewed_at)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function mapWorkOrder(row) {
|
|
|
+ const { id, ...item } = row
|
|
|
+ return {
|
|
|
+ work_order_id: id,
|
|
|
+ ...item,
|
|
|
+ create_time: formatBeijingTime(item.create_time),
|
|
|
+ update_time: formatBeijingTime(item.update_time)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function mapWorkOrderMessage(msg) {
|
|
|
+ return {
|
|
|
+ ...msg,
|
|
|
+ time: formatBeijingTime(msg.time)
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
class AiAssistantMcp {
|
|
|
constructor() {
|
|
|
@@ -636,6 +664,326 @@ class AiAssistantMcp {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+ async send_lepao_count(args) {
|
|
|
+ let { sender, username, count } = args
|
|
|
+ username = typeof username === 'string' ? username.trim() : username
|
|
|
+ count = Number(count)
|
|
|
+
|
|
|
+ if (required([sender, username]) || Number.isNaN(count)) return '缺少 sender、username 或 count'
|
|
|
+ if (!Number.isInteger(count) || count < 1 || count > 9999) return '超出赠送的次数范围,请重新选择赠送次数'
|
|
|
+
|
|
|
+ const conn = await db.connect()
|
|
|
+ try {
|
|
|
+ await conn.beginTransaction()
|
|
|
+
|
|
|
+ const [senderRows] = await conn.execute(
|
|
|
+ 'SELECT id, username, lepao_count, COALESCE(send_count_auto_approve, 0) AS send_count_auto_approve FROM users WHERE uuid = ?',
|
|
|
+ [sender]
|
|
|
+ )
|
|
|
+ if (!senderRows || senderRows.length !== 1) {
|
|
|
+ await conn.rollback()
|
|
|
+ return '获取用户信息失败'
|
|
|
+ }
|
|
|
+
|
|
|
+ const [targetRows] = await conn.execute(
|
|
|
+ 'SELECT id, uuid FROM users WHERE username = ?',
|
|
|
+ [username]
|
|
|
+ )
|
|
|
+ if (!targetRows || targetRows.length !== 1) {
|
|
|
+ await conn.rollback()
|
|
|
+ return '未找到接收用户,请检查用户名是否正确'
|
|
|
+ }
|
|
|
+ if (targetRows[0].uuid === sender) {
|
|
|
+ await conn.rollback()
|
|
|
+ return '不能给自己赠送次数'
|
|
|
+ }
|
|
|
+
|
|
|
+ const [decResult] = await conn.execute(
|
|
|
+ 'UPDATE users SET lepao_count = lepao_count - ? WHERE uuid = ? AND lepao_count >= ?',
|
|
|
+ [count, sender, count]
|
|
|
+ )
|
|
|
+ if (!decResult || decResult.affectedRows !== 1) {
|
|
|
+ await conn.rollback()
|
|
|
+ return '剩余乐跑次数不足,请购买后再赠送'
|
|
|
+ }
|
|
|
+
|
|
|
+ const senderUsername = senderRows[0].username
|
|
|
+ const senderLepaoBefore = Number(senderRows[0].lepao_count || 0)
|
|
|
+ const autoApprove = Number(senderRows[0].send_count_auto_approve) === 1
|
|
|
+
|
|
|
+ if (autoApprove) {
|
|
|
+ const [recvRows] = await conn.execute(
|
|
|
+ 'SELECT uuid, lepao_count FROM users WHERE id = ? FOR UPDATE',
|
|
|
+ [targetRows[0].id]
|
|
|
+ )
|
|
|
+ if (!recvRows || recvRows.length !== 1) {
|
|
|
+ await conn.rollback()
|
|
|
+ return '未找到接收用户,请检查用户名是否正确'
|
|
|
+ }
|
|
|
+
|
|
|
+ const receiverUuid = recvRows[0].uuid
|
|
|
+ const beforeRecv = Number(recvRows[0].lepao_count || 0)
|
|
|
+ const [incResult] = await conn.execute(
|
|
|
+ 'UPDATE users SET lepao_count = lepao_count + ? WHERE id = ?',
|
|
|
+ [count, targetRows[0].id]
|
|
|
+ )
|
|
|
+ if (!incResult || incResult.affectedRows !== 1) {
|
|
|
+ await conn.rollback()
|
|
|
+ return '接收方入账失败,请稍后再试'
|
|
|
+ }
|
|
|
+
|
|
|
+ const [insertResult] = await conn.execute(
|
|
|
+ `INSERT INTO lepao_send_count_request
|
|
|
+ (sender_uuid, receiver_user_id, count, status, created_at, reviewed_at, reviewer_uuid)
|
|
|
+ VALUES (?, ?, ?, 'approved', NOW(), NOW(), NULL)`,
|
|
|
+ [sender, targetRows[0].id, count]
|
|
|
+ )
|
|
|
+ if (!insertResult || insertResult.affectedRows !== 1) {
|
|
|
+ await conn.rollback()
|
|
|
+ return '记录赠送失败,请稍后再试'
|
|
|
+ }
|
|
|
+
|
|
|
+ const requestId = insertResult.insertId
|
|
|
+ await insertLedgerRecord({
|
|
|
+ executor: conn,
|
|
|
+ userUuid: sender,
|
|
|
+ delta: -count,
|
|
|
+ balanceBefore: senderLepaoBefore,
|
|
|
+ balanceAfter: senderLepaoBefore - count,
|
|
|
+ bizType: 'gift_send_lock',
|
|
|
+ bizId: `send_request:${requestId}`,
|
|
|
+ remark: `向${username}赠送${count}次`
|
|
|
+ })
|
|
|
+ await insertLedgerRecord({
|
|
|
+ executor: conn,
|
|
|
+ userUuid: receiverUuid,
|
|
|
+ delta: count,
|
|
|
+ balanceBefore: beforeRecv,
|
|
|
+ balanceAfter: beforeRecv + count,
|
|
|
+ bizType: 'gift_receive',
|
|
|
+ bizId: `send_request:${requestId}`,
|
|
|
+ operatorUuid: null,
|
|
|
+ remark: `${senderUsername}赠送${count}次`
|
|
|
+ })
|
|
|
+
|
|
|
+ await conn.commit()
|
|
|
+ const reviewTime = Date.now()
|
|
|
+ Promise.resolve().then(async () => {
|
|
|
+ try {
|
|
|
+ const infoRows = await db.query('SELECT email FROM users WHERE id = ?', [targetRows[0].id])
|
|
|
+ if (infoRows && infoRows.length === 1 && infoRows[0].email) {
|
|
|
+ await EmailTemplate.sendCountRequestApproved(infoRows[0].email, {
|
|
|
+ requestId,
|
|
|
+ senderUsername,
|
|
|
+ count,
|
|
|
+ reviewTime
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } catch (mailErr) {
|
|
|
+ this.logger.error(`[AiAssistantMcp][SendCount][auto][${requestId}] notify failed: ${mailErr.message || mailErr}`)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ return asJson({ success: true, status: 'approved', request_id: requestId, message: '赠送成功,对方已到账' })
|
|
|
+ }
|
|
|
+
|
|
|
+ const [insertResult] = await conn.execute(
|
|
|
+ `INSERT INTO lepao_send_count_request
|
|
|
+ (sender_uuid, receiver_user_id, count, status, created_at)
|
|
|
+ VALUES (?, ?, ?, 'pending', NOW())`,
|
|
|
+ [sender, targetRows[0].id, count]
|
|
|
+ )
|
|
|
+ if (!insertResult || insertResult.affectedRows !== 1) {
|
|
|
+ await conn.rollback()
|
|
|
+ return '提交赠送审核失败,请稍后再试'
|
|
|
+ }
|
|
|
+
|
|
|
+ const requestId = insertResult.insertId
|
|
|
+ await insertLedgerRecord({
|
|
|
+ executor: conn,
|
|
|
+ userUuid: sender,
|
|
|
+ delta: -count,
|
|
|
+ balanceBefore: senderLepaoBefore,
|
|
|
+ balanceAfter: senderLepaoBefore - count,
|
|
|
+ bizType: 'gift_send_lock',
|
|
|
+ bizId: `send_request:${requestId}`,
|
|
|
+ remark: `向${username}赠送${count}次`
|
|
|
+ })
|
|
|
+
|
|
|
+ await conn.commit()
|
|
|
+ const createTime = Date.now()
|
|
|
+ Promise.resolve().then(async () => {
|
|
|
+ try {
|
|
|
+ const adminRows = await db.query(`
|
|
|
+ SELECT email
|
|
|
+ FROM users
|
|
|
+ WHERE email IS NOT NULL
|
|
|
+ AND email <> ''
|
|
|
+ AND (JSON_CONTAINS(permission, '"admin"') OR JSON_CONTAINS(permission, '"service"'))
|
|
|
+ `)
|
|
|
+ const emails = [...new Set((adminRows || []).map(row => row.email).filter(Boolean))]
|
|
|
+ for (const email of emails) {
|
|
|
+ await EmailTemplate.sendCountRequestNotifyAdmins(email, {
|
|
|
+ requestId,
|
|
|
+ senderUsername,
|
|
|
+ receiverUsername: username,
|
|
|
+ count,
|
|
|
+ createTime
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } catch (mailErr) {
|
|
|
+ this.logger.error(`[AiAssistantMcp][SendCount][pending][${requestId}] notify failed: ${mailErr.message || mailErr}`)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ return asJson({ success: true, status: 'pending', request_id: requestId, message: '已提交审核,审核通过后接收方将到账' })
|
|
|
+ } catch (err) {
|
|
|
+ try { await conn.rollback() } catch (_) { }
|
|
|
+ this.logger.error(`MCP赠送乐跑次数失败:${err.stack || err}`)
|
|
|
+ return '赠送次数失败,请稍后再试'
|
|
|
+ } finally {
|
|
|
+ if (conn?.connection && typeof conn.connection.release === 'function' && typeof conn?.release === 'function') {
|
|
|
+ conn.release()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async list_send_count_requests(args) {
|
|
|
+ const { sender, direction, status, current, pagesize } = args
|
|
|
+ if (required([sender])) return '缺少 sender'
|
|
|
+
|
|
|
+ if (direction && !['sent', 'received'].includes(direction)) return 'direction 参数错误'
|
|
|
+ if (status && !['pending', 'approved', 'rejected'].includes(status)) return 'status 参数错误'
|
|
|
+
|
|
|
+ const page = pageArgs({ current, pagesize })
|
|
|
+ const userRows = await db.query('SELECT id FROM users WHERE uuid = ?', [sender])
|
|
|
+ if (!userRows || userRows.length !== 1) return '获取用户信息失败'
|
|
|
+
|
|
|
+ const userId = userRows[0].id
|
|
|
+ let sql = `
|
|
|
+ SELECT
|
|
|
+ r.id,
|
|
|
+ r.sender_uuid,
|
|
|
+ su.username AS sender_username,
|
|
|
+ su.avatar AS sender_avatar,
|
|
|
+ r.receiver_user_id,
|
|
|
+ ru.username AS receiver_username,
|
|
|
+ ru.avatar AS receiver_avatar,
|
|
|
+ CASE WHEN r.sender_uuid = ? THEN 'sent' ELSE 'received' END AS direction,
|
|
|
+ CASE WHEN r.sender_uuid = ? THEN ru.username ELSE su.username END AS counterparty_username,
|
|
|
+ CASE WHEN r.sender_uuid = ? THEN ru.avatar ELSE su.avatar END AS counterparty_avatar,
|
|
|
+ r.count,
|
|
|
+ r.status,
|
|
|
+ r.created_at,
|
|
|
+ r.reviewed_at,
|
|
|
+ r.reject_reason
|
|
|
+ FROM lepao_send_count_request r
|
|
|
+ LEFT JOIN users su ON su.uuid = r.sender_uuid
|
|
|
+ LEFT JOIN users ru ON ru.id = r.receiver_user_id
|
|
|
+ WHERE (r.sender_uuid = ? OR r.receiver_user_id = ?)
|
|
|
+ `
|
|
|
+ let countSql = `
|
|
|
+ SELECT COUNT(*) AS total
|
|
|
+ FROM lepao_send_count_request r
|
|
|
+ WHERE (r.sender_uuid = ? OR r.receiver_user_id = ?)
|
|
|
+ `
|
|
|
+ const params = [sender, sender, sender, sender, userId]
|
|
|
+ const countParams = [sender, userId]
|
|
|
+
|
|
|
+ if (direction === 'sent') {
|
|
|
+ sql += ' AND r.sender_uuid = ?'
|
|
|
+ countSql += ' AND r.sender_uuid = ?'
|
|
|
+ params.push(sender)
|
|
|
+ countParams.push(sender)
|
|
|
+ }
|
|
|
+ if (direction === 'received') {
|
|
|
+ sql += ' AND r.receiver_user_id = ?'
|
|
|
+ countSql += ' AND r.receiver_user_id = ?'
|
|
|
+ params.push(userId)
|
|
|
+ countParams.push(userId)
|
|
|
+ }
|
|
|
+ if (status) {
|
|
|
+ sql += ' AND r.status = ?'
|
|
|
+ countSql += ' AND r.status = ?'
|
|
|
+ params.push(status)
|
|
|
+ countParams.push(status)
|
|
|
+ }
|
|
|
+
|
|
|
+ sql += ' ORDER BY r.id DESC LIMIT ? OFFSET ?'
|
|
|
+ params.push(String(page.pagesize), String(page.offset))
|
|
|
+
|
|
|
+ const rows = await db.query(sql, params)
|
|
|
+ const totalRows = await db.query(countSql, countParams)
|
|
|
+ if (!rows || !totalRows) return '获取赠送记录失败'
|
|
|
+
|
|
|
+ return asJson({
|
|
|
+ list: rows.map(mapSendCountRequest),
|
|
|
+ pagination: pagination(page, totalRows)
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ async list_work_orders(args) {
|
|
|
+ const { sender, current, pagesize } = args
|
|
|
+ if (required([sender])) return '缺少 sender'
|
|
|
+ const page = pageArgs({ current, pagesize })
|
|
|
+
|
|
|
+ const rows = await db.query(
|
|
|
+ `SELECT id, title, state, email, create_time, update_time
|
|
|
+ FROM work_order
|
|
|
+ WHERE create_user = ?
|
|
|
+ ORDER BY update_time DESC
|
|
|
+ LIMIT ? OFFSET ?`,
|
|
|
+ [sender, String(page.pagesize), String(page.offset)]
|
|
|
+ )
|
|
|
+ const totalRows = await db.query(
|
|
|
+ `SELECT COUNT(*) AS total
|
|
|
+ FROM work_order
|
|
|
+ WHERE create_user = ?`,
|
|
|
+ [sender]
|
|
|
+ )
|
|
|
+ if (!rows || !totalRows) return '查询工单列表失败'
|
|
|
+
|
|
|
+ return asJson({
|
|
|
+ list: rows.map(mapWorkOrder),
|
|
|
+ pagination: pagination(page, totalRows)
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ async get_work_order_detail(args) {
|
|
|
+ const { sender, work_order_id } = args
|
|
|
+ if (required([sender, work_order_id])) return '缺少 sender 或 work_order_id'
|
|
|
+
|
|
|
+ const rows = await db.query(
|
|
|
+ `SELECT id, title, msg, state, email, create_time, update_time
|
|
|
+ FROM work_order
|
|
|
+ WHERE create_user = ? AND id = ?`,
|
|
|
+ [sender, work_order_id]
|
|
|
+ )
|
|
|
+ if (!rows || rows.length !== 1) return '工单异常,请提交新工单'
|
|
|
+
|
|
|
+ const order = mapWorkOrder(rows[0])
|
|
|
+ const msgList = Array.isArray(rows[0].msg) ? rows[0].msg : parseJson(rows[0].msg, [])
|
|
|
+ const uuidList = [...new Set(msgList.map(msg => msg?.uuid).filter(Boolean))]
|
|
|
+ let userInfo = {}
|
|
|
+ if (uuidList.length > 0) {
|
|
|
+ const placeholders = uuidList.map(() => '?').join(', ')
|
|
|
+ const userRows = await db.query(`SELECT uuid, username, avatar FROM users WHERE uuid IN (${placeholders})`, uuidList)
|
|
|
+ if (!userRows || userRows.length === 0) return '用户信息获取失败'
|
|
|
+ userRows.forEach(user => {
|
|
|
+ userInfo[user.uuid] = {
|
|
|
+ username: user.username,
|
|
|
+ avatar: user.avatar
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ return asJson({
|
|
|
+ ...order,
|
|
|
+ msg: msgList.map(mapWorkOrderMessage),
|
|
|
+ userInfo
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
async query_qxs_book_list(args) {
|
|
|
const { sender, username, password, current, pagesize } = args
|
|
|
if (required([sender, username, password])) return '缺少 sender、username 或 password'
|