commit a1944a573e852a485ce9d6c60609b6e30ffd75c0 Author: sunzhuangzhuang <961120009@qq.com> Date: Tue Aug 26 10:06:23 2025 +0800 初次提交 diff --git a/.env b/.env new file mode 100644 index 0000000..57467fe --- /dev/null +++ b/.env @@ -0,0 +1,26 @@ +# 数据库配置 +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 + +# 环境配置 +NODE_ENV=development +PORT=3000 + +# 前端地址配置 +FRONTEND_URL=https://www.zrbjr.com/frontend +# FRONTEND_URL=http://114.55.111.44:3001/frontend \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ad8bc0b --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +# 数据库配置 +DB_HOST=localhost +DB_USER=root +DB_PASSWORD=your_password +DB_NAME=your_database + +# JWT密钥 +JWT_SECRET=your_jwt_secret_key + +# 阿里云短信服务配置 +ALIYUN_ACCESS_KEY_ID=LTAI5tBHymRUu1vvo5tgYpaa +ALIYUN_ACCESS_KEY_SECRET=lNsDZvpUVX2b3pfBQCBawOEyr3dNB9 + +# 短信模板配置 +ALIYUN_SMS_SIGN_NAME=宁波炬融歆创科技 +ALIYUN_SMS_TEMPLATE_CODE=SMS_324470054 + +# 环境配置 +NODE_ENV=development +PORT=3000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa8db6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/node_modules +/logs +/uploads diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/UniappTool.xml b/.idea/UniappTool.xml new file mode 100644 index 0000000..f7328e8 --- /dev/null +++ b/.idea/UniappTool.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/.idea/code.iml b/.idea/code.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/.idea/code.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..23968dc --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..bd8b2bb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.trae/TODO.md b/.trae/TODO.md new file mode 100644 index 0000000..8ad817f --- /dev/null +++ b/.trae/TODO.md @@ -0,0 +1,7 @@ +# TODO: + +- [x] backend-api: 在 routes/transfers.js 中创建 GET /api/transfers/pending-allocations 接口 (priority: High) +- [x] frontend-tabs: 在 admin/src/views/Transfers.vue 中添加待处理匹配订单标签页 (priority: High) +- [x] frontend-display: 实现待处理匹配订单的数据显示和表格 (priority: High) +- [x] frontend-operations: 添加待处理匹配订单的操作功能(确认、拒绝等) (priority: Medium) +- [x] testing: 测试新功能的完整性和正确性 (priority: Medium) diff --git a/.vercel/project.json b/.vercel/project.json new file mode 100644 index 0000000..d931e9c --- /dev/null +++ b/.vercel/project.json @@ -0,0 +1 @@ +{"projectName":"trae_code_bv1k"} \ No newline at end of file diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..4898974 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,146 @@ +# 部署说明 + +## 问题解决 + +### 文件上传问题修复 + +之前程序部署到线上环境后无法上传文件的问题已经修复。主要修改包括: + +1. **移除硬编码地址**:将前端代码中硬编码的 `http://localhost:3000` 地址替换为动态配置 +2. **环境配置**:创建了环境配置文件来管理不同环境的API地址 +3. **统一图片处理**:统一了图片URL的处理逻辑 + +### 修改的文件 + +- `frontend/src/config/index.js` - 新增环境配置文件 +- `frontend/.env.development` - 开发环境配置 +- `frontend/.env.production` - 生产环境配置 +- `frontend/src/views/Transfers.vue` - 转账页面 +- `frontend/src/views/Profile.vue` - 个人资料页面 +- `frontend/src/views/Matching.vue` - 匹配页面 + +## 部署步骤 + +### 1. 前端构建 + +```bash +cd frontend +npm run build +``` + +### 2. 后端部署 + +确保服务器上有以下目录结构: +``` +/ +├── server.js +├── routes/ +├── middleware/ +├── uploads/ # 文件上传目录 +├── frontend/dist/ # 前端构建文件 +└── admin/dist/ # 管理后台构建文件 +``` + +### 3. 环境变量配置 + +在生产环境中,确保以下环境变量正确设置: + +```bash +# 数据库配置 +DB_HOST=your_db_host +DB_USER=your_db_user +DB_PASSWORD=your_db_password +DB_NAME=your_db_name + +# 服务器配置 +PORT=3000 +NODE_ENV=production +``` + +### 4. 文件权限 + +确保 `uploads` 目录有正确的读写权限: + +```bash +chmod 755 uploads +chown -R www-data:www-data uploads # 根据你的服务器用户调整 +``` + +### 5. Nginx 配置(推荐) + +如果使用 Nginx 作为反向代理,配置示例: + +```nginx +server { + listen 80; + server_name your-domain.com; + + # 静态文件 + location /uploads/ { + alias /path/to/your/app/uploads/; + expires 1d; + add_header Cache-Control "public, immutable"; + } + + # API 请求 + location /api/ { + proxy_pass http://localhost:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # 前端应用 + location / { + proxy_pass http://localhost:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +### 6. PM2 部署(推荐) + +使用 PM2 管理 Node.js 进程: + +```bash +# 安装 PM2 +npm install -g pm2 + +# 启动应用 +pm2 start server.js --name "your-app" + +# 设置开机自启 +pm2 startup +pm2 save +``` + +## 验证部署 + +1. 访问网站确保页面正常加载 +2. 测试用户注册/登录功能 +3. 测试文件上传功能(头像、收款码、转账凭证等) +4. 检查浏览器控制台是否有错误 + +## 故障排除 + +### 文件上传失败 + +1. 检查 `uploads` 目录权限 +2. 检查服务器磁盘空间 +3. 查看服务器日志:`pm2 logs your-app` + +### 图片显示不正常 + +1. 检查 Nginx 静态文件配置 +2. 确认图片文件存在于 `uploads` 目录 +3. 检查图片URL是否正确 + +### API 请求失败 + +1. 检查服务器是否正常运行 +2. 检查数据库连接 +3. 查看服务器错误日志 \ No newline at end of file diff --git a/add_commission.js b/add_commission.js new file mode 100644 index 0000000..9b651f0 --- /dev/null +++ b/add_commission.js @@ -0,0 +1,31 @@ +const { initDB, getDB } = require('./database'); + +async function addCommission() { + try { + await initDB(); + const db = getDB(); + + // 手动添加佣金记录 + await db.execute( + 'INSERT INTO agent_commission_records (agent_id, merchant_id, commission_amount, commission_type, description, created_at) VALUES (?, ?, ?, "matching", "用户完成第三次转账获得的代理佣金", NOW())', + [4, 9294, 39.9] + ); + + console.log('已手动添加佣金记录'); + + // 验证佣金记录是否添加成功 + const [commissions] = await db.execute( + 'SELECT * FROM agent_commission_records WHERE agent_id = ? AND merchant_id = ?', + [4, 9294] + ); + + console.log('佣金记录:', commissions); + + } catch (error) { + console.error('添加佣金记录失败:', error); + } finally { + process.exit(0); + } +} + +addCommission(); \ No newline at end of file diff --git a/admin b/admin new file mode 160000 index 0000000..4c6ab13 --- /dev/null +++ b/admin @@ -0,0 +1 @@ +Subproject commit 4c6ab13210df9fa2c5a7ffcc19d9abc4b25b28ba diff --git a/balance_audit.js b/balance_audit.js new file mode 100644 index 0000000..e022f9e --- /dev/null +++ b/balance_audit.js @@ -0,0 +1,347 @@ +const {getDB, initDB} = require('./database'); +const {logger} = require('./config/logger'); +const fs = require('fs'); +const path = require('path'); + +/** + * 余额一致性审计工具 + * 用于检查所有用户的余额是否与转账记录一致 + */ +class BalanceAuditor { + constructor() { + this.db = null; + this.auditResults = { + totalUsers: 0, + problematicUsers: [], + summary: { + totalDiscrepancy: 0, + usersWithPositiveDiscrepancy: 0, + usersWithNegativeDiscrepancy: 0, + maxDiscrepancy: 0, + minDiscrepancy: 0 + } + }; + } + + /** + * 初始化数据库连接 + */ + async init() { + await initDB(); + this.db = getDB(); + console.log('数据库连接已初始化'); + } + + /** + * 计算用户的理论余额 + * @param {number} userId - 用户ID + * @returns {Object} 包含收入、支出和理论余额的对象 + */ + async calculateUserBalance(userId) { + // 查询所有已确认和已收到的转账记录 + const [transfers] = await this.db.execute(` + SELECT + from_user_id, + to_user_id, + amount, + status, + transfer_type + FROM transfers + WHERE (from_user_id = ? OR to_user_id = ?) + AND status IN ('confirmed', 'received') + `, [userId, userId]); + + let totalReceived = 0; + let totalSent = 0; + + transfers.forEach(transfer => { + const amount = parseFloat(transfer.amount); + + // 计算收入(作为接收方的转账) + if (transfer.to_user_id === userId) { + totalReceived += amount; + } + + // 计算支出(作为发送方的转账) + if (transfer.from_user_id === userId) { + totalSent += amount; + } + }); + + return { + totalReceived, + totalSent, + theoreticalBalance: totalReceived - totalSent, + transferCount: transfers.length + }; + } + + /** + * 分析用户的问题转账记录 + * @param {number} userId - 用户ID + * @returns {Object} 问题转账分析结果 + */ + async analyzeProblematicTransfers(userId) { + // 查询被拒绝、取消的转账 + const [problemTransfers] = await this.db.execute(` + SELECT + id, + from_user_id, + to_user_id, + amount, + status, + admin_modified_at, + admin_modified_by, + admin_note + FROM transfers + WHERE (from_user_id = ? OR to_user_id = ?) + AND status IN ('rejected', 'cancelled', 'not_received') + ORDER BY admin_modified_at DESC + `, [userId, userId]); + + let shouldBeRefunded = 0; + const problemDetails = []; + + problemTransfers.forEach(transfer => { + const amount = parseFloat(transfer.amount); + + if (transfer.from_user_id === userId) { + shouldBeRefunded += amount; + problemDetails.push({ + transferId: transfer.id, + amount: amount, + status: transfer.status, + modifiedAt: transfer.admin_modified_at, + modifiedBy: transfer.admin_modified_by, + note: transfer.admin_note + }); + } + }); + + return { + shouldBeRefunded, + problemCount: problemDetails.length, + details: problemDetails + }; + } + + /** + * 审计单个用户的余额 + * @param {Object} user - 用户对象 + * @returns {Object} 审计结果 + */ + async auditUser(user) { + const userId = user.id; + const actualBalance = parseFloat(user.balance); + + // 计算理论余额 + const balanceCalc = await this.calculateUserBalance(userId); + + // 分析问题转账 + const problemAnalysis = await this.analyzeProblematicTransfers(userId); + + // 计算调整后的理论余额(考虑应退还金额) + const adjustedTheoreticalBalance = balanceCalc.theoreticalBalance + problemAnalysis.shouldBeRefunded; + + // 计算差异 + const discrepancy = actualBalance - adjustedTheoreticalBalance; + + const result = { + userId: userId, + username: user.username, + realName: user.real_name, + role: user.role, + actualBalance: actualBalance, + theoreticalBalance: balanceCalc.theoreticalBalance, + adjustedTheoreticalBalance: adjustedTheoreticalBalance, + discrepancy: discrepancy, + totalReceived: balanceCalc.totalReceived, + totalSent: balanceCalc.totalSent, + transferCount: balanceCalc.transferCount, + shouldBeRefunded: problemAnalysis.shouldBeRefunded, + problemTransferCount: problemAnalysis.problemCount, + problemDetails: problemAnalysis.details, + isProblematic: Math.abs(discrepancy) > 0.01 // 考虑浮点数精度 + }; + + return result; + } + + /** + * 审计所有用户的余额 + */ + async auditAllUsers() { + console.log('开始审计所有用户余额...'); + + // 获取所有用户 + const [users] = await this.db.execute(` + SELECT id, username, real_name, balance, role, created_at + FROM users + WHERE role != 'system' + ORDER BY id + `); + + this.auditResults.totalUsers = users.length; + console.log(`找到 ${users.length} 个用户需要审计`); + + let processedCount = 0; + + for (const user of users) { + try { + const auditResult = await this.auditUser(user); + + if (auditResult.isProblematic) { + this.auditResults.problematicUsers.push(auditResult); + console.log(`发现问题用户: ${user.username} (ID: ${user.id}), 差异: ${auditResult.discrepancy.toFixed(2)}`); + } + + // 更新统计信息 + this.updateSummary(auditResult); + + processedCount++; + if (processedCount % 100 === 0) { + console.log(`已处理 ${processedCount}/${users.length} 个用户`); + } + + } catch (error) { + console.error(`审计用户 ${user.id} 时出错:`, error.message); + } + } + + console.log('审计完成!'); + } + + /** + * 更新汇总统计信息 + * @param {Object} auditResult - 单个用户的审计结果 + */ + updateSummary(auditResult) { + const discrepancy = auditResult.discrepancy; + + this.auditResults.summary.totalDiscrepancy += Math.abs(discrepancy); + + if (discrepancy > 0) { + this.auditResults.summary.usersWithPositiveDiscrepancy++; + } else if (discrepancy < 0) { + this.auditResults.summary.usersWithNegativeDiscrepancy++; + } + + if (discrepancy > this.auditResults.summary.maxDiscrepancy) { + this.auditResults.summary.maxDiscrepancy = discrepancy; + } + + if (discrepancy < this.auditResults.summary.minDiscrepancy) { + this.auditResults.summary.minDiscrepancy = discrepancy; + } + } + + /** + * 生成审计报告 + */ + generateReport() { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const reportPath = path.join(__dirname, `balance_audit_report_${timestamp}.json`); + + const report = { + auditTime: new Date().toISOString(), + summary: { + totalUsers: this.auditResults.totalUsers, + problematicUsersCount: this.auditResults.problematicUsers.length, + totalDiscrepancy: this.auditResults.summary.totalDiscrepancy.toFixed(2), + usersWithPositiveDiscrepancy: this.auditResults.summary.usersWithPositiveDiscrepancy, + usersWithNegativeDiscrepancy: this.auditResults.summary.usersWithNegativeDiscrepancy, + maxDiscrepancy: this.auditResults.summary.maxDiscrepancy.toFixed(2), + minDiscrepancy: this.auditResults.summary.minDiscrepancy.toFixed(2) + }, + problematicUsers: this.auditResults.problematicUsers.map(user => ({ + userId: user.userId, + username: user.username, + realName: user.realName, + role: user.role, + actualBalance: user.actualBalance.toFixed(2), + theoreticalBalance: user.theoreticalBalance.toFixed(2), + adjustedTheoreticalBalance: user.adjustedTheoreticalBalance.toFixed(2), + discrepancy: user.discrepancy.toFixed(2), + shouldBeRefunded: user.shouldBeRefunded.toFixed(2), + problemTransferCount: user.problemTransferCount, + problemDetails: user.problemDetails + })) + }; + + fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf8'); + console.log(`\n审计报告已生成: ${reportPath}`); + + return report; + } + + /** + * 打印控制台报告 + */ + printConsoleReport() { + console.log('\n=== 余额审计报告 ==='); + console.log(`审计时间: ${new Date().toLocaleString()}`); + console.log(`总用户数: ${this.auditResults.totalUsers}`); + console.log(`问题用户数: ${this.auditResults.problematicUsers.length}`); + console.log(`总差异金额: ${this.auditResults.summary.totalDiscrepancy.toFixed(2)}`); + console.log(`余额偏高用户数: ${this.auditResults.summary.usersWithPositiveDiscrepancy}`); + console.log(`余额偏低用户数: ${this.auditResults.summary.usersWithNegativeDiscrepancy}`); + console.log(`最大差异: ${this.auditResults.summary.maxDiscrepancy.toFixed(2)}`); + console.log(`最小差异: ${this.auditResults.summary.minDiscrepancy.toFixed(2)}`); + + if (this.auditResults.problematicUsers.length > 0) { + console.log('\n=== 问题用户详情 ==='); + this.auditResults.problematicUsers.forEach((user, index) => { + console.log(`\n${index + 1}. 用户: ${user.username} (ID: ${user.userId})`); + console.log(` 实际余额: ${user.actualBalance.toFixed(2)}`); + console.log(` 理论余额: ${user.theoreticalBalance.toFixed(2)}`); + console.log(` 调整后理论余额: ${user.adjustedTheoreticalBalance.toFixed(2)}`); + console.log(` 差异: ${user.discrepancy.toFixed(2)}`); + console.log(` 应退还金额: ${user.shouldBeRefunded.toFixed(2)}`); + console.log(` 问题转账数: ${user.problemTransferCount}`); + + if (user.problemDetails.length > 0) { + console.log(' 问题转账详情:'); + user.problemDetails.forEach((detail, i) => { + console.log(` ${i + 1}. 转账ID: ${detail.transferId}, 金额: ${detail.amount}, 状态: ${detail.status}`); + }); + } + }); + } + } + + /** + * 运行完整的审计流程 + */ + async runAudit() { + try { + await this.init(); + await this.auditAllUsers(); + this.printConsoleReport(); + const report = this.generateReport(); + + // 记录到日志 + logger.info('Balance audit completed', { + totalUsers: this.auditResults.totalUsers, + problematicUsers: this.auditResults.problematicUsers.length, + totalDiscrepancy: this.auditResults.summary.totalDiscrepancy + }); + + return report; + + } catch (error) { + console.error('审计过程中发生错误:', error); + logger.error('Balance audit failed', { error: error.message }); + throw error; + } finally { + process.exit(0); + } + } +} + +// 如果直接运行此脚本 +if (require.main === module) { + const auditor = new BalanceAuditor(); + auditor.runAudit().catch(console.error); +} + +module.exports = BalanceAuditor; \ No newline at end of file diff --git a/balance_monitor.js b/balance_monitor.js new file mode 100644 index 0000000..2396587 --- /dev/null +++ b/balance_monitor.js @@ -0,0 +1,459 @@ +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 new file mode 100644 index 0000000..cd00e3b --- /dev/null +++ b/batch_process_commission.js @@ -0,0 +1,72 @@ +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/config.js b/config/config.js new file mode 100644 index 0000000..3099ef2 --- /dev/null +++ b/config/config.js @@ -0,0 +1,17 @@ +const mysql = require('mysql2') + +const sql = { + createConnection() { + return mysql.createPool({ + connectionLimit: 10, + host: '114.55.111.44', + user: 'test_mao', + password: 'nK2mPbWriBp25BRd', + database: 'test_mao', + charset: 'utf8mb4', + multipleStatements: true + + }) + } +} +module.exports = sql diff --git a/config/constants.js b/config/constants.js new file mode 100644 index 0000000..3272b44 --- /dev/null +++ b/config/constants.js @@ -0,0 +1,70 @@ +// 系统常量配置 +module.exports = { + // 转账类型 + TRANSFER_TYPES: { + USER_TO_USER: 'user_to_user', + SYSTEM_TO_USER: 'system_to_user', + USER_TO_SYSTEM: 'user_to_system' + }, + + // 转账状态 + TRANSFER_STATUS: { + PENDING: 'pending', + CONFIRMED: 'confirmed', + RECEIVED: 'received', + REJECTED: 'rejected', + CANCELLED: 'cancelled', + NOT_RECEIVED: 'not_received', + FAILED: 'failed' + }, + + // 用户角色 + USER_ROLES: { + ADMIN: 'admin', + USER: 'user' + }, + + // 订单状态 + ORDER_STATUS: { + PENDING: 'pending', + PAID: 'paid', + SHIPPED: 'shipped', + DELIVERED: 'delivered', + CANCELLED: 'cancelled' + }, + + // 错误代码 + ERROR_CODES: { + VALIDATION_ERROR: 'VALIDATION_ERROR', + AUTHENTICATION_ERROR: 'AUTHENTICATION_ERROR', + AUTHORIZATION_ERROR: 'AUTHORIZATION_ERROR', + NOT_FOUND: 'NOT_FOUND', + DUPLICATE_ENTRY: 'DUPLICATE_ENTRY', + DATABASE_ERROR: 'DATABASE_ERROR', + INTERNAL_ERROR: 'INTERNAL_ERROR' + }, + + // HTTP状态码 + HTTP_STATUS: { + OK: 200, + CREATED: 201, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOT_FOUND: 404, + CONFLICT: 409, + INTERNAL_SERVER_ERROR: 500 + }, + + // 分页默认值 + PAGINATION: { + DEFAULT_PAGE: 1, + DEFAULT_LIMIT: 10, + MAX_LIMIT: 100 + }, + + // JWT配置 + JWT: { + EXPIRES_IN: '24h' + } +}; \ No newline at end of file diff --git a/config/database-init.js b/config/database-init.js new file mode 100644 index 0000000..8877791 --- /dev/null +++ b/config/database-init.js @@ -0,0 +1,790 @@ +const mysql = require('mysql2/promise'); +const bcrypt = require('bcryptjs'); +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 addMissingFields(); + + // 创建默认数据 + await createDefaultData(); + + console.log('数据库初始化完成'); + } catch (error) { + console.error('数据库初始化失败:', error); + throw error; + } +} + +/** + * 创建所有数据库表 + */ +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, + 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, + 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, + details TEXT, + 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, + status ENUM('pending', 'paid', 'shipped', 'delivered', 'cancelled') 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, + quantity INT NOT NULL, + price INT NOT NULL, + points INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (order_id) REFERENCES orders(id), + FOREIGN KEY (product_id) REFERENCES products(id) + ) + `); + + // 积分记录表 + 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 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 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 + ) + `); +} + +/** + * 添加缺失的字段(处理数据库升级) + */ +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: 'image_url', sql: 'ALTER TABLE products ADD COLUMN image_url VARCHAR(500)' }, + { name: 'details', sql: 'ALTER TABLE products ADD COLUMN details TEXT' } + ]; + + 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(); +} + +/** + * 初始化浙江省区域数据 + */ +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); + } + } +} + +module.exports = { + initDatabase, + createTables, + addMissingFields, + createDefaultData, + initializeZhejiangRegions +}; \ No newline at end of file diff --git a/config/dbv2.js b/config/dbv2.js new file mode 100644 index 0000000..e7e253b --- /dev/null +++ b/config/dbv2.js @@ -0,0 +1,363 @@ +class QueryBuilder { + constructor() { + this.conditions = {}; + this.limit = null; + this.offset = null; + this.groupBy = null; + } + + where(condition, ...params) { + this.conditions[condition] = params; + return this; + } + + setLimit(limit) { + this.limit = limit; + return this; + } + + setOffset(offset) { + this.offset = offset; + return this; + } + + setGroupBy(groupBy) { + this.groupBy = groupBy; + return this; + } + + sqdata(sql, params) { + return new Promise((resolve, reject) => { + global.sqlReq.query(sql, params, (err, result) => { + if (err) { + reject(err); + } + resolve(result); + }); + }); + } + + getParams() { + return Object.values(this.conditions).flat(); + } + + buildConditions() { + return Object.keys(this.conditions).map(condition => `${condition}`).join(' AND '); + } +} + +class SelectBuilder extends QueryBuilder { + constructor() { + super(); + this.selectFields = []; + this.tables = []; + this.orderByField = ''; + this.orderByDirection = 'ASC'; + this.subQueries = []; // 用于存储子查询 + this.unions = []; // 存储UNION查询 + } + // 添加UNION查询 + union(queryBuilder, type = 'UNION') { + this.unions.push({queryBuilder, type}); + return this; + } + + // 添加UNION ALL查询 + unionAll(queryBuilder) { + this.union(queryBuilder, 'UNION ALL'); + return this; + } + + // 构建主查询部分(不含ORDER BY/LIMIT/OFFSET) + buildMainQuery() { + const subQuerySQL = this.subQueries.map(({alias, subQuery}) => `(${subQuery}) AS ${alias}`); + const selectClause = this.selectFields.concat(subQuerySQL).join(', '); + + let sql = `SELECT ${selectClause} + FROM ${this.tables.join(' ')}`; + + const conditionClauses = this.buildConditions(); + if (conditionClauses) { + sql += ` WHERE ${conditionClauses}`; + } + + if (this.groupBy) { + sql += ` GROUP BY ${this.groupBy}`; + } + + const params = this.getParams(); + return {sql, params}; + } + + // 供UNION查询调用的构建方法 + buildForUnion() { + return this.buildMainQuery(); + } + + select(fields) { + this.selectFields = fields.split(',').map(field => field.trim()); + return this; + } + + // 添加子查询 + addSubQuery(alias, subQuery) { + this.subQueries.push({alias, subQuery}); + return this; + } + + whereLike(fields, keyword) { + const likeConditions = fields.map(field => `${field} LIKE ?`).join(' OR '); + this.conditions[likeConditions] = fields.map(() => `%${keyword}%`); + return this; + } + + from(table) { + this.tables.push(table); + return this; + } + + leftJoin(table, condition) { + this.tables.push(`LEFT JOIN ${table} ON ${condition}`); + return this; + } + + orderBy(field, direction = 'ASC') { + this.orderByField = field; + this.orderByDirection = direction.toUpperCase(); + return this; + } + + paginate(page, pageSize) { + if (page <= 0 || pageSize <= 0) { + throw new Error('分页参数必须大于0'); + } + this.limit = pageSize; + this.offset = (page - 1) * pageSize; + return this; + } + + async chidBuild() { + + let sql = `SELECT ${this.selectFields.join(', ')} + FROM ${this.tables.join(' ')}`; + let conditionClauses = this.buildConditions(); + if (conditionClauses) { + sql += ` WHERE ${conditionClauses}`; + } + if (this.orderByField) { + sql += ` ORDER BY ${this.orderByField} ${this.orderByDirection}`; + } + if (this.limit !== null) { + sql += ` LIMIT ${this.limit}`; + } + if (this.offset !== null) { + sql += ` OFFSET ${this.offset}`; + } + return sql; + } + + async build() { + const main = this.buildMainQuery(); + let fullSql = `(${main.sql})`; + const allParams = [...main.params]; + + // 处理UNION部分 + for (const union of this.unions) { + const unionBuilder = union.queryBuilder; + if (!(unionBuilder instanceof SelectBuilder)) { + throw new Error('UNION query must be a SelectBuilder instance'); + } + const unionResult = unionBuilder.buildForUnion(); + fullSql += ` ${union.type} (${unionResult.sql})`; + allParams.push(...unionResult.params); + } + + // 添加ORDER BY、LIMIT、OFFSET + if (this.orderByField) { + fullSql += ` ORDER BY ${this.orderByField} ${this.orderByDirection}`; + } + if (this.limit !== null) { + fullSql += ` LIMIT ${this.limit}`; + } + if (this.offset !== null) { + fullSql += ` OFFSET ${this.offset}`; + } + console.log(fullSql,allParams); + return await this.sqdata(fullSql, allParams); + } +} + + +class UpdateBuilder extends QueryBuilder { + constructor() { + super(); + this.table = ''; + this.updateFields = {}; + } + + update(table) { + this.table = table; + return this; + } + + set(field, value) { + if (value && value.increment && typeof value === 'object' ) { + this.updateFields[field] = {increment: value.increment}; + } else { + this.updateFields[field] = value; + } + return this; + } + + async build() { + let sql = `UPDATE ${this.table} + SET `; + let updateClauses = Object.keys(this.updateFields).map(field => { + const value = this.updateFields[field]; + if (value && value.increment && typeof value === 'object' ) { + return `${field} = ${field} + ?`; + } + return `${field} = ?`; + }).join(', '); + + sql += updateClauses; + + let conditionClauses = this.buildConditions(); + if (conditionClauses) { + sql += ` WHERE ${conditionClauses}`; + } + // 处理参数,确保自增字段也传入增量值 + const params = [ + ...Object.values(this.updateFields).map(value => + (value && value.increment && typeof value === 'object' ) ? value.increment : value + ), + ...this.getParams() + ]; + return await this.sqdata(sql, params); + } +} + +class InsertBuilder extends QueryBuilder { + constructor() { + super(); + this.table = ''; + this.insertValues = []; + this.updateValues = {}; + } + + insertInto(table) { + this.table = table; + return this; + } + + // 仍然保留单条记录的插入 + values(values) { + if (Array.isArray(values)) { + this.insertValues = values; + } else { + this.insertValues = [values]; // 将单条记录包装成数组 + } + return this; + } + + // 新增方法,支持一次插入多条记录 + valuesMultiple(records) { + if (!Array.isArray(records) || records.length === 0) { + throw new Error('Values must be a non-empty array'); + } + + // 确保每一条记录都是对象 + records.forEach(record => { + if (typeof record !== 'object') { + throw new Error('Each record must be an object'); + } + }); + + this.insertValues = records; + return this; + } + + // 新增 upsert 方法,支持更新或插入 + upsert(values, updateFields) { + // values: 要插入的记录 + // updateFields: 如果记录存在时,需要更新的字段 + if (!Array.isArray(values) || values.length === 0) { + throw new Error('Values must be a non-empty array'); + } + + // 检查每条记录是否是对象 + values.forEach(record => { + if (typeof record !== 'object') { + throw new Error('Each record must be an object'); + } + }); + + this.insertValues = values; + this.updateValues = updateFields || {}; + return this; + } + + async build() { + if (this.insertValues.length === 0) { + throw new Error("No values to insert"); + } + + // 获取表单列名,假设所有记录有相同的字段 + const columns = Object.keys(this.insertValues[0]); + + // 构建 VALUES 子句,支持批量插入 + const valuePlaceholders = this.insertValues.map(() => + `(${columns.map(() => '?').join(', ')})` + ).join(', '); + + // 展平所有的插入值 + const params = this.insertValues.flatMap(record => + columns.map(column => record[column]) + ); + + // 如果有 updateFields,构建 ON DUPLICATE KEY UPDATE 子句 + let updateClause = ''; + if (Object.keys(this.updateValues).length > 0) { + updateClause = ' ON DUPLICATE KEY UPDATE ' + + Object.keys(this.updateValues).map(field => { + return `${field} = VALUES(${field})`; + }).join(', '); + } + + // 生成 SQL 语句 + const sql = `INSERT INTO ${this.table} (${columns.join(', ')}) + VALUES ${valuePlaceholders} ${updateClause}`; + // 执行查询 + return await this.sqdata(sql, params); + } +} + + +class DeleteBuilder extends QueryBuilder { + constructor() { + super(); + this.table = ''; + } + + deleteFrom(table) { + this.table = table; + return this; + } + + async build() { + let sql = `DELETE + FROM ${this.table}`; + let conditionClauses = this.buildConditions(); + if (conditionClauses) { + sql += ` WHERE ${conditionClauses}`; + } + return await this.sqdata(sql, this.getParams()); + } +} + +module.exports = { + SelectBuilder, + UpdateBuilder, + InsertBuilder, + DeleteBuilder, +}; diff --git a/config/logger.js b/config/logger.js new file mode 100644 index 0000000..45cda51 --- /dev/null +++ b/config/logger.js @@ -0,0 +1,73 @@ +const winston = require('winston'); +const path = require('path'); + +// 创建日志目录 +const logDir = path.join(__dirname, '../logs'); + +// 日志格式配置 +const logFormat = winston.format.combine( + winston.format.timestamp({ + format: 'YYYY-MM-DD HH:mm:ss' + }), + winston.format.errors({ stack: true }), + winston.format.json() +); + +// 控制台日志格式 +const consoleFormat = winston.format.combine( + winston.format.colorize(), + winston.format.timestamp({ + format: 'YYYY-MM-DD HH:mm:ss' + }), + winston.format.printf(({ timestamp, level, message, ...meta }) => { + return `${timestamp} [${level}]: ${message} ${Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''}`; + }) +); + +// 创建logger实例 +const logger = winston.createLogger({ + level: process.env.LOG_LEVEL || 'info', + format: logFormat, + defaultMeta: { service: 'integrated-system' }, + transports: [ + // 错误日志文件 + new winston.transports.File({ + filename: path.join(logDir, 'error.log'), + level: 'error', + maxsize: 5242880, // 5MB + maxFiles: 5 + }), + // 所有日志文件 + new winston.transports.File({ + filename: path.join(logDir, 'combined.log'), + maxsize: 5242880, // 5MB + maxFiles: 5 + }) + ] +}); + +// 开发环境添加控制台输出 +if (process.env.NODE_ENV !== 'production') { + logger.add(new winston.transports.Console({ + format: consoleFormat + })); +} + +// 审计日志记录器 +const auditLogger = winston.createLogger({ + level: 'info', + format: logFormat, + defaultMeta: { service: 'audit' }, + transports: [ + new winston.transports.File({ + filename: path.join(logDir, 'audit.log'), + maxsize: 5242880, // 5MB + maxFiles: 10 + }) + ] +}); + +module.exports = { + logger, + auditLogger +}; \ No newline at end of file diff --git a/config/withdrawal-init.sql b/config/withdrawal-init.sql new file mode 100644 index 0000000..a42c3fe --- /dev/null +++ b/config/withdrawal-init.sql @@ -0,0 +1,34 @@ +-- 创建代理提现记录表 +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, + FOREIGN KEY (agent_id) REFERENCES regional_agents(id) ON DELETE CASCADE, + FOREIGN KEY (processed_by) REFERENCES users(id) ON DELETE SET NULL, + -- 兼容旧字段 + bank_account VARCHAR(50) COMMENT '银行账号(兼容旧版本)' +); + +-- 为regional_agents表添加提现相关字段 +ALTER TABLE regional_agents ADD COLUMN withdrawn_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '已提现金额'; +ALTER TABLE regional_agents ADD COLUMN pending_withdrawal DECIMAL(10,2) DEFAULT 0.00 COMMENT '待审核提现金额'; +ALTER TABLE regional_agents ADD COLUMN payment_type ENUM('bank', 'wechat', 'alipay', 'unionpay') DEFAULT 'bank' COMMENT '收款方式类型'; +ALTER TABLE regional_agents ADD COLUMN bank_name VARCHAR(100) COMMENT '银行名称'; +ALTER TABLE regional_agents ADD COLUMN account_number VARCHAR(50) COMMENT '账号/银行账号'; +ALTER TABLE regional_agents ADD COLUMN account_holder VARCHAR(100) COMMENT '持有人姓名'; +ALTER TABLE regional_agents ADD COLUMN qr_code_url VARCHAR(255) COMMENT '收款码图片URL'; + +-- 兼容旧字段(可选,用于数据迁移) +ALTER TABLE regional_agents ADD COLUMN bank_account VARCHAR(50) COMMENT '银行账号(兼容旧版本)'; \ No newline at end of file diff --git a/database.js b/database.js new file mode 100644 index 0000000..50a1445 --- /dev/null +++ b/database.js @@ -0,0 +1,158 @@ +const mysql = require('mysql2/promise'); + +// 数据库配置 +const dbConfig = { + host: process.env.DB_HOST || '114.55.111.44', + user: process.env.DB_USER || 'maov2', + password: process.env.DB_PASSWORD || '5fYhw8z6T62b7heS', + database: process.env.DB_NAME || 'maov2', + // host: '114.55.111.44', + // user: 'test_mao', + // password: 'nK2mPbWriBp25BRd', + // database: 'test_mao', + // charset: 'utf8mb4', + // 连接池配置 + connectionLimit: 20, // 连接池最大连接数 + queueLimit: 0, // 排队等待连接的最大数量,0表示无限制 + // 连接超时配置 + // acquireTimeout: 60000, // 获取连接超时时间 60秒 + // timeout: 60000, // 查询超时时间 60秒 + // reconnect: true, // 自动重连 + // 连接保活配置 + multipleStatements: true, + // 空闲连接超时配置 + idleTimeout: 300000, // 5分钟空闲超时 + // maxLifetime: 1800000, // 30分钟最大生命周期 + // 连接保活设置 + keepAliveInitialDelay: 0, // 开始保活探测前的延迟时间 + enableKeepAlive: true, // 启用TCP保活 + // 添加类型转换配置 + typeCast: function (field, next) { + if (field.type === 'TINY' && field.length === 1) { + return (field.string() === '1'); // 1 = true, 0 = false + } + return next(); + }, + // 确保参数正确处理 + supportBigNumbers: true, + bigNumberStrings: false +}; + +// 创建数据库连接池 +let pool; + +/** + * 初始化数据库连接池 + * @returns {Promise} 数据库连接池 + */ +async function initDB() { + if (!pool) { + try { + pool = mysql.createPool(dbConfig); + + // 添加连接池事件监听 + pool.on('connection', function (connection) { + console.log('新的数据库连接建立:', connection.threadId); + }); + + // 注释掉频繁的连接获取和释放日志,避免日志过多 + // pool.on('acquire', function (connection) { + // console.log('连接池获取连接:', connection.threadId); + // }); + + // pool.on('release', function (connection) { + // console.log('连接池释放连接:', connection.threadId); + // }); + + pool.on('error', function(err) { + console.error('数据库连接池错误:', err); + if(err.code === 'PROTOCOL_CONNECTION_LOST') { + console.log('数据库连接丢失,尝试重新连接...'); + } else if(err.code === 'ECONNRESET') { + console.log('数据库连接被重置,尝试重新连接...'); + } else if(err.code === 'ETIMEDOUT') { + console.log('数据库连接超时,尝试重新连接...'); + } + }); + + // 测试连接 + const connection = await pool.getConnection(); + console.log('数据库连接池初始化成功'); + connection.release(); + + } catch (error) { + console.error('数据库连接池初始化失败:', error); + throw error; + } + } + return pool; +} + +/** + * 获取数据库连接池 + * @returns {mysql.Pool} 数据库连接池 + */ +function getDB() { + if (!pool) { + throw new Error('数据库连接池未初始化,请先调用 initDB()'); + } + return pool; +} + +/** + * 执行数据库查询(带重试机制) + * @param {string} sql SQL查询语句 + * @param {Array} params 查询参数 + * @param {number} retries 重试次数 + * @returns {Promise} 查询结果 + */ +async function executeQuery(sql, params = [], retries = 3) { + for (let i = 0; i < retries; i++) { + try { + const connection = await pool.getConnection(); + try { + const [results] = await connection.execute(sql, params); + connection.release(); + return results; + } catch (error) { + connection.release(); + throw error; + } + } catch (error) { + console.error(`数据库查询失败 (尝试 ${i + 1}/${retries}):`, error.message); + + if (i === retries - 1) { + throw error; + } + + // 如果是连接相关错误,等待后重试 + if (error.code === 'PROTOCOL_CONNECTION_LOST' || + error.code === 'ECONNRESET' || + error.code === 'ETIMEDOUT') { + console.log(`等待 ${(i + 1) * 1000}ms 后重试...`); + await new Promise(resolve => setTimeout(resolve, (i + 1) * 1000)); + } else { + throw error; + } + } + } +} + +/** + * 关闭数据库连接池 + */ +async function closeDB() { + if (pool) { + await pool.end(); + pool = null; + console.log('数据库连接池已关闭'); + } +} + +module.exports = { + initDB, + getDB, + closeDB, + executeQuery, + dbConfig +}; \ No newline at end of file diff --git a/db-monitor.js b/db-monitor.js new file mode 100644 index 0000000..0b08724 --- /dev/null +++ b/db-monitor.js @@ -0,0 +1,295 @@ +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/fix-image-paths.js b/fix-image-paths.js new file mode 100644 index 0000000..d860b5a --- /dev/null +++ b/fix-image-paths.js @@ -0,0 +1,89 @@ +const { getDB, initDB } = require('./database'); + +/** + * 修复商品图片路径 + * 将 /uploads/product/ 更新为 /uploads/products/ + */ +async function fixImagePaths() { + try { + console.log('开始修复商品图片路径...'); + + // 初始化数据库连接 + await initDB(); + const db = getDB(); + + // 查询所有包含错误路径的商品 + const [products] = await db.execute( + "SELECT id, image_url FROM products WHERE image_url LIKE '/uploads/product/%'" + ); + + console.log(`找到 ${products.length} 个需要修复的商品图片路径`); + + if (products.length === 0) { + console.log('没有需要修复的图片路径'); + return; + } + + // 开始事务 + await db.query('START TRANSACTION'); + + let updatedCount = 0; + + for (const product of products) { + const oldPath = product.image_url; + const newPath = oldPath.replace('/uploads/product/', '/uploads/products/'); + + console.log(`修复商品 ID ${product.id}: ${oldPath} -> ${newPath}`); + + await db.execute( + 'UPDATE products SET image_url = ? WHERE id = ?', + [newPath, product.id] + ); + + updatedCount++; + } + + // 提交事务 + await db.query('COMMIT'); + + console.log(`成功修复 ${updatedCount} 个商品的图片路径`); + + // 验证修复结果 + const [remainingProducts] = await db.execute( + "SELECT COUNT(*) as count FROM products WHERE image_url LIKE '/uploads/product/%'" + ); + + if (remainingProducts[0].count === 0) { + console.log('所有图片路径修复完成!'); + } else { + console.log(`还有 ${remainingProducts[0].count} 个图片路径未修复`); + } + + } catch (error) { + console.error('修复图片路径时发生错误:', error); + + // 回滚事务 + try { + const db = getDB(); + await db.query('ROLLBACK'); + console.log('已回滚数据库事务'); + } catch (rollbackError) { + console.error('回滚事务失败:', rollbackError); + } + } +} + +// 如果直接运行此脚本 +if (require.main === module) { + fixImagePaths() + .then(() => { + console.log('修复脚本执行完成'); + process.exit(0); + }) + .catch((error) => { + console.error('修复脚本执行失败:', error); + process.exit(1); + }); +} + +module.exports = { fixImagePaths }; \ No newline at end of file diff --git a/frontend b/frontend new file mode 160000 index 0000000..9bf9543 --- /dev/null +++ b/frontend @@ -0,0 +1 @@ +Subproject commit 9bf9543d0b9f3f9ddb6cdc166931759e1f72d8aa diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..800cd1d --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,52 @@ +const jwt = require('jsonwebtoken'); +const { getDB } = require('../database'); + +const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; // 在生产环境中应该使用环境变量 + +/** + * 用户认证中间件 + * 验证JWT令牌并检查用户状态(包括是否被拉黑) + */ +const auth = async (req, res, next) => { + try { + const token = req.header('Authorization')?.replace('Bearer ', ''); + + if (!token) { + return res.status(401).json({ success: false, message: '未提供认证令牌' }); + } + + const decoded = jwt.verify(token, JWT_SECRET); + const db = getDB(); + const [users] = await db.execute('SELECT * FROM users WHERE id = ?', [decoded.userId]); + + if (users.length === 0) { + return res.status(401).json({ success: false, message: '用户不存在' }); + } + + const user = users[0]; + + // 检查用户是否被拉黑 + if (user.is_blacklisted) { + return res.status(403).json({ + success: false, + message: '账户已被拉黑,请联系管理员', + code: 'USER_BLACKLISTED' + }); + } + + req.user = user; + next(); + } catch (error) { + res.status(401).json({ success: false, message: '无效的认证令牌' }); + } +}; + +// 管理员认证中间件 +const adminAuth = (req, res, next) => { + if (req.user.role !== 'admin') { + return res.status(403).json({ success: false, message: '需要管理员权限' }); + } + next(); +}; + +module.exports = { auth, adminAuth, JWT_SECRET }; \ No newline at end of file diff --git a/middleware/errorHandler.js b/middleware/errorHandler.js new file mode 100644 index 0000000..eb26f22 --- /dev/null +++ b/middleware/errorHandler.js @@ -0,0 +1,129 @@ +const { logger } = require('../config/logger'); +const { ERROR_CODES, HTTP_STATUS } = require('../config/constants'); + +// 全局错误处理中间件 +const errorHandler = (err, req, res, next) => { + let error = { ...err }; + error.message = err.message; + + // 记录错误日志 + logger.error('Error occurred:', { + message: err.message, + stack: err.stack, + url: req.originalUrl, + method: req.method, + ip: req.ip, + userAgent: req.get('User-Agent'), + userId: req.user?.id + }); + + // MySQL错误处理 + if (err.code) { + switch (err.code) { + case 'ER_DUP_ENTRY': + error.message = '数据已存在'; + error.statusCode = HTTP_STATUS.CONFLICT; + error.errorCode = ERROR_CODES.DUPLICATE_ENTRY; + break; + case 'ER_NO_REFERENCED_ROW_2': + error.message = '关联数据不存在'; + error.statusCode = HTTP_STATUS.BAD_REQUEST; + error.errorCode = ERROR_CODES.VALIDATION_ERROR; + break; + case 'ER_ROW_IS_REFERENCED_2': + error.message = '数据正在被使用,无法删除'; + error.statusCode = HTTP_STATUS.CONFLICT; + error.errorCode = ERROR_CODES.VALIDATION_ERROR; + break; + case 'ECONNREFUSED': + error.message = '数据库连接失败'; + error.statusCode = HTTP_STATUS.INTERNAL_SERVER_ERROR; + error.errorCode = ERROR_CODES.DATABASE_ERROR; + break; + default: + error.message = '数据库操作失败'; + error.statusCode = HTTP_STATUS.INTERNAL_SERVER_ERROR; + error.errorCode = ERROR_CODES.DATABASE_ERROR; + } + } + + // JWT错误处理 + if (err.name === 'JsonWebTokenError') { + error.message = '无效的访问令牌'; + error.statusCode = HTTP_STATUS.UNAUTHORIZED; + error.errorCode = ERROR_CODES.AUTHENTICATION_ERROR; + } + + if (err.name === 'TokenExpiredError') { + error.message = '访问令牌已过期'; + error.statusCode = HTTP_STATUS.UNAUTHORIZED; + error.errorCode = ERROR_CODES.AUTHENTICATION_ERROR; + } + + // 参数验证错误 + if (err.name === 'ValidationError' || err.isJoi) { + const message = err.details ? err.details.map(detail => detail.message).join(', ') : err.message; + error.message = `参数验证失败: ${message}`; + error.statusCode = HTTP_STATUS.BAD_REQUEST; + error.errorCode = ERROR_CODES.VALIDATION_ERROR; + } + + // 业务逻辑错误处理 + if (err.message === '余额不足') { + error.message = '用户积分余额不足,无法完成转账操作。请先为用户充值积分或选择其他用户。'; + error.statusCode = HTTP_STATUS.BAD_REQUEST; + error.errorCode = ERROR_CODES.VALIDATION_ERROR; + } + + if (err.message === '用户不存在') { + error.message = '指定的用户不存在,请检查用户信息后重试。'; + error.statusCode = HTTP_STATUS.BAD_REQUEST; + error.errorCode = ERROR_CODES.VALIDATION_ERROR; + } + + // 自定义错误 + if (err.statusCode) { + error.statusCode = err.statusCode; + error.errorCode = err.errorCode || ERROR_CODES.INTERNAL_ERROR; + } + + // 默认错误 + const statusCode = error.statusCode || HTTP_STATUS.INTERNAL_SERVER_ERROR; + const errorCode = error.errorCode || ERROR_CODES.INTERNAL_ERROR; + const message = error.message || '服务器内部错误'; + + res.status(statusCode).json({ + success: false, + error: { + code: errorCode, + message: message + }, + ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) + }); +}; + +// 404错误处理 +const notFound = (req, res, next) => { + const error = new Error(`路径 ${req.originalUrl} 未找到`); + error.statusCode = HTTP_STATUS.NOT_FOUND; + error.errorCode = ERROR_CODES.NOT_FOUND; + next(error); +}; + +// 自定义错误类 +class AppError extends Error { + constructor(message, statusCode, errorCode) { + super(message); + this.statusCode = statusCode; + this.errorCode = errorCode; + this.isOperational = true; + + Error.captureStackTrace(this, this.constructor); + } +} + +module.exports = { + errorHandler, + notFound, + AppError +}; \ No newline at end of file diff --git a/middleware/validation.js b/middleware/validation.js new file mode 100644 index 0000000..41e97f1 --- /dev/null +++ b/middleware/validation.js @@ -0,0 +1,230 @@ +const Joi = require('joi'); +const { AppError } = require('./errorHandler'); +const { ERROR_CODES, HTTP_STATUS } = require('../config/constants'); + +// 验证中间件工厂函数 +const validate = (schema) => { + return (req, res, next) => { + const { error } = schema.validate(req.body, { abortEarly: false }); + if (error) { + const errorMessage = error.details.map(detail => detail.message).join(', '); + return next(new AppError(errorMessage, HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR)); + } + next(); + }; +}; + +// 查询参数验证中间件 +const validateQuery = (schema) => { + return (req, res, next) => { + const { error } = schema.validate(req.query, { abortEarly: false }); + if (error) { + const errorMessage = error.details.map(detail => detail.message).join(', '); + return next(new AppError(errorMessage, HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR)); + } + next(); + }; +}; + +// 路径参数验证中间件 +const validateParams = (schema) => { + return (req, res, next) => { + const { error } = schema.validate(req.params, { abortEarly: false }); + if (error) { + const errorMessage = error.details.map(detail => detail.message).join(', '); + return next(new AppError(errorMessage, HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR)); + } + next(); + }; +}; + +// 通用验证规则 +const commonSchemas = { + // ID验证 + id: Joi.number().integer().positive().required().messages({ + 'number.base': 'ID必须是数字', + 'number.integer': 'ID必须是整数', + 'number.positive': 'ID必须是正数', + 'any.required': 'ID是必需的' + }), + + // 分页验证 + pagination: Joi.object({ + page: Joi.number().integer().min(1).default(1).messages({ + 'number.base': '页码必须是数字', + 'number.integer': '页码必须是整数', + 'number.min': '页码必须大于0' + }), + limit: Joi.number().integer().min(1).max(100).default(10).messages({ + 'number.base': '每页数量必须是数字', + 'number.integer': '每页数量必须是整数', + 'number.min': '每页数量必须大于0', + 'number.max': '每页数量不能超过100' + }) + }) +}; + +// 用户相关验证规则 +const userSchemas = { + // 用户注册 + register: Joi.object({ + username: Joi.string().alphanum().min(3).max(30).required().messages({ + 'string.base': '用户名必须是字符串', + 'string.alphanum': '用户名只能包含字母和数字', + 'string.min': '用户名至少3个字符', + 'string.max': '用户名最多30个字符', + 'any.required': '用户名是必需的' + }), + password: Joi.string().min(6).max(128).required().messages({ + 'string.base': '密码必须是字符串', + 'string.min': '密码至少6个字符', + 'string.max': '密码最多128个字符', + 'any.required': '密码是必需的' + }), + phone: Joi.string().pattern(/^1[3-9]\d{9}$/).required().messages({ + 'string.pattern.base': '手机号格式不正确', + 'any.required': '手机号是必需的' + }), + // 可选字段,注册时不需要填写 + real_name: Joi.string().max(50).allow('').optional().messages({ + 'string.max': '真实姓名最多50个字符' + }), + role: Joi.string().valid('admin', 'user').default('user').messages({ + 'any.only': '角色只能是admin或user' + }) + }), + + // 用户登录 + login: Joi.object({ + username: Joi.string().required().messages({ + 'any.required': '用户名是必需的' + }), + password: Joi.string().required().messages({ + 'any.required': '密码是必需的' + }) + }) +}; + +// 转账相关验证规则 +const transferSchemas = { + // 转账查询参数 + query: Joi.object({ + page: Joi.number().integer().min(1).default(1).messages({ + 'number.base': '页码必须是数字', + 'number.integer': '页码必须是整数', + 'number.min': '页码必须大于0' + }), + limit: Joi.number().integer().min(1).max(100).default(10).messages({ + 'number.base': '每页数量必须是数字', + 'number.integer': '每页数量必须是整数', + 'number.min': '每页数量必须大于0', + 'number.max': '每页数量不能超过100' + }), + status: Joi.string().valid('pending', 'confirmed', 'rejected', 'cancelled').allow('').messages({ + 'any.only': '状态值无效' + }), + type: Joi.string().valid('user_to_user', 'system_to_user', 'user_to_system').allow('').messages({ + 'any.only': '转账类型无效' + }), + search: Joi.string().allow('').max(100).messages({ + 'string.max': '搜索关键词最多100个字符' + }), + transfer_type: Joi.string().valid('user_to_user', 'system_to_user', 'user_to_system').allow('').messages({ + 'any.only': '转账类型无效' + }), + start_date: Joi.date().iso().allow('').messages({ + 'date.format': '开始日期格式不正确' + }), + end_date: Joi.date().iso().allow('').messages({ + 'date.format': '结束日期格式不正确' + }), + sort: Joi.string().valid('id', 'amount', 'created_at', 'updated_at', 'status').allow('').messages({ + 'any.only': '排序字段无效,只支持: id, amount, created_at, updated_at, status' + }), + order: Joi.string().valid('asc', 'desc').allow('').messages({ + 'any.only': '排序方向无效,只支持: asc, desc' + }), + // 优先显示待处理转账参数 + show_pending: Joi.alternatives().try( + Joi.boolean(), + Joi.string().valid('true', 'false', '') + ).allow('').messages({ + 'alternatives.match': 'show_pending参数只能是布尔值或字符串true/false' + }) + }), + + // 创建转账 + create: Joi.object({ + to_user_id: Joi.number().integer().positive().required().messages({ + 'number.base': '收款用户ID必须是数字', + 'number.integer': '收款用户ID必须是整数', + 'number.positive': '收款用户ID必须是正数', + 'any.required': '收款用户ID是必需的' + }), + amount: Joi.number().positive().precision(2).required().messages({ + 'number.base': '金额必须是数字', + 'number.positive': '金额必须是正数', + 'any.required': '金额是必需的' + }), + transfer_type: Joi.string().valid('user_to_user', 'system_to_user', 'user_to_system').required().messages({ + 'any.only': '转账类型无效', + 'any.required': '转账类型是必需的' + }), + description: Joi.string().max(500).allow('').messages({ + 'string.max': '描述最多500个字符' + }), + voucher_url: Joi.string().uri().allow('').messages({ + 'string.uri': '凭证URL格式不正确' + }) + }), + + // 确认转账 + confirm: Joi.object({ + transfer_id: Joi.number().integer().positive().required().messages({ + 'number.base': '转账ID必须是数字', + 'number.integer': '转账ID必须是整数', + 'number.positive': '转账ID必须是正数', + 'any.required': '转账ID是必需的' + }), + note: Joi.string().max(500).allow('').messages({ + 'string.max': '备注最多500个字符' + }) + }), + + // 拒绝转账 + reject: Joi.object({ + transfer_id: Joi.number().integer().positive().required().messages({ + 'number.base': '转账ID必须是数字', + 'number.integer': '转账ID必须是整数', + 'number.positive': '转账ID必须是正数', + 'any.required': '转账ID是必需的' + }), + note: Joi.string().max(500).allow('').messages({ + 'string.max': '备注最多500个字符' + }) + }) +}; +// 系统设置相关验证规则 +const systemSchemas = { + updateSettings: Joi.object({ + site_name: Joi.string().max(100).optional(), + site_description: Joi.string().max(500).optional(), + + contact_phone: Joi.string().max(20).optional(), + maintenance_mode: Joi.boolean().optional(), + max_transfer_amount: Joi.number().positive().optional(), + min_transfer_amount: Joi.number().positive().optional(), + transfer_fee_rate: Joi.number().min(0).max(1).optional() + }) +}; + +// 导出所有验证规则 +module.exports = { + validate, + validateQuery, + validateParams, + commonSchemas, + userSchemas, + transferSchemas, + systemSchemas +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b8949d6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3024 @@ +{ + "name": "integrated-system", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "integrated-system", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@alicloud/dysmsapi20170525": "^4.1.2", + "@alicloud/openapi-client": "^0.4.15", + "axios": "^1.11.0", + "bcryptjs": "^2.4.3", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "dayjs": "^1.11.13", + "dotenv": "^17.2.1", + "express": "^4.18.2", + "express-rate-limit": "^7.1.5", + "express-validator": "^7.2.1", + "helmet": "^8.1.0", + "joi": "^17.13.3", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "mysql2": "^3.14.3", + "node-cron": "^4.2.1", + "qrcode": "^1.5.4", + "winston": "^3.17.0" + }, + "devDependencies": { + "concurrently": "^8.2.2", + "nodemon": "^3.0.2" + } + }, + "node_modules/@alicloud/credentials": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@alicloud/credentials/-/credentials-2.4.4.tgz", + "integrity": "sha512-/eRAGSKcniLIFQ1UCpDhB/IrHUZisQ1sc65ws/c2avxUMpXwH1rWAohb76SVAUJhiF4mwvLzLJM1Mn1XL4Xe/Q==", + "license": "MIT", + "dependencies": { + "@alicloud/tea-typescript": "^1.8.0", + "httpx": "^2.3.3", + "ini": "^1.3.5", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/darabonba-array": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@alicloud/darabonba-array/-/darabonba-array-0.1.1.tgz", + "integrity": "sha512-UPP7p9//jywqM8EN6BjSbw1ovl/BzqreXdi5FmxT6m3PmFxsxabe+yamjeopyf2Gi0p3WqwJTBCeNji5eYUsJw==", + "license": "ISC", + "dependencies": { + "@alicloud/tea-typescript": "^1.7.1" + } + }, + "node_modules/@alicloud/darabonba-encode-util": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@alicloud/darabonba-encode-util/-/darabonba-encode-util-0.0.2.tgz", + "integrity": "sha512-mlsNctkeqmR0RtgE1Rngyeadi5snLOAHBCWEtYf68d7tyKskosXDTNeZ6VCD/UfrUu4N51ItO8zlpfXiOgeg3A==", + "license": "ISC", + "dependencies": { + "moment": "^2.29.1" + } + }, + "node_modules/@alicloud/darabonba-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@alicloud/darabonba-map/-/darabonba-map-0.0.1.tgz", + "integrity": "sha512-2ep+G3YDvuI+dRYVlmER1LVUQDhf9kEItmVB/bbEu1pgKzelcocCwAc79XZQjTcQGFgjDycf3vH87WLDGLFMlw==", + "license": "ISC", + "dependencies": { + "@alicloud/tea-typescript": "^1.7.1" + } + }, + "node_modules/@alicloud/darabonba-signature-util": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@alicloud/darabonba-signature-util/-/darabonba-signature-util-0.0.4.tgz", + "integrity": "sha512-I1TtwtAnzLamgqnAaOkN0IGjwkiti//0a7/auyVThdqiC/3kyafSAn6znysWOmzub4mrzac2WiqblZKFcN5NWg==", + "license": "ISC", + "dependencies": { + "@alicloud/darabonba-encode-util": "^0.0.1" + } + }, + "node_modules/@alicloud/darabonba-signature-util/node_modules/@alicloud/darabonba-encode-util": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@alicloud/darabonba-encode-util/-/darabonba-encode-util-0.0.1.tgz", + "integrity": "sha512-Sl5vCRVAYMqwmvXpJLM9hYoCHOMsQlGxaWSGhGWulpKk/NaUBArtoO1B0yHruJf1C5uHhEJIaylYcM48icFHgw==", + "license": "ISC", + "dependencies": { + "@alicloud/tea-typescript": "^1.7.1", + "moment": "^2.29.1" + } + }, + "node_modules/@alicloud/darabonba-string": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@alicloud/darabonba-string/-/darabonba-string-1.0.3.tgz", + "integrity": "sha512-NyWwrU8cAIesWk3uHL1Q7pTDTqLkCI/0PmJXC4/4A0MFNAZ9Ouq0iFBsRqvfyUujSSM+WhYLuTfakQXiVLkTMA==", + "license": "Apache-2.0", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1" + } + }, + "node_modules/@alicloud/dysmsapi20170525": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@alicloud/dysmsapi20170525/-/dysmsapi20170525-4.1.2.tgz", + "integrity": "sha512-Wxg+wQjpBGmXCvmIf9QE0mBv9dcGI0q13NxzF48akLYjSf/Mpk7jbnYttqEzNZPpRMShi1wViANwo8q+WkvYfQ==", + "license": "Apache-2.0", + "dependencies": { + "@alicloud/openapi-core": "^1.0.0", + "@darabonba/typescript": "^1.0.0" + } + }, + "node_modules/@alicloud/endpoint-util": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@alicloud/endpoint-util/-/endpoint-util-0.0.1.tgz", + "integrity": "sha512-+pH7/KEXup84cHzIL6UJAaPqETvln4yXlD9JzlrqioyCSaWxbug5FUobsiI6fuUOpw5WwoB3fWAtGbFnJ1K3Yg==", + "license": "Apache-2.0", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/gateway-pop": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@alicloud/gateway-pop/-/gateway-pop-0.0.6.tgz", + "integrity": "sha512-KF4I+JvfYuLKc3fWeWYIZ7lOVJ9jRW0sQXdXidZn1DKZ978ncfGf7i0LBfONGk4OxvNb/HD3/0yYhkgZgPbKtA==", + "license": "ISC", + "dependencies": { + "@alicloud/credentials": "^2", + "@alicloud/darabonba-array": "^0.1.0", + "@alicloud/darabonba-encode-util": "^0.0.2", + "@alicloud/darabonba-map": "^0.0.1", + "@alicloud/darabonba-signature-util": "^0.0.4", + "@alicloud/darabonba-string": "^1.0.2", + "@alicloud/endpoint-util": "^0.0.1", + "@alicloud/gateway-spi": "^0.0.8", + "@alicloud/openapi-util": "^0.3.2", + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "^1.4.8" + } + }, + "node_modules/@alicloud/gateway-spi": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@alicloud/gateway-spi/-/gateway-spi-0.0.8.tgz", + "integrity": "sha512-KM7fu5asjxZPmrz9sJGHJeSU+cNQNOxW+SFmgmAIrITui5hXL2LB+KNRuzWmlwPjnuA2X3/keq9h6++S9jcV5g==", + "license": "ISC", + "dependencies": { + "@alicloud/credentials": "^2", + "@alicloud/tea-typescript": "^1.7.1" + } + }, + "node_modules/@alicloud/openapi-client": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/@alicloud/openapi-client/-/openapi-client-0.4.15.tgz", + "integrity": "sha512-4VE0/k5ZdQbAhOSTqniVhuX1k5DUeUMZv74degn3wIWjLY6Bq+hxjaGsaHYlLZ2gA5wUrs8NcI5TE+lIQS3iiA==", + "license": "ISC", + "dependencies": { + "@alicloud/credentials": "^2.4.2", + "@alicloud/gateway-spi": "^0.0.8", + "@alicloud/openapi-util": "^0.3.2", + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "1.4.9", + "@alicloud/tea-xml": "0.0.3" + } + }, + "node_modules/@alicloud/openapi-core": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@alicloud/openapi-core/-/openapi-core-1.0.4.tgz", + "integrity": "sha512-e9WK1lKiMOOziuLgNaYWv7FL50FyrcpO+idoLhNmFR7k0Fax4lPht5suBpTBr1PSINg5R1W3eOCm5vaUTrY4lg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "@alicloud/credentials": "latest", + "@alicloud/gateway-pop": "0.0.6", + "@alicloud/gateway-spi": "^0.0.8", + "@darabonba/typescript": "^1.0.2" + } + }, + "node_modules/@alicloud/openapi-util": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@alicloud/openapi-util/-/openapi-util-0.3.2.tgz", + "integrity": "sha512-EC2JvxdcOgMlBAEG0+joOh2IB1um8CPz9EdYuRfTfd1uP8Yc9D8QRUWVGjP6scnj6fWSOaHFlit9H6PrJSyFow==", + "license": "ISC", + "dependencies": { + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "^1.3.0", + "kitx": "^2.1.0", + "sm3": "^1.0.3" + } + }, + "node_modules/@alicloud/tea-typescript": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@alicloud/tea-typescript/-/tea-typescript-1.8.0.tgz", + "integrity": "sha512-CWXWaquauJf0sW30mgJRVu9aaXyBth5uMBCUc+5vKTK1zlgf3hIqRUjJZbjlwHwQ5y9anwcu18r48nOZb7l2QQ==", + "license": "ISC", + "dependencies": { + "@types/node": "^12.0.2", + "httpx": "^2.2.6" + } + }, + "node_modules/@alicloud/tea-util": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@alicloud/tea-util/-/tea-util-1.4.9.tgz", + "integrity": "sha512-S0wz76rGtoPKskQtRTGqeuqBHFj8BqUn0Vh+glXKun2/9UpaaaWmuJwcmtImk6bJZfLYEShDF/kxDmDJoNYiTw==", + "license": "Apache-2.0", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/tea-xml": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@alicloud/tea-xml/-/tea-xml-0.0.3.tgz", + "integrity": "sha512-+/9GliugjrLglsXVrd1D80EqqKgGpyA0eQ6+1ZdUOYCaRguaSwz44trX3PaxPu/HhIPJg9PsGQQ3cSLXWZjbAA==", + "license": "Apache-2.0", + "dependencies": { + "@alicloud/tea-typescript": "^1", + "@types/xml2js": "^0.4.5", + "xml2js": "^0.6.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@darabonba/typescript": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@darabonba/typescript/-/typescript-1.0.3.tgz", + "integrity": "sha512-/y2y6wf5TsxD7pCPIm0OvTC+5qV0Tk7HQYxwpIuWRLXQLB0CRDvr6qk4bR6rTLO/JglJa8z2uCGZsaLYpQNqFQ==", + "license": "Apache License 2.0", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "httpx": "^2.3.2", + "lodash": "^4.17.21", + "moment": "^2.30.1", + "moment-timezone": "^0.5.45", + "xml2js": "^0.6.2" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "license": "MIT" + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express-validator": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", + "integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.12.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/httpx": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/httpx/-/httpx-2.3.3.tgz", + "integrity": "sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw==", + "license": "MIT", + "dependencies": { + "@types/node": "^20", + "debug": "^4.1.1" + } + }, + "node_modules/httpx/node_modules/@types/node": { + "version": "20.19.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz", + "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/httpx/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/httpx/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kitx": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/kitx/-/kitx-2.2.0.tgz", + "integrity": "sha512-tBMwe6AALTBQJb0woQDD40734NKzb0Kzi3k7wQj9ar3AbP9oqhoVrdXPh7rk2r00/glIgd0YbToIUJsnxWMiIg==", + "license": "MIT", + "dependencies": { + "@types/node": "^22.5.4" + } + }, + "node_modules/kitx/node_modules/@types/node": { + "version": "22.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz", + "integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/mysql2": { + "version": "3.14.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.3.tgz", + "integrity": "sha512-fD6MLV8XJ1KiNFIF0bS7Msl8eZyhlTDCDl75ajU5SJtpdx9ZPEACulJcqJWr1Y8OYyxsFc4j3+nflpmhxCU5aQ==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-cron": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sm3": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sm3/-/sm3-1.0.3.tgz", + "integrity": "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g==", + "license": "MIT" + }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3969491 --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "name": "integrated-system", + "version": "1.0.0", + "description": "Vue3 + Node.js 集成系统", + "main": "server.js", + "scripts": { + "dev": "concurrently \"npm run dev:frontend\" \"npm run dev:admin\" \"npm run dev:server\"", + "dev:frontend": "cd frontend && npm run dev", + "dev:admin": "cd admin && npm run dev", + "dev:server": "nodemon server.js", + "build": "npm run build:frontend && npm run build:admin", + "build:frontend": "cd frontend && npm run build", + "build:admin": "cd admin && npm run build", + "start": "node server.js" + }, + "dependencies": { + "@alicloud/dysmsapi20170525": "^4.1.2", + "@alicloud/openapi-client": "^0.4.15", + "axios": "^1.11.0", + "bcryptjs": "^2.4.3", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "dayjs": "^1.11.13", + "dotenv": "^17.2.1", + "express": "^4.18.2", + "express-rate-limit": "^7.1.5", + "express-validator": "^7.2.1", + "helmet": "^8.1.0", + "joi": "^17.13.3", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "mysql2": "^3.14.3", + "node-cron": "^4.2.1", + "qrcode": "^1.5.4", + "winston": "^3.17.0" + }, + "devDependencies": { + "concurrently": "^8.2.2", + "nodemon": "^3.0.2" + }, + "keywords": [ + "vue3", + "nodejs", + "express", + "mysql", + "element-plus" + ], + "author": "", + "license": "MIT" +} diff --git a/routes/agent-withdrawals.js b/routes/agent-withdrawals.js new file mode 100644 index 0000000..7b4cd34 --- /dev/null +++ b/routes/agent-withdrawals.js @@ -0,0 +1,475 @@ +const express = require('express'); +const router = express.Router(); +const { getDB } = require('../database'); +const { auth } = require('../middleware/auth'); +const multer = require('multer'); +const path = require('path'); +const fs = require('fs'); + +// 配置multer用于文件上传 +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + const uploadDir = 'uploads/qr-codes'; + // 确保上传目录存在 + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + } + cb(null, uploadDir); + }, + filename: function (req, file, cb) { + // 生成唯一文件名 + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + cb(null, 'qr-code-' + uniqueSuffix + path.extname(file.originalname)); + } +}); + +// 文件过滤器 +const fileFilter = (req, file, cb) => { + // 只允许图片文件 + if (file.mimetype.startsWith('image/')) { + cb(null, true); + } else { + cb(new Error('只允许上传图片文件'), false); + } +}; + +const upload = multer({ + storage: storage, + fileFilter: fileFilter, + limits: { + fileSize: 5 * 1024 * 1024 // 限制文件大小为5MB + } +}); + +// 获取数据库连接 +const db = { + query: async (sql, params = []) => { + const connection = getDB(); + const [rows] = await connection.execute(sql, params); + return rows; + } +}; + +/** + * 检查用户是否为代理商 + */ +const requireAgent = async (req, res, next) => { + try { + const userId = req.user.id; + + // 查询用户是否为代理商 + const agentResult = await db.query( + 'SELECT * FROM regional_agents WHERE user_id = ? AND status = "active"', + [userId] + ); + + if (!agentResult || agentResult.length === 0) { + return res.status(403).json({ success: false, message: '您不是活跃的代理商' }); + } + + req.agent = agentResult[0]; + next(); + } catch (error) { + console.error('检查代理商身份失败:', error); + res.status(500).json({ success: false, message: '检查代理商身份失败' }); + } +}; + +/** + * 获取代理商佣金统计信息 + */ +router.get('/stats', auth, requireAgent, async (req, res) => { + try { + const agentId = req.agent.id; + + // 查询佣金统计 + const statsQuery = ` + SELECT + CAST(COALESCE(commission_sum.total_commission, 0) AS DECIMAL(10,2)) as total_commission, + CAST(COALESCE(ra.withdrawn_amount, 0) AS DECIMAL(10,2)) as withdrawn_amount, + CAST(COALESCE(ra.pending_withdrawal, 0) AS DECIMAL(10,2)) as pending_withdrawal, + CAST(COALESCE(commission_sum.total_commission, 0) - COALESCE(ra.withdrawn_amount, 0) - COALESCE(ra.pending_withdrawal, 0) AS DECIMAL(10,2)) as available_amount + FROM regional_agents ra + LEFT JOIN ( + SELECT agent_id, SUM(commission_amount) as total_commission + FROM agent_commission_records + WHERE agent_id = ? + GROUP BY agent_id + ) commission_sum ON ra.id = commission_sum.agent_id + WHERE ra.id = ? + `; + + const statsResult = await db.query(statsQuery, [agentId, agentId]); + const stats = statsResult && statsResult.length > 0 ? statsResult[0] : { + total_commission: 0, + withdrawn_amount: 0, + pending_withdrawal: 0, + available_amount: 0 + }; + + // 查询代理商信息包括收款方式 + const agentInfo = await db.query( + 'SELECT payment_type, bank_name, account_number, account_holder, qr_code_url, bank_account FROM regional_agents WHERE id = ?', + [agentId] + ); + + const agent = agentInfo[0] || {}; + + // 构建收款方式信息,兼容旧数据 + const paymentInfo = { + payment_type: agent.payment_type || 'bank', + bank_name: agent.bank_name || '', + account_number: agent.account_number || agent.bank_account, // 兼容旧字段 + account_holder: agent.account_holder, + qr_code_url: agent.qr_code_url || '' + }; + + // 兼容旧的bankInfo字段 + const bankInfo = { + bank_name: agent.bank_name || '', + bank_account: agent.bank_account || agent.account_number, + account_holder: agent.account_holder + }; + + res.json({ + success: true, + data: { + ...stats, + paymentInfo: paymentInfo, + bank_info: bankInfo // 保持向后兼容 + } + }); + } catch (error) { + console.error('获取佣金统计失败:', error); + res.status(500).json({ success: false, message: '获取佣金统计失败' }); + } +}); + +/** + * 更新收款方式信息 + */ +router.put('/payment-info', auth, requireAgent, async (req, res) => { + try { + const { payment_type, bank_name, account_number, account_holder, qr_code_url } = req.body; + const agentId = req.agent.id; + + // 验证收款方式类型 + const validPaymentTypes = ['bank', 'wechat', 'alipay', 'unionpay']; + if (!validPaymentTypes.includes(payment_type)) { + return res.status(400).json({ success: false, message: '收款方式类型不正确' }); + } + + // 根据收款方式类型进行不同的验证 + if (payment_type === 'bank') { + // 银行卡验证 + if (!bank_name || !account_number || !account_holder) { + return res.status(400).json({ success: false, message: '银行信息不完整' }); + } + // 验证银行账号格式(简单验证) + if (!/^\d{10,25}$/.test(account_number.replace(/\s/g, ''))) { + return res.status(400).json({ success: false, message: '银行账号格式不正确' }); + } + } else { + // 收款码验证 + if (!account_holder || !qr_code_url) { + return res.status(400).json({ success: false, message: '收款码信息不完整' }); + } + } + + // 更新收款方式信息 + await db.query( + 'UPDATE regional_agents SET payment_type = ?, bank_name = ?, account_number = ?, account_holder = ?, qr_code_url = ? WHERE id = ?', + [payment_type, bank_name, account_number, account_holder, qr_code_url, agentId] + ); + + res.json({ + success: true, + message: '收款方式信息更新成功' + }); + } catch (error) { + console.error('更新收款方式信息失败:', error); + res.status(500).json({ success: false, message: '更新收款方式信息失败' }); + } +}); + +/** + * 上传收款码图片 + */ +router.post('/upload-qr-code', auth, requireAgent, upload.single('qrCode'), async (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ success: false, message: '请选择要上传的图片' }); + } + + // 构建文件访问URL + const fileUrl = `/uploads/qr-codes/${req.file.filename}`; + + res.json({ + success: true, + message: '收款码上传成功', + data: { + url: fileUrl, + filename: req.file.filename + } + }); + } catch (error) { + console.error('上传收款码失败:', error); + res.status(500).json({ success: false, message: '上传收款码失败' }); + } +}); + +/** + * 兼容旧的银行信息接口 + */ +router.put('/bank-info', auth, requireAgent, async (req, res) => { + try { + const agentId = req.agent.id; + const { bank_name, bank_account, account_holder } = req.body; + + // 验证必填字段 + if (!bank_name || !bank_account || !account_holder) { + return res.status(400).json({ success: false, message: '银行信息不完整' }); + } + + // 验证银行账号格式(简单验证) + if (!/^\d{10,25}$/.test(bank_account)) { + return res.status(400).json({ success: false, message: '银行账号格式不正确' }); + } + + // 更新银行信息 + await db.query( + 'UPDATE regional_agents SET payment_type = "bank", bank_name = ?, account_number = ?, account_holder = ?, bank_account = ? WHERE id = ?', + [bank_name, bank_account, account_holder, bank_account, agentId] + ); + + res.json({ + success: true, + message: '银行信息更新成功' + }); + } catch (error) { + console.error('更新银行信息失败:', error); + res.status(500).json({ success: false, message: '更新银行信息失败' }); + } +}); + +/** + * 申请提现 + */ +router.post('/apply', auth, requireAgent, async (req, res) => { + try { + const agentId = req.agent.id; + const { amount, apply_note } = req.body; + + // 验证提现金额 + if (!amount || amount <= 0) { + return res.status(400).json({ success: false, message: '提现金额必须大于0' }); + } + + if (amount < 10) { + return res.status(400).json({ success: false, message: '最低提现金额为100元' }); + } + + // 查询代理商信息和可提现金额 + const agentQuery = ` + SELECT + ra.*, + CAST(COALESCE(SUM(acr.commission_amount), 0) - COALESCE(ra.withdrawn_amount, 0) - COALESCE(ra.pending_withdrawal, 0) AS DECIMAL(10,2)) as available_amount + FROM regional_agents ra + LEFT JOIN agent_commission_records acr ON ra.id = acr.agent_id + WHERE ra.id = ? + GROUP BY ra.id + `; + + const agentResult = await db.query(agentQuery, [agentId]); + + if (!agentResult || agentResult.length === 0) { + return res.status(404).json({ success: false, message: '代理商信息不存在' }); + } + + const agent = agentResult[0]; + + // 检查收款方式信息是否完整 + const paymentType = agent.payment_type || 'bank'; + + if (paymentType === 'bank') { + // 银行卡收款方式验证 + if (!agent.bank_name || !agent.account_number || !agent.account_holder) { + return res.status(400).json({ success: false, message: '请先完善银行信息' }); + } + } else { + // 收款码收款方式验证 + if (!agent.account_holder || !agent.qr_code_url) { + return res.status(400).json({ success: false, message: '请先完善收款码信息' }); + } + } + + // 检查可提现金额 + if (amount > agent.available_amount) { + return res.status(400).json({ + success: false, + message: `可提现金额不足,当前可提现:¥${agent.available_amount}` + }); + } + + // 开始事务 + const pool = getDB(); + const connection = await pool.getConnection(); + await connection.beginTransaction(); + + try { + // 创建提现申请 + await connection.execute( + 'INSERT INTO agent_withdrawals (agent_id, amount, payment_type, bank_name, account_number, account_holder, qr_code_url, apply_note, bank_account) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', + [agentId, amount, paymentType, agent.bank_name || '', agent.account_number, agent.account_holder, agent.qr_code_url, apply_note || null, agent.account_number] + ); + + // 更新代理的待提现金额 + await connection.execute( + 'UPDATE regional_agents SET pending_withdrawal = pending_withdrawal + ? WHERE id = ?', + [amount, agentId] + ); + + await connection.commit(); + connection.release(); // 释放连接回连接池 + + res.json({ + success: true, + message: '提现申请提交成功,请等待审核', + data: { + paymentType: paymentType + } + }); + } catch (error) { + await connection.rollback(); + connection.release(); // 释放连接回连接池 + throw error; + } + } catch (error) { + console.error('申请提现失败:', error); + res.status(500).json({ success: false, message: '申请提现失败' }); + } +}); + +/** + * 获取提现记录 + */ +router.get('/records', auth, requireAgent, async (req, res) => { + try { + const agentId = req.agent.id; + const { page = 1, limit = 20, status } = req.query; + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(limit) || 20; + const offset = (pageNum - 1) * limitNum; + + // 构建查询条件 + let whereConditions = ['agent_id = ?']; + let queryParams = [agentId]; + + if (status) { + whereConditions.push('status = ?'); + queryParams.push(status); + } + + const whereClause = whereConditions.join(' AND '); + + // 查询提现记录 + const recordsQuery = ` + SELECT + id, + amount, + payment_type, + bank_name, + account_number, + account_holder, + qr_code_url, + status, + apply_note, + admin_note, + created_at, + processed_at, + bank_account + FROM agent_withdrawals + WHERE ${whereClause} + ORDER BY created_at DESC + LIMIT ${limitNum} OFFSET ${offset} + `; + + const records = await db.query(recordsQuery, queryParams); + + // 处理记录数据,兼容旧格式 + const processedRecords = records.map(record => ({ + ...record, + payment_type: record.payment_type || 'bank', + account_number: record.account_number || record.bank_account, + qr_code_url: record.qr_code_url || '', + // 保持向后兼容 + bank_account: record.bank_account || record.account_number + })); + + // 查询总数 + const totalResult = await db.query( + `SELECT COUNT(*) as total FROM agent_withdrawals WHERE ${whereClause}`, + queryParams + ); + const total = totalResult && totalResult.length > 0 ? totalResult[0].total : 0; + + res.json({ + success: true, + data: { + records: processedRecords, + total: parseInt(total) + } + }); + } catch (error) { + console.error('获取提现记录失败:', error); + res.status(500).json({ success: false, message: '获取提现记录失败' }); + } +}); + +/** + * 获取佣金明细 + */ +router.get('/commissions', auth, requireAgent, async (req, res) => { + try { + const agentId = req.agent.id; + const { page = 1, limit = 20 } = req.query; + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(limit) || 20; + const offset = (pageNum - 1) * limitNum; + + // 查询佣金记录 + const commissionsQuery = ` + SELECT + acr.*, + u.real_name as merchant_name, + CONCAT(SUBSTRING(u.phone, 1, 3), '****', SUBSTRING(u.phone, -4)) as merchant_phone_masked + FROM agent_commission_records acr + JOIN users u ON acr.merchant_id = u.id + WHERE acr.agent_id = ? + ORDER BY acr.created_at DESC + LIMIT ${limitNum} OFFSET ${offset} + `; + + const commissions = await db.query(commissionsQuery, [agentId]); + + // 查询总数 + const totalResult = await db.query( + 'SELECT COUNT(*) as total FROM agent_commission_records WHERE agent_id = ?', + [agentId] + ); + const total = totalResult && totalResult.length > 0 ? totalResult[0].total : 0; + + res.json({ + success: true, + data: { + commissions, + total: parseInt(total) + } + }); + } 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.js b/routes/agents.js new file mode 100644 index 0000000..f40b9ff --- /dev/null +++ b/routes/agents.js @@ -0,0 +1,727 @@ +const express = require('express'); +const router = express.Router(); +const { getDB } = require('../database'); +const QRCode = require('qrcode'); +const crypto = require('crypto'); +const bcrypt = require('bcryptjs'); + +// 获取浙江省所有区域列表 +router.get('/regions', async (req, res) => { + try { + const [regions] = await getDB().execute( + 'SELECT * FROM zhejiang_regions ORDER BY city_name, district_name' + ); + res.json({ success: true, data: regions }); + } catch (error) { + console.error('获取区域列表失败:', error); + res.status(500).json({ success: false, message: '获取区域列表失败' }); + } +}); + +// 申请成为区域代理 +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: '请填写完整信息' }); + } + + // 检查该区域是否已有代理(包括所有状态,不仅仅是active) + const [existingRegionAgent] = await getDB().execute( + 'SELECT id, status FROM regional_agents WHERE region_id = ? AND status IN ("pending", "active")', + [region_id] + ); + + if (existingRegionAgent.length > 0) { + const status = existingRegionAgent[0].status; + if (status === 'active') { + return res.status(400).json({ success: false, message: '该区域已有激活的代理,每个区域只能有一个代理账号' }); + } else if (status === 'pending') { + return res.status(400).json({ success: false, message: '该区域已有待审核的代理申请,每个区域只能有一个代理账号' }); + } + } + + // 检查手机号是否已存在用户 + const [existingUser] = await getDB().execute( + 'SELECT id FROM users WHERE phone = ?', + [phone] + ); + + 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') { + return res.status(400).json({ success: false, message: '该用户已是其他区域的激活代理,一个用户只能申请一个区域的代理' }); + } else if (agentStatus === 'pending') { + return res.status(400).json({ success: false, message: '该用户已有待审核的代理申请,一个用户只能申请一个区域的代理' }); + } else if (agentStatus === 'suspended' || agentStatus === 'terminated') { + return res.status(400).json({ success: false, message: '该用户的代理资格已被暂停或终止,无法重新申请' }); + } + } + } else { + // 创建新用户(为代理申请用户生成临时密码) + 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] + ); + userId = userResult.insertId; + } + + // 生成代理编码 + const agentCode = 'AG' + Date.now().toString().slice(-8); + + // 创建代理申请 + await getDB().execute( + 'INSERT INTO regional_agents (user_id, region_id, agent_code, status, created_at) VALUES (?, ?, ?, "pending", NOW())', + [userId, region_id, agentCode] + ); + + res.json({ success: true, message: '申请提交成功,请等待审核' }); + } catch (error) { + console.error('申请代理失败:', error); + res.status(500).json({ success: false, message: '申请失败' }); + } +}); + +// 代理登录 +router.post('/login', async (req, res) => { + try { + const { phone, password } = req.body; + + if (!phone || !password) { + return res.status(400).json({ success: false, message: '请输入手机号和密码' }); + } + + // 先查询用户和代理信息(包含密码用于验证) + const [agents] = await getDB().execute( + `SELECT ra.*, u.id as user_id, u.username, u.phone, u.real_name, u.password, u.role, zr.city_name, zr.district_name + FROM regional_agents ra + JOIN users u ON ra.user_id = u.id + JOIN zhejiang_regions zr ON ra.region_id = zr.id + WHERE u.phone = ? AND ra.status = "active"`, + [phone] + ); + + if (agents.length === 0) { + return res.status(401).json({ success: false, message: '手机号或密码错误,或账户未激活' }); + } + + const agent = agents[0]; + + // 验证密码 + const isPasswordValid = await bcrypt.compare(password, agent.password); + if (!isPasswordValid) { + return res.status(401).json({ success: false, message: '手机号或密码错误,或账户未激活' }); + } + + // 生成JWT token + 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, + role: agent.role || 'agent', + agentId: agent.id + }, + JWT_SECRET, + { expiresIn: '24h' } + ); + + delete agent.password; // 不返回密码 + + res.json({ + success: true, + data: { + ...agent, + token + }, + message: '登录成功' + }); + } catch (error) { + console.error('代理登录失败:', error); + res.status(500).json({ success: false, message: '登录失败' }); + } +}); + +// 生成注册二维码 +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 { + const { agent_id } = req.params; + const { page = 1, limit = 10 } = req.query; + const offset = (page - 1) * limit; + + // 首先获取代理的注册时间 + const [agentInfo] = await getDB().execute( + `SELECT ra.created_at as agent_created_at, ra.region_id FROM regional_agents ra WHERE ra.id = ?`, + [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表中的和符合条件的早期商户) + const [merchants] = await getDB().execute( + `SELECT + u.id, + u.username, + u.phone, + u.real_name, + u.created_at, + u.audit_status, + IFNULL(am.created_at, '未关联') as joined_at, + CASE + WHEN u.created_at < ? AND u.district_id = ? THEN 1 + ELSE 0 + END as is_early_merchant, + CASE + WHEN u.created_at < ? AND u.district_id = ? THEN '早期商户(不记录佣金)' + ELSE '正常商户' + END as merchant_status, + (SELECT COUNT(*) FROM matching_orders WHERE initiator_id = u.id AND status = 'completed') as completed_matches, + (SELECT COUNT(*) FROM agent_commission_records WHERE merchant_id = u.id AND agent_id = ?) as commission_count + FROM users u + LEFT JOIN agent_merchants am ON u.id = am.merchant_id AND am.agent_id = ? + WHERE (am.agent_id = ? OR (u.created_at < ? AND u.district_id = ? AND u.role = 'user')) + ORDER BY u.created_at DESC + LIMIT ${parseInt(limit)} OFFSET ${parseInt(offset)}`, + [agentCreatedAt, parseInt(regionId), agentCreatedAt, parseInt(regionId), parseInt(agent_id), parseInt(agent_id), parseInt(agent_id), agentCreatedAt, parseInt(regionId)] + ); + + // 获取总数(包括代理关联的商户和符合条件的早期商户) + const [countResult] = await getDB().execute( + `SELECT COUNT(*) as total + FROM users u + LEFT JOIN agent_merchants am ON u.id = am.merchant_id AND am.agent_id = ? + 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( + `SELECT + COUNT(*) as early_merchant_count + FROM users u + WHERE u.created_at < ? + AND u.district_id = ? + AND u.role = 'user'`, + [agentCreatedAt, parseInt(regionId)] + ); + + // 获取正常商户统计(包括代理关联的商户,排除符合条件的早期商户) + const [normalMerchantStats] = await getDB().execute( + `SELECT + COUNT(*) as normal_merchant_count + FROM users u + LEFT JOIN agent_merchants am ON u.id = am.merchant_id AND am.agent_id = ? + WHERE (am.agent_id = ? AND (u.created_at >= ? OR u.district_id != ?))`, + [parseInt(agent_id), parseInt(agent_id), agentCreatedAt, parseInt(regionId)] + ); + + res.json({ + success: true, + data: { + merchants, + total: parseInt(countResult[0].total), + page: parseInt(page), + limit: parseInt(limit), + stats: { + total_merchants: parseInt(countResult[0].total), + early_merchants: parseInt(earlyMerchantStats[0].early_merchant_count), + normal_merchants: parseInt(normalMerchantStats[0].normal_merchant_count) + } + } + }); + } catch (error) { + console.error('获取商户列表失败:', error); + res.status(500).json({ success: false, message: '获取商户列表失败' }); + } +}); + +// 获取代理的佣金记录 +router.get('/commissions/:agent_id', async (req, res) => { + try { + const { agent_id } = req.params; + const { page = 1, limit = 10 } = req.query; + const offset = (page - 1) * limit; + + // 获取佣金记录 + const [commissions] = await getDB().execute( + `SELECT acr.*, u.username, u.real_name + FROM agent_commission_records acr + JOIN users u ON acr.merchant_id = u.id + WHERE acr.agent_id = ${parseInt(agent_id)} + ORDER BY acr.created_at DESC + LIMIT ${parseInt(limit)} OFFSET ${parseInt(offset)}` + ); + + // 获取总数和总佣金 + const [summary] = await getDB().execute( + `SELECT COUNT(*) as total_records, + COALESCE(SUM(commission_amount), 0) as total_commission + FROM agent_commission_records + WHERE agent_id = ${parseInt(agent_id)}` + ); + + // 由于agent_commission_records表没有status字段,设置默认值 + summary[0].paid_commission = summary[0].total_commission; + summary[0].pending_commission = 0; + + res.json({ + success: true, + data: { + commissions, + summary: summary[0], + page: parseInt(page), + limit: parseInt(limit) + } + }); + } catch (error) { + console.error('获取佣金记录失败:', error); + res.status(500).json({ success: false, message: '获取佣金记录失败' }); + } +}); + +// 获取代理统计信息 +router.get('/stats/:agent_id', async (req, res) => { + try { + const { agent_id } = req.params; + + // 获取统计数据 + const [stats] = await getDB().execute( + `SELECT + (SELECT COUNT(*) FROM agent_merchants WHERE agent_id = ${parseInt(agent_id)}) as total_merchants, + (SELECT COUNT(*) FROM agent_merchants am JOIN users u ON am.merchant_id = u.id WHERE am.agent_id = ${parseInt(agent_id)} AND u.audit_status = 'approved') as approved_merchants, + (SELECT COALESCE(SUM(commission_amount), 0) FROM agent_commission_records WHERE agent_id = ${parseInt(agent_id)}) as total_commission, + (SELECT COALESCE(SUM(commission_amount), 0) FROM agent_commission_records WHERE agent_id = ${parseInt(agent_id)}) as paid_commission, + (SELECT COUNT(*) FROM registration_codes rc JOIN regional_agents ra ON rc.agent_id = ra.user_id WHERE ra.id = ${parseInt(agent_id)} AND rc.is_used = 1) as used_codes, + (SELECT COUNT(*) FROM registration_codes rc JOIN regional_agents ra ON rc.agent_id = ra.user_id WHERE ra.id = ${parseInt(agent_id)} AND rc.is_used = 0 AND rc.expires_at > NOW()) as active_codes` + ); + + res.json({ success: true, data: stats[0] }); + } catch (error) { + console.error('获取统计信息失败:', error); + res.status(500).json({ success: false, message: '获取统计信息失败' }); + } +}); + +// 获取代理列表 +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, + zr.city_name, zr.district_name, zr.region_code + FROM regional_agents ra + JOIN users u ON ra.user_id = u.id + JOIN zhejiang_regions zr ON ra.region_id = zr.id + WHERE ${whereClause} + ORDER BY ra.created_at DESC + LIMIT ${limit} OFFSET ${offset}` + ); + + // 获取总数 + const [countResult] = await getDB().execute( + `SELECT COUNT(*) as total + FROM regional_agents ra + JOIN users u ON ra.user_id = u.id + 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: { + agents, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + totalPages + } + } + }); + } catch (error) { + console.error('获取代理列表失败:', error); + res.status(500).json({ success: false, message: '获取代理列表失败' }); + } +}); + +/** + * 获取代理佣金趋势数据 + * @route GET /agents/commission-trend/:agent_id + * @param {string} agent_id - 代理ID + * @param {string} period - 时间周期 (7d, 30d, 3m) + * @returns {Object} 佣金趋势数据 + */ +router.get('/commission-trend/:agent_id', async (req, res) => { + try { + const { agent_id } = req.params; + const { period = '7d' } = req.query; + + // 根据周期确定天数 + let days; + switch (period) { + case '7d': + days = 7; + break; + case '30d': + days = 30; + break; + case '3m': + days = 90; + break; + default: + days = 7; + } + + // 获取指定时间范围内的佣金趋势数据 + const [trendData] = await getDB().execute( + `SELECT + DATE(created_at) as date, + COALESCE(SUM(commission_amount), 0) as amount + FROM agent_commission_records + WHERE agent_id = ? + AND created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY) + GROUP BY DATE(created_at) + 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] : + item.date; + return itemDateStr === dateStr; + }); + + filledData.push({ + date: dateStr, + amount: existingData ? parseFloat(existingData.amount) : 0 + }); + } + + res.json({ + success: true, + data: filledData + }); + } catch (error) { + console.error('获取佣金趋势数据失败:', error); + res.status(500).json({ success: false, message: '获取佣金趋势数据失败' }); + } +}); + +/** + * 获取代理商户状态分布数据 + * @route GET /agents/merchant-status/:agent_id + * @param {string} agent_id - 代理ID + * @returns {Object} 商户状态分布数据 + */ +router.get('/merchant-status/:agent_id', async (req, res) => { + try { + const { agent_id } = req.params; + + // 获取商户状态分布 + const [statusData] = await getDB().execute( + `SELECT + CASE + WHEN u.audit_status = 'approved' THEN '已审核' + WHEN u.audit_status = 'pending' THEN '待审核' + WHEN u.audit_status = 'rejected' THEN '已拒绝' + ELSE '未知状态' + END as status, + COUNT(*) as count + FROM agent_merchants am + JOIN users u ON am.merchant_id = u.id + WHERE am.agent_id = ? + GROUP BY u.audit_status + ORDER BY count DESC`, + [parseInt(agent_id)] + ); + + res.json({ + success: true, + data: statusData + }); + } catch (error) { + console.error('获取商户状态分布数据失败:', error); + res.status(500).json({ success: false, message: '获取商户状态分布数据失败' }); + } +}); + +/** + * 获取代理详细统计数据(包含更多维度) + * @route GET /agents/detailed-stats/:agent_id + * @param {string} agent_id - 代理ID + * @returns {Object} 详细统计数据 + */ +router.get('/detailed-stats/:agent_id', async (req, res) => { + try { + const { agent_id } = req.params; + + // 获取基础统计数据 + const [basicStats] = await getDB().execute( + `SELECT + (SELECT COUNT(*) FROM agent_merchants WHERE agent_id = ?) as total_merchants, + (SELECT COUNT(*) FROM agent_merchants am JOIN users u ON am.merchant_id = u.id WHERE am.agent_id = ? AND u.audit_status = 'approved') as approved_merchants, + (SELECT COUNT(*) FROM agent_merchants am JOIN users u ON am.merchant_id = u.id WHERE am.agent_id = ? AND u.audit_status = 'pending') as pending_merchants, + (SELECT COUNT(*) FROM agent_merchants am JOIN users u ON am.merchant_id = u.id WHERE am.agent_id = ? AND u.audit_status = 'rejected') as rejected_merchants, + (SELECT COALESCE(SUM(commission_amount), 0) FROM agent_commission_records WHERE agent_id = ?) as total_commission, + (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 + COALESCE(SUM(commission_amount), 0) as monthly_commission, + COUNT(*) as monthly_commission_records + FROM agent_commission_records + WHERE agent_id = ? + AND YEAR(created_at) = YEAR(CURDATE()) + AND MONTH(created_at) = MONTH(CURDATE())`, + [parseInt(agent_id)] + ); + + // 获取今日佣金 + const [dailyStats] = await getDB().execute( + `SELECT + COALESCE(SUM(commission_amount), 0) as daily_commission, + COUNT(*) as daily_commission_records + FROM agent_commission_records + WHERE agent_id = ? + AND DATE(created_at) = CURDATE()`, + [parseInt(agent_id)] + ); + + // 获取最近7天新增商户数 + const [weeklyMerchants] = await getDB().execute( + `SELECT COUNT(*) as weekly_new_merchants + FROM agent_merchants am + JOIN users u ON am.merchant_id = u.id + WHERE am.agent_id = ? + AND am.created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)`, + [parseInt(agent_id)] + ); + + // 获取提现相关统计数据 + const [withdrawalStats] = await getDB().execute( + `SELECT + CAST(COALESCE(commission_sum.total_commission, 0) AS DECIMAL(10,2)) as total_commission_calc, + CAST(COALESCE(ra.withdrawn_amount, 0) AS DECIMAL(10,2)) as withdrawn_amount, + CAST(COALESCE(ra.pending_withdrawal, 0) AS DECIMAL(10,2)) as pending_withdrawal, + CAST(COALESCE(commission_sum.total_commission, 0) - COALESCE(ra.withdrawn_amount, 0) - COALESCE(ra.pending_withdrawal, 0) AS DECIMAL(10,2)) as available_amount + FROM regional_agents ra + LEFT JOIN ( + SELECT agent_id, SUM(commission_amount) as total_commission + FROM agent_commission_records + WHERE agent_id = ? + GROUP BY agent_id + ) commission_sum ON ra.id = commission_sum.agent_id + WHERE ra.id = ?`, + [parseInt(agent_id), parseInt(agent_id)] + ); + + // 合并所有统计数据 + const stats = { + ...basicStats[0], + ...monthlyStats[0], + ...dailyStats[0], + ...weeklyMerchants[0], + ...(withdrawalStats[0] || { + withdrawn_amount: 0, + pending_withdrawal: 0, + available_amount: 0 + }) + }; + + res.json({ + success: true, + data: stats + }); + } catch (error) { + console.error('获取详细统计数据失败:', error); + res.status(500).json({ success: false, message: '获取详细统计数据失败' }); + } +}); + +/** + * 获取代理商户的转账记录 + * @route GET /agents/merchants/:agent_id/transfers + * @param {string} agent_id - 代理ID + * @param {string} page - 页码 + * @param {string} limit - 每页数量 + * @returns {Object} 转账记录列表 + */ +router.get('/merchants/:agent_id/transfers', async (req, res) => { + try { + const { agent_id } = req.params; + const { page = 1, limit = 10 } = req.query; + 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 + t.id, + t.from_user_id, + t.to_user_id, + t.amount, + t.status, + t.transfer_type, + t.description, + t.created_at, + t.confirmed_at, + from_user.real_name as from_real_name, + from_user.phone as from_phone, + to_user.real_name as to_real_name, + to_user.phone as to_phone + FROM agent_merchants am + JOIN transfers t ON am.merchant_id = t.from_user_id + LEFT JOIN users from_user ON t.from_user_id = from_user.id + LEFT JOIN users to_user ON t.to_user_id = to_user.id + WHERE am.agent_id = ? + ORDER BY t.created_at DESC + LIMIT ${limitNum} OFFSET ${offset} + `; + const [transfers] = await getDB().execute(transferQuery, [parseInt(agent_id)]); + + // 查询总数 + const [totalResult] = await getDB().execute( + `SELECT COUNT(*) as total + FROM agent_merchants am + JOIN transfers t ON am.merchant_id = t.from_user_id + WHERE am.agent_id = ?`, + [parseInt(agent_id)] + ); + + const total = totalResult[0].total; + + res.json({ + success: true, + data: { + transfers, + 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/auth.js b/routes/auth.js new file mode 100644 index 0000000..2cdfe31 --- /dev/null +++ b/routes/auth.js @@ -0,0 +1,333 @@ +const express = require('express'); +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); +const { getDB } = require('../database'); + +const router = express.Router(); +const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; + +/** + * 用户注册 + * 需要提供有效的激活码才能注册 + */ +router.post('/register', async (req, res) => { + try { + const db = getDB(); + await db.query('START TRANSACTION'); + + const { + username, + phone, + password, + registrationCode, + city, + district_id: district, + captchaId, + captchaText, + smsCode, // 短信验证码 + role = 'user' + } = req.body; + + if (!username || !phone || !password || !registrationCode || !city || !district) { + return res.status(400).json({ success: false, message: '用户名、手机号、密码、激活码、城市和区域不能为空' }); + } + + if (!captchaId || !captchaText) { + return res.status(400).json({ success: false, message: '图形验证码不能为空' }); + } + + if (!smsCode) { + return res.status(400).json({ success: false, message: '短信验证码不能为空' }); + } + + // 注意:图形验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证 + + // 验证短信验证码 + const smsAPI = require('./sms'); + const smsValid = smsAPI.verifySMSCode(phone, smsCode); + if (!smsValid) { + return res.status(400).json({ success: false, message: '短信验证码错误或已过期' }); + } + + // 验证手机号格式 + const phoneRegex = /^1[3-9]\d{9}$/; + if (!phoneRegex.test(phone)) { + return res.status(400).json({ success: false, message: '手机号格式不正确' }); + } + + // 验证激活码 + const [registrationCodes] = await db.execute( + 'SELECT id, is_used, expires_at, agent_id FROM registration_codes WHERE code = ?', + [registrationCode] + ); + + if (registrationCodes.length === 0) { + return res.status(400).json({ success: false, message: '激活码不存在' }); + } + + const regCode = registrationCodes[0]; + + // 检查激活码是否已使用 + if (regCode.is_used) { + return res.status(400).json({ success: false, message: '激活码已被使用' }); + } + + // 检查激活码是否过期 + if (new Date() > new Date(regCode.expires_at)) { + return res.status(400).json({ success: false, message: '激活码已过期' }); + } + + // 检查用户是否已存在 + const [existingUsers] = await db.execute( + 'SELECT id FROM users WHERE username = ? OR phone = ?', + [username, phone] + ); + + if (existingUsers.length > 0) { + return res.status(400).json({ success: false, message: '用户名或手机号已存在' }); + } + + // 加密密码 + const hashedPassword = await bcrypt.hash(password, 10); + + // 创建用户(待审核状态,可以进入系统但匹配需审核) + const [result] = await db.execute( + 'INSERT INTO users (username, phone, password, role, points, audit_status, city, district_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', + [username, phone, hashedPassword, role, 0, 'pending', city, district] + ); + + const userId = result.insertId; + + // 用户余额已在创建用户时设置为默认值0.00,无需额外操作 + + // 标记激活码为已使用 + await db.execute( + 'UPDATE registration_codes SET is_used = TRUE, used_at = NOW(), used_by_user_id = ? WHERE id = ?', + [userId, regCode.id] + ); + + // 如果是代理邀请码,建立代理关系 + if (regCode.agent_id) { + // 验证agent_id是否存在于regional_agents表中 + const [agentExists] = await db.execute( + 'SELECT id FROM regional_agents WHERE id = ?', + [regCode.agent_id] + ); + + if (agentExists.length > 0) { + await db.execute( + 'INSERT INTO agent_merchants (agent_id, merchant_id, created_at) VALUES (?, ?, NOW())', + [regCode.agent_id, userId] + ); + } + } else { + // 如果不是代理邀请码,根据地区自动关联代理 + 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', + [district] + ); + + if (agents.length > 0) { + await db.execute( + 'INSERT INTO agent_merchants (agent_id, merchant_id, created_at) VALUES (?, ?, NOW())', + [agents[0].id, userId] + ); + } + } + + await db.query('COMMIT'); + + // 生成JWT token + const token = jwt.sign( + { userId: userId, username, role }, + JWT_SECRET, + { expiresIn: '24h' } + ); + + res.status(201).json({ + success: true, + message: '注册成功', + token, + user: { + id: userId, + username, + phone, + role, + points: 0, + audit_status: 'pending', + city, + district + } + }); + } catch (error) { + try { + await getDB().query('ROLLBACK'); + } catch (rollbackError) { + console.error('回滚错误:', rollbackError); + } + console.error('注册错误详情:', error); + console.error('错误堆栈:', error.stack); + res.status(500).json({ + success: false, + message: '注册失败', + error: process.env.NODE_ENV === 'development' ? error.message : undefined + }); + } +}); + +// 用户登录 +router.post('/login', async (req, res) => { + try { + const db = getDB(); + const { username, password, captchaId, captchaText } = req.body; + + if (!username || !password) { + return res.status(400).json({ success: false, message: '用户名和密码不能为空' }); + } + + // if (!captchaId || !captchaText) { + // return res.status(400).json({ success: false, message: '验证码不能为空' }); + // } + + // 注意:验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证 + + // 查找用户 + console.log('登录尝试 - 用户名:', username); + const [users] = await db.execute( + 'SELECT * FROM users WHERE username = ?', + [username] + ); + + console.log('查找到的用户数量:', users.length); + if (users.length === 0) { + console.log('用户不存在:', username); + return res.status(401).json({ success: false, message: '用户名或密码错误' }); + } + + const user = users[0]; + console.log('找到用户:', user.username, '密码长度:', user.password ? user.password.length : 'null'); + + // 验证密码 + console.log('验证密码 - 输入密码:', password, '数据库密码前10位:', user.password ? user.password.substring(0, 10) : 'null'); + const isValidPassword = await bcrypt.compare(password, user.password); + console.log('密码验证结果:', isValidPassword); + + if (!isValidPassword) { + console.log('密码验证失败'); + return res.status(401).json({ success: false, message: '用户名或密码错误' }); + } + + // 检查用户审核状态(管理员除外,只阻止被拒绝的用户) + if (user.role !== 'admin' && user.audit_status === 'rejected') { + return res.status(403).json({ success: false, message: '您的账户审核未通过,请联系管理员' }); + } + // 待审核用户可以正常登录使用系统,但匹配功能会有限制 + + // 生成JWT token + const token = jwt.sign( + { userId: user.id, username: user.username, role: user.role }, + JWT_SECRET, + { expiresIn: '24h' } + ); + + res.json({ + success: true, + message: '登录成功', + token, + user: { + id: user.id, + username: user.username, + role: user.role, + avatar: user.avatar, + points: user.points + } + }); + } catch (error) { + console.error('登录错误:', error); + res.status(500).json({ success: false, message: '登录失败' }); + } +}); + +// 验证token中间件 +const authenticateToken = (req, res, next) => { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + return res.status(401).json({ success: false, message: '访问令牌缺失' }); + } + + jwt.verify(token, JWT_SECRET, (err, user) => { + if (err) { + return res.status(403).json({ success: false, message: '访问令牌无效' }); + } + req.user = user; + next(); + }); +}; + +// 获取当前用户信息 +router.get('/me', authenticateToken, async (req, res) => { + try { + const db = getDB(); + const [users] = await db.execute( + 'SELECT id, username, role, avatar, points, created_at FROM users WHERE id = ?', + [req.user.userId] + ); + + if (users.length === 0) { + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + res.json({ success: true, user: users[0] }); + } catch (error) { + console.error('获取用户信息错误:', error); + res.status(500).json({ success: false, message: '获取用户信息失败' }); + } +}); + +// 修改密码 +router.put('/change-password', authenticateToken, async (req, res) => { + try { + const db = getDB(); + const { currentPassword, newPassword } = req.body; + + if (!currentPassword || !newPassword) { + return res.status(400).json({ success: false, message: '旧密码和新密码不能为空' }); + } + + // 获取用户当前密码 + const [users] = await db.execute( + 'SELECT password FROM users WHERE id = ?', + [req.user.userId] + ); + + if (users.length === 0) { + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + // 验证旧密码 + const isValidPassword = await bcrypt.compare(currentPassword, users[0].password); + + if (!isValidPassword) { + return res.status(400).json({ success: false, message: '旧密码错误' }); + } + + // 加密新密码 + const hashedNewPassword = await bcrypt.hash(newPassword, 10); + + // 更新密码 + await db.execute( + 'UPDATE users SET password = ? WHERE id = ?', + [hashedNewPassword, req.user.userId] + ); + + res.json({ success: true, message: '密码修改成功' }); + } catch (error) { + console.error('修改密码错误:', error); + res.status(500).json({ success: false, message: '修改密码失败' }); + } +}); + +module.exports = router; +module.exports.authenticateToken = authenticateToken; \ No newline at end of file diff --git a/routes/captcha.js b/routes/captcha.js new file mode 100644 index 0000000..4c6ac12 --- /dev/null +++ b/routes/captcha.js @@ -0,0 +1,225 @@ +const express = require('express'); +const crypto = require('crypto'); +const router = express.Router(); + +// 内存存储验证码(生产环境建议使用Redis) +const captchaStore = new Map(); + +/** + * 生成随机验证码字符串 + * @param {number} length 验证码长度 + * @returns {string} 验证码字符串 + */ +function generateCaptchaText(length = 4) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + let result = ''; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} + +/** + * 生成SVG验证码图片 + * @param {string} text 验证码文本 + * @returns {string} SVG字符串 + */ +function generateCaptchaSVG(text) { + const width = 120; + const height = 40; + const fontSize = 18; + + // 生成随机颜色 + const getRandomColor = () => { + const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8']; + return colors[Math.floor(Math.random() * colors.length)]; + }; + + // 生成干扰线 + const generateNoise = () => { + let noise = ''; + for (let i = 0; i < 3; i++) { + const x1 = Math.random() * width; + const y1 = Math.random() * height; + const x2 = Math.random() * width; + const y2 = Math.random() * height; + noise += ``; + } + return noise; + }; + + // 生成干扰点 + const generateDots = () => { + let dots = ''; + for (let i = 0; i < 20; i++) { + const x = Math.random() * width; + const y = Math.random() * height; + const r = Math.random() * 2 + 1; + dots += ``; + } + return dots; + }; + + // 生成文字 + let textElements = ''; + const charWidth = width / text.length; + + for (let i = 0; i < text.length; i++) { + const char = text[i]; + const x = charWidth * i + charWidth / 2; + const y = height / 2 + fontSize / 3; + const rotation = (Math.random() - 0.5) * 30; // 随机旋转角度 + const color = getRandomColor(); + + textElements += ` + + ${char} + `; + } + + const svg = ` + + + + + + + + + ${generateNoise()} + ${generateDots()} + ${textElements} + `; + + return svg; +} + +/** + * 生成验证码接口 + */ +router.get('/generate', (req, res) => { + try { + // 生成验证码文本 + const captchaText = generateCaptchaText(); + + // 生成唯一ID + const captchaId = crypto.randomUUID(); + + // 存储验证码(5分钟过期) + captchaStore.set(captchaId, { + text: captchaText.toLowerCase(), // 存储小写用于比较 + expires: Date.now() + 5 * 60 * 1000 // 5分钟过期 + }); + + // 生成SVG图片 + const svgImage = generateCaptchaSVG(captchaText); + + res.json({ + success: true, + data: { + captchaId, + image: `data:image/svg+xml;base64,${Buffer.from(svgImage).toString('base64')}` + } + }); + } catch (error) { + console.error('生成验证码失败:', error); + res.status(500).json({ + success: false, + message: '生成验证码失败' + }); + } +}); + +/** + * 验证验证码接口 + * @param {string} captchaId 验证码ID + * @param {string} captchaText 用户输入的验证码 + */ +router.post('/verify', (req, res) => { + try { + const { captchaId, captchaText } = req.body; + + if (!captchaId || !captchaText) { + return res.status(400).json({ + success: false, + message: '验证码ID和验证码不能为空' + }); + } + + // 获取存储的验证码 + const storedCaptcha = captchaStore.get(captchaId); + + if (!storedCaptcha) { + return res.status(400).json({ + success: false, + message: '验证码不存在或已过期' + }); + } + + // 检查是否过期 + if (Date.now() > storedCaptcha.expires) { + captchaStore.delete(captchaId); + return res.status(400).json({ + success: false, + message: '验证码已过期' + }); + } + + // 验证验证码(不区分大小写) + const isValid = storedCaptcha.text === captchaText.toLowerCase(); + + // 验证后删除验证码(无论成功失败) + captchaStore.delete(captchaId); + + if (isValid) { + res.json({ + success: true, + message: '验证码验证成功' + }); + } else { + res.status(400).json({ + success: false, + message: '验证码错误' + }); + } + } catch (error) { + console.error('验证验证码失败:', error); + res.status(500).json({ + success: false, + message: '验证验证码失败' + }); + } +}); + +// 清理过期验证码的定时任务 +setInterval(() => { + const now = Date.now(); + for (const [id, captcha] of captchaStore.entries()) { + if (now > captcha.expires) { + captchaStore.delete(id); + } + } +}, 60 * 1000); // 每分钟清理一次 + +// 导出验证函数供其他模块使用 +module.exports = router; +module.exports.verifyCaptcha = (captchaId, captchaText) => { + const captcha = captchaStore.get(captchaId); + if (!captcha) { + return false; // 验证码不存在或已过期 + } + + if (captcha.text.toLowerCase() !== captchaText.toLowerCase()) { + return false; // 验证码错误 + } + + // 验证成功后删除验证码(一次性使用) + captchaStore.delete(captchaId); + return true; +}; \ No newline at end of file diff --git a/routes/matching.js b/routes/matching.js new file mode 100644 index 0000000..ab40d08 --- /dev/null +++ b/routes/matching.js @@ -0,0 +1,375 @@ +const express = require('express'); +const router = express.Router(); +const { getDB } = require('../database'); +const matchingService = require('../services/matchingService'); +const { auth } = require('../middleware/auth'); + +// 创建匹配订单 +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; + + // 验证匹配类型 + if (!['small', 'large'].includes(matchingType)) { + return res.status(400).json({ message: '无效的匹配类型' }); + } + + // 验证大额匹配的金额 + if (matchingType === 'large') { + if (!customAmount || typeof customAmount !== 'number') { + return res.status(400).json({ message: '大额匹配需要指定金额' }); + } + if (customAmount < 5000 || customAmount > 50000) { + return res.status(400).json({ message: '大额匹配金额必须在5000-50000之间' }); + } + } + + // 检查用户是否有未完成的匹配订单(排除已失败的订单) + const [existingOrders] = await getDB().execute( + 'SELECT COUNT(*) as count FROM matching_orders WHERE initiator_id = ? AND status IN ("pending", "matching")', + [userId] + ); + + if (existingOrders[0].count > 0) { + return res.status(400).json({ message: '您有未完成的匹配订单,请等待完成后再创建新订单' }); + } + + // 校验用户是否已上传必要的证件和收款码 + const [userInfo] = await getDB().execute( + 'SELECT business_license, id_card_front, id_card_back, wechat_qr, alipay_qr, bank_card, unionpay_qr FROM users WHERE id = ?', + [userId] + ); + + if (userInfo.length === 0) { + return res.status(404).json({ message: '用户不存在' }); + } + + const user = userInfo[0]; + + // 检查证件是否已上传 + if (!user.business_license || !user.id_card_front || !user.id_card_back) { + return res.status(400).json({ + message: '开始匹配前,请先在个人中心上传营业执照和身份证正反面', + code: 'MISSING_DOCUMENTS' + }); + } + + // 检查收款码是否已上传(至少需要一种收款方式) + if (!user.wechat_qr && !user.alipay_qr && !user.bank_card && !user.unionpay_qr) { + return res.status(400).json({ + message: '开始匹配前,请先在个人中心设置至少一种收款方式(微信、支付宝、银行卡或云闪付)', + code: 'MISSING_PAYMENT_METHODS' + }); + } + + // 创建匹配订单 + const result = await matchingService.createMatchingOrder(userId, matchingType, customAmount); + + const message = matchingType === 'small' + ? '小额匹配成功!已为您生成3笔转账分配' + : `大额匹配成功!已为您生成${result.totalAmount}笔转账分配`; + + res.json({ + success: true, + message, + data: { + matchingOrderId: result.orderId, + amounts: result.amounts, + matchingType: result.matchingType, + totalAmount: result.totalAmount + } + }); + + } catch (error) { + console.error('创建匹配订单失败:', error); + res.status(500).json({ message: error.message || '匹配失败,请稍后重试' }); + } +}); + +// 获取用户的匹配订单列表 +router.get('/my-orders', auth, async (req, res) => { + try { + const userId = req.user.id; + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + + const orders = await matchingService.getUserMatchingOrders(userId, page, limit); + + res.json({ + success: true, + data: orders + }); + + } catch (error) { + console.error('获取匹配订单失败:', error); + res.status(500).json({ message: '获取匹配订单失败' }); + } +}); + +// 获取用户待处理的分配 +router.get('/pending-allocations', auth, async (req, res) => { + try { + const userId = req.user.id; + + const allocations = await matchingService.getUserPendingAllocations(userId); + + res.json({ + success: true, + data: allocations + }); + + } catch (error) { + console.error('获取待处理分配失败:', error); + res.status(500).json({ message: '获取待处理分配失败' }); + } +}); + +// 获取分配详情 +router.get('/allocation/:id', auth, async (req, res) => { + try { + const db = getDB(); + const allocationId = req.params.id; + const userId = req.user.id; + + // 首先获取分配信息 + const [allocations] = await db.execute(` + SELECT + oa.id, + oa.from_user_id, + oa.to_user_id, + oa.cycle_number, + oa.amount, + oa.status, + oa.created_at, + from_user.username as from_user_name, + to_user.username as to_user_name + FROM transfers oa + JOIN users from_user ON oa.from_user_id = from_user.id + JOIN users to_user ON oa.to_user_id = to_user.id + WHERE oa.id = ? + `, [allocationId]); + + if (allocations.length === 0) { + return res.status(404).json({ success: false, message: '分配不存在' }); + } + + const allocation = allocations[0]; + + // 检查权限:只有分配的发起人或接收人可以查看 + if (allocation.from_user_id !== userId && allocation.to_user_id !== userId) { + return res.status(403).json({ success: false, message: '无权限访问此分配' }); + } + + res.json({ + success: true, + data: allocation + }); + } catch (error) { + console.error('获取分配详情错误:', error); + res.status(500).json({ success: false, message: '获取分配详情失败' }); + } +}); + +// 确认分配(创建转账) +router.post('/confirm-allocation/:allocationId', auth, async (req, res) => { + try { + const { allocationId } = req.params; + const userId = req.user.id; + const { transferAmount, description, voucher } = req.body; // 获取转账信息 + + // 校验转账凭证是否存在 + if (!voucher) { + return res.status(400).json({ + success: false, + message: '请上传转账凭证' + }); + } + + // 调用服务层方法,传递完整的转账信息 + const transferId = await matchingService.confirmAllocation( + allocationId, + userId, + transferAmount, + description, + voucher + ); + + res.json({ + success: true, + message: '转账凭证已提交,转账记录已创建', + data: { transferId } + }); + + } catch (error) { + console.error('确认分配失败:', error); + res.status(500).json({ message: error.message || '确认分配失败' }); + } +}); + +// 拒绝分配 +router.post('/reject-allocation/:allocationId', auth, async (req, res) => { + try { + const { allocationId } = req.params; + const userId = req.user.id; + const { reason } = req.body; + + const db = getDB(); + + // 获取分配信息 + const [allocations] = await db.execute( + 'SELECT * FROM transfers WHERE id = ? AND from_user_id = ?', + [allocationId, userId] + ); + + if (allocations.length === 0) { + return res.status(404).json({ message: '分配不存在或无权限' }); + } + + const allocation = allocations[0]; + + // 更新分配状态 + await db.execute( + 'UPDATE transfers SET status = "rejected" WHERE id = ?', + [allocationId] + ); + + // 记录拒绝动作 + await db.execute( + 'INSERT INTO matching_records (matching_order_id, user_id, action, note) VALUES (?, ?, "reject", ?)', + [allocation.matching_order_id, userId, reason || '用户拒绝'] + ); + + // 检查订单状态是否需要更新 + const statusResult = await matchingService.checkOrderStatusAfterRejection( + allocation.matching_order_id, + allocation.cycle_number + ); + + let message = '已拒绝分配'; + if (statusResult === 'failed') { + message = '已拒绝分配,该轮次所有分配均被拒绝,匹配订单已失败'; + } + + res.json({ + success: true, + message + }); + + } catch (error) { + console.error('拒绝分配失败:', error); + res.status(500).json({ message: '拒绝分配失败' }); + } +}); + +// 获取匹配订单详情 +router.get('/order/:orderId', auth, async (req, res) => { + try { + const { orderId } = req.params; + const userId = req.user.id; + + const db = getDB(); + + // 获取订单基本信息 + const [orders] = await db.execute( + `SELECT mo.*, u.username as initiator_name,u.real_name as initiator_real_name + FROM matching_orders mo + JOIN users u ON mo.initiator_id = u.id + WHERE mo.id = ?`, + [orderId] + ); + + if (orders.length === 0) { + return res.status(404).json({ message: '匹配订单不存在' }); + } + + const order = orders[0]; + + // 检查权限(订单发起人或参与者) + const [userCheck] = await db.execute( + `SELECT COUNT(*) as count FROM ( + SELECT initiator_id as user_id FROM matching_orders WHERE id = ? + UNION + SELECT from_user_id as user_id FROM transfers WHERE id = ? + ) as participants WHERE user_id = ?`, + [orderId, orderId, userId] + ); + + if (userCheck[0].count === 0) { + return res.status(403).json({ message: '无权限查看此订单' }); + } + + // 获取分配信息 + const [allocations] = await db.execute( + `SELECT oa.*, + uf.username as from_user_name, + ut.username as to_user_name + FROM transfers oa + JOIN users uf ON oa.from_user_id = uf.id + JOIN users ut ON oa.to_user_id = ut.id + WHERE oa.id = ? + ORDER BY oa.cycle_number, oa.created_at`, + [orderId] + ); + + // 获取匹配记录 + const [records] = await db.execute( + `SELECT mr.*, u.username + FROM matching_records mr + JOIN users u ON mr.user_id = u.id + WHERE mr.matching_order_id = ? + ORDER BY mr.created_at`, + [orderId] + ); + + res.json({ + success: true, + data: { + order, + allocations, + records + } + }); + + } catch (error) { + console.error('获取匹配订单详情失败:', error); + res.status(500).json({ message: '获取匹配订单详情失败' }); + } +}); + +// 获取匹配统计信息 +router.get('/stats', auth, async (req, res) => { + try { + const userId = req.user.id; + const db = getDB(); + + // 获取用户统计 + const [userStats] = await db.execute( + `SELECT + COUNT(CASE WHEN mo.initiator_id = ? THEN 1 END) as initiated_orders, + COUNT(CASE WHEN oa.from_user_id = ? THEN 1 END) as participated_allocations, + SUM(CASE WHEN mo.initiator_id = ? AND mo.status = 'completed' THEN mo.amount ELSE 0 END) as total_initiated_amount, + SUM(CASE WHEN oa.from_user_id = ? AND oa.status = 'completed' THEN oa.amount ELSE 0 END) as total_participated_amount + FROM matching_orders mo + LEFT JOIN transfers oa ON mo.id = oa.id`, + [userId, userId, userId, userId] + ); + + res.json({ + success: true, + data: { + userStats: userStats[0] + } + }); + + } catch (error) { + console.error('获取匹配统计失败:', error); + res.status(500).json({ message: '获取匹配统计失败' }); + } +}); + + + +module.exports = router; \ No newline at end of file diff --git a/routes/matchingAdmin.js b/routes/matchingAdmin.js new file mode 100644 index 0000000..f6e4fbc --- /dev/null +++ b/routes/matchingAdmin.js @@ -0,0 +1,472 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../database'); +const { auth, adminAuth } = require('../middleware/auth'); +const logger = require('../config/logger'); +const matchingService = require('../services/matchingService'); +const dayjs = require('dayjs'); + +// 获取不合理的匹配记录(正余额用户被匹配的情况) +router.get('/unreasonable-matches', auth, adminAuth, async (req, res) => { + try { + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 20; + const offset = (page - 1) * limit; + + // 查找正余额用户被匹配的情况 + const query = `SELECT + oa.id as allocation_id, + oa.from_user_id, + oa.to_user_id, + oa.amount, + oa.status, + oa.outbound_date, + oa.created_at, + u_to.username as to_username, + u_to.balance as to_user_balance, + u_from.username as from_username, + u_from.balance as from_user_balance, + mo.amount as total_order_amount + FROM transfers oa + JOIN users u_to ON oa.to_user_id = u_to.id + JOIN users u_from ON oa.from_user_id = u_from.id + JOIN matching_orders mo ON oa.id = mo.id + WHERE oa.source_type = 'allocation' + AND u_to.balance > 0 + AND u_to.is_system_account = FALSE + AND oa.status IN ('pending', 'confirmed') + ORDER BY oa.created_at DESC + LIMIT ${offset}, ${limit}`; + + const countQuery = `SELECT COUNT(*) as total + FROM transfers oa + JOIN users u_to ON oa.to_user_id = u_to.id + WHERE oa.source_type = 'allocation' + AND u_to.balance > 0 + AND u_to.is_system_account = FALSE + AND oa.status IN ('pending', 'confirmed')`; + + const unreasonableMatches = await db.executeQuery(query); + + // 获取总数 + const countResult = await db.executeQuery(countQuery); + + const total = countResult[0].total; + + res.json({ + success: true, + data: { + matches: unreasonableMatches, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit) + } + } + }); + + } catch (error) { + console.error('获取不合理匹配记录失败:', error); + res.status(500).json({ message: '获取不合理匹配记录失败' }); + } +}); + +// 修复不合理的匹配记录 +router.post('/fix-unreasonable-match/:allocationId', auth, adminAuth, async (req, res) => { + try { + const { allocationId } = req.params; + const { action } = req.body; // 'cancel' 或 'reassign' + + const connection = await db.getDB().getConnection(); + await connection.query('START TRANSACTION'); + + try { + // 获取分配详情 + const [allocationResult] = await connection.execute( + `SELECT oa.*, u_to.balance as to_user_balance, u_to.username as to_username + FROM transfers oa + JOIN users u_to ON oa.to_user_id = u_to.id + WHERE oa.source_type = 'allocation' AND oa.id = ?`, + [allocationId] + ); + + if (allocationResult.length === 0) { + await connection.query('ROLLBACK'); + connection.release(); + return res.status(404).json({ message: '分配记录不存在' }); + } + + const allocation = allocationResult[0]; + + if (allocation.to_user_balance <= 0) { + await connection.query('ROLLBACK'); + connection.release(); + return res.status(400).json({ message: '该用户余额已为负数,无需修复' }); + } + + if (action === 'cancel') { + // 获取当前时间 + const currentTime = new Date(); + + // 取消分配 + await connection.execute( + 'UPDATE transfers SET status = "cancelled", updated_at = ? WHERE id = ?', + [currentTime, allocationId] + ); + + // 记录操作日志 + await connection.execute( + 'INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, description, created_at) VALUES (?, "fix_matching", "allocation", ?, ?, ?)', + [req.user.id, allocationId, `取消不合理匹配:正余额用户${allocation.to_username}(余额${allocation.to_user_balance}元)被匹配${allocation.amount}元`, currentTime] + ); + + } else if (action === 'reassign') { + // 重新分配给负余额用户 + const usedTargetUsers = new Set([allocation.to_user_id]); + const newTargetUser = await matchingService.getMatchingTargetExcluding(allocation.from_user_id, usedTargetUsers); + + if (!newTargetUser) { + await connection.query('ROLLBACK'); + connection.release(); + return res.status(400).json({ message: '没有可用的负余额用户进行重新分配' }); + } + + // 获取当前时间 + const currentTime = new Date(); + + // 更新分配目标 + await connection.execute( + 'UPDATE transfers SET to_user_id = ?, updated_at = ? WHERE id = ?', + [newTargetUser, currentTime, allocationId] + ); + + // 获取新目标用户信息 + const [newUserResult] = await connection.execute( + 'SELECT username, balance FROM users WHERE id = ?', + [newTargetUser] + ); + + const newUser = newUserResult[0]; + + // 记录操作日志 + await connection.execute( + 'INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, description, created_at) VALUES (?, "fix_matching", "allocation", ?, ?, ?)', + [req.user.id, allocationId, `修复不合理匹配:从正余额用户${allocation.to_username}(余额${allocation.to_user_balance}元)重新分配给负余额用户${newUser.username}(余额${newUser.balance}元)`, currentTime] + ); + } else { + await connection.query('ROLLBACK'); + connection.release(); + return res.status(400).json({ message: '无效的操作类型' }); + } + + await connection.query('COMMIT'); + connection.release(); + + res.json({ + success: true, + message: action === 'cancel' ? '已取消不合理匹配' : '已重新分配给负余额用户' + }); + + } catch (innerError) { + await connection.query('ROLLBACK'); + connection.release(); + throw innerError; + } + + } catch (error) { + console.error('修复不合理匹配失败:', error); + res.status(500).json({ message: error.message || '修复不合理匹配失败' }); + } +}); + +// 获取匹配统计信息 +router.get('/matching-stats', auth, adminAuth, async (req, res) => { + try { + + // 获取各种统计数据 + const stats = await db.executeQuery( + `SELECT + COUNT(CASE WHEN u_to.balance > 0 AND u_to.is_system_account = FALSE AND oa.status IN ('pending', 'confirmed') THEN 1 END) as unreasonable_matches, + COUNT(CASE WHEN u_to.balance < 0 AND u_to.is_system_account = FALSE AND oa.status IN ('pending', 'confirmed') THEN 1 END) as reasonable_matches, + COUNT(CASE WHEN u_to.is_system_account = TRUE AND oa.status IN ('pending', 'confirmed') THEN 1 END) as system_matches, + SUM(CASE WHEN u_to.balance > 0 AND u_to.is_system_account = FALSE AND oa.status IN ('pending', 'confirmed') THEN oa.amount ELSE 0 END) as unreasonable_amount, + SUM(CASE WHEN u_to.balance < 0 AND u_to.is_system_account = FALSE AND oa.status IN ('pending', 'confirmed') THEN oa.amount ELSE 0 END) as reasonable_amount + FROM transfers oa + JOIN users u_to ON oa.to_user_id = u_to.id + WHERE oa.source_type = 'allocation'` + ); + + // 获取昨天的匹配验证统计 + const yesterdayStr = dayjs().subtract(1, 'day').format('YYYY-MM-DD'); + + const yesterdayStats = await db.executeQuery( + `SELECT + SUM(oa.amount) as total_outbound, + COUNT(DISTINCT oa.amount) as unique_amounts + FROM transfers oa + JOIN users u ON oa.from_user_id = u.id + WHERE oa.source_type = 'allocation' AND DATE(oa.outbound_date) = ? AND oa.status = 'confirmed' AND u.is_system_account = FALSE`, + [yesterdayStr] + ); + + res.json({ + success: true, + data: { + currentStats: stats[0], + yesterdayStats: yesterdayStats[0] + } + }); + + } catch (error) { + console.error('获取匹配统计失败:', error); + res.status(500).json({ message: '获取匹配统计失败' }); + } +}); + +// 批量修复所有不合理匹配 +router.post('/fix-all-unreasonable', auth, adminAuth, async (req, res) => { + try { + let fixedCount = 0; + let errorCount = 0; + const errors = []; + + // 获取所有不合理的匹配记录 + const unreasonableMatches = await db.executeQuery( + `SELECT oa.id, oa.from_user_id, oa.to_user_id, oa.amount, u_to.username, u_to.balance + FROM transfers oa + JOIN users u_to ON oa.to_user_id = u_to.id + WHERE oa.source_type = 'allocation' + AND u_to.balance > 0 + AND u_to.is_system_account = FALSE + AND oa.status IN ('pending', 'confirmed') + ORDER BY u_to.balance DESC` + ); + + for (const match of unreasonableMatches) { + const connection = await db.getDB().getConnection(); + try { + await connection.query('START TRANSACTION'); + + // 尝试重新分配给负余额用户 + const usedTargetUsers = new Set([match.to_user_id]); + const newTargetUser = await matchingService.getMatchingTargetExcluding(match.from_user_id, usedTargetUsers); + + // 获取当前时间 + const currentTime = new Date(); + + if (newTargetUser) { + // 更新分配目标 + await connection.execute( + 'UPDATE transfers SET to_user_id = ?, updated_at = ? WHERE id = ?', + [newTargetUser, currentTime, match.id] + ); + + // 记录操作日志 + await connection.execute( + 'INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, description, created_at) VALUES (?, "batch_fix_matching", "allocation", ?, ?, ?)', + [req.user.id, match.id, `批量修复:从正余额用户${match.username}(余额${match.balance}元)重新分配${match.amount}元给负余额用户`, currentTime] + ); + + fixedCount++; + } else { + // 如果没有可用的负余额用户,取消分配 + await connection.execute( + 'UPDATE transfers SET status = "cancelled", updated_at = ? WHERE id = ?', + [currentTime, match.id] + ); + + await connection.execute( + 'INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, description, created_at) VALUES (?, "batch_fix_matching", "allocation", ?, ?, ?)', + [req.user.id, match.id, `批量修复:取消正余额用户${match.username}(余额${match.balance}元)的${match.amount}元分配`, currentTime] + ); + + fixedCount++; + } + + await connection.query('COMMIT'); + connection.release(); + + } catch (error) { + await connection.query('ROLLBACK'); + connection.release(); + errorCount++; + errors.push(`分配ID ${match.id}: ${error.message}`); + console.error(`修复分配${match.id}失败:`, error); + } + } + + res.json({ + success: true, + message: `批量修复完成:成功修复${fixedCount}条记录,失败${errorCount}条记录`, + data: { + fixedCount, + errorCount, + errors: errors.slice(0, 10) // 只返回前10个错误 + } + }); + + } catch (error) { + console.error('批量修复不合理匹配失败:', error); + res.status(500).json({ message: '批量修复不合理匹配失败' }); + } +}); + +// 确认分配 +router.post('/confirm-allocation/:allocationId', auth, adminAuth, async (req, res) => { + try { + const { allocationId } = req.params; + const adminId = req.user.id; + + const connection = await db.getDB().getConnection(); + + try { + await connection.query('START TRANSACTION'); + + // 检查分配是否存在且状态为pending + const [allocations] = await connection.execute( + `SELECT oa.*, u_from.username as from_username, u_to.username as to_username + FROM transfers oa + JOIN users u_from ON oa.from_user_id = u_from.id + JOIN users u_to ON oa.to_user_id = u_to.id + WHERE oa.source_type = 'allocation' AND oa.id = ? AND oa.status = 'pending'`, + [allocationId] + ); + + if (allocations.length === 0) { + await connection.query('ROLLBACK'); + connection.release(); + return res.status(404).json({ message: '分配不存在或状态不是待处理' }); + } + + const allocation = allocations[0]; + + // 获取当前时间 + const currentTime = new Date(); + + // 计算3小时后的截止时间 + const deadline = new Date(); + deadline.setHours(deadline.getHours() + 3); + + // 创建转账记录,直接设置为confirmed状态 + const transferDescription = `匹配订单 ${allocation.matching_order_id} 第 ${allocation.cycle_number} 轮转账(管理员确认)`; + const [transferResult] = await connection.execute( + `INSERT INTO transfers (from_user_id, to_user_id, amount, transfer_type, status, description, deadline_at, confirmed_at, source_type) VALUES (?, ?, ?, "user_to_user", "confirmed", ?, ?, ?, 'allocation')`, + [ + allocation.from_user_id, + allocation.to_user_id, + allocation.amount, + transferDescription, + deadline, + currentTime + ] + ); + + // 更新分配状态为已确认,并关联转账记录 + await connection.execute( + 'UPDATE transfers SET status = "confirmed", transfer_id = ?, confirmed_at = ?, updated_at = ? WHERE id = ?', + [transferResult.insertId, currentTime, currentTime, allocationId] + ); + + // 记录管理员操作日志 + await connection.execute( + 'INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, description, created_at) VALUES (?, "confirm_allocation", "allocation", ?, ?, ?)', + [adminId, allocationId, `管理员确认分配:${allocation.from_username} -> ${allocation.to_username},金额:${allocation.amount}元`, currentTime] + ); + + // 记录确认动作到匹配记录 + await connection.execute( + 'INSERT INTO matching_records (matching_order_id, user_id, action, amount, note) VALUES (?, ?, "confirm", ?, ?)', + [ + allocation.matching_order_id, + adminId, + allocation.amount, + '管理员确认分配' + ] + ); + + await connection.query('COMMIT'); + connection.release(); + + res.json({ + success: true, + message: '分配已确认' + }); + + } catch (innerError) { + await connection.query('ROLLBACK'); + connection.release(); + throw innerError; + } + + } catch (error) { + console.error('确认分配失败:', error); + res.status(500).json({ message: error.message || '确认分配失败' }); + } + }); + + // 取消分配 + router.post('/cancel-allocation/:allocationId', auth, adminAuth, async (req, res) => { + try { + const { allocationId } = req.params; + const adminId = req.user.id; + + const connection = await db.getDB().getConnection(); + + try { + await connection.query('START TRANSACTION'); + + // 检查分配是否存在且状态为pending + const [allocations] = await connection.execute( + `SELECT oa.*, u_from.username as from_username, u_to.username as to_username + FROM transfers oa + JOIN users u_from ON oa.from_user_id = u_from.id + JOIN users u_to ON oa.to_user_id = u_to.id + WHERE oa.source_type = 'allocation' AND oa.id = ? AND oa.status = 'pending'`, + [allocationId] + ); + + if (allocations.length === 0) { + await connection.query('ROLLBACK'); + connection.release(); + return res.status(404).json({ message: '分配不存在或状态不是待处理' }); + } + + const allocation = allocations[0]; + + // 获取当前时间 + const currentTime = new Date(); + + // 更新分配状态为已取消 + await connection.execute( + 'UPDATE transfers SET status = "cancelled", updated_at = ? WHERE id = ?', + [currentTime, allocationId] + ); + + // 记录管理员操作日志 + await connection.execute( + 'INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, description, created_at) VALUES (?, "cancel_allocation", "allocation", ?, ?, ?)', + [adminId, allocationId, `管理员取消分配:${allocation.from_username} -> ${allocation.to_username},金额:${allocation.amount}元`, currentTime] + ); + + await connection.query('COMMIT'); + connection.release(); + + res.json({ + success: true, + message: '分配已取消' + }); + + } catch (innerError) { + await connection.query('ROLLBACK'); + connection.release(); + throw innerError; + } + + } catch (error) { + console.error('取消分配失败:', error); + res.status(500).json({ message: error.message || '取消分配失败' }); + } + }); + + module.exports = router; \ No newline at end of file diff --git a/routes/orders.js b/routes/orders.js new file mode 100644 index 0000000..e1a2f75 --- /dev/null +++ b/routes/orders.js @@ -0,0 +1,459 @@ +const express = require('express'); +const { getDB } = require('../database'); +const { auth, adminAuth } = require('../middleware/auth'); + +const router = express.Router(); + +// 生成订单号 +function generateOrderNo() { + const timestamp = Date.now().toString(); + const random = Math.random().toString(36).substr(2, 5); + return `ORD${timestamp}${random}`.toUpperCase(); +} + +// 获取订单列表 +router.get('/', auth, async (req, res) => { + try { + const { page = 1, limit = 10, search = '', orderNumber = '', username = '', status = '', startDate = '', endDate = '' } = req.query; + + + + // 确保参数为有效数字 + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(limit) || 10; + const offset = (pageNum - 1) * limitNum; + const isAdmin = req.user.role === 'admin'; + + let whereClause = 'WHERE 1=1'; + const params = []; + + // 非管理员只能查看自己的订单 + if (!isAdmin) { + whereClause += ' AND o.user_id = ?'; + params.push(req.user.id); + } + + if (search) { + whereClause += ' AND (o.order_no LIKE ? OR u.username LIKE ?)'; + params.push(`%${search}%`, `%${search}%`); + } + + if (orderNumber) { + whereClause += ' AND o.order_no LIKE ?'; + params.push(`%${orderNumber}%`); + } + + if (username ) { + whereClause += ' AND u.username LIKE ?'; + params.push(`%${username}%`); + } + + if (status && status.trim()) { + whereClause += ' AND o.status = ?'; + params.push(status); + } + + if (startDate && startDate.trim()) { + whereClause += ' AND DATE(o.created_at) >= ?'; + params.push(startDate); + } + + if (endDate && endDate.trim()) { + whereClause += ' AND DATE(o.created_at) <= ?'; + params.push(endDate); + } + + // 获取总数 + const countQuery = ` + SELECT COUNT(*) as total + FROM orders as o + LEFT JOIN users u ON o.user_id = u.id + ${whereClause} + `; + console.log(countQuery,params); + + const [countResult] = await getDB().execute(countQuery, params); + const total = countResult[0].total; + console.log(total,'数量'); + + // 获取订单列表 + const query = ` + SELECT + o.id, o.order_no, o.user_id, o.total_amount, o.total_points, + o.status, o.address, o.created_at, o.updated_at, + u.username + FROM orders o + LEFT JOIN users u ON o.user_id = u.id + ${whereClause} + ORDER BY o.created_at DESC + LIMIT ${limitNum} OFFSET ${offset} + `; + + const [orders] = await getDB().execute(query, [...params]); + + res.json({ + success: true, + data: { + orders, + pagination: { + page: pageNum, + limit: limitNum, + total, + pages: Math.ceil(total / limitNum) + } + } + }); + } catch (error) { + console.error('获取订单列表失败:', error); + res.status(500).json({ success: false, message: '获取订单列表失败' }); + } +}); + +// 获取单个订单详情 +router.get('/:id', auth, async (req, res) => { + try { + const { id } = req.params; + const isAdmin = req.user.role === 'admin'; + + let whereClause = 'WHERE o.id = ?'; + const params = [id]; + + // 非管理员只能查看自己的订单 + if (!isAdmin) { + whereClause += ' AND o.user_id = ?'; + params.push(req.user.id); + } + + const query = ` + SELECT + o.id, o.order_no, o.user_id, o.total_amount, o.total_points, + o.status, o.address, o.created_at, o.updated_at, + u.username, u.phone + FROM orders o + LEFT JOIN users u ON o.user_id = u.id + ${whereClause} + `; + + const [orders] = await getDB().execute(query, params); + + if (orders.length === 0) { + return res.status(404).json({ success: false, message: '订单不存在' }); + } + + res.json({ + success: true, + data: { order: orders[0] } + }); + } catch (error) { + console.error('获取订单详情失败:', error); + res.status(500).json({ success: false, message: '获取订单详情失败' }); + } +}); + +// 创建订单 +router.post('/', auth, async (req, res) => { + const db = getDB(); + await db.query('START TRANSACTION'); + + try { + + + const { product_id, quantity, shipping_address } = req.body; + const user_id = req.user.id; + + // 验证必填字段 + if (!product_id || !quantity || !shipping_address) { + return res.status(400).json({ success: false, message: '请填写所有必填字段' }); + } + + // 检查商品是否存在且有效 + const [products] = await db.execute( + 'SELECT id, name, points_price, stock, status FROM products WHERE id = ?', + [product_id] + ); + + if (products.length === 0 || products[0].status !== 'active') { + await db.query('ROLLBACK'); + return res.status(404).json({ success: false, message: '商品不存在或已下架' }); + } + + const product = products[0]; + + // 检查库存 + if (product.stock < quantity) { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: '库存不足' }); + } + + // 计算总积分 + const totalPoints = product.points_price * quantity; + + // 检查用户积分是否足够 + const [users] = await db.execute( + 'SELECT id, username, points FROM users WHERE id = ?', + [user_id] + ); + + if (users.length === 0) { + await db.query('ROLLBACK'); + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + const user = users[0]; + + if (user.points < totalPoints) { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: '积分不足' }); + } + + // 生成订单号 + const orderNumber = 'ORD' + Date.now() + Math.random().toString(36).substr(2, 4).toUpperCase(); + + // 创建订单 + const [orderResult] = await db.execute( + `INSERT INTO orders (order_no, user_id, total_amount, total_points, status, address, created_at, updated_at) + VALUES (?, ?, ?, ?, 'pending', ?, NOW(), NOW())`, + [orderNumber, user_id, 0, totalPoints, shipping_address] + ); + + // 扣除用户积分 + await db.execute( + 'UPDATE users SET points = points - ? WHERE id = ?', + [totalPoints, user_id] + ); + + // 减少商品库存 + await db.execute( + 'UPDATE products SET stock = stock - ? WHERE id = ?', + [quantity, product_id] + ); + + // 记录积分历史 + await db.execute( + `INSERT INTO points_history (user_id, amount, type, description, created_at) + VALUES (?, ?, 'spend', ?, NOW())`, + [user_id, -totalPoints, `购买商品:${product.name}`] + ); + + await db.query('COMMIT'); + + res.status(201).json({ + success: true, + message: '订单创建成功', + data: { + orderId: orderResult.insertId, + orderNumber, + pointsUsed: totalPoints + } + }); + } catch (error) { + await db.query('ROLLBACK'); + console.error('创建订单失败:', error); + res.status(500).json({ success: false, message: '创建订单失败' }); + } +}); + +// 用户取消订单 +router.put('/:id/cancel', auth, async (req, res) => { + const db = getDB(); + await db.query('START TRANSACTION'); + + try { + + const orderId = req.params.id; + const userId = req.user.id; + + // 检查订单是否存在且属于当前用户 + const [orders] = await db.execute( + 'SELECT id, user_id, total_points, status FROM orders WHERE id = ? AND user_id = ?', + [orderId, userId] + ); + + if (orders.length === 0) { + await db.query('ROLLBACK'); + return res.status(404).json({ success: false, message: '订单不存在' }); + } + + const order = orders[0]; + + if (order.status !== 'pending') { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: '只能取消待处理的订单' }); + } + + // 退还用户积分 + await db.execute( + 'UPDATE users SET points = points + ? WHERE id = ?', + [order.total_points, userId] + ); + + // 记录积分历史 + await db.execute( + `INSERT INTO points_history (user_id, amount, type, description, created_at) + VALUES (?, ?, 'refund', '订单取消退还积分', NOW())`, + [userId, order.total_points] + ); + + // 更新订单状态 + await db.execute( + 'UPDATE orders SET status = "cancelled", updated_at = NOW() WHERE id = ?', + [orderId] + ); + + await db.query('COMMIT'); + + res.json({ success: true, message: '订单已取消' }); + } catch (error) { + await db.query('ROLLBACK'); + console.error('取消订单失败:', error); + res.status(500).json({ success: false, message: '取消订单失败' }); + } +}); + +// 确认收货 +router.put('/:id/confirm', auth, async (req, res) => { + try { + const orderId = req.params.id; + const userId = req.user.id; + + // 检查订单是否存在且属于当前用户 + const [orders] = await getDB().execute( + 'SELECT id, status FROM orders WHERE id = ? AND user_id = ?', + [orderId, userId] + ); + + if (orders.length === 0) { + return res.status(404).json({ success: false, message: '订单不存在' }); + } + + const order = orders[0]; + + if (order.status !== 'shipped') { + return res.status(400).json({ success: false, message: '只能确认已发货的订单' }); + } + + // 更新订单状态 + await getDB().execute( + 'UPDATE orders SET status = "completed", updated_at = NOW() WHERE id = ?', + [orderId] + ); + + res.json({ success: true, message: '确认收货成功' }); + } catch (error) { + console.error('确认收货失败:', error); + res.status(500).json({ success: false, message: '确认收货失败' }); + } +}); + +// 更新订单状态(管理员) +router.put('/:id/status', auth, adminAuth, async (req, res) => { + const db = getDB(); + await db.query('START TRANSACTION'); + + try { + + const orderId = req.params.id; + const { status } = req.body; + + const validStatuses = ['pending', 'shipped', 'completed', 'cancelled']; + if (!validStatuses.includes(status)) { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: '无效的订单状态' }); + } + + // 检查订单是否存在 + const [orders] = await db.execute( + 'SELECT id, user_id, total_points, status FROM orders WHERE id = ?', + [orderId] + ); + + if (orders.length === 0) { + await db.query('ROLLBACK'); + return res.status(404).json({ success: false, message: '订单不存在' }); + } + + const order = orders[0]; + + // 如果是取消订单,需要退还积分 + if (status === 'cancelled' && order.status !== 'cancelled') { + // 退还用户积分 + await db.execute( + 'UPDATE users SET points = points + ? WHERE id = ?', + [order.total_points, order.user_id] + ); + + // 记录积分历史 + await db.execute( + `INSERT INTO points_history (user_id, amount, type, description, created_at) + VALUES (?, ?, 'earn', '订单取消退还积分', NOW())`, + [order.user_id, order.points_cost] + ); + } + + // 更新订单状态 + await db.execute( + 'UPDATE orders SET status = ?, updated_at = NOW() WHERE id = ?', + [status, orderId] + ); + + await db.query('COMMIT'); + + res.json({ success: true, message: '订单状态已更新' }); + } catch (error) { + await db.query('ROLLBACK'); + console.error('更新订单状态失败:', error); + res.status(500).json({ success: false, message: '更新订单状态失败' }); + } +}); + +// 获取订单统计信息(管理员权限) +router.get('/stats', auth, adminAuth, async (req, res) => { + try { + // 总订单数 + const [totalOrders] = await getDB().execute('SELECT COUNT(*) as count FROM orders'); + + // 待发货订单数 + const [pendingOrders] = await getDB().execute('SELECT COUNT(*) as count FROM orders WHERE status = "pending"'); + + // 已完成订单数 + const [completedOrders] = await getDB().execute('SELECT COUNT(*) as count FROM orders WHERE status = "completed"'); + + // 本月新增订单 + const [monthOrders] = await getDB().execute( + 'SELECT COUNT(*) as count FROM orders WHERE YEAR(created_at) = YEAR(NOW()) AND MONTH(created_at) = MONTH(NOW())' + ); + + // 上月订单数(用于计算增长率) + const [lastMonthOrders] = await getDB().execute( + 'SELECT COUNT(*) as count FROM orders WHERE YEAR(created_at) = YEAR(DATE_SUB(NOW(), INTERVAL 1 MONTH)) AND MONTH(created_at) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH))' + ); + + // 计算月增长率 + const lastMonthCount = lastMonthOrders[0].count; + const currentMonthCount = monthOrders[0].count; + let monthGrowthRate = 0; + if (lastMonthCount > 0) { + monthGrowthRate = ((currentMonthCount - lastMonthCount) / lastMonthCount * 100).toFixed(1); + } + + // 总积分消费 + const [totalPointsConsumed] = await getDB().execute('SELECT SUM(points_cost) as total FROM orders WHERE status != "cancelled"'); + + res.json({ + success: true, + data: { + totalOrders: totalOrders[0].count, + pendingOrders: pendingOrders[0].count, + completedOrders: completedOrders[0].count, + monthOrders: monthOrders[0].count, + monthGrowthRate: parseFloat(monthGrowthRate), + totalPointsConsumed: totalPointsConsumed[0].total || 0 + } + }); + } 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/points.js b/routes/points.js new file mode 100644 index 0000000..371dbdb --- /dev/null +++ b/routes/points.js @@ -0,0 +1,348 @@ +const express = require('express'); +const router = express.Router(); +const { getDB } = require('../database'); +const { auth, adminAuth } = require('../middleware/auth'); + +// 获取用户当前积分 +router.get('/balance', auth, async (req, res) => { + try { + const userId = req.user.id; + + const [users] = await getDB().execute( + 'SELECT points FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + res.json({ + success: true, + data: { + points: users[0].points + } + }); + } catch (error) { + console.error('获取积分余额失败:', error); + res.status(500).json({ success: false, message: '获取积分余额失败' }); + } +}); + +// 获取用户积分历史记录 +router.get('/history', auth, async (req, res) => { + try { + const { page = 1, limit = 10, type, username, change, startDate, endDate } = req.query; + + // 确保参数为有效数字 + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(limit) || 10; + const offset = (pageNum - 1) * limitNum; + + let whereClause = ''; + let queryParams = []; + + // 如果是管理员,可以查看所有用户的积分历史 + if (req.user.role === 'admin') { + whereClause = 'WHERE 1=1'; + + // 按用户名筛选 + if (username) { + whereClause += ' AND u.username LIKE ?'; + queryParams.push(`%${username}%`); + } + + // 按类型筛选 + if (type) { + whereClause += ' AND ph.type = ?'; + queryParams.push(type); + } + + // 按积分变化筛选 + if (change === 'positive') { + whereClause += ' AND ph.amount > 0'; + } else if (change === 'negative') { + whereClause += ' AND ph.amount < 0'; + } + + // 按时间范围筛选 + if (startDate) { + whereClause += ' AND DATE(ph.created_at) >= ?'; + queryParams.push(startDate); + } + if (endDate) { + whereClause += ' AND DATE(ph.created_at) <= ?'; + queryParams.push(endDate); + } + } else { + // 普通用户只能查看自己的积分历史 + whereClause = 'WHERE ph.user_id = ?'; + queryParams.push(req.user.id); + + if (type && ['earn', 'spend'].includes(type)) { + whereClause += ' AND ph.type = ?'; + queryParams.push(type); + } + } + + // 获取总数 + const countQuery = req.user.role === 'admin' + ? `SELECT COUNT(*) as total FROM points_history ph JOIN users u ON ph.user_id = u.id ${whereClause}` + : `SELECT COUNT(*) as total FROM points_history ph ${whereClause}`; + + const [countResult] = await getDB().execute(countQuery, queryParams); + + // 获取历史记录 + const historyQuery = req.user.role === 'admin' + ? `SELECT ph.id, ph.amount as points, ph.type, ph.description, ph.created_at, + u.username, + (SELECT points FROM users WHERE id = ph.user_id) as balance_after + FROM points_history ph + JOIN users u ON ph.user_id = u.id + ${whereClause} + ORDER BY ph.created_at DESC + LIMIT ${limitNum} OFFSET ${offset}` + : `SELECT id, amount as points_change, type, description, created_at + FROM points_history ph + ${whereClause} + ORDER BY created_at DESC + LIMIT ${limitNum} OFFSET ${offset}`; + + const [records] = await getDB().execute(historyQuery, queryParams); + + const responseData = req.user.role === 'admin' + ? { + history: records, + total: countResult[0].total + } + : { + records, + pagination: { + page: pageNum, + limit: limitNum, + total: countResult[0].total, + totalPages: Math.ceil(countResult[0].total / limitNum) + } + }; + + res.json({ + success: true, + data: responseData + }); + } catch (error) { + console.error('获取积分历史失败:', error); + res.status(500).json({ success: false, message: '获取积分历史失败' }); + } +}); + +// 管理员调整用户积分 +router.post('/adjust', auth, adminAuth, async (req, res) => { + const connection = await getDB().getConnection(); + + try { + await connection.beginTransaction(); + + const { userId, points, reason } = req.body; + + if (!userId || points === undefined || points === null || !reason) { + await connection.rollback(); + return res.status(400).json({ success: false, message: '请提供有效的用户ID、积分数量和调整原因' }); + } + + // 检查用户是否存在 + const [users] = await connection.execute( + 'SELECT id, username, points FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + await connection.rollback(); + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + const currentPoints = users[0].points; + const newPoints = currentPoints + points; + + // 检查积分是否会变为负数 + if (newPoints < 0) { + await connection.rollback(); + return res.status(400).json({ success: false, message: '用户积分不足,无法扣除' }); + } + + // 更新用户积分 + await connection.execute( + 'UPDATE users SET points = ? WHERE id = ?', + [newPoints, userId] + ); + + // 记录积分历史 + await connection.execute( + `INSERT INTO points_history (user_id, amount, type, description, created_at) + VALUES (?, ?, 'admin_adjust', ?, NOW())`, + [userId, points, reason] + ); + + await connection.commit(); + + res.json({ + success: true, + message: '积分调整成功', + data: { + userId: userId, + pointsChanged: points, + newBalance: newPoints + } + }); + } catch (error) { + await connection.rollback(); + console.error('积分调整失败:', error); + res.status(500).json({ success: false, message: '积分调整失败' }); + } finally { + connection.release(); + } +}); + +// 管理员给用户充值积分 +router.post('/recharge', auth, adminAuth, async (req, res) => { + const connection = await getDB().getConnection(); + + try { + await connection.beginTransaction(); + + const { user_id, points, description = '管理员充值' } = req.body; + + if (!user_id || !points || points <= 0) { + await connection.rollback(); + return res.status(400).json({ success: false, message: '请提供有效的用户ID和积分数量' }); + } + + // 检查用户是否存在 + const [users] = await connection.execute( + 'SELECT id, username FROM users WHERE id = ?', + [user_id] + ); + + if (users.length === 0) { + await connection.rollback(); + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + // 增加用户积分 + await connection.execute( + 'UPDATE users SET points = points + ? WHERE id = ?', + [points, user_id] + ); + + // 记录积分历史 + await connection.execute( + `INSERT INTO points_history (user_id, amount, type, description, created_at) + VALUES (?, ?, 'earn', ?, NOW())`, + [user_id, points, description] + ); + + await connection.commit(); + + res.json({ + success: true, + message: '积分充值成功', + data: { + userId: user_id, + pointsAdded: points + } + }); + } catch (error) { + await connection.rollback(); + console.error('积分充值失败:', error); + res.status(500).json({ success: false, message: '积分充值失败' }); + } finally { + connection.release(); + } +}); + + + +// 获取积分排行榜 +router.get('/leaderboard', auth, async (req, res) => { + try { + const { limit = 10 } = req.query; + + const [users] = await getDB().execute( + `SELECT id, username, points + FROM users + WHERE points > 0 + ORDER BY points DESC + LIMIT ?`, + [parseInt(limit)] + ); + + res.json({ + success: true, + data: { + leaderboard: users.map((user, index) => ({ + rank: index + 1, + userId: user.id, + username: user.username, + points: user.points + })) + } + }); + } catch (error) { + console.error('获取积分排行榜失败:', error); + res.status(500).json({ success: false, message: '获取积分排行榜失败' }); + } +}); + +// 获取积分统计信息(管理员权限) +router.get('/stats', auth, adminAuth, async (req, res) => { + try { + // 总积分发放量 + const [totalEarned] = await getDB().execute( + 'SELECT SUM(amount) as total FROM points_history WHERE type = "earn"' + ); + + // 总积分消费量 + const [totalConsumed] = await getDB().execute( + 'SELECT SUM(ABS(amount)) as total FROM points_history WHERE type = "spend"' + ); + + // 本月积分发放 + const [monthEarned] = await getDB().execute( + 'SELECT SUM(amount) as total FROM points_history WHERE type = "earn" AND YEAR(created_at) = YEAR(NOW()) AND MONTH(created_at) = MONTH(NOW())' + ); + + // 上月积分发放(用于计算增长率) + const [lastMonthEarned] = await getDB().execute( + 'SELECT SUM(amount) as total FROM points_history WHERE type = "earn" AND YEAR(created_at) = YEAR(DATE_SUB(NOW(), INTERVAL 1 MONTH)) AND MONTH(created_at) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH))' + ); + + // 计算月增长率 + const lastMonthTotal = lastMonthEarned[0].total || 0; + const currentMonthTotal = monthEarned[0].total || 0; + let monthGrowthRate = 0; + if (lastMonthTotal > 0) { + monthGrowthRate = ((currentMonthTotal - lastMonthTotal) / lastMonthTotal * 100).toFixed(1); + } + + // 活跃用户数(有积分记录的用户) + const [activeUsers] = await getDB().execute( + 'SELECT COUNT(DISTINCT user_id) as count FROM points_history' + ); + + res.json({ + success: true, + data: { + stats: { + totalPoints: totalEarned[0].total || 0, + totalEarned: totalEarned[0].total || 0, + totalSpent: totalConsumed[0].total || 0, + activeUsers: activeUsers[0].count + } + } + }); + } 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/products.js b/routes/products.js new file mode 100644 index 0000000..c17b888 --- /dev/null +++ b/routes/products.js @@ -0,0 +1,451 @@ +const express = require('express'); +const { getDB } = require('../database'); +const { auth, adminAuth } = require('../middleware/auth'); + +const router = express.Router(); + +// 获取商品列表 +router.get('/', async (req, res) => { + try { + const { page = 1, limit = 10, search = '', category = '', status = '' } = req.query; + + // 确保参数为有效数字 + const pageNum = Math.max(1, parseInt(page) || 1); + const limitNum = Math.max(1, Math.min(100, parseInt(limit) || 10)); // 限制最大100条 + const offset = Math.max(0, (pageNum - 1) * limitNum); + + console.log('分页参数:', { pageNum, limitNum, offset, search, category, status }); + + let whereClause = 'WHERE 1=1'; + const params = []; + + if (search) { + whereClause += ' AND name LIKE ?'; + params.push(`%${search}%`); + } + + if (category) { + whereClause += ' AND category = ?'; + params.push(category); + } + + if (status) { + whereClause += ' AND status = ?'; + params.push(status); + } else { + whereClause += ' AND status = "active"'; + } + + // 获取总数 + const countQuery = `SELECT COUNT(*) as total FROM products ${whereClause}`; + const [countResult] = await getDB().execute(countQuery, params); + const total = countResult[0].total; + + // 获取商品列表 + const query = ` + SELECT id, name, category, points_price as points, stock, image_url as image, description, status, created_at, updated_at + FROM products + ${whereClause} + ORDER BY created_at DESC + LIMIT ${limitNum} OFFSET ${offset} + `; + + // 确保参数数组正确传递 + const queryParams = [...params]; + console.log('Query params:', queryParams, 'Query:', query); + const [products] = await getDB().execute(query, queryParams); + + res.json({ + success: true, + data: { + products, + pagination: { + page: pageNum, + limit: limitNum, + total, + pages: Math.ceil(total / limitNum) + } + } + }); + } catch (error) { + console.error('获取商品列表失败:', error); + res.status(500).json({ success: false, message: '获取商品列表失败' }); + } +}); + +// 获取商品分类列表 +router.get('/categories', async (req, res) => { + try { + const [categories] = await getDB().execute( + 'SELECT DISTINCT category FROM products WHERE status = "active" AND category IS NOT NULL' + ); + + res.json({ + success: true, + data: { + categories: categories.map(item => item.category) + } + }); + } catch (error) { + console.error('获取商品分类失败:', error); + res.status(500).json({ success: false, message: '获取商品分类失败' }); + } +}); + +// 获取单个商品详情 +router.get('/:id', async (req, res) => { + try { + const { id } = req.params; + + const query = ` + SELECT id, name, category, price, points_price as points, stock, image_url as image, description, details, status, created_at, updated_at + FROM products + WHERE id = ? + `; + + const [products] = await getDB().execute(query, [id]); + + if (products.length === 0) { + return res.status(404).json({ success: false, message: '商品不存在' }); + } + + // 增强商品数据,添加前端需要的字段 + const product = products[0]; + const enhancedProduct = { + ...product, + images: product.image ? [product.image] : ['/imgs/default-product.png'], // 将单个图片转为数组 + tags: product.category ? [product.category] : [], // 将分类作为标签 + sales: Math.floor(Math.random() * 1000) + 100, // 模拟销量数据 + rating: (Math.random() * 2 + 3).toFixed(1), // 模拟评分 3-5分 + originalPoints: product.points + Math.floor(Math.random() * 100), // 模拟原价 + discount: Math.floor(Math.random() * 3 + 7) // 模拟折扣 7-9折 + }; + + res.json({ + success: true, + data: { product: enhancedProduct } + }); + } catch (error) { + console.error('获取商品详情失败:', error); + res.status(500).json({ success: false, message: '获取商品详情失败' }); + } +}); + +// 创建商品(管理员权限) +router.post('/', auth, adminAuth, async (req, res) => { + try { + + const { name, description, price, points_price, stock, category, image_url, details, status = 'active' } = req.body; + + if (!name || !price || !points_price || stock === undefined) { + return res.status(400).json({ message: '商品名称、原价、积分价格和库存不能为空' }); + } + + const [result] = await getDB().execute( + `INSERT INTO products (name, description, price, points_price, stock, category, image_url, details, status, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`, + [name, description, price, points_price, stock, category || null, image_url, details, status] + ); + + res.status(201).json({ + success: true, + message: '商品创建成功', + data: { productId: result.insertId } + }); + } catch (error) { + console.error('创建商品错误:', error); + res.status(500).json({ message: '创建商品失败' }); + } +}); + +// 更新商品(管理员权限) +router.put('/:id', auth, adminAuth, async (req, res) => { + try { + + const productId = req.params.id; + const { name, description, price, points_price, stock, category, image_url, details, status } = req.body; + + // 检查商品是否存在 + const [products] = await getDB().execute( + 'SELECT id FROM products WHERE id = ?', + [productId] + ); + + if (products.length === 0) { + return res.status(404).json({ message: '商品不存在' }); + } + + // 构建更新字段 + const updateFields = []; + const updateValues = []; + + if (name) { + updateFields.push('name = ?'); + updateValues.push(name); + } + + if (description !== undefined) { + updateFields.push('description = ?'); + updateValues.push(description); + } + + if (price !== undefined) { + updateFields.push('price = ?'); + updateValues.push(price); + } + + if (points_price !== undefined) { + updateFields.push('points_price = ?'); + updateValues.push(points_price); + } + + if (stock !== undefined) { + updateFields.push('stock = ?'); + updateValues.push(stock); + } + + if (category !== undefined) { + updateFields.push('category = ?'); + updateValues.push(category); + } + + if (image_url !== undefined) { + updateFields.push('image_url = ?'); + updateValues.push(image_url); + } + + if (details !== undefined) { + updateFields.push('details = ?'); + updateValues.push(details); + } + + if (status) { + updateFields.push('status = ?'); + updateValues.push(status); + } + + if (updateFields.length === 0) { + return res.status(400).json({ message: '没有要更新的字段' }); + } + + updateFields.push('updated_at = NOW()'); + updateValues.push(productId); + + await getDB().execute( + `UPDATE products SET ${updateFields.join(', ')} WHERE id = ?`, + updateValues + ); + + res.json({ + success: true, + message: '商品更新成功' + }); + } catch (error) { + console.error('更新商品错误:', error); + res.status(500).json({ message: '更新商品失败' }); + } +}); + +// 删除商品(管理员权限) +router.delete('/:id', auth, adminAuth, async (req, res) => { + try { + const { id } = req.params; + + // 检查商品是否存在 + const checkQuery = 'SELECT id FROM products WHERE id = ?'; + const [existing] = await getDB().execute(checkQuery, [id]); + + if (existing.length === 0) { + return res.status(404).json({ success: false, message: '商品不存在' }); + } + + // 检查是否有相关订单 + const orderCheckQuery = 'SELECT id FROM orders WHERE product_id = ? LIMIT 1'; + const [orders] = await getDB().execute(orderCheckQuery, [id]); + + if (orders.length > 0) { + return res.status(400).json({ success: false, message: '该商品存在相关订单,无法删除' }); + } + + const query = 'DELETE FROM products WHERE id = ?'; + await getDB().execute(query, [id]); + + res.json({ + success: true, + message: '商品删除成功' + }); + } catch (error) { + console.error('删除商品失败:', error); + res.status(500).json({ success: false, message: '删除商品失败' }); + } +}); + +// 获取商品统计信息(管理员权限) +router.get('/stats', auth, adminAuth, async (req, res) => { + try { + // 获取商品总数 + const totalQuery = 'SELECT COUNT(*) as total FROM products'; + const [totalResult] = await getDB().execute(totalQuery); + const totalProducts = totalResult[0].total; + + // 获取活跃商品数 + const activeQuery = 'SELECT COUNT(*) as total FROM products WHERE status = "active"'; + const [activeResult] = await getDB().execute(activeQuery); + const activeProducts = activeResult[0].total; + + // 获取库存不足商品数(库存小于10) + const lowStockQuery = 'SELECT COUNT(*) as total FROM products WHERE stock < 10'; + const [lowStockResult] = await getDB().execute(lowStockQuery); + const lowStockProducts = lowStockResult[0].total; + + // 获取本月新增商品数 + const monthlyQuery = ` + SELECT COUNT(*) as total + FROM products + WHERE YEAR(created_at) = YEAR(CURDATE()) AND MONTH(created_at) = MONTH(CURDATE()) + `; + const [monthlyResult] = await getDB().execute(monthlyQuery); + const monthlyProducts = monthlyResult[0].total; + + // 计算月增长率 + const lastMonthQuery = ` + SELECT COUNT(*) as total + FROM products + WHERE YEAR(created_at) = YEAR(DATE_SUB(CURDATE(), INTERVAL 1 MONTH)) + AND MONTH(created_at) = MONTH(DATE_SUB(CURDATE(), INTERVAL 1 MONTH)) + `; + const [lastMonthResult] = await getDB().execute(lastMonthQuery); + const lastMonthProducts = lastMonthResult[0].total; + + const monthlyGrowth = lastMonthProducts > 0 + ? ((monthlyProducts - lastMonthProducts) / lastMonthProducts * 100).toFixed(1) + : 0; + + res.json({ + success: true, + data: { + stats: { + totalProducts, + activeProducts, + lowStockProducts, + monthlyProducts, + monthlyGrowth: parseFloat(monthlyGrowth) + } + } + }); + } catch (error) { + console.error('获取商品统计失败:', error); + res.status(500).json({ success: false, message: '获取商品统计失败' }); + } +}); + +// 获取商品评论 +router.get('/:id/reviews', async (req, res) => { + try { + const { id } = req.params; + + // 这里可以从数据库获取真实的评论数据 + // 目前返回模拟数据,格式匹配前端期望 + const mockReviews = [ + { + id: 1, + user: { + name: '用户1', + avatar: null + }, + rating: 5, + content: '商品质量很好,非常满意!', + createdAt: '2024-01-15 10:30:00', + images: null + }, + { + id: 2, + user: { + name: '用户2', + avatar: null + }, + rating: 4, + content: '性价比不错,值得购买。', + createdAt: '2024-01-14 15:20:00', + images: null + }, + { + id: 3, + user: { + name: '用户3', + avatar: null + }, + rating: 5, + content: '发货速度快,包装精美。', + createdAt: '2024-01-13 09:45:00', + images: null + } + ]; + + res.json({ + success: true, + data: { + reviews: mockReviews, + total: mockReviews.length, + averageRating: 4.7 + } + }); + } catch (error) { + console.error('获取商品评论失败:', error); + res.status(500).json({ success: false, message: '获取商品评论失败' }); + } +}); + +// 获取推荐商品 +router.get('/:id/recommended', async (req, res) => { + try { + const id = parseInt(req.params.id); + + // 获取同类别的其他商品作为推荐 + const query = ` + SELECT p2.id, p2.name, p2.category, p2.price, p2.points_price as points, + p2.stock, p2.image_url as image, p2.description + FROM products p1 + JOIN products p2 ON p1.category = p2.category + WHERE p1.id = ? AND p2.id != ? AND p2.status = 'active' + ORDER BY RAND() + LIMIT 6 + `; + + const [recommendedProducts] = await getDB().execute(query, [id, id]); + + // 如果同类别商品不足,补充其他热门商品 + if (recommendedProducts.length < 6) { + const remainingCount = 6 - recommendedProducts.length; + if (remainingCount > 0) { + const additionalQuery = ` + SELECT id, name, category, price, points_price as points, + stock, image_url as image, description + FROM products + WHERE id != ? AND status = 'active' + ORDER BY RAND() + LIMIT ${remainingCount} + `; + + const [additionalProducts] = await getDB().execute( + additionalQuery, + [id] + ); + + recommendedProducts.push(...additionalProducts); + } + } + + res.json({ + success: true, + data: { + products: recommendedProducts + } + }); + } 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/regions.js b/routes/regions.js new file mode 100644 index 0000000..dc5880c --- /dev/null +++ b/routes/regions.js @@ -0,0 +1,31 @@ +const express = require('express') +const router = express.Router() +const { getDB } = require('../database') + +// 获取浙江省所有地区数据 +router.get('/zhejiang', async (req, res) => { + try { + const query = ` + SELECT id, city_name, district_name, region_code, is_available + FROM zhejiang_regions + WHERE is_available = 1 + ORDER BY city_name, district_name + ` + + const [rows] = await getDB().execute(query) + + res.json({ + success: true, + data: rows, + message: '获取地区数据成功' + }) + } 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/riskManagement.js b/routes/riskManagement.js new file mode 100644 index 0000000..00c9e39 --- /dev/null +++ b/routes/riskManagement.js @@ -0,0 +1,202 @@ +const express = require('express'); +const router = express.Router(); +const { auth } = require('../middleware/auth'); +const timeoutService = require('../services/timeoutService'); +const { getDB } = require('../database'); + +/** + * 检查管理员权限 + */ +const requireAdmin = (req, res, next) => { + if (req.user.role !== 'admin') { + return res.status(403).json({ success: false, message: '需要管理员权限' }); + } + next(); +}; + +/** + * 获取风险用户列表 + */ +router.get('/users', auth, requireAdmin, async (req, res) => { + try { + const { page = 1, limit = 10, is_blacklisted, username } = req.query; + + const filters = {}; + if (is_blacklisted !== undefined) { + filters.is_blacklisted = parseInt(is_blacklisted); + } + if (username) { + filters.username = username; + } + + const result = await timeoutService.getRiskUsers(filters, { page, limit }); + + res.json({ + success: true, + data: result + }); + } catch (error) { + console.error('获取风险用户列表失败:', error); + res.status(500).json({ success: false, message: '获取风险用户列表失败' }); + } +}); + +/** + * 拉黑用户 + */ +router.post('/blacklist/:userId', auth, requireAdmin, async (req, res) => { + try { + const { userId } = req.params; + const { reason } = req.body; + const operatorId = req.user.id; + + if (!reason || reason.trim() === '') { + return res.status(400).json({ success: false, message: '请提供拉黑原因' }); + } + + await timeoutService.blacklistUser(parseInt(userId), reason.trim(), operatorId); + + res.json({ + success: true, + message: '用户已被拉黑' + }); + } catch (error) { + console.error('拉黑用户失败:', error); + res.status(500).json({ success: false, message: error.message || '拉黑用户失败' }); + } +}); + +/** + * 解除拉黑 + */ +router.post('/unblacklist/:userId', auth, requireAdmin, async (req, res) => { + try { + const { userId } = req.params; + const operatorId = req.user.id; + + await timeoutService.unblacklistUser(parseInt(userId), operatorId); + + res.json({ + success: true, + message: '已解除拉黑' + }); + } catch (error) { + console.error('解除拉黑失败:', error); + res.status(500).json({ success: false, message: error.message || '解除拉黑失败' }); + } +}); + +/** + * 获取超时转账列表 + */ +router.get('/overdue-transfers', auth, requireAdmin, async (req, res) => { + try { + const { page = 1, limit = 10 } = req.query; + const pageNum = parseInt(page, 10) || 1; + const limitNum = parseInt(limit, 10) || 10; + const offset = (pageNum - 1) * limitNum; + + const db = getDB(); + + // 获取总数 + const [countResult] = await db.execute( + 'SELECT COUNT(*) as total FROM transfers WHERE is_overdue = 1' + ); + 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 + WHERE t.is_overdue = 1 + ORDER BY t.overdue_at DESC + LIMIT ${limitNum} OFFSET ${offset}` + ); + + res.json({ + success: true, + data: { + transfers, + pagination: { + page: pageNum, + limit: limitNum, + total, + pages: Math.ceil(total / limitNum) + } + } + }); + } catch (error) { + console.error('获取超时转账列表失败:', error); + res.status(500).json({ success: false, message: '获取超时转账列表失败' }); + } +}); + +/** + * 手动检查转账超时 + */ +router.post('/check-timeouts', auth, requireAdmin, async (req, res) => { + try { + await timeoutService.checkTransferTimeouts(); + + res.json({ + success: true, + message: '转账超时检查已完成' + }); + } catch (error) { + console.error('手动检查转账超时失败:', error); + res.status(500).json({ success: false, message: '检查转账超时失败' }); + } +}); + +/** + * 获取风险管理统计信息 + */ +router.get('/stats', auth, requireAdmin, async (req, res) => { + try { + const db = getDB(); + + // 获取统计数据 + const [stats] = await db.execute( + `SELECT + COUNT(CASE WHEN is_risk_user = 1 THEN 1 END) as risk_users_count, + COUNT(CASE WHEN is_blacklisted = 1 THEN 1 END) as blacklisted_users_count, + COUNT(CASE WHEN is_risk_user = 1 AND is_blacklisted = 0 THEN 1 END) as risk_not_blacklisted_count + FROM users` + ); + + const [overdueStats] = await db.execute( + `SELECT + COUNT(*) as overdue_transfers_count, + SUM(amount) as overdue_amount_total + FROM transfers + WHERE is_overdue = 1` + ); + + const [todayOverdue] = await db.execute( + `SELECT COUNT(*) as today_overdue_count + FROM transfers + WHERE is_overdue = 1 AND DATE(overdue_at) = CURDATE()` + ); + + res.json({ + success: true, + data: { + riskUsersCount: stats[0].risk_users_count, + blacklistedUsersCount: stats[0].blacklisted_users_count, + riskNotBlacklistedCount: stats[0].risk_not_blacklisted_count, + overdueTransfersCount: overdueStats[0].overdue_transfers_count, + overdueAmountTotal: overdueStats[0].overdue_amount_total || 0, + todayOverdueCount: todayOverdue[0].today_overdue_count + } + }); + } 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/sms.js b/routes/sms.js new file mode 100644 index 0000000..c6f6ede --- /dev/null +++ b/routes/sms.js @@ -0,0 +1,245 @@ +const express = require('express') +const router = express.Router() +const { getDB } = require('../database') +const Dysmsapi20170525 = require('@alicloud/dysmsapi20170525') +const OpenApi = require('@alicloud/openapi-client') +const { Config } = require('@alicloud/openapi-client') + +// 阿里云短信配置 +const config = new Config({ + // 您的AccessKey ID + accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID || 'your_access_key_id', + // 您的AccessKey Secret + accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET || 'your_access_key_secret', + // 访问的域名 + endpoint: 'dysmsapi.aliyuncs.com' +}) + +// 创建短信客户端 +const client = new Dysmsapi20170525.default(config) + +// 短信模板配置 +const SMS_CONFIG = { + signName: process.env.ALIYUN_SMS_SIGN_NAME || '您的签名', // 短信签名 + templateCode: process.env.ALIYUN_SMS_TEMPLATE_CODE || 'SMS_XXXXXX', // 短信模板CODE + // 开发环境标识 + isDevelopment: process.env.NODE_ENV !== 'production' +} + +// 存储验证码的内存对象(生产环境建议使用Redis) +const smsCodeStore = new Map() + +// 验证码有效期(5分钟) +const CODE_EXPIRE_TIME = 5 * 60 * 1000 +// 最大尝试次数 +const MAX_ATTEMPTS = 3 +// 发送频率限制(60秒) +const SEND_INTERVAL = 60 * 1000 + +/** + * 生成6位数字验证码 + * @returns {string} 验证码 + */ +function generateSMSCode() { + return Math.floor(100000 + Math.random() * 900000).toString(); +} + +/** + * 发送短信验证码 + */ +router.post('/send', async (req, res) => { + try { + const { phone } = req.body + + // 验证手机号格式 + const phoneRegex = /^1[3-9]\d{9}$/ + if (!phoneRegex.test(phone)) { + return res.json({ + success: false, + message: '手机号格式不正确' + }) + } + + // 检查发送频率限制 + const lastSendTime = smsCodeStore.get(`last_send_${phone}`) + if (lastSendTime && Date.now() - lastSendTime < SEND_INTERVAL) { + const remainingTime = Math.ceil((SEND_INTERVAL - (Date.now() - lastSendTime)) / 1000) + return res.json({ + success: false, + 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()) + + // 开发环境下模拟发送成功 + // if (SMS_CONFIG.isDevelopment) { + // console.log(`[开发环境] 短信验证码: ${code} 发送到 ${phone}`) + // return res.json({ + // success: true, + // message: '验证码发送成功', + // // 开发环境下返回验证码便于测试 + // code: process.env.NODE_ENV === 'development' ? code : undefined + // }) + // } + + // 生产环境发送真实短信 + try { + 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); + + if (response.body.code === 'OK') { + res.json({ + success: true, + message: '验证码发送成功' + }) + } else { + console.error('阿里云短信发送失败:', response.body) + res.json({ + success: false, + message: '发送失败,请稍后重试' + }) + } + } catch (smsError) { + console.error('阿里云短信API调用失败:', smsError) + res.json({ + success: false, + message: '发送失败,请稍后重试' + }) + } + + } catch (error) { + console.error('发送短信验证码失败:', error) + res.status(500).json({ + success: false, + message: '发送失败,请稍后重试' + }) + } +}); + +/** + * 验证短信验证码 + */ +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}次` + }); + } + + // 验证成功,删除验证码 + smsCodeStore.delete(phone); + smsCodeStore.delete(`time_${phone}`); + + res.json({ + success: true, + message: '手机号验证成功', + data: { + phone: phone, + verified: true + } + }); + + } catch (error) { + console.error('验证短信验证码错误:', error); + res.status(500).json({ success: false, message: '验证失败' }); + } +}); + +/** + * 导出验证手机号的函数供其他模块使用 + * @param {string} phone 手机号 + * @param {string} code 验证码 + * @returns {boolean} 验证结果 + */ +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; +} + +// 清理过期验证码的定时任务 +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}`); + } + } +}, 60000); // 每分钟清理一次 + +module.exports = router; +module.exports.verifySMSCode = verifySMSCode; \ No newline at end of file diff --git a/routes/system.js b/routes/system.js new file mode 100644 index 0000000..ffdaea0 --- /dev/null +++ b/routes/system.js @@ -0,0 +1,321 @@ +const express = require('express'); +const { auth } = require('../middleware/auth'); +const { validateQuery, validate } = require('../middleware/validation'); +const { logger } = require('../config/logger'); +const { HTTP_STATUS } = require('../config/constants'); +const { getDB } = require('../database'); +const Joi = require('joi'); + +const router = express.Router(); + +/** + * 系统设置验证规则 + */ +const systemSchemas = { + // 更新系统设置 + updateSettings: Joi.object({ + basic: Joi.object({ + siteName: Joi.string().max(100).allow(''), + siteDescription: Joi.string().max(500).allow(''), + siteKeywords: Joi.string().max(200).allow(''), + siteLogo: Joi.string().allow(''), + siteFavicon: Joi.string().allow(''), + + icp: Joi.string().max(100).allow('') + }).optional(), + features: Joi.object({ + allowRegister: Joi.boolean(), + allowTransfer: Joi.boolean(), + allowExchange: Joi.boolean(), + allowReview: Joi.boolean(), + allowComment: Joi.boolean() + }).optional(), + + security: Joi.object({ + maxLoginAttempts: Joi.number().integer().min(1).max(100), + lockoutDuration: Joi.number().integer().min(1).max(86400), + ipWhitelist: Joi.string().allow('') + }).optional() + }) +}; + +/** + * 获取系统设置 (公开接口,不需要认证) + * GET /api/system/settings + */ +router.get('/settings', async (req, res, next) => { + try { + + const db = getDB(); + + // 获取系统设置 + const [settings] = await db.execute( + 'SELECT setting_key, setting_value FROM system_settings' + ); + + // 组织设置数据 + const settingsData = { + basic: { + siteName: '', + siteDescription: '', + siteKeywords: '', + siteLogo: '', + siteFavicon: '', + + icp: '' + }, + features: { + allowRegister: true, + allowTransfer: true, + allowExchange: true, + allowReview: true, + allowComment: true + }, + + security: { + maxLoginAttempts: 5, + lockoutDuration: 300, + ipWhitelist: '' + } + }; + + // 填充数据库中的设置 + settings.forEach(setting => { + const keys = setting.setting_key.split('.'); + if (keys.length === 2 && settingsData[keys[0]]) { + try { + // 尝试解析JSON值,如果失败则使用原始值 + settingsData[keys[0]][keys[1]] = JSON.parse(setting.setting_value); + } catch { + settingsData[keys[0]][keys[1]] = setting.setting_value; + } + } + }); + + logger.info('System settings retrieved', { + settingsCount: settings.length + }); + + res.json({ + success: true, + data: settingsData + }); + } catch (error) { + next(error); + } +}); + +/** + * 更新系统设置 + * PUT /api/system/settings + */ +router.put('/settings', + auth, + validate(systemSchemas.updateSettings), + async (req, res, next) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(HTTP_STATUS.FORBIDDEN).json({ + success: false, + message: '权限不足' + }); + } + + const db = getDB(); + const settings = req.body; + + // 开始事务 + await db.beginTransaction(); + + try { + // 更新设置 + for (const [category, categorySettings] of Object.entries(settings)) { + for (const [key, value] of Object.entries(categorySettings)) { + const settingKey = `${category}.${key}`; + const settingValue = JSON.stringify(value); + + await db.execute( + `INSERT INTO system_settings (setting_key, setting_value, updated_at) + VALUES (?, ?, NOW()) + ON DUPLICATE KEY UPDATE + setting_value = VALUES(setting_value), + updated_at = VALUES(updated_at)`, + [settingKey, settingValue] + ); + } + } + + await db.commit(); + + logger.info('System settings updated', { + userId: req.user.id, + categories: Object.keys(settings) + }); + + res.json({ + success: true, + message: '系统设置更新成功' + }); + } catch (error) { + await db.rollback(); + throw error; + } + } catch (error) { + next(error); + } + } +); + +/** + * 获取系统信息 + * GET /api/system/info + */ +router.get('/info', auth, async (req, res, next) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(HTTP_STATUS.FORBIDDEN).json({ + success: false, + message: '权限不足' + }); + } + + const systemInfo = { + version: '1.0.0', + nodeVersion: process.version, + platform: process.platform, + uptime: process.uptime(), + memoryUsage: process.memoryUsage(), + timestamp: new Date().toISOString() + }; + + res.json({ + success: true, + data: systemInfo + }); + } catch (error) { + next(error); + } +}); + +/** + * 获取维护模式状态(公开接口) + * GET /api/system/maintenance-status + */ +router.get('/maintenance-status', async (req, res, next) => { + try { + const db = getDB(); + + // 从系统设置表获取维护模式状态 + const [rows] = await db.execute( + 'SELECT setting_value FROM system_settings WHERE setting_key = ?', + ['maintenance_mode'] + ); + + const maintenanceMode = rows.length > 0 ? rows[0].setting_value === 'true' : false; + + res.json({ + success: true, + data: { + maintenance_mode: maintenanceMode + } + }); + + } catch (error) { + console.error('获取维护模式状态失败:', error); + next(error); + } +}); + +/** + * 获取维护模式状态(管理员接口) + * GET /api/system/admin/maintenance-status + */ +router.get('/admin/maintenance-status', auth, async (req, res, next) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({ + success: false, + error: { + code: 'FORBIDDEN', + message: '权限不足' + } + }); + } + + const db = getDB(); + + // 从系统设置表获取维护模式状态 + const [rows] = await db.execute( + 'SELECT setting_value FROM system_settings WHERE setting_key = ?', + ['maintenance_mode'] + ); + + const maintenanceMode = rows.length > 0 ? rows[0].setting_value === 'true' : false; + + res.json({ + success: true, + data: { + maintenance_mode: maintenanceMode + } + }); + + } catch (error) { + console.error('获取维护模式状态失败:', error); + next(error); + } +}); + +/** + * 切换维护模式 + * POST /api/system/toggle-maintenance + */ +router.post('/toggle-maintenance', auth, async (req, res, next) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({ + success: false, + error: { + code: 'FORBIDDEN', + message: '权限不足' + } + }); + } + + const { maintenance_mode } = req.body; + const db = getDB(); + + // 更新或插入维护模式设置 + await db.execute( + `INSERT INTO system_settings (setting_key, setting_value, updated_at) + VALUES ('maintenance_mode', ?, NOW()) + ON DUPLICATE KEY UPDATE + setting_value = VALUES(setting_value), + updated_at = NOW()`, + [maintenance_mode ? 'true' : 'false'] + ); + + logger.info('Maintenance mode toggled', { + userId: req.user.id, + username: req.user.username, + maintenanceMode: maintenance_mode + }); + + res.json({ + success: true, + data: { + maintenance_mode: maintenance_mode + }, + message: maintenance_mode ? '维护模式已开启' : '维护模式已关闭' + }); + + } catch (error) { + console.error('切换维护模式失败:', error); + next(error); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/transfers.js b/routes/transfers.js new file mode 100644 index 0000000..bf107df --- /dev/null +++ b/routes/transfers.js @@ -0,0 +1,1237 @@ +const express = require('express'); +const transferService = require('../services/transferService'); +const { auth: authenticateToken } = require('../middleware/auth'); +const { validate, validateQuery, transferSchemas, commonSchemas } = require('../middleware/validation'); +const { logger } = require('../config/logger'); +const { HTTP_STATUS } = require('../config/constants'); +const { getDB } = require('../database'); +const multer = require('multer'); +const path = require('path'); +const dayjs = require('dayjs'); + +const router = express.Router(); + +// 配置文件上传 +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 + } +}); + +/** + * 获取转账列表 + * @param {string} status - 转账状态过滤 + * @param {string} transfer_type - 转账类型过滤 + * @param {string} start_date - 开始日期过滤 + * @param {string} end_date - 结束日期过滤 + * @param {string} search - 搜索关键词(用户名或真实姓名) + * @param {number} page - 页码 + * @param {number} limit - 每页数量 + * @param {string} sort - 排序字段 + * @param {string} order - 排序方向(asc/desc) + */ +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 filters = { + status, + transfer_type, + start_date, + end_date, + search + }; + + // 非管理员只能查看自己相关的转账 + if (req.user.role !== 'admin') { + filters.user_id = req.user.id; + } + + const result = await transferService.getTransfers(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), + async (req, res, next) => { + try { + const { page, limit, status, transfer_type, start_date, end_date, sort, order } = req.query; + + const filters = { + status, + transfer_type, + start_date, + end_date + }; + + // 非管理员只能查看自己相关的转账 + if (req.user.role !== 'admin') { + filters.user_id = req.user.id; + } + + const result = await transferService.getTransfers(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('/public-account', authenticateToken, async (req, res) => { + try { + const db = getDB(); + const [publicUser] = await db.execute(` + SELECT id, username, real_name, balance + FROM users + WHERE username = 'public_account' AND is_system_account = TRUE + `); + + if (publicUser.length === 0) { + return res.status(404).json({ success: false, message: '公户不存在' }); + } + + res.json({ success: true, data: publicUser[0] }); + } catch (error) { + console.error('获取公户信息失败:', error); + res.status(500).json({ success: false, message: '服务器错误' }); + } +}); + +// 创建转账记录 +router.post('/create', + authenticateToken, + validate(transferSchemas.create), + async (req, res, next) => { + try { + const result = await transferService.createTransfer(req.user.id, req.body); + + logger.info('Transfer creation requested', { + userId: req.user.id, + transferId: result.transfer_id, + amount: req.body.amount + }); + + res.status(HTTP_STATUS.CREATED).json({ + success: true, + message: '转账记录创建成功,等待确认', + data: result + }); + } catch (error) { + next(error); + } + } +); + +// 管理员创建转账记录 +router.post('/admin/create', + authenticateToken, + async (req, res, next) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({ success: false, message: '权限不足' }); + } + + const { from_user_id, to_user_id, amount, transfer_type, description } = req.body; + + // 验证必填字段 + if (!from_user_id || !to_user_id || !amount || !transfer_type) { + return res.status(400).json({ success: false, message: '缺少必填字段' }); + } + + const result = await transferService.createTransfer(from_user_id, { + to_user_id, + amount, + transfer_type, + description: description || '管理员分配转账' + }); + + logger.info('Admin transfer creation requested', { + adminId: req.user.id, + fromUserId: from_user_id, + toUserId: to_user_id, + transferId: result.transfer_id, + amount: amount + }); + + res.status(HTTP_STATUS.CREATED).json({ + success: true, + message: '转账分配成功', + data: result + }); + } catch (error) { + next(error); + } + } +); + +// 确认转账 +router.post('/confirm', + authenticateToken, + validate(transferSchemas.confirm), + async (req, res, next) => { + try { + const { transfer_id, note } = req.body; + + await transferService.confirmTransfer(transfer_id, note, req.user.id); + + logger.info('Transfer confirmed', { + transferId: transfer_id, + operatorId: req.user.id + }); + + res.json({ + success: true, + message: '转账确认成功' + }); + } catch (error) { + next(error); + } + } +); + +// 确认转账(路径参数形式) +router.post('/confirm/:id', + authenticateToken, + async (req, res, next) => { + try { + const transfer_id = req.params.id; + const { action, note } = req.body; + + // 验证action参数 + if (action !== 'confirm') { + return res.status(400).json({ + success: false, + message: 'action参数必须为confirm' + }); + } + + await transferService.confirmTransfer(transfer_id, note, req.user.id); + + logger.info('Transfer confirmed via path param', { + transferId: transfer_id, + operatorId: req.user.id + }); + + res.json({ + success: true, + message: '转账确认成功' + }); + } catch (error) { + next(error); + } + } +); + +// 拒绝转账 +router.post('/reject', + authenticateToken, + validate(transferSchemas.reject), + async (req, res, next) => { + try { + const { transfer_id, note='' } = req.body; + + await transferService.rejectTransfer(transfer_id, note, req.user.id); + + logger.info('Transfer rejected', { + transferId: transfer_id, + operatorId: req.user.id + }); + + res.json({ + success: true, + message: '转账已拒绝' + }); + } catch (error) { + next(error); + } + } +); + +// 用户确认收到转账 +router.post('/confirm-received', + authenticateToken, + async (req, res, next) => { + try { + const { transfer_id } = req.body; + + if (!transfer_id) { + return res.status(400).json({ success: false, message: '缺少转账ID' }); + } + + await transferService.confirmReceived(transfer_id, req.user.id); + + logger.info('Transfer received confirmed by user', { + transferId: transfer_id, + userId: req.user.id + }); + + res.json({ + success: true, + message: '已确认收到转账,余额已更新' + }); + } catch (error) { + next(error); + } + } +); + +// 用户确认未收到转账 +router.post('/confirm-not-received', + authenticateToken, + async (req, res, next) => { + try { + const { transfer_id } = req.body; + + if (!transfer_id) { + return res.status(400).json({ success: false, message: '缺少转账ID' }); + } + + await transferService.confirmNotReceived(transfer_id, req.user.id); + + logger.info('Transfer not received confirmed by user', { + transferId: transfer_id, + userId: req.user.id + }); + + res.json({ + success: true, + message: '已确认未收到转账' + }); + } catch (error) { + next(error); + } + } +); + +// 触发返还转账逻辑 +async function triggerReturnTransfers(db, user_id, total_amount) { + // 将总金额分成3笔随机金额 + const amounts = generateRandomAmounts(total_amount, 3); + const batch_id = `return_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + // 获取公户ID + const [publicAccount] = await db.execute(` + SELECT u.id FROM users u WHERE u.username = 'public_account' + `); + + if (publicAccount.length === 0) { + throw new Error('公户不存在'); + } + + const public_user_id = publicAccount[0].id; + + // 创建3笔返还转账记录 + for (let i = 0; i < amounts.length; i++) { + await db.execute(` + INSERT INTO transfers (from_user_id, to_user_id, amount, transfer_type, description, batch_id, status) + VALUES (?, ?, ?, 'return', ?, ?, 'pending') + `, [ + public_user_id, + user_id, + amounts[i], + `返还转账 ${i + 1}/3`, + batch_id + ]); + } +} + +// 生成随机金额分配 +function generateRandomAmounts(total, count) { + const amounts = []; + let remaining = parseFloat(total); + + for (let i = 0; i < count - 1; i++) { + // 确保每笔至少1元,最多不超过剩余金额的80% + const min = 1; + const max = Math.max(min, remaining * 0.8); + const amount = Math.round((Math.random() * (max - min) + min) * 100) / 100; + amounts.push(amount); + remaining -= amount; + } + + // 最后一笔是剩余金额 + amounts.push(Math.round(remaining * 100) / 100); + + return amounts; +} + +// 获取用户转账记录 +router.get('/user/:userId', authenticateToken, async (req, res) => { + try { + const userId = req.params.userId; + const { page = 1, limit = 10, status } = req.query; + + // 检查权限(只能查看自己的记录或管理员查看所有) + // if (req.user.id != userId && req.user.role !== 'admin') { + // return res.status(403).json({ success: false, message: '权限不足' }); + // } + + const db = getDB(); + + // 确保参数为有效数字 + const pageNum = Math.max(1, parseInt(page) || 1); + const limitNum = Math.max(1, Math.min(100, parseInt(limit) || 10)); + const offset = Math.max(0, (pageNum - 1) * limitNum); + + let whereClause = 'WHERE (t.from_user_id = ? OR t.to_user_id = ?)'; + const userIdInt = parseInt(userId); + let listParams = [userIdInt, userIdInt]; + let countParams = [userIdInt, userIdInt]; + + if (status) { + whereClause += ' AND t.status = ?'; + listParams.push(status); + countParams.push(status); + } + + // 添加分页参数 + listParams.push(limitNum.toString(), offset.toString()); + + const [transfers] = await db.execute(` + SELECT + t.*, + from_user.username as from_username, + from_user.real_name as from_real_name, + to_user.username as to_username, + to_user.real_name as to_real_name + FROM transfers t + LEFT JOIN users from_user ON t.from_user_id = from_user.id + LEFT JOIN users to_user ON t.to_user_id = to_user.id + ${whereClause} + ORDER BY t.created_at DESC + LIMIT ? OFFSET ? + `, listParams); + + const [countResult] = await db.execute(` + SELECT COUNT(*) as total FROM transfers t ${whereClause} + `, countParams); + + res.json({ + success: true, + data: { + transfers, + pagination: { + page: pageNum, + limit: limitNum, + total: countResult[0].total, + pages: Math.ceil(countResult[0].total / limitNum) + } + } + }); + } catch (error) { + console.error('获取转账记录失败:', error); + res.status(500).json({ success: false, message: '服务器错误' }); + } +}); + +// 获取转账统计信息 +router.get('/stats', authenticateToken, async (req, res) => { + try { + const userId = req.user.id; + const isAdmin = req.user.role === 'admin'; + const db = getDB(); + + let stats = {}; + + if (isAdmin) { + // 管理员可以查看全局统计 + const [totalStats] = await db.execute(` + SELECT + COUNT(*) as total_transfers, + SUM(amount) as total_flow_amount, + SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count, + SUM(CASE WHEN status = 'confirmed' THEN 1 ELSE 0 END) as confirmed_count, + SUM(CASE WHEN status = 'received' THEN 1 ELSE 0 END) as received_count, + SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected_count, + SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_count, + SUM(CASE WHEN status = 'not_received' THEN 1 ELSE 0 END) as not_received_count, + SUM(CASE WHEN is_overdue = 1 THEN 1 ELSE 0 END) as overdue_count, + SUM(CASE WHEN is_bad_debt = 1 THEN 1 ELSE 0 END) as bad_debt_count, + SUM(CASE WHEN status = 'confirmed' THEN amount ELSE 0 END) as total_amount, + SUM(CASE WHEN is_bad_debt = 1 THEN amount ELSE 0 END) as bad_debt_amount, + SUM(CASE WHEN transfer_type = 'initial' AND status = 'confirmed' THEN amount ELSE 0 END) as initial_amount, + SUM(CASE WHEN transfer_type = 'return' AND status = 'confirmed' THEN amount ELSE 0 END) as return_amount, + SUM(CASE WHEN transfer_type = 'user_to_user' AND status = 'confirmed' THEN amount ELSE 0 END) as user_to_user_amount, + (SELECT SUM(balance) FROM users WHERE role = 'user' AND is_system_account = 1) as total_merchant_balance, + COUNT(CASE WHEN status IN ('confirmed', 'received') THEN 1 END) as participated_transfers + FROM transfers + `); + + const todayStr = dayjs().format('YYYY-MM-DD'); + const currentYear = dayjs().year(); + const currentMonth = dayjs().month() + 1; + console.log(todayStr,'todayStr'); + + const [todayStats] = await db.execute(` + SELECT + COUNT(*) as today_transfers, + ( + COALESCE((SELECT SUM(amount) FROM transfers WHERE DATE(created_at) = ? AND to_user_id IN (SELECT id FROM users WHERE is_system_account = 1) AND status = 'received'), 0) - + COALESCE((SELECT SUM(amount) FROM transfers WHERE DATE(created_at) = ? AND from_user_id IN (SELECT id FROM users WHERE is_system_account = 1) AND status = 'received'), 0) + ) as today_amount + FROM transfers + WHERE DATE(created_at) = ? + `, [todayStr, todayStr, todayStr]); + + const [monthlyStats] = await db.execute(` + SELECT + COUNT(*) as monthly_transfers, + SUM(CASE WHEN status = 'received' THEN amount ELSE 0 END) as monthly_amount, + COUNT(CASE WHEN status IN ('confirmed', 'received') THEN 1 END) as monthly_participated_transfers + FROM transfers + WHERE YEAR(created_at) = ? AND MONTH(created_at) = ? + `, [currentYear, currentMonth]); + + // 获取上月统计数据用于对比 + const lastMonth = currentMonth === 1 ? 12 : currentMonth - 1; + const lastMonthYear = currentMonth === 1 ? currentYear - 1 : currentYear; + + const [lastMonthStats] = await db.execute(` + SELECT + COUNT(*) as last_monthly_transfers, + SUM(CASE WHEN status = 'confirmed' THEN amount ELSE 0 END) as last_monthly_amount, + COUNT(CASE WHEN status IN ('confirmed', 'received') THEN 1 END) as last_monthly_participated_transfers + FROM transfers + WHERE YEAR(created_at) = ? AND MONTH(created_at) = ? + `, [lastMonthYear, lastMonth]); + + stats = { + total: { + transfers: totalStats[0].total_transfers || 0, + pending: parseFloat(totalStats[0].total_flow_amount || 0), + pending_count: totalStats[0].pending_count || 0, + confirmed: totalStats[0].confirmed_count || 0, + received_count: totalStats[0].received_count || 0, + rejected: totalStats[0].rejected_count || 0, + cancelled_count: totalStats[0].cancelled_count || 0, + not_received_count: totalStats[0].not_received_count || 0, + overdue: totalStats[0].overdue_count || 0, + bad_debt: totalStats[0].bad_debt_count || 0, + amount: parseFloat(totalStats[0].total_amount || 0), + bad_debt_amount: parseFloat(totalStats[0].bad_debt_amount || 0), + total_merchant_balance: parseFloat(totalStats[0].total_merchant_balance || 0), + initial_amount: parseFloat(totalStats[0].initial_amount || 0), + return_amount: parseFloat(totalStats[0].return_amount || 0), + user_to_user_amount: parseFloat(totalStats[0].user_to_user_amount || 0), + participated_transfers: totalStats[0].participated_transfers || 0 + }, + today: { + transfers: todayStats[0].today_transfers || 0, + amount: parseFloat(todayStats[0].today_amount || 0) + }, + monthly: { + transfers: monthlyStats[0].monthly_transfers || 0, + amount: parseFloat(monthlyStats[0].monthly_amount || 0), + participated_transfers: monthlyStats[0].monthly_participated_transfers || 0 + }, + lastMonth: { + transfers: lastMonthStats[0].last_monthly_transfers || 0, + amount: parseFloat(lastMonthStats[0].last_monthly_amount || 0), + participated_transfers: lastMonthStats[0].last_monthly_participated_transfers || 0 + } + }; + } else { + // 普通用户只能查看自己的统计 + const [userStats] = await db.execute(` + SELECT + COUNT(*) as total_transfers, + SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count, + SUM(CASE WHEN status = 'confirmed' THEN 1 ELSE 0 END) as confirmed_count, + SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected_count, + SUM(CASE WHEN status = 'confirmed' AND from_user_id = ? THEN amount ELSE 0 END) as sent_amount, + SUM(CASE WHEN status = 'confirmed' AND to_user_id = ? THEN amount ELSE 0 END) as received_amount + FROM transfers + WHERE from_user_id = ? OR to_user_id = ? + `, [userId, userId, userId, userId]); + + const todayStr = dayjs().format('YYYY-MM-DD'); + + const [todayStats] = await db.execute(` + SELECT + COUNT(*) as today_transfers, + SUM(CASE WHEN status = 'confirmed' AND from_user_id = ? THEN amount ELSE 0 END) as today_sent, + SUM(CASE WHEN status = 'confirmed' AND to_user_id = ? THEN amount ELSE 0 END) as today_received + FROM transfers + WHERE (from_user_id = ? OR to_user_id = ?) AND DATE(created_at) = ? + `, [userId, userId, userId, userId, todayStr]); + + stats = { + total: { + transfers: userStats[0].total_transfers || 0, + pending: userStats[0].pending_count || 0, + confirmed: userStats[0].confirmed_count || 0, + rejected: userStats[0].rejected_count || 0, + sent_amount: parseFloat(userStats[0].sent_amount || 0), + received_amount: parseFloat(userStats[0].received_amount || 0) + }, + today: { + transfers: todayStats[0].today_transfers || 0, + sent_amount: parseFloat(todayStats[0].today_sent || 0), + received_amount: parseFloat(todayStats[0].today_received || 0) + } + }; + } + + res.json({ success: true, data: stats }); + } catch (error) { + console.error('获取转账统计失败:', error); + res.status(500).json({ success: false, message: '获取转账统计失败' }); + } +}); + +// 获取待确认的转账 +router.get('/pending', authenticateToken, async (req, res) => { + try { + const userId = parseInt(req.user.id); + const db = getDB(); + + const [transfers] = await db.execute(` + SELECT + t.*, + from_user.username as from_username, + from_user.real_name as from_real_name + FROM transfers t + LEFT JOIN users from_user ON t.from_user_id = from_user.id + WHERE t.to_user_id = ? AND t.status = 'pending' + ORDER BY t.created_at DESC + `, [userId]); + + res.json({ success: true, data: transfers }); + } catch (error) { + console.error('获取待确认转账失败:', error); + res.status(500).json({ success: false, message: '服务器错误' }); + } +}); + +// 获取当前用户账户信息(不需要传递用户ID) +router.get('/account', authenticateToken, async (req, res) => { + try { + const userId = req.user.id; + + const db = getDB(); + const [user] = await db.execute(` + SELECT id, username, real_name, balance, created_at, updated_at, points + FROM users + WHERE id = ? + `, [userId]); + + if (user.length === 0) { + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + // 返回用户账户信息,格式与原来的 accounts 表保持一致 + const accountData = { + id: user[0].id, + user_id: user[0].id, + account_type: 'user', + balance: user[0].balance, + username: user[0].username, + real_name: user[0].real_name, + created_at: user[0].created_at, + updated_at: user[0].updated_at, + points: user[0].points + }; + + res.json({ success: true, data: accountData }); + } catch (error) { + console.error('获取账户信息失败:', error); + res.status(500).json({ success: false, message: '服务器错误' }); + } +}); + +// 获取指定用户账户信息(管理员权限或用户本人) +router.get('/account/:userId', authenticateToken, async (req, res) => { + try { + const userId = req.params.userId; + + // 检查权限 + if (req.user.id != userId && req.user.role !== 'admin') { + return res.status(403).json({ success: false, message: '权限不足' }); + } + + const db = getDB(); + const [user] = await db.execute(` + SELECT id, username, real_name, balance, created_at, updated_at + FROM users + WHERE id = ? + `, [userId]); + + if (user.length === 0) { + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + // 返回用户账户信息,格式与原来的 accounts 表保持一致 + const accountData = { + id: user[0].id, + user_id: user[0].id, + account_type: 'user', + balance: user[0].balance, + username: user[0].username, + real_name: user[0].real_name, + created_at: user[0].created_at, + updated_at: user[0].updated_at + }; + + res.json({ success: true, data: accountData }); + } catch (error) { + console.error('获取账户信息失败:', error); + res.status(500).json({ success: false, message: '服务器错误' }); + } +}); + +// 获取转账趋势数据(管理员权限) +router.get('/trend', authenticateToken, async (req, res) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({ success: false, message: '权限不足' }); + } + + const db = getDB(); + const { days = 7 } = req.query; + const daysNum = Math.min(30, Math.max(1, parseInt(days) || 7)); + + // 首先获取数据库中最早和最晚的转账日期 + const [dateRange] = await db.execute(` + SELECT + MIN(DATE(created_at)) as min_date, + MAX(DATE(created_at)) as max_date, + COUNT(*) as total_count + FROM transfers + `); + + if (dateRange[0].total_count === 0) { + // 如果没有转账记录,返回空数据 + const result = []; + const now = new Date(); + + for (let i = daysNum - 1; i >= 0; i--) { + const date = dayjs().subtract(i, 'day'); + result.push({ + date: date.format('MM-DD'), + count: 0, + amount: 0 + }); + } + + return res.json({ + success: true, + data: result + }); + } + + // 获取最近的转账数据(基于实际数据的最大日期) + const maxDate = dayjs(dateRange[0].max_date); + + // 获取指定天数内的转账趋势(从最大日期往前推) + const [trendData] = await db.execute(` + SELECT + DATE(created_at) as date, + COUNT(*) as count, + SUM(amount) as amount + FROM transfers + WHERE DATE(created_at) >= DATE_SUB(?, INTERVAL ? DAY) + AND status IN ('confirmed', 'received') + GROUP BY DATE(created_at) + ORDER BY date ASC + `, [dateRange[0].max_date, daysNum - 1]); + + // 填充缺失的日期(转账数为0) + const result = []; + + for (let i = daysNum - 1; i >= 0; i--) { + const date = maxDate.subtract(i, 'day'); + const dateStr = date.format('YYYY-MM-DD'); + + // 修复日期比较:将数据库返回的Date对象转换为字符串进行比较 + const existingData = trendData.find(item => { + const itemDateStr = dayjs(item.date).format('YYYY-MM-DD'); + return itemDateStr === dateStr; + }); + + result.push({ + date: date.format('MM-DD'), + count: existingData ? existingData.count : 0, + amount: existingData ? parseFloat(existingData.amount) : 0 + }); + } + + res.json({ + success: true, + data: result + }); + } catch (error) { + console.error('获取转账趋势错误:', error); + res.status(500).json({ success: false, message: '获取转账趋势失败' }); + } +}); + +// 管理员解除坏账(管理员权限) +router.post('/remove-bad-debt/:transferId', authenticateToken, async (req, res) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({ success: false, message: '权限不足' }); + } + + const { transferId } = req.params; + const { reason } = req.body; + const adminId = req.user.id; + + // 验证转账ID + if (!transferId || isNaN(transferId)) { + return res.status(400).json({ success: false, message: '无效的转账ID' }); + } + + const result = await transferService.removeBadDebt(transferId, adminId, reason); + + res.json({ + success: true, + message: '坏账标记已解除', + data: result + }); + } catch (error) { + console.error('解除坏账失败:', error); + if (error.statusCode) { + return res.status(error.statusCode).json({ + success: false, + message: error.message + }); + } + res.status(500).json({ success: false, message: '解除坏账失败' }); + } +}); + +// 强制变更转账状态(管理员权限) +router.post('/force-change-status/:transferId', authenticateToken, async (req, res) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({ success: false, message: '权限不足' }); + } + + const { transferId } = req.params; + const { newStatus, status, reason, adjust_balance = false } = req.body; + // 兼容两种参数名:newStatus 和 status + const actualNewStatus = newStatus || status; + const adminId = req.user.id; + + // 验证转账ID + if (!transferId || isNaN(transferId)) { + return res.status(400).json({ success: false, message: '无效的转账ID' }); + } + + // 验证必填参数 + if (!actualNewStatus) { + return res.status(400).json({ success: false, message: '新状态不能为空' }); + } + + if (!reason) { + return res.status(400).json({ success: false, message: '变更原因不能为空' }); + } + + const result = await transferService.forceChangeTransferStatus( + transferId, + actualNewStatus, + reason, + adminId, + adjust_balance + ); + + res.json({ + success: true, + message: `转账状态已从 ${result.oldStatus} 变更为 ${result.newStatus}`, + data: result + }); + } catch (error) { + console.error('强制变更转账状态失败:', error); + if (error.statusCode) { + return res.status(error.statusCode).json({ + success: false, + message: error.message + }); + } + res.status(500).json({ success: false, message: '变更转账状态失败' }); + } +}); + +// 管理员查看数据库连接状态 +router.get('/admin/database/status', authenticateToken, async (req, res) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({ success: false, message: '权限不足' }); + } + + const dbMonitor = require('../db-monitor'); + const diagnosis = await dbMonitor.diagnose(); + + res.json({ + success: true, + data: diagnosis + }); + } catch (error) { + logger.error('Get database status failed', { + adminId: req.user.id, + error: error.message + }); + res.status(500).json({ + success: false, + message: '获取数据库状态失败: ' + error.message + }); + } +}); + +// 管理员获取数据库监控报告 +router.get('/admin/database/report', authenticateToken, async (req, res) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({ success: false, message: '权限不足' }); + } + + const dbMonitor = require('../db-monitor'); + const report = await dbMonitor.generateReport(); + + res.json({ + success: true, + data: { + report, + timestamp: new Date().toISOString() + } + }); + } catch (error) { + logger.error('Get database report failed', { + adminId: req.user.id, + error: error.message + }); + res.status(500).json({ + success: false, + message: '获取数据库报告失败: ' + error.message + }); + } +}); + +/** + * 获取待处理的匹配转账订单 + * @param {number} page - 页码 + * @param {number} limit - 每页数量 + * @param {string} status - 状态过滤 + * @param {string} search - 搜索关键词(用户名或真实姓名) + * @param {string} sort - 排序字段 + * @param {string} order - 排序方向(asc/desc) + */ +router.get('/pending-allocations', + authenticateToken, + async (req, res, next) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({ success: false, message: '权限不足' }); + } + + const { + page = 1, + limit = 20, + status = '', + search = '', + sort = 'created_at', + order = 'desc' + } = req.query; + + const db = getDB(); + const offset = (parseInt(page) - 1) * parseInt(limit); + + // 构建查询条件 + let whereConditions = []; + let queryParams = []; + + // 状态过滤 + if (status) { + whereConditions.push('oa.status = ?'); + queryParams.push(status); + } + + // 搜索过滤(用户名或真实姓名) + if (search) { + whereConditions.push('(uf.username LIKE ? OR uf.real_name LIKE ? OR ut.username LIKE ? OR ut.real_name LIKE ?)'); + const searchPattern = `%${search}%`; + queryParams.push(searchPattern, searchPattern, searchPattern, searchPattern); + } + + const whereClause = whereConditions.length > 0 ? 'WHERE ' + whereConditions.join(' AND ') : ''; + + // 验证排序字段 + const allowedSortFields = ['created_at', 'amount', 'status', 'cycle_number']; + const sortField = allowedSortFields.includes(sort) ? sort : 'created_at'; + const sortOrder = order.toLowerCase() === 'asc' ? 'ASC' : 'DESC'; + + // 获取总数 + const countQuery = ` + SELECT COUNT(*) as total + FROM transfers oa + JOIN users uf ON oa.from_user_id = uf.id + JOIN users ut ON oa.to_user_id = ut.id + JOIN matching_orders mo ON oa.id = mo.id + ${whereClause} + `; + + const [countResult] = await db.execute(countQuery, queryParams); + const total = countResult[0].total; + + // 使用 query 方法避免 LIMIT/OFFSET 参数问题 + const dataQuery = ` + SELECT + oa.id, + oa.from_user_id, + oa.to_user_id, + oa.amount, + oa.cycle_number, + oa.status, + oa.outbound_date, + oa.return_date, + oa.can_return_after, + oa.confirmed_at, + oa.created_at, + oa.updated_at, + uf.username as from_username, + uf.real_name as from_real_name, + ut.username as to_username, + ut.real_name as to_real_name, + mo.amount as order_total_amount, + mo.status as order_status, + mo.matching_type, + t.status as transfer_status, + t.voucher_url + FROM transfers oa + JOIN users uf ON oa.from_user_id = uf.id + JOIN users ut ON oa.to_user_id = ut.id + JOIN matching_orders mo ON oa.id = mo.id + ${whereClause} + ORDER BY oa.${sortField} ${sortOrder} + LIMIT ${parseInt(limit)} OFFSET ${parseInt(offset)} + `; + + const [allocations] = queryParams.length > 0 + ? await db.execute(dataQuery, queryParams) + : await db.query(dataQuery); + + // 计算分页信息 + const totalPages = Math.ceil(total / parseInt(limit)); + + logger.info('Pending allocations list requested', { + userId: req.user.id, + page: parseInt(page), + limit: parseInt(limit), + total, + resultCount: allocations.length + }); + + res.json({ + success: true, + data: { + allocations, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + totalPages + } + } + }); + } catch (error) { + logger.error('Get pending allocations failed', { + userId: req.user.id, + error: error.message + }); + next(error); + } + } +); + +/** + * 获取待处理匹配订单的统计信息 + */ +router.get('/pending-allocations/stats', + authenticateToken, + async (req, res, next) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({ success: false, message: '权限不足' }); + } + + const db = getDB(); + + // 获取统计数据 + const [stats] = await db.execute(` + SELECT + COUNT(*) as total_allocations, + COUNT(CASE WHEN oa.status = 'pending' THEN 1 END) as pending_count, + COUNT(CASE WHEN oa.status = 'confirmed' THEN 1 END) as confirmed_count, + COUNT(CASE WHEN oa.status = 'completed' THEN 1 END) as completed_count, + SUM(oa.amount) as total_amount, + SUM(CASE WHEN oa.status = 'pending' THEN oa.amount ELSE 0 END) as pending_amount + FROM transfers oa + JOIN matching_orders mo ON oa.id = mo.id + WHERE mo.status != 'cancelled' + `); + + res.json({ + success: true, + data: stats[0] + }); + } catch (error) { + logger.error('Get pending allocations stats failed', { + userId: req.user.id, + error: error.message + }); + next(error); + } + } +); + +/** + * 获取昨日用户转账统计列表 + */ +router.get('/daily-stats', + authenticateToken, + async (req, res, next) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({ success: false, message: '权限不足' }); + } + + const db = getDB(); + + // 获取昨日和今日的日期范围(从0点开始计算) + // 今日0点到23:59:59 + const todayStart = dayjs().startOf('day'); + const todayEnd = dayjs().endOf('day'); + + // 昨日0点到23:59:59 + const yesterdayStart = dayjs().subtract(1, 'day').startOf('day'); + const yesterdayEnd = dayjs().subtract(1, 'day').endOf('day'); + + // 转换为MySQL兼容的字符串格式 + const todayStartStr = todayStart.format('YYYY-MM-DD HH:mm:ss'); + const todayEndStr = todayEnd.format('YYYY-MM-DD HH:mm:ss'); + const yesterdayStartStr = yesterdayStart.format('YYYY-MM-DD HH:mm:ss'); + const yesterdayEndStr = yesterdayEnd.format('YYYY-MM-DD HH:mm:ss'); + + // 使用dayjs格式化日期字符串用于返回 + const todayStr = todayStart.format('YYYY-MM-DD'); + const yesterdayStr = yesterdayStart.format('YYYY-MM-DD'); + + // 获取所有用户的昨日转出和今日入账统计 + let [userStats] = await db.execute(` + SELECT + u.id as user_id, + u.username, + u.real_name, + u.phone, + u.balance, + COALESCE(yesterday_out.amount, 0) as yesterday_out_amount, + COALESCE(today_in.amount, 0) as today_in_amount, + CASE + WHEN (COALESCE(yesterday_out.amount, 0) - COALESCE(today_in.amount, 0)) > ABS(u.balance) + THEN ABS(u.balance) + ELSE (COALESCE(yesterday_out.amount, 0) - COALESCE(today_in.amount, 0)) + END as balance_needed + FROM users u + LEFT JOIN ( + SELECT + from_user_id, + SUM(amount) as amount + FROM transfers + WHERE created_at >= ? AND created_at <= ? + 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 >= ? AND created_at <= ? + AND status IN ('confirmed', 'received') + GROUP BY to_user_id + ) today_in ON u.id = today_in.to_user_id + WHERE u.role != 'admin' + AND u.is_system_account != 1 + AND yesterday_out.amount > 0 + AND u.balance < 0 + ORDER BY balance_needed DESC, yesterday_out_amount DESC + `, [yesterdayStartStr, yesterdayEndStr, todayStartStr, todayEndStr]); + userStats = userStats.filter(item=>item.balance_needed >= 100) + res.json({ + success: true, + data: { + date: { + yesterday: yesterdayStr, + today: todayStr + }, + users: userStats + } + }); + } catch (error) { + logger.error('Get daily transfer stats failed', { + userId: req.user.id, + error: error.message + }); + next(error); + } + } +); + +module.exports = router; \ No newline at end of file diff --git a/routes/upload.js b/routes/upload.js new file mode 100644 index 0000000..190df41 --- /dev/null +++ b/routes/upload.js @@ -0,0 +1,185 @@ +const express = require('express'); +const multer = require('multer'); +const path = require('path'); +const fs = require('fs'); +const { auth } = require('../middleware/auth'); +const { authenticateToken } = require('./auth'); + +const router = express.Router(); + +// 确保上传目录存在 +const uploadDir = path.join(__dirname, '../uploads'); +const documentsDir = path.join(uploadDir, 'documents'); +const avatarsDir = path.join(uploadDir, 'avatars'); +const productsDir = path.join(uploadDir, 'products'); + +[uploadDir, documentsDir, avatarsDir, productsDir].forEach(dir => { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } +}); + +// 配置multer存储 +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const type = req.body.type || 'documents'; + let uploadPath; + + switch (type) { + case 'avatar': + uploadPath = avatarsDir; + break; + case 'product': + uploadPath = productsDir; + break; + case 'document': + default: + uploadPath = documentsDir; + break; + } + + cb(null, uploadPath); + }, + filename: (req, file, cb) => { + // 生成唯一文件名:时间戳 + 随机数 + 原始扩展名 + const timestamp = Date.now(); + const random = Math.round(Math.random() * 1E9); + const ext = path.extname(file.originalname); + const filename = `${timestamp}_${random}${ext}`; + cb(null, filename); + } +}); + +// 文件过滤器 +const fileFilter = (req, file, cb) => { + // 只允许图片文件 + if (file.mimetype.startsWith('image/')) { + cb(null, true); + } else { + cb(new Error('只能上传图片文件'), false); + } +}; + +const upload = multer({ + storage: storage, + fileFilter: fileFilter, + limits: { + fileSize: 5 * 1024 * 1024, // 5MB + files: 1 // 一次只能上传一个文件 + } +}); + +/** + * @route POST /api/upload/image + * @desc 上传图片 + * @access Private + */ +router.post('/image', authenticateToken, (req, res) => { + upload.single('file')(req, res, (err) => { + if (err instanceof multer.MulterError) { + if (err.code === 'LIMIT_FILE_SIZE') { + return res.status(400).json({ + success: false, + message: '文件大小不能超过 5MB' + }); + } + if (err.code === 'LIMIT_FILE_COUNT') { + return res.status(400).json({ + success: false, + message: '一次只能上传一个文件' + }); + } + return res.status(400).json({ + success: false, + message: '文件上传失败:' + err.message + }); + } else if (err) { + return res.status(400).json({ + success: false, + message: err.message + }); + } + + if (!req.file) { + return res.status(400).json({ + success: false, + message: '请选择要上传的文件' + }); + } + + // 构建文件访问路径 + const type = req.body.type || 'documents'; + // 确保路径与实际目录结构一致 + let folderName = type; + if (type === 'product') { + folderName = 'products'; // 目录是复数形式 + } else if (type === 'avatar') { + folderName = 'avatars'; // 目录是复数形式 + } + const relativePath = path.join(folderName, req.file.filename).replace(/\\/g, '/'); + const fileUrl = `/uploads/${relativePath}`; + + res.json({ + success: true, + message: '文件上传成功', + data: { + filename: req.file.filename, + originalname: req.file.originalname, + mimetype: req.file.mimetype, + size: req.file.size, + path: relativePath, + url: fileUrl + } + }); + }); +}); + +// 保持原有的上传接口兼容性 +router.post('/', auth, upload.single('file'), (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ success: false, message: '没有上传文件' }); + } + + // 返回文件访问URL + const type = req.body.type || 'documents'; + // 确保路径与实际目录结构一致 + let folderName = type; + if (type === 'product') { + folderName = 'products'; // 目录是复数形式 + } else if (type === 'avatar') { + folderName = 'avatars'; // 目录是复数形式 + } + const relativePath = path.join(folderName, req.file.filename).replace(/\\/g, '/'); + const fileUrl = `/uploads/${relativePath}`; + + res.json({ + success: true, + message: '文件上传成功', + url: fileUrl, + filename: req.file.filename, + originalname: req.file.originalname, + size: req.file.size + }); + } catch (error) { + console.error('文件上传错误:', error); + res.status(500).json({ success: false, message: '文件上传失败' }); + } +}); + +// 错误处理中间件 +router.use((error, req, res, next) => { + if (error instanceof multer.MulterError) { + if (error.code === 'LIMIT_FILE_SIZE') { + return res.status(400).json({ success: false, message: '文件大小不能超过5MB' }); + } + } + + if (error.message === '只能上传图片文件') { + return res.status(400).json({ success: false, message: error.message }); + } + + res.status(500).json({ success: false, message: '上传失败' }); +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/users.js b/routes/users.js new file mode 100644 index 0000000..7043766 --- /dev/null +++ b/routes/users.js @@ -0,0 +1,1549 @@ +const express = require('express'); +const bcrypt = require('bcryptjs'); +const { getDB } = require('../database'); +const { auth, adminAuth } = require('../middleware/auth'); +const dayjs = require('dayjs'); + +const router = express.Router(); + +// 创建用户(管理员权限) +router.post('/', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + await db.query('START TRANSACTION'); + + const { + username, + password, + role = 'user', + isSystemAccount = false, // 是否为虚拟商户 + realName, + idCard, + wechatQr, + alipayQr, + bankCard, + unionpayQr, + phone + } = req.body; + + if (!username || !password) { + return res.status(400).json({ success: false, message: '用户名和密码不能为空' }); + } + + if (!realName || !idCard) { + 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]$/; + if (!idCardRegex.test(idCard)) { + return res.status(400).json({ success: false, message: '身份证号格式不正确' }); + } + + // 检查用户是否已存在 + const [existingUsers] = await db.execute( + 'SELECT id FROM users WHERE username = ? OR id_card = ? OR (phone IS NOT NULL AND phone = ?)', + [username, idCard, phone || null] + ); + + if (existingUsers.length > 0) { + return res.status(400).json({ success: false, message: '用户名、身份证号或手机号已存在' }); + } + + // 加密密码 + const hashedPassword = await bcrypt.hash(password, 10); + + // 创建用户 + 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] + ); + + const userId = result.insertId; + + // 用户余额已在创建用户时设置为默认值0.00,无需额外操作 + + await db.query('COMMIT'); + + // 返回创建的用户信息(不包含密码) + const [newUser] = await db.execute( + 'SELECT id, username, role, avatar, points, real_name, phone, created_at, updated_at FROM users WHERE id = ?', + [userId] + ); + + res.status(201).json({ + success: true, + message: '用户创建成功', + user: newUser[0] + }); + } catch (error) { + try { + await getDB().query('ROLLBACK'); + } catch (rollbackError) { + console.error('回滚错误:', rollbackError); + } + console.error('创建用户错误:', error); + res.status(500).json({ success: false, message: '创建用户失败' }); + } +}); + +/** + * 获取待审核用户列表(管理员权限) + */ +router.get('/pending-audit', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + const { page = 1, limit = 10 } = req.query; + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(limit) || 10; + const offset = (pageNum - 1) * limitNum; + + // 获取待审核用户总数 + const [countResult] = await db.execute( + 'SELECT COUNT(*) as total FROM users WHERE audit_status = ?', + ['pending'] + ); + const total = countResult[0].total; + + // 获取待审核用户列表 + const [users] = await db.execute( + `SELECT id, username, phone, real_name, business_license, id_card_front, id_card_back, + wechat_qr, alipay_qr, unionpay_qr, bank_card, audit_status, created_at + FROM users + WHERE audit_status = ? + ORDER BY created_at ASC + LIMIT ${limitNum} OFFSET ${offset}`, + ['pending'] + ); + + res.json({ + success: true, + data: { + users, + pagination: { + page: pageNum, + limit: limitNum, + total, + pages: Math.ceil(total / limitNum) + } + } + }); + } catch (error) { + console.error('获取待审核用户列表错误:', error); + res.status(500).json({ success: false, message: '获取待审核用户列表失败' }); + } +}); + +// 获取用户列表用于转账(普通用户权限) +router.get('/for-transfer', auth, async (req, res) => { + try { + const db = getDB(); + + // 获取所有用户的基本信息(用于转账选择) + const [users] = await db.execute( + 'SELECT id, username, real_name FROM users WHERE id != ? ORDER BY username', + [req.user.id] + ); + + res.json({ + success: true, + data: users + }); + } catch (error) { + console.error('获取转账用户列表错误:', error); + res.status(500).json({ success: false, message: '获取用户列表失败' }); + } +}); + +// 获取用户列表(管理员权限) +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 pageNum = Math.max(1, parseInt(page) || 1); + const limitNum = Math.max(1, Math.min(100, parseInt(limit) || 10)); + const offset = Math.max(0, (pageNum - 1) * limitNum); + + let whereConditions = []; + let countParams = []; + let listParams = []; + + // 构建查询条件 + if (search) { + whereConditions.push('(u.username LIKE ? OR u.real_name LIKE ?)'); + countParams.push(`%${search}%`, `%${search}%`); + listParams.push(`%${search}%`, `%${search}%`); + } + + if (role && role !== 'all') { + whereConditions.push('u.role = ?'); + countParams.push(role); + listParams.push(role); + } + + if (city) { + whereConditions.push('u.city = ?'); + countParams.push(city); + listParams.push(city); + } + + if (district) { + whereConditions.push('u.district_id = ?'); + countParams.push(district); + listParams.push(district); + } + + const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : ''; + + // 添加分页参数 + listParams.push(limitNum.toString(), offset.toString()); + + + // 获取总数 + const [countResult] = await db.execute( + `SELECT COUNT(*) as total FROM users u + LEFT JOIN zhejiang_regions r ON u.district_id = r.id + ${whereClause}`, + countParams + ); + + // 验证排序字段,防止SQL注入 + const validSortFields = ['id', 'username', 'role', 'points', 'balance', 'created_at', 'updated_at']; + const sortField = validSortFields.includes(sort) ? sort : 'created_at'; + + // 验证排序方向 + const sortOrder = (order && (order.toUpperCase() === 'ASC' || order.toUpperCase() === 'DESC')) + ? order.toUpperCase() + : 'DESC'; + + // 获取用户列表,关联地区信息和转账统计 + 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, + 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 ? OFFSET ?`, + listParams + ); + + res.json({ + success: true, + users, + total: countResult[0].total, + page: pageNum, + limit: limitNum, + totalPages: Math.ceil(countResult[0].total / limitNum) + }); + } catch (error) { + console.error('获取用户列表错误:', error); + res.status(500).json({ success: false, message: '获取用户列表失败' }); + } +}); + +// 获取当前用户的个人资料 +router.get('/profile', auth, async (req, res) => { + try { + const db = getDB(); + const userId = req.user.id; + + const [users] = await db.execute( + 'SELECT * FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({ success: false, message: '用户不存在' }); + } + const profile = { + ...users[0], + nickname: users[0].username, // 添加nickname字段,映射到username + realName: users[0].real_name, + idCard: users[0].id_card, + wechatQr: users[0].wechat_qr, + alipayQr: users[0].alipay_qr, + bankCard: users[0].bank_card, + unionpayQr: users[0].unionpay_qr, + businessLicense: users[0].business_license, + idCardFront: users[0].id_card_front, + idCardBack: users[0].id_card_back + }; + res.json({ success: true, user: profile }); + } catch (error) { + console.error('获取用户资料错误:', error); + res.status(500).json({ success: false, message: '获取用户资料失败' }); + } +}); + +/** + * 获取当前用户的收款码状态 + * 用于检查用户是否已上传微信、支付宝、云闪付收款码 + */ +router.get('/payment-codes-status', auth, async (req, res) => { + try { + const db = getDB(); + const userId = req.user.id; + + const [users] = await db.execute( + 'SELECT wechat_qr, alipay_qr, unionpay_qr FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + const paymentCodes = users[0]; + + res.json({ + success: true, + data: { + wechat_qr: paymentCodes.wechat_qr || '', + alipay_qr: paymentCodes.alipay_qr || '', + unionpay_qr: paymentCodes.unionpay_qr || '' + } + }); + } catch (error) { + console.error('获取收款码状态错误:', error); + res.status(500).json({ success: false, message: '获取收款码状态失败' }); + } +}); + +// 获取用户收款信息 +router.get('/payment-info/:userId', auth, async (req, res) => { + try { + const db = getDB(); + const targetUserId = req.params.userId; + + const [users] = await db.execute( + 'SELECT id, username, wechat_qr, alipay_qr, unionpay_qr, bank_card FROM users WHERE id = ?', + [targetUserId] + ); + + if (users.length === 0) { + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + const user = users[0]; + res.json({ + success: true, + data: { + id: user.id, + username: user.username, + wechat_qr: user.wechat_qr, + alipay_qr: user.alipay_qr, + unionpay_qr: user.unionpay_qr, + bank_card: user.bank_card + } + }); + } catch (error) { + console.error('获取用户收款信息错误:', error); + res.status(500).json({ success: false, message: '获取用户收款信息失败' }); + } +}); + +// 获取当前用户的统计信息 +router.get('/stats', auth, async (req, res) => { + try { + const db = getDB(); + const userId = req.user.id; + + // 如果是管理员,返回全局统计 + if (req.user.role === 'admin') { + // 总用户数 + const [totalUsers] = await db.execute('SELECT COUNT(*) as count FROM users'); + + // 管理员数量 + const [adminUsers] = await db.execute('SELECT COUNT(*) as count FROM users WHERE role = "admin"'); + + // 普通用户数量 + const [regularUsers] = await db.execute('SELECT COUNT(*) as count FROM users WHERE role = "user"'); + + // 本月新增用户 + const [monthUsers] = await db.execute( + 'SELECT COUNT(*) as count FROM users WHERE YEAR(created_at) = YEAR(NOW()) AND MONTH(created_at) = MONTH(NOW())' + ); + + // 上月新增用户 + const [lastMonthUsers] = await db.execute( + 'SELECT COUNT(*) as count FROM users WHERE YEAR(created_at) = YEAR(DATE_SUB(NOW(), INTERVAL 1 MONTH)) AND MONTH(created_at) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH))' + ); + + // 计算月增长率 + const monthGrowthRate = lastMonthUsers[0].count > 0 + ? ((monthUsers[0].count - lastMonthUsers[0].count) / lastMonthUsers[0].count * 100).toFixed(2) + : 0; + + // 用户总积分 + const [totalPoints] = await db.execute('SELECT COALESCE(SUM(points), 0) as total FROM users'); + + // 今日新增用户 + const [todayUsers] = await db.execute( + 'SELECT COUNT(*) as count FROM users WHERE DATE(created_at) = CURDATE()' + ); + + // 昨日新增用户 + const [yesterdayUsers] = await db.execute( + 'SELECT COUNT(*) as count FROM users WHERE DATE(created_at) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)' + ); + + // 活跃用户数(有订单的用户) + const [activeUsers] = await db.execute( + 'SELECT COUNT(DISTINCT user_id) as count FROM orders' + ); + + res.json({ + success: true, + stats: { + totalUsers: totalUsers[0].count, + adminUsers: adminUsers[0].count, + regularUsers: regularUsers[0].count, + monthNewUsers: monthUsers[0].count, + todayUsers: todayUsers[0].count, + yesterdayUsers: yesterdayUsers[0].count, + monthlyGrowth: parseFloat(monthGrowthRate), + totalPoints: totalPoints[0].total, + activeUsers: activeUsers[0].count + } + }); + } else { + // 普通用户返回个人统计 + // 用户订单数 + const [orderCount] = await db.execute( + 'SELECT COUNT(*) as count FROM orders WHERE user_id = ?', + [userId] + ); + + // 用户总消费 + const [totalSpent] = await db.execute( + 'SELECT COALESCE(SUM(total_amount), 0) as total FROM orders WHERE user_id = ? AND status = "completed"', + [userId] + ); + + // 用户积分历史 + const [pointsEarned] = await db.execute( + 'SELECT COALESCE(SUM(amount), 0) as total FROM points_history WHERE user_id = ? AND type = "earn"', + [userId] + ); + + const [pointsSpent] = await db.execute( + 'SELECT COALESCE(SUM(amount), 0) as total FROM points_history WHERE user_id = ? AND type = "spend"', + [userId] + ); + + res.json({ + success: true, + stats: { + orderCount: orderCount[0].count, + totalSpent: totalSpent[0].total, + pointsEarned: pointsEarned[0].total, + pointsSpent: pointsSpent[0].total, + currentPoints: req.user.points || 0 + } + }); + } + } catch (error) { + console.error('获取用户统计错误:', error); + res.status(500).json({ success: false, message: '获取用户统计失败' }); + } +}); + +// 获取用户统计信息(管理员权限)- 必须在/:id路由之前定义 +router.get('/admin/stats', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + // 总用户数 + const [totalUsers] = await db.execute('SELECT COUNT(*) as count FROM users'); + + // 管理员数量 + const [adminUsers] = await db.execute('SELECT COUNT(*) as count FROM users WHERE role = "admin"'); + + // 普通用户数量 + const [regularUsers] = await db.execute('SELECT COUNT(*) as count FROM users WHERE role = "user"'); + + // 本月新增用户 + const [monthUsers] = await db.execute( + 'SELECT COUNT(*) as count FROM users WHERE YEAR(created_at) = YEAR(NOW()) AND MONTH(created_at) = MONTH(NOW())' + ); + + // 上月新增用户 + const [lastMonthUsers] = await db.execute( + 'SELECT COUNT(*) as count FROM users WHERE YEAR(created_at) = YEAR(DATE_SUB(NOW(), INTERVAL 1 MONTH)) AND MONTH(created_at) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH))' + ); + + // 计算月增长率 + const monthGrowthRate = lastMonthUsers[0].count > 0 + ? ((monthUsers[0].count - lastMonthUsers[0].count) / lastMonthUsers[0].count * 100).toFixed(2) + : 0; + + // 用户总积分 + const [totalPoints] = await db.execute('SELECT COALESCE(SUM(points), 0) as total FROM users'); + + // 今日新增用户 + const [todayUsers] = await db.execute( + 'SELECT COUNT(*) as count FROM users WHERE DATE(created_at) = CURDATE()' + ); + + // 昨日新增用户 + const [yesterdayUsers] = await db.execute( + 'SELECT COUNT(*) as count FROM users WHERE DATE(created_at) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)' + ); + + // 活跃用户数(有订单的用户) + const [activeUsers] = await db.execute( + 'SELECT COUNT(DISTINCT user_id) as count FROM orders' + ); + + res.json({ + success: true, + stats: { + totalUsers: totalUsers[0].count, + adminUsers: adminUsers[0].count, + regularUsers: regularUsers[0].count, + monthNewUsers: monthUsers[0].count, + todayUsers: todayUsers[0].count, + yesterdayUsers: yesterdayUsers[0].count, + monthlyGrowth: parseFloat(monthGrowthRate), + totalPoints: totalPoints[0].total, + activeUsers: activeUsers[0].count + } + }); + } catch (error) { + console.error('获取用户统计错误:', error); + res.status(500).json({ success: false, message: '获取用户统计失败' }); + } +}); + +// 获取当前用户积分 +router.get('/points', auth, async (req, res) => { + try { + const userId = req.user.id; + + const [users] = await getDB().execute( + 'SELECT points FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + res.json({ + success: true, + points: users[0].points + }); + } catch (error) { + console.error('获取用户积分错误:', error); + res.status(500).json({ success: false, message: '获取用户积分失败' }); + } +}); + +// 获取用户积分历史记录 +router.get('/points/history', auth, async (req, res) => { + try { + const userId = req.user.id; + const { page = 1, limit = 20, type } = req.query; + + // 确保参数为有效数字 + const pageNum = Math.max(1, parseInt(page) || 1); + const limitNum = Math.max(1, Math.min(100, parseInt(limit) || 20)); + const offset = Math.max(0, (pageNum - 1) * limitNum); + + let whereClause = 'WHERE user_id = ?'; + let queryParams = [userId]; + + if (type && ['earn', 'spend'].includes(type)) { + whereClause += ' AND type = ?'; + queryParams.push(type); + } + + // 获取总数 + const [countResult] = await getDB().execute( + `SELECT COUNT(*) as total FROM points_history ${whereClause}`, + queryParams + ); + + // 获取历史记录 + const [records] = await getDB().execute( + `SELECT id, type, amount, description, order_id, created_at + FROM points_history + ${whereClause} + ORDER BY created_at DESC + LIMIT ? OFFSET ?`, + [...queryParams, limitNum.toString(), offset.toString()] + ); + + res.json({ + success: true, + data: { + records, + pagination: { + page: pageNum, + limit: limitNum, + total: countResult[0].total, + totalPages: Math.ceil(countResult[0].total / limitNum) + } + } + }); + } catch (error) { + console.error('获取积分历史失败:', error); + res.status(500).json({ success: false, message: '获取积分历史失败' }); + } +}); + +// 获取用户增长趋势数据(管理员权限) +router.get('/growth-trend', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + const { days = 7 } = req.query; + const daysNum = Math.min(90, Math.max(1, parseInt(days) || 7)); + + // 获取指定天数内的用户注册趋势 + const [trendData] = await db.execute(` + SELECT + DATE(created_at) as date, + COUNT(*) as count + FROM users + WHERE created_at >= DATE_SUB(NOW(), INTERVAL ? DAY) + GROUP BY DATE(created_at) + ORDER BY date ASC + `, [daysNum]); + + // 填充缺失的日期(注册数为0) + const result = []; + + for (let i = daysNum - 1; i >= 0; i--) { + const date = dayjs().subtract(i, 'day'); + const dateStr = date.format('YYYY-MM-DD'); + + // 修复日期比较:将数据库返回的Date对象转换为字符串进行比较 + const existingData = trendData.find(item => { + const itemDateStr = dayjs(item.date).format('YYYY-MM-DD'); + return itemDateStr === dateStr; + }); + + result.push({ + date: date.format('MM-DD'), + count: existingData ? existingData.count : 0 + }); + } + + res.json({ + success: true, + data: result + }); + } catch (error) { + console.error('获取用户增长趋势错误:', error); + res.status(500).json({ success: false, message: '获取用户增长趋势失败' }); + } +}); + +// 获取日收入统计数据(管理员权限) +router.get('/daily-revenue', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + const { days = 30 } = req.query; + const daysNum = Math.min(90, Math.max(1, parseInt(days) || 30)); + + // 获取指定天数内的用户注册数据,按天统计 + const [dailyData] = await db.execute(` + SELECT + DATE(created_at) as date, + COUNT(*) as user_count + FROM users + WHERE created_at >= DATE_SUB(NOW(), INTERVAL ? DAY) + GROUP BY DATE(created_at) + ORDER BY date ASC + `, [daysNum]); + + // 填充缺失的日期(注册数为0) + const result = []; + + for (let i = daysNum - 1; i >= 0; i--) { + const date = dayjs().subtract(i, 'day'); + const dateStr = date.format('YYYY-MM-DD'); // YYYY-MM-DD格式 + const dateDisplay = date.format('M/D'); // 显示格式 + + const existingData = dailyData.find(item => { + const itemDateStr = dayjs(item.date).format('YYYY-MM-DD'); + return itemDateStr === dateStr; + }); + const userCount = existingData ? existingData.user_count : 0; + const revenue = userCount * 398; // 每个用户398元收入 + + result.push({ + date: dateDisplay, + userCount: userCount, + amount: revenue + }); + } + + res.json({ + success: true, + data: result + }); + } catch (error) { + console.error('获取日收入统计错误:', error); + 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 ? OFFSET ? + `, listParams); + + // 获取总数 + 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 { + const db = getDB(); + const userId = req.user.id; + + const [users] = await db.execute( + 'SELECT id, username, role, avatar, points, real_name, id_card, phone, wechat_qr, alipay_qr, bank_card, unionpay_qr, business_license, id_card_front, id_card_back, audit_status, created_at, updated_at FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + // 转换字段名以匹配前端 + const user = users[0]; + const profile = { + ...user, + nickname: user.username, // 添加nickname字段,映射到username + realName: user.real_name, + idCard: user.id_card, + wechatQr: user.wechat_qr, + alipayQr: user.alipay_qr, + bankCard: user.bank_card, + unionpayQr: user.unionpay_qr, + businessLicense: user.business_license, + idCardFront: user.id_card_front, + idCardBack: user.id_card_back, + auditStatus: user.audit_status + }; + + res.json({ success: true, user: profile }); + } catch (error) { + console.error('获取用户个人资料错误:', error); + res.status(500).json({ success: false, message: '获取用户个人资料失败' }); + } +}); + +// 更新当前用户个人资料 +router.put('/profile', auth, async (req, res) => { + try { + const db = getDB(); + const userId = req.user.id; + const { + username, + nickname, + avatar, + realName, + idCard, + phone, + wechatQr, + alipayQr, + bankCard, + unionpayQr, + businessLicense, + idCardFront, + idCardBack, + city, + districtId + } = req.body; + + // 处理nickname字段,如果提供了nickname,则使用nickname作为username + const finalUsername = nickname || username; + + // 检查用户名、身份证号和手机号是否已被其他用户使用 + if (finalUsername || idCard || phone) { + const conditions = []; + const checkValues = []; + + if (finalUsername) { + conditions.push('username = ?'); + checkValues.push(finalUsername); + } + if (idCard) { + conditions.push('id_card = ?'); + checkValues.push(idCard); + } + if (phone) { + conditions.push('phone = ?'); + checkValues.push(phone); + } + + if (conditions.length > 0) { + const [existingUsers] = await db.execute( + `SELECT id FROM users WHERE (${conditions.join(' OR ')}) AND id != ?`, + [...checkValues, userId] + ); + + if (existingUsers.length > 0) { + return res.status(400).json({ success: false, message: '用户名、身份证号或手机号已被使用' }); + } + } + } + + // 验证身份证号格式 + if (idCard) { + 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]$/; + if (!idCardRegex.test(idCard)) { + return res.status(400).json({ success: false, message: '身份证号格式不正确' }); + } + } + + // 构建更新字段 + const updateFields = []; + const updateValues = []; + + if (finalUsername !== undefined) { + updateFields.push('username = ?'); + updateValues.push(finalUsername); + } + + if (avatar !== undefined) { + updateFields.push('avatar = ?'); + updateValues.push(avatar); + } + + if (realName !== undefined) { + updateFields.push('real_name = ?'); + updateValues.push(realName); + } + + if (idCard !== undefined) { + updateFields.push('id_card = ?'); + updateValues.push(idCard); + } + + if (phone !== undefined) { + updateFields.push('phone = ?'); + updateValues.push(phone); + } + + // 添加城市和地区字段更新 + if (city !== undefined) { + updateFields.push('city = ?'); + updateValues.push(city); + } + + if (districtId !== undefined) { + updateFields.push('district_id = ?'); + updateValues.push(districtId); + } + + // 检查是否更新了需要重新审核的关键信息 + let needsReaudit = false; + + if (wechatQr !== undefined) { + updateFields.push('wechat_qr = ?'); + updateValues.push(wechatQr); + needsReaudit = true; + } + + if (alipayQr !== undefined) { + updateFields.push('alipay_qr = ?'); + updateValues.push(alipayQr); + needsReaudit = true; + } + + if (bankCard !== undefined) { + updateFields.push('bank_card = ?'); + updateValues.push(bankCard); + needsReaudit = true; + } + + if (unionpayQr !== undefined) { + updateFields.push('unionpay_qr = ?'); + updateValues.push(unionpayQr); + needsReaudit = true; + } + + if (city !== undefined) { + updateFields.push('city = ?'); + updateValues.push(city); + } + + if (districtId !== undefined) { + updateFields.push('district_id = ?'); + updateValues.push(districtId); + } + + if (businessLicense !== undefined) { + updateFields.push('business_license = ?'); + updateValues.push(businessLicense); + needsReaudit = true; + } + + if (idCardFront !== undefined) { + updateFields.push('id_card_front = ?'); + updateValues.push(idCardFront); + needsReaudit = true; + } + + if (idCardBack !== undefined) { + updateFields.push('id_card_back = ?'); + updateValues.push(idCardBack); + needsReaudit = true; + } + + // 如果更新了关键信息且用户不是管理员,则重置审核状态为待审核 + if (needsReaudit && req.user.role !== 'admin') { + updateFields.push('audit_status = ?'); + updateValues.push('pending'); + } + + if (updateFields.length === 0) { + return res.status(400).json({ success: false, message: '没有要更新的字段' }); + } + + updateValues.push(userId); + + await db.execute( + `UPDATE users SET ${updateFields.join(', ')} WHERE id = ?`, + updateValues + ); + + // 返回更新后的用户信息 + const [updatedUsers] = await db.execute( + 'SELECT id, username, role, avatar, points, real_name, id_card, phone, wechat_qr, alipay_qr, bank_card, unionpay_qr, business_license, id_card_front, id_card_back, audit_status, is_system_account, created_at, updated_at FROM users WHERE id = ?', + [userId] + ); + + // 转换字段名以匹配前端 + const user = updatedUsers[0]; + const profile = { + ...user, + nickname: user.username, // 添加nickname字段,映射到username + realName: user.real_name, + idCard: user.id_card, + wechatQr: user.wechat_qr, + alipayQr: user.alipay_qr, + bankCard: user.bank_card, + unionpayQr: user.unionpay_qr, + businessLicense: user.business_license, + idCardFront: user.id_card_front, + idCardBack: user.id_card_back, + auditStatus: user.audit_status + }; + + res.json({ + success: true, + message: '个人资料更新成功', + data: profile + }); + } catch (error) { + console.error('更新个人资料错误:', error); + res.status(500).json({ success: false, message: '更新个人资料失败' }); + } +}); +// 获取用户详情 +router.get('/:id', auth, async (req, res) => { + try { + const db = getDB(); + const userId = req.params.id; + + // 只有管理员或用户本人可以查看详情 + if (req.user.role !== 'admin' && req.user.id != userId) { + return res.status(403).json({ success: false, message: '权限不足' }); + } + + const [users] = await db.execute( + 'SELECT id, username, role, avatar, points, real_name, id_card, phone, wechat_qr, alipay_qr, bank_card, unionpay_qr, created_at, updated_at FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + res.json({ success: true, user: users[0] }); + } catch (error) { + console.error('获取用户详情错误:', error); + res.status(500).json({ success: false, message: '获取用户详情失败' }); + } +}); + +// 更新用户信息 +router.put('/:id', auth, async (req, res) => { + try { + const db = getDB(); + const userId = req.params.id; + const { + username, + password, + role, + isSystemAccount, + avatar, + realName, + idCard, + phone, + wechatQr, + alipayQr, + bankCard, + unionpayQr, + city, + districtId, + idCardFront, + idCardBack, + businessLicense, + } = req.body; + + // 只有管理员或用户本人可以更新信息 + if (req.user.role !== 'admin' && req.user.id != userId) { + return res.status(403).json({ success: false, message: '权限不足' }); + } + + // 非管理员不能修改角色 + if (req.user.role !== 'admin' && role) { + return res.status(403).json({ success: false, message: '无权限修改用户角色' }); + } + + // 检查用户名、身份证号和手机号是否已被其他用户使用 + if (username || idCard || phone) { + const conditions = []; + const checkValues = []; + + if (username) { + conditions.push('username = ?'); + checkValues.push(username); + } + if (idCard) { + conditions.push('id_card = ?'); + checkValues.push(idCard); + } + if (phone) { + conditions.push('phone = ?'); + checkValues.push(phone); + } + + if (conditions.length > 0) { + const [existingUsers] = await db.execute( + `SELECT id FROM users WHERE (${conditions.join(' OR ')}) AND id != ?`, + [...checkValues, userId] + ); + + if (existingUsers.length > 0) { + return res.status(400).json({ success: false, message: '用户名、身份证号或手机号已被使用' }); + } + } + } + + // 验证身份证号格式 + if (idCard) { + 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]$/; + if (!idCardRegex.test(idCard)) { + return res.status(400).json({ success: false, message: '身份证号格式不正确' }); + } + } + + // 构建更新字段 + const updateFields = []; + const updateValues = []; + + if (username) { + updateFields.push('username = ?'); + updateValues.push(username); + } + + // 处理密码更新 + if (password && password.trim() !== '') { + const hashedPassword = await bcrypt.hash(password, 10); + updateFields.push('password = ?'); + updateValues.push(hashedPassword); + } + + if (role && req.user.role === 'admin') { + updateFields.push('role = ?'); + updateValues.push(role); + } + + // 只有管理员可以修改账户类型 + if (isSystemAccount !== undefined && req.user.role === 'admin') { + updateFields.push('is_system_account = ?'); + updateValues.push(isSystemAccount); + } + + if (avatar !== undefined) { + updateFields.push('avatar = ?'); + updateValues.push(avatar); + } + + if (realName !== undefined) { + updateFields.push('real_name = ?'); + updateValues.push(realName); + } + + if (idCard !== undefined) { + updateFields.push('id_card = ?'); + updateValues.push(idCard); + } + + if (phone !== undefined) { + updateFields.push('phone = ?'); + updateValues.push(phone); + } + if (city !== undefined) { + updateFields.push('city = ?'); + updateValues.push(city); + } + + if (districtId !== undefined) { + updateFields.push('district_id = ?'); + updateValues.push(districtId); + } + // 检查是否更新了需要重新审核的关键信息 + let needsReaudit = false; + + if (wechatQr !== undefined) { + updateFields.push('wechat_qr = ?'); + updateValues.push(wechatQr); + needsReaudit = true; + } + + if (alipayQr !== undefined) { + updateFields.push('alipay_qr = ?'); + updateValues.push(alipayQr); + needsReaudit = true; + } + + if (bankCard !== undefined) { + updateFields.push('bank_card = ?'); + updateValues.push(bankCard); + needsReaudit = true; + } + + if (unionpayQr !== undefined) { + updateFields.push('unionpay_qr = ?'); + updateValues.push(unionpayQr); + needsReaudit = true; + } + if (idCardFront !== undefined) { + updateFields.push('id_card_front = ?'); + updateValues.push(idCardFront); + needsReaudit = true; + } + if (idCardBack !== undefined) { + updateFields.push('id_card_back = ?'); + updateValues.push(idCardBack); + needsReaudit = true; + } + if (businessLicense !== undefined) { + updateFields.push('business_license = ?'); + updateValues.push(businessLicense); + needsReaudit = true; + } + + // 如果更新了关键信息且用户不是管理员,则重置审核状态为待审核 + if (needsReaudit && req.user.role !== 'admin') { + updateFields.push('audit_status = ?'); + updateValues.push('pending'); + } + + if (updateFields.length === 0) { + return res.status(400).json({ success: false, message: '没有要更新的字段' }); + } + + updateValues.push(userId); + + await db.execute( + `UPDATE users SET ${updateFields.join(', ')} WHERE id = ?`, + updateValues + ); + + // 返回更新后的用户信息 + 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 = ?', + [userId] + ); + + res.json({ + success: true, + message: '用户信息更新成功', + user: updatedUsers[0] + }); + } catch (error) { + console.error('更新用户信息错误:', error); + res.status(500).json({ success: false, message: '更新用户信息失败' }); + } +}); + + + +// 删除用户(管理员权限) +router.delete('/:id', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + const userId = req.params.id; + + // 不能删除自己 + if (req.user.id == userId) { + return res.status(400).json({ success: false, message: '不能删除自己的账户' }); + } + + // 检查用户是否存在 + const [users] = await db.execute( + 'SELECT id FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + // 删除用户 + await db.execute('DELETE FROM users WHERE id = ?', [userId]); + + res.json({ success: true, message: '用户删除成功' }); + } catch (error) { + console.error('删除用户错误:', error); + res.status(500).json({ success: false, message: '删除用户失败' }); + } +}); + +/** + * 审核用户(管理员权限) + */ +router.put('/:id/audit', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + const userId = req.params.id; + const { action, note } = req.body; // action: 'approve' 或 'reject' + + if (!action || !['approve', 'reject'].includes(action)) { + return res.status(400).json({ success: false, message: '审核操作无效' }); + } + + // 检查用户是否存在且为待审核状态 + const [users] = await db.execute( + 'SELECT id, username, audit_status FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + const user = users[0]; + if (user.audit_status !== 'pending') { + return res.status(400).json({ success: false, message: '该用户不是待审核状态' }); + } + + // 更新审核状态 + const auditStatus = action === 'approve' ? 'approved' : 'rejected'; + await db.execute( + `UPDATE users SET + audit_status = ?, + audit_note = ?, + audited_by = ?, + audited_at = NOW() + WHERE id = ?`, + [auditStatus, note || null, req.user.id, userId] + ); + + const message = action === 'approve' ? '用户审核通过' : '用户审核拒绝'; + res.json({ success: true, message }); + } catch (error) { + console.error('审核用户错误:', error); + res.status(500).json({ success: false, message: '审核用户失败' }); + } +}); + +/** + * 获取用户审核详情(管理员权限) + */ +router.get('/:id/audit-detail', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + const userId = req.params.id; + + // 获取用户详细信息 + const [users] = await db.execute( + `SELECT u.id, u.username, u.phone, u.real_name, u.business_license, + u.id_card_front, u.id_card_back, u.audit_status, u.audit_note, + u.audited_at, u.created_at, + auditor.username as auditor_name + FROM users u + LEFT JOIN users auditor ON u.audited_by = auditor.id + WHERE u.id = ?`, + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + res.json({ + success: true, + data: users[0] + }); + } catch (error) { + console.error('获取用户审核详情错误:', error); + res.status(500).json({ success: false, message: '获取用户审核详情失败' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/scripts/README_MERGE_TABLES.md b/scripts/README_MERGE_TABLES.md new file mode 100644 index 0000000..4b5a980 --- /dev/null +++ b/scripts/README_MERGE_TABLES.md @@ -0,0 +1,56 @@ +# 表合并说明文档 + +## 概述 + +本文档说明如何使用 `merge_tables.js` 脚本将 `order_allocations` 表数据合并到 `transfers` 表中,以实现数据库结构优化。 + +## 背景 + +原系统中,`order_allocations` 表和 `transfers` 表存在功能重叠,为了简化数据库结构和提高查询效率,我们决定将 `order_allocations` 表的数据合并到 `transfers` 表中,并通过 `source_type` 字段区分不同来源的转账记录。 + +## 合并策略 + +1. 为 `transfers` 表添加必要的字段,包括 `source_type`、`matching_order_id`、`cycle_number` 等 +2. 将 `order_allocations` 表中的数据迁移到 `transfers` 表 +3. 更新相关的外键引用 + +## 使用方法 + +### 前置条件 + +1. 确保已对数据库进行备份 +2. 确保系统处于维护状态,没有用户正在使用 + +### 执行步骤 + +1. 进入项目根目录 +2. 执行以下命令运行合并脚本: + +```bash +node scripts/merge_tables.js +``` + +3. 脚本执行完成后,检查控制台输出,确认迁移是否成功 + +### 验证步骤 + +1. 检查 `transfers` 表中是否包含所有 `order_allocations` 表的数据 +2. 验证系统功能是否正常,特别是与匹配订单相关的功能 +3. 确认无误后,可以考虑删除 `order_allocations` 表(可选) + +## 回滚方案 + +如果合并过程中出现问题,或者合并后系统功能异常,可以通过以下步骤回滚: + +1. 使用之前的数据库备份恢复数据 +2. 如果没有备份,可以手动将 `transfers` 表中 `source_type='allocation'` 的记录删除,并重新运行原有的系统 + +## 注意事项 + +1. 合并过程会自动处理已有关联的记录,避免重复迁移 +2. 合并脚本使用事务进行操作,确保数据一致性 +3. 建议在测试环境验证成功后再在生产环境执行 + +## 技术支持 + +如有问题,请联系技术支持团队。 \ No newline at end of file diff --git a/scripts/fix_sql_syntax.js b/scripts/fix_sql_syntax.js new file mode 100644 index 0000000..e10ef4f --- /dev/null +++ b/scripts/fix_sql_syntax.js @@ -0,0 +1,154 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * SQL 语法修复脚本:修复自动替换产生的 SQL 语法错误 + */ + +class SQLSyntaxFixer { + constructor() { + this.filesToFix = [ + 'services/matchingService.js', + 'routes/matchingAdmin.js', + 'routes/transfers.js', + 'routes/matching.js' + ]; + } + + /** + * 修复单个文件中的 SQL 语法错误 + * @param {string} filePath - 文件路径 + */ + async fixFile(filePath) { + const fullPath = path.join(process.cwd(), filePath); + + if (!fs.existsSync(fullPath)) { + console.log(`文件不存在: ${filePath}`); + return; + } + + console.log(`正在修复文件: ${filePath}`); + + let content = fs.readFileSync(fullPath, 'utf8'); + let originalContent = content; + + // 1. 修复 WHERE source_type = 'allocation' FROM transfers 的错误顺序 + content = content.replace( + /WHERE source_type = 'allocation' FROM transfers/g, + "FROM transfers WHERE source_type = 'allocation'" + ); + + // 2. 修复多个 WHERE 子句的问题 + content = content.replace( + /FROM transfers WHERE source_type = 'allocation'([\s\S]*?)WHERE/g, + "FROM transfers WHERE source_type = 'allocation'$1AND" + ); + + // 3. 修复 INSERT 语句中的引号问题 + content = content.replace( + /'allocation'/g, + "'allocation'" + ); + + // 4. 修复 JOIN 语句中的表别名问题 + content = content.replace( + /FROM transfers oa WHERE oa\.source_type = 'allocation'/g, + "FROM transfers oa WHERE oa.source_type = 'allocation'" + ); + + // 5. 修复复杂查询中的语法问题 + content = this.fixComplexQueries(content, filePath); + + if (content !== originalContent) { + fs.writeFileSync(fullPath, content); + console.log(`✓ 已修复: ${filePath}`); + } else { + console.log(`- 无需修复: ${filePath}`); + } + } + + /** + * 修复复杂查询 + * @param {string} content - 文件内容 + * @param {string} filePath - 文件路径 + * @returns {string} 修复后的内容 + */ + fixComplexQueries(content, filePath) { + if (filePath.includes('matchingService.js')) { + // 修复 matchingService.js 中的特定查询 + + // 修复获取匹配目标的查询 + content = content.replace( + /FROM transfers oa\s+WHERE oa\.source_type = 'allocation'\s+JOIN users u ON oa\.from_user_id = u\.id/g, + "FROM transfers oa JOIN users u ON oa.from_user_id = u.id WHERE oa.source_type = 'allocation'" + ); + + // 修复获取用户待处理分配的查询 + content = content.replace( + /SELECT \* FROM transfers WHERE source_type = 'allocation' WHERE matching_order_id = \? ORDER BY cycle_number, created_at/g, + "SELECT * FROM transfers WHERE source_type = 'allocation' AND matching_order_id = ? ORDER BY cycle_number, created_at" + ); + + // 修复检查周期完成的查询 + content = content.replace( + /SELECT COUNT\(\*\) as count FROM transfers WHERE source_type = 'allocation' WHERE matching_order_id = \? AND cycle_number = \? AND status = "pending"/g, + "SELECT COUNT(*) as count FROM transfers WHERE source_type = 'allocation' AND matching_order_id = ? AND cycle_number = ? AND status = 'pending'" + ); + } + + if (filePath.includes('matchingAdmin.js')) { + // 修复 matchingAdmin.js 中的查询 + content = content.replace( + /FROM transfers oa WHERE oa\.source_type = 'allocation'\s+JOIN/g, + "FROM transfers oa JOIN" + ); + + // 在 JOIN 后添加 WHERE 条件 + content = content.replace( + /(FROM transfers oa JOIN[\s\S]*?)WHERE(?!.*source_type)/g, + "$1WHERE oa.source_type = 'allocation' AND" + ); + } + + if (filePath.includes('matching.js')) { + // 修复 matching.js 中的查询 + content = content.replace( + /LEFT JOIN transfers oa ON mo\.id = oa\.matching_order_id WHERE oa\.source_type = 'allocation'/g, + "LEFT JOIN transfers oa ON mo.id = oa.matching_order_id AND oa.source_type = 'allocation'" + ); + } + + return content; + } + + /** + * 执行所有文件的修复 + */ + async fixAllFiles() { + console.log('开始修复 SQL 语法错误...'); + console.log('=' .repeat(60)); + + for (const filePath of this.filesToFix) { + try { + await this.fixFile(filePath); + } catch (error) { + console.error(`修复文件 ${filePath} 失败:`, error.message); + } + } + + console.log('\n' + '=' .repeat(60)); + console.log('✓ SQL 语法修复完成!'); + } +} + +async function main() { + const fixer = new SQLSyntaxFixer(); + await fixer.fixAllFiles(); +} + +// 如果直接运行此脚本 +if (require.main === module) { + main().catch(console.error); +} + +module.exports = SQLSyntaxFixer; \ No newline at end of file diff --git a/scripts/fix_table_aliases.js b/scripts/fix_table_aliases.js new file mode 100644 index 0000000..e065977 --- /dev/null +++ b/scripts/fix_table_aliases.js @@ -0,0 +1,133 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * 表别名修复脚本:修复 SQL 查询中的表别名问题 + */ + +class TableAliasFixer { + constructor() { + this.filesToFix = [ + 'services/matchingService.js', + 'routes/matchingAdmin.js', + 'routes/transfers.js', + 'routes/matching.js' + ]; + } + + /** + * 修复单个文件中的表别名问题 + * @param {string} filePath - 文件路径 + */ + async fixFile(filePath) { + const fullPath = path.join(process.cwd(), filePath); + + if (!fs.existsSync(fullPath)) { + console.log(`文件不存在: ${filePath}`); + return; + } + + console.log(`正在修复文件: ${filePath}`); + + let content = fs.readFileSync(fullPath, 'utf8'); + let originalContent = content; + + // 1. 修复 "FROM transfers WHERE source_type = 'allocation' oa" 的问题 + content = content.replace( + /FROM transfers WHERE source_type = 'allocation' (\w+)/g, + "FROM transfers $1 WHERE $1.source_type = 'allocation'" + ); + + // 2. 修复重复的 source_type 条件 + content = content.replace( + /FROM transfers WHERE source_type = 'allocation' (\w+) AND \1\.source_type = 'allocation'/g, + "FROM transfers $1 WHERE $1.source_type = 'allocation'" + ); + + // 3. 修复 "FROM transfers WHERE source_type = 'allocation'" 后面直接跟其他子句的情况 + content = content.replace( + /FROM transfers WHERE source_type = 'allocation'\s+(JOIN|ORDER|GROUP|LIMIT)/g, + "FROM transfers WHERE source_type = 'allocation' $1" + ); + + // 4. 修复子查询中的问题 + content = content.replace( + /\(SELECT[^)]*FROM transfers WHERE source_type = 'allocation' (\w+)/g, + (match, alias) => { + return match.replace( + `FROM transfers WHERE source_type = 'allocation' ${alias}`, + `FROM transfers ${alias} WHERE ${alias}.source_type = 'allocation'` + ); + } + ); + + // 5. 修复特定的查询模式 + content = this.fixSpecificPatterns(content, filePath); + + if (content !== originalContent) { + fs.writeFileSync(fullPath, content); + console.log(`✓ 已修复: ${filePath}`); + } else { + console.log(`- 无需修复: ${filePath}`); + } + } + + /** + * 修复特定的查询模式 + * @param {string} content - 文件内容 + * @param {string} filePath - 文件路径 + * @returns {string} 修复后的内容 + */ + fixSpecificPatterns(content, filePath) { + // 修复 SELECT 语句中的表别名问题 + content = content.replace( + /SELECT ([^F]*?) FROM transfers WHERE source_type = 'allocation' (\w+)/g, + "SELECT $1 FROM transfers $2 WHERE $2.source_type = 'allocation'" + ); + + // 修复 UPDATE 语句 + content = content.replace( + /UPDATE transfers WHERE source_type = 'allocation' SET/g, + "UPDATE transfers SET" + ); + + // 修复 WHERE 子句中的条件 + content = content.replace( + /WHERE source_type = 'allocation' AND (\w+)\./g, + "WHERE $1.source_type = 'allocation' AND $1." + ); + + return content; + } + + /** + * 执行所有文件的修复 + */ + async fixAllFiles() { + console.log('开始修复表别名问题...'); + console.log('=' .repeat(60)); + + for (const filePath of this.filesToFix) { + try { + await this.fixFile(filePath); + } catch (error) { + console.error(`修复文件 ${filePath} 失败:`, error.message); + } + } + + console.log('\n' + '=' .repeat(60)); + console.log('✓ 表别名修复完成!'); + } +} + +async function main() { + const fixer = new TableAliasFixer(); + await fixer.fixAllFiles(); +} + +// 如果直接运行此脚本 +if (require.main === module) { + main().catch(console.error); +} + +module.exports = TableAliasFixer; \ No newline at end of file diff --git a/scripts/update_code_references.js b/scripts/update_code_references.js new file mode 100644 index 0000000..eb9a509 --- /dev/null +++ b/scripts/update_code_references.js @@ -0,0 +1,273 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * 代码更新脚本:将所有 order_allocations 表引用更新为 transfers 表 + * + * 更新策略: + * 1. 将 order_allocations 表名替换为 transfers + * 2. 更新相关的字段映射和查询逻辑 + * 3. 添加必要的 WHERE 条件来过滤 allocation 类型的记录 + */ + +class CodeUpdater { + constructor() { + this.filesToUpdate = [ + 'services/matchingService.js', + 'routes/matchingAdmin.js', + 'routes/transfers.js', + 'routes/matching.js' + ]; + + // 字段映射关系 + this.fieldMappings = { + // order_allocations 字段 -> transfers 字段 + 'matching_order_id': 'matching_order_id', + 'from_user_id': 'from_user_id', + 'to_user_id': 'to_user_id', + 'amount': 'amount', + 'cycle_number': 'cycle_number', + 'status': 'status', + 'transfer_id': 'id', // order_allocations.transfer_id 对应 transfers.id + 'created_at': 'created_at', + 'updated_at': 'updated_at', + 'confirmed_at': 'confirmed_at', + 'outbound_date': 'outbound_date', + 'return_date': 'return_date', + 'can_return_after': 'can_return_after' + }; + } + + /** + * 更新单个文件 + * @param {string} filePath - 文件路径 + */ + async updateFile(filePath) { + const fullPath = path.join(process.cwd(), filePath); + + if (!fs.existsSync(fullPath)) { + console.log(`文件不存在: ${filePath}`); + return; + } + + console.log(`正在更新文件: ${filePath}`); + + let content = fs.readFileSync(fullPath, 'utf8'); + let originalContent = content; + + // 1. 替换表名 + content = content.replace(/\border_allocations\b/g, 'transfers'); + + // 2. 添加 source_type 过滤条件 + content = this.addSourceTypeFilters(content); + + // 3. 更新 INSERT 语句 + content = this.updateInsertStatements(content); + + // 4. 更新特定的查询逻辑 + content = this.updateSpecificQueries(content, filePath); + + // 5. 更新注释 + content = this.updateComments(content); + + if (content !== originalContent) { + // 创建备份 + fs.writeFileSync(fullPath + '.backup', originalContent); + + // 写入更新后的内容 + fs.writeFileSync(fullPath, content); + + console.log(`✓ 已更新: ${filePath}`); + console.log(`✓ 备份已创建: ${filePath}.backup`); + } else { + console.log(`- 无需更新: ${filePath}`); + } + } + + /** + * 添加 source_type 过滤条件 + * @param {string} content - 文件内容 + * @returns {string} 更新后的内容 + */ + addSourceTypeFilters(content) { + // 在 FROM transfers 后添加 WHERE source_type = 'allocation' 条件 + // 但要避免重复添加 + + // 匹配 FROM transfers 但不包含 source_type 的情况 + content = content.replace( + /FROM transfers(?!.*source_type)([\s\S]*?)(?=WHERE|ORDER|GROUP|LIMIT|;|$)/gi, + (match, afterFrom) => { + // 如果已经有 WHERE 子句,添加 AND 条件 + if (afterFrom.includes('WHERE')) { + return match.replace(/WHERE/, 'WHERE source_type = \'allocation\' AND'); + } else { + // 如果没有 WHERE 子句,添加新的 WHERE 条件 + const beforeNextClause = match.match(/(ORDER|GROUP|LIMIT|;|$)/); + if (beforeNextClause) { + return match.replace(beforeNextClause[0], ` WHERE source_type = 'allocation' ${beforeNextClause[0]}`); + } else { + return match + " WHERE source_type = 'allocation'"; + } + } + } + ); + + return content; + } + + /** + * 更新 INSERT 语句 + * @param {string} content - 文件内容 + * @returns {string} 更新后的内容 + */ + updateInsertStatements(content) { + // 更新 INSERT INTO transfers 语句,添加必要的字段 + content = content.replace( + /INSERT INTO transfers\s*\(([^)]+)\)\s*VALUES\s*\(([^)]+)\)/gi, + (match, fields, values) => { + // 如果字段列表中没有 source_type,添加它 + if (!fields.includes('source_type')) { + const fieldList = fields.trim() + ', source_type'; + const valueList = values.trim() + ', \'allocation\''; + return `INSERT INTO transfers (${fieldList}) VALUES (${valueList})`; + } + return match; + } + ); + + return content; + } + + /** + * 更新特定的查询逻辑 + * @param {string} content - 文件内容 + * @param {string} filePath - 文件路径 + * @returns {string} 更新后的内容 + */ + updateSpecificQueries(content, filePath) { + // 根据不同文件进行特定更新 + + if (filePath.includes('matchingService.js')) { + // 更新 matchingService.js 中的特定逻辑 + content = this.updateMatchingServiceQueries(content); + } + + if (filePath.includes('matchingAdmin.js')) { + // 更新 matchingAdmin.js 中的特定逻辑 + content = this.updateMatchingAdminQueries(content); + } + + return content; + } + + /** + * 更新 matchingService.js 中的查询 + * @param {string} content - 文件内容 + * @returns {string} 更新后的内容 + */ + updateMatchingServiceQueries(content) { + // 更新确认分配的逻辑 + content = content.replace( + /UPDATE transfers SET status = "confirmed", transfer_id = \?, confirmed_at = NOW\(\) WHERE id = \?/g, + 'UPDATE transfers SET status = "confirmed", confirmed_at = NOW() WHERE id = ? AND source_type = \'allocation\'' + ); + + // 更新获取分配记录的查询 + content = content.replace( + /SELECT \* FROM transfers WHERE id = \? AND from_user_id = \?/g, + 'SELECT * FROM transfers WHERE id = ? AND from_user_id = ? AND source_type = \'allocation\'' + ); + + return content; + } + + /** + * 更新 matchingAdmin.js 中的查询 + * @param {string} content - 文件内容 + * @returns {string} 更新后的内容 + */ + updateMatchingAdminQueries(content) { + // 更新管理员查询逻辑,确保只查询 allocation 类型的记录 + content = content.replace( + /FROM transfers oa/g, + 'FROM transfers oa WHERE oa.source_type = \'allocation\'' + ); + + return content; + } + + /** + * 更新注释 + * @param {string} content - 文件内容 + * @returns {string} 更新后的内容 + */ + updateComments(content) { + content = content.replace(/order_allocations/g, 'transfers (allocation type)'); + content = content.replace(/订单分配/g, '转账分配'); + content = content.replace(/分配表/g, '转账表(分配类型)'); + + return content; + } + + /** + * 执行所有文件的更新 + */ + async updateAllFiles() { + console.log('开始更新代码引用...'); + console.log('=' .repeat(60)); + + for (const filePath of this.filesToUpdate) { + try { + await this.updateFile(filePath); + } catch (error) { + console.error(`更新文件 ${filePath} 失败:`, error.message); + } + } + + console.log('\n' + '=' .repeat(60)); + console.log('✓ 代码更新完成!'); + console.log('\n注意事项:'); + console.log('1. 所有原始文件已备份为 .backup 文件'); + console.log('2. 请测试更新后的代码功能是否正常'); + console.log('3. 如有问题,可以使用备份文件恢复'); + console.log('4. 确认无误后可删除 .backup 文件'); + } + + /** + * 恢复所有备份文件 + */ + async restoreBackups() { + console.log('开始恢复备份文件...'); + + for (const filePath of this.filesToUpdate) { + const fullPath = path.join(process.cwd(), filePath); + const backupPath = fullPath + '.backup'; + + if (fs.existsSync(backupPath)) { + fs.copyFileSync(backupPath, fullPath); + console.log(`✓ 已恢复: ${filePath}`); + } + } + + console.log('✓ 备份恢复完成!'); + } +} + +async function main() { + const updater = new CodeUpdater(); + + const args = process.argv.slice(2); + + if (args.includes('--restore')) { + await updater.restoreBackups(); + } else { + await updater.updateAllFiles(); + } +} + +// 如果直接运行此脚本 +if (require.main === module) { + main().catch(console.error); +} + +module.exports = CodeUpdater; \ No newline at end of file diff --git a/scripts/verify_merge.js b/scripts/verify_merge.js new file mode 100644 index 0000000..e3e4995 --- /dev/null +++ b/scripts/verify_merge.js @@ -0,0 +1,115 @@ +const mysql = require('mysql2/promise'); +const { dbConfig } = require('../config/config'); + +/** + * 验证表合并结果的脚本 + * 检查 order_allocations 表和 transfers 表的数据一致性 + */ +async function verifyMerge() { + console.log('开始验证表合并结果...'); + console.log('=' .repeat(60)); + + let connection; + + try { + // 创建数据库连接 + connection = await mysql.createConnection({ + host: dbConfig.host, + user: dbConfig.user, + password: dbConfig.password, + database: dbConfig.database + }); + + // 1. 检查 order_allocations 表中有多少条记录 + const [allocationCount] = await connection.execute( + 'SELECT COUNT(*) as count FROM order_allocations' + ); + console.log(`order_allocations 表总记录数: ${allocationCount[0].count}`); + + // 2. 检查 transfers 表中有多少条 allocation 类型的记录 + const [transferCount] = await connection.execute( + 'SELECT COUNT(*) as count FROM transfers WHERE source_type = \'allocation\'' + ); + console.log(`transfers 表中 allocation 类型记录数: ${transferCount[0].count}`); + + // 3. 检查 order_allocations 表中有多少条记录没有关联的 transfer_id + const [unlinkedCount] = await connection.execute( + 'SELECT COUNT(*) as count FROM order_allocations WHERE transfer_id IS NULL' + ); + console.log(`order_allocations 表中无关联 transfer_id 的记录数: ${unlinkedCount[0].count}`); + + // 4. 检查数据一致性 - 抽样检查 + console.log('\n数据一致性检查(抽样):'); + const [sampleAllocations] = await connection.execute( + 'SELECT * FROM order_allocations WHERE transfer_id IS NOT NULL LIMIT 5' + ); + + for (const allocation of sampleAllocations) { + const [transfer] = await connection.execute( + 'SELECT * FROM transfers WHERE id = ?', + [allocation.transfer_id] + ); + + if (transfer.length === 0) { + console.log(` ✗ 错误: allocation_id=${allocation.id} 关联的 transfer_id=${allocation.transfer_id} 不存在`); + continue; + } + + const transferRecord = transfer[0]; + const isConsistent = + transferRecord.from_user_id == allocation.from_user_id && + transferRecord.to_user_id == allocation.to_user_id && + transferRecord.amount == allocation.amount && + transferRecord.matching_order_id == allocation.matching_order_id && + transferRecord.cycle_number == allocation.cycle_number; + + if (isConsistent) { + console.log(` ✓ allocation_id=${allocation.id} 与 transfer_id=${allocation.transfer_id} 数据一致`); + } else { + console.log(` ✗ 错误: allocation_id=${allocation.id} 与 transfer_id=${allocation.transfer_id} 数据不一致`); + console.log(' allocation:', { + from_user_id: allocation.from_user_id, + to_user_id: allocation.to_user_id, + amount: allocation.amount, + matching_order_id: allocation.matching_order_id, + cycle_number: allocation.cycle_number + }); + console.log(' transfer:', { + from_user_id: transferRecord.from_user_id, + to_user_id: transferRecord.to_user_id, + amount: transferRecord.amount, + matching_order_id: transferRecord.matching_order_id, + cycle_number: transferRecord.cycle_number + }); + } + } + + console.log('\n' + '=' .repeat(60)); + + // 总结 + if (allocationCount[0].count === transferCount[0].count && unlinkedCount[0].count === 0) { + console.log('✓ 验证成功! 所有 order_allocations 记录都已正确迁移到 transfers 表'); + } else { + console.log('⚠ 验证结果: 可能存在未完全迁移的数据'); + console.log(` - order_allocations 总数: ${allocationCount[0].count}`); + console.log(` - transfers 中 allocation 类型数: ${transferCount[0].count}`); + console.log(` - 未关联记录数: ${unlinkedCount[0].count}`); + } + + } catch (error) { + console.error('验证失败:', error); + throw error; + } finally { + // 关闭数据库连接 + if (connection) { + await connection.end(); + } + } +} + +// 如果直接运行此脚本 +if (require.main === module) { + verifyMerge().catch(console.error); +} + +module.exports = verifyMerge; \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..13a745a --- /dev/null +++ b/server.js @@ -0,0 +1,348 @@ +// 加载环境变量配置 +require('dotenv').config(); + +const express = require('express'); +const cors = require('cors'); +const bodyParser = require('body-parser'); +const path = require('path'); +const mysql = require('mysql2/promise'); +const rateLimit = require('express-rate-limit'); +const helmet = require('helmet'); +const { initDB, getDB, dbConfig } = require('./database'); +const { logger } = require('./config/logger'); +const { errorHandler, notFound } = require('./middleware/errorHandler'); +const fs = require('fs'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// 确保日志目录存在 +const logDir = path.join(__dirname, 'logs'); +if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); +} + +// 安全中间件 +app.use(helmet({ + contentSecurityPolicy: false, // 为了支持前端应用 + crossOriginEmbedderPolicy: false, + crossOriginOpenerPolicy: false, // 禁用 COOP 头部以避免非 HTTPS 环境的警告 + originAgentCluster: false // 禁用Origin-Agent-Cluster头部 +})); + +// 中间件配置 +// CORS配置 - 允许前端访问静态资源 +app.use(cors({ + origin: [ + 'http://localhost:5173', + 'http://localhost:5174', + 'http://localhost:3001', + 'https://www.zrbjr.com', + 'https://zrbjr.com' + ], + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'] +})); +app.use(bodyParser.json({ limit: '10mb' })); +app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' })); + + + +// 请求日志中间件 +app.use((req, res, next) => { + const start = Date.now(); + + res.on('finish', () => { + const duration = Date.now() - start; + logger.info('HTTP Request', { + method: req.method, + url: req.originalUrl, + statusCode: res.statusCode, + duration: `${duration}ms`, + ip: req.ip, + userAgent: req.get('User-Agent') + }); + }); + + next(); +}); + +// 限流中间件 +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15分钟 + max: 1000, // 限制每个IP 15分钟内最多100个请求 + message: { + success: false, + error: { + code: 'RATE_LIMIT_EXCEEDED', + message: '请求过于频繁,请稍后再试' + } + } +}); +app.use('/api', limiter); + +// 静态文件服务 - 必须在API路由之前 +// 为uploads路径配置CORS和静态文件服务 +app.use('/uploads', express.static(path.join(__dirname, 'uploads'), { + setHeaders: (res, filePath) => { + // 设置CORS头部 + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); + + // 设置缓存和内容类型 + if (filePath.endsWith('.jpg') || filePath.endsWith('.jpeg')) { + res.setHeader('Content-Type', 'image/jpeg'); + } else if (filePath.endsWith('.png')) { + res.setHeader('Content-Type', 'image/png'); + } else if (filePath.endsWith('.gif')) { + res.setHeader('Content-Type', 'image/gif'); + } else if (filePath.endsWith('.webp')) { + res.setHeader('Content-Type', 'image/webp'); + } + res.setHeader('Cache-Control', 'public, max-age=86400'); // 1天缓存 + } +})); + +// 处理vite.svg请求 +app.get('/vite.svg', (req, res) => { + const referer = req.get('Referer'); + if (referer && referer.includes('/admin')) { + // 为admin页面提供logo.svg + res.setHeader('Content-Type', 'image/svg+xml'); + res.sendFile(path.join(__dirname, 'admin/dist/logo.svg')); + } else { + // 前端页面没有vite.svg,返回404 + res.status(404).send('File not found'); + } +}); + +// 静态文件服务配置 +// 专门处理admin路径下的assets +app.use('/admin/assets', express.static(path.join(__dirname, 'admin/dist/assets'), { + setHeaders: (res, filePath) => { + res.removeHeader('Origin-Agent-Cluster'); + if (filePath.endsWith('.css')) { + res.setHeader('Content-Type', 'text/css; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + } else if (filePath.endsWith('.js')) { + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + } + } +})); + +// 为admin页面的assets提供服务(当从admin页面访问/assets/时) +app.use('/assets', (req, res, next) => { + // 检查referer来判断是否来自admin页面 + const referer = req.get('Referer'); + if (referer && referer.includes('/admin')) { + // 如果来自admin页面,从admin/dist/assets提供文件 + express.static(path.join(__dirname, 'admin/dist/assets'), { + setHeaders: (res, filePath) => { + res.removeHeader('Origin-Agent-Cluster'); + if (filePath.endsWith('.css')) { + res.setHeader('Content-Type', 'text/css; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + } else if (filePath.endsWith('.js')) { + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + } + } + })(req, res, next); + } else { + // 否则从frontend/dist/assets提供文件 + express.static(path.join(__dirname, 'frontend/dist/assets'), { + setHeaders: (res, filePath) => { + res.removeHeader('Origin-Agent-Cluster'); + if (filePath.endsWith('.css')) { + res.setHeader('Content-Type', 'text/css; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + } else if (filePath.endsWith('.js')) { + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + } + } + })(req, res, next); + } +}); + +app.use('/admin', express.static(path.join(__dirname, 'admin/dist'), { + setHeaders: (res, filePath) => { + // 移除Origin-Agent-Cluster头部以避免冲突 + res.removeHeader('Origin-Agent-Cluster'); + + if (filePath.endsWith('.css')) { + res.setHeader('Content-Type', 'text/css; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1年缓存 + } else if (filePath.endsWith('.js')) { + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1年缓存 + } else if (filePath.endsWith('.html')) { + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); // HTML文件不缓存 + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + } else if (filePath.endsWith('.svg')) { + res.setHeader('Content-Type', 'image/svg+xml'); + } + } +})); + +app.use(express.static(path.join(__dirname, 'frontend/dist'), { + setHeaders: (res, filePath) => { + // 移除Origin-Agent-Cluster头部以避免冲突 + res.removeHeader('Origin-Agent-Cluster'); + + if (filePath.endsWith('.css')) { + res.setHeader('Content-Type', 'text/css; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1年缓存 + } else if (filePath.endsWith('.js')) { + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1年缓存 + } else if (filePath.endsWith('.html')) { + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); // HTML文件不缓存 + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + } else if (filePath.endsWith('.svg')) { + res.setHeader('Content-Type', 'image/svg+xml'); + } + } +})); + +// 引入数据库初始化模块 +const { initDatabase } = require('./config/database-init'); + +// API路由 +app.use('/api/auth', require('./routes/auth')); +app.use('/api/users', require('./routes/users')); +app.use('/api/user', require('./routes/users')); // 添加单数形式的路由映射 +app.use('/api/products', require('./routes/products')); +app.use('/api/orders', require('./routes/orders')); +app.use('/api/points', require('./routes/points')); +app.use('/api/captcha', require('./routes/captcha')); // 验证码路由 +app.use('/api/sms', require('./routes/sms')); // 短信验证路由 + +app.use('/api/upload', require('./routes/upload')); +app.use('/api/transfers', require('./routes/transfers')); +app.use('/api/matching', require('./routes/matching')); +app.use('/api/admin/matching', require('./routes/matchingAdmin')); +app.use('/api/system', require('./routes/system')); +app.use('/api/risk', require('./routes/riskManagement')); +app.use('/api/agents', require('./routes/agents')); +app.use('/api/admin/agents', require('./admin/routes/agents')); +app.use('/api/admin/withdrawals', require('./admin/routes/withdrawals')); +app.use('/api/agent-withdrawals', require('./routes/agent-withdrawals')); +app.use('/api/regions', require('./routes/regions')); + +// 前端路由 - 必须在最后,作为fallback +app.get('/', (req, res) => { + res.removeHeader('Origin-Agent-Cluster'); + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + res.sendFile(path.join(__dirname, 'frontend/dist/index.html')); +}); + +app.get('/admin*', (req, res) => { + res.removeHeader('Origin-Agent-Cluster'); + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + res.sendFile(path.join(__dirname, 'admin/dist/index.html')); +}); + +app.get('/frontend*', (req, res) => { + res.removeHeader('Origin-Agent-Cluster'); + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + res.sendFile(path.join(__dirname, 'frontend/dist/index.html')); +}); + +// SPA fallback - 处理前端路由 +app.get('*', (req, res) => { + // 如果请求的是静态资源但找不到,返回404(不返回JSON) + if (req.path.includes('.')) { + return res.status(404).send('File not found'); + } + // 否则返回前端应用的index.html + res.removeHeader('Origin-Agent-Cluster'); + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + res.sendFile(path.join(__dirname, 'frontend/dist/index.html')); +}); + +// 404处理 +app.use(notFound); + +// 全局错误处理中间件 +app.use(errorHandler); + +// 导出数据库连接供路由使用 +module.exports = { + get db() { return getDB(); } +}; + +// 启动服务器 +app.listen(PORT, async () => { + try { + logger.info('Server starting', { port: PORT }); + console.log(`服务器运行在 http://localhost:${PORT}`); + console.log(`前端页面: http://localhost:${PORT}/frontend`); + console.log(`后台管理: http://localhost:${PORT}/admin`); + + await initDatabase(); + // global.sqlReq = mysql.createConnection() + // 启动转账超时检查服务 + const timeoutService = require('./services/timeoutService'); + timeoutService.startTimeoutChecker(); + console.log('转账超时检查服务已启动'); + + // 启动数据库连接监控 + // const dbMonitor = require('./db-monitor'); + // dbMonitor.startMonitoring(60000); // 每分钟监控一次 + // console.log('数据库连接监控已启动'); + + logger.info('Server started successfully', { + port: PORT, + environment: process.env.NODE_ENV || 'development' + }); + } catch (error) { + logger.error('Failed to start server', { error: error.message }); + process.exit(1); + } +}); + +// 优雅关闭 +process.on('SIGTERM', async () => { + logger.info('SIGTERM received, shutting down gracefully'); + try { + const { closeDB } = require('./database'); + await closeDB(); + } catch (error) { + logger.error('Error closing database', { error: error.message }); + } + process.exit(0); +}); + +process.on('SIGINT', async () => { + logger.info('SIGINT received, shutting down gracefully'); + try { + const { closeDB } = require('./database'); + await closeDB(); + } catch (error) { + logger.error('Error closing database', { error: error.message }); + } + process.exit(0); +}); + +process.on('unhandledRejection', (reason, promise) => { + logger.error('Unhandled Rejection', { reason, promise }); +}); + +process.on('uncaughtException', (error) => { + logger.error('Uncaught Exception', { error: error.message, stack: error.stack }); + process.exit(1); +}); \ No newline at end of file diff --git a/services/matchingService.js b/services/matchingService.js new file mode 100644 index 0000000..b3553a2 --- /dev/null +++ b/services/matchingService.js @@ -0,0 +1,1469 @@ +const { getDB } = require('../database'); +const timeoutService = require('./timeoutService'); +const dayjs = require('dayjs'); + +/** + * 获取本地时区的日期字符串(YYYY-MM-DD格式) + * 确保在晚上12点正确切换到第二天 + * @param {Date} date - 可选的日期对象,默认为当前时间 + * @returns {string} 格式化的日期字符串 + */ +function getLocalDateString(date = new Date()) { + return dayjs(date).format('YYYY-MM-DD'); +} + +class MatchingService { + // 创建匹配订单(支持两种模式) + async createMatchingOrder(userId, matchingType = 'small', customAmount = null) { + const db = getDB(); + + try { + // 检查用户是否被拉黑 + const isBlacklisted = await timeoutService.isUserBlacklisted(userId); + if (isBlacklisted) { + throw new Error('您已被拉黑,无法参与匹配。如有疑问请联系管理员。'); + } + + // 检查用户审核状态、必要信息和余额 + 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 = ?', + [userId] + ); + + if (userResult.length === 0) { + throw new Error('用户不存在'); + } + + const user = userResult[0]; + + // 检查用户余额:只有负余额用户才能发起匹配 + // if (user.balance > 0) { + // throw new Error('只有余额为负数的用户才能发起匹配,这是为了防止公司资金损失的重要规则'); + // } + + // 检查用户审核状态 + if (user.audit_status !== 'approved') { + if (user.audit_status === 'pending') { + throw new Error('您的账户正在审核中,审核通过后才能参与匹配'); + } else if (user.audit_status === 'rejected') { + throw new Error('您的账户审核未通过,请联系管理员'); + } + } + + // 检查必要的收款信息是否已上传 + const missingItems = []; + if (!user.wechat_qr && !user.alipay_qr && !user.unionpay_qr) { + missingItems.push('收款码(微信/支付宝/云闪付至少一种)'); + } + if (!user.bank_card) { + missingItems.push('银行卡号'); + } + if (!user.business_license) { + missingItems.push('营业执照'); + } + if (!user.id_card_front || !user.id_card_back) { + missingItems.push('身份证正反面'); + } + + if (missingItems.length > 0) { + throw new Error(`请先上传以下信息:${missingItems.join('、')}`); + } + + await db.query('START TRANSACTION'); + + let totalAmount, maxCycles; + + if (matchingType === 'small') { + // 小额匹配:固定5000元金额 + totalAmount = 5000; + maxCycles = 1; + } else if (matchingType === 'large') { + // 大额匹配:用户自定义金额(最高5万) + if (!customAmount || customAmount < 5000 || customAmount > 50000) { + throw new Error('大额匹配金额必须在5000-50000之间'); + } + totalAmount = customAmount; + maxCycles = 1; + } else { + throw new Error('不支持的匹配类型'); + } + + // 创建匹配订单 + const [result] = await db.execute( + 'INSERT INTO matching_orders (initiator_id, amount, status, max_cycles, matching_type) VALUES (?, ?, "matching", ?, ?)', + [userId, totalAmount, maxCycles, matchingType] + ); + + const orderId = result.insertId; + + // 记录用户参与 + await db.execute( + 'INSERT INTO matching_records (matching_order_id, user_id, action, amount) VALUES (?, ?, "join", ?)', + [orderId, userId, totalAmount] + ); + + await db.query('COMMIT'); + + // 立即生成智能分配 + const allocations = await this.generateSmartAllocationsWithDB(orderId, userId); + + // 检查并触发系统账户反向匹配 + // await this.checkAndTriggerSystemMatching(); + + return { + orderId, + matchingType, + totalAmount, + allocations: allocations || [], + allocationCount: allocations ? allocations.length : 0 + }; + } catch (error) { + await db.query('ROLLBACK'); + console.error('创建匹配订单失败:', error); + throw error; + } + } + + /** + * 检查资金平衡并触发系统账户反向匹配 + * 当收款需求大于打款资金时,系统账户主动发起匹配 + */ + async checkAndTriggerSystemMatching() { + const db = getDB(); + + try { + // 计算当前待收款总额(负余额用户的资金缺口) + const [negativeBalanceResult] = await db.execute(` + SELECT SUM(ABS(balance)) as total_deficit + FROM users + WHERE is_system_account = FALSE AND balance < 0 + `); + + const totalDeficit = negativeBalanceResult[0].total_deficit || 0; + + // 计算当前待打款总额(pending状态的分配) + const [pendingPaymentsResult] = await db.execute(` + SELECT SUM(oa.amount) as total_pending + FROM transfers oa + JOIN users u ON oa.from_user_id = u.id + WHERE oa.status = 'pending' AND u.is_system_account = FALSE + `); + + const totalPendingPayments = pendingPaymentsResult[0].total_pending || 0; + + console.log(`资金平衡检查: 总资金缺口=${totalDeficit}, 待打款总额=${totalPendingPayments}`); + + // 如果收款需求大于打款资金,触发系统账户反向匹配 + if (totalDeficit > totalPendingPayments) { + const shortfall = totalDeficit - totalPendingPayments; + console.log(`检测到资金缺口: ${shortfall}元,触发系统账户反向匹配`); + + await this.createSystemReverseMatching(shortfall); + } + } catch (error) { + console.error('检查资金平衡失败:', error); + // 不抛出错误,避免影响主流程 + } + } + + /** + * 创建系统账户反向匹配 + * 系统账户作为付款方,向有资金缺口的用户打款 + * @param {number} targetAmount - 目标匹配金额 + */ + async createSystemReverseMatching(targetAmount) { + const db = getDB(); + + try { + // 获取可用的系统账户 + const [systemAccounts] = await db.execute(` + SELECT id, balance FROM users + WHERE is_system_account = TRUE AND balance > 1000 + ORDER BY balance DESC + LIMIT 1 + `); + + if (systemAccounts.length === 0) { + console.log('没有可用的系统账户进行反向匹配'); + return; + } + + const systemAccount = systemAccounts[0]; + + // 确定实际匹配金额(不超过系统账户余额的80%) + const maxMatchAmount = Math.min(targetAmount, systemAccount.balance * 0.8); + + if (maxMatchAmount < 1000) { + console.log('系统账户余额不足,无法进行反向匹配'); + return; + } + + // 创建系统反向匹配订单 + const [result] = await db.execute( + 'INSERT INTO matching_orders (initiator_id, amount, status, max_cycles, matching_type, is_system_reverse) VALUES (?, ?, "matching", 1, "system_reverse", TRUE)', + [systemAccount.id, maxMatchAmount] + ); + + const orderId = result.insertId; + + // 生成分配给负余额用户 + await this.generateSystemReverseAllocations(orderId, maxMatchAmount, systemAccount.id); + + console.log(`系统反向匹配创建成功: 订单ID=${orderId}, 金额=${maxMatchAmount}`); + + } catch (error) { + console.error('创建系统反向匹配失败:', error); + } + } + + /** + * 为系统反向匹配生成分配 + * @param {number} orderId - 匹配订单ID + * @param {number} totalAmount - 总金额 + * @param {number} systemUserId - 系统账户ID + */ + async generateSystemReverseAllocations(orderId, totalAmount, systemUserId) { + const db = getDB(); + + try { + // 获取负余额用户,按缺口大小排序 + const [negativeUsers] = await db.execute(` + SELECT id, balance, ABS(balance) as deficit + FROM users + WHERE is_system_account = FALSE AND balance < 0 + ORDER BY deficit DESC + LIMIT 10 + `); + + if (negativeUsers.length === 0) { + console.log('没有负余额用户需要反向匹配'); + return; + } + + // 按比例分配金额给负余额用户 + const totalDeficit = negativeUsers.reduce((sum, user) => sum + user.deficit, 0); + let remainingAmount = totalAmount; + + for (let i = 0; i < negativeUsers.length && remainingAmount > 0; i++) { + const user = negativeUsers[i]; + let allocationAmount; + + if (i === negativeUsers.length - 1) { + // 最后一个用户分配剩余金额 + allocationAmount = remainingAmount; + } else { + // 按比例分配 + const proportion = user.deficit / totalDeficit; + allocationAmount = Math.min( + Math.floor(totalAmount * proportion), + user.deficit, + remainingAmount + ); + } + + if (allocationAmount > 0) { + // 创建分配记录(系统账户向用户转账) + await db.execute( + 'INSERT INTO transfers (matching_order_id, from_user_id, to_user_id, amount, cycle_number, status) VALUES (?, ?, ?, ?, 1, "pending")', + [orderId, systemUserId, user.id, allocationAmount] + ); + + remainingAmount -= allocationAmount; + console.log(`系统反向分配: ${allocationAmount}元 从系统账户${systemUserId} 到用户${user.id}`); + } + } + + } catch (error) { + console.error('生成系统反向分配失败:', error); + throw error; + } + } + + /** + * 生成大额匹配的随机金额分拆(15000以上) + * @param {number} totalAmount - 总金额 + * @returns {Array} 分拆后的金额数组 + */ + generateRandomLargeAmounts(totalAmount) { + const amounts = []; + let remaining = totalAmount; + const minAmount = 1000; // 最小单笔金额 + const maxAmount = 8000; // 最大单笔金额 + + while (remaining > maxAmount) { + // 生成随机金额,确保剩余金额至少还能分一笔 + const maxThisAmount = Math.min(maxAmount, remaining - minAmount); + const amount = Math.floor(Math.random() * (maxThisAmount - minAmount + 1)) + minAmount; + amounts.push(amount); + remaining -= amount; + } + + // 最后一笔是剩余金额 + if (remaining > 0) { + amounts.push(remaining); + } + + return amounts; + } + + /** + * 生成3笔分配(兼容旧版本接口) + * 确保不会分配给同一个用户 + * @param {number} orderId - 订单ID + * @param {Array} amounts - 金额数组 + * @param {number} initiatorId - 发起人ID + */ + /** + * 验证匹配金额是否符合业务规则 + * @param {number} userId - 用户ID + * @param {number} totalAmount - 总匹配金额 + * @param {Array} amounts - 分配金额数组 + * @returns {Object} 验证结果和建议金额 + */ + async validateMatchingAmount(userId, totalAmount, amounts) { + const db = getDB(); + + try { + // 获取昨天的日期(本地时区) + const yesterdayStr = dayjs().subtract(1, 'day').format('YYYY-MM-DD'); + + // 获取前一天所有用户的出款总数(系统总出款) + const [systemOutboundResult] = await db.execute( + `SELECT SUM(oa.amount) as total_outbound + FROM transfers oa + JOIN users u ON oa.from_user_id = u.id + WHERE DATE(oa.outbound_date) = ? AND oa.status = 'confirmed' AND u.is_system_account = FALSE`, + [yesterdayStr] + ); + + const systemOutbound = systemOutboundResult[0].total_outbound || 0; + + // 获取前一天所有用户的具体出款金额(用于检查重复) + const [yesterdayAmountsResult] = await db.execute( + `SELECT DISTINCT oa.amount + FROM transfers oa + JOIN users u ON oa.from_user_id = u.id + WHERE DATE(oa.outbound_date) = ? AND oa.status = 'confirmed' AND u.is_system_account = FALSE`, + [yesterdayStr] + ); + + const yesterdayAmounts = yesterdayAmountsResult.map(row => row.amount); + + // 检查每笔金额是否与前一天的金额不同 + const duplicateAmounts = []; + for (const amount of amounts) { + if (yesterdayAmounts.includes(amount)) { + duplicateAmounts.push(amount); + } + } + + return { + isValid: duplicateAmounts.length === 0, + systemOutbound, + duplicateAmounts, + suggestedAmount: systemOutbound, + message: duplicateAmounts.length > 0 + ? `以下金额与前一天重复: ${duplicateAmounts.join(', ')}元` + : '匹配金额验证通过' + }; + } catch (error) { + console.error('验证匹配金额失败:', error); + return { + isValid: false, + systemOutbound: 0, + duplicateAmounts: [], + suggestedAmount: 0, + message: '验证匹配金额时发生错误' + }; + } + } + + /** + * 生成智能分配并创建数据库记录 + * @param {number} orderId - 订单ID + * @param {number} initiatorId - 发起人ID + * @returns {Promise} 返回分配结果数组 + */ + async generateSmartAllocationsWithDB(orderId, initiatorId) { + const db = getDB(); + + try { + // 获取订单总金额 + const [orderResult] = await db.execute( + 'SELECT amount FROM matching_orders WHERE id = ?', + [orderId] + ); + + if (orderResult.length === 0) { + throw new Error('匹配订单不存在'); + } + + const totalAmount = orderResult[0].amount; + + // 使用智能分配算法生成分配方案 + const allocations = await this.generateSmartAllocations(totalAmount, initiatorId); + + if (allocations.length === 0) { + throw new Error('无法生成有效的分配方案'); + } + + // 验证总金额(简化版验证) + const totalAllocated = allocations.reduce((sum, allocation) => sum + allocation.amount, 0); + if (Math.abs(totalAllocated - totalAmount) > 0.01) { + throw new Error(`分配金额不匹配:期望${totalAmount}元,实际分配${totalAllocated}元`); + } + + console.log(`智能分配验证通过: 用户${initiatorId}, 匹配金额${totalAmount}元, 分配${allocations.length}笔`); + + // 创建分配记录 + const createdAllocations = []; + for (let i = 0; i < allocations.length; i++) { + const allocation = allocations[i]; + + // 设置出款日期为今天,可回款时间为明天的00:00:00 + const today = dayjs(); + const tomorrow = dayjs().add(1, 'day').startOf('day'); + + const [result] = await db.execute( + 'INSERT INTO transfers (matching_order_id, from_user_id, to_user_id, amount, cycle_number, status, outbound_date, can_return_after) VALUES (?, ?, ?, ?, 1, "pending", CURDATE(), ?)', + [orderId, initiatorId, allocation.userId, allocation.amount, tomorrow.format('YYYY-MM-DD HH:mm:ss')] + ); + + // 添加分配ID到结果中 + const createdAllocation = { + ...allocation, + allocationId: result.insertId, + status: 'pending', + outboundDate: today.format('YYYY-MM-DD'), + canReturnAfter: tomorrow.toISOString() + }; + + createdAllocations.push(createdAllocation); + + console.log(`创建智能分配: ${allocation.amount}元 从用户${initiatorId} 到用户${allocation.userId}(${allocation.username}) [${allocation.userType}]`); + } + + return createdAllocations; + } catch (error) { + console.error('生成智能分配失败:', error); + throw error; + } + } + + /** + * 生成传统三笔分配(保留原方法用于兼容性) + * @param {number} orderId - 订单ID + * @param {Array} amounts - 分配金额数组 + * @param {number} initiatorId - 发起人ID + * @returns {Promise} + */ + async generateThreeAllocations(orderId, amounts, initiatorId) { + const db = getDB(); + + try { + // 获取订单总金额 + const [orderResult] = await db.execute( + 'SELECT amount FROM matching_orders WHERE id = ?', + [orderId] + ); + + if (orderResult.length === 0) { + throw new Error('匹配订单不存在'); + } + + const totalAmount = orderResult[0].amount; + + // 验证匹配金额是否符合业务规则 + const validation = await this.validateMatchingAmount(initiatorId, totalAmount, amounts); + if (!validation.isValid) { + throw new Error(`匹配金额不符合业务规则:${validation.message}。建议匹配金额:${validation.suggestedAmount}元`); + } + + // 记录验证信息 + console.log(`匹配金额验证通过: 用户${initiatorId}, 匹配金额${totalAmount}元, 前一天系统出款${validation.systemOutbound}元`); + + const usedTargetUsers = new Set(); // 记录已使用的目标用户 + + for (let i = 0; i < amounts.length; i++) { + const amount = amounts[i]; + + // 获取匹配目标,排除已使用的用户 + const targetUser = await this.getMatchingTargetExcluding(initiatorId, usedTargetUsers); + + if (!targetUser) { + throw new Error(`无法为第${i + 1}笔分配找到匹配目标`); + } + + // 记录已使用的目标用户 + usedTargetUsers.add(targetUser); + + // 创建分配记录,默认为第1轮 + // 设置出款日期为今天,可回款时间为明天的00:00:00 + const today = new Date(); + const tomorrow = new Date(today); + tomorrow.setDate(tomorrow.getDate() + 1); + tomorrow.setHours(0, 0, 0, 0); + + // 将Date对象转换为MySQL兼容的字符串格式 + const tomorrowStr = tomorrow.toISOString().slice(0, 19).replace('T', ' '); + + await db.execute( + 'INSERT INTO transfers (matching_order_id, from_user_id, to_user_id, amount, cycle_number, status, outbound_date, can_return_after) VALUES (?, ?, ?, ?, 1, "pending", CURDATE(), ?)', + [orderId, initiatorId, targetUser, amount, tomorrowStr] + ); + + console.log(`创建分配: ${amount}元 从用户${initiatorId} 到用户${targetUser}`); + } + } catch (error) { + console.error('生成分配失败:', error); + throw error; + } + } + + /** + * 获取匹配目标用户 + * @param {number} excludeUserId - 要排除的用户ID + * @returns {number} 目标用户ID + */ + + + /** + * 获取匹配目标用户(排除指定用户集合) + * @param {number} excludeUserId - 要排除的发起人用户ID + * @param {Set} excludeUserIds - 要排除的用户ID集合 + * @returns {number} 目标用户ID + */ + async getMatchingTargetExcluding(excludeUserId, excludeUserIds = new Set()) { + const db = getDB(); + + try { + // 获取今天和昨天的日期(本地时区) + const today = new Date(); + const todayStr = getLocalDateString(today); + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + const yesterdayStr = getLocalDateString(yesterday); + + // 获取前一天打款的用户ID列表(需要排除) + const [yesterdayPayersResult] = await db.execute( + `SELECT DISTINCT oa.from_user_id + FROM transfers oa + WHERE DATE(oa.outbound_date) = ? AND oa.status = 'confirmed'`, + [yesterdayStr] + ); + const yesterdayPayers = yesterdayPayersResult.map(row => row.from_user_id); + + // 获取待确认/待处理/即将生成匹配金额总和超过0的普通用户(需要排除) + const [pendingUsersResult] = await db.execute( + `SELECT oa.to_user_id, SUM(oa.amount) as pending_amount + FROM transfers oa + JOIN users u ON oa.to_user_id = u.id + WHERE oa.status IN ('pending', 'processing', 'generating') + AND u.is_system_account = FALSE + GROUP BY oa.to_user_id + HAVING pending_amount > 0` + ); + const pendingUsers = pendingUsersResult.map(row => row.to_user_id); + + // 获取当天有转出订单的普通用户(需要排除) + const [todayPayersResult] = await db.execute( + `SELECT DISTINCT oa.from_user_id + FROM transfers oa + JOIN users u ON oa.from_user_id = u.id + WHERE DATE(oa.created_at) = ? + AND oa.status IN ('confirmed', 'pending', 'processing') + AND u.is_system_account = FALSE`, + [todayStr] + ); + const todayPayers = todayPayersResult.map(row => row.from_user_id); + + // 构建排除用户的条件(包括发起人、已使用的用户、昨天打款的用户、待处理用户、当天转出用户) + const excludeList = [ + excludeUserId, + ...Array.from(excludeUserIds), + ...yesterdayPayers, + ...pendingUsers, + ...todayPayers + ]; + const placeholders = excludeList.map(() => '?').join(','); + + // 第一优先级:最早成为负数且通过前面检查的普通用户 + // 使用最早的转出记录时间作为成为负数的参考时间 + const [earliestNegativeUsers] = await db.execute( + `SELECT u.id, u.balance, + (SELECT MIN(t.created_at) FROM transfers t + WHERE t.from_user_id = u.id AND t.status IN ('confirmed', 'received')) as first_transfer_time + FROM users u + WHERE u.id NOT IN (${placeholders}) + AND u.is_system_account = FALSE + AND u.balance < 0 + AND (SELECT COUNT(*) FROM transfers t + WHERE t.from_user_id = u.id AND t.status IN ('confirmed', 'received')) > 0 + ORDER BY first_transfer_time ASC, u.balance ASC, RAND() + LIMIT 1`, + excludeList + ); + + if (earliestNegativeUsers.length > 0) { + return earliestNegativeUsers[0].id; + } + + // 第二优先级:有可回款分配的普通用户(昨天或更早出款,今天可以回款) + // 但必须是负余额用户,且通过前面的检查 + const [returnableUsers] = await db.execute( + `SELECT DISTINCT oa.from_user_id as id, u.balance + FROM transfers oa + JOIN matching_orders mo ON oa.id = mo.id + JOIN users u ON oa.from_user_id = u.id + WHERE oa.from_user_id NOT IN (${placeholders}) + AND oa.status = 'confirmed' + AND oa.can_return_after <= NOW() + AND oa.return_date IS NULL + AND mo.status = 'matching' + AND u.balance < 0 + AND u.is_system_account = FALSE + ORDER BY oa.can_return_after ASC, u.balance ASC, RAND() + LIMIT 1`, + excludeList + ); + + if (returnableUsers.length > 0) { + return returnableUsers[0].id; + } + + // 第三优先级:其他负余额普通用户(余额为负数说明他们给其他用户转过钱,钱还没收回来) + const [negativeBalanceUsers] = await db.execute( + `SELECT id FROM users + WHERE id NOT IN (${placeholders}) + AND is_system_account = FALSE + AND balance < 0 + ORDER BY balance ASC, RAND() + LIMIT 1`, + excludeList + ); + + if (negativeBalanceUsers.length > 0) { + return negativeBalanceUsers[0].id; + } + + // 最后优先级:虚拟用户(系统账户) + const [systemUsers] = await db.execute( + `SELECT id FROM users + WHERE is_system_account = TRUE AND id NOT IN (${placeholders}) + ORDER BY balance DESC, RAND() + LIMIT 1`, + excludeList + ); + + if (systemUsers.length > 0) { + return systemUsers[0].id; + } + + // 如果连系统账户都没有,抛出错误 + throw new Error('没有可用的匹配目标:所有符合条件的用户都被排除'); + } catch (error) { + console.error('获取匹配目标失败:', error); + throw error; + } + } + + // 获取可用用户 + + + /** + * 生成智能分配金额 + * 1. 排除今天打款的用户 + * 2. 优先分配给负余额用户(余额+待确认收款为负数) + * 3. 每笔最高5000,不够再分配给虚拟用户 + * 4. 笔数3-10笔 + * @param {number} totalAmount - 总金额 + * @param {number} excludeUserId - 排除的用户ID(发起人) + * @returns {Promise} 分配金额数组 + */ + async generateSmartAllocations(totalAmount, excludeUserId) { + const db = getDB(); + const minAmount = 100; + const maxAmountPerTransfer = totalAmount; + const minTransfers = totalAmount > 5000 ? 4 : 3; + const maxTransfers = 10; + + try { + // 获取负余额用户 + let [userBalanceResult] = await db.execute( + `SELECT + u.id as user_id, + u.balance as current_balance + FROM users u + WHERE u.is_system_account = FALSE + AND u.id != ? + AND u.balance < 0 + AND u.audit_status = 'approved' + ORDER BY u.balance ASC`, + [excludeUserId] + ); + + // 处理查询到的负余额用户 + const availableUsers = []; + + for (const user of userBalanceResult) { + // 确保余额是数字类型 + const currentBalance = parseFloat(user.current_balance) || 0; + + // 更新用户对象 + user.current_balance = currentBalance; + + // 查询用户的分配订单金额统计 + const [orderStatusResult] = await db.execute( + `SELECT + SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) as pending_amount, + SUM(CASE WHEN status = 'processing' THEN amount ELSE 0 END) as processing_amount + FROM transfers + WHERE to_user_id = ?`, + [user.user_id] + ); + + // 查询用户的分配订单金待确认金额统计 + const [orderStatusConfirmedResult] = await db.execute( + `SELECT + SUM(CASE WHEN status = 'confirmed' THEN amount ELSE 0 END) as confirmed_amount + FROM transfers + WHERE to_user_id = ?`, + [user.user_id] + ); + // 查询用户当天在matching_orders表中打出去的款项 + const today = getLocalDateString(); + const [todayOutflowResult] = await db.execute( + `SELECT + SUM(amount) as today_outflow + FROM matching_orders + WHERE initiator_id = ? AND DATE(updated_at) = ?`, + [user.user_id, today] + ); + + // 添加分配金额信息到用户对象 + const orderStatus = orderStatusResult[0] || { pending_amount: 0, processing_amount: 0 }; + const todayOutflow = todayOutflowResult[0] || { today_outflow: 0 }; + const orderStatusConfirmed = orderStatusConfirmedResult[0] || { confirmed_amount: 0 }; + user.today_outflow = parseFloat(todayOutflow.today_outflow) || 0; + user.pending_amount = parseFloat(orderStatus.pending_amount) || 0; + user.processing_amount = parseFloat(orderStatus.processing_amount) || 0; + user.confirmed_amount = parseFloat(orderStatusConfirmed.confirmed_amount) || 0; + user.has_active_allocations = user.current_balance + user.pending_amount + user.processing_amount + user.confirmed_amount + user.today_outflow; + + + + // 所有查询到的用户都是负余额用户,直接添加到可用列表 + } + userBalanceResult = userBalanceResult.sort((a, b) => a.has_active_allocations - b.has_active_allocations); + for (const user of userBalanceResult) { + if (user.has_active_allocations < -100 && maxTransfers > availableUsers.length + 1) { + if (minTransfers === 3 && availableUsers.length < 3) { + availableUsers.push(user); + } + if (minTransfers === 4) { + availableUsers.push(user); + } + } + console.log(user, '普通用户'); + } + + + console.log(`可参与分配的负余额用户数量: ${availableUsers.length}`); + + // 第二步:由于第一步已经筛选了余额小于0的用户,这里直接使用可用用户作为优先分配用户 + // 用户已按余额升序排列(最负的优先),然后按可分配金额降序排列 + const priorityUsers = availableUsers; // 所有可用用户都是负余额用户,无需再次筛选 + + // 第三步:获取虚拟用户作为备选 + const [virtualUsersResult] = await db.execute( + `SELECT id, username, balance FROM users + WHERE is_system_account = TRUE + ORDER BY balance DESC, RAND()` + ); + + // 计算分配方案 + const allocations = []; + let remainingAmount = totalAmount; + + // 优先分配给当前余额为负的用户 + for (const user of priorityUsers) { + if (remainingAmount <= 0 || allocations.length >= maxTransfers) break; + + // 计算该用户可接受的最大分配金额 + // 确保分配后用户余额不会变成正数 + const currentBalance = Math.abs(user.has_active_allocations); + // 使用随机分配而不是平均分配 + const remainingTransfers = minTransfers - allocations.length; + const minRequiredForRemaining = Math.max(0, (remainingTransfers - 1) * minAmount); // 为剩余转账预留的最小金额,确保不为负数 + + console.log(`用户${user.user_id}分配计算: remainingAmount=${remainingAmount}, remainingTransfers=${remainingTransfers}, minRequiredForRemaining=${minRequiredForRemaining}`); + + const maxRandomAllocation = Math.min( + currentBalance, // 不能超过安全分配额度,确保接收后余额不会变成正数 + maxAmountPerTransfer, // 单笔最大金额限制 + remainingAmount - minRequiredForRemaining // 确保剩余金额足够分配给后续转账 + ); + + // 生成随机分配金额,使用极度偏向大值的算法 + let maxUserAllocation = 0; + if (maxRandomAllocation >= minAmount) { + const range = maxRandomAllocation - minAmount; + if (range <= 0) { + maxUserAllocation = minAmount; + } else { + // 使用极度偏向大值的策略 + const randomFactor = Math.pow(Math.random(), 0.15); // 0.15的幂使分布极度偏向较大值 + + // 基础分配:从range的80%开始,确保大部分分配都是较大值 + const baseOffset = Math.floor(range * 0.8); // 80%的基础偏移 + const adjustedRange = range - baseOffset; + maxUserAllocation = Math.floor(randomFactor * adjustedRange) + minAmount + baseOffset; + + // 几乎总是给予额外增量 + const bonusRange = Math.min(range * 0.6, maxRandomAllocation - maxUserAllocation); // 增加到60% + if (bonusRange > 0 && Math.random() > 0.1) { // 90%概率获得额外增量 + const bonus = Math.floor(Math.random() * bonusRange * 0.9); // 使用90%的bonus范围 + maxUserAllocation += bonus; + } + + // 确保不超过最大限制 + maxUserAllocation = Math.min(maxUserAllocation, maxRandomAllocation); + } + } + console.log(maxUserAllocation, minAmount, '+++++++++++++++'); + + if (maxUserAllocation >= minAmount) { + allocations.push({ + userId: user.user_id, + username: user.username || `用户${user.user_id}`, + amount: maxUserAllocation, + userType: 'priority_user', + currentBalance: user.current_balance, + historicalNetBalance: user.historical_net_balance, + totalPendingInflow: user.total_pending_inflow, + availableForAllocation: user.available_for_allocation, + todayOutflow: user.today_outflow + }); + remainingAmount -= maxUserAllocation; + } + } + + // 如果还有剩余金额且分配数量不足最小笔数,最后分配给虚拟用户 + const availableVirtualUsers = virtualUsersResult + + // 如果需要分配给虚拟用户,使用随机分配算法 + if (remainingAmount > 0 && availableVirtualUsers.length > 0) { + const maxPossibleTransfers = Math.min((minTransfers - allocations.length) <= 0 ? 1 : minTransfers - allocations.length, availableVirtualUsers.length); + + // 生成随机分配金额数组 + const randomAmounts = this.generateRandomAmounts(remainingAmount, maxPossibleTransfers, minAmount, maxAmountPerTransfer); + + // 为每个随机金额分配虚拟用户 + for (let i = 0; i < randomAmounts.length && availableVirtualUsers.length > 0; i++) { + const randomIndex = Math.floor(Math.random() * availableVirtualUsers.length); + const virtualUser = availableVirtualUsers[randomIndex]; + + allocations.push({ + userId: virtualUser.id, + username: virtualUser.username, + amount: randomAmounts[i], + userType: 'virtual', + balance: virtualUser.balance + }); + + remainingAmount -= randomAmounts[i]; + availableVirtualUsers.splice(randomIndex, 1); + } + } + + // 检查是否有足够的用户来完成分配 + if (remainingAmount > 0 && allocations.length < minTransfers && availableVirtualUsers.length === 0) { + throw new Error('没有足够的可用用户来完成分配(避免重复分配给同一用户)'); + } + + // 确保至少有最小笔数 + if (allocations.length < minTransfers) { + throw new Error(`无法生成足够的分配:需要至少${minTransfers}笔,但只能生成${allocations.length}笔`); + } + + // 精确控制总金额,避免超出预期 + const currentTotal = allocations.reduce((sum, a) => sum + a.amount, 0); + console.log('剩余金额处理前:', remainingAmount, '当前总分配金额:', currentTotal, '期望总金额:', totalAmount); + + if (remainingAmount > 0 && allocations.length > 0) { + // 检查是否会超出总金额 + if (currentTotal + remainingAmount <= totalAmount) { + console.log('将剩余金额', remainingAmount, '加到最后一笔分配'); + allocations[allocations.length - 1].amount += remainingAmount; + } else { + // 如果会超出,只加到刚好等于总金额的部分 + const allowedAmount = totalAmount - currentTotal; + if (allowedAmount > 0) { + + console.log('调整最后一笔分配,增加', allowedAmount, '元以达到精确总金额'); + allocations[allocations.length - 1].amount += allowedAmount; + } + } + remainingAmount = 0; // 重置剩余金额 + } + + console.log(`智能分配完成: 总金额${totalAmount}元,分配${allocations.length}笔`); + console.log('分配详情:', allocations.map(a => + `${a.amount}元 -> 用户${a.userId}(${a.username}) [${a.userType}]` + ).join(', ')); + + return allocations; + + } catch (error) { + console.error('智能分配失败:', error); + throw error; + } + } + + /** + * 生成随机分配金额数组 + * @param {number} totalAmount - 总金额 + * @param {number} transferCount - 分配笔数 + * @param {number} minAmount - 最小金额 + * @param {number} maxAmount - 最大金额 + * @returns {number[]} 随机金额数组 + */ + generateRandomAmounts(totalAmount, transferCount, minAmount, maxAmount) { + if (transferCount <= 0 || totalAmount < minAmount * transferCount) { + return []; + } + + const amounts = []; + let remainingAmount = totalAmount; + + // 为前n-1笔生成随机金额 + for (let i = 0; i < transferCount - 1; i++) { + const remainingTransfers = transferCount - i; + const minForThisTransfer = minAmount; + const maxForThisTransfer = Math.min( + maxAmount, + remainingAmount - (remainingTransfers - 1) * minAmount // 确保剩余金额足够分配给后续转账 + ); + + if (maxForThisTransfer < minForThisTransfer) { + // 如果无法满足约束,重新开始整个分配过程 + return this.generateRandomAmountsWithRetry(totalAmount, transferCount, minAmount, maxAmount); + } else { + // 在有效范围内生成随机金额 + const randomAmount = Math.floor(Math.random() * (maxForThisTransfer - minForThisTransfer + 1)) + minForThisTransfer; + amounts.push(randomAmount); + } + + remainingAmount -= amounts[amounts.length - 1]; + } + + // 最后一笔分配剩余金额 + if (remainingAmount >= minAmount && remainingAmount <= maxAmount) { + amounts.push(remainingAmount); + } else { + // 如果剩余金额不符合约束,使用重试机制 + return this.generateRandomAmountsWithRetry(totalAmount, transferCount, minAmount, maxAmount); + } + + return amounts; + } + + /** + * 使用重试机制生成随机金额分配(确保完全随机性) + * @param {number} totalAmount - 总金额 + * @param {number} transferCount - 分配笔数 + * @param {number} minAmount - 最小金额 + * @param {number} maxAmount - 最大金额 + * @returns {number[]} 随机金额数组 + */ + generateRandomAmountsWithRetry(totalAmount, transferCount, minAmount, maxAmount) { + const maxRetries = 100; + + for (let retry = 0; retry < maxRetries; retry++) { + const amounts = []; + let remainingAmount = totalAmount; + let success = true; + + // 为前n-1笔生成随机金额 + for (let i = 0; i < transferCount - 1; i++) { + const remainingTransfers = transferCount - i; + const minForThisTransfer = minAmount; + const maxForThisTransfer = Math.min( + maxAmount, + remainingAmount - (remainingTransfers - 1) * minAmount + ); + + if (maxForThisTransfer < minForThisTransfer) { + success = false; + break; + } + + // 在有效范围内生成随机金额 + const randomAmount = Math.floor(Math.random() * (maxForThisTransfer - minForThisTransfer + 1)) + minForThisTransfer; + amounts.push(randomAmount); + remainingAmount -= randomAmount; + } + + // 检查最后一笔是否符合约束 + if (success && remainingAmount >= minAmount && remainingAmount <= maxAmount) { + amounts.push(remainingAmount); + return amounts; + } + } + + // 如果重试失败,使用备用的随机分配策略 + return this.generateFallbackRandomAmounts(totalAmount, transferCount, minAmount, maxAmount); + } + + /** + * 备用随机分配策略(保证随机性的最后手段) + * @param {number} totalAmount - 总金额 + * @param {number} transferCount - 分配笔数 + * @param {number} minAmount - 最小金额 + * @param {number} maxAmount - 最大金额 + * @returns {number[]} 随机金额数组 + */ + generateFallbackRandomAmounts(totalAmount, transferCount, minAmount, maxAmount) { + // 检查是否可能分配 + if (totalAmount < minAmount * transferCount || totalAmount > maxAmount * transferCount) { + return []; + } + + const amounts = []; + + // 首先为每笔分配最小金额 + for (let i = 0; i < transferCount; i++) { + amounts.push(minAmount); + } + + let remainingToDistribute = totalAmount - (minAmount * transferCount); + + // 随机分配剩余金额,确保不超过最大限制 + while (remainingToDistribute > 0) { + // 找到所有还能增加金额的位置 + const availableIndices = []; + for (let i = 0; i < transferCount; i++) { + if (amounts[i] < maxAmount) { + availableIndices.push(i); + } + } + + // 如果没有可用位置,说明无法继续分配 + if (availableIndices.length === 0) { + break; + } + + // 随机选择一个可用位置 + const randomIndex = availableIndices[Math.floor(Math.random() * availableIndices.length)]; + + // 计算这个位置最多还能增加多少 + const maxIncrease = Math.min( + maxAmount - amounts[randomIndex], + remainingToDistribute + ); + + if (maxIncrease > 0) { + // 随机增加1到maxIncrease之间的金额 + const increase = Math.floor(Math.random() * maxIncrease) + 1; + amounts[randomIndex] += increase; + remainingToDistribute -= increase; + } + } + + // 如果还有剩余金额无法分配,说明约束条件无法满足 + if (remainingToDistribute > 0) { + return []; + } + + return amounts; + } + + // 生成3笔随机金额,总计指定金额(保留原方法用于兼容性) + generateThreeRandomAmounts(totalAmount) { + // 确保总金额足够分配三笔最小金额 + const minAmount = 500; + const maxAmount = Math.min(5000, totalAmount - 2 * minAmount); + + // 生成第一笔金额 (500-5000) + const amount1 = Math.floor(Math.random() * (maxAmount - minAmount + 1)) + minAmount; + + // 生成第二笔金额 (500-剩余金额-500) + const remaining1 = totalAmount - amount1; + const maxAmount2 = Math.min(5000, remaining1 - minAmount); + const amount2 = Math.floor(Math.random() * (maxAmount2 - minAmount + 1)) + minAmount; + + // 第三笔是剩余金额 + const amount3 = totalAmount - amount1 - amount2; + + return [amount1, amount2, amount3]; + } + + // 生成随机金额(保留原方法用于其他地方) + + + /** + * 确认分配并创建转账记录 + * @param {number} allocationId - 分配ID + * @param {number} userId - 用户ID + * @param {number} transferAmount - 实际转账金额(用于校验) + * @param {string} description - 转账描述 + * @param {string} voucher - 转账凭证URL + * @returns {number} 转账记录ID + */ + async confirmAllocation(allocationId, userId, transferAmount = null, description = null, voucher = null) { + const db = getDB(); + + try { + await db.query('START TRANSACTION'); + + // 获取分配信息 + const [allocations] = await db.execute( + 'SELECT * FROM transfers WHERE id = ? AND from_user_id = ?', + [allocationId, userId] + ); + + if (allocations.length === 0) { + throw new Error('分配不存在或无权限'); + } + + const allocation = allocations[0]; + + // 校验转账金额(如果提供了转账金额) + if (transferAmount !== null) { + const expectedAmount = parseFloat(allocation.amount); + const actualAmount = parseFloat(transferAmount); + + if (Math.abs(expectedAmount - actualAmount) > 0.01) { + throw new Error(`转账金额不匹配!应转账 ${expectedAmount} 元,实际转账 ${actualAmount} 元`); + } + } + + // 检查分配状态 + if (allocation.status !== 'pending') { + throw new Error('该分配已处理,无法重复确认'); + } + + // 检查匹配订单是否已超时 + const [matchingOrder] = await db.execute( + 'SELECT * FROM matching_orders WHERE id = ?', + [allocation.matching_order_id] + ); + + if (matchingOrder.length === 0) { + throw new Error('匹配订单不存在'); + } + + // 检查订单是否已被取消(超时会导致订单被取消) + if (matchingOrder[0].status === 'cancelled') { + throw new Error('该匹配订单已超时取消,无法进行转账'); + } + + // 检查是否存在相关的超时转账记录 + const [timeoutTransfers] = await db.execute( + `SELECT COUNT(*) as count FROM transfers + WHERE (from_user_id = ? OR to_user_id = ?) + AND is_overdue = 1 + AND description LIKE ?`, + [userId, userId, `%匹配订单 ${allocation.matching_order_id}%`] + ); + + if (timeoutTransfers[0].count > 0) { + throw new Error('该匹配订单存在超时记录,无法继续转账。请联系管理员处理'); + } + + // 计算3小时后的截止时间 + const deadline = dayjs().add(3, 'hour').toDate(); + + // 更新转账记录状态为confirmed,跳过待确认环节 + const transferDescription = description || `匹配订单 ${allocation.matching_order_id} 第 ${allocation.cycle_number} 轮转账`; + const [transferResult] = await db.execute( + `UPDATE transfers + SET status = "confirmed", description = ?, deadline_at = ?, confirmed_at = NOW(), voucher_url = ? + WHERE id = ?`, + [ + transferDescription, + deadline, + voucher, + allocationId + ] + ); + + // 注意:发送方余额将在接收方确认收款时扣除,而不是在确认转账时扣除 + // 这样可以避免资金被锁定但收款方未确认的情况 + + // 记录确认动作 + await db.execute( + 'INSERT INTO matching_records (matching_order_id, user_id, action, amount, note) VALUES (?, ?, "confirm", ?, ?)', + [ + allocation.matching_order_id, + userId, + allocation.amount, + transferAmount ? `实际转账金额: ${transferAmount}` : null + ] + ); + + await db.query('COMMIT'); + + // 检查是否需要进入下一轮 + await this.checkCycleCompletion(allocation.matching_order_id, allocation.cycle_number); + + return transferResult.insertId; + } catch (error) { + await db.query('ROLLBACK'); + throw error; + } + } + + // 检查轮次完成情况 + async checkCycleCompletion(matchingOrderId, cycleNumber) { + const db = getDB(); + + try { + // 检查当前轮次是否全部确认 + const [pending] = await db.execute( + 'SELECT COUNT(*) as count FROM transfers WHERE matching_order_id = ? AND cycle_number = ? AND status = "pending"', + [matchingOrderId, cycleNumber] + ); + + if (pending[0].count === 0) { + // 当前轮次完成,检查是否需要下一轮 + const [order] = await db.execute( + 'SELECT * FROM matching_orders WHERE id = ?', + [matchingOrderId] + ); + + const currentOrder = order[0]; + + if (currentOrder.cycle_count + 1 < currentOrder.max_cycles) { + // 开始下一轮 + await db.execute( + 'UPDATE matching_orders SET cycle_count = cycle_count + 1 WHERE id = ?', + [matchingOrderId] + ); + + // 生成下一轮分配 + const amounts = this.generateThreeRandomAmounts(currentOrder.amount); + await this.generateThreeAllocations(matchingOrderId, amounts, currentOrder.initiator_id); + } else { + // 完成所有轮次 + await db.execute( + 'UPDATE matching_orders SET status = "completed" WHERE id = ?', + [matchingOrderId] + ); + + console.log(`匹配订单 ${matchingOrderId} 已完成所有轮次`); + + // 检查用户是否完成第三次匹配,如果是则给代理分佣 + await this.checkAndProcessAgentCommission(currentOrder.initiator_id); + } + } + } catch (error) { + console.error('检查轮次完成情况失败:', error); + throw error; + } + } + + // 获取用户的匹配订单 + async getUserMatchingOrders(userId, page = 1, limit = 10) { + const db = getDB(); + const offset = (parseInt(page) - 1) * parseInt(limit); + + + + try { + // 获取用户发起的订单 + const [orders] = await db.execute( + `SELECT mo.*, u.username as initiator_name,u.real_name as initiator_real_name + FROM matching_orders mo + JOIN users u ON mo.initiator_id = u.id + WHERE mo.initiator_id = ? + ORDER BY mo.created_at DESC + LIMIT ${parseInt(limit)} OFFSET ${parseInt(offset)}`, + [userId] + ); + + // 同时获取系统反向匹配订单(如果用户参与了分配) + const [systemOrders] = await db.execute( + `SELECT DISTINCT mo.*, u.username as initiator_name + FROM matching_orders mo + JOIN users u ON mo.initiator_id = u.id + JOIN transfers oa ON mo.id = oa.id + WHERE mo.is_system_reverse = TRUE AND oa.to_user_id = ? + ORDER BY mo.created_at DESC + LIMIT ${parseInt(limit)} OFFSET ${parseInt(offset)}`, + [userId] + ); + + // 合并订单列表 + const allOrders = [...orders, ...systemOrders]; + + // 按创建时间排序 + allOrders.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + + // 为每个订单获取分配信息 + for (let order of allOrders) { + const [allocations] = await db.execute( + `SELECT * FROM transfers WHERE matching_order_id = ? ORDER BY cycle_number, created_at`, + [order.id] + ); + order.allocations = allocations; + } + + return allOrders; + } catch (error) { + console.error('获取用户匹配订单失败:', error); + throw error; + } + } + + + + // 获取用户待处理的分配 + async getUserPendingAllocations(userId) { + const db = getDB(); + + try { + const [allocations] = await db.execute( + `(SELECT oa.*, mo.amount as total_amount, mo.status as order_status, u.username as to_user_name, + u.real_name as to_user_real_name, + DATE_ADD(oa.created_at, INTERVAL 150 MINUTE) as expected_deadline, + oa.outbound_date, + oa.return_date, + oa.can_return_after, + oa.confirmed_at + FROM transfers oa + JOIN matching_orders mo ON oa.matching_order_id = mo.id + JOIN users u ON oa.to_user_id = u.id + WHERE oa.from_user_id = ? AND oa.status = "pending" AND mo.status != "cancelled" + AND (oa.source_type IS NULL)) + UNION ALL + (SELECT oa.*, oa.amount as total_amount, 'active' as order_status, u.username as to_user_name, + u.real_name as to_user_real_name, + DATE_ADD(oa.created_at, INTERVAL 150 MINUTE) as expected_deadline, + oa.outbound_date, + oa.return_date, + oa.can_return_after, + oa.confirmed_at + FROM transfers oa + JOIN users u ON oa.to_user_id = u.id + WHERE oa.from_user_id = ? AND oa.status = "pending") + ORDER BY created_at ASC`, + [userId, userId] + ); + + // 检查每个分配的超时状态,但不过滤掉 + const allocationsWithTimeoutStatus = []; + for (const allocation of allocations) { + // 检查是否存在相关的超时转账记录 + const [timeoutTransfers] = await db.execute( + `SELECT COUNT(*) as count FROM transfers + WHERE (from_user_id = ? OR to_user_id = ?) + AND is_overdue = 1 + AND description LIKE ?`, + [userId, userId, `%匹配订单 ${allocation.matching_order_id}%`] + ); + + // 添加超时状态标记 + allocation.has_timeout_record = timeoutTransfers[0].count > 0; + allocationsWithTimeoutStatus.push(allocation); + } + // 检查并处理超时订单 + const now = new Date(); + // 隐藏系统账户身份并添加时效状态 + const processedAllocations = allocationsWithTimeoutStatus.map(allocation => { + const deadline = allocation.transfer_deadline || allocation.expected_deadline; + const deadlineDate = new Date(deadline); + const timeLeft = deadlineDate - now; + + // 计算剩余时间 + let timeStatus = 'normal'; + let timeLeftText = ''; + + if (timeLeft <= 0) { + timeStatus = 'expired'; + timeLeftText = '已超时'; + } else if (timeLeft <= 2.5 * 60 * 60 * 1000) { // 2.5小时内 + timeStatus = 'urgent'; + const hours = Math.floor(timeLeft / (60 * 60 * 1000)); + const minutes = Math.floor((timeLeft % (60 * 60 * 1000)) / (60 * 1000)); + timeLeftText = hours > 0 ? `${hours}小时${minutes}分钟` : `${minutes}分钟`; + } else { + const hours = Math.floor(timeLeft / (60 * 60 * 1000)); + const minutes = Math.floor((timeLeft % (60 * 60 * 1000)) / (60 * 1000)); + timeLeftText = `${hours}小时${minutes}分钟`; + } + + return { + ...allocation, + to_user_name: allocation.to_user_name || '匿名用户', + is_system_account: undefined, // 移除系统账户标识 + deadline: deadline, + time_status: timeStatus, + time_left: timeLeftText, + can_transfer: !allocation.has_timeout_record, // 是否可以转账 + timeout_reason: allocation.has_timeout_record ? '该匹配订单存在超时记录,无法继续转账' : null + }; + }); + + return processedAllocations; + } catch (error) { + console.error('获取用户待处理分配失败:', error); + throw error; + } + } + + // 检查并处理代理佣金 + async checkAndProcessAgentCommission(userId) { + const db = getDB(); + + try { + // 检查用户是否有代理关系 + const [agentRelation] = await db.execute( + 'SELECT agent_id, created_at FROM agent_merchants WHERE merchant_id = ?', + [userId] + ); + + if (agentRelation.length === 0) { + return; // 用户没有代理,无需处理 + } + + const agentId = agentRelation[0].agent_id; + const agentJoinTime = agentRelation[0].created_at; + + // 检查用户给他人转账的次数(状态为已收款,且转账时间在代理商入驻之后) + const [completedTransfers] = await db.execute( + 'SELECT COUNT(*) as count FROM transfers WHERE from_user_id = ? AND status = "received" AND created_at >= ?', + [userId, agentJoinTime] + ); + + const transferCount = completedTransfers[0].count; + + // 如果完成至少三次转账,给代理分佣 + if (transferCount >= 3) { + // 检查是否已经给过佣金(防止重复分佣) + const [existingCommission] = await db.execute( + 'SELECT id FROM agent_commission_records WHERE agent_id = ? AND merchant_id = ? AND description LIKE "%第三次转账%"', + [agentId, userId] + ); + + if (existingCommission.length === 0) { + // 计算佣金:399元的10% = 39.9元 + const commissionAmount = 399 * 0.10; + + // 记录佣金 + await db.execute( + 'INSERT INTO agent_commission_records (agent_id, merchant_id, commission_amount, commission_type, description, created_at) VALUES (?, ?, ?, "matching", "用户完成第三次转账获得的代理佣金", NOW())', + [agentId, userId, commissionAmount] + ); + + console.log(`用户 ${userId} 完成第三次转账,为代理 ${agentId} 分佣 ${commissionAmount} 元`); + } + } + } catch (error) { + console.error('处理代理佣金失败:', error); + // 不抛出错误,避免影响主流程 + } + } +} + +module.exports = new MatchingService(); \ No newline at end of file diff --git a/services/timeoutService.js b/services/timeoutService.js new file mode 100644 index 0000000..794d018 --- /dev/null +++ b/services/timeoutService.js @@ -0,0 +1,380 @@ +const { getDB } = require('../database'); +const { logger, auditLogger } = require('../config/logger'); + +class TimeoutService { + /** + * 检查转账超时情况 + * 标记超时转账和风险用户,自动取消超过2.5小时的pending转账 + */ + async checkTransferTimeouts() { + const db = getDB(); + + try { + // 只在调试模式下输出开始检查的日志 + // console.log('开始检查转账超时情况...'); + + // 1. 查找所有超时的转账记录(有deadline_at的) + const [overdueTransfers] = await db.execute( + `SELECT t.*, u.username, u.real_name + FROM transfers t + JOIN users u ON t.from_user_id = u.id + WHERE t.status = 'pending' + AND t.deadline_at IS NOT NULL + AND t.deadline_at < NOW() + AND t.is_overdue = 0` + ); + + // 2. 查找所有pending状态超过2.5小时的转账记录 + const [longPendingTransfers] = await db.execute( + `SELECT t.*, u.username, u.real_name + FROM transfers t + JOIN users u ON t.from_user_id = u.id + WHERE t.status = 'pending' + AND t.created_at < DATE_SUB(NOW(), INTERVAL 150 MINUTE)` + ); + + let hasWork = false; + + // 处理有deadline的超时转账 + if (overdueTransfers.length > 0) { + hasWork = true; + console.log(`⚠️ 发现 ${overdueTransfers.length} 笔超时转账,开始处理...`); + + for (const transfer of overdueTransfers) { + await this.handleOverdueTransfer(transfer); + } + } + // 处理超过2.5小时的pending转账 + if (longPendingTransfers.length > 0) { + hasWork = true; + console.log(`⚠️ 发现 ${longPendingTransfers.length} 笔超过2.5小时的pending转账,开始自动取消...`); + + for (const transfer of longPendingTransfers) { + await this.handleLongPendingTransfer(transfer); + } + } + + if (hasWork) { + console.log('✅ 转账超时检查完成'); + } + + } catch (error) { + console.error('检查转账超时失败:', error); + logger.error('Transfer timeout check failed', { error: error.message }); + } + } + + /** + * 处理超时转账 + * @param {Object} transfer - 转账记录 + */ + async handleOverdueTransfer(transfer) { + const db = getDB(); + + try { + await db.query('START TRANSACTION'); + + // 标记转账为超时和坏账 + await db.execute( + 'UPDATE transfers SET is_overdue = 1, is_bad_debt = 1, overdue_at = NOW() WHERE id = ?', + [transfer.id] + ); + + // 标记用户为风险用户 + await db.execute( + `UPDATE users SET + is_risk_user = 1, + risk_reason = CONCAT(IFNULL(risk_reason, ''), '转账超时(转账ID: ', ?, ', 金额: ', ?, '元, 超时时间: ', NOW(), '); ') + WHERE id = ?`, + [transfer.id, transfer.amount, transfer.from_user_id] + ); + + await db.query('COMMIT'); + + // 记录审计日志 + auditLogger.info('Transfer marked as overdue and bad debt, user marked as risk', { + transferId: transfer.id, + userId: transfer.from_user_id, + username: transfer.username, + amount: transfer.amount, + deadlineAt: transfer.deadline_at + }); + + console.log(`转账 ${transfer.id} 已标记为超时和坏账,用户 ${transfer.username}(ID: ${transfer.from_user_id}) 已标记为风险用户`); + + } catch (error) { + await db.query('ROLLBACK'); + console.error(`处理超时转账 ${transfer.id} 失败:`, error); + throw error; + } + } + + /** + * 处理超过2.5小时的pending转账 + * @param {Object} transfer - 转账记录 + */ + async handleLongPendingTransfer(transfer) { + const db = getDB(); + + try { + await db.query('START TRANSACTION'); + + // 将转账状态改为cancelled + await db.execute( + 'UPDATE transfers SET status = "cancelled", updated_at = NOW() WHERE id = ?', + [transfer.id] + ); + + // 如果有关联的matching_order_id,检查并更新matching_orders状态 + if (transfer.matching_order_id) { + // 检查该matching_order下是否还有非cancelled状态的transfers + const [remainingTransfers] = await db.execute( + 'SELECT COUNT(*) as count FROM transfers WHERE matching_order_id = ? AND status != "cancelled"', + [transfer.matching_order_id] + ); + + // 如果所有关联的transfers都是cancelled状态,则更新matching_order状态为cancelled + if (remainingTransfers[0].count === 0) { + await db.execute( + 'UPDATE matching_orders SET status = "cancelled", updated_at = NOW() WHERE id = ?', + [transfer.matching_order_id] + ); + + console.log(`匹配订单 ${transfer.matching_order_id} 的所有转账都已取消,订单状态已更新为cancelled`); + } + } + + await db.query('COMMIT'); + + // 记录审计日志 + auditLogger.info('Long pending transfer auto-cancelled', { + transferId: transfer.id, + userId: transfer.from_user_id, + username: transfer.username, + amount: transfer.amount, + createdAt: transfer.created_at, + matchingOrderId: transfer.matching_order_id + }); + + console.log(`转账 ${transfer.id} 超过2.5小时未处理,已自动取消 (用户: ${transfer.username}, 金额: ${transfer.amount}元)`); + + } catch (error) { + await db.query('ROLLBACK'); + console.error(`处理长时间pending转账 ${transfer.id} 失败:`, error); + throw error; + } + } + + /** + * 获取风险用户列表 + * @param {Object} filters - 筛选条件 + * @param {Object} pagination - 分页参数 + * @returns {Object} 风险用户列表和分页信息 + */ + async getRiskUsers(filters = {}, pagination = {}) { + const db = getDB(); + const { page = 1, limit = 10 } = pagination; + const pageNum = parseInt(page, 10) || 1; + const limitNum = parseInt(limit, 10) || 10; + const offset = (pageNum - 1) * limitNum; + + let whereClause = 'WHERE is_risk_user = 1'; + const params = []; + + // 构建查询条件 + if (filters.is_blacklisted !== undefined) { + whereClause += ' AND is_blacklisted = ?'; + params.push(filters.is_blacklisted); + } + + if (filters.username) { + whereClause += ' AND username LIKE ?'; + params.push(`%${filters.username}%`); + } + + try { + // 获取总数 + const [countResult] = await db.execute( + `SELECT COUNT(*) as total FROM users ${whereClause}`, + params + ); + const total = countResult[0].total; + + // 获取数据 + const [users] = await db.execute( + `SELECT id, username, real_name, is_risk_user, is_blacklisted, + risk_reason, blacklist_reason, blacklisted_at, created_at,phone + FROM users + ${whereClause} + ORDER BY created_at DESC + LIMIT ${limitNum} OFFSET ${offset}`, + params + ); + + return { + users, + pagination: { + page: pageNum, + limit: limitNum, + total, + pages: Math.ceil(total / limitNum) + } + }; + } catch (error) { + logger.error('Failed to get risk users', { error: error.message, filters }); + throw error; + } + } + + /** + * 拉黑用户 + * @param {number} userId - 用户ID + * @param {string} reason - 拉黑原因 + * @param {number} operatorId - 操作员ID + */ + async blacklistUser(userId, reason, operatorId) { + const db = getDB(); + + try { + // 检查用户是否存在 + const [users] = await db.execute( + 'SELECT id, username, phone FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + throw new Error('用户不存在'); + } + + const user = users[0]; + + if (user.is_blacklisted) { + throw new Error('用户已被拉黑'); + } + + // 拉黑用户 + await db.execute( + `UPDATE users SET + is_blacklisted = 1, + blacklist_reason = ?, + blacklisted_at = NOW() + WHERE id = ?`, + [reason, userId] + ); + + // 记录审计日志 + auditLogger.info('User blacklisted', { + userId, + username: user.username, + reason, + operatorId + }); + + logger.info('User blacklisted successfully', { userId, operatorId }); + + } catch (error) { + logger.error('Failed to blacklist user', { + error: error.message, + userId, + operatorId + }); + throw error; + } + } + + /** + * 解除拉黑 + * @param {number} userId - 用户ID + * @param {number} operatorId - 操作员ID + */ + async unblacklistUser(userId, operatorId) { + const db = getDB(); + + try { + // 检查用户是否存在 + const [users] = await db.execute( + 'SELECT id, username, is_blacklisted FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + throw new Error('用户不存在'); + } + + const user = users[0]; + + if (!user.is_blacklisted) { + throw new Error('用户未被拉黑'); + } + + // 解除拉黑 + await db.execute( + `UPDATE users SET + is_blacklisted = 0, + blacklist_reason = NULL, + blacklisted_at = NULL + WHERE id = ?`, + [userId] + ); + + // 记录审计日志 + auditLogger.info('User unblacklisted', { + userId, + username: user.username, + operatorId + }); + + logger.info('User unblacklisted successfully', { userId, operatorId }); + + } catch (error) { + logger.error('Failed to unblacklist user', { + error: error.message, + userId, + operatorId + }); + throw error; + } + } + + /** + * 检查用户是否被拉黑 + * @param {number} userId - 用户ID + * @returns {boolean} 是否被拉黑 + */ + async isUserBlacklisted(userId) { + const db = getDB(); + + try { + const [users] = await db.execute( + 'SELECT is_blacklisted FROM users WHERE id = ?', + [userId] + ); + + return users.length > 0 && users[0].is_blacklisted === 1; + } catch (error) { + logger.error('Failed to check user blacklist status', { + error: error.message, + userId + }); + throw error; + } + } + + /** + * 启动定时检查任务 + * 每5分钟检查一次转账超时情况 + */ + startTimeoutChecker() { + console.log('启动转账超时检查定时任务...'); + + // 立即执行一次 + this.checkTransferTimeouts(); + + // 每5分钟执行一次 + setInterval(() => { + this.checkTransferTimeouts(); + }, 5 * 1000); // 5秒 + } +} + +module.exports = new TimeoutService(); \ No newline at end of file diff --git a/services/transferService.js b/services/transferService.js new file mode 100644 index 0000000..d4f7670 --- /dev/null +++ b/services/transferService.js @@ -0,0 +1,1114 @@ +const {getDB} = require('../database'); +const {logger, auditLogger} = require('../config/logger'); +const {AppError} = require('../middleware/errorHandler'); +const {TRANSFER_TYPES, TRANSFER_STATUS, ERROR_CODES, HTTP_STATUS} = require('../config/constants'); + +class TransferService { + // 创建转账记录 + async createTransfer(fromUserId, transferData) { + const {to_user_id, amount, transfer_type, description, voucher_url} = transferData; + const db = getDB(); + + try { + // 验证用户是否存在 + await this.validateUser(to_user_id); + + // 验证转账类型 + if (!Object.values(TRANSFER_TYPES).includes(transfer_type)) { + throw new AppError('无效的转账类型', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 检查余额(如果是用户转账)- 允许负余额转账 + if (transfer_type === TRANSFER_TYPES.USER_TO_USER || transfer_type === TRANSFER_TYPES.USER_TO_SYSTEM) { + if (!fromUserId) { + throw new AppError('用户转账必须指定发送方用户', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + // 获取当前余额但不检查是否足够,允许负余额转账 + await this.checkUserBalance(fromUserId, amount); + } + + // 系统转账时,from_user_id 设为 null + const actualFromUserId = transfer_type === TRANSFER_TYPES.SYSTEM_TO_USER ? null : fromUserId; + + // 生成批次ID + const batch_id = this.generateBatchId(); + + // 插入转账记录 + const currentTime = new Date(); + const [result] = await db.execute( + `INSERT INTO transfers (from_user_id, to_user_id, amount, transfer_type, status, description, + voucher_url, batch_id, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [actualFromUserId, to_user_id, amount, transfer_type, TRANSFER_STATUS.PENDING, description || null, voucher_url || null, batch_id, currentTime] + ); + + const transferId = result.insertId; + + // 记录审计日志 + auditLogger.info('Transfer created', { + transferId, + fromUserId, + toUserId: to_user_id, + amount, + transferType: transfer_type, + batchId: batch_id + }); + + logger.info('Transfer created successfully', {transferId, fromUserId, amount}); + + return { + transfer_id: transferId, + batch_id, + status: TRANSFER_STATUS.PENDING + }; + } catch (error) { + logger.error('Failed to create transfer', { + error: error.message, + fromUserId, + transferData + }); + throw error; + } + } + + // 管理员解除坏账 + async removeBadDebt(transferId, adminId, reason) { + const db = getDB(); + + try { + // 获取转账记录 + const transfer = await this.getTransferById(transferId); + + if (!transfer) { + throw new AppError('转账记录不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND); + } + + if (!transfer.is_bad_debt) { + throw new AppError('该转账未被标记为坏账', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 解除坏账标记 + await db.execute( + 'UPDATE transfers SET is_bad_debt = 0 WHERE id = ?', + [transferId] + ); + + // 记录管理员操作日志 + await db.execute( + `INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, description, + created_at) + VALUES (?, 'remove_bad_debt', 'transfer', ?, ?, NOW())`, + [adminId, transferId, reason || `管理员解除转账${transferId}的坏账标记`] + ); + + // 记录审计日志 + auditLogger.info('Bad debt removed by admin', { + transferId, + adminId, + fromUserId: transfer.from_user_id, + toUserId: transfer.to_user_id, + amount: transfer.amount, + reason + }); + + logger.info('Bad debt removed successfully', {transferId, adminId, reason}); + + return {success: true}; + } catch (error) { + logger.error('Failed to remove bad debt', { + error: error.message, + transferId, + adminId + }); + throw error; + } + } + + // 确认转账 + async confirmTransfer(transferId, note, operatorId) { + const mysql = require('mysql2/promise'); + const {dbConfig} = require('../database'); + + // 创建单独的连接用于事务处理 + const connection = await mysql.createConnection(dbConfig); + + try { + // 获取转账记录 + const transfer = await this.getTransferById(transferId); + + if (!transfer) { + throw new AppError('转账记录不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND); + } + + if (transfer.status !== TRANSFER_STATUS.PENDING) { + throw new AppError('转账记录状态不允许确认', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 检查是否为坏账 + if (transfer.is_bad_debt) { + throw new AppError('该转账已被标记为坏账,无法确认。请联系管理员处理', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 开始事务 + await connection.beginTransaction(); + + try { + // 更新转账状态 + await connection.execute( + 'UPDATE transfers SET status = ? WHERE id = ?', + [TRANSFER_STATUS.CONFIRMED, transferId] + ); + + // 如果存在匹配订单ID,更新匹配订单状态 + if (transfer.matching_order_id) { + // 查询该匹配订单下所有transfers的状态 + const [allTransfers] = await connection.execute( + `SELECT status FROM transfers + WHERE matching_order_id = ?`, + [transfer.matching_order_id] + ); + + let matchingOrderStatus; + + // 根据所有相关transfers的状态来决定matching_order的状态 + const transferStatuses = allTransfers.map(t => t.status); + + if (transferStatuses.every(status => status === 'cancelled' || status === 'rejected' || status === 'not_received')) { + // 如果所有transfers都被取消/拒绝/未收到,匹配订单标记为已完成 + matchingOrderStatus = 'completed'; + } else if (transferStatuses.every(status => status === 'received')) { + // 如果所有transfers都已收到,匹配订单完成 + matchingOrderStatus = 'completed'; + } else if (transferStatuses.includes('cancelled') || transferStatuses.includes('rejected') || transferStatuses.includes('not_received') || transferStatuses.some(status => status === 'confirmed' || status === 'received')) { + // 如果有任何一个transfer被取消/拒绝/未收到,或者有transfers已确认或已收到,匹配订单为进行中状态 + matchingOrderStatus = 'matching'; + } else { + // 其他情况为待处理状态 + matchingOrderStatus = 'matching'; + } + + await connection.execute( + `UPDATE matching_orders + SET status = ?, + updated_at = NOW() + WHERE id = ?`, + [matchingOrderStatus, transfer.matching_order_id] + ); + + logger.info('Matching order status updated after transfer confirmation', { + matchingOrderId: transfer.matching_order_id, + transferId: transferId, + newMatchingOrderStatus: matchingOrderStatus, + allTransferStatuses: transferStatuses + }); + } + + // 注意:发送方余额将在接收方确认收款时扣除,而不是在确认转账时扣除 + // 这样可以避免资金被锁定但收款方未确认的情况 + + await connection.commit(); + + // 记录审计日志 + auditLogger.info('Transfer confirmed', { + transferId, + operatorId, + fromUserId: transfer.from_user_id, + toUserId: transfer.to_user_id, + amount: transfer.amount + }); + + logger.info('Transfer confirmed successfully', {transferId, operatorId}); + + return {success: true}; + } catch (error) { + await connection.rollback(); + throw error; + } + } catch (error) { + logger.error('Failed to confirm transfer', { + error: error.message, + transferId, + operatorId + }); + throw error; + } finally { + await connection.end(); + } + } + + // 获取转账列表 + async getTransfers(filters = {}, pagination = {}) { + 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 = []; + + // 构建查询条件 + 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 { + // 获取总数 + 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 validateUser(userId) { + const db = getDB(); + const [users] = await db.execute('SELECT id FROM users WHERE id = ?', [userId]); + if (users.length === 0) { + throw new AppError('用户不存在', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + } + + // 检查用户余额(现在检查balance字段,允许负数) + async checkUserBalance(userId, amount) { + const db = getDB(); + const [users] = await db.execute('SELECT balance FROM users WHERE id = ?', [userId]); + if (users.length === 0) { + throw new AppError('用户不存在', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + // 余额可以为负数,所以不需要检查余额不足 + return users[0].balance; + } + + // 获取转账记录 + async getTransferById(transferId) { + const db = getDB(); + const [transfers] = await db.execute('SELECT * FROM transfers WHERE id = ?', [transferId]); + 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) { + const mysql = require('mysql2/promise'); + const {dbConfig} = require('../database'); + + // 创建单独的连接用于事务处理 + const connection = await mysql.createConnection(dbConfig); + + try { + // 获取转账记录 + const transfer = await this.getTransferById(transferId); + + if (!transfer) { + throw new AppError('转账记录不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND); + } + + // 检查用户权限:必须是收款方本人或管理员 + const [userRows] = await connection.execute( + 'SELECT role FROM users WHERE id = ?', + [userId] + ); + + const isAdmin = userRows[0]?.role === 'admin'; + const isRecipient = transfer.to_user_id === userId; + + if (!isRecipient && !isAdmin) { + throw new AppError('无权限操作此转账', HTTP_STATUS.FORBIDDEN, ERROR_CODES.VALIDATION_ERROR); + } + + if (transfer.status !== TRANSFER_STATUS.CONFIRMED) { + throw new AppError('转账状态不允许确认收款', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 检查是否为坏账 + if (transfer.is_bad_debt) { + throw new AppError('该转账已被标记为坏账,无法确认收款。请联系管理员处理', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 开始事务 + await connection.beginTransaction(); + + try { + // 更新转账状态为已收到 + await connection.execute( + 'UPDATE transfers SET status = ? WHERE id = ?', + [TRANSFER_STATUS.RECEIVED, transferId] + ); + + // 扣除发送方余额(在接收方确认收款时扣除) + if (transfer.from_user_id) { + await connection.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [transfer.amount, transfer.from_user_id] + ); + } + + // 所有类型的转账都需要在接收方确认收到时增加接收方余额 + // 这与 confirmTransfer 方法的修改保持一致 + await connection.execute( + 'UPDATE users SET balance = balance + ? WHERE id = ?', + [transfer.amount, transfer.to_user_id] + ); + + // 给发起人发放相应的积分(转账金额 = 积分数量) + await connection.execute( + 'UPDATE users SET points = points + ? WHERE id = ?', + [transfer.amount, transfer.from_user_id] + ); + + // 记录积分历史 + await connection.execute( + `INSERT INTO points_history (user_id, amount, type, description, created_at) + VALUES (?, ?, 'earn', ?, NOW())`, + [transfer.from_user_id, transfer.amount, `转账确认收款奖励积分,转账ID: ${transferId}`] + ); + + // 记录详细的余额变更审计日志 + auditLogger.info('Balance adjustment - confirm received', { + transferId: transferId, + fromUserId: transfer.from_user_id, + toUserId: transfer.to_user_id, + amount: transfer.amount, + operation: 'add_receiver_balance', + operatorId: userId, + operatorType: isAdmin ? 'admin' : 'user', + timestamp: new Date().toISOString() + }); + + await connection.commit(); + + // 记录审计日志 + auditLogger.info('Transfer received confirmed', { + transferId, + userId, + amount: transfer.amount, + pointsAwarded: transfer.amount, + pointsAwardedTo: transfer.from_user_id + }); + + logger.info('Transfer received confirmed successfully', { + transferId, + userId, + pointsAwarded: transfer.amount, + pointsAwardedTo: transfer.from_user_id + }); + + // 检查并处理代理佣金(转账完成后) + try { + const matchingService = require('./matchingService'); + await matchingService.checkAndProcessAgentCommission(transfer.from_user_id); + logger.info('Agent commission check completed', { + transferId, + fromUserId: transfer.from_user_id + }); + } catch (commissionError) { + // 代理佣金处理失败不影响主流程 + logger.error('Agent commission processing failed', { + transferId, + fromUserId: transfer.from_user_id, + error: commissionError.message + }); + } + + return {success: true}; + } catch (error) { + await connection.rollback(); + throw error; + } + } catch (error) { + logger.error('Failed to confirm received transfer', { + error: error.message, + transferId, + userId + }); + throw error; + } finally { + await connection.end(); + } + } + + /** + * 用户确认未收到转账 + * 当用户确认未收到款项时,将转账状态改为not_received并回滚发送方余额 + * @param {number} transferId - 转账ID + * @param {number} userId - 操作用户ID + * @returns {Object} 操作结果 + */ + async confirmNotReceived(transferId, userId) { + const db = getDB(); + const connection = await db.getConnection(); + + try { + // 获取转账记录 + const transfer = await this.getTransferById(transferId); + + if (!transfer) { + throw new AppError('转账记录不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND); + } + + // 检查用户权限:必须是收款方本人或管理员 + const [userRows] = await db.execute( + 'SELECT role FROM users WHERE id = ?', + [userId] + ); + + const isAdmin = userRows[0]?.role === 'admin'; + const isRecipient = transfer.to_user_id === userId; + + if (!isRecipient && !isAdmin) { + throw new AppError('无权限操作此转账', HTTP_STATUS.FORBIDDEN, ERROR_CODES.VALIDATION_ERROR); + } + + if (transfer.status !== TRANSFER_STATUS.CONFIRMED) { + throw new AppError('转账状态不允许确认未收款', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 开始事务 + await connection.beginTransaction(); + + try { + // 更新转账状态为未收到 + await connection.execute( + 'UPDATE transfers SET status = ? WHERE id = ?', + [TRANSFER_STATUS.NOT_RECEIVED, transferId] + ); + + // 注意:在新逻辑下,CONFIRMED状态时发送方余额还没有被扣除,所以无需回滚 + logger.info('Transfer marked as not received - no balance adjustment needed', { + transferId, + userId: transfer.from_user_id, + amount: transfer.amount, + operatorId: userId, + note: 'Sender balance was not deducted in confirmed status under new logic' + }); + + await connection.commit(); + + // 记录审计日志 + auditLogger.info('Transfer not received confirmed', { + transferId, + userId, + amount: transfer.amount, + fromUserId: transfer.from_user_id, + balanceRestored: false, + note: 'No balance restoration needed under new logic' + }); + + logger.info('Transfer not received confirmed successfully', { + transferId, + userId, + balanceRestored: false, + note: 'No balance restoration needed under new logic' + }); + + return {success: true}; + } catch (error) { + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + } catch (error) { + logger.error('Failed to confirm not received transfer', { + error: error.message, + transferId, + userId + }); + throw error; + } + } + + // 拒绝转账 + async rejectTransfer(transferId, note, operatorId) { + const db = getDB(); + let connection; + + try { + // 从连接池获取连接用于事务处理 + connection = await db.getConnection(); + + try { + // 获取转账记录 + const transfer = await this.getTransferById(transferId); + + if (!transfer) { + throw new AppError('转账记录不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND); + } + + if (transfer.status !== TRANSFER_STATUS.PENDING) { + throw new AppError('转账记录状态不允许拒绝', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 检查是否已超时 + if (transfer.is_overdue) { + throw new AppError('已超时的转账不能拒绝,异常状态只能后台解除', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 检查是否有截止时间且已过期 + if (transfer.deadline_at) { + const deadline = new Date(transfer.deadline_at); + const now = new Date(); + if (now > deadline) { + throw new AppError('已超时的转账不能拒绝,异常状态只能后台解除', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + } + + // 开始事务 + await connection.beginTransaction(); + + try { + // 更新转账状态 + await connection.execute( + 'UPDATE transfers SET status = ? WHERE id = ?', + [TRANSFER_STATUS.REJECTED, transferId] + ); + + // 注意:在新逻辑下,CONFIRMED状态时发送方余额还没有被扣除,所以无需回滚 + // 只有在RECEIVED状态时才需要回滚余额,但RECEIVED状态的转账不应该被拒绝 + logger.info('Transfer rejected - no balance adjustment needed', { + transferId, + userId: transfer.from_user_id, + amount: transfer.amount, + message: 'Sender balance was not deducted in confirmed status under new logic' + }); + + // 如果是分配类型的转账,需要更新对应的matching_order状态 + if ( transfer.matching_order_id) { + // 查询该matching_order下所有source_type为allocation的transfers状态 + const [allTransfers] = await connection.execute( + `SELECT status FROM transfers + WHERE matching_order_id = ? AND source_type = 'allocation'`, + [transfer.matching_order_id] + ); + + // 统计各种状态的数量 + const statusCounts = { + cancelled: 0, + rejected: 0, + not_received: 0, + confirmed: 0, + received: 0, + pending: 0 + }; + + allTransfers.forEach(t => { + if (statusCounts.hasOwnProperty(t.status)) { + statusCounts[t.status]++; + } + }); + + const totalTransfers = allTransfers.length; + const problemTransfers = statusCounts.cancelled + statusCounts.rejected + statusCounts.not_received; + const completedTransfers = statusCounts.received; + const activeTransfers = statusCounts.confirmed + statusCounts.received; + + let matchingOrderStatus; + if (problemTransfers === totalTransfers) { + // 所有transfers都是问题状态,matching_order为已完成 + matchingOrderStatus = 'completed'; + } else if (completedTransfers === totalTransfers) { + // 所有transfers都已收到,matching_order为已完成 + matchingOrderStatus = 'completed'; + } else if (problemTransfers > 0 || activeTransfers > 0) { + // 有问题transfers或有活跃transfers,matching_order为进行中 + matchingOrderStatus = 'matching'; + } else { + // 其他情况为等待中 + matchingOrderStatus = 'pending'; + } + + // 更新matching_order状态 + await connection.execute( + `UPDATE matching_orders + SET status = ?, updated_at = NOW() + WHERE id = ?`, + [matchingOrderStatus, transfer.matching_order_id] + ); + + logger.info('Updated matching_order status after transfer rejection', { + matchingOrderId: transfer.matching_order_id, + newStatus: matchingOrderStatus, + transferId, + statusCounts + }); + } + + await connection.commit(); + + // 记录审计日志 + auditLogger.info('Transfer rejected', { + transferId, + operatorId, + fromUserId: transfer.from_user_id, + toUserId: transfer.to_user_id, + amount: transfer.amount, + note, + balanceRestored: false, // 在新逻辑下无需回滚余额 + balanceRestoredNote: 'No balance restoration needed under new logic' + }); + + logger.info('Transfer rejected successfully', {transferId, operatorId}); + + return {success: true}; + } catch (error) { + await connection.rollback(); + throw error; + } + } catch (error) { + logger.error('Failed to reject transfer', { + error: error.message, + transferId, + operatorId + }); + throw error; + } finally { + if (connection) { + connection.release(); // 释放连接回连接池 + } + } + } catch (error) { + logger.error('Failed to get database connection for reject transfer', { + error: error.message, + transferId, + operatorId + }); + throw error; + } + } + + /** + * 强制变更转账状态(管理员权限) + * 用于处理货款纠纷等异常情况 + * @param {number} transferId - 转账ID + * @param {string} newStatus - 新状态 + * @param {string} reason - 变更原因 + * @param {number} adminId - 管理员ID + * @param {boolean} adjust_balance - 是否调整余额 + */ + async forceChangeTransferStatus(transferId, newStatus, reason, adminId, adjust_balance = false) { + const db = getDB(); + let connection; + + try { + // 从连接池获取连接用于事务处理 + connection = await db.getConnection(); + + try { + // 获取转账记录 + const transfer = await this.getTransferById(transferId); + + if (!transfer) { + throw new AppError('转账记录不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND); + } + + const oldStatus = transfer.status; + + // 验证新状态 + const validStatuses = [ + TRANSFER_STATUS.PENDING, + TRANSFER_STATUS.CONFIRMED, + TRANSFER_STATUS.RECEIVED, + TRANSFER_STATUS.REJECTED, + TRANSFER_STATUS.CANCELLED, + TRANSFER_STATUS.NOT_RECEIVED + ]; + if (!validStatuses.includes(newStatus)) { + throw new AppError('无效的转账状态', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 开始事务 + await connection.beginTransaction(); + + try { + // 更新转账状态 + await connection.execute( + `UPDATE transfers + SET status = ?, + admin_note = ?, + admin_modified_at = NOW(), + admin_modified_by = ? + WHERE id = ?`, + [newStatus, reason, adminId, transferId] + ); + + // 同步更新matching_orders表的状态 + if (transfer.matching_order_id) { + // 查询该匹配订单下所有transfers的状态 + const [allTransfers] = await connection.execute( + `SELECT status FROM transfers + WHERE matching_order_id = ?`, + [transfer.matching_order_id] + ); + + let matchingOrderStatus; + + // 根据所有相关transfers的状态来决定matching_order的状态 + const transferStatuses = allTransfers.map(t => t.status); + + if (transferStatuses.every(status => status === 'cancelled' || status === 'rejected' || status === 'not_received')) { + // 如果所有transfers都被取消/拒绝/未收到,匹配订单标记为已完成 + matchingOrderStatus = 'completed'; + } else if (transferStatuses.every(status => status === 'received')) { + // 如果所有transfers都已收到,匹配订单完成 + matchingOrderStatus = 'completed'; + } else if (transferStatuses.includes('cancelled') || transferStatuses.includes('rejected') || transferStatuses.includes('not_received') || transferStatuses.some(status => status === 'confirmed' || status === 'received')) { + // 如果有任何一个transfer被取消/拒绝/未收到,或者有transfers已确认或已收到,匹配订单为进行中状态 + matchingOrderStatus = 'matching'; + } else { + // 其他情况为待处理状态 + matchingOrderStatus = 'pending'; + } + console.log('matchingOrderStatus', matchingOrderStatus); + + await connection.execute( + `UPDATE matching_orders + SET status = ?, + updated_at = NOW() + WHERE id = ?`, + [matchingOrderStatus, transfer.matching_order_id] + ); + + logger.info('Matching order status updated based on all transfers', { + matchingOrderId: transfer.matching_order_id, + transferId: transferId, + oldTransferStatus: oldStatus, + newTransferStatus: newStatus, + allTransferStatuses: transferStatuses, + newMatchingOrderStatus: matchingOrderStatus, + adminId + }); + } + + // 根据状态变更调整余额 + if (adjust_balance && transfer.from_user_id) { + if (oldStatus === TRANSFER_STATUS.CONFIRMED && (newStatus === TRANSFER_STATUS.REJECTED || newStatus === TRANSFER_STATUS.CANCELLED || newStatus === TRANSFER_STATUS.NOT_RECEIVED)) { + // 从已确认变为拒绝/取消/未收到:由于新逻辑下CONFIRMED状态时发送方余额未扣除,所以无需回滚 + logger.info('Status change from confirmed to rejected/cancelled/not_received - no balance adjustment needed', { + transferId, + userId: transfer.from_user_id, + amount: transfer.amount, + oldStatus, + newStatus, + note: 'Sender balance was not deducted in confirmed status under new logic' + }); + + // 记录详细的余额变更审计日志 + auditLogger.info('Balance adjustment - status change (no action needed)', { + transferId: transferId, + fromUserId: transfer.from_user_id, + toUserId: transfer.to_user_id, + amount: transfer.amount, + operation: 'no_action_needed', + oldStatus: oldStatus, + newStatus: newStatus, + adminId: adminId, + reason: reason, + note: 'Sender balance was not deducted in confirmed status under new logic', + timestamp: new Date().toISOString() + }); + } else if ((oldStatus === TRANSFER_STATUS.PENDING || oldStatus === TRANSFER_STATUS.REJECTED || oldStatus === TRANSFER_STATUS.CANCELLED || oldStatus === TRANSFER_STATUS.NOT_RECEIVED) && newStatus === TRANSFER_STATUS.CONFIRMED) { + // 从待处理/拒绝/取消/未收到变为确认:新逻辑下CONFIRMED状态不扣除发送方余额 + logger.info('Status change to confirmed - no balance deduction needed', { + transferId, + userId: transfer.from_user_id, + amount: transfer.amount, + oldStatus, + newStatus, + note: 'Sender balance will be deducted when receiver confirms receipt' + }); + } else if ((oldStatus === TRANSFER_STATUS.PENDING || oldStatus === TRANSFER_STATUS.REJECTED || oldStatus === TRANSFER_STATUS.CANCELLED || oldStatus === TRANSFER_STATUS.NOT_RECEIVED) && newStatus === TRANSFER_STATUS.RECEIVED) { + // 从待处理/拒绝/取消/未收到变为已收到:扣除发送方余额和积分 + await connection.execute( + 'UPDATE users SET balance = balance - ?, points = points + ? WHERE id = ?', + [transfer.amount, transfer.amount, transfer.from_user_id] + ); + + logger.info('Balance and points deducted due to status change to received', { + transferId, + userId: transfer.from_user_id, + amount: transfer.amount, + oldStatus, + newStatus + }); + } else if (oldStatus === TRANSFER_STATUS.RECEIVED && (newStatus === TRANSFER_STATUS.REJECTED || newStatus === TRANSFER_STATUS.CANCELLED || newStatus === TRANSFER_STATUS.NOT_RECEIVED)) { + // 从已收到变为拒绝/取消/未收到:需要从接收方扣除余额和积分,并回滚发送方余额和积分 + if (transfer.to_user_id) { + await connection.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [transfer.amount, transfer.to_user_id] + ); + + logger.info('Receiver balance and points deducted due to status change', { + transferId, + userId: transfer.to_user_id, + amount: transfer.amount, + oldStatus, + newStatus + }); + } + + await connection.execute( + 'UPDATE users SET balance = balance + ?, points = points - ? WHERE id = ?', + [transfer.amount, transfer.amount, transfer.from_user_id] + ); + + logger.info('Sender balance and points restored due to status change', { + transferId, + userId: transfer.from_user_id, + amount: transfer.amount, + oldStatus, + newStatus + }); + + // 记录详细的余额变更审计日志 + auditLogger.info('Balance adjustment - status change deduction', { + transferId: transferId, + fromUserId: transfer.from_user_id, + toUserId: transfer.to_user_id, + amount: transfer.amount, + operation: 'deduct_sender_balance', + oldStatus: oldStatus, + newStatus: newStatus, + adminId: adminId, + reason: reason, + timestamp: new Date().toISOString() + }); + } else if (oldStatus === TRANSFER_STATUS.RECEIVED && newStatus === TRANSFER_STATUS.CONFIRMED) { + // 从已收到变为已确认:需要从接收方扣除余额和积分(因为confirmed状态下接收方不应该有余额) + if (transfer.to_user_id) { + await connection.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [transfer.amount, transfer.to_user_id] + ); + + logger.info('Receiver balance and points deducted due to status change from received to confirmed', { + transferId, + userId: transfer.to_user_id, + amount: transfer.amount, + oldStatus, + newStatus + }); + } + } else if (oldStatus === TRANSFER_STATUS.CONFIRMED && newStatus === TRANSFER_STATUS.RECEIVED) { + // 从已确认变为已收到:新逻辑下需要扣除发送方余额和积分(因为CONFIRMED状态下未扣除) + await connection.execute( + 'UPDATE users SET balance = balance - ?, points = points + ? WHERE id = ?', + [transfer.amount, transfer.amount, transfer.from_user_id] + ); + + logger.info('Status change from confirmed to received - sender balance and points deducted', { + transferId, + userId: transfer.from_user_id, + amount: transfer.amount, + oldStatus, + newStatus, + note: 'Sender balance and points deducted as per new logic' + }); + } + } + + // 如果变更为received状态,需要增加接收方余额和积分 + if (adjust_balance && newStatus === TRANSFER_STATUS.RECEIVED && oldStatus !== TRANSFER_STATUS.RECEIVED && transfer.to_user_id) { + await connection.execute( + 'UPDATE users SET balance = balance + ? WHERE id = ?', + [transfer.amount, transfer.to_user_id] + ); + + logger.info('Receiver balance and points increased due to status change', { + transferId, + userId: transfer.to_user_id, + amount: transfer.amount, + oldStatus, + newStatus + }); + + // 记录详细的余额变更审计日志 + auditLogger.info('Balance adjustment - receiver balance and points increase', { + transferId: transferId, + fromUserId: transfer.from_user_id, + toUserId: transfer.to_user_id, + amount: transfer.amount, + operation: 'add_receiver_balance_and_points', + oldStatus: oldStatus, + newStatus: newStatus, + adminId: adminId, + reason: reason, + timestamp: new Date().toISOString() + }); + } + + await connection.commit(); + + // 记录审计日志 + auditLogger.info('Transfer status force changed by admin', { + transferId, + adminId, + oldStatus, + newStatus, + reason, + adjust_balance, + fromUserId: transfer.from_user_id, + toUserId: transfer.to_user_id, + amount: transfer.amount + }); + + logger.info('Transfer status force changed successfully', { + transferId, + adminId, + oldStatus, + newStatus + }); + + return {success: true, oldStatus, newStatus}; + } catch (error) { + await connection.rollback(); + throw error; + } + } catch (error) { + logger.error('Failed to force change transfer status', { + error: error.message, + transferId, + adminId, + newStatus + }); + throw error; + } finally { + if (connection) { + connection.release(); // 释放连接回连接池 + } + } + } catch (error) { + logger.error('Failed to get database connection for force change transfer status', { + error: error.message, + transferId, + adminId, + newStatus + }); + throw error; + } + } + + // 生成批次ID + generateBatchId() { + return `T${Date.now()}${Math.random().toString(36).substr(2, 9)}`; + } +} + +module.exports = new TransferService(); \ No newline at end of file diff --git a/src/components/ImageUpload.vue b/src/components/ImageUpload.vue new file mode 100644 index 0000000..aa93f9d --- /dev/null +++ b/src/components/ImageUpload.vue @@ -0,0 +1,345 @@ + + + + + \ No newline at end of file diff --git a/test_mao.sql b/test_mao.sql new file mode 100644 index 0000000..960aecb --- /dev/null +++ b/test_mao.sql @@ -0,0 +1,577 @@ +/* + Navicat Premium Dump SQL + + Source Server : 测试端 + Source Server Type : MySQL + Source Server Version : 80036 (8.0.36) + Source Host : 114.55.111.44:3306 + Source Schema : test_mao + + Target Server Type : MySQL + Target Server Version : 80036 (8.0.36) + File Encoding : 65001 + + Date: 22/08/2025 14:36:05 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for accounts +-- ---------------------------- +DROP TABLE IF EXISTS `accounts`; +CREATE TABLE `accounts` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `account_type` enum('public','user') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'user', + `balance` decimal(10, 2) NULL DEFAULT 0.00, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + CONSTRAINT `accounts_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 40 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for admin_operation_logs +-- ---------------------------- +DROP TABLE IF EXISTS `admin_operation_logs`; +CREATE TABLE `admin_operation_logs` ( + `id` int NOT NULL AUTO_INCREMENT, + `admin_id` int NOT NULL, + `operation_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `target_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `target_id` int NOT NULL, + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `admin_id`(`admin_id` ASC) USING BTREE, + CONSTRAINT `admin_operation_logs_ibfk_1` FOREIGN KEY (`admin_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 39 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for agent_commission_records +-- ---------------------------- +DROP TABLE IF EXISTS `agent_commission_records`; +CREATE TABLE `agent_commission_records` ( + `id` int NOT NULL AUTO_INCREMENT, + `agent_id` int NOT NULL, + `merchant_id` int NOT NULL, + `order_id` int NULL DEFAULT NULL, + `commission_amount` decimal(10, 2) NOT NULL, + `commission_type` enum('registration','matching') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'matching', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `agent_id`(`agent_id` ASC) USING BTREE, + INDEX `merchant_id`(`merchant_id` ASC) USING BTREE, + INDEX `order_id`(`order_id` ASC) USING BTREE, + CONSTRAINT `agent_commission_records_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `regional_agents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `agent_commission_records_ibfk_2` FOREIGN KEY (`merchant_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `agent_commission_records_ibfk_3` FOREIGN KEY (`order_id`) REFERENCES `matching_orders` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for agent_merchants +-- ---------------------------- +DROP TABLE IF EXISTS `agent_merchants`; +CREATE TABLE `agent_merchants` ( + `id` int NOT NULL AUTO_INCREMENT, + `agent_id` int NOT NULL, + `merchant_id` int NOT NULL, + `registration_code_id` int NULL DEFAULT NULL, + `matching_count` int NULL DEFAULT 0, + `commission_earned` decimal(10, 2) NULL DEFAULT 0.00, + `is_qualified` tinyint(1) NULL DEFAULT 0, + `qualified_at` timestamp NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `unique_agent_merchant`(`agent_id` ASC, `merchant_id` ASC) USING BTREE, + INDEX `merchant_id`(`merchant_id` ASC) USING BTREE, + INDEX `registration_code_id`(`registration_code_id` ASC) USING BTREE, + CONSTRAINT `agent_merchants_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `regional_agents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `agent_merchants_ibfk_2` FOREIGN KEY (`merchant_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `agent_merchants_ibfk_3` FOREIGN KEY (`registration_code_id`) REFERENCES `registration_codes` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for agent_withdrawals +-- ---------------------------- +DROP TABLE IF EXISTS `agent_withdrawals`; +CREATE TABLE `agent_withdrawals` ( + `id` int NOT NULL AUTO_INCREMENT, + `agent_id` int NOT NULL, + `amount` decimal(10, 2) NOT NULL, + `payment_type` enum('bank','wechat','alipay','unionpay') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'bank' COMMENT '收款方式类型', + `bank_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行名称', + `account_number` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号/银行账号', + `account_holder` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '持有人姓名', + `qr_code_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收款码图片URL', + `status` enum('pending','approved','rejected','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'pending', + `apply_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, + `admin_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, + `processed_by` int NULL DEFAULT NULL, + `processed_at` timestamp NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `bank_account` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行账号(兼容旧版本)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `agent_id`(`agent_id` ASC) USING BTREE, + INDEX `processed_by`(`processed_by` ASC) USING BTREE, + CONSTRAINT `agent_withdrawals_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `regional_agents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `agent_withdrawals_ibfk_2` FOREIGN KEY (`processed_by`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for articles +-- ---------------------------- +DROP TABLE IF EXISTS `articles`; +CREATE TABLE `articles` ( + `id` int NOT NULL AUTO_INCREMENT, + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `author_id` int NULL DEFAULT NULL, + `status` enum('draft','published') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'draft', + `views` int NULL DEFAULT 0, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `author_id`(`author_id` ASC) USING BTREE, + CONSTRAINT `articles_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for balance_fix_log +-- ---------------------------- +DROP TABLE IF EXISTS `balance_fix_log`; +CREATE TABLE `balance_fix_log` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `amount_deducted` decimal(10, 2) NOT NULL, + `transfer_count` int NOT NULL, + `fix_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`user_id` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for matching_orders +-- ---------------------------- +DROP TABLE IF EXISTS `matching_orders`; +CREATE TABLE `matching_orders` ( + `id` int NOT NULL AUTO_INCREMENT, + `initiator_id` int NOT NULL, + `amount` decimal(10, 2) NOT NULL, + `status` enum('pending','matching','completed','cancelled','failed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending', + `cycle_count` int NULL DEFAULT 0, + `max_cycles` int NULL DEFAULT 3, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `matching_type` enum('small','large') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'small', + `is_system_reverse` tinyint(1) NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE, + INDEX `initiator_id`(`initiator_id` ASC) USING BTREE, + CONSTRAINT `matching_orders_ibfk_1` FOREIGN KEY (`initiator_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 200 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for matching_records +-- ---------------------------- +DROP TABLE IF EXISTS `matching_records`; +CREATE TABLE `matching_records` ( + `id` int NOT NULL AUTO_INCREMENT, + `matching_order_id` int NOT NULL, + `user_id` int NOT NULL, + `action` enum('join','confirm','reject','complete') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `amount` decimal(10, 2) NULL DEFAULT NULL, + `note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `matching_order_id`(`matching_order_id` ASC) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + CONSTRAINT `matching_records_ibfk_1` FOREIGN KEY (`matching_order_id`) REFERENCES `matching_orders` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `matching_records_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 773 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for order_allocations +-- ---------------------------- +DROP TABLE IF EXISTS `order_allocations`; +CREATE TABLE `order_allocations` ( + `id` int NOT NULL AUTO_INCREMENT, + `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') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending', + `transfer_id` int NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `confirmed_at` timestamp NULL DEFAULT NULL, + `outbound_date` date NULL DEFAULT NULL, + `return_date` date NULL DEFAULT NULL, + `can_return_after` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `matching_order_id`(`matching_order_id` ASC) USING BTREE, + INDEX `from_user_id`(`from_user_id` ASC) USING BTREE, + INDEX `to_user_id`(`to_user_id` ASC) USING BTREE, + INDEX `transfer_id`(`transfer_id` ASC) USING BTREE, + CONSTRAINT `order_allocations_ibfk_1` FOREIGN KEY (`matching_order_id`) REFERENCES `matching_orders` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `order_allocations_ibfk_2` FOREIGN KEY (`from_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `order_allocations_ibfk_3` FOREIGN KEY (`to_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `order_allocations_ibfk_4` FOREIGN KEY (`transfer_id`) REFERENCES `transfers` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 673 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for order_allocations_backup +-- ---------------------------- +DROP TABLE IF EXISTS `order_allocations_backup`; +CREATE TABLE `order_allocations_backup` ( + `id` int NOT NULL DEFAULT 0, + `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') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending', + `transfer_id` int NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `confirmed_at` timestamp NULL DEFAULT NULL, + `outbound_date` date NULL DEFAULT NULL, + `return_date` date NULL DEFAULT NULL, + `can_return_after` timestamp NULL DEFAULT NULL +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for order_items +-- ---------------------------- +DROP TABLE IF EXISTS `order_items`; +CREATE TABLE `order_items` ( + `id` int NOT NULL AUTO_INCREMENT, + `order_id` int NOT NULL, + `product_id` int NOT NULL, + `quantity` int NOT NULL, + `price` int NOT NULL, + `points` int NOT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `order_id`(`order_id` ASC) USING BTREE, + INDEX `product_id`(`product_id` ASC) USING BTREE, + CONSTRAINT `order_items_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `order_items_ibfk_2` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for orders +-- ---------------------------- +DROP TABLE IF EXISTS `orders`; +CREATE TABLE `orders` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `total_amount` int NOT NULL, + `total_points` int NOT NULL, + `status` enum('pending','paid','shipped','delivered','cancelled') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending', + `address` json NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `order_no`(`order_no` ASC) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for points_history +-- ---------------------------- +DROP TABLE IF EXISTS `points_history`; +CREATE TABLE `points_history` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `type` enum('earn','spend','admin_adjust') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `amount` int NOT NULL, + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `order_id` int NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + INDEX `order_id`(`order_id` ASC) USING BTREE, + CONSTRAINT `points_history_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `points_history_ibfk_2` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 326 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for product_reviews +-- ---------------------------- +DROP TABLE IF EXISTS `product_reviews`; +CREATE TABLE `product_reviews` ( + `id` int NOT NULL AUTO_INCREMENT, + `product_id` int NOT NULL, + `user_id` int NOT NULL, + `order_id` int NOT NULL, + `rating` int NOT NULL, + `comment` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `images` json NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `product_id`(`product_id` ASC) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + INDEX `order_id`(`order_id` ASC) USING BTREE, + CONSTRAINT `product_reviews_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `product_reviews_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `product_reviews_ibfk_3` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for products +-- ---------------------------- +DROP TABLE IF EXISTS `products`; +CREATE TABLE `products` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `price` int NOT NULL, + `original_price` int NULL DEFAULT NULL, + `stock` int NULL DEFAULT 0, + `sales` int NULL DEFAULT 0, + `rating` decimal(3, 2) NULL DEFAULT 5.00, + `category` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `images` json NULL, + `status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'active', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `points_price` int NOT NULL DEFAULT 0, + `image_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `details` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for regional_agents +-- ---------------------------- +DROP TABLE IF EXISTS `regional_agents`; +CREATE TABLE `regional_agents` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `region_id` int NOT NULL, + `agent_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `status` enum('pending','active','suspended','terminated') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'pending', + `commission_rate` decimal(5, 4) NULL DEFAULT 0.0500, + `total_earnings` decimal(10, 2) NULL DEFAULT 0.00, + `recruited_merchants` int NULL DEFAULT 0, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `approved_at` timestamp NULL DEFAULT NULL, + `approved_by_admin_id` int NULL DEFAULT NULL, + `withdrawn_amount` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '已提现金额', + `pending_withdrawal` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '待审核提现金额', + `payment_type` enum('bank','wechat','alipay','unionpay') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'bank' COMMENT '收款方式类型', + `account_number` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号/银行账号', + `account_holder` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '持有人姓名', + `qr_code_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收款码图片URL', + `bank_account` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行账号(兼容旧版本)', + `bank_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行名称', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `agent_code`(`agent_code` ASC) USING BTREE, + UNIQUE INDEX `unique_agent_region`(`user_id` ASC, `region_id` ASC) USING BTREE, + INDEX `region_id`(`region_id` ASC) USING BTREE, + CONSTRAINT `regional_agents_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `regional_agents_ibfk_2` FOREIGN KEY (`region_id`) REFERENCES `zhejiang_regions` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for registration_codes +-- ---------------------------- +DROP TABLE IF EXISTS `registration_codes`; +CREATE TABLE `registration_codes` ( + `id` int NOT NULL AUTO_INCREMENT, + `code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '注册码', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `expires_at` timestamp NOT NULL COMMENT '过期时间', + `used_at` timestamp NULL DEFAULT NULL COMMENT '使用时间', + `used_by_user_id` int NULL DEFAULT NULL COMMENT '使用该注册码的用户ID', + `is_used` tinyint(1) NULL DEFAULT 0 COMMENT '是否已使用', + `created_by_admin_id` int NOT NULL COMMENT '创建该注册码的管理员ID', + `agent_id` int NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `code`(`code` ASC) USING BTREE, + INDEX `idx_code`(`code` ASC) USING BTREE, + INDEX `idx_expires_at`(`expires_at` ASC) USING BTREE, + INDEX `idx_is_used`(`is_used` ASC) USING BTREE, + INDEX `used_by_user_id`(`used_by_user_id` ASC) USING BTREE, + INDEX `created_by_admin_id`(`created_by_admin_id` ASC) USING BTREE, + INDEX `fk_registration_codes_agent_id`(`agent_id` ASC) USING BTREE, + CONSTRAINT `fk_registration_codes_agent_id` FOREIGN KEY (`agent_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT, + CONSTRAINT `registration_codes_ibfk_1` FOREIGN KEY (`used_by_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT, + CONSTRAINT `registration_codes_ibfk_2` FOREIGN KEY (`created_by_admin_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 141 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '注册码表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for system_settings +-- ---------------------------- +DROP TABLE IF EXISTS `system_settings`; +CREATE TABLE `system_settings` ( + `id` int NOT NULL AUTO_INCREMENT, + `setting_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `setting_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `setting_key`(`setting_key` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 71 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for test_users +-- ---------------------------- +DROP TABLE IF EXISTS `test_users`; +CREATE TABLE `test_users` ( + `id` int NOT NULL AUTO_INCREMENT, + `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for transfer_confirmations +-- ---------------------------- +DROP TABLE IF EXISTS `transfer_confirmations`; +CREATE TABLE `transfer_confirmations` ( + `id` int NOT NULL AUTO_INCREMENT, + `transfer_id` int NOT NULL, + `confirmer_id` int NOT NULL, + `action` enum('confirm','reject') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `transfer_id`(`transfer_id` ASC) USING BTREE, + INDEX `confirmer_id`(`confirmer_id` ASC) USING BTREE, + CONSTRAINT `transfer_confirmations_ibfk_1` FOREIGN KEY (`transfer_id`) REFERENCES `transfers` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `transfer_confirmations_ibfk_2` FOREIGN KEY (`confirmer_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for transfers +-- ---------------------------- +DROP TABLE IF EXISTS `transfers`; +CREATE TABLE `transfers` ( + `id` int NOT NULL AUTO_INCREMENT, + `from_user_id` int NULL DEFAULT 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') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'user_to_user', + `status` enum('pending','confirmed','rejected','received','not_received','cancelled') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending', + `voucher_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `batch_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deadline_at` timestamp NULL DEFAULT NULL COMMENT '转账截止时间', + `is_overdue` tinyint(1) NULL DEFAULT 0 COMMENT '是否超时', + `overdue_at` timestamp NULL DEFAULT NULL COMMENT '超时时间', + `is_bad_debt` tinyint(1) NULL DEFAULT 0, + `confirmed_at` timestamp NULL DEFAULT NULL, + `admin_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `admin_modified_at` timestamp NULL DEFAULT NULL, + `admin_modified_by` int NULL DEFAULT NULL, + `source_type` enum('manual','allocation','system') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'manual' COMMENT '转账来源类型', + `matching_order_id` int NULL DEFAULT NULL, + `cycle_number` int NULL DEFAULT NULL, + `outbound_date` date NULL DEFAULT NULL, + `return_date` date NULL DEFAULT NULL, + `can_return_after` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `from_user_id`(`from_user_id` ASC) USING BTREE, + INDEX `to_user_id`(`to_user_id` ASC) USING BTREE, + INDEX `fk_transfers_matching_order_id`(`matching_order_id` ASC) USING BTREE, + CONSTRAINT `fk_transfers_matching_order_id` FOREIGN KEY (`matching_order_id`) REFERENCES `matching_orders` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT, + CONSTRAINT `transfers_ibfk_1` FOREIGN KEY (`from_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `transfers_ibfk_2` FOREIGN KEY (`to_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 558 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for user_matching_pool +-- ---------------------------- +DROP TABLE IF EXISTS `user_matching_pool`; +CREATE TABLE `user_matching_pool` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `available_amount` decimal(10, 2) NULL DEFAULT 0.00, + `is_active` tinyint(1) NULL DEFAULT 1, + `last_matched_at` timestamp NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `unique_user`(`user_id` ASC) USING BTREE, + CONSTRAINT `user_matching_pool_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 61 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for users +-- ---------------------------- +DROP TABLE IF EXISTS `users`; +CREATE TABLE `users` ( + `id` int NOT NULL AUTO_INCREMENT, + `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `role` enum('user','admin') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'user', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `points` int NULL DEFAULT 0, + `real_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `id_card` varchar(18) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `wechat_qr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `alipay_qr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `bank_card` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `unionpay_qr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `is_system_account` tinyint(1) NULL DEFAULT 0, + `completed_withdrawals` int NULL DEFAULT 0, + `balance` decimal(10, 2) NULL DEFAULT 0.00, + `is_risk_user` tinyint(1) NULL DEFAULT 0 COMMENT '是否为风险用户', + `is_blacklisted` tinyint(1) NULL DEFAULT 0 COMMENT '是否被拉黑', + `risk_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '风险原因', + `blacklist_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '拉黑原因', + `blacklisted_at` timestamp NULL DEFAULT NULL COMMENT '拉黑时间', + `business_license` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `id_card_front` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `id_card_back` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `audit_status` enum('pending','approved','rejected') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending', + `audit_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `audited_by` int NULL DEFAULT NULL, + `audited_at` timestamp NULL DEFAULT NULL, + `city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `district_id` int NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `username`(`username` ASC) USING BTREE, + UNIQUE INDEX `email`(`email` ASC) USING BTREE, + UNIQUE INDEX `phone`(`phone` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 9548 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for zhejiang_regions +-- ---------------------------- +DROP TABLE IF EXISTS `zhejiang_regions`; +CREATE TABLE `zhejiang_regions` ( + `id` int NOT NULL AUTO_INCREMENT, + `city_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `district_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `region_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `is_available` tinyint(1) NULL DEFAULT 1, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `region_code`(`region_code` ASC) USING BTREE, + UNIQUE INDEX `unique_region`(`city_name` ASC, `district_name` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 20041 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +SET FOREIGN_KEY_CHECKS = 1;