提交
This commit is contained in:
4
.env
4
.env
@@ -60,5 +60,5 @@ WECHAT_NOTIFY_URL=https://www.zrbjr.com/api/wechat-pay/notify
|
||||
# 2. 应用私钥和支付宝公钥现在从文件读取
|
||||
ALIPAY_APP_ID=2021005188682022
|
||||
ALIPAY_NOTIFY_URL=https://www.zrbjr.com/api/payment/alipay/notify
|
||||
ALIPAY_RETURN_URL=https://www.zrbjr.com/payment/success
|
||||
ALIPAY_QUIT_URL=https://www.zrbjr.com/payment/cancel
|
||||
ALIPAY_RETURN_URL=https://www.zrbjr.com/payment
|
||||
ALIPAY_QUIT_URL=https://www.zrbjr.com/payment/
|
||||
|
||||
52
.env.example
52
.env.example
@@ -1,17 +1,19 @@
|
||||
# 数据库配置
|
||||
DB_HOST=localhost
|
||||
DB_USER=root
|
||||
DB_PASSWORD=your_password
|
||||
DB_NAME=your_database
|
||||
DB_HOST=114.55.111.44
|
||||
DB_USER=maov2
|
||||
DB_PASSWORD=5fYhw8z6T62b7heS
|
||||
DB_NAME=maov2
|
||||
|
||||
# JWT密钥
|
||||
JWT_SECRET=your_jwt_secret_key
|
||||
|
||||
# 阿里云短信服务配置
|
||||
# 请在阿里云控制台获取以下配置信息:
|
||||
# 1. AccessKey ID 和 AccessKey Secret:在阿里云控制台 -> AccessKey管理中创建
|
||||
# 2. 短信签名:在阿里云短信服务控制台中申请并审核通过的签名
|
||||
# 3. 短信模板CODE:在阿里云短信服务控制台中申请并审核通过的模板CODE
|
||||
ALIYUN_ACCESS_KEY_ID=LTAI5tBHymRUu1vvo5tgYpaa
|
||||
ALIYUN_ACCESS_KEY_SECRET=lNsDZvpUVX2b3pfBQCBawOEyr3dNB9
|
||||
|
||||
# 短信模板配置
|
||||
ALIYUN_SMS_SIGN_NAME=宁波炬融歆创科技
|
||||
ALIYUN_SMS_TEMPLATE_CODE=SMS_324470054
|
||||
|
||||
@@ -20,21 +22,22 @@ NODE_ENV=development
|
||||
PORT=3000
|
||||
|
||||
# 前端地址配置
|
||||
FRONTEND_URL=http://localhost:3001
|
||||
FRONTEND_URL=https://www.zrbjr.com/frontend
|
||||
# FRONTEND_URL=http://114.55.111.44:3001/frontend
|
||||
|
||||
# MinIO 对象存储配置
|
||||
# MinIO服务器地址(不包含协议)
|
||||
MINIO_ENDPOINT=localhost
|
||||
MINIO_ENDPOINT=114.55.111.44
|
||||
# MinIO服务器端口
|
||||
MINIO_PORT=9000
|
||||
# 是否使用SSL(true/false)
|
||||
MINIO_USE_SSL=false
|
||||
# MinIO访问密钥
|
||||
MINIO_ACCESS_KEY=minioadmin
|
||||
MINIO_ACCESS_KEY=minio
|
||||
# MinIO秘密密钥
|
||||
MINIO_SECRET_KEY=minioadmin
|
||||
MINIO_SECRET_KEY=CNy6fMCfyfeaEjbE
|
||||
# MinIO公开访问地址(用于生成文件URL)
|
||||
MINIO_PUBLIC_URL=http://minio.zrbjr.com:9000
|
||||
MINIO_PUBLIC_URL=https://minio.zrbjr.com
|
||||
|
||||
# MinIO存储桶配置
|
||||
MINIO_BUCKET_UPLOADS=jurongquan
|
||||
@@ -42,19 +45,20 @@ MINIO_BUCKET_AVATARS=jurongquan
|
||||
MINIO_BUCKET_PRODUCTS=jurongquan
|
||||
MINIO_BUCKET_DOCUMENTS=jurongquan
|
||||
|
||||
# 微信支付配置
|
||||
WECHAT_APP_ID=your_wechat_app_id
|
||||
WECHAT_MCH_ID=your_wechat_mch_id
|
||||
WECHAT_API_KEY=your_wechat_api_key
|
||||
WECHAT_API_V3_KEY=your_wechat_api_v3_key
|
||||
WECHAT_NOTIFY_URL=https://your-domain.com/api/wechat-pay/notify
|
||||
#支付配置
|
||||
WECHAT_APP_ID=wx3a702dbe13fd2217
|
||||
WECHAT_MCH_ID=1726377336
|
||||
WECHAT_API_KEY=NINGBOJURONGkejiyouxiangongsi202
|
||||
WECHAT_API_V3_KEY=NINGBOJURONGkejiyouxiangongsi202
|
||||
WECHAT_CERT_PATH=./cert/apiclient_cert.pem
|
||||
WECHAT_KEY_PATH=./cert/apiclient_key.pem
|
||||
WECHAT_NOTIFY_URL=https://www.zrbjr.com/api/wechat-pay/notify
|
||||
|
||||
# 支付宝支付配置
|
||||
ALIPAY_APP_ID=your_alipay_app_id
|
||||
ALIPAY_PRIVATE_KEY=your_alipay_private_key
|
||||
ALIPAY_PUBLIC_KEY=your_alipay_public_key
|
||||
ALIPAY_GATEWAY_URL=https://openapi.alipay.com/gateway.do
|
||||
ALIPAY_NOTIFY_URL=https://your-domain.com/api/alipay/notify
|
||||
ALIPAY_RETURN_URL=https://your-domain.com/payment/success
|
||||
# 支付宝配置
|
||||
# 请在支付宝开放平台获取以下配置信息:
|
||||
# 1. 应用ID:在支付宝开放平台创建应用后获得
|
||||
# 2. 应用私钥和支付宝公钥现在从文件读取
|
||||
ALIPAY_APP_ID=2021005188682022
|
||||
ALIPAY_NOTIFY_URL=https://www.zrbjr.com/api/payment/alipay/notify
|
||||
ALIPAY_RETURN_URL=https://www.zrbjr.com/payment/success
|
||||
ALIPAY_QUIT_URL=https://www.zrbjr.com/payment/cancel
|
||||
13
.idea/dataSources.xml
generated
Normal file
13
.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="测试服务器" uuid="5c67c46f-1d09-4751-a201-e01d3162c9fe">
|
||||
<driver-ref>mysql.8</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<remarks>测试服务器1</remarks>
|
||||
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
|
||||
<jdbc-url>jdbc:mysql://114.55.111.44:3306/test_mao</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/sqldialects.xml
generated
Normal file
6
.idea/sqldialects.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="PROJECT" dialect="MySQL" />
|
||||
</component>
|
||||
</project>
|
||||
1
.idea/vcs.xml
generated
1
.idea/vcs.xml
generated
@@ -2,6 +2,5 @@
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/admin" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
347
balance_audit.js
347
balance_audit.js
@@ -1,347 +0,0 @@
|
||||
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;
|
||||
@@ -1,459 +0,0 @@
|
||||
const {getDB, initDB} = require('./database');
|
||||
const {logger, auditLogger} = require('./config/logger');
|
||||
const BalanceAuditor = require('./balance_audit');
|
||||
const cron = require('node-cron');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* 余额监控服务
|
||||
* 定期检查用户余额一致性,发现异常时发送警报
|
||||
*/
|
||||
class BalanceMonitor {
|
||||
constructor() {
|
||||
this.db = null;
|
||||
this.auditor = new BalanceAuditor();
|
||||
this.alertThreshold = 10; // 差异超过10元时发送警报
|
||||
this.maxProblematicUsers = 50; // 最多报告50个问题用户
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化监控服务
|
||||
*/
|
||||
async init() {
|
||||
await initDB();
|
||||
this.db = getDB();
|
||||
await this.auditor.init(); // 初始化审计器的数据库连接
|
||||
console.log('余额监控服务已初始化');
|
||||
logger.info('Balance monitor service initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行快速余额检查
|
||||
* 只检查最近有转账活动的用户
|
||||
*/
|
||||
async quickBalanceCheck() {
|
||||
try {
|
||||
console.log('开始执行快速余额检查...');
|
||||
|
||||
// 获取最近7天有转账活动的用户
|
||||
const [activeUsers] = await this.db.execute(`
|
||||
SELECT DISTINCT u.id, u.username, u.real_name, u.balance, u.role
|
||||
FROM users u
|
||||
INNER JOIN transfers t ON (u.id = t.from_user_id OR u.id = t.to_user_id)
|
||||
WHERE t.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
|
||||
AND u.role != 'system'
|
||||
ORDER BY u.id
|
||||
`);
|
||||
|
||||
console.log(`检查 ${activeUsers.length} 个活跃用户的余额`);
|
||||
|
||||
const problematicUsers = [];
|
||||
|
||||
for (const user of activeUsers) {
|
||||
const auditResult = await this.auditor.auditUser(user);
|
||||
|
||||
if (auditResult.isProblematic && Math.abs(auditResult.discrepancy) > this.alertThreshold) {
|
||||
problematicUsers.push(auditResult);
|
||||
|
||||
// 记录警报日志
|
||||
auditLogger.warn('Balance discrepancy detected', {
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
actualBalance: auditResult.actualBalance,
|
||||
theoreticalBalance: auditResult.theoreticalBalance,
|
||||
discrepancy: auditResult.discrepancy,
|
||||
checkTime: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (problematicUsers.length > 0) {
|
||||
await this.sendAlert(problematicUsers, 'quick');
|
||||
}
|
||||
|
||||
console.log(`快速检查完成,发现 ${problematicUsers.length} 个问题用户`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('快速余额检查失败:', error);
|
||||
logger.error('Quick balance check failed', { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行完整余额审计
|
||||
*/
|
||||
async fullBalanceAudit() {
|
||||
try {
|
||||
console.log('开始执行完整余额审计...');
|
||||
|
||||
await this.auditor.init();
|
||||
await this.auditor.auditAllUsers();
|
||||
|
||||
const report = this.auditor.generateReport();
|
||||
|
||||
// 如果发现严重问题,发送警报
|
||||
if (report.summary.problematicUsersCount > 0) {
|
||||
const criticalUsers = this.auditor.auditResults.problematicUsers
|
||||
.filter(user => Math.abs(user.discrepancy) > this.alertThreshold)
|
||||
.slice(0, this.maxProblematicUsers);
|
||||
|
||||
if (criticalUsers.length > 0) {
|
||||
await this.sendAlert(criticalUsers, 'full');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('完整审计完成');
|
||||
|
||||
} catch (error) {
|
||||
console.error('完整余额审计失败:', error);
|
||||
logger.error('Full balance audit failed', { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送余额异常警报
|
||||
* @param {Array} problematicUsers - 问题用户列表
|
||||
* @param {string} checkType - 检查类型 ('quick' 或 'full')
|
||||
*/
|
||||
async sendAlert(problematicUsers, checkType) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const alertData = {
|
||||
alertTime: timestamp,
|
||||
checkType: checkType,
|
||||
problematicUsersCount: problematicUsers.length,
|
||||
totalDiscrepancy: problematicUsers.reduce((sum, user) => sum + Math.abs(user.discrepancy), 0),
|
||||
users: problematicUsers.map(user => ({
|
||||
userId: user.userId,
|
||||
username: user.username,
|
||||
actualBalance: user.actualBalance,
|
||||
theoreticalBalance: user.theoreticalBalance,
|
||||
discrepancy: user.discrepancy,
|
||||
shouldBeRefunded: user.shouldBeRefunded
|
||||
}))
|
||||
};
|
||||
|
||||
// 保存警报到文件
|
||||
const alertPath = path.join(__dirname, 'logs', `balance_alert_${timestamp.replace(/[:.]/g, '-')}.json`);
|
||||
|
||||
// 确保logs目录存在
|
||||
const logsDir = path.join(__dirname, 'logs');
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(alertPath, JSON.stringify(alertData, null, 2), 'utf8');
|
||||
|
||||
// 记录到审计日志
|
||||
auditLogger.error('Balance discrepancy alert', {
|
||||
checkType: checkType,
|
||||
problematicUsersCount: problematicUsers.length,
|
||||
totalDiscrepancy: alertData.totalDiscrepancy,
|
||||
alertFile: alertPath
|
||||
});
|
||||
|
||||
console.log(`余额异常警报已生成: ${alertPath}`);
|
||||
|
||||
// 这里可以添加其他警报方式,如发送邮件、短信等
|
||||
// await this.sendEmailAlert(alertData);
|
||||
// await this.sendSMSAlert(alertData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查特定用户的余额
|
||||
* @param {number} userId - 用户ID
|
||||
* @returns {Object} 检查结果
|
||||
*/
|
||||
async checkUserBalance(userId) {
|
||||
try {
|
||||
const [users] = await this.db.execute(
|
||||
'SELECT id, username, real_name, balance, role FROM users WHERE id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (users.length === 0) {
|
||||
throw new Error(`用户 ${userId} 不存在`);
|
||||
}
|
||||
|
||||
const auditResult = await this.auditor.auditUser(users[0]);
|
||||
|
||||
// 记录检查日志
|
||||
auditLogger.info('Manual balance check', {
|
||||
userId: userId,
|
||||
username: users[0].username,
|
||||
actualBalance: auditResult.actualBalance,
|
||||
theoreticalBalance: auditResult.theoreticalBalance,
|
||||
discrepancy: auditResult.discrepancy,
|
||||
isProblematic: auditResult.isProblematic,
|
||||
checkTime: new Date().toISOString()
|
||||
});
|
||||
|
||||
return auditResult;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('User balance check failed', { userId, error: error.message });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查管理员操作的余额一致性
|
||||
* 监控最近的管理员状态变更操作,检查是否正确调整了余额
|
||||
*/
|
||||
async checkAdminOperations() {
|
||||
try {
|
||||
console.log('开始检查管理员操作的余额一致性...');
|
||||
|
||||
// 查询最近24小时内管理员修改的转账记录
|
||||
const [adminModifiedTransfers] = await this.db.execute(`
|
||||
SELECT t.id, t.from_user_id, t.to_user_id, t.amount, t.status,
|
||||
t.admin_modified_at, t.admin_modified_by, t.admin_note,
|
||||
u_admin.username as admin_username
|
||||
FROM transfers t
|
||||
LEFT JOIN users u_admin ON t.admin_modified_by = u_admin.id
|
||||
WHERE t.admin_modified_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
||||
AND t.admin_modified_by IS NOT NULL
|
||||
ORDER BY t.admin_modified_at DESC
|
||||
`);
|
||||
|
||||
if (adminModifiedTransfers.length === 0) {
|
||||
console.log('最近24小时内没有管理员操作记录');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`检查 ${adminModifiedTransfers.length} 个管理员操作记录`);
|
||||
|
||||
const suspiciousOperations = [];
|
||||
|
||||
for (const transfer of adminModifiedTransfers) {
|
||||
// 检查涉及的用户余额是否正常
|
||||
const userIds = [transfer.from_user_id, transfer.to_user_id].filter(id => id);
|
||||
|
||||
for (const userId of userIds) {
|
||||
const [users] = await this.db.execute(
|
||||
'SELECT id, username, balance FROM users WHERE id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (users.length > 0) {
|
||||
const auditResult = await this.auditor.auditUser(users[0]);
|
||||
|
||||
// 如果发现余额异常,且与管理员操作时间相近,标记为可疑
|
||||
if (auditResult.isProblematic && Math.abs(auditResult.discrepancy) >= this.alertThreshold) {
|
||||
suspiciousOperations.push({
|
||||
transferId: transfer.id,
|
||||
userId: userId,
|
||||
username: users[0].username,
|
||||
adminUsername: transfer.admin_username,
|
||||
adminModifiedAt: transfer.admin_modified_at,
|
||||
adminNote: transfer.admin_note,
|
||||
transferStatus: transfer.status,
|
||||
transferAmount: transfer.amount,
|
||||
balanceDiscrepancy: auditResult.discrepancy,
|
||||
actualBalance: auditResult.actualBalance,
|
||||
theoreticalBalance: auditResult.theoreticalBalance
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (suspiciousOperations.length > 0) {
|
||||
console.log(`⚠️ 发现 ${suspiciousOperations.length} 个可疑的管理员操作`);
|
||||
|
||||
// 生成管理员操作警报
|
||||
await this.generateAdminOperationAlert(suspiciousOperations);
|
||||
|
||||
// 记录警报日志
|
||||
logger.warn('Suspicious admin operations detected', {
|
||||
count: suspiciousOperations.length,
|
||||
operations: suspiciousOperations
|
||||
});
|
||||
} else {
|
||||
console.log('✅ 管理员操作检查正常,未发现余额异常');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('检查管理员操作失败:', error);
|
||||
logger.error('Admin operations check failed', { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成管理员操作警报文件
|
||||
* @param {Array} suspiciousOperations - 可疑操作列表
|
||||
*/
|
||||
async generateAdminOperationAlert(suspiciousOperations) {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const alertPath = path.join(__dirname, 'logs', `admin_operation_alert_${timestamp}.json`);
|
||||
|
||||
const alertData = {
|
||||
alertType: 'admin_operation_balance_discrepancy',
|
||||
timestamp: new Date().toISOString(),
|
||||
summary: {
|
||||
suspiciousOperationsCount: suspiciousOperations.length,
|
||||
totalDiscrepancy: suspiciousOperations.reduce((sum, op) => sum + Math.abs(op.balanceDiscrepancy), 0)
|
||||
},
|
||||
suspiciousOperations: suspiciousOperations,
|
||||
recommendations: [
|
||||
'检查管理员是否在状态变更时正确设置了adjust_balance参数',
|
||||
'验证转账状态变更的合理性',
|
||||
'如有必要,手动修复用户余额并记录修复日志'
|
||||
]
|
||||
};
|
||||
|
||||
// 确保logs目录存在
|
||||
const logsDir = path.join(__dirname, 'logs');
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 写入警报文件
|
||||
fs.writeFileSync(alertPath, JSON.stringify(alertData, null, 2));
|
||||
|
||||
console.log(`管理员操作警报已生成: ${alertPath}`);
|
||||
|
||||
// 记录警报日志
|
||||
auditLogger.warn('Admin operation alert generated', {
|
||||
suspiciousOperationsCount: suspiciousOperations.length,
|
||||
totalDiscrepancy: alertData.summary.totalDiscrepancy,
|
||||
alertFile: alertPath
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动定时监控任务
|
||||
*/
|
||||
startScheduledTasks() {
|
||||
console.log('启动定时余额监控任务...');
|
||||
|
||||
// 每小时执行一次快速检查
|
||||
cron.schedule('0 * * * *', async () => {
|
||||
console.log('执行定时快速余额检查');
|
||||
await this.quickBalanceCheck();
|
||||
});
|
||||
|
||||
// 每30分钟检查一次管理员操作
|
||||
cron.schedule('*/30 * * * *', async () => {
|
||||
console.log('执行管理员操作余额一致性检查');
|
||||
await this.checkAdminOperations();
|
||||
});
|
||||
|
||||
// 每天凌晨2点执行完整审计
|
||||
cron.schedule('0 2 * * *', async () => {
|
||||
console.log('执行定时完整余额审计');
|
||||
await this.fullBalanceAudit();
|
||||
});
|
||||
|
||||
// 每周日凌晨3点执行深度审计
|
||||
cron.schedule('0 3 * * 0', async () => {
|
||||
console.log('执行定时深度余额审计');
|
||||
await this.fullBalanceAudit();
|
||||
});
|
||||
|
||||
logger.info('Balance monitoring scheduled tasks started');
|
||||
console.log('定时监控任务已启动');
|
||||
console.log('- 快速检查: 每小时执行一次');
|
||||
console.log('- 管理员操作检查: 每30分钟执行一次');
|
||||
console.log('- 完整审计: 每天凌晨2点执行');
|
||||
console.log('- 深度审计: 每周日凌晨3点执行');
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止监控服务
|
||||
*/
|
||||
stop() {
|
||||
console.log('停止余额监控服务');
|
||||
logger.info('Balance monitor service stopped');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行监控服务
|
||||
*/
|
||||
async run() {
|
||||
try {
|
||||
await this.init();
|
||||
|
||||
// 启动时执行一次快速检查
|
||||
await this.quickBalanceCheck();
|
||||
|
||||
// 启动时执行一次管理员操作检查
|
||||
await this.checkAdminOperations();
|
||||
|
||||
// 启动定时任务
|
||||
this.startScheduledTasks();
|
||||
|
||||
// 监听退出信号
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n收到退出信号,正在停止监控服务...');
|
||||
this.stop();
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('\n收到终止信号,正在停止监控服务...');
|
||||
this.stop();
|
||||
});
|
||||
|
||||
console.log('余额监控服务正在运行中...');
|
||||
console.log('按 Ctrl+C 停止服务');
|
||||
|
||||
} catch (error) {
|
||||
console.error('启动监控服务失败:', error);
|
||||
logger.error('Balance monitor startup failed', { error: error.message });
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果直接运行此脚本
|
||||
if (require.main === module) {
|
||||
const monitor = new BalanceMonitor();
|
||||
|
||||
// 检查命令行参数
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.includes('--quick')) {
|
||||
// 执行一次快速检查后退出
|
||||
monitor.init().then(() => {
|
||||
return monitor.quickBalanceCheck();
|
||||
}).then(() => {
|
||||
console.log('快速检查完成');
|
||||
process.exit(0);
|
||||
}).catch(console.error);
|
||||
} else if (args.includes('--full')) {
|
||||
// 执行一次完整审计后退出
|
||||
monitor.init().then(() => {
|
||||
return monitor.fullBalanceAudit();
|
||||
}).then(() => {
|
||||
console.log('完整审计完成');
|
||||
process.exit(0);
|
||||
}).catch(console.error);
|
||||
} else if (args.includes('--user')) {
|
||||
// 检查特定用户
|
||||
const userIdIndex = args.indexOf('--user') + 1;
|
||||
const userId = args[userIdIndex];
|
||||
|
||||
if (!userId) {
|
||||
console.error('请指定用户ID: --user <userId>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
monitor.init().then(() => {
|
||||
return monitor.checkUserBalance(parseInt(userId));
|
||||
}).then((result) => {
|
||||
console.log('用户余额检查结果:');
|
||||
console.log(`用户: ${result.username} (ID: ${result.userId})`);
|
||||
console.log(`实际余额: ${result.actualBalance}`);
|
||||
console.log(`理论余额: ${result.theoreticalBalance}`);
|
||||
console.log(`差异: ${result.discrepancy}`);
|
||||
console.log(`是否有问题: ${result.isProblematic ? '是' : '否'}`);
|
||||
process.exit(0);
|
||||
}).catch(console.error);
|
||||
} else {
|
||||
// 启动持续监控服务
|
||||
monitor.run().catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BalanceMonitor;
|
||||
@@ -1,72 +0,0 @@
|
||||
const { initDB, getDB } = require('./database');
|
||||
const matchingService = require('./services/matchingService');
|
||||
|
||||
/**
|
||||
* 批量处理所有符合条件的代理佣金
|
||||
*/
|
||||
async function batchProcessCommissions() {
|
||||
try {
|
||||
console.log('初始化数据库连接...');
|
||||
await initDB();
|
||||
|
||||
console.log('开始批量处理代理佣金...');
|
||||
|
||||
const db = getDB();
|
||||
|
||||
// 查找所有有代理关系且转账次数>=3但没有佣金记录的用户
|
||||
const [usersNeedCommission] = await db.execute(`
|
||||
SELECT
|
||||
am.agent_id,
|
||||
am.merchant_id,
|
||||
u.phone as merchant_phone,
|
||||
agent.phone as agent_phone,
|
||||
COUNT(t.id) as transfer_count,
|
||||
COUNT(acr.id) as commission_count
|
||||
FROM agent_merchants am
|
||||
JOIN users u ON am.merchant_id = u.id
|
||||
JOIN users agent ON am.agent_id = agent.id
|
||||
LEFT JOIN transfers t ON am.merchant_id = t.from_user_id AND t.status = 'received'
|
||||
LEFT JOIN agent_commission_records acr ON am.agent_id = acr.agent_id
|
||||
AND am.merchant_id = acr.merchant_id
|
||||
AND acr.description LIKE '%第三次转账%'
|
||||
GROUP BY am.agent_id, am.merchant_id
|
||||
HAVING transfer_count >= 3 AND commission_count = 0
|
||||
`);
|
||||
|
||||
if (usersNeedCommission.length === 0) {
|
||||
console.log('没有找到需要处理佣金的用户');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`找到 ${usersNeedCommission.length} 个用户需要处理佣金:`);
|
||||
|
||||
for (const user of usersNeedCommission) {
|
||||
console.log(`\n处理用户: ${user.merchant_phone} (ID: ${user.merchant_id})`);
|
||||
console.log(` - 代理: ${user.agent_phone} (ID: ${user.agent_id})`);
|
||||
console.log(` - 转账次数: ${user.transfer_count}`);
|
||||
|
||||
try {
|
||||
await matchingService.checkAndProcessAgentCommission(user.merchant_id);
|
||||
console.log(` ✅ 佣金处理成功`);
|
||||
} catch (error) {
|
||||
console.log(` ❌ 佣金处理失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== 批量处理完成 ===');
|
||||
|
||||
// 检查处理结果
|
||||
const [finalCommissions] = await db.execute(
|
||||
'SELECT COUNT(*) as total FROM agent_commission_records WHERE description LIKE "%第三次转账%"'
|
||||
);
|
||||
|
||||
console.log(`总共生成了 ${finalCommissions[0].total} 条佣金记录`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('批量处理失败:', error);
|
||||
} finally {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
batchProcessCommissions();
|
||||
File diff suppressed because it is too large
Load Diff
295
db-monitor.js
295
db-monitor.js
@@ -1,295 +0,0 @@
|
||||
const { getDB, dbConfig } = require('./database');
|
||||
const { logger } = require('./config/logger');
|
||||
|
||||
/**
|
||||
* 数据库连接监控工具
|
||||
* 用于诊断和监控数据库连接池状态
|
||||
*/
|
||||
class DatabaseMonitor {
|
||||
/**
|
||||
* 获取连接池详细状态
|
||||
* @returns {Object} 连接池状态信息
|
||||
*/
|
||||
/**
|
||||
* 获取连接池详细状态
|
||||
* @returns {Object} 连接池状态信息
|
||||
*/
|
||||
getPoolStatus() {
|
||||
try {
|
||||
const pool = getDB();
|
||||
|
||||
const status = {
|
||||
// 基本连接信息
|
||||
totalConnections: pool._allConnections ? pool._allConnections.length : 0,
|
||||
freeConnections: pool._freeConnections ? pool._freeConnections.length : 0,
|
||||
acquiringConnections: pool._acquiringConnections ? pool._acquiringConnections.length : 0,
|
||||
|
||||
// 计算使用率
|
||||
connectionLimit: dbConfig.connectionLimit,
|
||||
usageRate: 0,
|
||||
|
||||
// 配置信息
|
||||
config: {
|
||||
connectionLimit: dbConfig.connectionLimit,
|
||||
acquireTimeout: dbConfig.acquireTimeout,
|
||||
timeout: dbConfig.timeout,
|
||||
idleTimeout: dbConfig.idleTimeout,
|
||||
maxLifetime: dbConfig.maxLifetime,
|
||||
queueLimit: dbConfig.queueLimit,
|
||||
host: dbConfig.host,
|
||||
database: dbConfig.database
|
||||
},
|
||||
|
||||
// 时间戳
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 计算连接使用率
|
||||
if (status.connectionLimit > 0) {
|
||||
const usedConnections = status.totalConnections - status.freeConnections;
|
||||
status.usageRate = Math.round((usedConnections / status.connectionLimit) * 100);
|
||||
}
|
||||
|
||||
return status;
|
||||
} catch (error) {
|
||||
logger.error('Failed to get pool status', { error: error.message });
|
||||
return {
|
||||
error: error.message,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试数据库连接
|
||||
* @returns {Object} 连接测试结果
|
||||
*/
|
||||
async testConnection() {
|
||||
const startTime = Date.now();
|
||||
let connection;
|
||||
|
||||
try {
|
||||
const pool = getDB();
|
||||
|
||||
// 获取连接
|
||||
const acquireStart = Date.now();
|
||||
connection = await pool.getConnection();
|
||||
const acquireTime = Date.now() - acquireStart;
|
||||
|
||||
// 执行测试查询
|
||||
const queryStart = Date.now();
|
||||
const [result] = await connection.execute('SELECT 1 as test, NOW() as server_time');
|
||||
const queryTime = Date.now() - queryStart;
|
||||
|
||||
const totalTime = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
acquireTime,
|
||||
queryTime,
|
||||
totalTime,
|
||||
serverTime: result[0].server_time,
|
||||
connectionId: connection.threadId,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
const totalTime = Date.now() - startTime;
|
||||
|
||||
logger.error('Database connection test failed', {
|
||||
error: error.message,
|
||||
totalTime
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
errorCode: error.code,
|
||||
totalTime,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
} finally {
|
||||
if (connection) {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行连接池诊断
|
||||
* @returns {Object} 诊断结果
|
||||
*/
|
||||
async diagnose() {
|
||||
const poolStatus = this.getPoolStatus();
|
||||
const connectionTest = await this.testConnection();
|
||||
|
||||
const diagnosis = {
|
||||
poolStatus,
|
||||
connectionTest,
|
||||
issues: [],
|
||||
recommendations: [],
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 分析潜在问题
|
||||
if (poolStatus.usageRate > 90) {
|
||||
diagnosis.issues.push('连接池使用率过高 (>90%)');
|
||||
diagnosis.recommendations.push('考虑增加连接池大小或优化查询性能');
|
||||
}
|
||||
|
||||
if (poolStatus.freeConnections === 0) {
|
||||
diagnosis.issues.push('没有空闲连接可用');
|
||||
diagnosis.recommendations.push('立即检查是否存在连接泄漏或增加连接池大小');
|
||||
}
|
||||
|
||||
if (!connectionTest.success) {
|
||||
diagnosis.issues.push(`数据库连接失败: ${connectionTest.error}`);
|
||||
diagnosis.recommendations.push('检查数据库服务器状态和网络连接');
|
||||
} else {
|
||||
if (connectionTest.acquireTime > 5000) {
|
||||
diagnosis.issues.push('获取连接耗时过长 (>5秒)');
|
||||
diagnosis.recommendations.push('检查连接池配置和数据库负载');
|
||||
}
|
||||
|
||||
if (connectionTest.queryTime > 1000) {
|
||||
diagnosis.issues.push('查询响应时间过长 (>1秒)');
|
||||
diagnosis.recommendations.push('检查数据库性能和网络延迟');
|
||||
}
|
||||
}
|
||||
|
||||
return diagnosis;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成监控报告
|
||||
* @returns {string} 格式化的监控报告
|
||||
*/
|
||||
async generateReport() {
|
||||
const diagnosis = await this.diagnose();
|
||||
|
||||
let report = '\n=== 数据库连接监控报告 ===\n';
|
||||
report += `生成时间: ${diagnosis.timestamp}\n\n`;
|
||||
|
||||
// 连接池状态
|
||||
report += '【连接池状态】\n';
|
||||
if (diagnosis.poolStatus.error) {
|
||||
report += `错误: ${diagnosis.poolStatus.error}\n`;
|
||||
} else {
|
||||
report += `总连接数: ${diagnosis.poolStatus.totalConnections}\n`;
|
||||
report += `空闲连接: ${diagnosis.poolStatus.freeConnections}\n`;
|
||||
report += `获取中连接: ${diagnosis.poolStatus.acquiringConnections}\n`;
|
||||
report += `连接限制: ${diagnosis.poolStatus.connectionLimit}\n`;
|
||||
report += `使用率: ${diagnosis.poolStatus.usageRate}%\n`;
|
||||
}
|
||||
|
||||
// 连接测试
|
||||
report += '\n【连接测试】\n';
|
||||
if (diagnosis.connectionTest.success) {
|
||||
report += `状态: 成功\n`;
|
||||
report += `获取连接耗时: ${diagnosis.connectionTest.acquireTime}ms\n`;
|
||||
report += `查询耗时: ${diagnosis.connectionTest.queryTime}ms\n`;
|
||||
report += `总耗时: ${diagnosis.connectionTest.totalTime}ms\n`;
|
||||
report += `连接ID: ${diagnosis.connectionTest.connectionId}\n`;
|
||||
report += `服务器时间: ${diagnosis.connectionTest.serverTime}\n`;
|
||||
} else {
|
||||
report += `状态: 失败\n`;
|
||||
report += `错误: ${diagnosis.connectionTest.error}\n`;
|
||||
report += `错误代码: ${diagnosis.connectionTest.errorCode || 'N/A'}\n`;
|
||||
report += `总耗时: ${diagnosis.connectionTest.totalTime}ms\n`;
|
||||
}
|
||||
|
||||
// 问题和建议
|
||||
if (diagnosis.issues.length > 0) {
|
||||
report += '\n【发现的问题】\n';
|
||||
diagnosis.issues.forEach((issue, index) => {
|
||||
report += `${index + 1}. ${issue}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
if (diagnosis.recommendations.length > 0) {
|
||||
report += '\n【建议】\n';
|
||||
diagnosis.recommendations.forEach((rec, index) => {
|
||||
report += `${index + 1}. ${rec}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
if (diagnosis.issues.length === 0) {
|
||||
report += '\n【状态】\n数据库连接正常,未发现问题。\n';
|
||||
}
|
||||
|
||||
report += '\n=== 报告结束 ===\n';
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动实时监控
|
||||
* @param {number} interval 监控间隔(毫秒),默认30秒
|
||||
*/
|
||||
startMonitoring(interval = 30000) {
|
||||
console.log('启动数据库连接实时监控...');
|
||||
|
||||
const monitor = async () => {
|
||||
try {
|
||||
const status = this.getPoolStatus();
|
||||
|
||||
// 只在有问题时输出详细信息
|
||||
if (status.usageRate > 80 || status.freeConnections < 2) {
|
||||
console.warn('数据库连接池警告:', {
|
||||
usageRate: `${status.usageRate}%`,
|
||||
freeConnections: status.freeConnections,
|
||||
totalConnections: status.totalConnections
|
||||
});
|
||||
}
|
||||
|
||||
// 减少频繁的日志记录,只在有问题时记录
|
||||
if (status.usageRate > 80 || status.freeConnections < 2) {
|
||||
logger.warn('Database pool status warning', status);
|
||||
}
|
||||
// 注释掉正常情况下的日志记录
|
||||
// logger.info('Database pool status', status);
|
||||
|
||||
} catch (error) {
|
||||
console.error('监控过程中发生错误:', error);
|
||||
logger.error('Database monitoring error', { error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 立即执行一次
|
||||
monitor();
|
||||
|
||||
// 定期执行
|
||||
const intervalId = setInterval(monitor, interval);
|
||||
|
||||
// 返回停止函数
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
console.log('数据库连接监控已停止');
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
const dbMonitor = new DatabaseMonitor();
|
||||
|
||||
// 如果直接运行此文件,执行诊断
|
||||
if (require.main === module) {
|
||||
(async () => {
|
||||
try {
|
||||
// 初始化数据库
|
||||
const { initDB } = require('./database');
|
||||
await initDB();
|
||||
|
||||
console.log('正在执行数据库连接诊断...');
|
||||
const report = await dbMonitor.generateReport();
|
||||
console.log(report);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('诊断失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
module.exports = dbMonitor;
|
||||
87
docs/apis/captcha.js
Normal file
87
docs/apis/captcha.js
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Captcha
|
||||
* description: 验证码API
|
||||
*/
|
||||
/**
|
||||
* @swagger
|
||||
* /captcha/generate:
|
||||
* get:
|
||||
* summary: 生成图形验证码
|
||||
* tags: [Captcha]
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功生成验证码
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* captchaId:
|
||||
* type: string
|
||||
* description: 验证码唯一ID
|
||||
* image:
|
||||
* type: string
|
||||
* description: Base64编码的SVG验证码图片
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
/**
|
||||
* @swagger
|
||||
* /captcha/verify:
|
||||
* post:
|
||||
* summary: 验证用户输入的验证码
|
||||
* tags: [Captcha]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - captchaId
|
||||
* - captchaText
|
||||
* properties:
|
||||
* captchaId:
|
||||
* type: string
|
||||
* description: 验证码唯一ID
|
||||
* captchaText:
|
||||
* type: string
|
||||
* description: 用户输入的验证码
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 验证码验证成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* message:
|
||||
* type: string
|
||||
* example: 验证码验证成功
|
||||
* 400:
|
||||
* description: 验证码错误或已过期
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: false
|
||||
* message:
|
||||
* type: string
|
||||
* example: 验证码错误
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
159
docs/apis/matching.js
Normal file
159
docs/apis/matching.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Matching
|
||||
* description: 匹配订单相关接口
|
||||
*/
|
||||
/**
|
||||
* @swagger
|
||||
* /api/matching/my-orders:
|
||||
* get:
|
||||
* summary: 获取用户的匹配订单列表
|
||||
* tags: [Matching]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* description: 每页数量
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功获取匹配订单列表
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/MatchingOrder'
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* MatchingOrder:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 匹配订单ID
|
||||
* initiator_id:
|
||||
* type: integer
|
||||
* description: 发起人ID
|
||||
* matching_type:
|
||||
* type: string
|
||||
* enum: [small, large]
|
||||
* description: 匹配类型(小额或大额)
|
||||
* amount:
|
||||
* type: number
|
||||
* description: 匹配总金额
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [pending, matching, completed, failed]
|
||||
* description: 订单状态
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
* Allocation:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 分配ID
|
||||
* from_user_id:
|
||||
* type: integer
|
||||
* description: 发送方用户ID
|
||||
* to_user_id:
|
||||
* type: integer
|
||||
* description: 接收方用户ID
|
||||
* amount:
|
||||
* type: number
|
||||
* description: 分配金额
|
||||
* cycle_number:
|
||||
* type: integer
|
||||
* description: 轮次编号
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [pending, confirmed, rejected, cancelled]
|
||||
* description: 分配状态
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/matching/create:
|
||||
* post:
|
||||
* summary: 创建匹配订单
|
||||
* tags: [Matching]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* matchingType:
|
||||
* type: string
|
||||
* enum: [small, large]
|
||||
* default: small
|
||||
* description: 匹配类型(小额或大额)
|
||||
* customAmount:
|
||||
* type: number
|
||||
* description: 大额匹配时的自定义金额(5000-50000之间)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 匹配订单创建成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* matchingOrderId:
|
||||
* type: integer
|
||||
* amounts:
|
||||
* type: array
|
||||
* items:
|
||||
* type: number
|
||||
* matchingType:
|
||||
* type: string
|
||||
* totalAmount:
|
||||
* type: number
|
||||
* 400:
|
||||
* description: 参数错误或用户未满足匹配条件
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 404:
|
||||
* description: 用户不存在
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
388
docs/apis/transfers.js
Normal file
388
docs/apis/transfers.js
Normal file
@@ -0,0 +1,388 @@
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Transfer:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 转账记录ID
|
||||
* user_id:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* recipient_id:
|
||||
* type: integer
|
||||
* description: 接收方用户ID
|
||||
* amount:
|
||||
* type: number
|
||||
* format: float
|
||||
* description: 转账金额
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [pending, completed, failed, cancelled]
|
||||
* description: 转账状态
|
||||
* transfer_type:
|
||||
* type: string
|
||||
* enum: [user_to_user, user_to_system, system_to_user]
|
||||
* description: 转账类型
|
||||
* voucher_image:
|
||||
* type: string
|
||||
* description: 转账凭证图片路径
|
||||
* remark:
|
||||
* type: string
|
||||
* description: 转账备注
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 更新时间
|
||||
* Pagination:
|
||||
* type: object
|
||||
* properties:
|
||||
* total:
|
||||
* type: integer
|
||||
* description: 总记录数
|
||||
* page:
|
||||
* type: integer
|
||||
* description: 当前页码
|
||||
* limit:
|
||||
* type: integer
|
||||
* description: 每页记录数
|
||||
* total_pages:
|
||||
* type: integer
|
||||
* description: 总页数
|
||||
*/
|
||||
/**
|
||||
* @swagger
|
||||
* /transfers:
|
||||
* get:
|
||||
* summary: 获取转账列表
|
||||
* tags: [Transfers]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 转账状态过滤
|
||||
* - in: query
|
||||
* name: transfer_type
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 转账类型过滤
|
||||
* - in: query
|
||||
* name: start_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 开始日期过滤
|
||||
* - in: query
|
||||
* name: end_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 结束日期过滤
|
||||
* - in: query
|
||||
* name: search
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 搜索关键词(用户名或真实姓名)
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: sort
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 排序字段
|
||||
* - in: query
|
||||
* name: order
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [asc, desc]
|
||||
* description: 排序方向
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功获取转账列表
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* transfers:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Transfer'
|
||||
* pagination:
|
||||
* $ref: '#/components/schemas/Pagination'
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
/**
|
||||
* @swagger
|
||||
* /transfers/list:
|
||||
* get:
|
||||
* summary: 获取转账记录列表
|
||||
* tags: [Transfers]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 转账状态过滤
|
||||
* - in: query
|
||||
* name: transfer_type
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 转账类型过滤
|
||||
* - in: query
|
||||
* name: start_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 开始日期过滤
|
||||
* - in: query
|
||||
* name: end_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 结束日期过滤
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: sort
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 排序字段
|
||||
* - in: query
|
||||
* name: order
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [asc, desc]
|
||||
* description: 排序方向
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功获取转账记录列表
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* transfers:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Transfer'
|
||||
* pagination:
|
||||
* $ref: '#/components/schemas/Pagination'
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
/**
|
||||
* @swagger
|
||||
* /transfers/public-account:
|
||||
* get:
|
||||
* summary: 获取公户信息
|
||||
* tags: [Transfers]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功获取公户信息
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 公户ID
|
||||
* username:
|
||||
* type: string
|
||||
* description: 公户用户名
|
||||
* example: public_account
|
||||
* real_name:
|
||||
* type: string
|
||||
* description: 公户名称
|
||||
* balance:
|
||||
* type: number
|
||||
* format: float
|
||||
* description: 公户余额
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 404:
|
||||
* description: 公户不存在
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
/**
|
||||
* @swagger
|
||||
* /transfers/create:
|
||||
* post:
|
||||
* summary: 创建转账记录
|
||||
* tags: [Transfers]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - to_user_id
|
||||
* - amount
|
||||
* - transfer_type
|
||||
* properties:
|
||||
* to_user_id:
|
||||
* type: integer
|
||||
* description: 接收方用户ID
|
||||
* amount:
|
||||
* type: number
|
||||
* format: float
|
||||
* description: 转账金额
|
||||
* transfer_type:
|
||||
* type: string
|
||||
* enum: [user_to_user, user_to_system, system_to_user]
|
||||
* description: 转账类型
|
||||
* remark:
|
||||
* type: string
|
||||
* description: 转账备注
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 转账记录创建成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* message:
|
||||
* type: string
|
||||
* example: 转账记录创建成功,等待确认
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* transfer_id:
|
||||
* type: integer
|
||||
* description: 转账记录ID
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
/**
|
||||
* @swagger
|
||||
* /transfers/admin/create:
|
||||
* post:
|
||||
* summary: 管理员创建转账记录
|
||||
* tags: [Transfers]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - from_user_id
|
||||
* - to_user_id
|
||||
* - amount
|
||||
* - transfer_type
|
||||
* properties:
|
||||
* from_user_id:
|
||||
* type: integer
|
||||
* description: 发送方用户ID
|
||||
* to_user_id:
|
||||
* type: integer
|
||||
* description: 接收方用户ID
|
||||
* amount:
|
||||
* type: number
|
||||
* format: float
|
||||
* description: 转账金额
|
||||
* transfer_type:
|
||||
* type: string
|
||||
* enum: [user_to_user, user_to_system, system_to_user]
|
||||
* description: 转账类型
|
||||
* description:
|
||||
* type: string
|
||||
* description: 转账描述
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 转账记录创建成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* message:
|
||||
* type: string
|
||||
* example: 转账记录创建成功
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* transfer_id:
|
||||
* type: integer
|
||||
* description: 转账记录ID
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
367
docs/apis/user.js
Normal file
367
docs/apis/user.js
Normal file
@@ -0,0 +1,367 @@
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Authentication
|
||||
* description: 用户认证API
|
||||
*/
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* LoginCredentials:
|
||||
* type: object
|
||||
* required:
|
||||
* - username
|
||||
* - password
|
||||
* properties:
|
||||
* username:
|
||||
* type: string
|
||||
* description: 用户名或手机号
|
||||
* password:
|
||||
* type: string
|
||||
* description: 密码
|
||||
* RegisterRequest:
|
||||
* type: object
|
||||
* required:
|
||||
* - username
|
||||
* - phone
|
||||
* - password
|
||||
* - registrationCode
|
||||
* - city
|
||||
* - district_id
|
||||
* - captchaId
|
||||
* - captchaText
|
||||
* - smsCode
|
||||
* properties:
|
||||
* username:
|
||||
* type: string
|
||||
* description: 用户名
|
||||
* phone:
|
||||
* type: string
|
||||
* description: 手机号
|
||||
* password:
|
||||
* type: string
|
||||
* description: 密码
|
||||
* registrationCode:
|
||||
* type: string
|
||||
* description: 注册激活码
|
||||
* city:
|
||||
* type: string
|
||||
* description: 城市
|
||||
* district_id:
|
||||
* type: string
|
||||
* description: 区域ID
|
||||
* captchaId:
|
||||
* type: string
|
||||
* description: 图形验证码ID
|
||||
* captchaText:
|
||||
* type: string
|
||||
* description: 图形验证码文本
|
||||
* smsCode:
|
||||
* type: string
|
||||
* description: 短信验证码
|
||||
* role:
|
||||
* type: string
|
||||
* description: 用户角色
|
||||
* default: user
|
||||
*/
|
||||
/**
|
||||
* @swagger
|
||||
* /auth/register:
|
||||
* post:
|
||||
* summary: 用户注册
|
||||
* description: 需要提供有效的激活码才能注册
|
||||
* tags: [Authentication]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/RegisterRequest'
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 用户注册成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* token:
|
||||
* type: string
|
||||
* description: JWT认证令牌
|
||||
* user:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* username:
|
||||
* type: string
|
||||
* role:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /auth/login:
|
||||
* post:
|
||||
* summary: 用户登录
|
||||
* tags: [Authentication]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/LoginCredentials'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 登录成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* token:
|
||||
* type: string
|
||||
* description: JWT认证令牌
|
||||
* user:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* username:
|
||||
* type: string
|
||||
* role:
|
||||
* type: string
|
||||
* avatar:
|
||||
* type: string
|
||||
* points:
|
||||
* type: integer
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 用户名或密码错误
|
||||
* 403:
|
||||
* description: 账户审核未通过
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
/**
|
||||
* @swagger
|
||||
* /api/users/{id}/distribute:
|
||||
* put:
|
||||
* summary: 设置用户分发状态
|
||||
* description: 更新指定用户的分发状态
|
||||
* tags: [Users]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - is_distribute
|
||||
* properties:
|
||||
* is_distribute:
|
||||
* type: boolean
|
||||
* description: 分发状态,true为启用分发,false为禁用分发
|
||||
* example: true
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 分发状态更新成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* message:
|
||||
* type: string
|
||||
* example: "分发状态更新成功"
|
||||
* is_distribute:
|
||||
* type: boolean
|
||||
* description: 更新后的分发状态
|
||||
* example: true
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: false
|
||||
* message:
|
||||
* type: string
|
||||
* example: "分发状态无效"
|
||||
* 404:
|
||||
* description: 用户不存在
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: false
|
||||
* message:
|
||||
* type: string
|
||||
* example: "用户不存在"
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: false
|
||||
* message:
|
||||
* type: string
|
||||
* example: "服务器内部错误"
|
||||
*/
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* User:
|
||||
* type: object
|
||||
* required:
|
||||
* - username
|
||||
* - password
|
||||
* - real_name
|
||||
* - id_card
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* username:
|
||||
* type: string
|
||||
* description: 用户名
|
||||
* role:
|
||||
* type: string
|
||||
* description: 用户角色
|
||||
* enum: [user, admin, merchant]
|
||||
* avatar:
|
||||
* type: string
|
||||
* description: 用户头像URL
|
||||
* points:
|
||||
* type: integer
|
||||
* description: 用户积分
|
||||
* real_name:
|
||||
* type: string
|
||||
* description: 真实姓名
|
||||
* id_card:
|
||||
* type: string
|
||||
* description: 身份证号
|
||||
* phone:
|
||||
* type: string
|
||||
* description: 手机号
|
||||
* is_system_account:
|
||||
* type: boolean
|
||||
* description: 是否为系统账户
|
||||
* is_distribute:
|
||||
* type: boolean
|
||||
* description: 是否为分发账户
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 更新时间
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /users:
|
||||
* post:
|
||||
* summary: 创建用户(管理员权限)
|
||||
* tags: [Users]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - username
|
||||
* - password
|
||||
* - real_name
|
||||
* - id_card
|
||||
* properties:
|
||||
* username:
|
||||
* type: string
|
||||
* password:
|
||||
* type: string
|
||||
* role:
|
||||
* type: string
|
||||
* enum: [user, admin, merchant]
|
||||
* default: user
|
||||
* is_system_account:
|
||||
* type: boolean
|
||||
* default: false
|
||||
* real_name:
|
||||
* type: string
|
||||
* id_card:
|
||||
* type: string
|
||||
* wechat_qr:
|
||||
* type: string
|
||||
* alipay_qr:
|
||||
* type: string
|
||||
* bank_card:
|
||||
* type: string
|
||||
* unionpay_qr:
|
||||
* type: string
|
||||
* phone:
|
||||
* type: string
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 用户创建成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* user:
|
||||
* $ref: '#/components/schemas/User'
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
214
routes/agents.js
214
routes/agents.js
@@ -4,6 +4,8 @@ const { getDB } = require('../database');
|
||||
const QRCode = require('qrcode');
|
||||
const crypto = require('crypto');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { auth } = require('../middleware/auth');
|
||||
const dayjs = require('dayjs');
|
||||
|
||||
// 获取浙江省所有区域列表
|
||||
router.get('/regions', async (req, res) => {
|
||||
@@ -22,7 +24,7 @@ router.get('/regions', async (req, res) => {
|
||||
router.post('/apply', async (req, res) => {
|
||||
try {
|
||||
const { region_id, real_name, phone, id_card, contact_address } = req.body;
|
||||
|
||||
|
||||
if (!region_id || !real_name || !phone || !id_card) {
|
||||
return res.status(400).json({ success: false, message: '请填写完整信息' });
|
||||
}
|
||||
@@ -51,13 +53,13 @@ router.post('/apply', async (req, res) => {
|
||||
let userId;
|
||||
if (existingUser.length > 0) {
|
||||
userId = existingUser[0].id;
|
||||
|
||||
|
||||
// 检查该用户是否已申请过代理(包括所有状态)
|
||||
const [existingUserAgent] = await getDB().execute(
|
||||
'SELECT id, status, region_id FROM regional_agents WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
|
||||
if (existingUserAgent.length > 0) {
|
||||
const agentStatus = existingUserAgent[0].status;
|
||||
if (agentStatus === 'active') {
|
||||
@@ -73,7 +75,7 @@ router.post('/apply', async (req, res) => {
|
||||
const bcrypt = require('bcryptjs');
|
||||
const tempPassword = Math.random().toString(36).slice(-8); // 生成8位临时密码
|
||||
const hashedPassword = await bcrypt.hash(tempPassword, 10);
|
||||
|
||||
|
||||
const [userResult] = await getDB().execute(
|
||||
'INSERT INTO users (username, password, phone, real_name, id_card, created_at) VALUES (?, ?, ?, ?, ?, NOW())',
|
||||
[phone, hashedPassword, phone, real_name, id_card]
|
||||
@@ -101,7 +103,7 @@ router.post('/apply', async (req, res) => {
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const { phone, password } = req.body;
|
||||
|
||||
|
||||
if (!phone || !password) {
|
||||
return res.status(400).json({ success: false, message: '请输入手机号和密码' });
|
||||
}
|
||||
@@ -121,7 +123,7 @@ router.post('/login', async (req, res) => {
|
||||
}
|
||||
|
||||
const agent = agents[0];
|
||||
|
||||
|
||||
// 验证密码
|
||||
const isPasswordValid = await bcrypt.compare(password, agent.password);
|
||||
if (!isPasswordValid) {
|
||||
@@ -132,9 +134,9 @@ router.post('/login', async (req, res) => {
|
||||
const jwt = require('jsonwebtoken');
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
userId: agent.user_id,
|
||||
username: agent.username || agent.phone,
|
||||
{
|
||||
userId: agent.user_id,
|
||||
username: agent.username || agent.phone,
|
||||
role: agent.role || 'agent',
|
||||
agentId: agent.id
|
||||
},
|
||||
@@ -144,13 +146,13 @@ router.post('/login', async (req, res) => {
|
||||
|
||||
delete agent.password; // 不返回密码
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
...agent,
|
||||
token
|
||||
},
|
||||
message: '登录成功'
|
||||
},
|
||||
message: '登录成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('代理登录失败:', error);
|
||||
@@ -158,56 +160,6 @@ router.post('/login', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 生成注册二维码
|
||||
router.post('/generate-invite-code', async (req, res) => {
|
||||
try {
|
||||
const { agent_id } = req.body;
|
||||
|
||||
if (!agent_id) {
|
||||
return res.status(400).json({ success: false, message: '代理ID不能为空' });
|
||||
}
|
||||
|
||||
// 验证代理是否存在且激活,并获取对应的user_id
|
||||
const [agents] = await getDB().execute(
|
||||
'SELECT id, user_id FROM regional_agents WHERE id = ? AND status = "active"',
|
||||
[parseInt(agent_id)]
|
||||
);
|
||||
|
||||
if (agents.length === 0) {
|
||||
return res.status(404).json({ success: false, message: '代理不存在或未激活' });
|
||||
}
|
||||
|
||||
const userIdForAgent = agents[0].user_id;
|
||||
|
||||
// 生成唯一激活码
|
||||
const code = crypto.randomBytes(8).toString('hex').toUpperCase();
|
||||
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30天后过期
|
||||
|
||||
// 插入激活码记录(created_by_admin_id设为3608,agent_id使用user_id)
|
||||
await getDB().execute(
|
||||
`INSERT INTO registration_codes (code, expires_at, created_by_admin_id, agent_id, is_used, created_at) VALUES ('${code}', '${expiresAt.toISOString().slice(0, 19).replace('T', ' ')}', 3608, ${userIdForAgent}, 0, NOW())`
|
||||
);
|
||||
|
||||
// 生成二维码 - 使用注册页面URL(不包含邀请码参数)
|
||||
const registerUrl = `${process.env.FRONTEND_URL || 'https://www.zrbjr.com/frontend'}/register`;
|
||||
|
||||
const qrCodeUrl = await QRCode.toDataURL(registerUrl);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
code: code,
|
||||
qr_code: qrCodeUrl,
|
||||
expires_at: expiresAt
|
||||
},
|
||||
message: '二维码生成成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('生成二维码失败:', error);
|
||||
res.status(500).json({ success: false, message: '生成二维码失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取代理的商户列表(包含所有商户,标注早期商户状态)
|
||||
router.get('/merchants/:agent_id', async (req, res) => {
|
||||
try {
|
||||
@@ -221,11 +173,11 @@ router.get('/merchants/:agent_id', async (req, res) => {
|
||||
[parseInt(agent_id)]
|
||||
);
|
||||
const regionId = agentInfo[0].region_id;
|
||||
|
||||
|
||||
if (!agentInfo || agentInfo.length === 0) {
|
||||
return res.status(404).json({ success: false, message: '代理不存在' });
|
||||
}
|
||||
|
||||
|
||||
const agentCreatedAt = agentInfo[0].agent_created_at;
|
||||
|
||||
// 获取商户列表(包含所有商户,包括agent_merchants表中的和符合条件的早期商户)
|
||||
@@ -264,7 +216,7 @@ router.get('/merchants/:agent_id', async (req, res) => {
|
||||
WHERE (am.agent_id = ? OR (u.created_at < ? AND u.district_id = ? AND u.role = 'user'))`,
|
||||
[parseInt(agent_id), parseInt(agent_id), agentCreatedAt, parseInt(regionId)]
|
||||
);
|
||||
|
||||
|
||||
// 获取早期商户统计(从user表获取所有符合条件的早期商户)
|
||||
// 早期商户的判断条件:1.早期商户注册时间比代理要早。2.代理商代理的区县与商户的区县一致
|
||||
const [earlyMerchantStats] = await getDB().execute(
|
||||
@@ -276,7 +228,7 @@ router.get('/merchants/:agent_id', async (req, res) => {
|
||||
AND u.role = 'user'`,
|
||||
[agentCreatedAt, parseInt(regionId)]
|
||||
);
|
||||
|
||||
|
||||
// 获取正常商户统计(包括代理关联的商户,排除符合条件的早期商户)
|
||||
const [normalMerchantStats] = await getDB().execute(
|
||||
`SELECT
|
||||
@@ -287,8 +239,8 @@ router.get('/merchants/:agent_id', async (req, res) => {
|
||||
[parseInt(agent_id), parseInt(agent_id), agentCreatedAt, parseInt(regionId)]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
merchants,
|
||||
total: parseInt(countResult[0].total),
|
||||
@@ -336,8 +288,8 @@ router.get('/commissions/:agent_id', async (req, res) => {
|
||||
summary[0].paid_commission = summary[0].total_commission;
|
||||
summary[0].pending_commission = 0;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
commissions,
|
||||
summary: summary[0],
|
||||
@@ -379,20 +331,20 @@ router.get('/list', async (req, res) => {
|
||||
try {
|
||||
const { page = 1, limit = 10, status, region_id } = req.query;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
|
||||
let whereClause = '1=1';
|
||||
let params = [];
|
||||
|
||||
|
||||
if (status) {
|
||||
whereClause += ' AND ra.status = ?';
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
|
||||
if (region_id) {
|
||||
whereClause += ' AND ra.region_id = ?';
|
||||
params.push(region_id);
|
||||
}
|
||||
|
||||
|
||||
// 获取代理列表
|
||||
const [agents] = await getDB().execute(
|
||||
`SELECT ra.*, u.username, u.phone, u.real_name, u.created_at as user_created_at,
|
||||
@@ -404,7 +356,7 @@ router.get('/list', async (req, res) => {
|
||||
ORDER BY ra.created_at DESC
|
||||
LIMIT ${limit} OFFSET ${offset}`
|
||||
);
|
||||
|
||||
|
||||
// 获取总数
|
||||
const [countResult] = await getDB().execute(
|
||||
`SELECT COUNT(*) as total
|
||||
@@ -413,10 +365,10 @@ router.get('/list', async (req, res) => {
|
||||
JOIN zhejiang_regions zr ON ra.region_id = zr.id
|
||||
WHERE ${whereClause}`
|
||||
);
|
||||
|
||||
|
||||
const total = countResult[0].total;
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
@@ -446,7 +398,7 @@ router.get('/commission-trend/:agent_id', async (req, res) => {
|
||||
try {
|
||||
const { agent_id } = req.params;
|
||||
const { period = '7d' } = req.query;
|
||||
|
||||
|
||||
// 根据周期确定天数
|
||||
let days;
|
||||
switch (period) {
|
||||
@@ -462,7 +414,7 @@ router.get('/commission-trend/:agent_id', async (req, res) => {
|
||||
default:
|
||||
days = 7;
|
||||
}
|
||||
|
||||
|
||||
// 获取指定时间范围内的佣金趋势数据
|
||||
const [trendData] = await getDB().execute(
|
||||
`SELECT
|
||||
@@ -475,33 +427,33 @@ router.get('/commission-trend/:agent_id', async (req, res) => {
|
||||
ORDER BY date ASC`,
|
||||
[parseInt(agent_id), days]
|
||||
);
|
||||
|
||||
|
||||
// 填充缺失的日期(确保每天都有数据点)
|
||||
const filledData = [];
|
||||
const today = new Date();
|
||||
|
||||
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = new Date(today);
|
||||
date.setDate(date.getDate() - i);
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
|
||||
|
||||
// 修复日期比较:将数据库返回的Date对象转换为字符串进行比较
|
||||
const existingData = trendData.find(item => {
|
||||
const itemDateStr = item.date instanceof Date ?
|
||||
item.date.toISOString().split('T')[0] :
|
||||
const itemDateStr = item.date instanceof Date ?
|
||||
item.date.toISOString().split('T')[0] :
|
||||
item.date;
|
||||
return itemDateStr === dateStr;
|
||||
});
|
||||
|
||||
|
||||
filledData.push({
|
||||
date: dateStr,
|
||||
amount: existingData ? parseFloat(existingData.amount) : 0
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: filledData
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: filledData
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取佣金趋势数据失败:', error);
|
||||
@@ -518,7 +470,7 @@ router.get('/commission-trend/:agent_id', async (req, res) => {
|
||||
router.get('/merchant-status/:agent_id', async (req, res) => {
|
||||
try {
|
||||
const { agent_id } = req.params;
|
||||
|
||||
|
||||
// 获取商户状态分布
|
||||
const [statusData] = await getDB().execute(
|
||||
`SELECT
|
||||
@@ -536,10 +488,10 @@ router.get('/merchant-status/:agent_id', async (req, res) => {
|
||||
ORDER BY count DESC`,
|
||||
[parseInt(agent_id)]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: statusData
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: statusData
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取商户状态分布数据失败:', error);
|
||||
@@ -556,7 +508,7 @@ router.get('/merchant-status/:agent_id', async (req, res) => {
|
||||
router.get('/detailed-stats/:agent_id', async (req, res) => {
|
||||
try {
|
||||
const { agent_id } = req.params;
|
||||
|
||||
|
||||
// 获取基础统计数据
|
||||
const [basicStats] = await getDB().execute(
|
||||
`SELECT
|
||||
@@ -568,7 +520,7 @@ router.get('/detailed-stats/:agent_id', async (req, res) => {
|
||||
(SELECT COUNT(*) FROM agent_commission_records WHERE agent_id = ?) as total_commission_records`,
|
||||
[parseInt(agent_id), parseInt(agent_id), parseInt(agent_id), parseInt(agent_id), parseInt(agent_id), parseInt(agent_id)]
|
||||
);
|
||||
|
||||
|
||||
// 获取本月佣金
|
||||
const [monthlyStats] = await getDB().execute(
|
||||
`SELECT
|
||||
@@ -580,7 +532,7 @@ router.get('/detailed-stats/:agent_id', async (req, res) => {
|
||||
AND MONTH(created_at) = MONTH(CURDATE())`,
|
||||
[parseInt(agent_id)]
|
||||
);
|
||||
|
||||
|
||||
// 获取今日佣金
|
||||
const [dailyStats] = await getDB().execute(
|
||||
`SELECT
|
||||
@@ -591,7 +543,7 @@ router.get('/detailed-stats/:agent_id', async (req, res) => {
|
||||
AND DATE(created_at) = CURDATE()`,
|
||||
[parseInt(agent_id)]
|
||||
);
|
||||
|
||||
|
||||
// 获取最近7天新增商户数
|
||||
const [weeklyMerchants] = await getDB().execute(
|
||||
`SELECT COUNT(*) as weekly_new_merchants
|
||||
@@ -601,7 +553,7 @@ router.get('/detailed-stats/:agent_id', async (req, res) => {
|
||||
AND am.created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)`,
|
||||
[parseInt(agent_id)]
|
||||
);
|
||||
|
||||
|
||||
// 获取提现相关统计数据
|
||||
const [withdrawalStats] = await getDB().execute(
|
||||
`SELECT
|
||||
@@ -619,7 +571,7 @@ router.get('/detailed-stats/:agent_id', async (req, res) => {
|
||||
WHERE ra.id = ?`,
|
||||
[parseInt(agent_id), parseInt(agent_id)]
|
||||
);
|
||||
|
||||
|
||||
// 合并所有统计数据
|
||||
const stats = {
|
||||
...basicStats[0],
|
||||
@@ -632,10 +584,10 @@ router.get('/detailed-stats/:agent_id', async (req, res) => {
|
||||
available_amount: 0
|
||||
})
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: stats
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: stats
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取详细统计数据失败:', error);
|
||||
@@ -658,17 +610,17 @@ router.get('/merchants/:agent_id/transfers', async (req, res) => {
|
||||
const pageNum = parseInt(page) || 1;
|
||||
const limitNum = parseInt(limit) || 10;
|
||||
const offset = (pageNum - 1) * limitNum;
|
||||
|
||||
|
||||
// 检查代理是否存在
|
||||
const [agentResult] = await getDB().execute(
|
||||
'SELECT * FROM regional_agents WHERE id = ?',
|
||||
[parseInt(agent_id)]
|
||||
);
|
||||
|
||||
|
||||
if (agentResult.length === 0) {
|
||||
return res.status(404).json({ success: false, message: '代理不存在' });
|
||||
}
|
||||
|
||||
|
||||
// 查询商户转账记录
|
||||
const transferQuery = `
|
||||
SELECT
|
||||
@@ -694,7 +646,7 @@ router.get('/merchants/:agent_id/transfers', async (req, res) => {
|
||||
LIMIT ${limitNum} OFFSET ${offset}
|
||||
`;
|
||||
const [transfers] = await getDB().execute(transferQuery, [parseInt(agent_id)]);
|
||||
|
||||
|
||||
// 查询总数
|
||||
const [totalResult] = await getDB().execute(
|
||||
`SELECT COUNT(*) as total
|
||||
@@ -703,9 +655,9 @@ router.get('/merchants/:agent_id/transfers', async (req, res) => {
|
||||
WHERE am.agent_id = ?`,
|
||||
[parseInt(agent_id)]
|
||||
);
|
||||
|
||||
|
||||
const total = totalResult[0].total;
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
@@ -723,5 +675,45 @@ router.get('/merchants/:agent_id/transfers', async (req, res) => {
|
||||
res.status(500).json({ success: false, message: '获取代理商户转账记录失败,请稍后再试' });
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 获取分销列表
|
||||
* @route GET /agents/distribution
|
||||
* @returns {Object} 分销列表
|
||||
*/
|
||||
router.get('/distribution', auth, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.user;
|
||||
const { page = 1, size = 10 } = req.query;
|
||||
const pageNum = parseInt(page) || 1;
|
||||
const limitNum = parseInt(size) || 10;
|
||||
const offset = (page - 1) * size;
|
||||
const [result] = await getDB().execute(
|
||||
`SELECT real_name,phone,username,avatar,created_at FROM users WHERE inviter = ? ORDER BY created_at DESC
|
||||
LIMIT ${size} OFFSET ${offset}`,
|
||||
[parseInt(id)]
|
||||
);
|
||||
const [totalResult] = await getDB().execute(
|
||||
`SELECT COUNT(*) as total FROM users WHERE inviter = ? `,
|
||||
[parseInt(id)]
|
||||
);
|
||||
result.forEach(item => {
|
||||
item.created_at = dayjs(item.created_at).format('YYYY-MM-DD HH:mm:ss');
|
||||
})
|
||||
|
||||
const total = totalResult[0].total;
|
||||
res.json({
|
||||
success: true, data: result, pagination: {
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
total,
|
||||
pages: Math.ceil(total / limitNum)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取分销列表失败:', error);
|
||||
res.status(500).json({ success: false, message: '获取分销列表失败' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
@@ -3,6 +3,7 @@ const router = express.Router();
|
||||
const { getDB } = require('../../database');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { auth, adminAuth } = require('../../middleware/auth');
|
||||
const dayjs = require('dayjs');
|
||||
|
||||
// 创建管理员认证中间件组合
|
||||
const authenticateAdmin = [auth, adminAuth];
|
||||
|
||||
190
routes/auth.js
190
routes/auth.js
@@ -5,119 +5,6 @@ const { getDB } = require('../database');
|
||||
|
||||
const router = express.Router();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
||||
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Authentication
|
||||
* description: 用户认证API
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* LoginCredentials:
|
||||
* type: object
|
||||
* required:
|
||||
* - username
|
||||
* - password
|
||||
* properties:
|
||||
* username:
|
||||
* type: string
|
||||
* description: 用户名或手机号
|
||||
* password:
|
||||
* type: string
|
||||
* description: 密码
|
||||
* RegisterRequest:
|
||||
* type: object
|
||||
* required:
|
||||
* - username
|
||||
* - phone
|
||||
* - password
|
||||
* - registrationCode
|
||||
* - city
|
||||
* - district_id
|
||||
* - captchaId
|
||||
* - captchaText
|
||||
* - smsCode
|
||||
* properties:
|
||||
* username:
|
||||
* type: string
|
||||
* description: 用户名
|
||||
* phone:
|
||||
* type: string
|
||||
* description: 手机号
|
||||
* password:
|
||||
* type: string
|
||||
* description: 密码
|
||||
* registrationCode:
|
||||
* type: string
|
||||
* description: 注册激活码
|
||||
* city:
|
||||
* type: string
|
||||
* description: 城市
|
||||
* district_id:
|
||||
* type: string
|
||||
* description: 区域ID
|
||||
* captchaId:
|
||||
* type: string
|
||||
* description: 图形验证码ID
|
||||
* captchaText:
|
||||
* type: string
|
||||
* description: 图形验证码文本
|
||||
* smsCode:
|
||||
* type: string
|
||||
* description: 短信验证码
|
||||
* role:
|
||||
* type: string
|
||||
* description: 用户角色
|
||||
* default: user
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /auth/register:
|
||||
* post:
|
||||
* summary: 用户注册
|
||||
* description: 需要提供有效的激活码才能注册
|
||||
* tags: [Authentication]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/RegisterRequest'
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 用户注册成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* token:
|
||||
* type: string
|
||||
* description: JWT认证令牌
|
||||
* user:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* username:
|
||||
* type: string
|
||||
* role:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.post('/register', async (req, res) => {
|
||||
try {
|
||||
const db = getDB();
|
||||
@@ -129,13 +16,15 @@ router.post('/register', async (req, res) => {
|
||||
password,
|
||||
city,
|
||||
district_id: district,
|
||||
province,
|
||||
inviter = '',
|
||||
captchaId,
|
||||
captchaText,
|
||||
smsCode, // 短信验证码
|
||||
role = 'user'
|
||||
} = req.body;
|
||||
|
||||
if (!username || !phone || !password || !city || !district) {
|
||||
if (!username || !phone || !password || !city || !district || !province) {
|
||||
return res.status(400).json({ success: false, message: '用户名、手机号、密码、城市和区域不能为空' });
|
||||
}
|
||||
|
||||
@@ -146,9 +35,6 @@ router.post('/register', async (req, res) => {
|
||||
if (!smsCode) {
|
||||
return res.status(400).json({ success: false, message: '短信验证码不能为空' });
|
||||
}
|
||||
|
||||
// 注意:图形验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证
|
||||
|
||||
// 验证短信验证码
|
||||
const smsAPI = require('./sms');
|
||||
const smsValid = smsAPI.verifySMSCode(phone, smsCode);
|
||||
@@ -171,14 +57,7 @@ router.post('/register', async (req, res) => {
|
||||
);
|
||||
|
||||
if (existingUsers.length > 0) {
|
||||
const existingUser = existingUsers[0];
|
||||
// 如果用户存在但未支付,允许重新注册(覆盖原用户信息)
|
||||
if (existingUser.payment_status === 'unpaid') {
|
||||
// 删除未支付的用户记录
|
||||
await db.execute('DELETE FROM users WHERE id = ?', [existingUser.id]);
|
||||
} else {
|
||||
return res.status(400).json({ success: false, message: '用户名或手机号已存在' });
|
||||
}
|
||||
return res.status(400).json({ success: false, message: '用户名或手机号已存在' });
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
@@ -186,16 +65,11 @@ router.post('/register', async (req, res) => {
|
||||
|
||||
// 创建用户(初始状态为未支付)
|
||||
const [result] = await db.execute(
|
||||
'INSERT INTO users (username, phone, password, role, points, audit_status, city, district_id, payment_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, "unpaid")',
|
||||
[username, phone, hashedPassword, role, 0, 'pending', city, district]
|
||||
'INSERT INTO users (username, phone, password, role, points, audit_status, city, district_id, payment_status, province, inviter) VALUES (?, ?, ?, ?, ?, ?, ?, ?, "unpaid", ?, ?)',
|
||||
[username, phone, hashedPassword, role, 0, 'pending', city, district, province, inviter]
|
||||
);
|
||||
|
||||
const userId = result.insertId;
|
||||
|
||||
// 用户余额已在创建用户时设置为默认值0.00,无需额外操作
|
||||
|
||||
|
||||
|
||||
// 根据地区自动关联代理
|
||||
const [agents] = await db.execute(
|
||||
'SELECT ra.id FROM users u INNER JOIN regional_agents ra ON u.id = ra.user_id WHERE ra.region_id = ? AND ra.status = "active" ORDER BY ra.created_at ASC LIMIT 1',
|
||||
@@ -237,7 +111,7 @@ router.post('/register', async (req, res) => {
|
||||
});
|
||||
} catch (error) {
|
||||
try {
|
||||
await getDB().query('ROLLBACK');
|
||||
// await getDB().query('ROLLBACK');
|
||||
} catch (rollbackError) {
|
||||
console.error('回滚错误:', rollbackError);
|
||||
}
|
||||
@@ -251,55 +125,7 @@ router.post('/register', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /auth/login:
|
||||
* post:
|
||||
* summary: 用户登录
|
||||
* tags: [Authentication]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/LoginCredentials'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 登录成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* token:
|
||||
* type: string
|
||||
* description: JWT认证令牌
|
||||
* user:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* username:
|
||||
* type: string
|
||||
* role:
|
||||
* type: string
|
||||
* avatar:
|
||||
* type: string
|
||||
* points:
|
||||
* type: integer
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 用户名或密码错误
|
||||
* 403:
|
||||
* description: 账户审核未通过
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const db = getDB();
|
||||
|
||||
@@ -2,12 +2,7 @@ const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Captcha
|
||||
* description: 验证码API
|
||||
*/
|
||||
|
||||
|
||||
// 内存存储验证码(生产环境建议使用Redis)
|
||||
|
||||
@@ -107,35 +102,7 @@ function generateCaptchaSVG(text) {
|
||||
return svg;
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /captcha/generate:
|
||||
* get:
|
||||
* summary: 生成图形验证码
|
||||
* tags: [Captcha]
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功生成验证码
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* captchaId:
|
||||
* type: string
|
||||
* description: 验证码唯一ID
|
||||
* image:
|
||||
* type: string
|
||||
* description: Base64编码的SVG验证码图片
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
|
||||
router.get('/generate', (req, res) => {
|
||||
try {
|
||||
// 生成验证码文本
|
||||
@@ -169,58 +136,7 @@ router.get('/generate', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /captcha/verify:
|
||||
* post:
|
||||
* summary: 验证用户输入的验证码
|
||||
* tags: [Captcha]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - captchaId
|
||||
* - captchaText
|
||||
* properties:
|
||||
* captchaId:
|
||||
* type: string
|
||||
* description: 验证码唯一ID
|
||||
* captchaText:
|
||||
* type: string
|
||||
* description: 用户输入的验证码
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 验证码验证成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* message:
|
||||
* type: string
|
||||
* example: 验证码验证成功
|
||||
* 400:
|
||||
* description: 验证码错误或已过期
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: false
|
||||
* message:
|
||||
* type: string
|
||||
* example: 验证码错误
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
|
||||
router.post('/verify', (req, res) => {
|
||||
try {
|
||||
const { captchaId, captchaText } = req.body;
|
||||
|
||||
@@ -4,133 +4,17 @@ const { getDB } = require('../database');
|
||||
const matchingService = require('../services/matchingService');
|
||||
const { auth } = require('../middleware/auth');
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Matching
|
||||
* description: 匹配订单相关接口
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* MatchingOrder:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 匹配订单ID
|
||||
* initiator_id:
|
||||
* type: integer
|
||||
* description: 发起人ID
|
||||
* matching_type:
|
||||
* type: string
|
||||
* enum: [small, large]
|
||||
* description: 匹配类型(小额或大额)
|
||||
* amount:
|
||||
* type: number
|
||||
* description: 匹配总金额
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [pending, matching, completed, failed]
|
||||
* description: 订单状态
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
* Allocation:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 分配ID
|
||||
* from_user_id:
|
||||
* type: integer
|
||||
* description: 发送方用户ID
|
||||
* to_user_id:
|
||||
* type: integer
|
||||
* description: 接收方用户ID
|
||||
* amount:
|
||||
* type: number
|
||||
* description: 分配金额
|
||||
* cycle_number:
|
||||
* type: integer
|
||||
* description: 轮次编号
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [pending, confirmed, rejected, cancelled]
|
||||
* description: 分配状态
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/matching/create:
|
||||
* post:
|
||||
* summary: 创建匹配订单
|
||||
* tags: [Matching]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* matchingType:
|
||||
* type: string
|
||||
* enum: [small, large]
|
||||
* default: small
|
||||
* description: 匹配类型(小额或大额)
|
||||
* customAmount:
|
||||
* type: number
|
||||
* description: 大额匹配时的自定义金额(5000-50000之间)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 匹配订单创建成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* matchingOrderId:
|
||||
* type: integer
|
||||
* amounts:
|
||||
* type: array
|
||||
* items:
|
||||
* type: number
|
||||
* matchingType:
|
||||
* type: string
|
||||
* totalAmount:
|
||||
* type: number
|
||||
* 400:
|
||||
* description: 参数错误或用户未满足匹配条件
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 404:
|
||||
* description: 用户不存在
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.post('/create', auth, async (req, res) => {
|
||||
try {
|
||||
console.log('匹配订单创建请求 - 用户ID:', req.user.id);
|
||||
console.log('请求体:', req.body);
|
||||
const userId = req.user.id;
|
||||
const { matchingType = 'small', customAmount } = req.body;
|
||||
|
||||
const [user_type] = await getDB().query(`SELECT count(*) as total FROM users WHERE id=${userId} and user_type='directly_operated'`);
|
||||
if(user_type[0].total > 0){
|
||||
return res.status(400).json({message: '平台暂不支持直营用户获得融豆'})
|
||||
}
|
||||
// 验证匹配类型
|
||||
if (!['small', 'large'].includes(matchingType)) {
|
||||
return res.status(400).json({ message: '无效的匹配类型' });
|
||||
@@ -141,8 +25,8 @@ router.post('/create', auth, async (req, res) => {
|
||||
if (!customAmount || typeof customAmount !== 'number') {
|
||||
return res.status(400).json({ message: '大额匹配需要指定金额' });
|
||||
}
|
||||
if (customAmount < 5000 || customAmount > 50000) {
|
||||
return res.status(400).json({ message: '大额匹配金额必须在5000-50000之间' });
|
||||
if (customAmount < 3000 || customAmount > 50000) {
|
||||
return res.status(400).json({ message: '大额匹配金额必须在3000-50000之间' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,46 +92,7 @@ router.post('/create', auth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/matching/my-orders:
|
||||
* get:
|
||||
* summary: 获取用户的匹配订单列表
|
||||
* tags: [Matching]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* description: 每页数量
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功获取匹配订单列表
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/MatchingOrder'
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
|
||||
router.get('/my-orders', auth, async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
|
||||
@@ -106,7 +106,7 @@ function generateSMSCode() {
|
||||
router.post('/send', async (req, res) => {
|
||||
try {
|
||||
const { phone } = req.body
|
||||
|
||||
|
||||
// 验证手机号格式
|
||||
const phoneRegex = /^1[3-9]\d{9}$/
|
||||
if (!phoneRegex.test(phone)) {
|
||||
@@ -115,7 +115,7 @@ router.post('/send', async (req, res) => {
|
||||
message: '手机号格式不正确'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 检查发送频率限制
|
||||
const lastSendTime = smsCodeStore.get(`last_send_${phone}`)
|
||||
if (lastSendTime && Date.now() - lastSendTime < SEND_INTERVAL) {
|
||||
@@ -125,31 +125,38 @@ router.post('/send', async (req, res) => {
|
||||
message: `请等待${remainingTime}秒后再发送`
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 生成6位数字验证码
|
||||
const code = Math.random().toString().slice(-6)
|
||||
|
||||
|
||||
// 存储验证码信息
|
||||
smsCodeStore.set(phone, {
|
||||
code,
|
||||
timestamp: Date.now(),
|
||||
attempts: 0
|
||||
})
|
||||
|
||||
|
||||
// 记录发送时间
|
||||
smsCodeStore.set(`last_send_${phone}`, Date.now())
|
||||
// 生产环境发送真实短信
|
||||
try {
|
||||
console.log(code);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '验证码发送成功'
|
||||
})
|
||||
return
|
||||
const sendSmsRequest = new Dysmsapi20170525.SendSmsRequest({
|
||||
phoneNumbers: phone,
|
||||
signName: SMS_CONFIG.signName,
|
||||
templateCode: SMS_CONFIG.templateCode,
|
||||
templateParam: JSON.stringify({ code })
|
||||
})
|
||||
|
||||
|
||||
const response = await client.sendSms(sendSmsRequest)
|
||||
console.log(response.body);
|
||||
|
||||
console.log(response.body);
|
||||
|
||||
if (response.body.code === 'OK') {
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -169,7 +176,7 @@ router.post('/send', async (req, res) => {
|
||||
message: '发送失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('发送短信验证码失败:', error)
|
||||
res.status(500).json({
|
||||
@@ -230,43 +237,43 @@ router.post('/send', async (req, res) => {
|
||||
router.post('/verify', async (req, res) => {
|
||||
try {
|
||||
const { phone, code } = req.body;
|
||||
|
||||
|
||||
if (!phone || !code) {
|
||||
return res.status(400).json({ success: false, message: '手机号和验证码不能为空' });
|
||||
}
|
||||
|
||||
|
||||
const storedData = smsCodeStore.get(phone);
|
||||
|
||||
|
||||
if (!storedData) {
|
||||
return res.status(400).json({ success: false, message: '验证码不存在或已过期' });
|
||||
}
|
||||
|
||||
|
||||
// 检查验证码是否过期(5分钟)
|
||||
if (Date.now() - storedData.timestamp > 300000) {
|
||||
smsCodeStore.delete(phone);
|
||||
return res.status(400).json({ success: false, message: '验证码已过期' });
|
||||
}
|
||||
|
||||
|
||||
// 检查尝试次数(最多3次)
|
||||
if (storedData.attempts >= 3) {
|
||||
smsCodeStore.delete(phone);
|
||||
return res.status(400).json({ success: false, message: '验证码错误次数过多,请重新获取' });
|
||||
}
|
||||
|
||||
|
||||
// 验证验证码
|
||||
if (storedData.code !== code) {
|
||||
storedData.attempts++;
|
||||
smsCodeStore.set(phone, storedData);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `验证码错误,还可尝试${3 - storedData.attempts}次`
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `验证码错误,还可尝试${3 - storedData.attempts}次`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 验证成功,删除验证码
|
||||
smsCodeStore.delete(phone);
|
||||
smsCodeStore.delete(`time_${phone}`);
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '手机号验证成功',
|
||||
@@ -275,7 +282,7 @@ router.post('/verify', async (req, res) => {
|
||||
verified: true
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('验证短信验证码错误:', error);
|
||||
res.status(500).json({ success: false, message: '验证失败' });
|
||||
@@ -290,30 +297,30 @@ router.post('/verify', async (req, res) => {
|
||||
*/
|
||||
function verifySMSCode(phone, code) {
|
||||
const storedData = smsCodeStore.get(phone);
|
||||
|
||||
|
||||
if (!storedData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 检查是否过期
|
||||
if (Date.now() - storedData.timestamp > 300000) {
|
||||
smsCodeStore.delete(phone);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 检查尝试次数
|
||||
if (storedData.attempts >= 3) {
|
||||
smsCodeStore.delete(phone);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 验证验证码
|
||||
if (storedData.code === code) {
|
||||
smsCodeStore.delete(phone);
|
||||
smsCodeStore.delete(`time_${phone}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -322,7 +329,7 @@ setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, value] of smsCodeStore.entries()) {
|
||||
if (key.startsWith('time_')) continue;
|
||||
|
||||
|
||||
if (value.timestamp && now - value.timestamp > 300000) {
|
||||
smsCodeStore.delete(key);
|
||||
smsCodeStore.delete(`time_${key}`);
|
||||
|
||||
@@ -11,183 +11,16 @@ const dayjs = require('dayjs');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Transfer:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 转账记录ID
|
||||
* user_id:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* recipient_id:
|
||||
* type: integer
|
||||
* description: 接收方用户ID
|
||||
* amount:
|
||||
* type: number
|
||||
* format: float
|
||||
* description: 转账金额
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [pending, completed, failed, cancelled]
|
||||
* description: 转账状态
|
||||
* transfer_type:
|
||||
* type: string
|
||||
* enum: [user_to_user, user_to_system, system_to_user]
|
||||
* description: 转账类型
|
||||
* voucher_image:
|
||||
* type: string
|
||||
* description: 转账凭证图片路径
|
||||
* remark:
|
||||
* type: string
|
||||
* description: 转账备注
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 更新时间
|
||||
* Pagination:
|
||||
* type: object
|
||||
* properties:
|
||||
* total:
|
||||
* type: integer
|
||||
* description: 总记录数
|
||||
* page:
|
||||
* type: integer
|
||||
* description: 当前页码
|
||||
* limit:
|
||||
* type: integer
|
||||
* description: 每页记录数
|
||||
* total_pages:
|
||||
* type: integer
|
||||
* description: 总页数
|
||||
*/
|
||||
|
||||
// 配置文件上传
|
||||
const storage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, 'uploads/')
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9)
|
||||
cb(null, 'voucher-' + uniqueSuffix + path.extname(file.originalname))
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (file.mimetype.startsWith('image/')) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('只允许上传图片文件'));
|
||||
}
|
||||
},
|
||||
limits: {
|
||||
fileSize: 5 * 1024 * 1024 // 5MB
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /transfers:
|
||||
* get:
|
||||
* summary: 获取转账列表
|
||||
* tags: [Transfers]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 转账状态过滤
|
||||
* - in: query
|
||||
* name: transfer_type
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 转账类型过滤
|
||||
* - in: query
|
||||
* name: start_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 开始日期过滤
|
||||
* - in: query
|
||||
* name: end_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 结束日期过滤
|
||||
* - in: query
|
||||
* name: search
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 搜索关键词(用户名或真实姓名)
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: sort
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 排序字段
|
||||
* - in: query
|
||||
* name: order
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [asc, desc]
|
||||
* description: 排序方向
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功获取转账列表
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* transfers:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Transfer'
|
||||
* pagination:
|
||||
* $ref: '#/components/schemas/Pagination'
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.get('/',
|
||||
authenticateToken,
|
||||
validateQuery(transferSchemas.query),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const { page, limit, status, transfer_type, start_date, end_date, search, sort, order } = req.query;
|
||||
const { page, limit, status, start_date, end_date, search, sort, order } = req.query;
|
||||
|
||||
const filters = {
|
||||
status,
|
||||
transfer_type,
|
||||
start_date,
|
||||
end_date,
|
||||
search
|
||||
@@ -216,84 +49,38 @@ router.get('/',
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /transfers/list:
|
||||
* get:
|
||||
* summary: 获取转账记录列表
|
||||
* tags: [Transfers]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 转账状态过滤
|
||||
* - in: query
|
||||
* name: transfer_type
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 转账类型过滤
|
||||
* - in: query
|
||||
* name: start_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 开始日期过滤
|
||||
* - in: query
|
||||
* name: end_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 结束日期过滤
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: sort
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 排序字段
|
||||
* - in: query
|
||||
* name: order
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [asc, desc]
|
||||
* description: 排序方向
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功获取转账记录列表
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* transfers:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Transfer'
|
||||
* pagination:
|
||||
* $ref: '#/components/schemas/Pagination'
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.get('/history',authenticateToken,async (req, res, next) => {
|
||||
try {
|
||||
const { page, limit, start_date, end_date, search, sort, order } = req.query;
|
||||
|
||||
const filters = {
|
||||
start_date,
|
||||
end_date,
|
||||
search
|
||||
};
|
||||
|
||||
// 非管理员只能查看自己相关的转账
|
||||
if (req.user.role !== 'admin') {
|
||||
filters.user_id = req.user.id;
|
||||
}
|
||||
|
||||
const result = await transferService.getTransfersHistory(filters, { page, limit, sort, order });
|
||||
|
||||
logger.info('Transfer list requested', {
|
||||
userId: req.user.id,
|
||||
filters,
|
||||
resultCount: result.transfers.length
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/list',
|
||||
authenticateToken,
|
||||
validateQuery(transferSchemas.query),
|
||||
@@ -331,49 +118,7 @@ router.get('/list',
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /transfers/public-account:
|
||||
* get:
|
||||
* summary: 获取公户信息
|
||||
* tags: [Transfers]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功获取公户信息
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 公户ID
|
||||
* username:
|
||||
* type: string
|
||||
* description: 公户用户名
|
||||
* example: public_account
|
||||
* real_name:
|
||||
* type: string
|
||||
* description: 公户名称
|
||||
* balance:
|
||||
* type: number
|
||||
* format: float
|
||||
* description: 公户余额
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 404:
|
||||
* description: 公户不存在
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
|
||||
router.get('/public-account', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const db = getDB();
|
||||
@@ -394,66 +139,7 @@ router.get('/public-account', authenticateToken, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /transfers/create:
|
||||
* post:
|
||||
* summary: 创建转账记录
|
||||
* tags: [Transfers]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - to_user_id
|
||||
* - amount
|
||||
* - transfer_type
|
||||
* properties:
|
||||
* to_user_id:
|
||||
* type: integer
|
||||
* description: 接收方用户ID
|
||||
* amount:
|
||||
* type: number
|
||||
* format: float
|
||||
* description: 转账金额
|
||||
* transfer_type:
|
||||
* type: string
|
||||
* enum: [user_to_user, user_to_system, system_to_user]
|
||||
* description: 转账类型
|
||||
* remark:
|
||||
* type: string
|
||||
* description: 转账备注
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 转账记录创建成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* message:
|
||||
* type: string
|
||||
* example: 转账记录创建成功,等待确认
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* transfer_id:
|
||||
* type: integer
|
||||
* description: 转账记录ID
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
|
||||
router.post('/create',
|
||||
authenticateToken,
|
||||
validate(transferSchemas.create),
|
||||
@@ -478,72 +164,7 @@ router.post('/create',
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /transfers/admin/create:
|
||||
* post:
|
||||
* summary: 管理员创建转账记录
|
||||
* tags: [Transfers]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - from_user_id
|
||||
* - to_user_id
|
||||
* - amount
|
||||
* - transfer_type
|
||||
* properties:
|
||||
* from_user_id:
|
||||
* type: integer
|
||||
* description: 发送方用户ID
|
||||
* to_user_id:
|
||||
* type: integer
|
||||
* description: 接收方用户ID
|
||||
* amount:
|
||||
* type: number
|
||||
* format: float
|
||||
* description: 转账金额
|
||||
* transfer_type:
|
||||
* type: string
|
||||
* enum: [user_to_user, user_to_system, system_to_user]
|
||||
* description: 转账类型
|
||||
* description:
|
||||
* type: string
|
||||
* description: 转账描述
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 转账记录创建成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* message:
|
||||
* type: string
|
||||
* example: 转账记录创建成功
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* transfer_id:
|
||||
* type: integer
|
||||
* description: 转账记录ID
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
|
||||
router.post('/admin/create',
|
||||
authenticateToken,
|
||||
async (req, res, next) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ const { initializeBuckets } = require('../config/minio');
|
||||
const router = express.Router();
|
||||
|
||||
// 初始化MinIO存储桶
|
||||
initializeBuckets().catch(console.error);
|
||||
// initializeBuckets().catch(console.error);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
|
||||
932
routes/users.js
932
routes/users.js
File diff suppressed because it is too large
Load Diff
@@ -6,18 +6,66 @@ const fs = require('fs');
|
||||
|
||||
class AlipayService {
|
||||
constructor() {
|
||||
// 读取密钥文件
|
||||
const privateKeyPath = path.join(__dirname, '../certs/alipay-private-key.pem');
|
||||
const publicKeyPath = path.join(__dirname, '../certs/alipay-public-key.pem');
|
||||
this.privateKey = null;
|
||||
this.alipayPublicKey = null;
|
||||
this.alipaySdk = null;
|
||||
this.isInitialized = false;
|
||||
|
||||
const privateKey = fs.readFileSync(privateKeyPath, 'utf8');
|
||||
const alipayPublicKey = fs.readFileSync(publicKeyPath, 'utf8');
|
||||
this.initializeAlipay();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化支付宝服务
|
||||
*/
|
||||
initializeAlipay() {
|
||||
try {
|
||||
// 读取密钥文件
|
||||
const privateKeyPath = this.resolveCertPath('../certs/alipay-private-key.pem');
|
||||
const publicKeyPath = this.resolveCertPath('../certs/alipay-public-key.pem');
|
||||
|
||||
console.log('支付宝私钥路径:', privateKeyPath);
|
||||
console.log('支付宝公钥路径:', publicKeyPath);
|
||||
|
||||
// 验证文件有效性
|
||||
if (!this.isValidFile(privateKeyPath)) {
|
||||
throw new Error(`支付宝私钥文件无效或不存在: ${privateKeyPath}`);
|
||||
}
|
||||
|
||||
if (!this.isValidFile(publicKeyPath)) {
|
||||
throw new Error(`支付宝公钥文件无效或不存在: ${publicKeyPath}`);
|
||||
}
|
||||
|
||||
console.log('尝试加载支付宝私钥文件:', privateKeyPath);
|
||||
this.privateKey = fs.readFileSync(privateKeyPath, 'utf8');
|
||||
console.log('支付宝私钥加载成功');
|
||||
|
||||
console.log('尝试加载支付宝公钥文件:', publicKeyPath);
|
||||
this.alipayPublicKey = fs.readFileSync(publicKeyPath, 'utf8');
|
||||
console.log('支付宝公钥加载成功');
|
||||
|
||||
this.initializeSDK();
|
||||
|
||||
} catch (error) {
|
||||
console.error('支付宝服务初始化失败:', error.message);
|
||||
console.error('支付宝功能将不可用');
|
||||
// 不抛出错误,允许服务继续运行
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化支付宝SDK
|
||||
*/
|
||||
initializeSDK() {
|
||||
if (!this.privateKey || !this.alipayPublicKey) {
|
||||
console.warn('支付宝密钥未加载,跳过SDK初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
// 支付宝配置
|
||||
this.config = {
|
||||
appId: process.env.ALIPAY_APP_ID || '2021001161683774', // 替换为实际的应用ID
|
||||
privateKey: privateKey, // 从文件读取的应用私钥
|
||||
alipayPublicKey: alipayPublicKey, // 从文件读取的支付宝公钥
|
||||
privateKey: this.privateKey, // 从文件读取的应用私钥
|
||||
alipayPublicKey: this.alipayPublicKey, // 从文件读取的支付宝公钥
|
||||
gateway: 'https://openapi.alipay.com/gateway.do', // 支付宝网关地址
|
||||
signType: 'RSA2',
|
||||
charset: 'utf-8',
|
||||
@@ -34,6 +82,40 @@ class AlipayService {
|
||||
signType: this.config.signType,
|
||||
timeout: this.config.timeout
|
||||
});
|
||||
|
||||
this.isInitialized = true;
|
||||
console.log('支付宝SDK初始化成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析证书文件路径
|
||||
* @param {string} relativePath - 相对路径
|
||||
* @returns {string} 绝对路径
|
||||
*/
|
||||
resolveCertPath(relativePath) {
|
||||
return path.resolve(__dirname, relativePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文件是否有效
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {boolean} 是否为有效文件
|
||||
*/
|
||||
isValidFile(filePath) {
|
||||
try {
|
||||
const stats = fs.statSync(filePath);
|
||||
return stats.isFile();
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查支付宝服务是否已初始化
|
||||
* @returns {boolean} 是否已初始化
|
||||
*/
|
||||
isServiceAvailable() {
|
||||
return this.isInitialized && this.alipaySdk !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,6 +128,11 @@ class AlipayService {
|
||||
* @returns {Promise<Object>} 支付结果
|
||||
*/
|
||||
async createRegistrationPayOrder({ userId, username, phone, clientIp }) {
|
||||
// 检查服务是否可用
|
||||
if (!this.isServiceAvailable()) {
|
||||
throw new Error('支付宝服务未初始化或不可用');
|
||||
}
|
||||
|
||||
try {
|
||||
const db = getDB();
|
||||
|
||||
@@ -111,6 +198,11 @@ class AlipayService {
|
||||
* @returns {Promise<Object>} 查询结果
|
||||
*/
|
||||
async queryPaymentStatus(outTradeNo) {
|
||||
// 检查服务是否可用
|
||||
if (!this.isServiceAvailable()) {
|
||||
throw new Error('支付宝服务未初始化或不可用');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.alipaySdk.exec('alipay.trade.query', {
|
||||
bizContent: {
|
||||
@@ -202,6 +294,12 @@ class AlipayService {
|
||||
* @returns {boolean} 验证结果
|
||||
*/
|
||||
verifyNotifySign(params) {
|
||||
// 检查服务是否可用
|
||||
if (!this.isServiceAvailable()) {
|
||||
console.error('支付宝服务未初始化,无法验证签名');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return this.alipaySdk.checkNotifySign(params);
|
||||
} catch (error) {
|
||||
|
||||
@@ -26,7 +26,17 @@ class MatchingService {
|
||||
|
||||
// 检查用户审核状态、必要信息和余额
|
||||
const [userResult] = await db.execute(
|
||||
'SELECT audit_status, balance, wechat_qr, alipay_qr, unionpay_qr, bank_card, business_license, id_card_front, id_card_back FROM users WHERE id = ?',
|
||||
`SELECT audit_status,
|
||||
balance,
|
||||
wechat_qr,
|
||||
alipay_qr,
|
||||
unionpay_qr,
|
||||
bank_card,
|
||||
business_license,
|
||||
id_card_front,
|
||||
id_card_back
|
||||
FROM users
|
||||
WHERE id = ?`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
@@ -79,8 +89,8 @@ class MatchingService {
|
||||
maxCycles = 1;
|
||||
} else if (matchingType === 'large') {
|
||||
// 大额匹配:用户自定义金额(最高5万)
|
||||
if (!customAmount || customAmount < 5000 || customAmount > 50000) {
|
||||
throw new Error('大额匹配金额必须在5000-50000之间');
|
||||
if (!customAmount || customAmount < 3000 || customAmount > 50000) {
|
||||
throw new Error('大额匹配金额必须在3000-50000之间');
|
||||
}
|
||||
totalAmount = customAmount;
|
||||
maxCycles = 1;
|
||||
@@ -688,19 +698,41 @@ class MatchingService {
|
||||
const maxTransfers = 10;
|
||||
|
||||
try {
|
||||
// 获取负余额用户
|
||||
// 首先获取当前用户的城市、省份和区域信息
|
||||
const [currentUserResult] = await db.execute(
|
||||
`SELECT city, province, district_id FROM users WHERE id = ?`,
|
||||
[excludeUserId]
|
||||
);
|
||||
|
||||
const currentUserCity = currentUserResult[0]?.city;
|
||||
const currentUserProvince = currentUserResult[0]?.province;
|
||||
const currentUserDistrictId = currentUserResult[0]?.district_id;
|
||||
|
||||
// 获取负余额用户,按区县、城市、省份优先级排序
|
||||
let [userBalanceResult] = await db.execute(
|
||||
`SELECT
|
||||
u.id as user_id,
|
||||
u.balance as current_balance
|
||||
u.balance as current_balance,
|
||||
u.city,
|
||||
u.province,
|
||||
u.district_id
|
||||
FROM users u
|
||||
WHERE u.is_system_account = FALSE
|
||||
AND u.is_distribute = TRUE
|
||||
AND u.id != ?
|
||||
AND u.balance < -100
|
||||
AND u.audit_status = 'approved'
|
||||
ORDER BY u.balance ASC`,
|
||||
[excludeUserId]
|
||||
AND u.user_type != 'directly_operated'
|
||||
AND u.payment_status = 'paid'
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN u.city = ? AND u.district_id = ? THEN 1 -- 相同城市且相同区县排第一
|
||||
WHEN u.city = ? THEN 2 -- 相同城市但不同区县排第二
|
||||
WHEN u.province = ? THEN 3 -- 相同省份但不同城市排第三
|
||||
ELSE 4 -- 其他省份排第四
|
||||
END,
|
||||
u.balance ASC`,
|
||||
[excludeUserId, currentUserCity, currentUserDistrictId, currentUserCity, currentUserProvince]
|
||||
);
|
||||
|
||||
// 处理查询到的负余额用户
|
||||
@@ -765,7 +797,7 @@ class MatchingService {
|
||||
userBalanceResult = userBalanceResult.filter(user => user.has_active_allocations < -100);
|
||||
userBalanceResult = userBalanceResult.sort((a, b) => a.has_active_allocations - b.has_active_allocations);
|
||||
for (const user of userBalanceResult) {
|
||||
if ( maxTransfers > availableUsers.length + 1) {
|
||||
if (maxTransfers > availableUsers.length + 1) {
|
||||
if (minTransfers === 3 && availableUsers.length < 3) {
|
||||
availableUsers.push(user);
|
||||
}
|
||||
@@ -820,21 +852,26 @@ class MatchingService {
|
||||
if (range <= 0) {
|
||||
maxUserAllocation = minAmount;
|
||||
} else {
|
||||
// 使用更均匀的分配策略
|
||||
const randomFactor = Math.random(); // 使用均匀分布
|
||||
if (maxRandomAllocation > 1000) {
|
||||
// 使用更均匀的分配策略
|
||||
const randomFactor = Math.random(); // 使用均匀分布
|
||||
|
||||
// 基础分配:在整个范围内更均匀分布,减少偏向性
|
||||
const baseOffset = Math.floor(range * 0.15); // 降低到15%的基础偏移
|
||||
const adjustedRange = range - baseOffset;
|
||||
maxUserAllocation = Math.floor(randomFactor * adjustedRange) + minAmount + baseOffset;
|
||||
// 基础分配:在整个范围内更均匀分布,减少偏向性
|
||||
const baseOffset = Math.floor(range * 0.15); // 降低到15%的基础偏移
|
||||
const adjustedRange = range - baseOffset;
|
||||
maxUserAllocation = Math.floor(randomFactor * adjustedRange) + minAmount + baseOffset;
|
||||
|
||||
// 进一步减少额外增量的影响
|
||||
const bonusRange = Math.min(range * 0.1, maxRandomAllocation - maxUserAllocation); // 降低到10%
|
||||
if (bonusRange > 0 && Math.random() > 0.7) { // 30%概率获得额外增量,进一步降低
|
||||
const bonus = Math.floor(Math.random() * bonusRange * 0.3); // 使用30%的bonus范围
|
||||
maxUserAllocation += bonus;
|
||||
// 进一步减少额外增量的影响
|
||||
const bonusRange = Math.min(range * 0.1, maxRandomAllocation - maxUserAllocation); // 降低到10%
|
||||
if (bonusRange > 0 && Math.random() > 0.7) { // 30%概率获得额外增量,进一步降低
|
||||
const bonus = Math.floor(Math.random() * bonusRange * 0.3); // 使用30%的bonus范围
|
||||
maxUserAllocation += bonus;
|
||||
}
|
||||
}else{
|
||||
maxUserAllocation = maxRandomAllocation
|
||||
}
|
||||
|
||||
|
||||
// 确保不超过最大限制
|
||||
maxUserAllocation = Math.min(maxUserAllocation, maxRandomAllocation);
|
||||
}
|
||||
@@ -852,7 +889,7 @@ class MatchingService {
|
||||
totalPendingInflow: user.total_pending_inflow,
|
||||
availableForAllocation: user.available_for_allocation,
|
||||
todayOutflow: user.today_outflow,
|
||||
has_active_allocations:user.has_active_allocations
|
||||
has_active_allocations: user.has_active_allocations
|
||||
});
|
||||
remainingAmount -= maxUserAllocation;
|
||||
}
|
||||
@@ -864,7 +901,7 @@ class MatchingService {
|
||||
// 如果有剩余金额,优先检查现有非虚拟用户是否还能消化
|
||||
if (remainingAmount > 0) {
|
||||
// 筛选出非虚拟用户分配记录
|
||||
|
||||
|
||||
if (allocations.length > 0) {
|
||||
let totalAvailableCapacity = 0;
|
||||
const userCapacities = [];
|
||||
@@ -874,7 +911,7 @@ class MatchingService {
|
||||
// 获取用户当前的实际余额状态(使用has_active_allocations作为实际可分配余额)
|
||||
const maxSafeAmount = Math.abs(allocation.has_active_allocations);
|
||||
const remainingCapacity = maxSafeAmount - allocation.amount;
|
||||
|
||||
|
||||
if (remainingCapacity > 0) {
|
||||
userCapacities.push({
|
||||
allocation,
|
||||
@@ -887,7 +924,7 @@ class MatchingService {
|
||||
console.log(`现有用户剩余容量: ${totalAvailableCapacity}, 待分配金额: ${remainingAmount}`);
|
||||
|
||||
// 如果现有用户能够消化剩余金额
|
||||
if (totalAvailableCapacity >= remainingAmount && userCapacities.length > 0) {
|
||||
if (totalAvailableCapacity >= remainingAmount && userCapacities.length > 0 && allocations.length >= 3) {
|
||||
// 按平均分配给这些用户,但需要检查每个用户的分配上限
|
||||
const averageAmount = Math.floor(remainingAmount / userCapacities.length);
|
||||
let distributedAmount = 0;
|
||||
@@ -895,10 +932,10 @@ class MatchingService {
|
||||
|
||||
for (let i = 0; i < userCapacities.length; i++) {
|
||||
const { allocation, capacity } = userCapacities[i];
|
||||
|
||||
|
||||
// 计算本次可分配的金额
|
||||
let amountToAdd = 0;
|
||||
|
||||
|
||||
if (i === userCapacities.length - 1) {
|
||||
// 最后一个用户分配剩余的所有金额,但不能超过其容量
|
||||
amountToAdd = Math.min(remainingToDistribute, capacity);
|
||||
@@ -914,15 +951,15 @@ class MatchingService {
|
||||
console.log(`为用户${allocation.userId}追加分配${amountToAdd}元,总分配${allocation.amount}元,剩余容量${capacity - amountToAdd}元`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 更新实际分配的剩余金额
|
||||
remainingAmount = remainingToDistribute;
|
||||
|
||||
if (remainingAmount === 0) {
|
||||
console.log('剩余金额已全部分配给现有用户');
|
||||
} else {
|
||||
console.log(`部分剩余金额已分配给现有用户,仍有${remainingAmount}元未分配`);
|
||||
}
|
||||
remainingAmount = remainingToDistribute;
|
||||
|
||||
if (remainingAmount === 0) {
|
||||
console.log('剩余金额已全部分配给现有用户');
|
||||
} else {
|
||||
console.log(`部分剩余金额已分配给现有用户,仍有${remainingAmount}元未分配`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -931,17 +968,17 @@ class MatchingService {
|
||||
if (remainingAmount > 0) {
|
||||
// 获取已分配的用户ID列表
|
||||
const allocatedUserIds = new Set(allocations.map(a => a.userId));
|
||||
|
||||
|
||||
// 从原始用户列表中找到未分配的用户
|
||||
const unallocatedUsers = priorityUsers.filter(user => !allocatedUserIds.has(user.user_id));
|
||||
|
||||
const unallocatedUsers = userBalanceResult.filter(user => !allocatedUserIds.has(user.user_id));
|
||||
|
||||
if (unallocatedUsers.length > 0) {
|
||||
console.log(`发现${unallocatedUsers.length}个未分配的用户,剩余金额: ${remainingAmount}`);
|
||||
|
||||
|
||||
// 查找可分配金额大于剩余金额的用户
|
||||
for (const user of unallocatedUsers) {
|
||||
const maxSafeAmount = Math.abs(user.has_active_allocations);
|
||||
|
||||
|
||||
if (maxSafeAmount >= remainingAmount) {
|
||||
// 找到合适的用户,分配剩余金额
|
||||
allocations.push({
|
||||
@@ -952,7 +989,7 @@ class MatchingService {
|
||||
currentBalance: user.current_balance,
|
||||
availableForAllocation: user.has_active_allocations
|
||||
});
|
||||
|
||||
|
||||
console.log(`为未分配用户${user.user_id}分配剩余金额${remainingAmount}元`);
|
||||
remainingAmount = 0;
|
||||
break;
|
||||
@@ -1045,17 +1082,17 @@ class MatchingService {
|
||||
|
||||
// 使用更均匀的分配策略
|
||||
const amounts = [];
|
||||
|
||||
|
||||
// 首先为每笔分配最小金额
|
||||
for (let i = 0; i < transferCount; i++) {
|
||||
amounts.push(minAmount);
|
||||
}
|
||||
|
||||
|
||||
let remainingToDistribute = totalAmount - (minAmount * transferCount);
|
||||
|
||||
|
||||
// 计算平均每笔应该额外分配的金额
|
||||
const averageExtra = Math.floor(remainingToDistribute / transferCount);
|
||||
|
||||
|
||||
// 为每笔添加平均额外金额,但加入一些随机性
|
||||
for (let i = 0; i < transferCount && remainingToDistribute > 0; i++) {
|
||||
// 计算这笔最多还能增加多少(不超过maxAmount)
|
||||
@@ -1063,20 +1100,20 @@ class MatchingService {
|
||||
maxAmount - amounts[i],
|
||||
remainingToDistribute
|
||||
);
|
||||
|
||||
|
||||
if (maxPossibleIncrease > 0) {
|
||||
// 在平均值附近随机分配,但控制在更小的范围内以保证更均匀
|
||||
const baseIncrease = Math.min(averageExtra, maxPossibleIncrease);
|
||||
const randomVariation = Math.floor(baseIncrease * 0.15); // 减少到15%的随机变化
|
||||
const minIncrease = Math.max(0, baseIncrease - randomVariation);
|
||||
const maxIncrease = Math.min(maxPossibleIncrease, baseIncrease + randomVariation);
|
||||
|
||||
|
||||
const increase = Math.floor(Math.random() * (maxIncrease - minIncrease + 1)) + minIncrease;
|
||||
amounts[i] += increase;
|
||||
remainingToDistribute -= increase;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果还有剩余金额,尽量均匀分配给还能接受的笔数
|
||||
while (remainingToDistribute > 0) {
|
||||
const availableIndices = [];
|
||||
@@ -1085,45 +1122,45 @@ class MatchingService {
|
||||
availableIndices.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (availableIndices.length === 0) {
|
||||
break; // 无法继续分配
|
||||
}
|
||||
|
||||
|
||||
// 计算每个可用位置应该分配多少
|
||||
const perIndexAmount = Math.floor(remainingToDistribute / availableIndices.length);
|
||||
const remainder = remainingToDistribute % availableIndices.length;
|
||||
|
||||
|
||||
// 为每个可用位置分配相等的金额
|
||||
for (let i = 0; i < availableIndices.length && remainingToDistribute > 0; i++) {
|
||||
const index = availableIndices[i];
|
||||
const maxIncrease = Math.min(maxAmount - amounts[index], remainingToDistribute);
|
||||
|
||||
|
||||
if (maxIncrease > 0) {
|
||||
// 基础分配金额
|
||||
let increase = Math.min(perIndexAmount, maxIncrease);
|
||||
|
||||
|
||||
// 如果是前几个位置,额外分配余数
|
||||
if (i < remainder) {
|
||||
increase = Math.min(increase + 1, maxIncrease);
|
||||
}
|
||||
|
||||
|
||||
amounts[index] += increase;
|
||||
remainingToDistribute -= increase;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果所有位置都已达到最大值,退出循环
|
||||
if (perIndexAmount === 0 && remainder === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果还有剩余金额无法分配,返回空数组表示失败
|
||||
if (remainingToDistribute > 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
return amounts;
|
||||
}
|
||||
|
||||
|
||||
@@ -238,16 +238,16 @@ class TransferService {
|
||||
}
|
||||
|
||||
// 获取转账列表
|
||||
async getTransfers(filters = {}, pagination = {}) {
|
||||
async getTransfers(filters = {}, pagination = {},user_type = 'user_to_user') {
|
||||
const db = getDB();
|
||||
const {page = 1, limit = 10, sort = 'created_at', order = 'desc'} = pagination;
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
const limitNum = parseInt(limit, 10) || 10;
|
||||
const offset = (pageNum - 1) * limitNum;
|
||||
|
||||
let whereClause = 'WHERE 1=1';
|
||||
let whereClause = 'WHERE 1=1 ';
|
||||
const params = [];
|
||||
|
||||
whereClause += `AND transfer_type='${user_type}'`;
|
||||
// 构建查询条件
|
||||
if (filters.user_id) {
|
||||
whereClause += ' AND (from_user_id = ? OR to_user_id = ?)';
|
||||
@@ -284,7 +284,98 @@ class TransferService {
|
||||
const validSortFields = ['id', 'amount', 'created_at', 'updated_at', 'status'];
|
||||
const sortField = validSortFields.includes(sort) ? sort : 'created_at';
|
||||
const sortOrder = order && order.toLowerCase() === 'asc' ? 'ASC' : 'DESC';
|
||||
|
||||
|
||||
const orderClause = `ORDER BY t.${sortField} ${sortOrder}`;
|
||||
|
||||
try {
|
||||
// 获取总数
|
||||
const [countResult] = await db.execute(
|
||||
`SELECT COUNT(*) as total
|
||||
FROM transfers t
|
||||
LEFT JOIN users fu ON t.from_user_id = fu.id
|
||||
LEFT JOIN users tu ON t.to_user_id = tu.id
|
||||
${whereClause}`,
|
||||
params
|
||||
);
|
||||
const total = countResult[0].total;
|
||||
|
||||
// 获取数据
|
||||
const [transfers] = await db.execute(
|
||||
`SELECT t.*,
|
||||
fu.username as from_username,
|
||||
fu.real_name as from_real_name,
|
||||
tu.username as to_username,
|
||||
tu.real_name as to_real_name
|
||||
FROM transfers t
|
||||
LEFT JOIN users fu ON t.from_user_id = fu.id
|
||||
LEFT JOIN users tu ON t.to_user_id = tu.id
|
||||
${whereClause} ${orderClause}
|
||||
LIMIT ${limitNum}
|
||||
OFFSET ${offset}`,
|
||||
params
|
||||
);
|
||||
|
||||
return {
|
||||
transfers,
|
||||
pagination: {
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
total,
|
||||
pages: Math.ceil(total / limitNum)
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to get transfers', {error: error.message, filters});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async getTransfersHistory(filters = {}, pagination = {},user_type = 'user_to_user') {
|
||||
const db = getDB();
|
||||
const {page = 1, limit = 10, sort = 'created_at', order = 'desc'} = pagination;
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
const limitNum = parseInt(limit, 10) || 10;
|
||||
const offset = (pageNum - 1) * limitNum;
|
||||
|
||||
let whereClause = 'WHERE 1=1 ';
|
||||
const params = [];
|
||||
whereClause += `AND transfer_type != '${user_type}'`;
|
||||
// 构建查询条件
|
||||
if (filters.user_id) {
|
||||
whereClause += ' AND (from_user_id = ? OR to_user_id = ?)';
|
||||
params.push(filters.user_id, filters.user_id);
|
||||
}
|
||||
|
||||
if (filters.status) {
|
||||
whereClause += ' AND status = ?';
|
||||
params.push(filters.status);
|
||||
}
|
||||
|
||||
if (filters.transfer_type) {
|
||||
whereClause += ' AND transfer_type = ?';
|
||||
params.push(filters.transfer_type);
|
||||
}
|
||||
|
||||
if (filters.start_date) {
|
||||
whereClause += ' AND created_at >= ?';
|
||||
params.push(filters.start_date);
|
||||
}
|
||||
|
||||
if (filters.end_date) {
|
||||
whereClause += ' AND created_at <= ?';
|
||||
params.push(filters.end_date);
|
||||
}
|
||||
|
||||
if (filters.search) {
|
||||
whereClause += ' AND (fu.username LIKE ? OR fu.real_name LIKE ? OR tu.username LIKE ? OR tu.real_name LIKE ?)';
|
||||
const searchPattern = `%${filters.search}%`;
|
||||
params.push(searchPattern, searchPattern, searchPattern, searchPattern);
|
||||
}
|
||||
|
||||
// 构建排序子句
|
||||
const validSortFields = ['id', 'amount', 'created_at', 'updated_at', 'status'];
|
||||
const sortField = validSortFields.includes(sort) ? sort : 'created_at';
|
||||
const sortOrder = order && order.toLowerCase() === 'asc' ? 'ASC' : 'DESC';
|
||||
|
||||
const orderClause = `ORDER BY t.${sortField} ${sortOrder}`;
|
||||
|
||||
try {
|
||||
@@ -357,53 +448,6 @@ class TransferService {
|
||||
return transfers[0] || null;
|
||||
}
|
||||
|
||||
// 更新用户余额(新的余额系统)
|
||||
async updateUserBalance(transfer) {
|
||||
const {from_user_id, to_user_id, amount, transfer_type} = transfer;
|
||||
|
||||
const db = getDB();
|
||||
|
||||
// 扣除发送方余额(使用行锁防止并发问题)
|
||||
if (transfer_type === TRANSFER_TYPES.USER_TO_USER || transfer_type === TRANSFER_TYPES.USER_TO_SYSTEM) {
|
||||
if (from_user_id) {
|
||||
await db.execute(
|
||||
'UPDATE users SET balance = balance - ? WHERE id = ?',
|
||||
[amount, from_user_id]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 增加接收方余额
|
||||
if (transfer_type === TRANSFER_TYPES.SYSTEM_TO_USER || transfer_type === TRANSFER_TYPES.USER_TO_USER) {
|
||||
await db.execute(
|
||||
'UPDATE users SET balance = balance + ? WHERE id = ?',
|
||||
[amount, to_user_id]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 使用指定连接更新用户余额(用于事务处理)
|
||||
async updateUserBalanceWithConnection(transfer, connection) {
|
||||
const {from_user_id, to_user_id, amount, transfer_type} = transfer;
|
||||
|
||||
// 扣除发送方余额(使用行锁防止并发问题)
|
||||
if (transfer_type === TRANSFER_TYPES.USER_TO_USER || transfer_type === TRANSFER_TYPES.USER_TO_SYSTEM) {
|
||||
if (from_user_id) {
|
||||
await connection.execute(
|
||||
'UPDATE users SET balance = balance - ? WHERE id = ?',
|
||||
[amount, from_user_id]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 增加接收方余额
|
||||
if (transfer_type === TRANSFER_TYPES.SYSTEM_TO_USER || transfer_type === TRANSFER_TYPES.USER_TO_USER) {
|
||||
await connection.execute(
|
||||
'UPDATE users SET balance = balance + ? WHERE id = ?',
|
||||
[amount, to_user_id]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 用户确认收到转账
|
||||
async confirmReceived(transferId, userId) {
|
||||
|
||||
@@ -19,26 +19,69 @@ class WechatPayService {
|
||||
// 初始化API v3配置
|
||||
async initializeV3() {
|
||||
try {
|
||||
// 检查配置是否存在
|
||||
if (!this.config.keyPath || !this.config.certPath) {
|
||||
console.warn('微信支付证书路径未配置,跳过API v3初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载私钥
|
||||
const keyPath = path.resolve(__dirname, '..', this.config.keyPath.replace(/^\.\//, ''));
|
||||
if (fs.existsSync(keyPath)) {
|
||||
const keyPath = this.resolveCertPath(this.config.keyPath);
|
||||
console.log('尝试加载私钥文件:', keyPath);
|
||||
|
||||
if (this.isValidFile(keyPath)) {
|
||||
this.privateKey = fs.readFileSync(keyPath, 'utf8');
|
||||
console.log('API v3 私钥加载成功');
|
||||
} else {
|
||||
console.error('私钥文件不存在:', keyPath);
|
||||
console.error('私钥文件不存在或不是有效文件:', keyPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取证书序列号
|
||||
const certPath = path.resolve(__dirname, '..', this.config.certPath.replace(/^\.\//, ''));
|
||||
if (fs.existsSync(certPath)) {
|
||||
const certPath = this.resolveCertPath(this.config.certPath);
|
||||
console.log('尝试加载证书文件:', certPath);
|
||||
|
||||
if (this.isValidFile(certPath)) {
|
||||
const cert = fs.readFileSync(certPath, 'utf8');
|
||||
this.serialNo = this.getCertificateSerialNumber(cert);
|
||||
console.log('证书序列号:', this.serialNo);
|
||||
} else {
|
||||
console.error('证书文件不存在:', certPath);
|
||||
console.error('证书文件不存在或不是有效文件:', certPath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化API v3配置失败:', error.message);
|
||||
console.error('错误详情:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 解析证书文件路径
|
||||
resolveCertPath(configPath) {
|
||||
// 如果是绝对路径,直接使用
|
||||
if (path.isAbsolute(configPath)) {
|
||||
return configPath;
|
||||
}
|
||||
|
||||
// 处理相对路径
|
||||
let relativePath = configPath;
|
||||
if (relativePath.startsWith('./')) {
|
||||
relativePath = relativePath.substring(2);
|
||||
}
|
||||
|
||||
return path.resolve(__dirname, '..', relativePath);
|
||||
}
|
||||
|
||||
// 检查是否为有效的文件(不是目录)
|
||||
isValidFile(filePath) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stats = fs.statSync(filePath);
|
||||
return stats.isFile();
|
||||
} catch (error) {
|
||||
console.error('检查文件状态失败:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user