380 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			380 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| const { getDB } = require('../database');
 | ||
| const { logger, auditLogger } = require('../config/logger');
 | ||
| 
 | ||
| class TimeoutService {
 | ||
|   /**
 | ||
|    * 检查转账超时情况
 | ||
|    * 标记超时转账和风险用户,自动取消超过2.5小时的pending转账
 | ||
|    */
 | ||
|   async checkTransferTimeouts() {
 | ||
|     const db = getDB();
 | ||
|     
 | ||
|     try {
 | ||
|       // 只在调试模式下输出开始检查的日志
 | ||
|       // console.log('开始检查转账超时情况...');
 | ||
|       
 | ||
|       // 1. 查找所有超时的转账记录(有deadline_at的)
 | ||
|       const [overdueTransfers] = await db.execute(
 | ||
|         `SELECT t.*, u.username, u.real_name 
 | ||
|          FROM transfers t
 | ||
|          JOIN users u ON t.from_user_id = u.id
 | ||
|          WHERE t.status = 'pending' 
 | ||
|          AND t.deadline_at IS NOT NULL 
 | ||
|          AND t.deadline_at < NOW() 
 | ||
|          AND t.is_overdue = 0`
 | ||
|       );
 | ||
|       
 | ||
|       // 2. 查找所有pending状态超过2.5小时的转账记录
 | ||
|       const [longPendingTransfers] = await db.execute(
 | ||
|         `SELECT t.*, u.username, u.real_name 
 | ||
|          FROM transfers t
 | ||
|          JOIN users u ON t.from_user_id = u.id
 | ||
|          WHERE t.status = 'pending' 
 | ||
|          AND t.created_at < DATE_SUB(NOW(), INTERVAL 150 MINUTE)`
 | ||
|       );
 | ||
|       
 | ||
|       let hasWork = false;
 | ||
|       
 | ||
|       // 处理有deadline的超时转账
 | ||
|       if (overdueTransfers.length > 0) {
 | ||
|         hasWork = true;
 | ||
|         console.log(`⚠️ 发现 ${overdueTransfers.length} 笔超时转账,开始处理...`);
 | ||
|         
 | ||
|         for (const transfer of overdueTransfers) {
 | ||
|           await this.handleOverdueTransfer(transfer);
 | ||
|         }
 | ||
|       }    
 | ||
|       // 处理超过2.5小时的pending转账
 | ||
|       if (longPendingTransfers.length > 0) {
 | ||
|         hasWork = true;
 | ||
|         console.log(`⚠️ 发现 ${longPendingTransfers.length} 笔超过2.5小时的pending转账,开始自动取消...`);
 | ||
|         
 | ||
|         for (const transfer of longPendingTransfers) {
 | ||
|           await this.handleLongPendingTransfer(transfer);
 | ||
|         }
 | ||
|       }
 | ||
|       
 | ||
|       if (hasWork) {
 | ||
|         console.log('✅ 转账超时检查完成');
 | ||
|       }
 | ||
|       
 | ||
|     } catch (error) {
 | ||
|       console.error('检查转账超时失败:', error);
 | ||
|       logger.error('Transfer timeout check failed', { error: error.message });
 | ||
|     }
 | ||
|   }
 | ||
|   
 | ||
|   /**
 | ||
|    * 处理超时转账
 | ||
|    * @param {Object} transfer - 转账记录
 | ||
|    */
 | ||
|   async handleOverdueTransfer(transfer) {
 | ||
|     const db = getDB();
 | ||
|     
 | ||
|     try {
 | ||
|       await db.query('START TRANSACTION');
 | ||
|       
 | ||
|       // 标记转账为超时和坏账
 | ||
|       await db.execute(
 | ||
|         'UPDATE transfers SET is_overdue = 1, is_bad_debt = 1, overdue_at = NOW() WHERE id = ?',
 | ||
|         [transfer.id]
 | ||
|       );
 | ||
|       
 | ||
|       // 标记用户为风险用户
 | ||
|       await db.execute(
 | ||
|         `UPDATE users SET 
 | ||
|          is_risk_user = 1, 
 | ||
|          risk_reason = CONCAT(IFNULL(risk_reason, ''), '转账超时(转账ID: ', ?, ', 金额: ', ?, '元, 超时时间: ', NOW(), '); ') 
 | ||
|          WHERE id = ?`,
 | ||
|         [transfer.id, transfer.amount, transfer.from_user_id]
 | ||
|       );
 | ||
|       
 | ||
|       await db.query('COMMIT');
 | ||
|       
 | ||
|       // 记录审计日志
 | ||
|       auditLogger.info('Transfer marked as overdue and bad debt, user marked as risk', {
 | ||
|         transferId: transfer.id,
 | ||
|         userId: transfer.from_user_id,
 | ||
|         username: transfer.username,
 | ||
|         amount: transfer.amount,
 | ||
|         deadlineAt: transfer.deadline_at
 | ||
|       });
 | ||
|       
 | ||
|       console.log(`转账 ${transfer.id} 已标记为超时和坏账,用户 ${transfer.username}(ID: ${transfer.from_user_id}) 已标记为风险用户`);
 | ||
|       
 | ||
|     } catch (error) {
 | ||
|       await db.query('ROLLBACK');
 | ||
|       console.error(`处理超时转账 ${transfer.id} 失败:`, error);
 | ||
|       throw error;
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   /**
 | ||
|    * 处理超过2.5小时的pending转账
 | ||
|    * @param {Object} transfer - 转账记录
 | ||
|    */
 | ||
|   async handleLongPendingTransfer(transfer) {
 | ||
|     const db = getDB();
 | ||
|     
 | ||
|     try {
 | ||
|       await db.query('START TRANSACTION');
 | ||
|       
 | ||
|       // 将转账状态改为cancelled
 | ||
|       await db.execute(
 | ||
|         'UPDATE transfers SET status = "cancelled", updated_at = NOW() WHERE id = ?',
 | ||
|         [transfer.id]
 | ||
|       );
 | ||
|       
 | ||
|       // 如果有关联的matching_order_id,检查并更新matching_orders状态
 | ||
|       if (transfer.matching_order_id) {
 | ||
|         // 检查该matching_order下是否还有非cancelled状态的transfers
 | ||
|         const [remainingTransfers] = await db.execute(
 | ||
|           'SELECT COUNT(*) as count FROM transfers WHERE matching_order_id = ? AND status != "cancelled"',
 | ||
|           [transfer.matching_order_id]
 | ||
|         );
 | ||
|         
 | ||
|         // 如果所有关联的transfers都是cancelled状态,则更新matching_order状态为cancelled
 | ||
|         if (remainingTransfers[0].count === 0) {
 | ||
|           await db.execute(
 | ||
|             'UPDATE matching_orders SET status = "cancelled", updated_at = NOW() WHERE id = ?',
 | ||
|             [transfer.matching_order_id]
 | ||
|           );
 | ||
|           
 | ||
|           console.log(`匹配订单 ${transfer.matching_order_id} 的所有转账都已取消,订单状态已更新为cancelled`);
 | ||
|         }
 | ||
|       }
 | ||
|       
 | ||
|       await db.query('COMMIT');
 | ||
|       
 | ||
|       // 记录审计日志
 | ||
|       auditLogger.info('Long pending transfer auto-cancelled', {
 | ||
|         transferId: transfer.id,
 | ||
|         userId: transfer.from_user_id,
 | ||
|         username: transfer.username,
 | ||
|         amount: transfer.amount,
 | ||
|         createdAt: transfer.created_at,
 | ||
|         matchingOrderId: transfer.matching_order_id
 | ||
|       });
 | ||
|       
 | ||
|       console.log(`转账 ${transfer.id} 超过2.5小时未处理,已自动取消 (用户: ${transfer.username}, 金额: ${transfer.amount}元)`);
 | ||
|       
 | ||
|     } catch (error) {
 | ||
|       await db.query('ROLLBACK');
 | ||
|       console.error(`处理长时间pending转账 ${transfer.id} 失败:`, error);
 | ||
|       throw error;
 | ||
|     }
 | ||
|   }
 | ||
|   
 | ||
|   /**
 | ||
|    * 获取风险用户列表
 | ||
|    * @param {Object} filters - 筛选条件
 | ||
|    * @param {Object} pagination - 分页参数
 | ||
|    * @returns {Object} 风险用户列表和分页信息
 | ||
|    */
 | ||
|   async getRiskUsers(filters = {}, pagination = {}) {
 | ||
|     const db = getDB();
 | ||
|     const { page = 1, limit = 10 } = pagination;
 | ||
|     const pageNum = parseInt(page, 10) || 1;
 | ||
|     const limitNum = parseInt(limit, 10) || 10;
 | ||
|     const offset = (pageNum - 1) * limitNum;
 | ||
|     
 | ||
|     let whereClause = 'WHERE is_risk_user = 1';
 | ||
|     const params = [];
 | ||
|     
 | ||
|     // 构建查询条件
 | ||
|     if (filters.is_blacklisted !== undefined) {
 | ||
|       whereClause += ' AND is_blacklisted = ?';
 | ||
|       params.push(filters.is_blacklisted);
 | ||
|     }
 | ||
|     
 | ||
|     if (filters.username) {
 | ||
|       whereClause += ' AND username LIKE ?';
 | ||
|       params.push(`%${filters.username}%`);
 | ||
|     }
 | ||
|     
 | ||
|     try {
 | ||
|       // 获取总数
 | ||
|       const [countResult] = await db.execute(
 | ||
|         `SELECT COUNT(*) as total FROM users ${whereClause}`,
 | ||
|         params
 | ||
|       );
 | ||
|       const total = countResult[0].total;
 | ||
|       
 | ||
|       // 获取数据
 | ||
|       const [users] = await db.execute(
 | ||
|         `SELECT id, username, real_name, is_risk_user, is_blacklisted, 
 | ||
|                 risk_reason, blacklist_reason, blacklisted_at, created_at,phone
 | ||
|          FROM users 
 | ||
|          ${whereClause}
 | ||
|          ORDER BY created_at DESC
 | ||
|          LIMIT ${limitNum} OFFSET ${offset}`,
 | ||
|         params
 | ||
|       );
 | ||
|       
 | ||
|       return {
 | ||
|         users,
 | ||
|         pagination: {
 | ||
|           page: pageNum,
 | ||
|           limit: limitNum,
 | ||
|           total,
 | ||
|           pages: Math.ceil(total / limitNum)
 | ||
|         }
 | ||
|       };
 | ||
|     } catch (error) {
 | ||
|       logger.error('Failed to get risk users', { error: error.message, filters });
 | ||
|       throw error;
 | ||
|     }
 | ||
|   }
 | ||
|   
 | ||
|   /**
 | ||
|    * 拉黑用户
 | ||
|    * @param {number} userId - 用户ID
 | ||
|    * @param {string} reason - 拉黑原因
 | ||
|    * @param {number} operatorId - 操作员ID
 | ||
|    */
 | ||
|   async blacklistUser(userId, reason, operatorId) {
 | ||
|     const db = getDB();
 | ||
|     
 | ||
|     try {
 | ||
|       // 检查用户是否存在
 | ||
|       const [users] = await db.execute(
 | ||
|       'SELECT id, username, phone FROM users WHERE id = ?',
 | ||
|       [userId]
 | ||
|     );
 | ||
|       
 | ||
|       if (users.length === 0) {
 | ||
|         throw new Error('用户不存在');
 | ||
|       }
 | ||
|       
 | ||
|       const user = users[0];
 | ||
|       
 | ||
|       if (user.is_blacklisted) {
 | ||
|         throw new Error('用户已被拉黑');
 | ||
|       }
 | ||
|       
 | ||
|       // 拉黑用户
 | ||
|       await db.execute(
 | ||
|         `UPDATE users SET 
 | ||
|          is_blacklisted = 1, 
 | ||
|          blacklist_reason = ?, 
 | ||
|          blacklisted_at = NOW() 
 | ||
|          WHERE id = ?`,
 | ||
|         [reason, userId]
 | ||
|       );
 | ||
|       
 | ||
|       // 记录审计日志
 | ||
|       auditLogger.info('User blacklisted', {
 | ||
|         userId,
 | ||
|         username: user.username,
 | ||
|         reason,
 | ||
|         operatorId
 | ||
|       });
 | ||
|       
 | ||
|       logger.info('User blacklisted successfully', { userId, operatorId });
 | ||
|       
 | ||
|     } catch (error) {
 | ||
|       logger.error('Failed to blacklist user', {
 | ||
|         error: error.message,
 | ||
|         userId,
 | ||
|         operatorId
 | ||
|       });
 | ||
|       throw error;
 | ||
|     }
 | ||
|   }
 | ||
|   
 | ||
|   /**
 | ||
|    * 解除拉黑
 | ||
|    * @param {number} userId - 用户ID
 | ||
|    * @param {number} operatorId - 操作员ID
 | ||
|    */
 | ||
|   async unblacklistUser(userId, operatorId) {
 | ||
|     const db = getDB();
 | ||
|     
 | ||
|     try {
 | ||
|       // 检查用户是否存在
 | ||
|       const [users] = await db.execute(
 | ||
|         'SELECT id, username, is_blacklisted FROM users WHERE id = ?',
 | ||
|         [userId]
 | ||
|       );
 | ||
|       
 | ||
|       if (users.length === 0) {
 | ||
|         throw new Error('用户不存在');
 | ||
|       }
 | ||
|       
 | ||
|       const user = users[0];
 | ||
|       
 | ||
|       if (!user.is_blacklisted) {
 | ||
|         throw new Error('用户未被拉黑');
 | ||
|       }
 | ||
|       
 | ||
|       // 解除拉黑
 | ||
|       await db.execute(
 | ||
|         `UPDATE users SET 
 | ||
|          is_blacklisted = 0, 
 | ||
|          blacklist_reason = NULL, 
 | ||
|          blacklisted_at = NULL 
 | ||
|          WHERE id = ?`,
 | ||
|         [userId]
 | ||
|       );
 | ||
|       
 | ||
|       // 记录审计日志
 | ||
|       auditLogger.info('User unblacklisted', {
 | ||
|         userId,
 | ||
|         username: user.username,
 | ||
|         operatorId
 | ||
|       });
 | ||
|       
 | ||
|       logger.info('User unblacklisted successfully', { userId, operatorId });
 | ||
|       
 | ||
|     } catch (error) {
 | ||
|       logger.error('Failed to unblacklist user', {
 | ||
|         error: error.message,
 | ||
|         userId,
 | ||
|         operatorId
 | ||
|       });
 | ||
|       throw error;
 | ||
|     }
 | ||
|   }
 | ||
|   
 | ||
|   /**
 | ||
|    * 检查用户是否被拉黑
 | ||
|    * @param {number} userId - 用户ID
 | ||
|    * @returns {boolean} 是否被拉黑
 | ||
|    */
 | ||
|   async isUserBlacklisted(userId) {
 | ||
|     const db = getDB();
 | ||
|     
 | ||
|     try {
 | ||
|       const [users] = await db.execute(
 | ||
|         'SELECT is_blacklisted FROM users WHERE id = ?',
 | ||
|         [userId]
 | ||
|       );
 | ||
|       
 | ||
|       return users.length > 0 && users[0].is_blacklisted === 1;
 | ||
|     } catch (error) {
 | ||
|       logger.error('Failed to check user blacklist status', {
 | ||
|         error: error.message,
 | ||
|         userId
 | ||
|       });
 | ||
|       throw error;
 | ||
|     }
 | ||
|   }
 | ||
|   
 | ||
|   /**
 | ||
|    * 启动定时检查任务
 | ||
|    * 每5分钟检查一次转账超时情况
 | ||
|    */
 | ||
|   startTimeoutChecker() {
 | ||
|     console.log('启动转账超时检查定时任务...');
 | ||
|     
 | ||
|     // 立即执行一次
 | ||
|     this.checkTransferTimeouts();
 | ||
|     
 | ||
|     // 每5分钟执行一次
 | ||
|     setInterval(() => {
 | ||
|       this.checkTransferTimeouts();
 | ||
|     }, 5 * 1000); // 5秒
 | ||
|   }
 | ||
| } 
 | ||
| 
 | ||
| module.exports = new TimeoutService(); |