Files
jurong_circle_black/services/timeoutService.js
2025-08-26 10:06:23 +08:00

380 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();