diff --git a/.env b/.env index eba2d37..7209ee9 100644 --- a/.env +++ b/.env @@ -60,5 +60,5 @@ WECHAT_NOTIFY_URL=https://www.zrbjr.com/api/wechat-pay/notify # 2. 应用私钥和支付宝公钥现在从文件读取 ALIPAY_APP_ID=2021005188682022 ALIPAY_NOTIFY_URL=https://www.zrbjr.com/api/payment/alipay/notify -ALIPAY_RETURN_URL=https://www.zrbjr.com/payment/success -ALIPAY_QUIT_URL=https://www.zrbjr.com/payment/cancel \ No newline at end of file +ALIPAY_RETURN_URL=https://www.zrbjr.com/payment +ALIPAY_QUIT_URL=https://www.zrbjr.com/payment/ diff --git a/.env.example b/.env.example index f84b5d0..eba2d37 100644 --- a/.env.example +++ b/.env.example @@ -1,17 +1,19 @@ # 数据库配置 -DB_HOST=localhost -DB_USER=root -DB_PASSWORD=your_password -DB_NAME=your_database +DB_HOST=114.55.111.44 +DB_USER=maov2 +DB_PASSWORD=5fYhw8z6T62b7heS +DB_NAME=maov2 # JWT密钥 JWT_SECRET=your_jwt_secret_key # 阿里云短信服务配置 +# 请在阿里云控制台获取以下配置信息: +# 1. AccessKey ID 和 AccessKey Secret:在阿里云控制台 -> AccessKey管理中创建 +# 2. 短信签名:在阿里云短信服务控制台中申请并审核通过的签名 +# 3. 短信模板CODE:在阿里云短信服务控制台中申请并审核通过的模板CODE ALIYUN_ACCESS_KEY_ID=LTAI5tBHymRUu1vvo5tgYpaa ALIYUN_ACCESS_KEY_SECRET=lNsDZvpUVX2b3pfBQCBawOEyr3dNB9 - -# 短信模板配置 ALIYUN_SMS_SIGN_NAME=宁波炬融歆创科技 ALIYUN_SMS_TEMPLATE_CODE=SMS_324470054 @@ -20,21 +22,22 @@ NODE_ENV=development PORT=3000 # 前端地址配置 -FRONTEND_URL=http://localhost:3001 +FRONTEND_URL=https://www.zrbjr.com/frontend +# FRONTEND_URL=http://114.55.111.44:3001/frontend # MinIO 对象存储配置 # MinIO服务器地址(不包含协议) -MINIO_ENDPOINT=localhost +MINIO_ENDPOINT=114.55.111.44 # MinIO服务器端口 MINIO_PORT=9000 # 是否使用SSL(true/false) MINIO_USE_SSL=false # MinIO访问密钥 -MINIO_ACCESS_KEY=minioadmin +MINIO_ACCESS_KEY=minio # MinIO秘密密钥 -MINIO_SECRET_KEY=minioadmin +MINIO_SECRET_KEY=CNy6fMCfyfeaEjbE # MinIO公开访问地址(用于生成文件URL) -MINIO_PUBLIC_URL=http://minio.zrbjr.com:9000 +MINIO_PUBLIC_URL=https://minio.zrbjr.com # MinIO存储桶配置 MINIO_BUCKET_UPLOADS=jurongquan @@ -42,19 +45,20 @@ MINIO_BUCKET_AVATARS=jurongquan MINIO_BUCKET_PRODUCTS=jurongquan MINIO_BUCKET_DOCUMENTS=jurongquan -# 微信支付配置 -WECHAT_APP_ID=your_wechat_app_id -WECHAT_MCH_ID=your_wechat_mch_id -WECHAT_API_KEY=your_wechat_api_key -WECHAT_API_V3_KEY=your_wechat_api_v3_key -WECHAT_NOTIFY_URL=https://your-domain.com/api/wechat-pay/notify +#支付配置 +WECHAT_APP_ID=wx3a702dbe13fd2217 +WECHAT_MCH_ID=1726377336 +WECHAT_API_KEY=NINGBOJURONGkejiyouxiangongsi202 +WECHAT_API_V3_KEY=NINGBOJURONGkejiyouxiangongsi202 WECHAT_CERT_PATH=./cert/apiclient_cert.pem WECHAT_KEY_PATH=./cert/apiclient_key.pem +WECHAT_NOTIFY_URL=https://www.zrbjr.com/api/wechat-pay/notify -# 支付宝支付配置 -ALIPAY_APP_ID=your_alipay_app_id -ALIPAY_PRIVATE_KEY=your_alipay_private_key -ALIPAY_PUBLIC_KEY=your_alipay_public_key -ALIPAY_GATEWAY_URL=https://openapi.alipay.com/gateway.do -ALIPAY_NOTIFY_URL=https://your-domain.com/api/alipay/notify -ALIPAY_RETURN_URL=https://your-domain.com/payment/success +# 支付宝配置 +# 请在支付宝开放平台获取以下配置信息: +# 1. 应用ID:在支付宝开放平台创建应用后获得 +# 2. 应用私钥和支付宝公钥现在从文件读取 +ALIPAY_APP_ID=2021005188682022 +ALIPAY_NOTIFY_URL=https://www.zrbjr.com/api/payment/alipay/notify +ALIPAY_RETURN_URL=https://www.zrbjr.com/payment/success +ALIPAY_QUIT_URL=https://www.zrbjr.com/payment/cancel \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..04e2064 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,13 @@ + + + + + mysql.8 + true + 测试服务器1 + com.mysql.cj.jdbc.Driver + jdbc:mysql://114.55.111.44:3306/test_mao + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..56782ca --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index e105af4..94a25f7 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,6 +2,5 @@ - \ No newline at end of file diff --git a/balance_audit.js b/balance_audit.js deleted file mode 100644 index e022f9e..0000000 --- a/balance_audit.js +++ /dev/null @@ -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; \ No newline at end of file diff --git a/balance_monitor.js b/balance_monitor.js deleted file mode 100644 index 2396587..0000000 --- a/balance_monitor.js +++ /dev/null @@ -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 '); - 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; \ No newline at end of file diff --git a/batch_process_commission.js b/batch_process_commission.js deleted file mode 100644 index cd00e3b..0000000 --- a/batch_process_commission.js +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/config/database-init.js b/config/database-init.js index 7a7d115..a0481bf 100644 --- a/config/database-init.js +++ b/config/database-init.js @@ -8,29 +8,20 @@ const { initDB, getDB, dbConfig } = require('../database'); */ async function initDatabase() { try { - // 首先创建数据库(如果不存在) - const tempConnection = await mysql.createConnection({ - host: dbConfig.host, - user: dbConfig.user, - password: dbConfig.password - }); - - // 创建数据库 - await tempConnection.execute(`CREATE DATABASE IF NOT EXISTS ${dbConfig.database} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`); - await tempConnection.end(); + // 初始化数据库连接池 await initDB(); console.log('数据库连接池初始化成功'); // 创建所有表 - await createTables(); + // await createTables(); // 添加字段(处理表结构升级) - await addMissingFields(); + // await addMissingFields(); // 创建默认数据 - await createDefaultData(); + // await createDefaultData(); console.log('数据库初始化完成'); } catch (error) { @@ -39,1108 +30,9 @@ async function initDatabase() { } } -/** - * 创建所有数据库表 - */ -async function createTables() { - // 用户表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS users ( - id INT AUTO_INCREMENT PRIMARY KEY, - username VARCHAR(50) UNIQUE NOT NULL, - phone VARCHAR(20) UNIQUE, - password VARCHAR(255) NOT NULL, - role ENUM('user', 'admin') DEFAULT 'user', - avatar VARCHAR(255), - points INT DEFAULT 0, - rongdou INT DEFAULT 0, - balance DECIMAL(10,2) DEFAULT 0.00, - real_name VARCHAR(100), - id_card VARCHAR(18), - wechat_qr VARCHAR(255), - alipay_qr VARCHAR(255), - bank_card VARCHAR(30), - unionpay_qr VARCHAR(255), - business_license VARCHAR(500), - id_card_front VARCHAR(500), - id_card_back VARCHAR(500), - audit_status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending', - audit_note TEXT, - audited_by INT, - audited_at TIMESTAMP NULL, - city VARCHAR(50), - district_id INT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (audited_by) REFERENCES users(id), - FOREIGN KEY (district_id) REFERENCES zhejiang_regions(id) - ) - `); - - // 商品表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS products ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - description TEXT, - price INT NOT NULL, - points_price INT NOT NULL, - rongdou_price INT NOT NULL DEFAULT 0, - original_price INT, - stock INT DEFAULT 0, - sales INT DEFAULT 0, - rating DECIMAL(3,2) DEFAULT 5.00, - category VARCHAR(100), - image_url VARCHAR(500), - images JSON, - videos JSON, - details TEXT, - shop_name VARCHAR(255), - shop_avatar VARCHAR(500), - payment_methods JSON, - status ENUM('active', 'inactive') DEFAULT 'active', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP - ) - `); - - // 订单表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS orders ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT NOT NULL, - order_no VARCHAR(50) UNIQUE NOT NULL, - total_amount INT NOT NULL, - total_points INT NOT NULL, - total_rongdou INT NOT NULL DEFAULT 0, - status ENUM('pending', 'paid', 'shipped', 'delivered', 'cancelled', 'pre_order') DEFAULT 'pending', - address JSON, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) - ) - `); - // 创建转账记录表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS transfers ( - id INT AUTO_INCREMENT PRIMARY KEY, - from_user_id INT NULL, - to_user_id INT NOT NULL, - amount DECIMAL(10,2) NOT NULL, - transfer_type ENUM('initial', 'return', 'user_to_user', 'system_to_user', 'user_to_system') DEFAULT 'user_to_user', - status ENUM('pending', 'confirmed', 'rejected', 'received', 'not_received') DEFAULT 'pending', - voucher_url VARCHAR(500), - description TEXT, - batch_id VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (from_user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (to_user_id) REFERENCES users(id) ON DELETE CASCADE - ) - `); - // 创建转账确认表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS transfer_confirmations ( - id INT AUTO_INCREMENT PRIMARY KEY, - transfer_id INT NOT NULL, - confirmer_id INT NOT NULL, - action ENUM('confirm', 'reject') NOT NULL, - note TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (transfer_id) REFERENCES transfers(id) ON DELETE CASCADE, - FOREIGN KEY (confirmer_id) REFERENCES users(id) ON DELETE CASCADE - ) - `); - - // 订单商品表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS order_items ( - id INT AUTO_INCREMENT PRIMARY KEY, - order_id INT NOT NULL, - product_id INT NOT NULL, - spec_combination_id INT NULL COMMENT '规格组合ID', - quantity INT NOT NULL, - price INT NOT NULL, - points INT NOT NULL, - points_price INT NOT NULL DEFAULT 0, - rongdou INT DEFAULT 0 COMMENT '融豆价格', - rongdou_price INT NOT NULL DEFAULT 0, - spec_info JSON COMMENT '规格信息快照', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (order_id) REFERENCES orders(id), - FOREIGN KEY (product_id) REFERENCES products(id), - FOREIGN KEY (spec_combination_id) REFERENCES product_spec_combinations(id) ON DELETE SET NULL - ) - `); - - // 积分记录表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS points_history ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT NOT NULL, - type ENUM('earn', 'spend') NOT NULL, - amount INT NOT NULL, - description VARCHAR(255), - order_id INT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (order_id) REFERENCES orders(id) - ) - `); - - // 融豆记录表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS rongdou_history ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT NOT NULL, - type ENUM('earn', 'spend') NOT NULL, - amount INT NOT NULL, - description VARCHAR(255), - order_id INT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (order_id) REFERENCES orders(id) - ) - `); - - // 管理员操作日志表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS admin_operation_logs ( - id INT AUTO_INCREMENT PRIMARY KEY, - admin_id INT NOT NULL, - operation_type VARCHAR(50) NOT NULL, - target_type VARCHAR(50) NOT NULL, - target_id INT NOT NULL, - description TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (admin_id) REFERENCES users(id) ON DELETE CASCADE - ) - `); - - // 商品评价表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS product_reviews ( - id INT AUTO_INCREMENT PRIMARY KEY, - product_id INT NOT NULL, - user_id INT NOT NULL, - order_id INT NOT NULL, - rating INT NOT NULL, - comment TEXT, - images JSON, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (product_id) REFERENCES products(id), - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (order_id) REFERENCES orders(id) - ) - `); - - // 规格名称表(规格维度) - await getDB().execute(` - CREATE TABLE IF NOT EXISTS spec_names ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL COMMENT '规格名称,如:颜色、尺寸、材质', - display_name VARCHAR(100) NOT NULL COMMENT '显示名称', - sort_order INT DEFAULT 0 COMMENT '排序', - status ENUM('active', 'inactive') DEFAULT 'active', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - UNIQUE KEY unique_name (name) - ) - `); - - // 规格值表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS spec_values ( - id INT AUTO_INCREMENT PRIMARY KEY, - spec_name_id INT NOT NULL COMMENT '规格名称ID', - value VARCHAR(100) NOT NULL COMMENT '规格值,如:红色、XL、棉质', - display_value VARCHAR(100) NOT NULL COMMENT '显示值', - color_code VARCHAR(20) COMMENT '颜色代码(仅颜色规格使用)', - image_url VARCHAR(500) COMMENT '规格图片', - sort_order INT DEFAULT 0 COMMENT '排序', - status ENUM('active', 'inactive') DEFAULT 'active', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (spec_name_id) REFERENCES spec_names(id) ON DELETE CASCADE, - UNIQUE KEY unique_spec_value (spec_name_id, value) - ) - `); - - // 商品规格组合表(笛卡尔积结果) - await getDB().execute(` - CREATE TABLE IF NOT EXISTS product_spec_combinations ( - id INT AUTO_INCREMENT PRIMARY KEY, - product_id INT NOT NULL COMMENT '商品ID', - combination_key VARCHAR(255) NOT NULL COMMENT '组合键,如:color_1-size_2-material_3', - spec_values JSON NOT NULL COMMENT '规格值组合,存储spec_value_id数组', - price_adjustment INT DEFAULT 0 COMMENT '价格调整', - points_adjustment INT DEFAULT 0 COMMENT '积分调整', - rongdou_adjustment INT DEFAULT 0 COMMENT '融豆调整', - stock INT DEFAULT 0 COMMENT '库存', - sku_code VARCHAR(100) COMMENT 'SKU编码', - barcode VARCHAR(100) COMMENT '条形码', - weight DECIMAL(8,3) COMMENT '重量(kg)', - volume DECIMAL(10,3) COMMENT '体积(cm³)', - status ENUM('active', 'inactive') DEFAULT 'active', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE, - UNIQUE KEY unique_product_combination (product_id, combination_key), - INDEX idx_product_status (product_id, status), - INDEX idx_sku_code (sku_code) - ) - `); - - // 商品规格关联表(定义商品使用哪些规格维度) - await getDB().execute(` - CREATE TABLE IF NOT EXISTS product_spec_names ( - id INT AUTO_INCREMENT PRIMARY KEY, - product_id INT NOT NULL COMMENT '商品ID', - spec_name_id INT NOT NULL COMMENT '规格名称ID', - is_required BOOLEAN DEFAULT TRUE COMMENT '是否必选规格', - sort_order INT DEFAULT 0 COMMENT '排序', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE, - FOREIGN KEY (spec_name_id) REFERENCES spec_names(id) ON DELETE CASCADE, - UNIQUE KEY unique_product_spec_name (product_id, spec_name_id) - ) - `); - - // 商品属性表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS product_attributes ( - id INT AUTO_INCREMENT PRIMARY KEY, - product_id INT NOT NULL, - attribute_key VARCHAR(100) NOT NULL, - attribute_value TEXT NOT NULL, - sort_order INT DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE - ) - `); - - // 商品收藏表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS product_favorites ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT NOT NULL, - product_id INT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE, - UNIQUE KEY unique_user_product (user_id, product_id) - ) - `); - - // 购物车表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS cart_items ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT NOT NULL, - product_id INT NOT NULL, - quantity INT NOT NULL DEFAULT 1, - spec_combination_id INT NULL COMMENT '规格组合ID', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE, - FOREIGN KEY (spec_combination_id) REFERENCES product_spec_combinations(id) ON DELETE CASCADE, - UNIQUE KEY unique_user_product_spec (user_id, product_id, spec_combination_id) - ) - `); - - // 用户收货地址表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS user_addresses ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT NOT NULL, - recipient_name VARCHAR(100) NOT NULL, - phone VARCHAR(20) NOT NULL, - province_code VARCHAR(20), - province_name VARCHAR(50) NOT NULL, - city_code VARCHAR(20), - city_name VARCHAR(50) NOT NULL, - district_code VARCHAR(20), - district_name VARCHAR(50) NOT NULL, - detailed_address TEXT NOT NULL, - postal_code VARCHAR(10), - label_id INT, - is_default BOOLEAN DEFAULT FALSE, - deleted_at TIMESTAMP NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (label_id) REFERENCES address_labels(id) ON DELETE SET NULL - ) - `); - - // 地址标签表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS address_labels ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(50) NOT NULL, - color VARCHAR(20) DEFAULT '#1890ff', - user_id INT NULL COMMENT '用户ID,NULL表示系统标签', - is_system BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - ) - `); - - // 全国省市区表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS china_regions ( - id INT AUTO_INCREMENT PRIMARY KEY, - code VARCHAR(20) NOT NULL UNIQUE, - name VARCHAR(100) NOT NULL, - parent_code VARCHAR(20), - level TINYINT NOT NULL COMMENT '1:省 2:市 3:区', - sort_order INT DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - INDEX idx_parent_code (parent_code), - INDEX idx_level (level) - ) - `); - - // 匹配订单表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS matching_orders ( - id INT AUTO_INCREMENT PRIMARY KEY, - initiator_id INT NOT NULL, - amount DECIMAL(10,2) NOT NULL, - status ENUM('pending', 'matching', 'completed', 'cancelled') DEFAULT 'pending', - cycle_count INT DEFAULT 0, - max_cycles INT DEFAULT 3, - matching_type ENUM('small', 'large', 'system_reverse') DEFAULT 'small', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (initiator_id) REFERENCES users(id) ON DELETE CASCADE - ) - `); - - // 匹配订单分配表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS order_allocations ( - id INT AUTO_INCREMENT PRIMARY KEY, - matching_order_id INT NOT NULL, - from_user_id INT NOT NULL, - to_user_id INT NOT NULL, - amount DECIMAL(10,2) NOT NULL, - cycle_number INT NOT NULL, - status ENUM('pending', 'confirmed', 'rejected', 'completed') DEFAULT 'pending', - transfer_id INT, - outbound_date DATE, - return_date DATE, - can_return_after TIMESTAMP, - confirmed_at TIMESTAMP NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (matching_order_id) REFERENCES matching_orders(id) ON DELETE CASCADE, - FOREIGN KEY (from_user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (to_user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (transfer_id) REFERENCES transfers(id) ON DELETE SET NULL - ) - `); - - // 用户匹配池表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS user_matching_pool ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT NOT NULL, - available_amount DECIMAL(10,2) DEFAULT 0.00, - is_active BOOLEAN DEFAULT TRUE, - last_matched_at TIMESTAMP NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - UNIQUE KEY unique_user (user_id) - ) - `); - - // 匹配记录表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS matching_records ( - id INT AUTO_INCREMENT PRIMARY KEY, - matching_order_id INT NOT NULL, - user_id INT NOT NULL, - action ENUM('join', 'confirm', 'reject', 'complete') NOT NULL, - amount DECIMAL(10,2), - note TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (matching_order_id) REFERENCES matching_orders(id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - ) - `); - - // 创建系统设置表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS system_settings ( - id INT AUTO_INCREMENT PRIMARY KEY, - setting_key VARCHAR(100) NOT NULL UNIQUE, - setting_value TEXT, - description VARCHAR(255), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP - ) - `); - - // 创建激活码表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS registration_codes ( - id INT AUTO_INCREMENT PRIMARY KEY, - code VARCHAR(10) UNIQUE NOT NULL, - expires_at TIMESTAMP NOT NULL, - is_used BOOLEAN DEFAULT FALSE, - used_at TIMESTAMP NULL, - created_by_admin_id INT, - used_by_user_id INT, - agent_id INT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (created_by_admin_id) REFERENCES users(id) ON DELETE SET NULL, - FOREIGN KEY (used_by_user_id) REFERENCES users(id) ON DELETE SET NULL, - FOREIGN KEY (agent_id) REFERENCES users(id) ON DELETE SET NULL - ) - `); - - // 创建浙江省区域表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS zhejiang_regions ( - id INT AUTO_INCREMENT PRIMARY KEY, - city_name VARCHAR(50) NOT NULL, - district_name VARCHAR(50) NOT NULL, - region_code VARCHAR(20) UNIQUE NOT NULL, - is_available BOOLEAN DEFAULT TRUE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE KEY unique_region (city_name, district_name) - ) - `); - - // 创建区域代理表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS regional_agents ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT NOT NULL, - region_id INT NOT NULL, - agent_code VARCHAR(20) UNIQUE NOT NULL, - status ENUM('pending', 'active', 'suspended', 'terminated') DEFAULT 'pending', - commission_rate DECIMAL(5,4) DEFAULT 0.1000, - total_earnings DECIMAL(10,2) DEFAULT 0.00, - recruited_merchants INT DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (region_id) REFERENCES zhejiang_regions(id) ON DELETE CASCADE, - UNIQUE KEY unique_agent_region (user_id, region_id) - ) - `); - - // 创建代理商户关系表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS agent_merchants ( - id INT AUTO_INCREMENT PRIMARY KEY, - agent_id INT NOT NULL, - merchant_id INT NOT NULL, - registration_code_id INT, - matching_count INT DEFAULT 0, - commission_earned DECIMAL(10,2) DEFAULT 0.00, - is_qualified BOOLEAN DEFAULT FALSE, - qualified_at TIMESTAMP NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (agent_id) REFERENCES regional_agents(id) ON DELETE CASCADE, - FOREIGN KEY (merchant_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (registration_code_id) REFERENCES registration_codes(id) ON DELETE SET NULL, - UNIQUE KEY unique_agent_merchant (agent_id, merchant_id) - ) - `); - - // 创建代理佣金记录表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS agent_commission_records ( - id INT AUTO_INCREMENT PRIMARY KEY, - agent_id INT NOT NULL, - merchant_id INT NOT NULL, - order_id INT, - commission_amount DECIMAL(10,2) NOT NULL, - commission_type ENUM('registration', 'matching') DEFAULT 'matching', - description TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (agent_id) REFERENCES regional_agents(id) ON DELETE CASCADE, - FOREIGN KEY (merchant_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (order_id) REFERENCES matching_orders(id) ON DELETE SET NULL - ) - `); - - // 通知公告表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS announcements ( - id INT AUTO_INCREMENT PRIMARY KEY, - title VARCHAR(255) NOT NULL COMMENT '公告标题', - content TEXT NOT NULL COMMENT '公告内容', - type ENUM('system', 'maintenance', 'promotion', 'warning') DEFAULT 'system' COMMENT '公告类型', - priority ENUM('low', 'medium', 'high', 'urgent') DEFAULT 'medium' COMMENT '优先级', - status ENUM('draft', 'published', 'archived') DEFAULT 'draft' COMMENT '状态', - is_pinned BOOLEAN DEFAULT FALSE COMMENT '是否置顶', - publish_time TIMESTAMP NULL COMMENT '发布时间', - expire_time TIMESTAMP NULL COMMENT '过期时间', - created_by INT NOT NULL COMMENT '创建者ID', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE, - INDEX idx_status (status), - INDEX idx_type (type), - INDEX idx_publish_time (publish_time), - INDEX idx_created_at (created_at) - ) COMMENT='通知公告表' - `); - - // 用户公告阅读状态表 - await getDB().execute(` - CREATE TABLE IF NOT EXISTS user_announcement_reads ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT NOT NULL COMMENT '用户ID', - announcement_id INT NOT NULL COMMENT '公告ID', - is_read BOOLEAN DEFAULT FALSE COMMENT '是否已读', - read_at TIMESTAMP NULL COMMENT '阅读时间', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - UNIQUE KEY unique_user_announcement (user_id, announcement_id), - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (announcement_id) REFERENCES announcements(id) ON DELETE CASCADE, - INDEX idx_user_id (user_id), - INDEX idx_announcement_id (announcement_id), - INDEX idx_is_read (is_read) - ) COMMENT='用户公告阅读状态表' - `);} - -/** - * 添加缺失的字段(处理数据库升级) - */ -async function addMissingFields() { - // 为现有的matching_orders表添加字段 - const matchingOrderFields = [ - { name: 'matching_type', sql: 'ALTER TABLE matching_orders ADD COLUMN matching_type ENUM(\'small\', \'large\') DEFAULT \'small\'' }, - { name: 'is_system_reverse', sql: 'ALTER TABLE matching_orders ADD COLUMN is_system_reverse BOOLEAN DEFAULT FALSE' } - ]; - - for (const field of matchingOrderFields) { - try { - await getDB().execute(field.sql); - } catch (error) { - if (!error.message.includes('Duplicate column name')) { - console.log(`添加${field.name}字段错误:`, error.message); - } - } - } - - // 为现有的users表添加字段 - const userFields = [ - { name: 'balance', sql: 'ALTER TABLE users ADD COLUMN balance DECIMAL(10,2) DEFAULT 0.00' }, - { name: 'is_system_account', sql: 'ALTER TABLE users ADD COLUMN is_system_account BOOLEAN DEFAULT FALSE' }, - { name: 'points', sql: 'ALTER TABLE users ADD COLUMN points INT DEFAULT 0' }, - { name: 'avatar', sql: 'ALTER TABLE users ADD COLUMN avatar VARCHAR(255)' }, - { name: 'real_name', sql: 'ALTER TABLE users ADD COLUMN real_name VARCHAR(100)' }, - { name: 'id_card', sql: 'ALTER TABLE users ADD COLUMN id_card VARCHAR(18)' }, - { name: 'wechat_qr', sql: 'ALTER TABLE users ADD COLUMN wechat_qr VARCHAR(255)' }, - { name: 'alipay_qr', sql: 'ALTER TABLE users ADD COLUMN alipay_qr VARCHAR(255)' }, - { name: 'bank_card', sql: 'ALTER TABLE users ADD COLUMN bank_card VARCHAR(30)' }, - { name: 'unionpay_qr', sql: 'ALTER TABLE users ADD COLUMN unionpay_qr VARCHAR(255)' }, - { name: 'phone', sql: 'ALTER TABLE users ADD COLUMN phone VARCHAR(20) UNIQUE' }, - { name: 'completed_withdrawals', sql: 'ALTER TABLE users ADD COLUMN completed_withdrawals INT DEFAULT 0' }, - { name: 'business_license', sql: 'ALTER TABLE users ADD COLUMN business_license VARCHAR(500)' }, - { name: 'id_card_front', sql: 'ALTER TABLE users ADD COLUMN id_card_front VARCHAR(500)' }, - { name: 'id_card_back', sql: 'ALTER TABLE users ADD COLUMN id_card_back VARCHAR(500)' }, - { name: 'audit_status', sql: 'ALTER TABLE users ADD COLUMN audit_status ENUM(\'pending\', \'approved\', \'rejected\') DEFAULT \'pending\'' }, - { name: 'audit_note', sql: 'ALTER TABLE users ADD COLUMN audit_note TEXT' }, - { name: 'audited_by', sql: 'ALTER TABLE users ADD COLUMN audited_by INT' }, - { name: 'audited_at', sql: 'ALTER TABLE users ADD COLUMN audited_at TIMESTAMP NULL' }, - { name: 'city', sql: 'ALTER TABLE users ADD COLUMN city VARCHAR(50)' }, - { name: 'district_id', sql: 'ALTER TABLE users ADD COLUMN district_id INT' } - ]; - - for (const field of userFields) { - try { - await getDB().execute(field.sql); - } catch (error) { - if (!error.message.includes('Duplicate column name')) { - console.log(`添加用户表${field.name}字段错误:`, error.message); - } - } - } - - // 为现有的products表添加字段 - const productFields = [ - { name: 'points_price', sql: 'ALTER TABLE products ADD COLUMN points_price INT NOT NULL DEFAULT 0' }, - { name: 'rongdou_price', sql: 'ALTER TABLE products ADD COLUMN rongdou_price INT NOT NULL DEFAULT 0' }, - { name: 'image_url', sql: 'ALTER TABLE products ADD COLUMN image_url VARCHAR(500)' }, - { name: 'images', sql: 'ALTER TABLE products ADD COLUMN images JSON' }, - { name: 'videos', sql: 'ALTER TABLE products ADD COLUMN videos JSON' }, - { name: 'details', sql: 'ALTER TABLE products ADD COLUMN details TEXT' }, - { name: 'shop_name', sql: 'ALTER TABLE products ADD COLUMN shop_name VARCHAR(255)' }, - { name: 'shop_avatar', sql: 'ALTER TABLE products ADD COLUMN shop_avatar VARCHAR(500)' }, - { name: 'payment_methods', sql: 'ALTER TABLE products ADD COLUMN payment_methods JSON' } - ]; - - for (const field of productFields) { - try { - await getDB().execute(field.sql); - } catch (error) { - if (!error.message.includes('Duplicate column name')) { - console.log(`添加商品表${field.name}字段错误:`, error.message); - } - } - } - - // 为现有的transfers表添加字段 - const transferFields = [ - { name: 'is_overdue', sql: 'ALTER TABLE transfers ADD COLUMN is_overdue BOOLEAN DEFAULT FALSE' }, - { name: 'is_bad_debt', sql: 'ALTER TABLE transfers ADD COLUMN is_bad_debt BOOLEAN DEFAULT FALSE' }, - { name: 'confirmed_at', sql: 'ALTER TABLE transfers ADD COLUMN confirmed_at TIMESTAMP NULL' }, - { name: 'deadline_at', sql: 'ALTER TABLE transfers ADD COLUMN deadline_at TIMESTAMP NULL' }, - { name: 'admin_note', sql: 'ALTER TABLE transfers ADD COLUMN admin_note TEXT' }, - { name: 'admin_modified_at', sql: 'ALTER TABLE transfers ADD COLUMN admin_modified_at TIMESTAMP NULL' }, - { name: 'admin_modified_by', sql: 'ALTER TABLE transfers ADD COLUMN admin_modified_by INT' } - ]; - - for (const field of transferFields) { - try { - await getDB().execute(field.sql); - } catch (error) { - if (!error.message.includes('Duplicate column name')) { - console.log(`添加转账表${field.name}字段错误:`, error.message); - } - } - } - - // 修改transfers表的字段类型 - try { - await getDB().execute(` - ALTER TABLE transfers - MODIFY COLUMN status ENUM('pending', 'confirmed', 'rejected', 'received', 'not_received', 'cancelled') DEFAULT 'pending' - `); - } catch (error) { - console.log('修改transfers状态字段错误:', error.message); - } - - try { - await getDB().execute(` - ALTER TABLE transfers - MODIFY COLUMN from_user_id INT NULL - `); - } catch (error) { - console.log('修改transfers from_user_id字段错误:', error.message); - } - - try { - await getDB().execute(` - ALTER TABLE transfers - MODIFY COLUMN transfer_type ENUM('initial', 'return', 'user_to_user', 'system_to_user', 'user_to_system') DEFAULT 'user_to_user' - `); - } catch (error) { - console.log('修改transfers transfer_type字段错误:', error.message); - } - - // 为现有的order_allocations表添加字段 - const allocationFields = [ - { name: 'confirmed_at', sql: 'ALTER TABLE order_allocations ADD COLUMN confirmed_at TIMESTAMP NULL' }, - { name: 'outbound_date', sql: 'ALTER TABLE order_allocations ADD COLUMN outbound_date DATE' }, - { name: 'return_date', sql: 'ALTER TABLE order_allocations ADD COLUMN return_date DATE' }, - { name: 'can_return_after', sql: 'ALTER TABLE order_allocations ADD COLUMN can_return_after TIMESTAMP' } - ]; - - for (const field of allocationFields) { - try { - await getDB().execute(field.sql); - } catch (error) { - if (!error.message.includes('Duplicate column name')) { - console.log(`添加分配表${field.name}字段错误:`, error.message); - } - } - } - - // 为现有的regional_agents表添加字段 - const agentFields = [ - { name: 'approved_at', sql: 'ALTER TABLE regional_agents ADD COLUMN approved_at TIMESTAMP NULL' }, - { name: 'approved_by_admin_id', sql: 'ALTER TABLE regional_agents ADD COLUMN approved_by_admin_id INT' }, - // 提现相关字段 - { name: 'withdrawn_amount', sql: 'ALTER TABLE regional_agents ADD COLUMN withdrawn_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT "已提现金额"' }, - { name: 'pending_withdrawal', sql: 'ALTER TABLE regional_agents ADD COLUMN pending_withdrawal DECIMAL(10,2) DEFAULT 0.00 COMMENT "待审核提现金额"' }, - { name: 'payment_type', sql: 'ALTER TABLE regional_agents ADD COLUMN payment_type ENUM("bank", "wechat", "alipay", "unionpay") DEFAULT "bank" COMMENT "收款方式类型"' }, - { name: 'bank_name', sql: 'ALTER TABLE regional_agents ADD COLUMN bank_name VARCHAR(100) COMMENT "银行名称"' }, - { name: 'account_number', sql: 'ALTER TABLE regional_agents ADD COLUMN account_number VARCHAR(50) COMMENT "账号/银行账号"' }, - { name: 'account_holder', sql: 'ALTER TABLE regional_agents ADD COLUMN account_holder VARCHAR(100) COMMENT "持有人姓名"' }, - { name: 'qr_code_url', sql: 'ALTER TABLE regional_agents ADD COLUMN qr_code_url VARCHAR(255) COMMENT "收款码图片URL"' }, - { name: 'bank_account', sql: 'ALTER TABLE regional_agents ADD COLUMN bank_account VARCHAR(50) COMMENT "银行账号(兼容旧版本)"' } - ]; - - for (const field of agentFields) { - try { - await getDB().execute(field.sql); - } catch (error) { - if (!error.message.includes('Duplicate column name')) { - console.log(`添加代理表${field.name}字段错误:`, error.message); - } - } - } - - // 为现有的registration_codes表添加字段 - const registrationCodeFields = [ - { name: 'agent_id', sql: 'ALTER TABLE registration_codes ADD COLUMN agent_id INT NULL' } - ]; - - for (const field of registrationCodeFields) { - try { - await getDB().execute(field.sql); - } catch (error) { - if (!error.message.includes('Duplicate column name')) { - console.log(`添加激活码表${field.name}字段错误:`, error.message); - } - } - } - - // 为registration_codes表的agent_id字段添加外键约束 - try { - await getDB().execute(` - ALTER TABLE registration_codes - ADD CONSTRAINT fk_registration_codes_agent_id - FOREIGN KEY (agent_id) REFERENCES users(id) ON DELETE SET NULL - `); - } catch (error) { - if (!error.message.includes('Duplicate foreign key constraint name')) { - console.log('添加激活码表agent_id外键约束错误:', error.message); - } - } - - // 注意:MySQL不支持带WHERE条件的唯一索引 - // 区域激活代理的唯一性通过应用层验证来确保 - // 每个区域只能有一个激活状态的代理,这在代理申请、审核和启用时都会进行验证 - - // 创建代理提现记录表 - try { - await getDB().execute(` - CREATE TABLE IF NOT EXISTS agent_withdrawals ( - id INT AUTO_INCREMENT PRIMARY KEY, - agent_id INT NOT NULL, - amount DECIMAL(10,2) NOT NULL, - payment_type ENUM('bank', 'wechat', 'alipay', 'unionpay') DEFAULT 'bank' COMMENT '收款方式类型', - bank_name VARCHAR(100) COMMENT '银行名称', - account_number VARCHAR(50) COMMENT '账号/银行账号', - account_holder VARCHAR(100) COMMENT '持有人姓名', - qr_code_url VARCHAR(255) COMMENT '收款码图片URL', - status ENUM('pending', 'approved', 'rejected', 'completed') DEFAULT 'pending', - apply_note TEXT, - admin_note TEXT, - processed_by INT NULL, - processed_at TIMESTAMP NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - bank_account VARCHAR(50) COMMENT '银行账号(兼容旧版本)', - FOREIGN KEY (agent_id) REFERENCES regional_agents(id) ON DELETE CASCADE, - FOREIGN KEY (processed_by) REFERENCES users(id) ON DELETE SET NULL - ) - `); - console.log('代理提现记录表创建成功'); - } catch (error) { - if (!error.message.includes('Table') || !error.message.includes('already exists')) { - console.log('创建代理提现记录表错误:', error.message); - } - } -} - -/** - * 创建默认数据 - */ -async function createDefaultData() { - // 创建默认管理员账号 - const defaultPassword = await bcrypt.hash('admin123', 10); - - try { - await getDB().execute(` - INSERT IGNORE INTO users (username, phone, password, role) - VALUES ('admin', '13800000000', ?, 'admin') - `, [defaultPassword]); - console.log('默认管理员账号已创建: 用户名: admin, 密码: admin123'); - } catch (error) { - console.log('默认管理员账号已存在或创建失败:', error.message); - } - - // 创建多个系统公户账号 - const publicPassword = await bcrypt.hash('public123', 10); - const systemAccounts = [ - { username: 'merchant_001', phone: '13800000001', real_name: '优选商户' }, - { username: 'merchant_002', phone: '13800000002', real_name: '品质商家' }, - { username: 'merchant_003', phone: '13800000003', real_name: '信誉商户' }, - { username: 'merchant_004', phone: '13800000004', real_name: '金牌商家' }, - { username: 'merchant_005', phone: '13800000005', real_name: '钻石商户' } - ]; - - for (const account of systemAccounts) { - try { - const [result] = await getDB().execute(` - INSERT IGNORE INTO users (username, phone, password, role, real_name, is_system_account) - VALUES (?, ?, ?, 'user', ?, TRUE) - `, [account.username, account.phone, publicPassword, account.real_name]); - - if (result.affectedRows > 0) { - // 为新创建的系统账户设置初始余额 - await getDB().execute(` - UPDATE users SET balance = 0.00 WHERE id = ? - `, [result.insertId]); - console.log(`系统账户已创建: ${account.real_name} (${account.username})`); - } else { - // 确保现有系统账户的is_system_account字段正确设置 - await getDB().execute(` - UPDATE users SET is_system_account = TRUE WHERE username = ? - `, [account.username]); - } - } catch (error) { - console.log(`系统账户${account.username}已存在或创建失败:`, error.message); - } - } - - // 初始化浙江省区域数据 - // await initializeZhejiangRegions(); - - // 初始化默认地址标签 - // await initializeDefaultAddressLabels(); - - // 初始化全国省市区数据 - // await initializeChinaRegions(); -} - -/** - * 初始化浙江省区域数据 - */ -async function initializeZhejiangRegions() { - const zhejiangRegions = [ - // 杭州市 - { city: '杭州市', district: '上城区', code: 'HZ_SC' }, - { city: '杭州市', district: '拱墅区', code: 'HZ_GS' }, - { city: '杭州市', district: '西湖区', code: 'HZ_XH' }, - { city: '杭州市', district: '滨江区', code: 'HZ_BJ' }, - { city: '杭州市', district: '萧山区', code: 'HZ_XS' }, - { city: '杭州市', district: '余杭区', code: 'HZ_YH' }, - { city: '杭州市', district: '临平区', code: 'HZ_LP' }, - { city: '杭州市', district: '钱塘区', code: 'HZ_QT' }, - { city: '杭州市', district: '富阳区', code: 'HZ_FY' }, - { city: '杭州市', district: '临安区', code: 'HZ_LA' }, - { city: '杭州市', district: '桐庐县', code: 'HZ_TL' }, - { city: '杭州市', district: '淳安县', code: 'HZ_CA' }, - { city: '杭州市', district: '建德市', code: 'HZ_JD' }, - // 宁波市 - { city: '宁波市', district: '海曙区', code: 'NB_HS' }, - { city: '宁波市', district: '江北区', code: 'NB_JB' }, - { city: '宁波市', district: '北仑区', code: 'NB_BL' }, - { city: '宁波市', district: '镇海区', code: 'NB_ZH' }, - { city: '宁波市', district: '鄞州区', code: 'NB_YZ' }, - { city: '宁波市', district: '奉化区', code: 'NB_FH' }, - { city: '宁波市', district: '象山县', code: 'NB_XS' }, - { city: '宁波市', district: '宁海县', code: 'NB_NH' }, - { city: '宁波市', district: '余姚市', code: 'NB_YY' }, - { city: '宁波市', district: '慈溪市', code: 'NB_CX' }, - // 温州市 - { city: '温州市', district: '鹿城区', code: 'WZ_LC' }, - { city: '温州市', district: '龙湾区', code: 'WZ_LW' }, - { city: '温州市', district: '瓯海区', code: 'WZ_OH' }, - { city: '温州市', district: '洞头区', code: 'WZ_DT' }, - { city: '温州市', district: '永嘉县', code: 'WZ_YJ' }, - { city: '温州市', district: '平阳县', code: 'WZ_PY' }, - { city: '温州市', district: '苍南县', code: 'WZ_CN' }, - { city: '温州市', district: '文成县', code: 'WZ_WC' }, - { city: '温州市', district: '泰顺县', code: 'WZ_TS' }, - { city: '温州市', district: '瑞安市', code: 'WZ_RA' }, - { city: '温州市', district: '乐清市', code: 'WZ_LQ' }, - // 嘉兴市 - { city: '嘉兴市', district: '南湖区', code: 'JX_NH' }, - { city: '嘉兴市', district: '秀洲区', code: 'JX_XZ' }, - { city: '嘉兴市', district: '嘉善县', code: 'JX_JS' }, - { city: '嘉兴市', district: '海盐县', code: 'JX_HY' }, - { city: '嘉兴市', district: '海宁市', code: 'JX_HN' }, - { city: '嘉兴市', district: '平湖市', code: 'JX_PH' }, - { city: '嘉兴市', district: '桐乡市', code: 'JX_TX' }, - // 湖州市 - { city: '湖州市', district: '吴兴区', code: 'HuZ_WX' }, - { city: '湖州市', district: '南浔区', code: 'HuZ_NX' }, - { city: '湖州市', district: '德清县', code: 'HuZ_DQ' }, - { city: '湖州市', district: '长兴县', code: 'HuZ_CX' }, - { city: '湖州市', district: '安吉县', code: 'HuZ_AJ' }, - // 绍兴市 - { city: '绍兴市', district: '越城区', code: 'SX_YC' }, - { city: '绍兴市', district: '柯桥区', code: 'SX_KQ' }, - { city: '绍兴市', district: '上虞区', code: 'SX_SY' }, - { city: '绍兴市', district: '新昌县', code: 'SX_XC' }, - { city: '绍兴市', district: '诸暨市', code: 'SX_ZJ' }, - { city: '绍兴市', district: '嵊州市', code: 'SX_SZ' }, - // 金华市 - { city: '金华市', district: '婺城区', code: 'JH_WC' }, - { city: '金华市', district: '金东区', code: 'JH_JD' }, - { city: '金华市', district: '武义县', code: 'JH_WY' }, - { city: '金华市', district: '浦江县', code: 'JH_PJ' }, - { city: '金华市', district: '磐安县', code: 'JH_PA' }, - { city: '金华市', district: '兰溪市', code: 'JH_LX' }, - { city: '金华市', district: '义乌市', code: 'JH_YW' }, - { city: '金华市', district: '东阳市', code: 'JH_DY' }, - { city: '金华市', district: '永康市', code: 'JH_YK' }, - // 衢州市 - { city: '衢州市', district: '柯城区', code: 'QZ_KC' }, - { city: '衢州市', district: '衢江区', code: 'QZ_QJ' }, - { city: '衢州市', district: '常山县', code: 'QZ_CS' }, - { city: '衢州市', district: '开化县', code: 'QZ_KH' }, - { city: '衢州市', district: '龙游县', code: 'QZ_LY' }, - { city: '衢州市', district: '江山市', code: 'QZ_JS' }, - // 舟山市 - { city: '舟山市', district: '定海区', code: 'ZS_DH' }, - { city: '舟山市', district: '普陀区', code: 'ZS_PT' }, - { city: '舟山市', district: '岱山县', code: 'ZS_DS' }, - { city: '舟山市', district: '嵊泗县', code: 'ZS_SS' }, - // 台州市 - { city: '台州市', district: '椒江区', code: 'TZ_JJ' }, - { city: '台州市', district: '黄岩区', code: 'TZ_HY' }, - { city: '台州市', district: '路桥区', code: 'TZ_LQ' }, - { city: '台州市', district: '三门县', code: 'TZ_SM' }, - { city: '台州市', district: '天台县', code: 'TZ_TT' }, - { city: '台州市', district: '仙居县', code: 'TZ_XJ' }, - { city: '台州市', district: '温岭市', code: 'TZ_WL' }, - { city: '台州市', district: '临海市', code: 'TZ_LH' }, - { city: '台州市', district: '玉环市', code: 'TZ_YH' }, - // 丽水市 - { city: '丽水市', district: '莲都区', code: 'LS_LD' }, - { city: '丽水市', district: '青田县', code: 'LS_QT' }, - { city: '丽水市', district: '缙云县', code: 'LS_JY' }, - { city: '丽水市', district: '遂昌县', code: 'LS_SC' }, - { city: '丽水市', district: '松阳县', code: 'LS_SY' }, - { city: '丽水市', district: '云和县', code: 'LS_YH' }, - { city: '丽水市', district: '庆元县', code: 'LS_QY' }, - { city: '丽水市', district: '景宁县', code: 'LS_JN' }, - { city: '丽水市', district: '龙泉市', code: 'LS_LQ' } - ]; - - // 批量插入区域数据 - for (const region of zhejiangRegions) { - try { - await getDB().execute( - 'INSERT IGNORE INTO zhejiang_regions (city_name, district_name, region_code) VALUES (?, ?, ?)', - [region.city, region.district, region.code] - ); - } catch (error) { - console.log(`插入区域数据失败: ${region.city} ${region.district}`, error.message); - } - } -} - -/** - * 初始化默认地址标签 - */ -async function initializeDefaultAddressLabels() { - const defaultLabels = [ - { name: '家', color: '#52c41a' }, - { name: '公司', color: '#1890ff' }, - { name: '学校', color: '#722ed1' } - ]; - - for (const label of defaultLabels) { - try { - await getDB().execute(` - INSERT IGNORE INTO address_labels (name, color, user_id, is_system) - VALUES (?, ?, NULL, TRUE) - `, [label.name, label.color]); - } catch (error) { - console.log(`默认标签${label.name}创建失败:`, error.message); - } - } - console.log('默认地址标签初始化完成'); -} - -/** - * 初始化全国省市区数据 - */ -async function initializeChinaRegions() { - const regions = [ - // 省级 - { code: '110000', name: '北京市', parent_code: null, level: 1 }, - { code: '120000', name: '天津市', parent_code: null, level: 1 }, - { code: '130000', name: '河北省', parent_code: null, level: 1 }, - { code: '140000', name: '山西省', parent_code: null, level: 1 }, - { code: '150000', name: '内蒙古自治区', parent_code: null, level: 1 }, - { code: '210000', name: '辽宁省', parent_code: null, level: 1 }, - { code: '220000', name: '吉林省', parent_code: null, level: 1 }, - { code: '230000', name: '黑龙江省', parent_code: null, level: 1 }, - { code: '310000', name: '上海市', parent_code: null, level: 1 }, - { code: '320000', name: '江苏省', parent_code: null, level: 1 }, - { code: '330000', name: '浙江省', parent_code: null, level: 1 }, - { code: '340000', name: '安徽省', parent_code: null, level: 1 }, - { code: '350000', name: '福建省', parent_code: null, level: 1 }, - { code: '360000', name: '江西省', parent_code: null, level: 1 }, - { code: '370000', name: '山东省', parent_code: null, level: 1 }, - { code: '410000', name: '河南省', parent_code: null, level: 1 }, - { code: '420000', name: '湖北省', parent_code: null, level: 1 }, - { code: '430000', name: '湖南省', parent_code: null, level: 1 }, - { code: '440000', name: '广东省', parent_code: null, level: 1 }, - { code: '450000', name: '广西壮族自治区', parent_code: null, level: 1 }, - { code: '460000', name: '海南省', parent_code: null, level: 1 }, - { code: '500000', name: '重庆市', parent_code: null, level: 1 }, - { code: '510000', name: '四川省', parent_code: null, level: 1 }, - { code: '520000', name: '贵州省', parent_code: null, level: 1 }, - { code: '530000', name: '云南省', parent_code: null, level: 1 }, - { code: '540000', name: '西藏自治区', parent_code: null, level: 1 }, - { code: '610000', name: '陕西省', parent_code: null, level: 1 }, - { code: '620000', name: '甘肃省', parent_code: null, level: 1 }, - { code: '630000', name: '青海省', parent_code: null, level: 1 }, - { code: '640000', name: '宁夏回族自治区', parent_code: null, level: 1 }, - { code: '650000', name: '新疆维吾尔自治区', parent_code: null, level: 1 }, - - // 浙江省市级 - { code: '330100', name: '杭州市', parent_code: '330000', level: 2 }, - { code: '330200', name: '宁波市', parent_code: '330000', level: 2 }, - { code: '330300', name: '温州市', parent_code: '330000', level: 2 }, - { code: '330400', name: '嘉兴市', parent_code: '330000', level: 2 }, - { code: '330500', name: '湖州市', parent_code: '330000', level: 2 }, - { code: '330600', name: '绍兴市', parent_code: '330000', level: 2 }, - { code: '330700', name: '金华市', parent_code: '330000', level: 2 }, - { code: '330800', name: '衢州市', parent_code: '330000', level: 2 }, - { code: '330900', name: '舟山市', parent_code: '330000', level: 2 }, - { code: '331000', name: '台州市', parent_code: '330000', level: 2 }, - { code: '331100', name: '丽水市', parent_code: '330000', level: 2 }, - - // 杭州市区级 - { code: '330102', name: '上城区', parent_code: '330100', level: 3 }, - { code: '330105', name: '拱墅区', parent_code: '330100', level: 3 }, - { code: '330106', name: '西湖区', parent_code: '330100', level: 3 }, - { code: '330108', name: '滨江区', parent_code: '330100', level: 3 }, - { code: '330109', name: '萧山区', parent_code: '330100', level: 3 }, - { code: '330110', name: '余杭区', parent_code: '330100', level: 3 }, - { code: '330111', name: '富阳区', parent_code: '330100', level: 3 }, - { code: '330112', name: '临安区', parent_code: '330100', level: 3 }, - { code: '330113', name: '临平区', parent_code: '330100', level: 3 }, - { code: '330114', name: '钱塘区', parent_code: '330100', level: 3 }, - { code: '330122', name: '桐庐县', parent_code: '330100', level: 3 }, - { code: '330127', name: '淳安县', parent_code: '330100', level: 3 }, - { code: '330182', name: '建德市', parent_code: '330100', level: 3 } - ]; - - for (const region of regions) { - try { - await getDB().execute(` - INSERT IGNORE INTO china_regions (code, name, parent_code, level) - VALUES (?, ?, ?, ?) - `, [region.code, region.name, region.parent_code, region.level]); - } catch (error) { - console.log(`区域${region.name}创建失败:`, error.message); - } - } - console.log('全国省市区数据初始化完成'); -} module.exports = { - initDatabase, - createTables, - addMissingFields, - createDefaultData, - initializeZhejiangRegions, - initializeDefaultAddressLabels, - initializeChinaRegions + initDatabase }; \ No newline at end of file diff --git a/db-monitor.js b/db-monitor.js deleted file mode 100644 index 0b08724..0000000 --- a/db-monitor.js +++ /dev/null @@ -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; \ No newline at end of file diff --git a/docs/apis/captcha.js b/docs/apis/captcha.js new file mode 100644 index 0000000..3a46213 --- /dev/null +++ b/docs/apis/captcha.js @@ -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: 服务器错误 + */ \ No newline at end of file diff --git a/docs/apis/matching.js b/docs/apis/matching.js new file mode 100644 index 0000000..7e7a72b --- /dev/null +++ b/docs/apis/matching.js @@ -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: 服务器错误 + */ \ No newline at end of file diff --git a/docs/apis/transfers.js b/docs/apis/transfers.js new file mode 100644 index 0000000..1db9f85 --- /dev/null +++ b/docs/apis/transfers.js @@ -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: 服务器错误 + */ \ No newline at end of file diff --git a/docs/apis/user.js b/docs/apis/user.js new file mode 100644 index 0000000..6055bc1 --- /dev/null +++ b/docs/apis/user.js @@ -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: 服务器错误 + */ \ No newline at end of file diff --git a/routes/agents.js b/routes/agents.js index f40b9ff..7af70a3 100644 --- a/routes/agents.js +++ b/routes/agents.js @@ -4,6 +4,8 @@ const { getDB } = require('../database'); const QRCode = require('qrcode'); const crypto = require('crypto'); const bcrypt = require('bcryptjs'); +const { auth } = require('../middleware/auth'); +const dayjs = require('dayjs'); // 获取浙江省所有区域列表 router.get('/regions', async (req, res) => { @@ -22,7 +24,7 @@ router.get('/regions', async (req, res) => { router.post('/apply', async (req, res) => { try { const { region_id, real_name, phone, id_card, contact_address } = req.body; - + if (!region_id || !real_name || !phone || !id_card) { return res.status(400).json({ success: false, message: '请填写完整信息' }); } @@ -51,13 +53,13 @@ router.post('/apply', async (req, res) => { let userId; if (existingUser.length > 0) { userId = existingUser[0].id; - + // 检查该用户是否已申请过代理(包括所有状态) const [existingUserAgent] = await getDB().execute( 'SELECT id, status, region_id FROM regional_agents WHERE user_id = ?', [userId] ); - + if (existingUserAgent.length > 0) { const agentStatus = existingUserAgent[0].status; if (agentStatus === 'active') { @@ -73,7 +75,7 @@ router.post('/apply', async (req, res) => { const bcrypt = require('bcryptjs'); const tempPassword = Math.random().toString(36).slice(-8); // 生成8位临时密码 const hashedPassword = await bcrypt.hash(tempPassword, 10); - + const [userResult] = await getDB().execute( 'INSERT INTO users (username, password, phone, real_name, id_card, created_at) VALUES (?, ?, ?, ?, ?, NOW())', [phone, hashedPassword, phone, real_name, id_card] @@ -101,7 +103,7 @@ router.post('/apply', async (req, res) => { router.post('/login', async (req, res) => { try { const { phone, password } = req.body; - + if (!phone || !password) { return res.status(400).json({ success: false, message: '请输入手机号和密码' }); } @@ -121,7 +123,7 @@ router.post('/login', async (req, res) => { } const agent = agents[0]; - + // 验证密码 const isPasswordValid = await bcrypt.compare(password, agent.password); if (!isPasswordValid) { @@ -132,9 +134,9 @@ router.post('/login', async (req, res) => { const jwt = require('jsonwebtoken'); const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; const token = jwt.sign( - { - userId: agent.user_id, - username: agent.username || agent.phone, + { + userId: agent.user_id, + username: agent.username || agent.phone, role: agent.role || 'agent', agentId: agent.id }, @@ -144,13 +146,13 @@ router.post('/login', async (req, res) => { delete agent.password; // 不返回密码 - res.json({ - success: true, + res.json({ + success: true, data: { ...agent, token - }, - message: '登录成功' + }, + message: '登录成功' }); } catch (error) { console.error('代理登录失败:', error); @@ -158,56 +160,6 @@ router.post('/login', async (req, res) => { } }); -// 生成注册二维码 -router.post('/generate-invite-code', async (req, res) => { - try { - const { agent_id } = req.body; - - if (!agent_id) { - return res.status(400).json({ success: false, message: '代理ID不能为空' }); - } - - // 验证代理是否存在且激活,并获取对应的user_id - const [agents] = await getDB().execute( - 'SELECT id, user_id FROM regional_agents WHERE id = ? AND status = "active"', - [parseInt(agent_id)] - ); - - if (agents.length === 0) { - return res.status(404).json({ success: false, message: '代理不存在或未激活' }); - } - - const userIdForAgent = agents[0].user_id; - - // 生成唯一激活码 - const code = crypto.randomBytes(8).toString('hex').toUpperCase(); - const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30天后过期 - - // 插入激活码记录(created_by_admin_id设为3608,agent_id使用user_id) - await getDB().execute( - `INSERT INTO registration_codes (code, expires_at, created_by_admin_id, agent_id, is_used, created_at) VALUES ('${code}', '${expiresAt.toISOString().slice(0, 19).replace('T', ' ')}', 3608, ${userIdForAgent}, 0, NOW())` - ); - - // 生成二维码 - 使用注册页面URL(不包含邀请码参数) - const registerUrl = `${process.env.FRONTEND_URL || 'https://www.zrbjr.com/frontend'}/register`; - - const qrCodeUrl = await QRCode.toDataURL(registerUrl); - - res.json({ - success: true, - data: { - code: code, - qr_code: qrCodeUrl, - expires_at: expiresAt - }, - message: '二维码生成成功' - }); - } catch (error) { - console.error('生成二维码失败:', error); - res.status(500).json({ success: false, message: '生成二维码失败' }); - } -}); - // 获取代理的商户列表(包含所有商户,标注早期商户状态) router.get('/merchants/:agent_id', async (req, res) => { try { @@ -221,11 +173,11 @@ router.get('/merchants/:agent_id', async (req, res) => { [parseInt(agent_id)] ); const regionId = agentInfo[0].region_id; - + if (!agentInfo || agentInfo.length === 0) { return res.status(404).json({ success: false, message: '代理不存在' }); } - + const agentCreatedAt = agentInfo[0].agent_created_at; // 获取商户列表(包含所有商户,包括agent_merchants表中的和符合条件的早期商户) @@ -264,7 +216,7 @@ router.get('/merchants/:agent_id', async (req, res) => { WHERE (am.agent_id = ? OR (u.created_at < ? AND u.district_id = ? AND u.role = 'user'))`, [parseInt(agent_id), parseInt(agent_id), agentCreatedAt, parseInt(regionId)] ); - + // 获取早期商户统计(从user表获取所有符合条件的早期商户) // 早期商户的判断条件:1.早期商户注册时间比代理要早。2.代理商代理的区县与商户的区县一致 const [earlyMerchantStats] = await getDB().execute( @@ -276,7 +228,7 @@ router.get('/merchants/:agent_id', async (req, res) => { AND u.role = 'user'`, [agentCreatedAt, parseInt(regionId)] ); - + // 获取正常商户统计(包括代理关联的商户,排除符合条件的早期商户) const [normalMerchantStats] = await getDB().execute( `SELECT @@ -287,8 +239,8 @@ router.get('/merchants/:agent_id', async (req, res) => { [parseInt(agent_id), parseInt(agent_id), agentCreatedAt, parseInt(regionId)] ); - res.json({ - success: true, + res.json({ + success: true, data: { merchants, total: parseInt(countResult[0].total), @@ -336,8 +288,8 @@ router.get('/commissions/:agent_id', async (req, res) => { summary[0].paid_commission = summary[0].total_commission; summary[0].pending_commission = 0; - res.json({ - success: true, + res.json({ + success: true, data: { commissions, summary: summary[0], @@ -379,20 +331,20 @@ router.get('/list', async (req, res) => { try { const { page = 1, limit = 10, status, region_id } = req.query; const offset = (page - 1) * limit; - + let whereClause = '1=1'; let params = []; - + if (status) { whereClause += ' AND ra.status = ?'; params.push(status); } - + if (region_id) { whereClause += ' AND ra.region_id = ?'; params.push(region_id); } - + // 获取代理列表 const [agents] = await getDB().execute( `SELECT ra.*, u.username, u.phone, u.real_name, u.created_at as user_created_at, @@ -404,7 +356,7 @@ router.get('/list', async (req, res) => { ORDER BY ra.created_at DESC LIMIT ${limit} OFFSET ${offset}` ); - + // 获取总数 const [countResult] = await getDB().execute( `SELECT COUNT(*) as total @@ -413,10 +365,10 @@ router.get('/list', async (req, res) => { JOIN zhejiang_regions zr ON ra.region_id = zr.id WHERE ${whereClause}` ); - + const total = countResult[0].total; const totalPages = Math.ceil(total / limit); - + res.json({ success: true, data: { @@ -446,7 +398,7 @@ router.get('/commission-trend/:agent_id', async (req, res) => { try { const { agent_id } = req.params; const { period = '7d' } = req.query; - + // 根据周期确定天数 let days; switch (period) { @@ -462,7 +414,7 @@ router.get('/commission-trend/:agent_id', async (req, res) => { default: days = 7; } - + // 获取指定时间范围内的佣金趋势数据 const [trendData] = await getDB().execute( `SELECT @@ -475,33 +427,33 @@ router.get('/commission-trend/:agent_id', async (req, res) => { ORDER BY date ASC`, [parseInt(agent_id), days] ); - + // 填充缺失的日期(确保每天都有数据点) const filledData = []; const today = new Date(); - + for (let i = days - 1; i >= 0; i--) { const date = new Date(today); date.setDate(date.getDate() - i); const dateStr = date.toISOString().split('T')[0]; - + // 修复日期比较:将数据库返回的Date对象转换为字符串进行比较 const existingData = trendData.find(item => { - const itemDateStr = item.date instanceof Date ? - item.date.toISOString().split('T')[0] : + const itemDateStr = item.date instanceof Date ? + item.date.toISOString().split('T')[0] : item.date; return itemDateStr === dateStr; }); - + filledData.push({ date: dateStr, amount: existingData ? parseFloat(existingData.amount) : 0 }); } - - res.json({ - success: true, - data: filledData + + res.json({ + success: true, + data: filledData }); } catch (error) { console.error('获取佣金趋势数据失败:', error); @@ -518,7 +470,7 @@ router.get('/commission-trend/:agent_id', async (req, res) => { router.get('/merchant-status/:agent_id', async (req, res) => { try { const { agent_id } = req.params; - + // 获取商户状态分布 const [statusData] = await getDB().execute( `SELECT @@ -536,10 +488,10 @@ router.get('/merchant-status/:agent_id', async (req, res) => { ORDER BY count DESC`, [parseInt(agent_id)] ); - - res.json({ - success: true, - data: statusData + + res.json({ + success: true, + data: statusData }); } catch (error) { console.error('获取商户状态分布数据失败:', error); @@ -556,7 +508,7 @@ router.get('/merchant-status/:agent_id', async (req, res) => { router.get('/detailed-stats/:agent_id', async (req, res) => { try { const { agent_id } = req.params; - + // 获取基础统计数据 const [basicStats] = await getDB().execute( `SELECT @@ -568,7 +520,7 @@ router.get('/detailed-stats/:agent_id', async (req, res) => { (SELECT COUNT(*) FROM agent_commission_records WHERE agent_id = ?) as total_commission_records`, [parseInt(agent_id), parseInt(agent_id), parseInt(agent_id), parseInt(agent_id), parseInt(agent_id), parseInt(agent_id)] ); - + // 获取本月佣金 const [monthlyStats] = await getDB().execute( `SELECT @@ -580,7 +532,7 @@ router.get('/detailed-stats/:agent_id', async (req, res) => { AND MONTH(created_at) = MONTH(CURDATE())`, [parseInt(agent_id)] ); - + // 获取今日佣金 const [dailyStats] = await getDB().execute( `SELECT @@ -591,7 +543,7 @@ router.get('/detailed-stats/:agent_id', async (req, res) => { AND DATE(created_at) = CURDATE()`, [parseInt(agent_id)] ); - + // 获取最近7天新增商户数 const [weeklyMerchants] = await getDB().execute( `SELECT COUNT(*) as weekly_new_merchants @@ -601,7 +553,7 @@ router.get('/detailed-stats/:agent_id', async (req, res) => { AND am.created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)`, [parseInt(agent_id)] ); - + // 获取提现相关统计数据 const [withdrawalStats] = await getDB().execute( `SELECT @@ -619,7 +571,7 @@ router.get('/detailed-stats/:agent_id', async (req, res) => { WHERE ra.id = ?`, [parseInt(agent_id), parseInt(agent_id)] ); - + // 合并所有统计数据 const stats = { ...basicStats[0], @@ -632,10 +584,10 @@ router.get('/detailed-stats/:agent_id', async (req, res) => { available_amount: 0 }) }; - - res.json({ - success: true, - data: stats + + res.json({ + success: true, + data: stats }); } catch (error) { console.error('获取详细统计数据失败:', error); @@ -658,17 +610,17 @@ router.get('/merchants/:agent_id/transfers', async (req, res) => { const pageNum = parseInt(page) || 1; const limitNum = parseInt(limit) || 10; const offset = (pageNum - 1) * limitNum; - + // 检查代理是否存在 const [agentResult] = await getDB().execute( 'SELECT * FROM regional_agents WHERE id = ?', [parseInt(agent_id)] ); - + if (agentResult.length === 0) { return res.status(404).json({ success: false, message: '代理不存在' }); } - + // 查询商户转账记录 const transferQuery = ` SELECT @@ -694,7 +646,7 @@ router.get('/merchants/:agent_id/transfers', async (req, res) => { LIMIT ${limitNum} OFFSET ${offset} `; const [transfers] = await getDB().execute(transferQuery, [parseInt(agent_id)]); - + // 查询总数 const [totalResult] = await getDB().execute( `SELECT COUNT(*) as total @@ -703,9 +655,9 @@ router.get('/merchants/:agent_id/transfers', async (req, res) => { WHERE am.agent_id = ?`, [parseInt(agent_id)] ); - + const total = totalResult[0].total; - + res.json({ success: true, data: { @@ -723,5 +675,45 @@ router.get('/merchants/:agent_id/transfers', async (req, res) => { res.status(500).json({ success: false, message: '获取代理商户转账记录失败,请稍后再试' }); } }); +/** + * 获取分销列表 + * @route GET /agents/distribution + * @returns {Object} 分销列表 + */ +router.get('/distribution', auth, async (req, res) => { + try { + const { id } = req.user; + const { page = 1, size = 10 } = req.query; + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(size) || 10; + const offset = (page - 1) * size; + const [result] = await getDB().execute( + `SELECT real_name,phone,username,avatar,created_at FROM users WHERE inviter = ? ORDER BY created_at DESC + LIMIT ${size} OFFSET ${offset}`, + [parseInt(id)] + ); + const [totalResult] = await getDB().execute( + `SELECT COUNT(*) as total FROM users WHERE inviter = ? `, + [parseInt(id)] + ); + result.forEach(item => { + item.created_at = dayjs(item.created_at).format('YYYY-MM-DD HH:mm:ss'); + }) + + const total = totalResult[0].total; + res.json({ + success: true, data: result, pagination: { + page: pageNum, + limit: limitNum, + total, + pages: Math.ceil(total / limitNum) + } + }); + } catch (error) { + console.error('获取分销列表失败:', error); + res.status(500).json({ success: false, message: '获取分销列表失败' }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/routes/agents/agents.js b/routes/agents/agents.js index d05b488..15420ff 100644 --- a/routes/agents/agents.js +++ b/routes/agents/agents.js @@ -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]; diff --git a/routes/auth.js b/routes/auth.js index 654fe8f..1ba620c 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -5,119 +5,6 @@ const { getDB } = require('../database'); const router = express.Router(); const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; - - -/** - * @swagger - * tags: - * name: Authentication - * description: 用户认证API - */ - -/** - * @swagger - * components: - * schemas: - * LoginCredentials: - * type: object - * required: - * - username - * - password - * properties: - * username: - * type: string - * description: 用户名或手机号 - * password: - * type: string - * description: 密码 - * RegisterRequest: - * type: object - * required: - * - username - * - phone - * - password - * - registrationCode - * - city - * - district_id - * - captchaId - * - captchaText - * - smsCode - * properties: - * username: - * type: string - * description: 用户名 - * phone: - * type: string - * description: 手机号 - * password: - * type: string - * description: 密码 - * registrationCode: - * type: string - * description: 注册激活码 - * city: - * type: string - * description: 城市 - * district_id: - * type: string - * description: 区域ID - * captchaId: - * type: string - * description: 图形验证码ID - * captchaText: - * type: string - * description: 图形验证码文本 - * smsCode: - * type: string - * description: 短信验证码 - * role: - * type: string - * description: 用户角色 - * default: user - */ - -/** - * @swagger - * /auth/register: - * post: - * summary: 用户注册 - * description: 需要提供有效的激活码才能注册 - * tags: [Authentication] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/RegisterRequest' - * responses: - * 201: - * description: 用户注册成功 - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - * token: - * type: string - * description: JWT认证令牌 - * user: - * type: object - * properties: - * id: - * type: integer - * username: - * type: string - * role: - * type: string - * 400: - * description: 请求参数错误 - * 500: - * description: 服务器错误 - */ router.post('/register', async (req, res) => { try { const db = getDB(); @@ -129,13 +16,15 @@ router.post('/register', async (req, res) => { password, city, district_id: district, + province, + inviter = '', captchaId, captchaText, smsCode, // 短信验证码 role = 'user' } = req.body; - if (!username || !phone || !password || !city || !district) { + if (!username || !phone || !password || !city || !district || !province) { return res.status(400).json({ success: false, message: '用户名、手机号、密码、城市和区域不能为空' }); } @@ -146,9 +35,6 @@ router.post('/register', async (req, res) => { if (!smsCode) { return res.status(400).json({ success: false, message: '短信验证码不能为空' }); } - - // 注意:图形验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证 - // 验证短信验证码 const smsAPI = require('./sms'); const smsValid = smsAPI.verifySMSCode(phone, smsCode); @@ -171,14 +57,7 @@ router.post('/register', async (req, res) => { ); if (existingUsers.length > 0) { - const existingUser = existingUsers[0]; - // 如果用户存在但未支付,允许重新注册(覆盖原用户信息) - if (existingUser.payment_status === 'unpaid') { - // 删除未支付的用户记录 - await db.execute('DELETE FROM users WHERE id = ?', [existingUser.id]); - } else { - return res.status(400).json({ success: false, message: '用户名或手机号已存在' }); - } + return res.status(400).json({ success: false, message: '用户名或手机号已存在' }); } // 加密密码 @@ -186,16 +65,11 @@ router.post('/register', async (req, res) => { // 创建用户(初始状态为未支付) const [result] = await db.execute( - 'INSERT INTO users (username, phone, password, role, points, audit_status, city, district_id, payment_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, "unpaid")', - [username, phone, hashedPassword, role, 0, 'pending', city, district] + 'INSERT INTO users (username, phone, password, role, points, audit_status, city, district_id, payment_status, province, inviter) VALUES (?, ?, ?, ?, ?, ?, ?, ?, "unpaid", ?, ?)', + [username, phone, hashedPassword, role, 0, 'pending', city, district, province, inviter] ); const userId = result.insertId; - - // 用户余额已在创建用户时设置为默认值0.00,无需额外操作 - - - // 根据地区自动关联代理 const [agents] = await db.execute( 'SELECT ra.id FROM users u INNER JOIN regional_agents ra ON u.id = ra.user_id WHERE ra.region_id = ? AND ra.status = "active" ORDER BY ra.created_at ASC LIMIT 1', @@ -237,7 +111,7 @@ router.post('/register', async (req, res) => { }); } catch (error) { try { - await getDB().query('ROLLBACK'); + // await getDB().query('ROLLBACK'); } catch (rollbackError) { console.error('回滚错误:', rollbackError); } @@ -251,55 +125,7 @@ router.post('/register', async (req, res) => { } }); -/** - * @swagger - * /auth/login: - * post: - * summary: 用户登录 - * tags: [Authentication] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/LoginCredentials' - * responses: - * 200: - * description: 登录成功 - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - * token: - * type: string - * description: JWT认证令牌 - * user: - * type: object - * properties: - * id: - * type: integer - * username: - * type: string - * role: - * type: string - * avatar: - * type: string - * points: - * type: integer - * 400: - * description: 请求参数错误 - * 401: - * description: 用户名或密码错误 - * 403: - * description: 账户审核未通过 - * 500: - * description: 服务器错误 - */ + router.post('/login', async (req, res) => { try { const db = getDB(); diff --git a/routes/captcha.js b/routes/captcha.js index 52f5985..8d3d64e 100644 --- a/routes/captcha.js +++ b/routes/captcha.js @@ -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; diff --git a/routes/matching.js b/routes/matching.js index 9d1222d..456857f 100644 --- a/routes/matching.js +++ b/routes/matching.js @@ -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; diff --git a/routes/sms.js b/routes/sms.js index 5492867..5f0040d 100644 --- a/routes/sms.js +++ b/routes/sms.js @@ -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}`); diff --git a/routes/transfers.js b/routes/transfers.js index 3c6c1e3..179ebe7 100644 --- a/routes/transfers.js +++ b/routes/transfers.js @@ -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) => { diff --git a/routes/upload.js b/routes/upload.js index 365f5f9..2222e0b 100644 --- a/routes/upload.js +++ b/routes/upload.js @@ -9,7 +9,7 @@ const { initializeBuckets } = require('../config/minio'); const router = express.Router(); // 初始化MinIO存储桶 -initializeBuckets().catch(console.error); +// initializeBuckets().catch(console.error); /** * @swagger diff --git a/routes/users.js b/routes/users.js index 9d18bd8..60c2f13 100644 --- a/routes/users.js +++ b/routes/users.js @@ -13,127 +13,7 @@ const router = express.Router(); * description: 用户管理API */ -/** - * @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: 服务器错误 - */ router.post('/', auth, adminAuth, async (req, res) => { try { const db = getDB(); @@ -150,7 +30,13 @@ router.post('/', auth, adminAuth, async (req, res) => { alipayQr, bankCard, unionpayQr, - phone + province, + city, + districtId, + phone, + avatar, + user_type = 'directly_operated', + inviter = '' } = req.body; if (!username || !password) { @@ -160,6 +46,9 @@ router.post('/', auth, adminAuth, async (req, res) => { if (!realName || !idCard) { return res.status(400).json({ success: false, message: '姓名和身份证号不能为空' }); } + if(!city || !districtId || !province){ + return res.status(400).json({ success: false, message: '请选择城市和区县' }); + } // 验证身份证号格式 const idCardRegex = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/; @@ -181,12 +70,21 @@ router.post('/', auth, adminAuth, async (req, res) => { const hashedPassword = await bcrypt.hash(password, 10); // 创建用户 + console.log([username, hashedPassword, role, isSystemAccount, 0, realName, idCard, wechatQr, alipayQr, bankCard, unionpayQr, phone, province, city, districtId, user_type, inviter],'info'); + const [result] = await db.execute( - 'INSERT INTO users (username, password, role, is_system_account, points, real_name, id_card, wechat_qr, alipay_qr, bank_card, unionpay_qr, phone) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', - [username, hashedPassword, role, isSystemAccount, 0, realName, idCard, wechatQr, alipayQr, bankCard, unionpayQr, phone] + 'INSERT INTO users (username, password, role, is_system_account, points, real_name, id_card, wechat_qr, alipay_qr, bank_card, unionpay_qr, phone, province, city, district_id, user_type, inviter,avatar) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?,?)', + [username, hashedPassword, role, isSystemAccount, 0, realName, idCard, wechatQr, alipayQr, bankCard, unionpayQr, phone, province, city, districtId, user_type, inviter,avatar] ); const userId = result.insertId; + if(user_type === 'agent_directly'){ + const agentCode = 'AG' + Date.now().toString().slice(-8); + await db.execute( + 'INSERT INTO regional_agents (user_id, region_id,status,agent_code) VALUES (?, ?,?,?)', + [userId, districtId,'active',agentCode] + ); + } // 用户余额已在创建用户时设置为默认值0.00,无需额外操作 @@ -339,7 +237,17 @@ router.get('/for-transfer', auth, async (req, res) => { router.get('/', auth, adminAuth, async (req, res) => { try { const db = getDB(); - const { page = 1, limit = 10, search = '', role = '', city = '', district = '', sort = 'created_at', order = 'desc' } = req.query; + const { + page = 1, + limit = 10, + search = '', + role = '', + city = '', + district = '', + province = '', + sort = 'created_at', + order = 'desc' + } = req.query; // 确保参数为有效数字 const pageNum = Math.max(1, parseInt(page) || 1); @@ -369,6 +277,12 @@ router.get('/', auth, adminAuth, async (req, res) => { listParams.push(city); } + if (province) { + whereConditions.push('u.province = ?'); + countParams.push(province); + listParams.push(province); + } + if (district) { whereConditions.push('u.district_id = ?'); countParams.push(district); @@ -400,36 +314,66 @@ router.get('/', auth, adminAuth, async (req, res) => { // 获取用户列表,关联地区信息和转账统计 const [users] = await db.execute( - `SELECT u.id, u.username, u.role, u.avatar, u.points, u.balance, u.real_name, u.id_card, u.phone, - u.wechat_qr, u.alipay_qr, u.bank_card, u.unionpay_qr, u.audit_status, u.is_system_account, - u.created_at, u.updated_at, u.city, u.district_id,u.id_card_front,u.id_card_back, - u.business_license,u.is_distribute, - r.city_name, r.district_name, - COALESCE(yesterday_out.amount, 0) as yesterday_transfer_amount, - COALESCE(today_in.amount, 0) as today_received_amount - FROM users u - LEFT JOIN zhejiang_regions r ON u.district_id = r.id - LEFT JOIN ( - SELECT from_user_id, SUM(amount) as amount - FROM transfers - WHERE created_at >= DATE(DATE_SUB(NOW(), INTERVAL 1 DAY)) - AND created_at < DATE(NOW()) - AND status IN ('confirmed', 'received') - GROUP BY from_user_id - ) yesterday_out ON u.id = yesterday_out.from_user_id - LEFT JOIN ( - SELECT to_user_id, SUM(amount) as amount - FROM transfers - WHERE created_at >= DATE(NOW()) - AND created_at < DATE(DATE_ADD(NOW(), INTERVAL 1 DAY)) - AND status IN ('confirmed', 'received') - GROUP BY to_user_id - ) today_in ON u.id = today_in.to_user_id - ${whereClause} - ORDER BY u.${sortField} ${sortOrder} - LIMIT ${limitNum} OFFSET ${offset}`, + `SELECT + u.id, + u.username, + u.role, + u.avatar, + u.points, + u.balance, + u.real_name, + u.id_card, + u.phone, + u.wechat_qr, + u.alipay_qr, + u.bank_card, + u.unionpay_qr, + u.audit_status, + u.is_system_account, + u.created_at, + u.updated_at, + u.province, + u.city, + u.district_id, + u.id_card_front, + u.id_card_back, + u.business_license, + u.is_distribute, + u.user_type, + u.inviter, + p.name as province_name, + c.name as city_name, + d.name as district_name, + COALESCE(yesterday_out.amount, 0) as yesterday_transfer_amount, + COALESCE(today_in.amount, 0) as today_received_amount + FROM users u + LEFT JOIN china_regions p ON u.province = p.code + LEFT JOIN china_regions c ON u.city = c.code + LEFT JOIN china_regions d ON u.district_id = d.code + LEFT JOIN ( + SELECT from_user_id, SUM(amount) as amount + FROM transfers + WHERE created_at >= DATE(DATE_SUB(NOW(), INTERVAL 1 DAY)) + AND created_at < DATE(NOW()) + AND status IN ('confirmed', 'received') + GROUP BY from_user_id + ) yesterday_out ON u.id = yesterday_out.from_user_id + LEFT JOIN ( + SELECT to_user_id, SUM(amount) as amount + FROM transfers + WHERE created_at >= DATE(NOW()) + AND created_at < DATE(DATE_ADD(NOW(), INTERVAL 1 DAY)) + AND status IN ('confirmed', 'received') + GROUP BY to_user_id + ) today_in ON u.id = today_in.to_user_id + ${whereClause} + ORDER BY u.${sortField} ${sortOrder} + LIMIT ${limitNum} OFFSET ${offset}`, listParams.slice(0, -2) ); + users.forEach(user => { + user.region = [user.province, user.city, user.district_id] + }) res.json({ success: true, @@ -891,247 +835,6 @@ router.get('/daily-revenue', auth, adminAuth, async (req, res) => { res.status(500).json({ success: false, message: '获取日收入统计失败' }); } }); - -// 生成注册码(管理员权限)==================== 激活码管理 ==================== - -/** - * 生成激活码(管理员权限) - */ -router.post('/registration-codes', auth, adminAuth, async (req, res) => { - try { - const db = getDB(); - const adminId = req.user.id; - - // 生成6位随机激活码 - const crypto = require('crypto'); - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - let code = ''; - for (let i = 0; i < 6; i++) { - code += chars.charAt(Math.floor(Math.random() * chars.length)); - } - - // 设置过期时间为1小时后 - const expiresAt = req.body.expiresAt || new Date(Date.now() + 60 * 60 * 1000); - - // 插入激活码 - const [result] = await db.execute( - 'INSERT INTO registration_codes (code, expires_at, created_by_admin_id) VALUES (?, ?, ?)', - [code, expiresAt, adminId] - ); - - res.status(201).json({ - success: true, - message: '激活码生成成功', - data: { - id: result.insertId, - code, - expiresAt, - createdAt: new Date() - } - }); - } catch (error) { - console.error('生成激活码错误:', error); - res.status(500).json({ success: false, message: '生成激活码失败' }); - } -}); - -/** - * 批量生成激活码(管理员权限) - */ -router.post('/registration-codes/batch', auth, adminAuth, async (req, res) => { - try { - const db = getDB(); - const adminId = req.user.id; - const { count = 1 } = req.body; - - // 验证参数 - const codeCount = Math.max(1, Math.min(100, parseInt(count) || 1)); - - const crypto = require('crypto'); - const codes = []; - const values = []; - const expiresAt = req.body.expiresAt || new Date(Date.now() + 60 * 60 * 1000); - - // 生成指定数量的激活码 - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - for (let i = 0; i < codeCount; i++) { - let code = ''; - for (let j = 0; j < 6; j++) { - code += chars.charAt(Math.floor(Math.random() * chars.length)); - } - codes.push(code); - values.push(code, expiresAt, adminId); - } - - // 批量插入数据库 - const placeholders = Array(codeCount).fill('(?, ?, ?)').join(', '); - - await db.execute( - `INSERT INTO registration_codes (code, expires_at, created_by_admin_id) VALUES ${placeholders}`, - values - ); - - res.status(201).json({ - success: true, - message: `成功生成 ${codeCount} 个激活码`, - data: { - codes, - count: codeCount, - expiresAt, - } - }); - } catch (error) { - console.error('批量生成激活码错误:', error); - res.status(500).json({ success: false, message: '批量生成激活码失败' }); - } -}); - -/** - * 获取激活码列表(管理员权限) - */ -router.get('/registration-codes', auth, adminAuth, async (req, res) => { - try { - const db = getDB(); - const { page = 1, limit = 20, status, keyword, sort = 'created_at', order = 'desc' } = req.query; - const offset = (page - 1) * limit; - - let whereClause = ''; - let whereConditions = []; - let countParams = []; - let listParams = []; - - // 根据状态筛选 - if (status === 'unused') { - whereConditions.push('rc.is_used = FALSE AND rc.expires_at > NOW()'); - } else if (status === 'used') { - whereConditions.push('rc.is_used = TRUE'); - } else if (status === 'expired') { - whereConditions.push('rc.is_used = FALSE AND rc.expires_at <= NOW()'); - } - - // 关键词搜索 - if (keyword) { - whereConditions.push(`rc.code LIKE '%${keyword}%'`); - } - - // 构建WHERE子句 - if (whereConditions.length > 0) { - whereClause = 'WHERE ' + whereConditions.join(' AND '); - } - - // 处理排序参数 - const allowedSortFields = ['created_at', 'expires_at', 'used_at', 'code', 'status']; - const allowedOrders = ['asc', 'desc']; - - let sortField = 'rc.created_at'; - let sortOrder = 'DESC'; - - if (allowedSortFields.includes(sort)) { - if (sort === 'status') { - // 状态字段需要使用CASE表达式 - sortField = `CASE - WHEN rc.is_used = TRUE THEN 'used' - WHEN rc.expires_at <= NOW() THEN 'expired' - ELSE 'unused' - END`; - } else { - sortField = `rc.${sort}`; - } - } - - if (allowedOrders.includes(order.toLowerCase())) { - sortOrder = order.toUpperCase(); - } - - // 设置查询参数(MySQL驱动需要字符串形式的LIMIT和OFFSET) - const limitStr = String(parseInt(limit)); - const offsetStr = String(parseInt(offset)); - listParams = [limitStr, offsetStr]; - countParams = []; - - // 获取激活码列表 - const [codes] = await db.execute(` - SELECT - rc.id, - rc.code, - rc.created_at, - rc.expires_at, - rc.used_at, - rc.is_used, - admin.username as created_by_admin, - user.username as used_by_user, - CASE - WHEN rc.is_used = TRUE THEN 'used' - WHEN rc.expires_at <= NOW() THEN 'expired' - ELSE 'unused' - END as status - FROM registration_codes rc - LEFT JOIN users admin ON rc.created_by_admin_id = admin.id - LEFT JOIN users user ON rc.used_by_user_id = user.id - ${whereClause} - ORDER BY ${sortField} ${sortOrder} - LIMIT ${limit} OFFSET ${offset} - `, countParams); - - // 获取总数 - const [countResult] = await db.execute(` - SELECT COUNT(*) as total - FROM registration_codes rc - ${whereClause} - `, countParams); - - const total = countResult[0].total; - - res.json({ - success: true, - data: { - codes, - pagination: { - page: parseInt(page), - limit: parseInt(limit), - total, - pages: Math.ceil(total / limit) - } - } - }); - } catch (error) { - console.error('获取激活码列表错误:', error); - res.status(500).json({ success: false, message: '获取激活码列表失败' }); - } -}); - -/** - * 删除激活码(管理员权限) - */ -router.delete('/registration-codes/:id', auth, adminAuth, async (req, res) => { - try { - const db = getDB(); - const codeId = req.params.id; - - // 检查激活码是否存在 - const [codes] = await db.execute( - 'SELECT id, is_used FROM registration_codes WHERE id = ?', - [codeId] - ); - - if (codes.length === 0) { - return res.status(404).json({ success: false, message: '激活码不存在' }); - } - - // 不能删除已使用的激活码 - if (codes[0].is_used) { - return res.status(400).json({ success: false, message: '不能删除已使用的激活码' }); - } - - // 删除激活码 - await db.execute('DELETE FROM registration_codes WHERE id = ?', [codeId]); - - res.json({ success: true, message: '激活码删除成功' }); - } catch (error) { - console.error('删除激活码错误:', error); - res.status(500).json({ success: false, message: '删除激活码失败' }); - } -}); // 获取当前用户个人资料 router.get('/profile', auth, async (req, res) => { try { @@ -1425,11 +1128,14 @@ router.put('/:id', auth, async (req, res) => { alipayQr, bankCard, unionpayQr, - city, - districtId, idCardFront, idCardBack, businessLicense, + province, + city, + districtId, + user_type, + inviter, } = req.body; // 只有管理员或用户本人可以更新信息 @@ -1571,11 +1277,26 @@ router.put('/:id', auth, async (req, res) => { updateValues.push(idCardBack); needsReaudit = true; } + if (province !== undefined) { + updateFields.push('province = ?'); + updateValues.push(province); + needsReaudit = true; + } if (businessLicense !== undefined) { updateFields.push('business_license = ?'); updateValues.push(businessLicense); needsReaudit = true; } + if (user_type !== undefined) { + updateFields.push('user_type = ?'); + updateValues.push(user_type); + needsReaudit = true; + } + if (inviter !== undefined) { + updateFields.push('inviter = ?'); + updateValues.push(inviter); + needsReaudit = true; + } // 如果更新了关键信息且用户不是管理员,则重置审核状态为待审核 if (needsReaudit && req.user.role !== 'admin') { @@ -1596,7 +1317,7 @@ router.put('/:id', auth, async (req, res) => { // 返回更新后的用户信息 const [updatedUsers] = await db.execute( - 'SELECT id, username, role, avatar, points, real_name, id_card, phone, wechat_qr, alipay_qr, bank_card, unionpay_qr, city, district_id, created_at, updated_at FROM users WHERE id = ?', + 'SELECT id, username, role, avatar, points, real_name, id_card, phone, wechat_qr, alipay_qr, bank_card, unionpay_qr, city, district_id, province, created_at, updated_at FROM users WHERE id = ?', [userId] ); @@ -1725,93 +1446,7 @@ router.get('/:id/audit-detail', auth, adminAuth, async (req, res) => { res.status(500).json({ success: false, message: '获取用户审核详情失败' }); } }); -/** - * @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: "服务器内部错误" - */ + router.put('/:id/distribute', auth, async (req, res) => { try { const db = getDB(); @@ -1822,30 +1457,335 @@ router.put('/:id/distribute', auth, async (req, res) => { return res.status(400).json({ success: false, message: '分发状态无效' }); } + // 检查用户是否存在 const [users] = await db.execute( - 'SELECT id FROM users WHERE id = ?', + 'SELECT id,user_type FROM users WHERE id = ?', [userId] ); if (users.length === 0) { return res.status(404).json({ success: false, message: '用户不存在' }); } - - // 更新分发状态 - await db.execute( - 'UPDATE users SET is_distribute = ? WHERE id = ?', - [is_distribute, userId] - ); - - res.json({ - success: true, - message: '分发状态更新成功', - is_distribute - }); + if(users[0].user_type === 'directly_operated'){ + return res.status(400).json({ success: false, message: '直营用户不允许开启委托出售' }); + } + let [isServiceFee] = await db.execute('SELECT COUNT(*) AS total FROM distribution WHERE created_at >= DATE_SUB(NOW(), INTERVAL 1 YEAR) AND user_id = ?', [userId]); + if (isServiceFee[0].total > 0) { + // 更新分发状态 + await db.execute( + 'UPDATE users SET is_distribute = ? WHERE id = ?', + [is_distribute, userId] + ); + res.json({ + success: true, + message: '分发状态更新成功', + is_distribute + }); + } else { + return res.json({ success: false, message: '请缴纳2980融豆服务费' }); + } } catch (error) { } }) +/** + * 扣除2980融豆服务费 +*/ +router.post('/:id/deduct-service-fee', auth, async (req, res) => { + const db = getDB(); + try { + const userId = req.params.id; + const serviceFee = 2980; // 服务费金额 + + // 开始事务 + await db.query('START TRANSACTION'); + + // 使用行级锁定查询用户信息,只锁定当前用户记录 + const [users] = await db.execute( + 'SELECT id, balance, username FROM users WHERE id = ? FOR UPDATE', + [userId] + ); + + if (users.length === 0) { + await db.query('ROLLBACK'); + return res.status(404).json({ success: false, message: '用户不存在' }); + } + //判断今年是否已扣款 + let [isServiceFee] = await db.execute('SELECT COUNT(*) AS total FROM distribution WHERE created_at >= DATE_SUB(NOW(), INTERVAL 1 YEAR) AND user_id = ?', [userId]); + if (isServiceFee[0].total > 0) { + return res.status(400).json({ success: false, message: '已缴纳2980融豆服务费' }); + } + const user = users[0]; + const currentBalance = Math.abs(user.balance); // 将负数转为正数处理 + + // 检查融豆余额是否足够 + if (currentBalance < serviceFee) { + await db.query('ROLLBACK'); + return res.status(400).json({ + success: false, + message: `融豆余额不足,当前余额:${currentBalance},需要:${serviceFee}` + }); + } + + // 扣除融豆(balance字段为负数,所以减去服务费实际是增加负数) + await db.execute( + 'UPDATE users SET balance = balance + ? WHERE id = ?', + [serviceFee, userId] + ); + //查找上级分销 + let [distribute] = await db.execute( + 'SELECT inviter FROM users WHERE id = ?', + [userId] + ); + distribute = distribute[0] + //如果有上级分销 + if (distribute.inviter) { + // 查找上级分销 + let [distributeUser] = await db.execute( + 'SELECT id, balance,user_type,inviter FROM users WHERE id = ?', + [distribute.inviter] + ); + distributeUser = distributeUser[0] + if (distributeUser.user_type == 'agent') { + //给代理添加2980融豆的70% + await db.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [serviceFee * 0.7, distributeUser.id] + ); + //记录转账记录 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.id, 'user_to_agent', 'received', serviceFee * 0.7, '用户服务费返现', 'agent'] + ); + //记录平台利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, 3512, 'user_to_system', 'received', serviceFee * 0.3, '用户服务费返现', 'system'] + ); + //记录服务费 + await db.execute( + 'INSERT INTO distribution (user_id,agent_id, amount, type) VALUES (?, ?, ?,?)', + [userId, distributeUser.id, serviceFee, 'agent'] + ) + } + //如果不是代理,查看是否是直营代理 + if (distributeUser.user_type == 'agent_directly') { + //给直营代理50%融豆给平台50%融豆 + await db.execute( + 'UPDATE regional_agents SET balance = balance - ? WHERE id = ?', + [serviceFee * 0.5, distributeUser.id] + ); + //记录转账记录 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.id, 'user_to_agent', 'received', serviceFee * 0.5, '用户服务费返现', 'agent'] + ); + //记录平台利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, 3512, 'user_to_system', 'received', serviceFee * 0.5, '用户服务费返现', 'system'] + ); + //记录服务费 + await db.execute( + 'INSERT INTO distribution (user_id,agent_id, amount, type) VALUES (?, ?, ?,?)', + [userId, distributeUser.id, serviceFee, 'direct_agent'] + ) + } + //是否是直营 + if (distributeUser.user_type == 'directly_operated') { + //查询这个月直营做了多少单 + let [orderCount] = await db.execute( + `SELECT COUNT(*) AS total FROM distribution WHERE agent_id = ? AND created_at >= DATE_FORMAT(NOW(), '%Y-%m-01')`, + [distributeUser.id] + ); + orderCount = orderCount[0] + if (orderCount.total <= 5) { + //给直营代理20%融豆给平台50%融豆给用户30% + await db.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [serviceFee * 0.2, distributeUser.inviter] + ); + //给直营添加30%融豆 + await db.execute( + 'UPDATE users SET balance = balance + ? WHERE id = ?', + [serviceFee * 0.3, distributeUser.id] + ); + //记录转账记录 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.inviter, 'user_to_agent', 'received', serviceFee * 0.2, '用户服务费返现', 'operated_agent'] + ); + //记录直营利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.id, 'user_to_operated', 'received', serviceFee * 0.3, '用户服务费返现', 'directly_operated'] + ); + //记录平台利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, 3512, 'user_to_system', 'received', serviceFee * 0.5, '用户服务费返现', 'system'] + ); + } + if (orderCount.total > 5 && orderCount.total <= 15) { + //给直营代理20%融豆给平台50%融豆给用户30% + await db.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [serviceFee * 0.15, distributeUser.inviter] + ); + //给直营添加30%融豆 + await db.execute( + 'UPDATE users SET balance = balance + ? WHERE id = ?', + [serviceFee * 0.35, distributeUser.id] + ); + //记录转账记录 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.inviter, 'user_to_agent', 'received', serviceFee * 0.2, '用户服务费返现', 'operated_agent'] + ); + //记录直营利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.id, 'user_to_operated', 'received', serviceFee * 0.3, '用户服务费返现', 'directly_operated'] + ); + //记录平台利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, 3512, 'user_to_system', 'received', serviceFee * 0.5, '用户服务费返现', 'system'] + ); + } + if (orderCount.total > 15) { + //给直营代理20%融豆给平台50%融豆给用户30% + await db.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [serviceFee * 0.1, distributeUser.inviter] + ); + //给直营添加30%融豆 + await db.execute( + 'UPDATE users SET balance = balance + ? WHERE id = ?', + [serviceFee * 0.4, distributeUser.id] + ); + //记录转账记录 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.inviter, 'user_to_agent', 'received', serviceFee * 0.2, '用户服务费返现', 'operated_agent'] + ); + //记录直营利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.id, 'user_to_operated', 'received', serviceFee * 0.3, '用户服务费返现', 'directly_operated'] + ); + //记录平台利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, 3512, 'user_to_system', 'received', serviceFee * 0.5, '用户服务费返现', 'system'] + ); + } + //记录服务费 + await db.execute( + 'INSERT INTO distribution (user_id,agent_id, amount, type) VALUES (?, ?, ?,?)', + [userId, distributeUser.id, serviceFee, 'direct_agent'] + ) + } + //要是用户之间分销 + if (distributeUser.user_type == 'user') { + //查询用户是否有上级 + let [userUpInfo] = await db.execute( + `SELECT * FROM users WHERE id = ?`, + [distributeUser.inviter] + ) + userUpInfo = userUpInfo[0] + //判断用户上级是否是代理 + if (userUpInfo && userUpInfo.user_type === 'agent') { + //给用户分配 + await db.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [serviceFee * 0.2, distributeUser.id] + ); + //给代理分配 + await db.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [serviceFee * 0.5, userUpInfo.id] + ); + //记录用户转账信息 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.id, 'user_to_user', 'received', serviceFee * 0.2, '用户服务费返现', 'manual'] + ); + //记录代理转账信息 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, userUpInfo.id, 'user_to_agent', 'received', serviceFee * 0.5, '用户服务费返现', 'manual'] + ); + //记录平台利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, 3512, 'user_to_system', 'received', serviceFee * 0.3, '用户服务费返现', 'system'] + ); + } else { + //用户没有上级 + await db.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [serviceFee * 0.2, distributeUser.id] + ); + //记录转账记录 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.id, 'user_to_agent', 'received', serviceFee * 0.2, '用户服务费返现', 'manual'] + ); + //记录平台利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, 3512, 'user_to_system', 'received', serviceFee * 0.8, '用户服务费返现', 'system'] + ); + + } + //记录服务费 + await db.execute( + 'INSERT INTO distribution (user_id,agent_id, amount, type) VALUES (?, ?, ?,?)', + [userId, distributeUser.id, serviceFee, 'user'] + ) + } + + } else { + //记录平台利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, 3512, 'user_to_system', 'received', serviceFee, '用户服务费返现', 'system'] + ); + await db.execute( + 'INSERT INTO distribution (user_id,agent_id, amount, type) VALUES (?, ?, ?,?)', + [userId, 3512, serviceFee, 'user'] + ) + } + await db.execute( + 'UPDATE users SET is_distribute = ? WHERE id = ?', + [true, userId] + ); + // 提交事务 + await db.query('COMMIT'); + + res.json({ + success: true, + message: '服务费扣除成功', + data: { + user_id: userId, + username: user.username, + deducted_amount: serviceFee, + remaining_balance: currentBalance - serviceFee + } + }); + + } catch (error) { + try { + // 发生错误时回滚事务 + await db.query('ROLLBACK'); + } catch (rollbackError) { + console.error('回滚失败:', rollbackError); + } + console.error('扣除服务费失败:', error); + res.status(500).json({ success: false, message: '扣除服务费失败' }); + } +}); module.exports = router; \ No newline at end of file diff --git a/services/alipayservice.js b/services/alipayservice.js index 6b82db0..977d58d 100644 --- a/services/alipayservice.js +++ b/services/alipayservice.js @@ -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} 支付结果 */ async createRegistrationPayOrder({ userId, username, phone, clientIp }) { + // 检查服务是否可用 + if (!this.isServiceAvailable()) { + throw new Error('支付宝服务未初始化或不可用'); + } + try { const db = getDB(); @@ -111,6 +198,11 @@ class AlipayService { * @returns {Promise} 查询结果 */ 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) { diff --git a/services/matchingService.js b/services/matchingService.js index c76476a..b893c16 100644 --- a/services/matchingService.js +++ b/services/matchingService.js @@ -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; } diff --git a/services/transferService.js b/services/transferService.js index 55b9e86..56d61ea 100644 --- a/services/transferService.js +++ b/services/transferService.js @@ -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) { diff --git a/services/wechatPayService.js b/services/wechatPayService.js index 748e070..b83a9c4 100644 --- a/services/wechatPayService.js +++ b/services/wechatPayService.js @@ -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; } }