初次提交
This commit is contained in:
347
balance_audit.js
Normal file
347
balance_audit.js
Normal file
@@ -0,0 +1,347 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user