This commit is contained in:
2025-09-10 18:10:40 +08:00
parent 8530e97ab6
commit d50290e8fe
27 changed files with 2025 additions and 3913 deletions

4
.env
View File

@@ -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/

View File

@@ -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
# 是否使用SSLtrue/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
View 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
View 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
View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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
View 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
View 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
View 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
View 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: 服务器错误
*/

View File

@@ -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设为3608agent_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;

View File

@@ -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];

View File

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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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}`);

View File

@@ -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) => {

View File

@@ -9,7 +9,7 @@ const { initializeBuckets } = require('../config/minio');
const router = express.Router();
// 初始化MinIO存储桶
initializeBuckets().catch(console.error);
// initializeBuckets().catch(console.error);
/**
* @swagger

File diff suppressed because it is too large Load Diff

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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;
}
}