This commit is contained in:
2025-09-15 17:27:13 +08:00
parent d50290e8fe
commit 14a3c39f9d
27 changed files with 3571 additions and 31198 deletions

4
.env
View File

@@ -62,3 +62,7 @@ ALIPAY_APP_ID=2021005188682022
ALIPAY_NOTIFY_URL=https://www.zrbjr.com/api/payment/alipay/notify
ALIPAY_RETURN_URL=https://www.zrbjr.com/payment
ALIPAY_QUIT_URL=https://www.zrbjr.com/payment/
#ALIPAY_APP_ID=9021000151699946
#ALIPAY_NOTIFY_URL=https://test.zrbjr.com/api/payment/alipay/notify
#ALIPAY_RETURN_URL=http://192.168.1.124:5173/frontend/payment
#ALIPAY_QUIT_URL=http://192.168.1.124:5173/frontend/payment

View File

@@ -1,146 +0,0 @@
# 部署说明
## 问题解决
### 文件上传问题修复
之前程序部署到线上环境后无法上传文件的问题已经修复。主要修改包括:
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. 查看服务器错误日志

View File

@@ -1,31 +0,0 @@
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();

View File

@@ -0,0 +1 @@
MIIEowIBAAKCAQEAk3HgYh7dK97PBX4UhlsusYGI4bLL7EQak2JfOZUeoVJK144ENg6yF0t3eohiryiK/vpyuyqX3hP5jEQMQHM92Mor9mf/KPj6Dw5AAvNLEdEaDCLwKUByk8sN2M6Xibdh6X0nQ67wOe6kaDkOG5Efkr4rHJCBfwbINXuaxXnJUr1mLdzwp1XN09IBUcKt+EXOKfpwbIuU6JoxTMiz7hnRHDuVKTYqnvx9CdV5Ng1eqYv5OI4r9abiFttRUJ37+RXbvXzffJGa/HFjpyPp4MOuK8n24ZW4vdjSBlflzzRHTAuo3tSo56gy9UouY6XtT0C9sXE7CxHkHeupOO3Wzl6t9wIDAQABAoIBAFvPRNDUNxPiITZiU5V1oZWV+w3Or3vmzEWJs5G/cNsyyrd+DtE6RVqL+1GpKwm2TRaIDHjPBNGbzn+wv5BCMfiTqtw71X5Fyi8lyGYN6Gins3hrKPAG2VF91plxyppOPgGNlK1oeN0Z4/Wh0U8JBofxMhcPRBM8vd3PoYflVZ7WpeHaApdL4n0S0Z+HcaCtnhfbJDh2fTXNkl01e2nI1Rqatkqc/Giwc8Pg5SrTKBO2g6GhPbk9LUUPqat9+3jmbT1CI56HS4OVTWFbqCMFqQE/KSXNpuFYRRTCgWDQSPbmJEKyDQMinG+DdJj2IIMhD60qYut+OBb6XQVqtdlBnoECgYEA/fbCiFIZPZ6IU7hnn75Jj2JDc6XIqFAVrbofViL3HUyeZ1KSYkXtDdh42Cl8fBCiT0xyagA/fdHmFTTJBueKfoW3+sqFMm3vKe4spJCNLmSAP8gu60zTAslmiQ5zyq+UVd74BE1Y7HSLoAv3lZnwIJkhOgrUq2mH4+r73SPN+C8CgYEAlKB+s7RZW+h4NX0YGZFdQ1AQJI0uSYOFt6YVqhTIRhUSqM++Yy/giE8X0TRckqhhAsBD6SN05rB54o6CdSacVriALtD+vrVZBDt+abTmeh1BhRUkrYeC7wZK5/+5pni9Nr1GfNKe+U2a8rzKCUJg6BWW6qGUrHOiU+9lYhQ17LkCgYBudtiipuMntD8j+z/HceNZJKqmMOQYoczsJdrfgpHuApeb5YSajkPQE+psS49D/5A54cyaYsU7GwNzEeSmxiutYMhno5NQHhU7LcfpRJ7EIR7Pn2kZG+9kdOnOj6S58qkYuMU0Sdh18TOSR+JHBhA2faTANFnQvTRIZLtsstgyLQKBgQCBl8/icYbZFMJ8IS86W/2uC8mHlXKetweJMlABlU1rjkRO3ZVsdvqY4B4sVDPDzP2JoIuWZUwxOf+NBCXMcHYxR369U45MS2PqxNVc5ldwcsIGgIESre4E7L+zus7t0KlraW5kuGHVj01kCilAGZjVxL1qqKkyFUGdXkhQVL8QQQKBgCO6GkpCR5yBs181iPh+AdI3EZh70SEBHfZNNJFs5a4FvDhxZhvkwIG77FY9IVyBn/aZLrVHJXUVB/HGA1QeNhHSXvIkWGwsNQ04OZJu0uio+4D+yGxY5XQ4F6Ti9KPI9dbu9S7OZKfZCsj5nhm/kKB3MlExN2MKqoQIMGHDcI0k

View File

@@ -0,0 +1 @@
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhptRt5n0KVACDX69KfZogfeLULSuZGkH0m3wVDI32a4eBH3TrzKjSShdO2WBF6glfvdCruuMuLmYJrIxa09koNcAF5CNc/2iHE8O8pUbrsLKgX+dpXoNZt6XVo/jHqsxmYDFjZogL3xFVW0z0pGcxaIWnVEhQ0hQ+ji2RxJ0Bb45mmOZOOVihpLf9hEFW0rHamf2Tfu+Hd4NWTb/CVZwgzchJ/cwLTqP1Ar3GeQdmB2tmaCTu1h8wyt6lkSUTOYTEf8xCdmv8xfS12dXAeh11t1a3SuqPT2b4UxuLJHHMmiKndD7BnPZIENxi7e6N5JKz1zahyuh23GeBCHs1wHtQQIDAQAB

View File

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlfXRCD4ppMMYSV18tnJ7
TWanmXVeCQHce2aZ7241ejo3hGXM5LChWupYLGEelquhv5IRHVUcjVuEbHqQUcFR
gwccVDdJwfbk7T7YtttD7V2SCcnC2OfOZy/4mlI1LfgsodqrMiBkNJRsfVRVlPse
bLRgi4H2WxzmjKNEvuPbqRLF+aeDMkW3OMwP73/sYJhpuX9WAdTcJt9iQYVUaLq5
h4YvNjC19x8cKOkf0iDqwyHFKxC2AvV0Qti1FmCSLENUDLaxP9F1RZiAevIFPSak
UzV4Swsi9fSt1lBr0VxvTeZk2mUgSGHKO2a7W0xR7SMUjmMamEzrZDylWMRp9SNl
hwIDAQAB
-----END PUBLIC KEY-----

View File

@@ -1,9 +1 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlfXRCD4ppMMYSV18tnJ7
TWanmXVeCQHce2aZ7241ejo3hGXM5LChWupYLGEelquhv5IRHVUcjVuEbHqQUcFR
gwccVDdJwfbk7T7YtttD7V2SCcnC2OfOZy/4mlI1LfgsodqrMiBkNJRsfVRVlPse
bLRgi4H2WxzmjKNEvuPbqRLF+aeDMkW3OMwP73/sYJhpuX9WAdTcJt9iQYVUaLq5
h4YvNjC19x8cKOkf0iDqwyHFKxC2AvV0Qti1FmCSLENUDLaxP9F1RZiAevIFPSak
UzV4Swsi9fSt1lBr0VxvTeZk2mUgSGHKO2a7W0xR7SMUjmMamEzrZDylWMRp9SNl
hwIDAQAB
-----END PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5swLKPSzOMucRC52c9kKJZI9cYWDFd+s3UuE+aDtWodGrGV8g3szmp7hUWlaWY/didKc9vQNq93y67eEyw6QsMn26WwlzDbgP0xTcHEt+qDCeAltSqf6MX3KPmlz0f/DNneR9DR9ZGwaW1ATY3kg8gj+kIWngrqgjOv37UJWEpQOxUfWDGTBC1zzhC0PTXY7lX3GUZmDEtDtBs1BsFUdk995TbTD1cTiyDFuea49br0dovmU1ROOg6vK3G9xDd4Mke/opDunLTHe63+fBCnB7FyZ9F8zWg4LYND1QPmIX2m5gwICBHhNm8WqIfp9T64vpAxlM74BEsMlv3hNy0INQQIDAQAB

View File

@@ -2,40 +2,41 @@ 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
// 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',
dateStrings: true,
// 连接池配置
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
};
// 创建数据库连接池
@@ -46,46 +47,46 @@ let pool;
* @returns {Promise<mysql.Pool>} 数据库连接池
*/
async function initDB() {
if (!pool) {
try {
pool = mysql.createPool(dbConfig);
if (!pool) {
try {
pool = mysql.createPool(dbConfig);
// 添加连接池事件监听
pool.on('connection', function (connection) {
console.log('新的数据库连接建立:', connection.threadId);
});
// 添加连接池事件监听
pool.on('connection', function (connection) {
console.log('新的数据库连接建立:', connection.threadId);
});
// 注释掉频繁的连接获取和释放日志,避免日志过多
// pool.on('acquire', 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('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('数据库连接超时,尝试重新连接...');
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;
}
});
// 测试连接
const connection = await pool.getConnection();
console.log('数据库连接池初始化成功');
connection.release();
} catch (error) {
console.error('数据库连接池初始化失败:', error);
throw error;
}
}
return pool;
return pool;
}
/**
@@ -93,10 +94,10 @@ async function initDB() {
* @returns {mysql.Pool} 数据库连接池
*/
function getDB() {
if (!pool) {
throw new Error('数据库连接池未初始化,请先调用 initDB()');
}
return pool;
if (!pool) {
throw new Error('数据库连接池未初始化,请先调用 initDB()');
}
return pool;
}
/**
@@ -107,52 +108,52 @@ function getDB() {
* @returns {Promise<Array>} 查询结果
*/
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);
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 (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;
}
// 如果是连接相关错误,等待后重试
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('数据库连接池已关闭');
}
if (pool) {
await pool.end();
pool = null;
console.log('数据库连接池已关闭');
}
}
module.exports = {
initDB,
getDB,
closeDB,
executeQuery,
dbConfig
initDB,
getDB,
closeDB,
executeQuery,
dbConfig
};

View File

@@ -65,6 +65,62 @@
* description: 用户角色
* default: user
*/
/**
* @swagger
* /users/pending-audit:
* get:
* summary: 获取待审核用户列表(管理员权限)
* tags: [Users]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: limit
* schema:
* type: integer
* default: 10
* description: 每页数量
* responses:
* 200:
* description: 成功获取待审核用户列表
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* users:
* type: array
* items:
* $ref: '#/components/schemas/User'
* pagination:
* type: object
* properties:
* page:
* type: integer
* limit:
* type: integer
* total:
* type: integer
* pages:
* type: integer
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 500:
* description: 服务器错误
*/
/**
* @swagger
* /auth/register:

View File

@@ -1,89 +0,0 @@
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 };

View File

@@ -1,398 +0,0 @@
const fs = require('fs');
const path = require('path');
const minioService = require('./services/minioService');
const { getDB, initDB } = require('./database');
/**
* 文件迁移到 MinIO 的脚本
* 将本地 uploads 目录下的文件迁移到 MinIO 存储
*/
class FilesMigration {
constructor() {
this.minioService = minioService;
this.uploadsDir = path.join(__dirname, 'uploads');
this.migratedFiles = [];
this.failedFiles = [];
}
/**
* 递归获取目录下所有文件
* @param {string} dir - 目录路径
* @param {string} baseDir - 基础目录路径
* @returns {Array} 文件列表
*/
getAllFiles(dir, baseDir = dir) {
const files = [];
const items = fs.readdirSync(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
files.push(...this.getAllFiles(fullPath, baseDir));
} else {
const relativePath = path.relative(baseDir, fullPath);
files.push({
fullPath,
relativePath: relativePath.replace(/\\/g, '/'), // 统一使用正斜杠
fileName: item,
size: stat.size,
mtime: stat.mtime
});
}
}
return files;
}
/**
* 生成新的 MinIO 路径(去掉 uploads 前缀)
* @param {Object} fileInfo - 文件信息
* @returns {string} 去掉 uploads 前缀的文件路径
*/
generateMinioPath(fileInfo) {
// 去掉 uploads/ 前缀,因为数据库中存储的是 /uploads/xxx而 MinIO 中应该是 xxx
let minioPath = fileInfo.relativePath;
if (minioPath.startsWith('uploads/')) {
minioPath = minioPath.substring('uploads/'.length);
}
return minioPath;
}
/**
* 上传单个文件到 MinIO
* @param {Object} fileInfo - 文件信息
* @returns {Promise<Object>} 上传结果
*/
async uploadFileToMinio(fileInfo) {
try {
const minioPath = this.generateMinioPath(fileInfo);
// 使用固定的存储桶名称
const bucketName = 'jurongquan';
console.log(`正在上传: ${fileInfo.relativePath} -> ${minioPath}`);
// 读取文件内容
const fileBuffer = fs.readFileSync(fileInfo.fullPath);
// 上传到 MinIO
const uploadResult = await this.minioService.uploadFileForMigration(
bucketName,
minioPath,
fileBuffer,
this.getContentType(fileInfo.fileName)
);
const result = {
url: uploadResult.data.url,
path: uploadResult.data.path
};
return {
success: true,
originalPath: fileInfo.relativePath,
minioPath,
minioUrl: result.url,
fileInfo
};
} catch (error) {
console.error(`上传失败 ${fileInfo.relativePath}:`, error.message);
return {
success: false,
originalPath: fileInfo.relativePath,
error: error.message,
fileInfo
};
}
}
/**
* 根据文件扩展名获取 Content-Type
* @param {string} fileName - 文件名
* @returns {string} Content-Type
*/
getContentType(fileName) {
const ext = path.extname(fileName).toLowerCase();
const contentTypes = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.pdf': 'application/pdf',
'.doc': 'application/msword',
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'.txt': 'text/plain'
};
return contentTypes[ext] || 'application/octet-stream';
}
/**
* 更新数据库中的文件路径引用
* @param {Array} migratedFiles - 已迁移的文件列表
*/
async updateDatabaseReferences(migratedFiles) {
const db = getDB();
let updatedCount = 0;
console.log('\n开始更新数据库中的文件路径引用...');
for (const file of migratedFiles) {
console.log(file,'file');
try {
// 原路径:/uploads/avatars/xxx.jpg
const oldPath = `/${file.originalPath}`;
// 新路径:/avatars/xxx.jpg (去掉 uploads 前缀)
const newPath = `/${file.minioPath}`;
// 更新用户头像
const [avatarResult] = await db.execute(
'UPDATE users SET avatar = ? WHERE avatar = ?',
[newPath, oldPath]
);
if (avatarResult.affectedRows > 0) {
console.log(`更新用户头像: ${oldPath} -> ${newPath}`);
updatedCount += avatarResult.affectedRows;
}
// 更新用户微信收款码
const [wechatQrResult] = await db.execute(
'UPDATE users SET wechat_qr = ? WHERE wechat_qr = ?',
[newPath, oldPath]
);
if (wechatQrResult.affectedRows > 0) {
console.log(`更新用户微信收款码: ${oldPath} -> ${newPath}`);
updatedCount += wechatQrResult.affectedRows;
}
// 更新用户支付宝收款码
const [alipayQrResult] = await db.execute(
'UPDATE users SET alipay_qr = ? WHERE alipay_qr = ?',
[newPath, oldPath]
);
if (alipayQrResult.affectedRows > 0) {
console.log(`更新用户支付宝收款码: ${oldPath} -> ${newPath}`);
updatedCount += alipayQrResult.affectedRows;
}
// 更新用户云闪付收款码
const [unionpayQrResult] = await db.execute(
'UPDATE users SET unionpay_qr = ? WHERE unionpay_qr = ?',
[newPath, oldPath]
);
if (unionpayQrResult.affectedRows > 0) {
console.log(`更新用户云闪付收款码: ${oldPath} -> ${newPath}`);
updatedCount += unionpayQrResult.affectedRows;
}
// 更新用户营业执照
const [businessLicenseResult] = await db.execute(
'UPDATE users SET business_license = ? WHERE business_license = ?',
[newPath, oldPath]
);
if (businessLicenseResult.affectedRows > 0) {
console.log(`更新用户营业执照: ${oldPath} -> ${newPath}`);
updatedCount += businessLicenseResult.affectedRows;
}
// 更新用户身份证正面
const [idCardFrontResult] = await db.execute(
'UPDATE users SET id_card_front = ? WHERE id_card_front = ?',
[newPath, oldPath]
);
if (idCardFrontResult.affectedRows > 0) {
console.log(`更新用户身份证正面: ${oldPath} -> ${newPath}`);
updatedCount += idCardFrontResult.affectedRows;
}
// 更新用户身份证反面
const [idCardBackResult] = await db.execute(
'UPDATE users SET id_card_back = ? WHERE id_card_back = ?',
[newPath, oldPath]
);
if (idCardBackResult.affectedRows > 0) {
console.log(`更新用户身份证反面: ${oldPath} -> ${newPath}`);
updatedCount += idCardBackResult.affectedRows;
}
// 更新产品图片
const [productResult] = await db.execute(
'UPDATE products SET image_url = ? WHERE image_url = ?',
[newPath, oldPath]
);
if (productResult.affectedRows > 0) {
console.log(`更新产品图片: ${oldPath} -> ${newPath}`);
updatedCount += productResult.affectedRows;
}
// 更新产品店铺头像
const [shopAvatarResult] = await db.execute(
'UPDATE products SET shop_avatar = ? WHERE shop_avatar = ?',
[newPath, oldPath]
);
if (shopAvatarResult.affectedRows > 0) {
console.log(`更新产品店铺头像: ${oldPath} -> ${newPath}`);
updatedCount += shopAvatarResult.affectedRows;
}
// 更新产品图片JSON 字段中的图片)
const [products] = await db.execute(
'SELECT id, images FROM products WHERE images LIKE ?',
[`%${oldPath}%`]
);
for (const product of products) {
if (product.images) {
const updatedImages = product.images.replace(new RegExp(oldPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newPath);
await db.execute(
'UPDATE products SET images = ? WHERE id = ?',
[updatedImages, product.id]
);
console.log(`更新产品图片集合: 产品ID ${product.id}`);
updatedCount++;
}
}
// 更新转账凭证
const [transferResult] = await db.execute(
'UPDATE transfers SET voucher_url = ? WHERE voucher_url = ?',
[newPath, oldPath]
);
if (transferResult.affectedRows > 0) {
console.log(`更新转账凭证: ${oldPath} -> ${newPath}`);
updatedCount += transferResult.affectedRows;
}
// 更新区域代理收款码
const [agentQrResult] = await db.execute(
'UPDATE regional_agents SET qr_code_url = ? WHERE qr_code_url = ?',
[newPath, oldPath]
);
if (agentQrResult.affectedRows > 0) {
console.log(`更新区域代理收款码: ${oldPath} -> ${newPath}`);
updatedCount += agentQrResult.affectedRows;
}
// 更新规格图片
const [specResult] = await db.execute(
'UPDATE spec_values SET image_url = ? WHERE image_url = ?',
[newPath, oldPath]
);
if (specResult.affectedRows > 0) {
console.log(`更新规格图片: ${oldPath} -> ${newPath}`);
updatedCount += specResult.affectedRows;
}
} catch (error) {
console.error(`更新数据库引用失败 ${file.originalPath}:`, error.message);
}
}
console.log(`\n数据库更新完成,共更新 ${updatedCount} 条记录`);
}
/**
* 执行迁移
*/
async migrate() {
try {
console.log('开始文件迁移到 MinIO...');
console.log(`源目录: ${this.uploadsDir}`);
// 初始化数据库连接
console.log('初始化数据库连接...');
await initDB();
console.log('数据库连接初始化完成');
// 检查源目录是否存在
if (!fs.existsSync(this.uploadsDir)) {
console.log('uploads 目录不存在,无需迁移');
return;
}
// 获取所有文件
const allFiles = this.getAllFiles(this.uploadsDir);
console.log(`找到 ${allFiles.length} 个文件需要迁移`);
if (allFiles.length === 0) {
console.log('没有文件需要迁移');
return;
}
// 逐个上传文件
for (let i = 0; i < allFiles.length; i++) {
const file = allFiles[i];
console.log(`\n进度: ${i + 1}/${allFiles.length}`);
const result = await this.uploadFileToMinio(file);
if (result.success) {
this.migratedFiles.push(result);
} else {
this.failedFiles.push(result);
}
// 添加小延迟避免过快请求
await new Promise(resolve => setTimeout(resolve, 100));
}
// 输出迁移结果
console.log('\n=== 迁移结果 ===');
console.log(`成功迁移: ${this.migratedFiles.length} 个文件`);
console.log(`迁移失败: ${this.failedFiles.length} 个文件`);
if (this.failedFiles.length > 0) {
console.log('\n失败的文件:');
this.failedFiles.forEach(file => {
console.log(`- ${file.originalPath}: ${file.error}`);
});
}
// 更新数据库引用,将 /uploads/xxx 路径更新为 /xxx
if (this.migratedFiles.length > 0) {
await this.updateDatabaseReferences(this.migratedFiles);
}
// 生成迁移报告
await this.generateMigrationReport();
console.log('\n迁移完成');
console.log('\n注意事项:');
console.log('1. 请验证文件访问是否正常');
console.log('2. 确认数据库中的文件路径已正确更新');
console.log('3. 测试完成后可以删除本地 uploads 目录');
console.log('4. 查看 migration-report.json 了解详细迁移信息');
} catch (error) {
console.error('迁移过程中发生错误:', error);
}
}
/**
* 生成迁移报告
*/
async generateMigrationReport() {
const report = {
migrationDate: new Date().toISOString(),
totalFiles: this.migratedFiles.length + this.failedFiles.length,
successCount: this.migratedFiles.length,
failedCount: this.failedFiles.length,
migratedFiles: this.migratedFiles,
failedFiles: this.failedFiles
};
const reportPath = path.join(__dirname, 'migration-report.json');
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(`\n迁移报告已生成: ${reportPath}`);
}
}
// 如果直接运行此脚本
if (require.main === module) {
const migration = new FilesMigration();
migration.migrate().catch(console.error);
}
module.exports = FilesMigration;

File diff suppressed because it is too large Load Diff

View File

@@ -110,10 +110,11 @@ router.post('/login', async (req, res) => {
// 先查询用户和代理信息(包含密码用于验证)
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
`SELECT ra.*, u.id as user_id, u.username, u.phone, u.real_name, u.password, u.role, zr.name as city_name, d.name as 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
JOIN china_regions d ON d.code = u.district_id
JOIN china_regions zr ON ra.region_id = zr.code
WHERE u.phone = ? AND ra.status = "active"`,
[phone]
);
@@ -682,13 +683,14 @@ router.get('/merchants/:agent_id/transfers', async (req, res) => {
*/
router.get('/distribution', auth, async (req, res) => {
try {
const { id } = req.user;
const { page = 1, size = 10 } = req.query;
const { page = 1, size = 10,user_id } = req.query;
const { id } = user_id || req.user;
const pageNum = parseInt(page) || 1;
const limitNum = parseInt(size) || 10;
const offset = (page - 1) * size;
const [result] = await getDB().execute(
`SELECT real_name,phone,username,avatar,created_at FROM users WHERE inviter = ? ORDER BY created_at DESC
`SELECT real_name,phone,username,avatar,created_at,id as user_id FROM users WHERE inviter = ? ORDER BY created_at DESC
LIMIT ${size} OFFSET ${offset}`,
[parseInt(id)]
);
@@ -715,5 +717,4 @@ router.get('/distribution', auth, async (req, res) => {
}
});
module.exports = router;

View File

@@ -20,7 +20,7 @@ const db = {
// 获取代理列表和统计信息
router.get('/', authenticateAdmin, async (req, res) => {
try {
const { page = 1, limit = 20, status, city, search } = req.query;
const { page = 1, limit = 20, status, city, search,district } = req.query;
const pageNum = parseInt(page) || 1;
const limitNum = parseInt(limit) || 20;
const offset = (pageNum - 1) * limitNum;
@@ -34,13 +34,13 @@ router.get('/', authenticateAdmin, async (req, res) => {
queryParams.push(status);
}
if (city) {
whereConditions.push('zr.city_name = ?');
queryParams.push(city);
if (district) {
whereConditions.push('zr.name = ?');
queryParams.push(district);
}
if (search) {
whereConditions.push('(ra.real_name LIKE ? OR ra.phone LIKE ?)');
whereConditions.push('(u.real_name LIKE ? OR u.phone LIKE ?)');
queryParams.push(`%${search}%`, `%${search}%`);
}
@@ -48,37 +48,33 @@ router.get('/', authenticateAdmin, async (req, res) => {
// 查询代理列表
const agentsQuery = `
SELECT
ra.*,
u.real_name,
u.phone,
u.id_card,
zr.city_name,
zr.district_name,
(
SELECT COUNT(DISTINCT merchant_id)
FROM agent_merchants
WHERE agent_id = ra.id
) as merchant_count,
(
SELECT CAST(COALESCE(SUM(commission_amount), 0) AS DECIMAL(10,2))
FROM agent_commission_records
WHERE agent_id = ra.id
) as total_commission,
0 as paid_commission,
(
SELECT CAST(COALESCE(SUM(commission_amount), 0) AS DECIMAL(10,2))
FROM agent_commission_records
WHERE agent_id = ra.id
) as pending_commission
FROM regional_agents ra
LEFT JOIN users u ON ra.user_id = u.id
LEFT JOIN zhejiang_regions zr ON ra.region_id = zr.id
${whereClause}
ORDER BY ra.created_at DESC
LIMIT ${limitNum} OFFSET ${offset}
`;
SELECT ra.*,
u.real_name,
u.phone,
u.id_card,
province.name AS province_name,
city.name AS city_name,
zr.name AS district_name,
(SELECT COUNT(DISTINCT merchant_id)
FROM agent_merchants
WHERE agent_id = ra.id) as merchant_count,
(SELECT CAST(COALESCE(SUM(commission_amount), 0) AS DECIMAL(10, 2))
FROM agent_commission_records
WHERE agent_id = ra.id) as total_commission,
0 as paid_commission,
(SELECT CAST(COALESCE(SUM(commission_amount), 0) AS DECIMAL(10, 2))
FROM agent_commission_records
WHERE agent_id = ra.id) as pending_commission
FROM regional_agents ra
LEFT JOIN users u ON ra.user_id = u.id
LEFT JOIN china_regions zr ON ra.region_id = zr.code -- 区
LEFT JOIN china_regions city ON zr.parent_code = city.code -- 市
LEFT JOIN china_regions province ON city.parent_code = province.code -- 省
${whereClause}
ORDER BY ra.created_at
DESC
LIMIT ${limitNum} OFFSET ${offset};`;
console.log(agentsQuery,queryParams)
const agents = await db.query(agentsQuery, queryParams);
// 查询总数
@@ -86,7 +82,7 @@ router.get('/', authenticateAdmin, async (req, res) => {
SELECT COUNT(DISTINCT ra.id) as total
FROM regional_agents ra
LEFT JOIN users u ON ra.user_id = u.id
LEFT JOIN zhejiang_regions zr ON ra.region_id = zr.id
LEFT JOIN china_regions zr ON ra.region_id = zr.code
${whereClause}
`;
@@ -377,7 +373,6 @@ router.get('/:id/merchants', authenticateAdmin, async (req, res) => {
if (!agentResult || agentResult.length === 0) {
return res.status(404).json({ success: false, message: '代理不存在' });
}
const agent = agentResult[0];
// 查询代理的商户列表
const merchantsQuery = `

View File

@@ -58,11 +58,9 @@ router.get('/', auth, async (req, res) => {
const [announcements] = await db.execute(query, [req.user.id, ...params]);
// 自动将过期公告标记为已读
console.log(announcements);
const expiredUnreadAnnouncements = announcements.filter(a => a.is_expired && !a.is_read);
console.log(expiredUnreadAnnouncements);
if (expiredUnreadAnnouncements.length > 0) {
const expiredIds = expiredUnreadAnnouncements.map(a => a.id);

View File

@@ -1,331 +1,314 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { getDB } = require('../database');
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,
city,
district_id: district,
province,
inviter = '',
captchaId,
captchaText,
smsCode, // 短信验证码
role = 'user'
} = req.body;
if (!username || !phone || !password || !city || !district || !province) {
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: '短信验证码不能为空' });
}
// 验证短信验证码
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 [existingUsers] = await db.execute(
'SELECT id, payment_status 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, payment_status, province, inviter) VALUES (?, ?, ?, ?, ?, ?, ?, ?, "unpaid", ?, ?)',
[username, phone, hashedPassword, role, 0, 'pending', city, district, province, inviter]
);
const userId = result.insertId;
// 根据地区自动关联代理
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,
paymentStatus: 'unpaid'
},
needPayment: true
});
} catch (error) {
try {
// await getDB().query('ROLLBACK');
} catch (rollbackError) {
console.error('回滚错误:', rollbackError);
const db = getDB();
await db.query('START TRANSACTION');
const {
username,
phone,
password,
city,
district_id: district,
province,
inviter = null,
captchaId,
captchaText,
smsCode, // 短信验证码
role = 'user'
} = req.body;
if (!username || !phone || !password || !city || !district || !province) {
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: '短信验证码不能为空'});
}
// 验证短信验证码
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 [existingUsers] = await db.execute(
'SELECT id, payment_status 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, payment_status, province, inviter) VALUES (?, ?, ?, ?, ?, ?, ?, ?, "unpaid", ?, ?)',
[username, phone, hashedPassword, role, 0, 'pending', city, district, province, inviter]
);
const userId = result.insertId;
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,
paymentStatus: 'unpaid'
},
needPayment: true
});
} 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
});
}
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;
try {
const db = getDB();
const {username, password, captchaId, captchaText} = req.body;
if (!username || !password) {
return res.status(400).json({ success: false, message: '用户名和密码不能为空' });
if (!username || !password) {
return res.status(400).json({success: false, message: '用户名和密码不能为空'});
}
if (!captchaId || !captchaText) {
return res.status(400).json({success: false, message: '验证码不能为空'});
}
// 获取存储的验证码
const storedCaptcha = global.captchaStore.get(captchaId);
console.log(storedCaptcha);
if (!storedCaptcha) {
return res.status(400).json({
success: false,
message: '验证码不存在或已过期'
});
}
// 检查是否过期
if (Date.now() > storedCaptcha.expires) {
global.captchaStore.delete(captchaId);
return res.status(400).json({
success: false,
message: '验证码已过期'
});
}
// 验证验证码(不区分大小写)
const isValid = storedCaptcha.text === captchaText.toLowerCase();
// 删除已验证的验证码
global.captchaStore.delete(captchaId);
if (!isValid) {
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.payment_status === 'unpaid') {
const token = jwt.sign(
{userId: user.id, username: user.username, role: user.role},
JWT_SECRET,
{expiresIn: '5m'}
);
return res.status(200).json({
success: false,
message: '您的账户尚未激活,请完成支付后再登录',
needPayment: true,
user: user[0],
token
});
}
// 检查用户审核状态(管理员除外,只阻止被拒绝的用户)
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'}
);
const [is_distribution] = await db.execute(`
SELECT *
FROM distribution
WHERE user_id = ?`, [user.id]);
user.distribution = is_distribution.length > 0 ? true : false;
res.json({
success: true,
message: '登录成功',
token,
user
});
} catch (error) {
console.error('登录错误:', error);
res.status(500).json({success: false, message: '登录失败'});
}
if (!captchaId || !captchaText) {
return res.status(400).json({ success: false, message: '验证码不能为空' });
}
// 获取存储的验证码
const storedCaptcha = global.captchaStore.get(captchaId);
console.log(storedCaptcha);
if (!storedCaptcha) {
return res.status(400).json({
success: false,
message: '验证码不存在或已过期'
});
}
// 检查是否过期
if (Date.now() > storedCaptcha.expires) {
global.captchaStore.delete(captchaId);
return res.status(400).json({
success: false,
message: '验证码已过期'
});
}
// 验证验证码(不区分大小写)
const isValid = storedCaptcha.text === captchaText.toLowerCase();
// 删除已验证的验证码
global.captchaStore.delete(captchaId);
if (!isValid) {
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.payment_status === 'unpaid') {
const token = jwt.sign(
{ userId: user.id, username: user.username, role: user.role },
JWT_SECRET,
{ expiresIn: '5m' }
);
return res.status(200).json({
success: false,
message: '您的账户尚未激活,请完成支付后再登录',
needPayment: true,
user: user[0],
token
});
}
// 检查用户审核状态(管理员除外,只阻止被拒绝的用户)
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,
payment_status: user.payment_status
}
});
} 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];
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: '访问令牌无效' });
if (!token) {
return res.status(401).json({success: false, message: '访问令牌缺失'});
}
req.user = user;
next();
});
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]
);
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: '用户不存在' });
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: '获取用户信息失败'});
}
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;
try {
const db = getDB();
const {currentPassword, newPassword} = req.body;
if (!currentPassword || !newPassword) {
return res.status(400).json({ success: false, message: '旧密码和新密码不能为空' });
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: '修改密码失败'});
}
// 获取用户当前密码
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;

View File

@@ -119,7 +119,6 @@ router.get('/generate', (req, res) => {
// 生成SVG图片
const svgImage = generateCaptchaSVG(captchaText);
res.json({
success: true,
data: {

View File

@@ -1,6 +1,6 @@
const express = require('express')
const router = express.Router()
const { getDB } = require('../database')
const {getDB} = require('../database')
/**
* @swagger
@@ -70,28 +70,28 @@ const { getDB } = require('../database')
* description: 服务器错误
*/
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
`
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)
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: '获取地区数据失败'
})
}
res.json({
success: true,
data: rows,
message: '获取地区数据成功'
})
} catch (error) {
console.error('获取浙江省地区数据失败:', error)
res.status(500).json({
success: false,
message: '获取地区数据失败'
})
}
})
/**
@@ -119,56 +119,64 @@ router.get('/zhejiang', async (req, res) => {
* description: 服务器错误
*/
router.get('/provinces', async (req, res) => {
try {
// 一次性获取所有区域数据(省、市、区县)
const [allRegions] = await getDB().execute(
`SELECT code, name as label, level, parent_code FROM china_regions
WHERE level <= 3
ORDER BY level, code`
);
try {
// 按level分组数据
const regionsByLevel = {
1: [], // 省份
2: [], // 城市
3: [] // 区县
};
if (!global.provinces) {
// 一次性获取所有区域数据(省、市、区县)
const [allRegions] = await getDB().execute(
`SELECT code, name as label, level, parent_code
FROM china_regions
WHERE level <= 3
ORDER BY level, code`
);
// 按level分组数据
const regionsByLevel = {
1: [], // 省份
2: [], // 城市
3: [] // 区县
};
// 创建code到region的映射便于快速查找
const regionMap = {};
// 创建code到region的映射便于快速查找
const regionMap = {};
// 分组并建立映射
allRegions.forEach(region => {
region.children = []; // 初始化children数组
regionsByLevel[region.level].push(region);
regionMap[region.code] = region;
});
// 分组并建立映射
allRegions.forEach(region => {
region.children = []; // 初始化children数组
regionsByLevel[region.level].push(region);
regionMap[region.code] = region;
});
// 构建层级关系:先处理区县到城市的关系
regionsByLevel[3].forEach(district => {
const parentCity = regionMap[district.parent_code];
if (parentCity) {
parentCity.children.push(district);
}
});
// 构建层级关系:先处理区县到城市的关系
regionsByLevel[3].forEach(district => {
const parentCity = regionMap[district.parent_code];
if (parentCity) {
parentCity.children.push(district);
}
});
// 再处理城市到省份的关系
regionsByLevel[2].forEach(city => {
const parentProvince = regionMap[city.parent_code];
if (parentProvince) {
parentProvince.children.push(city);
}
});
// 再处理城市到省份的关系
regionsByLevel[2].forEach(city => {
const parentProvince = regionMap[city.parent_code];
if (parentProvince) {
parentProvince.children.push(city);
}
});
global.provinces = regionsByLevel[1];
}else {
console.log('1111')
regionsByLevel[1] = global.provinces;
}
// 返回省份数据(已包含完整的层级结构)
res.json({
success: true,
data: regionsByLevel[1]
});
} catch (error) {
console.error('获取省份列表错误:', error);
res.status(500).json({ message: '获取省份列表失败' });
}
// 返回省份数据(已包含完整的层级结构)
res.json({
success: true,
data: regionsByLevel[1]
});
} catch (error) {
console.error('获取省份列表错误:', error);
res.status(500).json({message: '获取省份列表失败'});
}
});
/**
@@ -203,24 +211,26 @@ router.get('/provinces', async (req, res) => {
* description: 服务器错误
*/
router.get('/cities/:provinceCode', async (req, res) => {
try {
const provinceCode = req.params.provinceCode;
try {
const provinceCode = req.params.provinceCode;
const [cities] = await getDB().execute(
`SELECT code, name FROM china_regions
WHERE level = 2 AND parent_code = ?
ORDER BY code`,
[provinceCode]
);
const [cities] = await getDB().execute(
`SELECT code, name
FROM china_regions
WHERE level = 2
AND parent_code = ?
ORDER BY code`,
[provinceCode]
);
res.json({
success: true,
data: cities
});
} catch (error) {
console.error('获取城市列表错误:', error);
res.status(500).json({ message: '获取城市列表失败' });
}
res.json({
success: true,
data: cities
});
} catch (error) {
console.error('获取城市列表错误:', error);
res.status(500).json({message: '获取城市列表失败'});
}
});
/**
@@ -255,24 +265,26 @@ router.get('/cities/:provinceCode', async (req, res) => {
* description: 服务器错误
*/
router.get('/districts/:cityCode', async (req, res) => {
try {
const cityCode = req.params.cityCode;
try {
const cityCode = req.params.cityCode;
const [districts] = await getDB().execute(
`SELECT code, name FROM china_regions
WHERE level = 3 AND parent_code = ?
ORDER BY code`,
[cityCode]
);
const [districts] = await getDB().execute(
`SELECT code, name
FROM china_regions
WHERE level = 3
AND parent_code = ?
ORDER BY code`,
[cityCode]
);
res.json({
success: true,
data: districts
});
} catch (error) {
console.error('获取区县列表错误:', error);
res.status(500).json({ message: '获取区县列表失败' });
}
res.json({
success: true,
data: districts
});
} catch (error) {
console.error('获取区县列表错误:', error);
res.status(500).json({message: '获取区县列表失败'});
}
});
/**
@@ -314,110 +326,111 @@ router.get('/districts/:cityCode', async (req, res) => {
* description: 服务器错误
*/
router.get('/path/:regionCode', async (req, res) => {
try {
const regionCode = req.params.regionCode;
try {
const regionCode = req.params.regionCode;
// 获取当前区域信息
const [currentRegion] = await getDB().execute(
'SELECT code, name, level, parent_code FROM china_regions WHERE code = ?',
[regionCode]
);
// 获取当前区域信息
const [currentRegion] = await getDB().execute(
'SELECT code, name, level, parent_code FROM china_regions WHERE code = ?',
[regionCode]
);
if (currentRegion.length === 0) {
return res.status(404).json({ message: '区域不存在' });
if (currentRegion.length === 0) {
return res.status(404).json({message: '区域不存在'});
}
const region = currentRegion[0];
const path = [region];
// 递归获取父级区域
let parentCode = region.parent_code;
while (parentCode) {
const [parentRegion] = await getDB().execute(
'SELECT code, name, level, parent_code FROM china_regions WHERE code = ? AND status = "active"',
[parentCode]
);
if (parentRegion.length > 0) {
path.unshift(parentRegion[0]);
parentCode = parentRegion[0].parent_code;
} else {
break;
}
}
res.json({
success: true,
data: {
path,
province: path.find(r => r.level === 1) || null,
city: path.find(r => r.level === 2) || null,
district: path.find(r => r.level === 3) || null
}
});
} catch (error) {
console.error('获取区域路径错误:', error);
res.status(500).json({message: '获取区域路径失败'});
}
const region = currentRegion[0];
const path = [region];
// 递归获取父级区域
let parentCode = region.parent_code;
while (parentCode) {
const [parentRegion] = await getDB().execute(
'SELECT code, name, level, parent_code FROM china_regions WHERE code = ? AND status = "active"',
[parentCode]
);
if (parentRegion.length > 0) {
path.unshift(parentRegion[0]);
parentCode = parentRegion[0].parent_code;
} else {
break;
}
}
res.json({
success: true,
data: {
path,
province: path.find(r => r.level === 1) || null,
city: path.find(r => r.level === 2) || null,
district: path.find(r => r.level === 3) || null
}
});
} catch (error) {
console.error('获取区域路径错误:', error);
res.status(500).json({ message: '获取区域路径失败' });
}
});
// 搜索区域(支持模糊搜索)
router.get('/search', async (req, res) => {
try {
const { keyword, level } = req.query;
try {
const {keyword, level} = req.query;
if (!keyword || keyword.trim() === '') {
return res.status(400).json({ message: '搜索关键词不能为空' });
}
let sql = `SELECT code, name, level, parent_code FROM china_regions
WHERE name LIKE ?`;
const params = [`%${keyword.trim()}%`];
if (level) {
sql += ' AND level = ?';
params.push(parseInt(level));
}
sql += ' ORDER BY level, code LIMIT 50';
const [regions] = await getDB().execute(sql, params);
// 为每个搜索结果获取完整路径
const results = [];
for (const region of regions) {
const path = [region];
let parentCode = region.parent_code;
while (parentCode) {
const [parentRegion] = await getDB().execute(
'SELECT code, name, level, parent_code FROM china_regions WHERE code = ? AND status = "active"',
[parentCode]
);
if (parentRegion.length > 0) {
path.unshift(parentRegion[0]);
parentCode = parentRegion[0].parent_code;
} else {
break;
if (!keyword || keyword.trim() === '') {
return res.status(400).json({message: '搜索关键词不能为空'});
}
}
results.push({
...region,
path,
fullName: path.map(r => r.name).join(' - ')
});
let sql = `SELECT code, name, level, parent_code
FROM china_regions
WHERE name LIKE ?`;
const params = [`%${keyword.trim()}%`];
if (level) {
sql += ' AND level = ?';
params.push(parseInt(level));
}
sql += ' ORDER BY level, code LIMIT 50';
const [regions] = await getDB().execute(sql, params);
// 为每个搜索结果获取完整路径
const results = [];
for (const region of regions) {
const path = [region];
let parentCode = region.parent_code;
while (parentCode) {
const [parentRegion] = await getDB().execute(
'SELECT code, name, level, parent_code FROM china_regions WHERE code = ? AND status = "active"',
[parentCode]
);
if (parentRegion.length > 0) {
path.unshift(parentRegion[0]);
parentCode = parentRegion[0].parent_code;
} else {
break;
}
}
results.push({
...region,
path,
fullName: path.map(r => r.name).join(' - ')
});
}
res.json({
success: true,
data: results
});
} catch (error) {
console.error('搜索区域错误:', error);
res.status(500).json({message: '搜索区域失败'});
}
res.json({
success: true,
data: results
});
} catch (error) {
console.error('搜索区域错误:', error);
res.status(500).json({ message: '搜索区域失败' });
}
});
module.exports = router

View File

@@ -141,12 +141,6 @@ router.post('/send', async (req, res) => {
// 生产环境发送真实短信
try {
console.log(code);
res.json({
success: true,
message: '验证码发送成功'
})
return
const sendSmsRequest = new Dysmsapi20170525.SendSmsRequest({
phoneNumbers: phone,
signName: SMS_CONFIG.signName,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,87 +0,0 @@
const mysql = require('mysql2/promise');
const fs = require('fs');
const path = require('path');
require('dotenv').config();
async function runMigration() {
let connection;
try {
// 创建数据库连接
connection = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
charset: 'utf8mb4'
});
console.log('数据库连接成功');
// 直接定义SQL语句
const sqlStatements = [
`CREATE TABLE IF NOT EXISTS \`payment_orders\` (
\`id\` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
\`user_id\` int(11) NOT NULL COMMENT '用户ID',
\`out_trade_no\` varchar(64) NOT NULL COMMENT '商户订单号',
\`transaction_id\` varchar(64) DEFAULT NULL COMMENT '微信支付订单号',
\`total_fee\` int(11) NOT NULL COMMENT '订单金额(分)',
\`body\` varchar(128) NOT NULL COMMENT '商品描述',
\`trade_type\` varchar(16) NOT NULL COMMENT '交易类型',
\`prepay_id\` varchar(64) DEFAULT NULL COMMENT '预支付交易会话标识',
\`mweb_url\` text DEFAULT NULL COMMENT 'H5支付跳转链接',
\`status\` enum('pending','paid','failed','cancelled') NOT NULL DEFAULT 'pending' COMMENT '支付状态',
\`paid_at\` datetime DEFAULT NULL COMMENT '支付完成时间',
\`created_at\` datetime NOT NULL COMMENT '创建时间',
\`updated_at\` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (\`id\`),
UNIQUE KEY \`uk_out_trade_no\` (\`out_trade_no\`),
KEY \`idx_user_id\` (\`user_id\`),
KEY \`idx_status\` (\`status\`),
KEY \`idx_created_at\` (\`created_at\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付订单表'`,
`ALTER TABLE \`users\` ADD COLUMN \`payment_status\` enum('unpaid','paid') NOT NULL DEFAULT 'unpaid' COMMENT '支付状态'`,
`ALTER TABLE \`users\` ADD KEY \`idx_payment_status\` (\`payment_status\`)`
];
console.log(`准备执行 ${sqlStatements.length} 条SQL语句`);
// 执行每条SQL语句
for (let i = 0; i < sqlStatements.length; i++) {
const statement = sqlStatements[i];
console.log(`执行第 ${i + 1} 条语句...`);
try {
await connection.execute(statement);
console.log(`✓ 第 ${i + 1} 条语句执行成功`);
} catch (error) {
if (error.code === 'ER_DUP_FIELDNAME') {
console.log(`⚠ 第 ${i + 1} 条语句跳过(字段已存在): ${error.message}`);
} else if (error.code === 'ER_TABLE_EXISTS_ERROR') {
console.log(`⚠ 第 ${i + 1} 条语句跳过(表已存在): ${error.message}`);
} else if (error.code === 'ER_DUP_KEYNAME') {
console.log(`⚠ 第 ${i + 1} 条语句跳过(索引已存在): ${error.message}`);
} else {
console.error(`✗ 第 ${i + 1} 条语句执行失败:`, error.message);
throw error;
}
}
}
console.log('\n✅ 数据库迁移完成!');
} catch (error) {
console.error('❌ 数据库迁移失败:', error.message);
process.exit(1);
} finally {
if (connection) {
await connection.end();
console.log('数据库连接已关闭');
}
}
}
// 运行迁移
runMigration();

View File

@@ -5,7 +5,6 @@ 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');
@@ -13,9 +12,6 @@ const { logger } = require('./config/logger');
const { errorHandler, notFound } = require('./middleware/errorHandler');
const fs = require('fs');
// Swagger文档相关
const swaggerUi = require('swagger-ui-express');
const swaggerSpecs = require('./swagger');
const app = express();
const PORT = process.env.PORT || 3000;
@@ -223,8 +219,6 @@ app.use(express.static(path.join(__dirname, 'frontend/dist'), {
// 引入数据库初始化模块
const { initDatabase } = require('./config/database-init');
// Swagger API文档
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs, { explorer: true }));
// API路由
app.use('/api/auth', require('./routes/auth'));

View File

@@ -25,24 +25,8 @@ class AlipayService {
console.log('支付宝私钥路径:', privateKeyPath);
console.log('支付宝公钥路径:', publicKeyPath);
// 验证文件有效性
if (!this.isValidFile(privateKeyPath)) {
throw new Error(`支付宝私钥文件无效或不存在: ${privateKeyPath}`);
}
if (!this.isValidFile(publicKeyPath)) {
throw new Error(`支付宝公钥文件无效或不存在: ${publicKeyPath}`);
}
console.log('尝试加载支付宝私钥文件:', privateKeyPath);
this.privateKey = fs.readFileSync(privateKeyPath, 'utf8');
console.log('支付宝私钥加载成功');
console.log('尝试加载支付宝公钥文件:', publicKeyPath);
this.alipayPublicKey = fs.readFileSync(publicKeyPath, 'utf8');
console.log('支付宝公钥加载成功');
this.initializeSDK();
} catch (error) {
@@ -149,14 +133,14 @@ class AlipayService {
subject: subject,
body: body,
product_code: 'QUICK_WAP_WAY',
quit_url: process.env.ALIPAY_QUIT_URL || 'https://your-domain.com/payment/cancel'
quit_url: process.env.ALIPAY_QUIT_URL
};
// 使用新版SDK的pageExecute方法生成支付URL
const payUrl = this.alipaySdk.pageExecute('alipay.trade.wap.pay', 'GET', {
bizContent: bizContent,
notifyUrl: process.env.ALIPAY_NOTIFY_URL || 'https://your-domain.com/api/payment/alipay/notify',
returnUrl: process.env.ALIPAY_RETURN_URL || 'https://your-domain.com/payment/success'
notifyUrl: process.env.ALIPAY_NOTIFY_URL,
returnUrl: process.env.ALIPAY_RETURN_URL
});
// 保存订单到数据库

View File

@@ -724,6 +724,7 @@ class MatchingService {
AND u.audit_status = 'approved'
AND u.user_type != 'directly_operated'
AND u.payment_status = 'paid'
AND u.province = ?
ORDER BY
CASE
WHEN u.city = ? AND u.district_id = ? THEN 1 -- 相同城市且相同区县排第一
@@ -732,7 +733,7 @@ class MatchingService {
ELSE 4 -- 其他省份排第四
END,
u.balance ASC`,
[excludeUserId, currentUserCity, currentUserDistrictId, currentUserCity, currentUserProvince]
[excludeUserId,currentUserProvince, currentUserCity, currentUserDistrictId, currentUserCity, currentUserProvince]
);
// 处理查询到的负余额用户
@@ -795,7 +796,23 @@ class MatchingService {
// 所有查询到的用户都是负余额用户,直接添加到可用列表
}
userBalanceResult = userBalanceResult.filter(user => user.has_active_allocations < -100);
userBalanceResult = userBalanceResult.sort((a, b) => a.has_active_allocations - b.has_active_allocations);
userBalanceResult = userBalanceResult.sort((a, b) => {
const getPriority = (user) => {
if (user.district_id === currentUserDistrictId) return 1; // 同区县
if (user.city === currentUserCity) return 2; // 同城市
return 3; // 其他
};
const priorityA = getPriority(a);
const priorityB = getPriority(b);
if (priorityA !== priorityB) {
return priorityA - priorityB; // 优先级小的排前
}
// 同优先级里越接近0数值越大排前 -> 使用降序
return b.has_active_allocations - a.has_active_allocations;
});
for (const user of userBalanceResult) {
if (maxTransfers > availableUsers.length + 1) {
if (minTransfers === 3 && availableUsers.length < 3) {

View File

@@ -163,7 +163,8 @@ class TransferService {
if (transfer.matching_order_id) {
// 查询该匹配订单下所有transfers的状态
const [allTransfers] = await connection.execute(
`SELECT status FROM transfers
`SELECT status
FROM transfers
WHERE matching_order_id = ?`,
[transfer.matching_order_id]
);
@@ -172,7 +173,7 @@ class TransferService {
// 根据所有相关transfers的状态来决定matching_order的状态
const transferStatuses = allTransfers.map(t => t.status);
console.log(transferStatuses,'transferStatuses');
console.log(transferStatuses, 'transferStatuses');
if (transferStatuses.every(status => status === 'cancelled' || status === 'rejected' || status === 'not_received')) {
// 如果所有transfers都被取消/拒绝/未收到,匹配订单标记为已完成
@@ -190,7 +191,7 @@ class TransferService {
await connection.execute(
`UPDATE matching_orders
SET status = ?,
SET status = ?,
updated_at = NOW()
WHERE id = ?`,
[matchingOrderStatus, transfer.matching_order_id]
@@ -238,16 +239,16 @@ class TransferService {
}
// 获取转账列表
async getTransfers(filters = {}, pagination = {},user_type = 'user_to_user') {
async getTransfers(filters = {}, pagination = {}, user_type = 'user_to_user') {
const db = getDB();
const {page = 1, limit = 10, sort = 'created_at', order = 'desc'} = pagination;
const pageNum = parseInt(page, 10) || 1;
const limitNum = parseInt(limit, 10) || 10;
const offset = (pageNum - 1) * limitNum;
let whereClause = 'WHERE 1=1 ';
let whereClause = 'WHERE ';
const params = [];
whereClause += `AND transfer_type='${user_type}'`;
whereClause += `transfer_type='${user_type}'`;
// 构建查询条件
if (filters.user_id) {
whereClause += ' AND (from_user_id = ? OR to_user_id = ?)';
@@ -298,20 +299,52 @@ class TransferService {
params
);
const total = countResult[0].total;
console.log(`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,
f_p.name as from_province,
f_c.name as from_city,
f_d.name as from_district,
t_p.name as to_province,
t_c.name as to_city,
t_d.name as to_district
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
LEFT JOIN china_regions f_p on f_p.code = fu.province
LEFT JOIN china_regions f_c on f_c.code = fu.city
LEFT JOIN china_regions f_d on f_d.code = fu.district_id
LEFT JOIN china_regions t_p on t_p.code = tu.province
LEFT JOIN china_regions t_c on t_c.code = tu.city
LEFT JOIN china_regions t_d on t_d.code = tu.district_id
${whereClause} ${orderClause}
LIMIT ${limitNum} OFFSET ${offset}`)
// 获取数据
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
tu.real_name as to_real_name,
f_p.name as from_province,
f_c.name as from_city,
f_d.name as from_district,
t_p.name as to_province,
t_c.name as to_city,
t_d.name as to_district
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
LEFT JOIN china_regions f_p ON f_p.code = fu.province
LEFT JOIN china_regions f_c ON f_c.code = fu.city
LEFT JOIN china_regions f_d ON f_d.code = fu.district_id
LEFT JOIN china_regions t_p ON t_p.code = tu.province
LEFT JOIN china_regions t_c ON t_c.code = tu.city
LEFT JOIN china_regions t_d ON t_d.code = tu.district_id
${whereClause} ${orderClause}
LIMIT ${limitNum}
OFFSET ${offset}`,
LIMIT ${limitNum}`,
params
);
@@ -329,7 +362,8 @@ class TransferService {
throw error;
}
}
async getTransfersHistory(filters = {}, pagination = {},user_type = 'user_to_user') {
async getTransfersHistory(filters = {}, pagination = {}, user_type = 'user_to_user') {
const db = getDB();
const {page = 1, limit = 10, sort = 'created_at', order = 'desc'} = pagination;
const pageNum = parseInt(page, 10) || 1;
@@ -401,13 +435,29 @@ class TransferService {
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}`,
LIMIT ${limitNum} OFFSET ${offset}`,
params
);
//获取总数
const stats = {};
//获取系统转给融豆的总数
let [total_to_admin] = await db.execute(`SELECT SUM(t.amount) as total FROM transfers t WHERE t.source_type = 'system'`)
stats.total_to_admin = total_to_admin[0].total || 0
//转给代理的融豆总数
let [total_to_agent] = await db.execute(`SELECT SUM(t.amount) as total FROM transfers t WHERE t.source_type = 'agent'`)
stats.total_to_agent = total_to_agent[0].total || 0
//转给直营代理的融豆数量
let [total_to_agent_directly] = await db.execute(`SELECT SUM(t.amount) as total FROM transfers t WHERE t.source_type = 'operated_agent'`)
stats.total_to_agent_directly = total_to_agent_directly[0].total || 0
//转给直营的融豆总数
let [total_to_directly_operated] = await db.execute(`SELECT SUM(t.amount) as total FROM transfers t WHERE t.source_type = 'directly_operated'`)
stats.total_to_directly_operated = total_to_directly_operated[0].total || 0
//提现总数
let [total_get] = await db.execute(`SELECT SUM(t.amount) as total FROM transfers t WHERE t.source_type = 'withdraw'`)
stats.total_get = total_get[0].total || 0
return {
transfers,
stats,
pagination: {
page: pageNum,
limit: limitNum,
@@ -722,7 +772,7 @@ class TransferService {
// 更新转账状态
await connection.execute(
'UPDATE transfers SET status = ? WHERE id = ?',
[TRANSFER_STATUS.REJECTED, transferId]
[TRANSFER_STATUS.REJECTED, transferId]
);
// 注意在新逻辑下CONFIRMED状态时发送方余额还没有被扣除所以无需回滚
@@ -735,11 +785,13 @@ class TransferService {
});
// 如果是分配类型的转账需要更新对应的matching_order状态
if ( transfer.matching_order_id) {
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'`,
`SELECT status
FROM transfers
WHERE matching_order_id = ?
AND source_type = 'allocation'`,
[transfer.matching_order_id]
);
@@ -782,7 +834,8 @@ class TransferService {
// 更新matching_order状态
await connection.execute(
`UPDATE matching_orders
SET status = ?, updated_at = NOW()
SET status = ?,
updated_at = NOW()
WHERE id = ?`,
[matchingOrderStatus, transfer.matching_order_id]
);
@@ -897,7 +950,8 @@ class TransferService {
if (transfer.matching_order_id) {
// 查询该匹配订单下所有transfers的状态
const [allTransfers] = await connection.execute(
`SELECT status FROM transfers
`SELECT status
FROM transfers
WHERE matching_order_id = ?`,
[transfer.matching_order_id]
);
@@ -906,26 +960,26 @@ class TransferService {
// 根据所有相关transfers的状态来决定matching_order的状态
const transferStatuses = allTransfers.map(t => t.status);
console.log(transferStatuses,'transferStatuses');
console.log(transferStatuses, 'transferStatuses');
if (transferStatuses.every(status => status === 'cancelled' || status === 'rejected' || status === 'not_received' || status === 'confirmed' || status === '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') ) {
// 如果有任何一个transfer被取消/拒绝/未收到或者有transfers已确认或已收到匹配订单为进行中状态
matchingOrderStatus = 'matching';
} else {
// 其他情况为待处理状态
matchingOrderStatus = 'pending';
}
// 如果所有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')) {
// 如果有任何一个transfer被取消/拒绝/未收到或者有transfers已确认或已收到匹配订单为进行中状态
matchingOrderStatus = 'matching';
} else {
// 其他情况为待处理状态
matchingOrderStatus = 'pending';
}
console.log('matchingOrderStatus', matchingOrderStatus);
await connection.execute(
`UPDATE matching_orders
SET status = ?,
SET status = ?,
updated_at = NOW()
WHERE id = ?`,
[matchingOrderStatus, transfer.matching_order_id]
@@ -998,7 +1052,7 @@ class TransferService {
if (transfer.to_user_id) {
await connection.execute(
'UPDATE users SET balance = balance - ? WHERE id = ?',
[transfer.amount, transfer.to_user_id]
[transfer.amount, transfer.to_user_id]
);
logger.info('Receiver balance and points deducted due to status change', {
@@ -1074,7 +1128,7 @@ class TransferService {
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]
[transfer.amount, transfer.to_user_id]
);
logger.info('Receiver balance and points increased due to status change', {

View File

@@ -1,345 +0,0 @@
<template>
<div class="image-upload">
<el-upload
ref="uploadRef"
:action="uploadUrl"
:headers="uploadHeaders"
:data="uploadData"
:before-upload="beforeUpload"
:on-success="handleSuccess"
:on-error="handleError"
:on-progress="handleProgress"
:show-file-list="false"
:auto-upload="true"
accept="image/*"
class="upload-container"
>
<div class="upload-area" :class="{ 'uploading': uploading, 'has-image': imageUrl }">
<div v-if="!imageUrl && !uploading" class="upload-placeholder">
<el-icon class="upload-icon"><Plus /></el-icon>
<div class="upload-text">{{ placeholder }}</div>
<div class="upload-hint">支持 JPGPNG 格式大小不超过 5MB</div>
</div>
<div v-if="uploading" class="upload-progress">
<el-icon class="loading-icon"><Loading /></el-icon>
<div class="progress-text">上传中... {{ uploadProgress }}%</div>
<el-progress :percentage="uploadProgress" :show-text="false" />
</div>
<div v-if="imageUrl && !uploading" class="image-preview">
<img :src="imageUrl" :alt="placeholder" class="preview-image" />
<div class="image-overlay">
<el-button type="primary" size="small" @click.stop="previewImage">
<el-icon><ZoomIn /></el-icon>
预览
</el-button>
<el-button type="danger" size="small" @click.stop="removeImage">
<el-icon><Delete /></el-icon>
删除
</el-button>
</div>
</div>
</div>
</el-upload>
<!-- 图片预览对话框 -->
<el-dialog v-model="previewVisible" title="图片预览" width="60%" center>
<div class="preview-dialog">
<img :src="imageUrl" :alt="placeholder" class="preview-dialog-image" />
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus, Loading, ZoomIn, Delete } from '@element-plus/icons-vue'
// Props
const props = defineProps({
modelValue: {
type: String,
default: ''
},
placeholder: {
type: String,
default: '点击上传图片'
},
disabled: {
type: Boolean,
default: false
}
})
// Emits
const emit = defineEmits(['update:modelValue', 'upload-success', 'upload-error'])
// 响应式数据
const uploadRef = ref()
const uploading = ref(false)
const uploadProgress = ref(0)
const imageUrl = ref(props.modelValue)
const previewVisible = ref(false)
// 上传配置
const uploadUrl = '/api/upload/image'
const uploadHeaders = computed(() => {
const token = localStorage.getItem('token')
return token ? { Authorization: `Bearer ${token}` } : {}
})
const uploadData = { type: 'document' }
// 监听 modelValue 变化
watch(() => props.modelValue, (newValue) => {
imageUrl.value = newValue
})
/**
* 上传前验证
*/
const beforeUpload = (file) => {
if (props.disabled) {
ElMessage.warning('当前状态下不允许上传')
return false
}
// 检查文件类型
const isImage = file.type.startsWith('image/')
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
// 检查文件大小5MB
const isLt5M = file.size / 1024 / 1024 < 5
if (!isLt5M) {
ElMessage.error('图片大小不能超过 5MB')
return false
}
uploading.value = true
uploadProgress.value = 0
return true
}
/**
* 上传进度
*/
const handleProgress = (event) => {
uploadProgress.value = Math.round(event.percent)
}
/**
* 上传成功
*/
const handleSuccess = (response) => {
uploading.value = false
uploadProgress.value = 0
if (response.success) {
imageUrl.value = response.data.url
emit('update:modelValue', response.data.url)
emit('upload-success', response.data)
ElMessage.success('图片上传成功')
} else {
ElMessage.error(response.message || '上传失败')
emit('upload-error', response)
}
}
/**
* 上传失败
*/
const handleError = (error) => {
uploading.value = false
uploadProgress.value = 0
console.error('上传失败:', error)
ElMessage.error('图片上传失败,请重试')
emit('upload-error', error)
}
/**
* 预览图片
*/
const previewImage = () => {
previewVisible.value = true
}
/**
* 删除图片
*/
const removeImage = () => {
imageUrl.value = ''
emit('update:modelValue', '')
ElMessage.success('图片已删除')
}
/**
* 手动触发上传
*/
const triggerUpload = () => {
if (!props.disabled) {
uploadRef.value?.$el.querySelector('input').click()
}
}
// 暴露方法
defineExpose({
triggerUpload
})
</script>
<style scoped>
.image-upload {
width: 100%;
}
.upload-container {
width: 100%;
}
.upload-area {
width: 200px;
height: 120px;
border: 2px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
background-color: #fafafa;
}
.upload-area:hover {
border-color: #409eff;
background-color: #f0f9ff;
}
.upload-area.uploading {
border-color: #409eff;
background-color: #f0f9ff;
cursor: not-allowed;
}
.upload-area.has-image {
border-color: #409eff;
padding: 0;
}
.upload-placeholder {
text-align: center;
color: #8c939d;
}
.upload-icon {
font-size: 28px;
color: #8c939d;
margin-bottom: 8px;
}
.upload-text {
font-size: 14px;
margin-bottom: 4px;
}
.upload-hint {
font-size: 12px;
color: #c0c4cc;
}
.upload-progress {
text-align: center;
width: 100%;
padding: 0 20px;
}
.loading-icon {
font-size: 24px;
color: #409eff;
margin-bottom: 8px;
animation: rotate 2s linear infinite;
}
.progress-text {
font-size: 14px;
color: #606266;
margin-bottom: 8px;
}
.image-preview {
width: 100%;
height: 100%;
position: relative;
}
.preview-image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.image-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
opacity: 0;
transition: opacity 0.3s ease;
}
.image-preview:hover .image-overlay {
opacity: 1;
}
.preview-dialog {
text-align: center;
}
.preview-dialog-image {
max-width: 100%;
max-height: 70vh;
object-fit: contain;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.upload-area {
width: 150px;
height: 100px;
}
.upload-icon {
font-size: 24px;
}
.upload-text {
font-size: 12px;
}
.upload-hint {
font-size: 10px;
}
}
</style>