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