Transaction.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. const { pool } = require('../config/database');
  2. // 创建交易表
  3. const createTransactionTable = async () => {
  4. try {
  5. // 先检查表是否存在
  6. const [tables] = await pool.query('SHOW TABLES LIKE "transactions"');
  7. if (tables.length === 0) {
  8. // 如果表不存在,创建新表
  9. await pool.query(`
  10. CREATE TABLE transactions (
  11. id INT AUTO_INCREMENT PRIMARY KEY,
  12. group_id VARCHAR(50) NOT NULL,
  13. group_name VARCHAR(100) NOT NULL,
  14. type ENUM('deposit', 'withdrawal') NOT NULL COMMENT 'deposit:入款,withdrawal:出款',
  15. amount DECIMAL(10,2) NOT NULL,
  16. time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  17. remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
  18. operator_id VARCHAR(50) NOT NULL COMMENT '操作人ID',
  19. fee_rate DECIMAL(5,2) DEFAULT NULL COMMENT '费率',
  20. exchange_rate DECIMAL(10,4) DEFAULT NULL COMMENT '汇率',
  21. INDEX idx_group_time (group_id, time),
  22. FOREIGN KEY (operator_id) REFERENCES users(id)
  23. )
  24. `);
  25. console.log('交易表创建成功');
  26. }
  27. } catch (error) {
  28. console.error('创建交易表失败:', error);
  29. throw error;
  30. }
  31. };
  32. // 初始化表
  33. createTransactionTable();
  34. const Transaction = {
  35. // 获取交易列表
  36. findAll: async (query = {}, page = 1, limit = 10) => {
  37. try {
  38. const offset = (page - 1) * limit;
  39. let sql = `
  40. SELECT
  41. t.*,
  42. u.username as operator_name,
  43. u.uid as uid,
  44. g.group_name
  45. FROM transactions t
  46. LEFT JOIN users u ON t.operator_id = u.id
  47. LEFT JOIN groups g ON t.group_id = g.group_id
  48. WHERE 1=1
  49. `;
  50. const params = [];
  51. if (query.groupId) {
  52. sql += ' AND t.group_id = ?';
  53. params.push(query.groupId);
  54. }
  55. if (query.type) {
  56. sql += ' AND t.type = ?';
  57. params.push(query.type);
  58. }
  59. if (query.startDate) {
  60. sql += ' AND DATE(t.time) >= ?';
  61. params.push(query.startDate);
  62. }
  63. if (query.endDate) {
  64. sql += ' AND DATE(t.time) <= ?';
  65. params.push(query.endDate);
  66. }
  67. // 获取总记录数
  68. const [countResult] = await pool.query(
  69. sql.replace('t.*,', 'COUNT(*) as total,'),
  70. params
  71. );
  72. const total = countResult[0].total;
  73. // 添加排序和分页
  74. sql += ' ORDER BY t.time DESC LIMIT ? OFFSET ?';
  75. params.push(limit, offset);
  76. const [rows] = await pool.query(sql, params);
  77. return {
  78. transactions: rows,
  79. total,
  80. page: parseInt(page),
  81. pages: Math.ceil(total / limit)
  82. };
  83. } catch (error) {
  84. console.error('查询交易列表失败:', error);
  85. throw error;
  86. }
  87. },
  88. // 创建交易
  89. create: async (transactionData) => {
  90. try {
  91. // 获取群组的默认费率和汇率
  92. const [groupInfo] = await pool.query(
  93. 'SELECT in_fee_rate, in_exchange_rate, out_fee_rate, out_exchange_rate FROM groups WHERE group_id = ?',
  94. [transactionData.groupId]
  95. );
  96. if (!groupInfo || groupInfo.length === 0) {
  97. throw new Error('群组不存在');
  98. }
  99. // 根据交易类型选择对应的费率和汇率
  100. const defaultFeeRate = transactionData.type === 'deposit' ?
  101. groupInfo[0].in_fee_rate : parseFloat(groupInfo[0].out_fee_rate);
  102. const defaultExchangeRate = transactionData.type === 'deposit' ?
  103. groupInfo[0].in_exchange_rate : groupInfo[0].out_exchange_rate;
  104. // 使用指定的费率和汇率,如果没有指定则使用默认值
  105. const feeRate = transactionData.feeRate || defaultFeeRate;
  106. const exchangeRate = transactionData.exchangeRate || defaultExchangeRate;
  107. // 使用群内操作人的ID作为operator_id
  108. const operatorId = transactionData.operatorId || 1;
  109. // 检查是否是今天的第一笔交易
  110. const [todayFirstRecord] = await pool.query(
  111. 'SELECT COUNT(*) as count FROM transactions WHERE group_id = ? AND DATE(time) = CURDATE()',
  112. [transactionData.groupId]
  113. );
  114. // 初始化总金额数据
  115. let totalDeposit = 0;
  116. let totalWithdrawal = 0;
  117. let totalUDeposit = 0;
  118. let totalUWithdrawal = 0;
  119. // 如果不是今天的第一笔交易,获取上一条记录的总金额数据
  120. if (todayFirstRecord[0].count > 0) {
  121. const [lastRecord] = await pool.query(
  122. 'SELECT totalDeposit, totalWithdrawal, totalUDeposit, totalUWithdrawal FROM transactions WHERE group_id = ? ORDER BY time DESC LIMIT 1',
  123. [transactionData.groupId]
  124. );
  125. if (lastRecord && lastRecord.length > 0) {
  126. totalDeposit = parseFloat(lastRecord[0].totalDeposit) || 0;
  127. totalWithdrawal = parseFloat(lastRecord[0].totalWithdrawal) || 0;
  128. totalUDeposit = parseFloat(lastRecord[0].totalUDeposit) || 0;
  129. totalUWithdrawal = parseFloat(lastRecord[0].totalUWithdrawal) || 0;
  130. }
  131. }
  132. // 计算本条交易的手续费和实际金额
  133. let currentFee = 0;
  134. let actualAmount = 0;
  135. if (transactionData.type === 'deposit') {
  136. // 入款:手续费 = 金额 * 费率
  137. currentFee = Math.abs(transactionData.amount) * (feeRate / 100);
  138. actualAmount = Math.abs(transactionData.amount) - currentFee;
  139. } else {
  140. // 出款:实际金额 = 金额 * (1 + 费率)
  141. actualAmount = Math.abs(transactionData.amount) * (1 + feeRate / 100);
  142. currentFee = actualAmount - Math.abs(transactionData.amount);
  143. }
  144. // 计算本条交易的U币金额(保持与金额相同的正负号)
  145. const uAmount = (actualAmount / exchangeRate) * (transactionData.amount < 0 ? -1 : 1);
  146. // 根据交易类型更新总金额
  147. if (transactionData.type === 'deposit') {
  148. totalDeposit += transactionData.amount;
  149. totalUDeposit += uAmount;
  150. } else {
  151. totalWithdrawal += transactionData.amount;
  152. totalUWithdrawal += uAmount;
  153. }
  154. const [result] = await pool.query(
  155. `INSERT INTO transactions (
  156. group_id, group_name, type, amount, remark, operator_id,
  157. fee_rate, exchange_rate, totalDeposit, totalWithdrawal,
  158. depositFee, withdrawalFee, totalUDeposit, totalUWithdrawal
  159. ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
  160. [
  161. transactionData.groupId,
  162. transactionData.groupName,
  163. transactionData.type,
  164. transactionData.amount,
  165. transactionData.remark || null,
  166. operatorId,
  167. feeRate,
  168. exchangeRate,
  169. totalDeposit,
  170. totalWithdrawal,
  171. transactionData.type === 'deposit' ? currentFee : 0,
  172. transactionData.type === 'withdrawal' ? currentFee : 0,
  173. totalUDeposit,
  174. totalUWithdrawal
  175. ]
  176. );
  177. return result.insertId;
  178. } catch (error) {
  179. console.error('创建交易记录失败:', error);
  180. throw error;
  181. }
  182. },
  183. // 删除交易
  184. delete: async (id) => {
  185. await pool.query('DELETE FROM transactions WHERE id = ?', [id]);
  186. },
  187. // 入款方法
  188. deposit: async (transactionData) => {
  189. try {
  190. const id = await Transaction.create({
  191. groupId: transactionData.groupId,
  192. groupName: transactionData.groupName,
  193. type: 'deposit',
  194. amount: parseFloat(transactionData.amount),
  195. operatorId: transactionData.operatorId || 1, // 添加默认操作者ID
  196. feeRate: transactionData.feeRate,
  197. exchangeRate: transactionData.exchangeRate
  198. });
  199. const transaction = await Transaction.findById(id);
  200. if (transaction) {
  201. return {
  202. success: true,
  203. transaction,
  204. message: '入款记录创建成功'
  205. };
  206. } else {
  207. return {
  208. success: false,
  209. message: '入款记录创建失败'
  210. };
  211. }
  212. } catch (error) {
  213. console.error('入款记录创建失败:', error);
  214. return {
  215. success: false,
  216. message: '入款记录创建失败,请稍后重试'
  217. };
  218. }
  219. },
  220. // 出款方法
  221. withdrawal: async (transactionData) => {
  222. try {
  223. const id = await Transaction.create({
  224. groupId: transactionData.groupId,
  225. groupName: transactionData.groupName,
  226. type: 'withdrawal',
  227. amount: parseFloat(transactionData.amount),
  228. operatorId: transactionData.operatorId || 1, // 添加默认操作者ID
  229. feeRate: transactionData.feeRate,
  230. exchangeRate: transactionData.exchangeRate
  231. });
  232. const transaction = await Transaction.findById(id);
  233. if (transaction) {
  234. return {
  235. success: true,
  236. transaction,
  237. message: '出款记录创建成功'
  238. };
  239. } else {
  240. return {
  241. success: false,
  242. message: '出款记录创建失败'
  243. };
  244. }
  245. } catch (error) {
  246. console.error('出款记录创建失败:', error);
  247. return {
  248. success: false,
  249. message: '出款记录创建失败,请稍后重试'
  250. };
  251. }
  252. },
  253. // 下发方法
  254. distribute: async (transactionData) => {
  255. try {
  256. // 获取上一条记录的总金额数据
  257. const [lastRecord] = await pool.query(
  258. 'SELECT totalDeposit, totalWithdrawal, totalUDeposit, totalUWithdrawal FROM transactions WHERE group_id = ? ORDER BY time DESC LIMIT 1',
  259. [transactionData.groupId]
  260. );
  261. // 初始化总金额数据
  262. let totalDeposit = 0;
  263. let totalWithdrawal = 0;
  264. let totalUDeposit = 0;
  265. let totalUWithdrawal = 0;
  266. // 如果有上一条记录,使用其数据
  267. if (lastRecord && lastRecord.length > 0) {
  268. totalDeposit = parseFloat(lastRecord[0].totalDeposit) || 0;
  269. totalWithdrawal = parseFloat(lastRecord[0].totalWithdrawal) || 0;
  270. totalUDeposit = parseFloat(lastRecord[0].totalUDeposit) || 0;
  271. totalUWithdrawal = parseFloat(lastRecord[0].totalUWithdrawal) || 0;
  272. }
  273. // 下发金额直接使用输入的U币金额
  274. const uAmount = parseFloat(transactionData.amount);
  275. // 更新总金额
  276. totalUWithdrawal += uAmount; // 累加下发金额
  277. const [result] = await pool.query(
  278. `INSERT INTO transactions (
  279. group_id, group_name, type, amount, remark, operator_id,
  280. fee_rate, exchange_rate, totalDeposit, totalWithdrawal,
  281. depositFee, withdrawalFee, totalUDeposit, totalUWithdrawal
  282. ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
  283. [
  284. transactionData.groupId,
  285. transactionData.groupName,
  286. 'distribute', // 使用withdrawal类型,因为distribute类型还未添加
  287. uAmount,
  288. transactionData.remark || null,
  289. transactionData.operatorId || 1,
  290. 0, // fee_rate
  291. 1, // exchange_rate
  292. totalDeposit,
  293. totalWithdrawal,
  294. 0, // depositFee
  295. 0, // withdrawalFee
  296. totalUDeposit,
  297. totalUWithdrawal
  298. ]
  299. );
  300. const transaction = await Transaction.findById(result.insertId);
  301. if (transaction) {
  302. return {
  303. success: true,
  304. transaction,
  305. message: '下发记录创建成功'
  306. };
  307. } else {
  308. return {
  309. success: false,
  310. message: '下发记录创建失败'
  311. };
  312. }
  313. } catch (error) {
  314. console.error('下发记录创建失败:', error);
  315. return {
  316. success: false,
  317. message: '下发记录创建失败,请稍后重试'
  318. };
  319. }
  320. },
  321. // 查询群组账单
  322. getGroupTransactions: async (groupId, options = {}) => {
  323. const {
  324. startDate,
  325. endDate,
  326. type,
  327. page = 1,
  328. limit = 10
  329. } = options;
  330. let sql = `
  331. SELECT
  332. t.*,
  333. u.username as operator_name,
  334. u.uid as uid,
  335. DATE_FORMAT(t.time, '%Y-%m-%d %H:%i:%s') as formatted_time
  336. FROM transactions t
  337. LEFT JOIN users u ON t.operator_id = u.id
  338. WHERE t.group_id = ?
  339. `;
  340. const params = [groupId];
  341. if (startDate) {
  342. sql += ' AND t.time >= ?';
  343. params.push(startDate);
  344. }
  345. if (endDate) {
  346. sql += ' AND t.time <= ?';
  347. params.push(endDate);
  348. }
  349. if (type) {
  350. sql += ' AND t.type = ?';
  351. params.push(type);
  352. }
  353. // 获取总数
  354. const countSql = `
  355. SELECT COUNT(*) as total
  356. FROM transactions t
  357. WHERE 1=1
  358. ${startDate ? 'AND t.time >= ?' : ''}
  359. ${endDate ? 'AND t.time <= ?' : ''}
  360. ${type ? 'AND t.type = ?' : ''}
  361. ${groupId ? 'AND t.group_id = ?' : ''}
  362. `;
  363. const [countResult] = await pool.query(countSql, params);
  364. const total = countResult[0].total;
  365. // 获取分页数据
  366. sql += ' ORDER BY t.time DESC LIMIT ? OFFSET ?';
  367. params.push(limit, (page - 1) * limit);
  368. const [rows] = await pool.query(sql, params);
  369. // 计算总入款和总出款
  370. const [summary] = await pool.query(`
  371. SELECT
  372. SUM(CASE WHEN type = 'deposit' THEN amount ELSE 0 END) as total_deposit,
  373. SUM(CASE WHEN type = 'withdrawal' THEN amount ELSE 0 END) as total_withdrawal
  374. FROM transactions
  375. WHERE group_id = ?
  376. ${startDate ? 'AND time >= ?' : ''}
  377. ${endDate ? 'AND time <= ?' : ''}
  378. `, [groupId, ...(startDate ? [startDate] : []), ...(endDate ? [endDate] : [])]);
  379. return {
  380. transactions: rows,
  381. total,
  382. page: parseInt(page),
  383. pages: Math.ceil(total / limit),
  384. summary: {
  385. totalDeposit: summary[0].total_deposit || 0,
  386. totalWithdrawal: summary[0].total_withdrawal || 0,
  387. balance: (summary[0].total_deposit || 0) - (summary[0].total_withdrawal || 0)
  388. }
  389. };
  390. },
  391. // 根据ID查找交易记录
  392. findById: async (id) => {
  393. try {
  394. const [rows] = await pool.query(`
  395. SELECT
  396. t.*,
  397. u.username as operator_name
  398. FROM transactions t
  399. LEFT JOIN users u ON t.operator_id = u.id
  400. WHERE t.id = ?
  401. `, [id]);
  402. return rows[0] || null;
  403. } catch (error) {
  404. console.error('查询交易记录失败:', error);
  405. return null;
  406. }
  407. }
  408. };
  409. module.exports = Transaction;