347 lines
12 KiB
JavaScript
347 lines
12 KiB
JavaScript
const {getDB, initDB} = require('./database');
|
|
const {logger} = require('./config/logger');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
/**
|
|
* 余额一致性审计工具
|
|
* 用于检查所有用户的余额是否与转账记录一致
|
|
*/
|
|
class BalanceAuditor {
|
|
constructor() {
|
|
this.db = null;
|
|
this.auditResults = {
|
|
totalUsers: 0,
|
|
problematicUsers: [],
|
|
summary: {
|
|
totalDiscrepancy: 0,
|
|
usersWithPositiveDiscrepancy: 0,
|
|
usersWithNegativeDiscrepancy: 0,
|
|
maxDiscrepancy: 0,
|
|
minDiscrepancy: 0
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 初始化数据库连接
|
|
*/
|
|
async init() {
|
|
await initDB();
|
|
this.db = getDB();
|
|
console.log('数据库连接已初始化');
|
|
}
|
|
|
|
/**
|
|
* 计算用户的理论余额
|
|
* @param {number} userId - 用户ID
|
|
* @returns {Object} 包含收入、支出和理论余额的对象
|
|
*/
|
|
async calculateUserBalance(userId) {
|
|
// 查询所有已确认和已收到的转账记录
|
|
const [transfers] = await this.db.execute(`
|
|
SELECT
|
|
from_user_id,
|
|
to_user_id,
|
|
amount,
|
|
status,
|
|
transfer_type
|
|
FROM transfers
|
|
WHERE (from_user_id = ? OR to_user_id = ?)
|
|
AND status IN ('confirmed', 'received')
|
|
`, [userId, userId]);
|
|
|
|
let totalReceived = 0;
|
|
let totalSent = 0;
|
|
|
|
transfers.forEach(transfer => {
|
|
const amount = parseFloat(transfer.amount);
|
|
|
|
// 计算收入(作为接收方的转账)
|
|
if (transfer.to_user_id === userId) {
|
|
totalReceived += amount;
|
|
}
|
|
|
|
// 计算支出(作为发送方的转账)
|
|
if (transfer.from_user_id === userId) {
|
|
totalSent += amount;
|
|
}
|
|
});
|
|
|
|
return {
|
|
totalReceived,
|
|
totalSent,
|
|
theoreticalBalance: totalReceived - totalSent,
|
|
transferCount: transfers.length
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 分析用户的问题转账记录
|
|
* @param {number} userId - 用户ID
|
|
* @returns {Object} 问题转账分析结果
|
|
*/
|
|
async analyzeProblematicTransfers(userId) {
|
|
// 查询被拒绝、取消的转账
|
|
const [problemTransfers] = await this.db.execute(`
|
|
SELECT
|
|
id,
|
|
from_user_id,
|
|
to_user_id,
|
|
amount,
|
|
status,
|
|
admin_modified_at,
|
|
admin_modified_by,
|
|
admin_note
|
|
FROM transfers
|
|
WHERE (from_user_id = ? OR to_user_id = ?)
|
|
AND status IN ('rejected', 'cancelled', 'not_received')
|
|
ORDER BY admin_modified_at DESC
|
|
`, [userId, userId]);
|
|
|
|
let shouldBeRefunded = 0;
|
|
const problemDetails = [];
|
|
|
|
problemTransfers.forEach(transfer => {
|
|
const amount = parseFloat(transfer.amount);
|
|
|
|
if (transfer.from_user_id === userId) {
|
|
shouldBeRefunded += amount;
|
|
problemDetails.push({
|
|
transferId: transfer.id,
|
|
amount: amount,
|
|
status: transfer.status,
|
|
modifiedAt: transfer.admin_modified_at,
|
|
modifiedBy: transfer.admin_modified_by,
|
|
note: transfer.admin_note
|
|
});
|
|
}
|
|
});
|
|
|
|
return {
|
|
shouldBeRefunded,
|
|
problemCount: problemDetails.length,
|
|
details: problemDetails
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 审计单个用户的余额
|
|
* @param {Object} user - 用户对象
|
|
* @returns {Object} 审计结果
|
|
*/
|
|
async auditUser(user) {
|
|
const userId = user.id;
|
|
const actualBalance = parseFloat(user.balance);
|
|
|
|
// 计算理论余额
|
|
const balanceCalc = await this.calculateUserBalance(userId);
|
|
|
|
// 分析问题转账
|
|
const problemAnalysis = await this.analyzeProblematicTransfers(userId);
|
|
|
|
// 计算调整后的理论余额(考虑应退还金额)
|
|
const adjustedTheoreticalBalance = balanceCalc.theoreticalBalance + problemAnalysis.shouldBeRefunded;
|
|
|
|
// 计算差异
|
|
const discrepancy = actualBalance - adjustedTheoreticalBalance;
|
|
|
|
const result = {
|
|
userId: userId,
|
|
username: user.username,
|
|
realName: user.real_name,
|
|
role: user.role,
|
|
actualBalance: actualBalance,
|
|
theoreticalBalance: balanceCalc.theoreticalBalance,
|
|
adjustedTheoreticalBalance: adjustedTheoreticalBalance,
|
|
discrepancy: discrepancy,
|
|
totalReceived: balanceCalc.totalReceived,
|
|
totalSent: balanceCalc.totalSent,
|
|
transferCount: balanceCalc.transferCount,
|
|
shouldBeRefunded: problemAnalysis.shouldBeRefunded,
|
|
problemTransferCount: problemAnalysis.problemCount,
|
|
problemDetails: problemAnalysis.details,
|
|
isProblematic: Math.abs(discrepancy) > 0.01 // 考虑浮点数精度
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* 审计所有用户的余额
|
|
*/
|
|
async auditAllUsers() {
|
|
console.log('开始审计所有用户余额...');
|
|
|
|
// 获取所有用户
|
|
const [users] = await this.db.execute(`
|
|
SELECT id, username, real_name, balance, role, created_at
|
|
FROM users
|
|
WHERE role != 'system'
|
|
ORDER BY id
|
|
`);
|
|
|
|
this.auditResults.totalUsers = users.length;
|
|
console.log(`找到 ${users.length} 个用户需要审计`);
|
|
|
|
let processedCount = 0;
|
|
|
|
for (const user of users) {
|
|
try {
|
|
const auditResult = await this.auditUser(user);
|
|
|
|
if (auditResult.isProblematic) {
|
|
this.auditResults.problematicUsers.push(auditResult);
|
|
console.log(`发现问题用户: ${user.username} (ID: ${user.id}), 差异: ${auditResult.discrepancy.toFixed(2)}`);
|
|
}
|
|
|
|
// 更新统计信息
|
|
this.updateSummary(auditResult);
|
|
|
|
processedCount++;
|
|
if (processedCount % 100 === 0) {
|
|
console.log(`已处理 ${processedCount}/${users.length} 个用户`);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error(`审计用户 ${user.id} 时出错:`, error.message);
|
|
}
|
|
}
|
|
|
|
console.log('审计完成!');
|
|
}
|
|
|
|
/**
|
|
* 更新汇总统计信息
|
|
* @param {Object} auditResult - 单个用户的审计结果
|
|
*/
|
|
updateSummary(auditResult) {
|
|
const discrepancy = auditResult.discrepancy;
|
|
|
|
this.auditResults.summary.totalDiscrepancy += Math.abs(discrepancy);
|
|
|
|
if (discrepancy > 0) {
|
|
this.auditResults.summary.usersWithPositiveDiscrepancy++;
|
|
} else if (discrepancy < 0) {
|
|
this.auditResults.summary.usersWithNegativeDiscrepancy++;
|
|
}
|
|
|
|
if (discrepancy > this.auditResults.summary.maxDiscrepancy) {
|
|
this.auditResults.summary.maxDiscrepancy = discrepancy;
|
|
}
|
|
|
|
if (discrepancy < this.auditResults.summary.minDiscrepancy) {
|
|
this.auditResults.summary.minDiscrepancy = discrepancy;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 生成审计报告
|
|
*/
|
|
generateReport() {
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
const reportPath = path.join(__dirname, `balance_audit_report_${timestamp}.json`);
|
|
|
|
const report = {
|
|
auditTime: new Date().toISOString(),
|
|
summary: {
|
|
totalUsers: this.auditResults.totalUsers,
|
|
problematicUsersCount: this.auditResults.problematicUsers.length,
|
|
totalDiscrepancy: this.auditResults.summary.totalDiscrepancy.toFixed(2),
|
|
usersWithPositiveDiscrepancy: this.auditResults.summary.usersWithPositiveDiscrepancy,
|
|
usersWithNegativeDiscrepancy: this.auditResults.summary.usersWithNegativeDiscrepancy,
|
|
maxDiscrepancy: this.auditResults.summary.maxDiscrepancy.toFixed(2),
|
|
minDiscrepancy: this.auditResults.summary.minDiscrepancy.toFixed(2)
|
|
},
|
|
problematicUsers: this.auditResults.problematicUsers.map(user => ({
|
|
userId: user.userId,
|
|
username: user.username,
|
|
realName: user.realName,
|
|
role: user.role,
|
|
actualBalance: user.actualBalance.toFixed(2),
|
|
theoreticalBalance: user.theoreticalBalance.toFixed(2),
|
|
adjustedTheoreticalBalance: user.adjustedTheoreticalBalance.toFixed(2),
|
|
discrepancy: user.discrepancy.toFixed(2),
|
|
shouldBeRefunded: user.shouldBeRefunded.toFixed(2),
|
|
problemTransferCount: user.problemTransferCount,
|
|
problemDetails: user.problemDetails
|
|
}))
|
|
};
|
|
|
|
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf8');
|
|
console.log(`\n审计报告已生成: ${reportPath}`);
|
|
|
|
return report;
|
|
}
|
|
|
|
/**
|
|
* 打印控制台报告
|
|
*/
|
|
printConsoleReport() {
|
|
console.log('\n=== 余额审计报告 ===');
|
|
console.log(`审计时间: ${new Date().toLocaleString()}`);
|
|
console.log(`总用户数: ${this.auditResults.totalUsers}`);
|
|
console.log(`问题用户数: ${this.auditResults.problematicUsers.length}`);
|
|
console.log(`总差异金额: ${this.auditResults.summary.totalDiscrepancy.toFixed(2)}`);
|
|
console.log(`余额偏高用户数: ${this.auditResults.summary.usersWithPositiveDiscrepancy}`);
|
|
console.log(`余额偏低用户数: ${this.auditResults.summary.usersWithNegativeDiscrepancy}`);
|
|
console.log(`最大差异: ${this.auditResults.summary.maxDiscrepancy.toFixed(2)}`);
|
|
console.log(`最小差异: ${this.auditResults.summary.minDiscrepancy.toFixed(2)}`);
|
|
|
|
if (this.auditResults.problematicUsers.length > 0) {
|
|
console.log('\n=== 问题用户详情 ===');
|
|
this.auditResults.problematicUsers.forEach((user, index) => {
|
|
console.log(`\n${index + 1}. 用户: ${user.username} (ID: ${user.userId})`);
|
|
console.log(` 实际余额: ${user.actualBalance.toFixed(2)}`);
|
|
console.log(` 理论余额: ${user.theoreticalBalance.toFixed(2)}`);
|
|
console.log(` 调整后理论余额: ${user.adjustedTheoreticalBalance.toFixed(2)}`);
|
|
console.log(` 差异: ${user.discrepancy.toFixed(2)}`);
|
|
console.log(` 应退还金额: ${user.shouldBeRefunded.toFixed(2)}`);
|
|
console.log(` 问题转账数: ${user.problemTransferCount}`);
|
|
|
|
if (user.problemDetails.length > 0) {
|
|
console.log(' 问题转账详情:');
|
|
user.problemDetails.forEach((detail, i) => {
|
|
console.log(` ${i + 1}. 转账ID: ${detail.transferId}, 金额: ${detail.amount}, 状态: ${detail.status}`);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 运行完整的审计流程
|
|
*/
|
|
async runAudit() {
|
|
try {
|
|
await this.init();
|
|
await this.auditAllUsers();
|
|
this.printConsoleReport();
|
|
const report = this.generateReport();
|
|
|
|
// 记录到日志
|
|
logger.info('Balance audit completed', {
|
|
totalUsers: this.auditResults.totalUsers,
|
|
problematicUsers: this.auditResults.problematicUsers.length,
|
|
totalDiscrepancy: this.auditResults.summary.totalDiscrepancy
|
|
});
|
|
|
|
return report;
|
|
|
|
} catch (error) {
|
|
console.error('审计过程中发生错误:', error);
|
|
logger.error('Balance audit failed', { error: error.message });
|
|
throw error;
|
|
} finally {
|
|
process.exit(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 如果直接运行此脚本
|
|
if (require.main === module) {
|
|
const auditor = new BalanceAuditor();
|
|
auditor.runAudit().catch(console.error);
|
|
}
|
|
|
|
module.exports = BalanceAuditor; |