index.js 22 KB

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