UNPKG

koishi-plugin-pay-tool

Version:

适用于Koishi框架的易支付工具插件,支持订单创建、查询、退款、分配等功能

582 lines (581 loc) 29.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.sendPaymentSuccessNotification = sendPaymentSuccessNotification; exports.setupCommands = setupCommands; exports.cleanupAllPollings = cleanupAllPollings; const koishi_1 = require("koishi"); const utils_1 = require("./utils"); // 简单的轮询跟踪 const activePollings = new Map(); /** * 通用消息发送函数,处理私聊和群聊的不同格式 */ async function sendMessage(session, content, options) { try { const shouldQuote = options?.quote !== false; const shouldMention = options?.mention !== false; // 检查是否为私聊 const isPrivate = session.channelId?.startsWith('private:'); // 构建消息元素 const elements = []; if (shouldQuote) { elements.push(koishi_1.h.quote(session.messageId)); } // 只有在非私聊且需要@时才添加@ if (!isPrivate && shouldMention) { elements.push(koishi_1.h.at(session.userId), '\n'); } elements.push(...content); return await session.send(elements); } catch (error) { throw new Error(`发送消息失败: ${error?.message || '未知错误'}`); } } /** * 发送支付成功通知到指定会话 */ async function sendPaymentSuccessNotification(ctx, config, logger, orderRecord, successMessages, triggerSource = "未知") { const targetChannelId = orderRecord.channel_id; const targetGuildId = orderRecord.guild_id; let messageSent = false; for (const bot of ctx.bots) { try { if (targetGuildId && targetChannelId) { // 群聊通知 await bot.sendMessage(targetChannelId, (0, koishi_1.h)('message', { forward: true }, [ (0, koishi_1.h)('message', {}, successMessages.join('\n')) ])); logger.info(`已发送支付成功通知到群聊 ${targetGuildId}:${targetChannelId} (触发源: ${triggerSource})`); } else { // 私聊通知 await bot.sendPrivateMessage(orderRecord.user_id, (0, koishi_1.h)('message', { forward: true }, [ (0, koishi_1.h)('message', {}, successMessages.join('\n')) ])); logger.info(`已发送支付成功通知到用户 ${orderRecord.user_id} (触发源: ${triggerSource})`); } messageSent = true; break; // 成功发送后退出循环 } catch (botError) { logger.warn(`Bot ${bot.platform}:${bot.selfId} 发送消息失败: ${botError?.message}`); } } if (!messageSent) { throw new Error('所有Bot都发送失败'); } } function setupCommands(ctx, config, epayClient, orderDb, logger) { // 支付指令组 const payCmd = ctx.command('pay', '支付工具'); // 创建订单指令 payCmd.subcommand('.create <amount:number> [payment:string]', '创建支付订单') .action(async ({ session }, amount, payment) => { if (!session || !session.userId) return; // 验证管理员权限 if (!(0, utils_1.isAdmin)(session.userId, config.adminQQ)) { return '❌ 此指令仅限管理员使用'; } try { // 验证参数格式 if (!amount || !(0, utils_1.isValidAmount)(amount.toString())) { await sendMessage(session, ['❌ 参数错误:金额必须是大于0的数字(最大99999)\n📖 正确格式:pay create <金额> [支付方式]']); return; } // 验证并确定支付方式 let paymentType; if (payment) { // 验证用户提供的支付方式 const validatedPayment = (0, utils_1.validateAndConvertPaymentType)(payment, config.paymentMethods); if (!validatedPayment) { const availableMethods = (0, utils_1.getAvailablePaymentMethods)(config.paymentMethods); await sendMessage(session, [`❌ 参数错误:不支持的支付方式 "${payment}"\n📖 正确格式:pay create <金额> [支付方式]\n💳 支持的支付方式:${availableMethods}`]); return; } paymentType = validatedPayment; } else { // 使用默认支付方式,但也要验证默认支付方式是否有效 const defaultPayment = (0, utils_1.validateAndConvertPaymentType)(config.defaultPayment, config.paymentMethods); if (!defaultPayment) { const availableMethods = (0, utils_1.getAvailablePaymentMethods)(config.paymentMethods); await sendMessage(session, [`❌ 配置错误:默认支付方式 "${config.defaultPayment}" 无效\n📖 支持的支付方式:${availableMethods}`]); return; } paymentType = defaultPayment; } // 使用配置的回调通知地址 const notifyUrl = config.notifyUrl; // 生成商户订单号 const outTradeNo = (0, utils_1.generateOrderNo)(session.userId); // 重要操作日志 - 始终显示 logger.info(`用户 ${session.userId} 创建订单,金额: ${amount},支付方式: ${paymentType},订单号: ${outTradeNo}`); // 调用API创建订单 const orderResult = await epayClient.createOrder(amount, paymentType, outTradeNo, notifyUrl, config.returnUrl); // 保存订单到数据库 await orderDb.createOrder(orderResult.trade_no, // 易支付订单号 outTradeNo, // 我们生成的商户订单号 session.userId, session.guildId || '', session.channelId || '', (0, utils_1.formatAmount)(amount), paymentType); // 构建支付信息合并消息 const paymentTypeText = (0, utils_1.formatPaymentType)(paymentType, config.paymentMethods); const messages = [ `✅ 订单创建成功!`, `📋 订单号: ${outTradeNo}`, `💰 订单金额: ¥${(0, utils_1.formatAmount)(amount)}`, `💳 支付方式: ${paymentTypeText}`, `⏰ 请在30分钟内完成支付` ]; // 发送订单创建成功消息 if (orderResult.img) { await session.send((0, koishi_1.h)('message', { forward: true }, [ (0, koishi_1.h)('message', {}, messages.join('\n')), (0, koishi_1.h)('message', {}, ['💳 支付二维码:', koishi_1.h.image(orderResult.img)]) ])); } else { await session.send((0, koishi_1.h)('message', { forward: true }, [ (0, koishi_1.h)('message', {}, messages.join('\n')) ])); } // 启动主动查询模式(如果启用) if (config.activeQueryEnabled) { startActivePolling(ctx, config, epayClient, orderDb, logger, outTradeNo, session); } } catch (error) { logger.error(`创建订单失败: ${error?.message || '未知错误'}`, error); // 根据用户身份发送不同错误信息 if ((0, utils_1.isAdmin)(session.userId, config.adminQQ)) { // 管理员错误信息:根据devMode决定详细程度 if (config.devMode) { // 开发模式:显示详细错误信息 await session.send((0, koishi_1.h)('message', { forward: true }, [ (0, koishi_1.h)('message', {}, `❌ 订单创建失败`), (0, koishi_1.h)('message', {}, `错误详情: ${error?.message || '未知错误'}`) ])); } else { // 生产模式:只显示简洁错误 await session.send(`❌ 订单创建失败: ${error?.message || '未知错误'}`); } } else { // 普通用户看简单错误 await session.send('❌ 订单创建失败,请稍后重试'); // 通知管理员详细错误 try { for (const bot of ctx.bots) { await bot.sendPrivateMessage(config.adminQQ, (0, koishi_1.h)('message', { forward: true }, [ (0, koishi_1.h)('message', {}, `❌ 用户 ${session.userId} 创建订单失败`), (0, koishi_1.h)('message', {}, `错误详情: ${error?.message || '未知错误'}`) ])); } } catch (e) { logger.error(`无法通知管理员: ${e?.message || '未知错误'}`); } } return; } }); // 查询订单指令 payCmd.subcommand('.query <target:string>', '查询订单状态或用户订单') .action(async ({ session }, target) => { if (!session || !session.userId) return; // 验证管理员权限 if (!(0, utils_1.isAdmin)(session.userId, config.adminQQ)) { await sendMessage(session, ['❌ 此指令仅限管理员使用']); return; } try { // 验证参数格式 if (!target || target.trim() === '') { await sendMessage(session, ['❌ 参数错误:请提供订单号或@用户\n📖 正确格式:pay query <订单号> 或 pay query @用户']); return; } // 尝试解析@用户 const normalizedQQ = (0, utils_1.normalizeQQId)(target); if (normalizedQQ) { // 如果是有效的QQ号,查询该用户的所有订单 // 重要操作日志 - 始终显示 logger.info(`管理员 ${session.userId} 查询用户订单: ${normalizedQQ}`); const userOrders = await orderDb.getOrdersByCustomerQQ(normalizedQQ); if (userOrders.length === 0) { await sendMessage(session, [`❌ 未找到用户 ${normalizedQQ} 的订单记录`]); return; } const orderList = userOrders.map(order => { const statusText = order.status === 'paid' ? '✅ 已支付' : order.status === 'refunded' ? '💰 已退款' : order.status === 'failed' ? '❌ 失败' : '⏳ 未支付'; return `📋 ${order.out_trade_no} - ${statusText}`; }).join('\n'); await sendMessage(session, [`👤 用户 ${normalizedQQ} 的订单列表:\n${orderList}`], { mention: false }); return; } else { // 如果不是QQ号,当作订单号处理 const tradeNo = target; // 验证订单号格式 if (!(0, utils_1.isValidTradeNo)(tradeNo)) { await sendMessage(session, ['❌ 参数错误:订单号格式无效(应为10-25位数字)\n📖 正确格式:pay query <订单号> 或 pay query @用户']); return; } // 重要操作日志 - 始终显示 logger.info(`管理员 ${session.userId} 查询订单: ${tradeNo}`); // 查询本地数据库 const localOrder = await orderDb.getOrderByOutTradeNo(tradeNo) || await orderDb.getOrderByTradeNo(tradeNo); if (!localOrder) { await sendMessage(session, ['❌ 未找到该订单记录']); return; } // 如果启用主动查询模式且该订单正在轮询中,立即触发一次查询 if (config.activeQueryEnabled && activePollings.has(localOrder.out_trade_no)) { if (config.devMode) { logger.info(`强制触发订单查询: ${localOrder.out_trade_no}`); } try { // 直接查询API,不依赖轮询逻辑 const orderStatus = await epayClient.queryOrder(localOrder.out_trade_no); // 更新本地订单状态 const newStatus = (orderStatus.status == 1 || orderStatus.status === '1') ? 'paid' : 'pending'; if (localOrder.status !== newStatus) { await orderDb.updateOrderStatus(localOrder.out_trade_no, newStatus); } // 如果支付成功,清理轮询 if ((orderStatus.status == 1 || orderStatus.status === '1')) { activePollings.delete(localOrder.out_trade_no); } // 构建查询结果消息 const statusText = (orderStatus.status == 1 || orderStatus.status === '1') ? '✅ 已支付' : '⏳ 未支付'; const paymentTypeText = (0, utils_1.formatPaymentType)(localOrder.payment_type, config.paymentMethods); let queryResult = `📋 订单查询结果:\n📋 订单号: ${orderStatus.out_trade_no}\n💰 订单金额: ¥${orderStatus.money}\n💳 支付方式: ${paymentTypeText}`; // 如果订单有归属人,添加归属人信息 if (localOrder.customer_qq) { queryResult += `\n👤 订单归属人: ${localOrder.customer_qq}`; } queryResult += `\n📊 支付状态: ${statusText}\n📅 创建时间: ${orderStatus.addtime}`; await sendMessage(session, [queryResult], { mention: false }); return; } catch (error) { // 内部错误 - 只在devMode下显示 if (config.devMode) { logger.error(`强制查询订单失败: ${error?.message || '未知错误'}`, error); } // 继续执行正常的查询逻辑 } } // 调用API查询最新状态 const orderStatus = await epayClient.queryOrder(localOrder.out_trade_no); // 更新本地订单状态 const newStatus = (orderStatus.status == 1 || orderStatus.status === '1') ? 'paid' : 'pending'; if (localOrder.status !== newStatus) { await orderDb.updateOrderStatus(localOrder.out_trade_no, newStatus); } // 构建查询结果消息 const statusText = (orderStatus.status == 1 || orderStatus.status === '1') ? '✅ 已支付' : '⏳ 未支付'; const paymentTypeText = (0, utils_1.formatPaymentType)(localOrder.payment_type, config.paymentMethods); let queryResult = `📋 订单查询结果:\n📋 订单号: ${orderStatus.out_trade_no}\n💰 订单金额: ¥${orderStatus.money}\n💳 支付方式: ${paymentTypeText}`; // 如果订单有归属人,添加归属人信息 if (localOrder.customer_qq) { queryResult += `\n👤 订单归属人: ${localOrder.customer_qq}`; } queryResult += `\n📊 支付状态: ${statusText}\n📅 创建时间: ${orderStatus.addtime}`; if (orderStatus.endtime) { queryResult += `\n✅ 完成时间: ${orderStatus.endtime}`; } // 直接回复给查询用户 await sendMessage(session, [queryResult], { mention: false }); } } catch (error) { logger.error(`查询订单失败: ${error?.message || '未知错误'}`, error); // 根据devMode决定错误信息详细程度 if (config.devMode) { await sendMessage(session, [`❌ 查询订单失败: ${error?.message || '未知错误'}`]); } else { await sendMessage(session, ['❌ 查询订单失败,请稍后重试']); } } }); // 退款指令 payCmd.subcommand('.refund <tradeNo:string>', '申请退款') .action(async ({ session }, tradeNo) => { if (!session || !session.userId) return; // 验证管理员权限 if (!(0, utils_1.isAdmin)(session.userId, config.adminQQ)) { await sendMessage(session, ['❌ 此指令仅限管理员使用']); return; } try { // 验证参数格式 if (!tradeNo || !(0, utils_1.isValidTradeNo)(tradeNo)) { await sendMessage(session, ['❌ 参数错误:订单号格式无效(应为10-25位数字)\n📖 正确格式:pay refund <订单号>']); return; } if (config.devMode) { logger.info(`管理员 ${session.userId} 申请退款: ${tradeNo}`); } // 查询本地订单 const localOrder = await orderDb.getOrderByOutTradeNo(tradeNo) || await orderDb.getOrderByTradeNo(tradeNo); if (!localOrder) { await sendMessage(session, ['❌ 未找到该订单记录']); return; } if (localOrder.status !== 'paid') { await sendMessage(session, ['❌ 只有已支付的订单才能退款']); return; } // 调用退款API await epayClient.refundOrder(tradeNo, localOrder.amount); // 更新本地订单状态 await orderDb.updateOrderStatus(localOrder.out_trade_no, 'refunded'); // 构建退款成功消息 const messages = [ `✅ 退款成功!`, `📋 订单号: ${tradeNo}`, `💰 退款金额: ¥${localOrder.amount}` ]; // 发送到创建订单时的会话 const targetChannelId = localOrder.channel_id; const targetGuildId = localOrder.guild_id; for (const bot of ctx.bots) { try { if (targetGuildId && targetChannelId) { // 群聊通知 await bot.sendMessage(targetChannelId, (0, koishi_1.h)('message', { forward: true }, [ (0, koishi_1.h)('message', {}, messages.join('\n')) ])); } else { // 私聊通知 await bot.sendPrivateMessage(localOrder.user_id, (0, koishi_1.h)('message', { forward: true }, [ (0, koishi_1.h)('message', {}, messages.join('\n')) ])); } break; // 成功发送后退出循环 } catch (botError) { logger.warn(`Bot ${bot.platform}:${bot.selfId} 退款通知发送失败: ${botError?.message}`); } } } catch (error) { logger.error(`申请退款失败: ${error?.message || '未知错误'}`, error); // 根据devMode决定错误信息详细程度 if (config.devMode) { await sendMessage(session, [`❌ 申请退款失败: ${error?.message || '未知错误'}`]); } else { await sendMessage(session, ['❌ 申请退款失败,请稍后重试']); } } }); // 订单分配指令 payCmd.subcommand('.provisioning <tradeNo:string> <targetUser:string>', '分配订单给指定用户') .action(async ({ session }, tradeNo, targetUser) => { if (!session || !session.userId) return; // 验证管理员权限 if (!(0, utils_1.isAdmin)(session.userId, config.adminQQ)) { await sendMessage(session, ['❌ 此指令仅限管理员使用']); return; } try { // 验证参数格式 if (!tradeNo || !(0, utils_1.isValidTradeNo)(tradeNo)) { await sendMessage(session, ['❌ 参数错误:订单号格式无效(应为10-25位数字)\n📖 正确格式:pay provisioning <订单号> @用户']); return; } if (!targetUser || targetUser.trim() === '') { await sendMessage(session, ['❌ 参数错误:请提供目标用户QQ号或@用户\n📖 正确格式:pay provisioning <订单号> @用户']); return; } // 解析目标用户QQ号 const customerQQ = (0, utils_1.normalizeQQId)(targetUser); if (!customerQQ) { await sendMessage(session, ['❌ 参数错误:无效的用户QQ号格式(应为5-12位数字或@用户)\n📖 正确格式:pay provisioning <订单号> @用户']); return; } // 重要操作日志 - 始终显示 logger.info(`管理员 ${session.userId} 分配订单 ${tradeNo} 给用户 ${customerQQ}`); // 查询订单是否存在 const localOrder = await orderDb.getOrderByOutTradeNo(tradeNo) || await orderDb.getOrderByTradeNo(tradeNo); if (!localOrder) { await sendMessage(session, ['❌ 未找到该订单记录']); return; } // 更新订单归属人 await orderDb.updateCustomerQQ(localOrder.out_trade_no, customerQQ); const messages = [ `✅ 订单分配成功!`, `📋 订单号: ${localOrder.out_trade_no}`, `💰 订单金额: ¥${localOrder.amount}`, `👤 归属用户: ${customerQQ}` ]; await session.send((0, koishi_1.h)('message', { forward: true }, [ (0, koishi_1.h)('message', {}, messages.join('\n')) ])); } catch (error) { logger.error(`订单分配失败: ${error?.message || '未知错误'}`, error); // 根据devMode决定错误信息详细程度 if (config.devMode) { await sendMessage(session, [`❌ 订单分配失败: ${error?.message || '未知错误'}`]); } else { await sendMessage(session, ['❌ 订单分配失败,请稍后重试']); } } }); return payCmd; } /** * 清理所有活跃轮询 */ function cleanupAllPollings() { for (const [outTradeNo, polling] of activePollings) { if (polling.timer) { clearTimeout(polling.timer); } } activePollings.clear(); } /** * 启动主动查询轮询 */ async function startActivePolling(ctx, config, epayClient, orderDb, logger, outTradeNo, session) { // 先检查订单是否已超过过期时间 const localOrder = await orderDb.getOrderByOutTradeNo(outTradeNo); if (localOrder) { const now = new Date(); const createdAt = new Date(localOrder.created_at); const timeDiff = now.getTime() - createdAt.getTime(); const expirationTime = (config.orderExpirationTime || 30) * 60 * 1000; // 转换为毫秒 if (timeDiff > expirationTime) { if (config.devMode) { logger.info(`订单 ${outTradeNo} 已超过${config.orderExpirationTime || 30}分钟时效,不启动主动查询`); } return; // 不启动轮询 } } const initialWaitTime = config.initialWaitTime || 30000; const pollingInterval = config.pollingInterval || 30000; const maxPollingCount = 60; let pollingCount = 0; if (config.devMode) { logger.info(`启动主动查询模式,订单号: ${outTradeNo},等待时长: ${initialWaitTime}ms,轮询间隔: ${pollingInterval}ms`); } // 查询函数 const doQuery = async () => { try { pollingCount++; if (config.devMode) { logger.info(`主动查询订单状态,订单号: ${outTradeNo},第${pollingCount}次查询`); } // 检查订单是否已超过过期时间 const localOrder = await orderDb.getOrderByOutTradeNo(outTradeNo); if (localOrder) { const now = new Date(); const createdAt = new Date(localOrder.created_at); const timeDiff = now.getTime() - createdAt.getTime(); const expirationTime = (config.orderExpirationTime || 30) * 60 * 1000; // 转换为毫秒 if (timeDiff > expirationTime) { if (config.devMode) { logger.info(`订单 ${outTradeNo} 已超过${config.orderExpirationTime || 30}分钟时效,停止主动查询`); } activePollings.delete(outTradeNo); return; // 停止轮询 } } const orderStatus = await epayClient.queryOrder(outTradeNo); if ((orderStatus.status == 1 || orderStatus.status === '1')) { // 支付成功,清理轮询 activePollings.delete(outTradeNo); // 检查订单是否已经处理过(防止重复通知) if (localOrder?.status === 'paid') { logger.info(`订单 ${outTradeNo} 已支付完成,跳过主动查询通知`); return; // 已经通知过,直接返回 } await orderDb.updateOrderStatus(outTradeNo, 'paid'); // 发送通知 - 使用创建订单时的支付方式,而不是API返回的 const paymentTypeText = (0, utils_1.formatPaymentType)(localOrder?.payment_type || orderStatus.type, config.paymentMethods); const successMessages = [ `🎉 支付成功!`, `📋 订单号: ${outTradeNo}`, `💰 支付金额: ¥${orderStatus.money}`, `💳 支付方式: ${paymentTypeText}`, `⏰ 支付时间: ${new Date().toLocaleString('zh-CN')}` ]; // 如果订单有归属人,添加归属人信息 if (localOrder?.customer_qq) { successMessages.splice(4, 0, `👤 订单归属人: ${localOrder.customer_qq}`); } // 发送通知到原会话 try { // 确保使用最新的订单数据 const currentOrder = await orderDb.getOrderByOutTradeNo(outTradeNo); if (!currentOrder) { logger.error(`主动查询通知失败:无法获取订单 ${outTradeNo} 的最新数据`); return; } // 调试日志:对比订单数据 if (config.devMode) { logger.info(`主动查询订单数据: guild_id=${currentOrder.guild_id}, channel_id=${currentOrder.channel_id}, user_id=${currentOrder.user_id}`); } await sendPaymentSuccessNotification(ctx, config, logger, currentOrder, successMessages, "主动查询"); } catch (error) { // 内部错误 - 只在devMode下显示 if (config.devMode) { logger.error(`发送支付成功通知失败: ${error?.message || '未知错误'}`); } } return; // 支付成功,停止轮询 } // 检查最大轮询次数 if (pollingCount >= maxPollingCount) { if (config.devMode) { logger.info(`订单 ${outTradeNo} 达到最大轮询次数,停止查询`); } activePollings.delete(outTradeNo); return; } // 继续轮询 const polling = activePollings.get(outTradeNo); if (polling) { polling.timer = setTimeout(doQuery, pollingInterval); } } catch (error) { // 内部错误 - 只在devMode下显示 if (config.devMode) { logger.error(`主动查询订单失败: ${error?.message || '未知错误'}`); } // 查询失败也继续轮询 if (pollingCount < maxPollingCount) { const polling = activePollings.get(outTradeNo); if (polling) { polling.timer = setTimeout(doQuery, pollingInterval); } } else { activePollings.delete(outTradeNo); } } }; // 注册轮询 activePollings.set(outTradeNo, { session, timer: setTimeout(doQuery, initialWaitTime) }); }