index.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. require('dotenv').config();
  2. const express = require('express');
  3. const cors = require('cors');
  4. const path = require('path');
  5. const TelegramBot = require('node-telegram-bot-api');
  6. const fs = require('fs');
  7. const moment = require('moment');
  8. const { pool, testConnection } = require('./config/database');
  9. const initDatabase = require('./config/initDb');
  10. const Group = require('./models/Group');
  11. const Transaction = require('./models/Transaction');
  12. const app = express();
  13. // 初始化数据存储
  14. let data = {
  15. deposits: [], // 入款记录
  16. withdrawals: [], // 下发记录
  17. lastUpdate: null,
  18. allowedGroups: ['4754375683'] // 允许使用的群组ID
  19. };
  20. // 创建机器人实例
  21. const bot = new TelegramBot(process.env.BOT_TOKEN, { polling: true });
  22. // 中间件
  23. app.use(cors());
  24. app.use(express.json());
  25. app.use(express.static('views'));
  26. // 路由
  27. app.get('/', (req, res) => {
  28. res.sendFile(path.join(__dirname, 'views', 'login.html'));
  29. });
  30. app.use('/api/users', require('./routes/userRoutes'));
  31. app.use('/api/groups', require('./routes/groupRoutes'));
  32. app.use('/api/transactions', require('./routes/transactionRoutes'));
  33. // 检查群组权限
  34. function isGroupAllowed(chatId) {
  35. const chatIdStr = chatId.toString();
  36. return data.allowedGroups.includes(chatIdStr) ||
  37. data.allowedGroups.includes(chatIdStr.replace('-', ''));
  38. }
  39. // 检查是否是管理员
  40. function isAdmin(userId) {
  41. return process.env.ADMIN_IDS.split(',').includes(userId.toString());
  42. }
  43. // 处理消息发送
  44. async function sendMessage(chatId, text, options = {}) {
  45. try {
  46. // 如果包含内联键盘,验证URL
  47. if (options.reply_markup && options.reply_markup.inline_keyboard) {
  48. const keyboard = generateInlineKeyboard(chatId);
  49. if (!keyboard) {
  50. // 如果键盘无效,发送不带键盘的消息
  51. return await bot.sendMessage(chatId, text, { parse_mode: 'HTML' });
  52. }
  53. options.reply_markup = keyboard;
  54. }
  55. return await bot.sendMessage(chatId, text, { ...options, parse_mode: 'HTML' });
  56. } catch (error) {
  57. console.error('发送消息失败:', error);
  58. if (error.message.includes('bot was kicked from the group chat')) {
  59. const index = data.allowedGroups.indexOf(chatId.toString());
  60. if (index > -1) {
  61. data.allowedGroups.splice(index, 1);
  62. saveData();
  63. console.log(`群组 ${chatId} 已被移除出允许列表`);
  64. }
  65. }
  66. return null;
  67. }
  68. }
  69. // 处理快捷命令
  70. bot.on('message', async (msg) => {
  71. if (!isGroupAllowed(msg.chat.id)) return;
  72. const text = msg.text?.trim();
  73. if (!text) return;
  74. if (text.startsWith('+')) {
  75. const amount = parseFloat(text.substring(1));
  76. if (!isNaN(amount)) {
  77. const transactionData = {
  78. groupId: msg.chat.id.toString(),
  79. groupName: msg.chat.title || '未命名群组',
  80. amount: amount
  81. };
  82. try {
  83. const result = await Transaction.deposit(transactionData);
  84. if (result.success) {
  85. const billMessage = await generateBillMessage(msg.chat.id);
  86. if (billMessage) {
  87. await sendMessage(msg.chat.id, billMessage, {
  88. reply_markup: generateInlineKeyboard(msg.chat.id)
  89. });
  90. console.log(`入款成功 - 群组: ${msg.chat.title}, 金额: ${amount}, 时间: ${new Date().toLocaleString()}`);
  91. } else {
  92. await sendMessage(msg.chat.id, '入款成功,但获取账单信息失败');
  93. console.log(`入款成功(无账单) - 群组: ${msg.chat.title}, 金额: ${amount}, 时间: ${new Date().toLocaleString()}`);
  94. }
  95. } else {
  96. await sendMessage(msg.chat.id, result.message || '入款失败');
  97. console.log(`入款失败 - 群组: ${msg.chat.title}, 金额: ${amount}, 原因: ${result.message}, 时间: ${new Date().toLocaleString()}`);
  98. }
  99. } catch (error) {
  100. console.error('快捷入款失败:', error);
  101. await sendMessage(msg.chat.id, '记录入款失败,请稍后重试');
  102. }
  103. }
  104. } else if (text.startsWith('-')) {
  105. const amount = parseFloat(text.substring(1));
  106. if (!isNaN(amount)) {
  107. const transactionData = {
  108. groupId: msg.chat.id.toString(),
  109. groupName: msg.chat.title || '未命名群组',
  110. amount: amount
  111. };
  112. try {
  113. const result = await Transaction.withdrawal(transactionData);
  114. if (result.success) {
  115. const billMessage = await generateBillMessage(msg.chat.id);
  116. if (billMessage) {
  117. await sendMessage(msg.chat.id, billMessage, {
  118. reply_markup: generateInlineKeyboard(msg.chat.id)
  119. });
  120. console.log(`出款成功 - 群组: ${msg.chat.title}, 金额: ${amount}, 时间: ${new Date().toLocaleString()}`);
  121. } else {
  122. await sendMessage(msg.chat.id, '出款成功,但获取账单信息失败');
  123. console.log(`出款成功(无账单) - 群组: ${msg.chat.title}, 金额: ${amount}, 时间: ${new Date().toLocaleString()}`);
  124. }
  125. } else {
  126. await sendMessage(msg.chat.id, result.message || '出款失败');
  127. console.log(`出款失败 - 群组: ${msg.chat.title}, 金额: ${amount}, 原因: ${result.message}, 时间: ${new Date().toLocaleString()}`);
  128. }
  129. } catch (error) {
  130. console.error('快捷出款失败:', error);
  131. await sendMessage(msg.chat.id, '记录出款失败,请稍后重试');
  132. }
  133. }
  134. }
  135. });
  136. // 处理新成员加入
  137. bot.on('new_chat_members', async (msg) => {
  138. const chatId = msg.chat.id;
  139. const newMembers = msg.new_chat_members;
  140. for (const member of newMembers) {
  141. if (member.id === (await bot.getMe()).id) {
  142. // 机器人被添加到群组
  143. console.log(`机器人被添加到群组: ${chatId}`);
  144. // 检查群组是否在允许列表中
  145. const chatIdStr = chatId.toString();
  146. try {
  147. // 先检查数据库中是否存在该群组
  148. const existingGroup = await Group.findByGroupId(chatIdStr);
  149. if (existingGroup) {
  150. // 如果群组存在,更新群组状态为活跃,同时更新群组名称和加入时间
  151. await pool.query(`
  152. UPDATE groups
  153. SET is_active = true,
  154. group_name = ?,
  155. last_join_time = CURRENT_TIMESTAMP
  156. WHERE group_id = ?
  157. `, [msg.chat.title || existingGroup.group_name, chatIdStr]);
  158. // 更新内存中的群组列表
  159. if (!data.allowedGroups.includes(chatIdStr)) {
  160. data.allowedGroups.push(chatIdStr);
  161. saveData();
  162. }
  163. // 发送欢迎消息并显示当前账单
  164. await sendMessage(chatId, '感谢重新添加我为群组成员!');
  165. const billMessage = await generateBillMessage(chatId);
  166. if (billMessage) {
  167. await sendMessage(chatId, billMessage, {
  168. reply_markup: generateInlineKeyboard(chatId)
  169. });
  170. }
  171. } else {
  172. // 如果群组不存在,创建新群组
  173. const groupData = {
  174. groupId: chatIdStr,
  175. groupName: msg.chat.title || '未命名群组',
  176. groupType: msg.chat.type === 'private' ? 'personal' : 'public',
  177. creatorId: msg.from.id.toString()
  178. };
  179. const id = await Group.create({
  180. groupId: groupData.groupId,
  181. groupName: groupData.groupName,
  182. creatorId: groupData.creatorId
  183. });
  184. const group = await Group.findById(id);
  185. if (group) {
  186. // 更新内存中的群组列表
  187. data.allowedGroups.push(chatIdStr);
  188. saveData();
  189. await sendMessage(chatId, '感谢添加我为群组成员!使用 /help 查看可用命令。');
  190. } else {
  191. await sendMessage(chatId, '添加群组失败,请联系管理员。');
  192. }
  193. }
  194. } catch (error) {
  195. console.error('处理群组加入失败:', error);
  196. await sendMessage(chatId, '添加群组失败,请联系管理员。');
  197. }
  198. } else {
  199. // 其他新成员
  200. console.log(`新成员加入群组: ${member.username || member.first_name} (${member.id})`);
  201. await sendMessage(chatId, `欢迎 ${member.username || member.first_name} 加入群组!`);
  202. }
  203. }
  204. });
  205. // 处理管理员命令
  206. bot.onText(/\/addgroup (.+)/, async (msg, match) => {
  207. if (!isAdmin(msg.from.id)) {
  208. sendMessage(msg.chat.id, '您没有权限执行此命令。');
  209. return;
  210. }
  211. const groupId = match[1].trim();
  212. if (!data.allowedGroups.includes(groupId)) {
  213. try {
  214. // 使用 createGroup 创建新群组
  215. const groupData = {
  216. groupId: groupId,
  217. groupName: '手动添加的群组',
  218. groupType: 'public',
  219. creatorId: msg.from.id.toString()
  220. };
  221. const result = await createGroup({ body: groupData });
  222. if (result) {
  223. data.allowedGroups.push(groupId);
  224. saveData();
  225. sendMessage(msg.chat.id, `群组 ${groupId} 已添加到允许列表。`);
  226. } else {
  227. sendMessage(msg.chat.id, '添加群组失败,请检查群组ID是否正确。');
  228. }
  229. } catch (error) {
  230. console.error('创建群组失败:', error);
  231. sendMessage(msg.chat.id, '添加群组失败,请稍后重试。');
  232. }
  233. } else {
  234. sendMessage(msg.chat.id, '该群组已在允许列表中。');
  235. }
  236. });
  237. bot.onText(/\/removegroup (.+)/, async (msg, match) => {
  238. if (!isAdmin(msg.from.id)) {
  239. sendMessage(msg.chat.id, '您没有权限执行此命令。');
  240. return;
  241. }
  242. const groupId = match[1].trim();
  243. try {
  244. // 使用 updateGroup 更新群组状态
  245. const result = await updateGroup({
  246. params: { id: groupId },
  247. body: { isActive: false }
  248. });
  249. if (result) {
  250. const index = data.allowedGroups.indexOf(groupId);
  251. if (index > -1) {
  252. data.allowedGroups.splice(index, 1);
  253. saveData();
  254. sendMessage(msg.chat.id, `群组 ${groupId} 已从允许列表中移除。`);
  255. } else {
  256. sendMessage(msg.chat.id, '该群组不在允许列表中。');
  257. }
  258. } else {
  259. sendMessage(msg.chat.id, '移除群组失败,请稍后重试。');
  260. }
  261. } catch (error) {
  262. console.error('更新群组状态失败:', error);
  263. sendMessage(msg.chat.id, '移除群组失败,请稍后重试。');
  264. }
  265. });
  266. bot.onText(/\/listgroups/, async (msg) => {
  267. if (!isAdmin(msg.from.id)) {
  268. sendMessage(msg.chat.id, '您没有权限执行此命令。');
  269. return;
  270. }
  271. try {
  272. const groups = await pool.query('SELECT group_id, group_name, group_type, is_active FROM groups WHERE is_active = 1');
  273. if (groups.length === 0) {
  274. sendMessage(msg.chat.id, '当前没有允许的群组。');
  275. return;
  276. }
  277. const groupsList = groups.map(group =>
  278. `ID: ${group.group_id}\n名称: ${group.group_name}\n类型: ${group.group_type}\n状态: ${group.is_active ? '启用' : '禁用'}`
  279. ).join('\n\n');
  280. sendMessage(msg.chat.id, `允许的群组列表:\n\n${groupsList}`);
  281. } catch (error) {
  282. console.error('获取群组列表失败:', error);
  283. sendMessage(msg.chat.id, '获取群组列表失败,请稍后重试。');
  284. }
  285. });
  286. // 处理入款命令
  287. bot.onText(/\/deposit (.+)/, async (msg, match) => {
  288. if (!isGroupAllowed(msg.chat.id)) {
  289. sendMessage(msg.chat.id, '该群组未授权使用此功能');
  290. return;
  291. }
  292. const amount = parseFloat(match[1]);
  293. if (isNaN(amount)) {
  294. sendMessage(msg.chat.id, '请输入有效的金额');
  295. return;
  296. }
  297. const transactionData = {
  298. groupId: msg.chat.id.toString(),
  299. groupName: msg.chat.title || '未命名群组',
  300. amount: amount
  301. };
  302. try {
  303. const result = await Transaction.deposit(transactionData);
  304. if (result.success) {
  305. const billMessage = await generateBillMessage(msg.chat.id);
  306. if (billMessage) {
  307. await sendMessage(msg.chat.id, billMessage, {
  308. reply_markup: generateInlineKeyboard(msg.chat.id)
  309. });
  310. console.log(`入款成功 - 群组: ${msg.chat.title}, 金额: ${amount}, 时间: ${new Date().toLocaleString()}`);
  311. } else {
  312. await sendMessage(msg.chat.id, '入款成功,但获取账单信息失败');
  313. console.log(`入款成功(无账单) - 群组: ${msg.chat.title}, 金额: ${amount}, 时间: ${new Date().toLocaleString()}`);
  314. }
  315. } else {
  316. await sendMessage(msg.chat.id, result.message || '入款失败');
  317. console.log(`入款失败 - 群组: ${msg.chat.title}, 金额: ${amount}, 原因: ${result.message}, 时间: ${new Date().toLocaleString()}`);
  318. }
  319. } catch (error) {
  320. console.error('记录入款失败:', error);
  321. await sendMessage(msg.chat.id, '记录入款失败,请稍后重试');
  322. }
  323. });
  324. // 处理下发命令
  325. bot.onText(/\/withdraw (.+)/, async (msg, match) => {
  326. if (!isGroupAllowed(msg.chat.id)) {
  327. sendMessage(msg.chat.id, '该群组未授权使用此功能');
  328. return;
  329. }
  330. const amount = parseFloat(match[1]);
  331. if (isNaN(amount)) {
  332. sendMessage(msg.chat.id, '请输入有效的金额');
  333. return;
  334. }
  335. const transactionData = {
  336. groupId: msg.chat.id.toString(),
  337. groupName: msg.chat.title || '未命名群组',
  338. amount: amount
  339. };
  340. try {
  341. const result = await Transaction.withdrawal(transactionData);
  342. if (result.success) {
  343. const billMessage = await generateBillMessage(msg.chat.id);
  344. if (billMessage) {
  345. await sendMessage(msg.chat.id, billMessage, {
  346. reply_markup: generateInlineKeyboard(msg.chat.id)
  347. });
  348. console.log(`出款成功 - 群组: ${msg.chat.title}, 金额: ${amount}, 时间: ${new Date().toLocaleString()}`);
  349. } else {
  350. await sendMessage(msg.chat.id, '出款成功,但获取账单信息失败');
  351. console.log(`出款成功(无账单) - 群组: ${msg.chat.title}, 金额: ${amount}, 时间: ${new Date().toLocaleString()}`);
  352. }
  353. } else {
  354. await sendMessage(msg.chat.id, result.message || '出款失败');
  355. console.log(`出款失败 - 群组: ${msg.chat.title}, 金额: ${amount}, 原因: ${result.message}, 时间: ${new Date().toLocaleString()}`);
  356. }
  357. } catch (error) {
  358. console.error('记录出款失败:', error);
  359. await sendMessage(msg.chat.id, '记录出款失败,请稍后重试');
  360. }
  361. });
  362. // 处理查看账单命令
  363. bot.onText(/\/bill/, async (msg) => {
  364. const billMessage = await generateBillMessage(msg.chat.id);
  365. sendMessage(msg.chat.id, billMessage, {
  366. reply_markup: generateInlineKeyboard(msg.chat.id)
  367. });
  368. });
  369. // 更新帮助命令
  370. bot.onText(/\/help/, (msg) => {
  371. const helpMessage = `
  372. 🤖 *机器人使用指南*
  373. 📝 *基础命令*
  374. • /deposit <金额> - 记录入款
  375. • /withdraw <金额> - 记录下发
  376. • /bill - 查看当前账单
  377. • /help - 显示此帮助信息
  378. ⚡️ *快捷命令*
  379. • +<金额> - 快速记录入款(例如:+2000)
  380. • -<金额> - 快速记录下发(例如:-2000)
  381. 👨‍💼 *管理员命令*
  382. • /addgroup <群组ID> - 添加允许的群组
  383. • /removegroup <群组ID> - 移除允许的群组
  384. • /listgroups - 列出所有允许的群组
  385. 💡 *使用提示*
  386. • 所有金额输入请使用数字
  387. • 账单信息实时更新
  388. • 如需帮助请联系管理员
  389. `;
  390. sendMessage(msg.chat.id, helpMessage, { parse_mode: 'Markdown' });
  391. });
  392. // 生成账单消息
  393. async function generateBillMessage(chatId) {
  394. try {
  395. // 获取群组的最后加入时间
  396. const [groupInfo] = await pool.query(
  397. 'SELECT last_join_time FROM groups WHERE group_id = ?',
  398. [chatId.toString()]
  399. );
  400. if (!groupInfo || groupInfo.length === 0) {
  401. return '暂无交易记录';
  402. }
  403. const lastJoinTime = groupInfo[0].last_join_time;
  404. // 获取机器人加入后的交易记录
  405. const [records] = await pool.query(`
  406. SELECT * FROM transactions
  407. WHERE group_id = ?
  408. AND time >= ?
  409. ORDER BY time DESC
  410. LIMIT 10
  411. `, [chatId.toString(), lastJoinTime]);
  412. if (!records || records.length === 0) {
  413. return '暂无交易记录';
  414. }
  415. const deposits = records.filter(r => r.type === 'deposit');
  416. const withdrawals = records.filter(r => r.type === 'withdrawal');
  417. const totalDeposit = deposits.reduce((sum, d) => sum + parseFloat(d.amount), 0);
  418. const totalWithdrawal = withdrawals.reduce((sum, w) => sum + parseFloat(w.amount), 0);
  419. const depositFee = totalDeposit * (process.env.DEPOSIT_FEE_RATE || 0);
  420. const withdrawalFee = totalWithdrawal * (process.env.WITHDRAWAL_FEE_RATE || 0);
  421. const remaining = totalDeposit - depositFee - totalWithdrawal - withdrawalFee;
  422. let message = `📊 *账单明细*\n\n`;
  423. // 添加入款记录
  424. if (deposits.length > 0) {
  425. message += `💰 *入款记录* (${deposits.length}笔)\n`;
  426. deposits.forEach(deposit => {
  427. message += `• <code>${moment(deposit.time).format('HH:mm:ss')}</code> | ${parseFloat(deposit.amount).toFixed(2)}\n`;
  428. });
  429. message += '\n';
  430. }
  431. // 添加下发记录
  432. if (withdrawals.length > 0) {
  433. message += `💸 *下发记录* (${withdrawals.length}笔)\n`;
  434. withdrawals.forEach(withdrawal => {
  435. message += `• <code>${moment(withdrawal.time).format('HH:mm:ss')}</code> | ${parseFloat(withdrawal.amount).toFixed(2)}\n`;
  436. });
  437. message += '\n';
  438. }
  439. // 添加统计信息
  440. message += `📈 *统计信息*\n`;
  441. message += `• 总入款:${totalDeposit.toFixed(2)}\n`;
  442. message += `• 入款费率:${((process.env.DEPOSIT_FEE_RATE || 0) * 100).toFixed(1)}%\n`;
  443. message += `• 下发费率:${((process.env.WITHDRAWAL_FEE_RATE || 0) * 100).toFixed(1)}%\n`;
  444. message += `• 应下发:${(totalDeposit - depositFee).toFixed(2)}\n`;
  445. message += `• 总下发:${totalWithdrawal.toFixed(2)}\n`;
  446. message += `• 下发单笔附加费:0.0\n`;
  447. message += `• 单笔附费加总计:0.0\n\n`;
  448. message += `💵 *余额:${remaining.toFixed(2)}*`;
  449. return message;
  450. } catch (error) {
  451. console.error('生成账单消息失败:', error);
  452. return '获取账单信息失败,请稍后重试';
  453. }
  454. }
  455. // 生成内联键盘
  456. function generateInlineKeyboard(chatId) {
  457. const keyboard = {
  458. inline_keyboard: [
  459. [
  460. {
  461. text: '点击跳转完整账单',
  462. callback_data: `bill_page_${chatId}`
  463. }
  464. ],
  465. [
  466. {
  467. text: '24小时商务对接',
  468. callback_data: 'business_contact'
  469. }
  470. ]
  471. ]
  472. };
  473. return keyboard;
  474. }
  475. // 处理内联按钮回调
  476. bot.on('callback_query', async (callbackQuery) => {
  477. const chatId = callbackQuery.message.chat.id;
  478. const data = callbackQuery.data;
  479. try {
  480. if (data.startsWith('bill_page_')) {
  481. const groupId = data.split('_')[2];
  482. await bot.answerCallbackQuery(callbackQuery.id, {
  483. url: `${process.env.BILL_PAGE_BASE_URL}?groupId=${groupId}`
  484. });
  485. } else if (data === 'business_contact') {
  486. await bot.answerCallbackQuery(callbackQuery.id, {
  487. url: 'https://t.me/your_business_account'
  488. });
  489. }
  490. } catch (error) {
  491. console.error('处理内联按钮回调失败:', error);
  492. await bot.answerCallbackQuery(callbackQuery.id, {
  493. text: '操作失败,请稍后重试',
  494. show_alert: true
  495. });
  496. }
  497. });
  498. // 保存数据
  499. function saveData() {
  500. try {
  501. fs.writeFileSync(process.env.DB_FILE, JSON.stringify(data, null, 2));
  502. } catch (error) {
  503. console.error('Error saving data:', error);
  504. }
  505. }
  506. // 加载数据
  507. function loadData() {
  508. try {
  509. if (fs.existsSync(process.env.DB_FILE)) {
  510. const savedData = JSON.parse(fs.readFileSync(process.env.DB_FILE));
  511. data = { ...data, ...savedData };
  512. }
  513. } catch (error) {
  514. console.error('Error loading data:', error);
  515. }
  516. }
  517. // 测试数据库连接并初始化
  518. testConnection().then(() => {
  519. return initDatabase();
  520. }).then(() => {
  521. // 加载数据
  522. loadData();
  523. // 启动服务器
  524. const PORT = process.env.PORT || 3000;
  525. app.listen(PORT, () => {
  526. console.log(`服务器运行在端口 ${PORT}`);
  527. console.log('机器人已准备就绪!');
  528. });
  529. }).catch(error => {
  530. console.error('启动失败:', error);
  531. process.exit(1);
  532. });