提交
This commit is contained in:
4
.env
4
.env
@@ -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
|
||||
|
||||
146
DEPLOYMENT.md
146
DEPLOYMENT.md
@@ -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. 查看服务器错误日志
|
||||
@@ -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();
|
||||
1
certs/alipay-private-key-test.pem
Normal file
1
certs/alipay-private-key-test.pem
Normal 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
|
||||
1
certs/alipay-public-key-test.pem
Normal file
1
certs/alipay-public-key-test.pem
Normal file
@@ -0,0 +1 @@
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhptRt5n0KVACDX69KfZogfeLULSuZGkH0m3wVDI32a4eBH3TrzKjSShdO2WBF6glfvdCruuMuLmYJrIxa09koNcAF5CNc/2iHE8O8pUbrsLKgX+dpXoNZt6XVo/jHqsxmYDFjZogL3xFVW0z0pGcxaIWnVEhQ0hQ+ji2RxJ0Bb45mmOZOOVihpLf9hEFW0rHamf2Tfu+Hd4NWTb/CVZwgzchJ/cwLTqP1Ar3GeQdmB2tmaCTu1h8wyt6lkSUTOYTEf8xCdmv8xfS12dXAeh11t1a3SuqPT2b4UxuLJHHMmiKndD7BnPZIENxi7e6N5JKz1zahyuh23GeBCHs1wHtQQIDAQAB
|
||||
9
certs/alipay-public-key-y.pem
Normal file
9
certs/alipay-public-key-y.pem
Normal file
@@ -0,0 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlfXRCD4ppMMYSV18tnJ7
|
||||
TWanmXVeCQHce2aZ7241ejo3hGXM5LChWupYLGEelquhv5IRHVUcjVuEbHqQUcFR
|
||||
gwccVDdJwfbk7T7YtttD7V2SCcnC2OfOZy/4mlI1LfgsodqrMiBkNJRsfVRVlPse
|
||||
bLRgi4H2WxzmjKNEvuPbqRLF+aeDMkW3OMwP73/sYJhpuX9WAdTcJt9iQYVUaLq5
|
||||
h4YvNjC19x8cKOkf0iDqwyHFKxC2AvV0Qti1FmCSLENUDLaxP9F1RZiAevIFPSak
|
||||
UzV4Swsi9fSt1lBr0VxvTeZk2mUgSGHKO2a7W0xR7SMUjmMamEzrZDylWMRp9SNl
|
||||
hwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -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
|
||||
217
database.js
217
database.js
@@ -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
|
||||
};
|
||||
@@ -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:
|
||||
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
26659
migration-report.json
26659
migration-report.json
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
@@ -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 = `
|
||||
|
||||
@@ -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);
|
||||
|
||||
573
routes/auth.js
573
routes/auth.js
@@ -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;
|
||||
|
||||
@@ -119,7 +119,6 @@ router.get('/generate', (req, res) => {
|
||||
|
||||
// 生成SVG图片
|
||||
const svgImage = generateCaptchaSVG(captchaText);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
2007
routes/transfers.js
2007
routes/transfers.js
File diff suppressed because it is too large
Load Diff
3213
routes/users.js
3213
routes/users.js
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
@@ -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'));
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
// 保存订单到数据库
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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">支持 JPG、PNG 格式,大小不超过 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>
|
||||
Reference in New Issue
Block a user