提交
This commit is contained in:
		| @@ -5,6 +5,23 @@ const jwt = require('jsonwebtoken'); | ||||
| const { getDB } = require('../database'); | ||||
| const { logger } = require('../config/logger'); | ||||
|  | ||||
| const authenticateToken = (req, res, next) => { | ||||
|   const authHeader = req.headers['authorization']; | ||||
|   const token = authHeader && authHeader.split(' ')[1]; | ||||
|  | ||||
|   if (!token) { | ||||
|     return res.status(401).json({ success: false, message: '访问令牌缺失' }); | ||||
|   } | ||||
|  | ||||
|   jwt.verify(token, JWT_SECRET, (err, user) => { | ||||
|     if (err) { | ||||
|       return res.status(403).json({ success: false, message: '访问令牌无效' }); | ||||
|     } | ||||
|     req.user = user; | ||||
|     next(); | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| // JWT密钥 | ||||
| const JWT_SECRET = process.env.JWT_SECRET || 'agent_jwt_secret_key_2024'; | ||||
|  | ||||
| @@ -37,11 +54,13 @@ router.post('/login', async (req, res) => { | ||||
|         u.password, | ||||
|         u.real_name, | ||||
|         u.avatar, | ||||
|         zr.city_name, | ||||
|         zr.district_name | ||||
|         u.user_type, | ||||
|         c.name as city_name, | ||||
|         d.name as district_name | ||||
|       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 c ON ra.region_id = c.code | ||||
|       LEFT JOIN china_regions d ON u.district_id = d.code | ||||
|       WHERE u.phone = ? AND ra.status = 'active' | ||||
|     `, [username]); | ||||
|  | ||||
| @@ -95,6 +114,7 @@ router.post('/login', async (req, res) => { | ||||
|           phone: agent.phone, | ||||
|           realName: agent.real_name, | ||||
|           avatar: agent.avatar, | ||||
|           user_type: agent.user_type, | ||||
|           region: { | ||||
|             id: agent.region_id, | ||||
|             cityName: agent.city_name, | ||||
| @@ -223,4 +243,5 @@ router.post('/logout', (req, res) => { | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| module.exports = router; | ||||
| module.exports = router; | ||||
| module.exports.authenticateToken = authenticateToken; | ||||
| @@ -1,186 +1,190 @@ | ||||
| const express = require('express'); | ||||
| const router = express.Router(); | ||||
| const { getDB } = require('../database'); | ||||
| const { agentAuth } = require('../middleware/agentAuth'); | ||||
| const { logger } = require('../config/logger'); | ||||
| const {getDB} = require('../database'); | ||||
| const {agentAuth} = require('../middleware/agentAuth'); | ||||
| const {logger} = require('../config/logger'); | ||||
|  | ||||
| /** | ||||
|  * 获取代理下级用户转账记录列表 | ||||
|  * GET /api/transfers | ||||
|  */ | ||||
| router.get('/', agentAuth, async (req, res) => { | ||||
|   try { | ||||
|     const agentId = req.agent.id; | ||||
|     const {  | ||||
|       page = 1,  | ||||
|       limit = 20,  | ||||
|       search,  | ||||
|       status, | ||||
|       type, | ||||
|       start_date, | ||||
|       end_date, | ||||
|       min_amount, | ||||
|       max_amount, | ||||
|       sort_by = 'created_at',  | ||||
|       sort_order = 'desc' | ||||
|     } = req.query; | ||||
|     try { | ||||
|         const agentId = req.agent.id; | ||||
|         const { | ||||
|             page = 1, | ||||
|             limit = 20, | ||||
|             size = 20, | ||||
|             search, | ||||
|             status, | ||||
|             type, | ||||
|             start_date, | ||||
|             end_date, | ||||
|             min_amount, | ||||
|             max_amount, | ||||
|             sort_by = 'created_at', | ||||
|             sort_order = 'desc' | ||||
|         } = req.query; | ||||
|  | ||||
|     const pageNum = parseInt(page) || 1; | ||||
|     const limitNum = parseInt(limit) || 20; | ||||
|     const offset = (pageNum - 1) * limitNum; | ||||
|         const pageNum = parseInt(page) || 1; | ||||
|         const limitNum = parseInt(size ||limit ) || 20; | ||||
|         const offset = (pageNum - 1) * limitNum; | ||||
|  | ||||
|     // 构建查询条件 | ||||
|     let whereConditions = [ | ||||
|       '(am1.agent_id = ? OR am2.agent_id = ?)' // 转出方或转入方属于当前代理 | ||||
|     ]; | ||||
|     let queryParams = [agentId, agentId]; | ||||
|         // 构建查询条件 | ||||
|         let whereConditions = [ | ||||
|             '(am1.agent_id = ? OR am2.agent_id = ?)' // 转出方或转入方属于当前代理 | ||||
|         ]; | ||||
|         let queryParams = [agentId, agentId]; | ||||
|  | ||||
|     if (search) { | ||||
|       whereConditions.push('(u1.username LIKE ? OR u1.real_name LIKE ? OR u1.phone LIKE ? OR u2.username LIKE ? OR u2.real_name LIKE ? OR u2.phone LIKE ?)'); | ||||
|       queryParams.push(`%${search}%`, `%${search}%`, `%${search}%`, `%${search}%`, `%${search}%`, `%${search}%`); | ||||
|         if (search) { | ||||
|             whereConditions.push('(u1.username LIKE ? OR u1.real_name LIKE ? OR u1.phone LIKE ? OR u2.username LIKE ? OR u2.real_name LIKE ? OR u2.phone LIKE ?)'); | ||||
|             queryParams.push(`%${search}%`, `%${search}%`, `%${search}%`, `%${search}%`, `%${search}%`, `%${search}%`); | ||||
|         } | ||||
|  | ||||
|         if (status) { | ||||
|             whereConditions.push('t.status = ?'); | ||||
|             queryParams.push(status); | ||||
|         } | ||||
|  | ||||
|         if (type) { | ||||
|             whereConditions.push('t.source_type = ?'); | ||||
|             queryParams.push(type); | ||||
|         } | ||||
|  | ||||
|         if (start_date) { | ||||
|             whereConditions.push('DATE(t.created_at) >= ?'); | ||||
|             queryParams.push(start_date); | ||||
|         } | ||||
|  | ||||
|         if (end_date) { | ||||
|             whereConditions.push('DATE(t.created_at) <= ?'); | ||||
|             queryParams.push(end_date); | ||||
|         } | ||||
|  | ||||
|         if (min_amount) { | ||||
|             whereConditions.push('t.amount >= ?'); | ||||
|             queryParams.push(parseFloat(min_amount)); | ||||
|         } | ||||
|  | ||||
|         if (max_amount) { | ||||
|             whereConditions.push('t.amount <= ?'); | ||||
|             queryParams.push(parseFloat(max_amount)); | ||||
|         } | ||||
|  | ||||
|         const whereClause = whereConditions.join(' AND '); | ||||
|  | ||||
|         // 验证排序字段 | ||||
|         const allowedSortFields = ['created_at', 'amount', 'status']; | ||||
|         const sortBy = allowedSortFields.includes(sort_by) ? sort_by : 'created_at'; | ||||
|         const sortOrder = sort_order.toLowerCase() === 'asc' ? 'ASC' : 'DESC'; | ||||
|  | ||||
|         // 查询转账记录列表 | ||||
|         const transfersQuery = ` | ||||
|             SELECT t.id, | ||||
|                    t.from_user_id, | ||||
|                    t.to_user_id, | ||||
|                    t.amount, | ||||
|                    t.source_type, | ||||
|                    t.status, | ||||
|                    t.description, | ||||
|                    t.matching_order_id, | ||||
|                    t.created_at, | ||||
|                    t.updated_at, | ||||
|                    u1.username  as from_username, | ||||
|                    u1.real_name as from_real_name, | ||||
|                    u1.phone     as from_phone, | ||||
|                    u1.avatar    as from_avatar, | ||||
|                    u2.username  as to_username, | ||||
|                    u2.real_name as to_real_name, | ||||
|                    u2.phone     as to_phone, | ||||
|                    u2.avatar    as to_avatar, | ||||
|                    CASE | ||||
|                        WHEN am1.agent_id = ? THEN 'out' | ||||
|                        WHEN am2.agent_id = ? THEN 'in' | ||||
|                        ELSE 'both' | ||||
|                        END      as direction | ||||
|             FROM transfers t | ||||
|                      LEFT JOIN users u1 ON t.from_user_id = u1.id | ||||
|                      LEFT JOIN users u2 ON t.to_user_id = u2.id | ||||
|                      LEFT JOIN agent_merchants am1 ON t.from_user_id = am1.merchant_id | ||||
|                      LEFT JOIN agent_merchants am2 ON t.to_user_id = am2.merchant_id | ||||
|             WHERE ${whereClause} | ||||
|             ORDER BY t.${sortBy} ${sortOrder} | ||||
|       LIMIT ${limitNum} | ||||
|             OFFSET ${offset} | ||||
|         `; | ||||
|         console.log(transfersQuery, [agentId, agentId, ...queryParams]); | ||||
|         const [transfers] = await getDB().execute(transfersQuery, [agentId, agentId, ...queryParams]); | ||||
|  | ||||
|         // 查询总数 | ||||
|         const countQuery = ` | ||||
|             SELECT COUNT(*) as total | ||||
|             FROM transfers t | ||||
|                      LEFT JOIN agent_merchants am1 ON t.from_user_id = am1.merchant_id | ||||
|                      LEFT JOIN agent_merchants am2 ON t.to_user_id = am2.merchant_id | ||||
|                      LEFT JOIN users u1 ON t.from_user_id = u1.id | ||||
|                      LEFT JOIN users u2 ON t.to_user_id = u2.id | ||||
|             WHERE ${whereClause} | ||||
|         `; | ||||
|         console.log(countQuery, [agentId, agentId, ...queryParams]); | ||||
|         const [countResult] = await getDB().execute(countQuery, [...queryParams]); | ||||
|         const total = countResult[0]?.total || 0; | ||||
|  | ||||
|         // 查询统计信息 | ||||
|         let statsResult; | ||||
|         [statsResult] = await getDB().execute(` | ||||
|             SELECT COUNT(*)                                                    as total_transfers, | ||||
|                    COUNT(CASE WHEN t.status = 'completed' THEN 1 END)          as completed_transfers, | ||||
|                    COUNT(CASE WHEN t.status = 'pending' THEN 1 END)            as pending_transfers, | ||||
|                    COUNT(CASE WHEN t.status = 'failed' THEN 1 END)             as failed_transfers, | ||||
|                    CAST(COALESCE(SUM(CASE WHEN t.status = 'completed' THEN t.amount ELSE 0 END), | ||||
|                                  0) AS DECIMAL(10, 2))                         as total_amount, | ||||
|                    CAST(COALESCE( | ||||
|                            SUM(CASE WHEN t.status = 'completed' AND DATE (t.created_at) = CURDATE() THEN t.amount ELSE 0 | ||||
|                                END), 0) AS DECIMAL(10, 2))                     as today_amount, | ||||
|                    COUNT(CASE WHEN DATE (t.created_at) = CURDATE() THEN 1 END) as today_transfers | ||||
|             FROM transfers t | ||||
|                      LEFT JOIN agent_merchants am1 ON t.from_user_id = am1.merchant_id | ||||
|                      LEFT JOIN agent_merchants am2 ON t.to_user_id = am2.merchant_id | ||||
|             WHERE (am1.agent_id = ? OR am2.agent_id = ?) | ||||
|         `, [agentId, agentId]); | ||||
|  | ||||
|         const stats = statsResult[0] || { | ||||
|             total_transfers: 0, | ||||
|             completed_transfers: 0, | ||||
|             pending_transfers: 0, | ||||
|             failed_transfers: 0, | ||||
|             total_amount: '0.00', | ||||
|             today_amount: '0.00', | ||||
|             today_transfers: 0 | ||||
|         }; | ||||
|  | ||||
|         res.json({ | ||||
|             success: true, | ||||
|             data: { | ||||
|                 transfers, | ||||
|                 pagination: { | ||||
|                     current_page: pageNum, | ||||
|                     per_page: limitNum, | ||||
|                     total, | ||||
|                     total_pages: Math.ceil(total / limitNum) | ||||
|                 }, | ||||
|                 stats | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|     } catch (error) { | ||||
|         logger.error('获取转账记录失败', { | ||||
|             error: error.message, | ||||
|             stack: error.stack, | ||||
|             agentId: req.agent?.id | ||||
|         }); | ||||
|  | ||||
|         res.status(500).json({ | ||||
|             success: false, | ||||
|             message: '获取转账记录失败' | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     if (status) { | ||||
|       whereConditions.push('t.status = ?'); | ||||
|       queryParams.push(status); | ||||
|     } | ||||
|  | ||||
|     if (type) { | ||||
|       whereConditions.push('t.source_type = ?'); | ||||
|       queryParams.push(type); | ||||
|     } | ||||
|  | ||||
|     if (start_date) { | ||||
|       whereConditions.push('DATE(t.created_at) >= ?'); | ||||
|       queryParams.push(start_date); | ||||
|     } | ||||
|  | ||||
|     if (end_date) { | ||||
|       whereConditions.push('DATE(t.created_at) <= ?'); | ||||
|       queryParams.push(end_date); | ||||
|     } | ||||
|  | ||||
|     if (min_amount) { | ||||
|       whereConditions.push('t.amount >= ?'); | ||||
|       queryParams.push(parseFloat(min_amount)); | ||||
|     } | ||||
|  | ||||
|     if (max_amount) { | ||||
|       whereConditions.push('t.amount <= ?'); | ||||
|       queryParams.push(parseFloat(max_amount)); | ||||
|     } | ||||
|  | ||||
|     const whereClause = whereConditions.join(' AND '); | ||||
|  | ||||
|     // 验证排序字段 | ||||
|     const allowedSortFields = ['created_at', 'amount', 'status']; | ||||
|     const sortBy = allowedSortFields.includes(sort_by) ? sort_by : 'created_at'; | ||||
|     const sortOrder = sort_order.toLowerCase() === 'asc' ? 'ASC' : 'DESC'; | ||||
|  | ||||
|     // 查询转账记录列表 | ||||
|     const transfersQuery = ` | ||||
|       SELECT  | ||||
|         t.id, | ||||
|         t.from_user_id, | ||||
|         t.to_user_id, | ||||
|         t.amount, | ||||
|         t.source_type, | ||||
|         t.status, | ||||
|         t.description, | ||||
|         t.matching_order_id, | ||||
|         t.created_at, | ||||
|         t.updated_at, | ||||
|         u1.username as from_username, | ||||
|         u1.real_name as from_real_name, | ||||
|         u1.phone as from_phone, | ||||
|         u1.avatar as from_avatar, | ||||
|         u2.username as to_username, | ||||
|         u2.real_name as to_real_name, | ||||
|         u2.phone as to_phone, | ||||
|         u2.avatar as to_avatar, | ||||
|         CASE  | ||||
|           WHEN am1.agent_id = ? THEN 'out' | ||||
|           WHEN am2.agent_id = ? THEN 'in' | ||||
|           ELSE 'both' | ||||
|         END as direction | ||||
|       FROM transfers t | ||||
|       LEFT JOIN users u1 ON t.from_user_id = u1.id | ||||
|       LEFT JOIN users u2 ON t.to_user_id = u2.id | ||||
|       LEFT JOIN agent_merchants am1 ON t.from_user_id = am1.merchant_id | ||||
|       LEFT JOIN agent_merchants am2 ON t.to_user_id = am2.merchant_id | ||||
|       WHERE ${whereClause} | ||||
|       ORDER BY t.${sortBy} ${sortOrder} | ||||
|       LIMIT ${limitNum} OFFSET ${offset} | ||||
|     `; | ||||
|  | ||||
|     const [transfers] = await getDB().execute(transfersQuery, [agentId, agentId, ...queryParams]); | ||||
|  | ||||
|     // 查询总数 | ||||
|     const countQuery = ` | ||||
|       SELECT COUNT(*) as total | ||||
|       FROM transfers t | ||||
|       LEFT JOIN agent_merchants am1 ON t.from_user_id = am1.merchant_id | ||||
|       LEFT JOIN agent_merchants am2 ON t.to_user_id = am2.merchant_id | ||||
|       LEFT JOIN users u1 ON t.from_user_id = u1.id | ||||
|       LEFT JOIN users u2 ON t.to_user_id = u2.id | ||||
|       WHERE ${whereClause} | ||||
|     `; | ||||
|      | ||||
|     const [countResult] = await getDB().execute(countQuery, [agentId, agentId, ...queryParams]); | ||||
|     const total = countResult[0]?.total || 0; | ||||
|  | ||||
|     // 查询统计信息 | ||||
|     const [statsResult] = await getDB().execute(` | ||||
|       SELECT  | ||||
|         COUNT(*) as total_transfers, | ||||
|         COUNT(CASE WHEN t.status = 'completed' THEN 1 END) as completed_transfers, | ||||
|         COUNT(CASE WHEN t.status = 'pending' THEN 1 END) as pending_transfers, | ||||
|         COUNT(CASE WHEN t.status = 'failed' THEN 1 END) as failed_transfers, | ||||
|         CAST(COALESCE(SUM(CASE WHEN t.status = 'completed' THEN t.amount ELSE 0 END), 0) AS DECIMAL(10,2)) as total_amount, | ||||
|         CAST(COALESCE(SUM(CASE WHEN t.status = 'completed' AND DATE(t.created_at) = CURDATE() THEN t.amount ELSE 0 END), 0) AS DECIMAL(10,2)) as today_amount, | ||||
|         COUNT(CASE WHEN DATE(t.created_at) = CURDATE() THEN 1 END) as today_transfers | ||||
|       FROM transfers t | ||||
|       LEFT JOIN agent_merchants am1 ON t.from_user_id = am1.merchant_id | ||||
|       LEFT JOIN agent_merchants am2 ON t.to_user_id = am2.merchant_id | ||||
|       WHERE (am1.agent_id = ? OR am2.agent_id = ?) | ||||
|     `, [agentId, agentId]); | ||||
|  | ||||
|     const stats = statsResult[0] || { | ||||
|       total_transfers: 0, | ||||
|       completed_transfers: 0, | ||||
|       pending_transfers: 0, | ||||
|       failed_transfers: 0, | ||||
|       total_amount: '0.00', | ||||
|       today_amount: '0.00', | ||||
|       today_transfers: 0 | ||||
|     }; | ||||
|  | ||||
|     res.json({ | ||||
|       success: true, | ||||
|       data: { | ||||
|         transfers, | ||||
|         pagination: { | ||||
|           current_page: pageNum, | ||||
|           per_page: limitNum, | ||||
|           total, | ||||
|           total_pages: Math.ceil(total / limitNum) | ||||
|         }, | ||||
|         stats | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|   } catch (error) { | ||||
|     logger.error('获取转账记录失败', { | ||||
|       error: error.message, | ||||
|       stack: error.stack, | ||||
|       agentId: req.agent?.id | ||||
|     }); | ||||
|      | ||||
|     res.status(500).json({ | ||||
|       success: false, | ||||
|       message: '获取转账记录失败' | ||||
|     }); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| /** | ||||
| @@ -188,70 +192,70 @@ router.get('/', agentAuth, async (req, res) => { | ||||
|  * GET /api/transfers/:id | ||||
|  */ | ||||
| router.get('/:id', agentAuth, async (req, res) => { | ||||
|   try { | ||||
|     const agentId = req.agent.id; | ||||
|     const transferId = req.params.id; | ||||
|     try { | ||||
|         const agentId = req.agent.id; | ||||
|         const transferId = req.params.id; | ||||
|  | ||||
|     // 查询转账记录详情 | ||||
|     const [transfers] = await getDB().execute(` | ||||
|       SELECT  | ||||
|         t.id, | ||||
|         t.from_user_id, | ||||
|         t.to_user_id, | ||||
|         t.amount, | ||||
|         t.source_type, | ||||
|         t.status, | ||||
|         t.description, | ||||
|         t.matching_order_id, | ||||
|         t.created_at, | ||||
|         t.updated_at, | ||||
|         u1.username as from_username, | ||||
|         u1.real_name as from_real_name, | ||||
|         u1.phone as from_phone, | ||||
|         u1.avatar as from_avatar, | ||||
|         u1.city as from_city, | ||||
|         u1.district as from_district, | ||||
|         u2.username as to_username, | ||||
|         u2.real_name as to_real_name, | ||||
|         u2.phone as to_phone, | ||||
|         u2.avatar as to_avatar, | ||||
|         u2.city as to_city, | ||||
|         u2.district as to_district | ||||
|       FROM transfers t | ||||
|       LEFT JOIN users u1 ON t.from_user_id = u1.id | ||||
|       LEFT JOIN users u2 ON t.to_user_id = u2.id | ||||
|       LEFT JOIN agent_merchants am1 ON t.from_user_id = am1.merchant_id | ||||
|       LEFT JOIN agent_merchants am2 ON t.to_user_id = am2.merchant_id | ||||
|       WHERE t.id = ? AND (am1.agent_id = ? OR am2.agent_id = ?) | ||||
|     `, [transferId, agentId, agentId]); | ||||
|         // 查询转账记录详情 | ||||
|         const [transfers] = await getDB().execute(` | ||||
|             SELECT t.id, | ||||
|                    t.from_user_id, | ||||
|                    t.to_user_id, | ||||
|                    t.amount, | ||||
|                    t.source_type, | ||||
|                    t.status, | ||||
|                    t.description, | ||||
|                    t.matching_order_id, | ||||
|                    t.created_at, | ||||
|                    t.updated_at, | ||||
|                    u1.username  as from_username, | ||||
|                    u1.real_name as from_real_name, | ||||
|                    u1.phone     as from_phone, | ||||
|                    u1.avatar    as from_avatar, | ||||
|                    u1.city      as from_city, | ||||
|                    u1.district  as from_district, | ||||
|                    u2.username  as to_username, | ||||
|                    u2.real_name as to_real_name, | ||||
|                    u2.phone     as to_phone, | ||||
|                    u2.avatar    as to_avatar, | ||||
|                    u2.city      as to_city, | ||||
|                    u2.district  as to_district | ||||
|             FROM transfers t | ||||
|                      LEFT JOIN users u1 ON t.from_user_id = u1.id | ||||
|                      LEFT JOIN users u2 ON t.to_user_id = u2.id | ||||
|                      LEFT JOIN agent_merchants am1 ON t.from_user_id = am1.merchant_id | ||||
|                      LEFT JOIN agent_merchants am2 ON t.to_user_id = am2.merchant_id | ||||
|             WHERE t.id = ? | ||||
|               AND (am1.agent_id = ? OR am2.agent_id = ?) | ||||
|         `, [transferId, agentId, agentId]); | ||||
|  | ||||
|     if (transfers.length === 0) { | ||||
|       return res.status(404).json({ | ||||
|         success: false, | ||||
|         message: '转账记录不存在或无权限查看' | ||||
|       }); | ||||
|         if (transfers.length === 0) { | ||||
|             return res.status(404).json({ | ||||
|                 success: false, | ||||
|                 message: '转账记录不存在或无权限查看' | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         const transfer = transfers[0]; | ||||
|  | ||||
|         res.json({ | ||||
|             success: true, | ||||
|             data: transfer | ||||
|         }); | ||||
|  | ||||
|     } catch (error) { | ||||
|         logger.error('获取转账记录详情失败', { | ||||
|             error: error.message, | ||||
|             stack: error.stack, | ||||
|             agentId: req.agent?.id, | ||||
|             transferId: req.params.id | ||||
|         }); | ||||
|  | ||||
|         res.status(500).json({ | ||||
|             success: false, | ||||
|             message: '获取转账记录详情失败' | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     const transfer = transfers[0]; | ||||
|  | ||||
|     res.json({ | ||||
|       success: true, | ||||
|       data: transfer | ||||
|     }); | ||||
|  | ||||
|   } catch (error) { | ||||
|     logger.error('获取转账记录详情失败', { | ||||
|       error: error.message, | ||||
|       stack: error.stack, | ||||
|       agentId: req.agent?.id, | ||||
|       transferId: req.params.id | ||||
|     }); | ||||
|      | ||||
|     res.status(500).json({ | ||||
|       success: false, | ||||
|       message: '获取转账记录详情失败' | ||||
|     }); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| /** | ||||
| @@ -259,46 +263,48 @@ router.get('/:id', agentAuth, async (req, res) => { | ||||
|  * GET /api/transfers/trend | ||||
|  */ | ||||
| router.get('/trend/data', agentAuth, async (req, res) => { | ||||
|   try { | ||||
|     const agentId = req.agent.id; | ||||
|     const { days = 7, type = 'amount' } = req.query; | ||||
|     try { | ||||
|         const agentId = req.agent.id; | ||||
|         const {days = 7, type = 'amount'} = req.query; | ||||
|  | ||||
|     let selectField = 'CAST(COALESCE(SUM(t.amount), 0) AS DECIMAL(10,2)) as value'; | ||||
|     if (type === 'count') { | ||||
|       selectField = 'COUNT(*) as value'; | ||||
|         let selectField = 'CAST(COALESCE(SUM(t.amount), 0) AS DECIMAL(10,2)) as value'; | ||||
|         if (type === 'count') { | ||||
|             selectField = 'COUNT(*) as value'; | ||||
|         } | ||||
|  | ||||
|         const [trendData] = await getDB().execute(` | ||||
|             SELECT | ||||
|                 DATE (t.created_at) as date, ${selectField} | ||||
|             FROM transfers t | ||||
|                 LEFT JOIN agent_merchants am1 | ||||
|             ON t.from_user_id = am1.merchant_id | ||||
|                 LEFT JOIN agent_merchants am2 ON t.to_user_id = am2.merchant_id | ||||
|             WHERE (am1.agent_id = ? | ||||
|                OR am2.agent_id = ?) | ||||
|               AND t.status = 'completed' | ||||
|               AND t.created_at >= DATE_SUB(CURDATE() | ||||
|                 , INTERVAL ? DAY) | ||||
|             GROUP BY DATE (t.created_at) | ||||
|             ORDER BY date ASC | ||||
|         `, [agentId, agentId, parseInt(days)]); | ||||
|  | ||||
|         res.json({ | ||||
|             success: true, | ||||
|             data: trendData | ||||
|         }); | ||||
|  | ||||
|     } catch (error) { | ||||
|         logger.error('获取转账趋势失败', { | ||||
|             error: error.message, | ||||
|             stack: error.stack, | ||||
|             agentId: req.agent?.id | ||||
|         }); | ||||
|  | ||||
|         res.status(500).json({ | ||||
|             success: false, | ||||
|             message: '获取转账趋势失败' | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     const [trendData] = await getDB().execute(` | ||||
|       SELECT  | ||||
|         DATE(t.created_at) as date, | ||||
|         ${selectField} | ||||
|       FROM transfers t | ||||
|       LEFT JOIN agent_merchants am1 ON t.from_user_id = am1.merchant_id | ||||
|       LEFT JOIN agent_merchants am2 ON t.to_user_id = am2.merchant_id | ||||
|       WHERE (am1.agent_id = ? OR am2.agent_id = ?) | ||||
|         AND t.status = 'completed' | ||||
|         AND t.created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY) | ||||
|       GROUP BY DATE(t.created_at) | ||||
|       ORDER BY date ASC | ||||
|     `, [agentId, agentId, parseInt(days)]); | ||||
|  | ||||
|     res.json({ | ||||
|       success: true, | ||||
|       data: trendData | ||||
|     }); | ||||
|  | ||||
|   } catch (error) { | ||||
|     logger.error('获取转账趋势失败', { | ||||
|       error: error.message, | ||||
|       stack: error.stack, | ||||
|       agentId: req.agent?.id | ||||
|     }); | ||||
|      | ||||
|     res.status(500).json({ | ||||
|       success: false, | ||||
|       message: '获取转账趋势失败' | ||||
|     }); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| /** | ||||
| @@ -306,129 +312,128 @@ router.get('/trend/data', agentAuth, async (req, res) => { | ||||
|  * GET /api/transfers/export | ||||
|  */ | ||||
| router.get('/export/data', agentAuth, async (req, res) => { | ||||
|   try { | ||||
|     const agentId = req.agent.id; | ||||
|     const {  | ||||
|       format = 'json',  | ||||
|       search,  | ||||
|       status,  | ||||
|       type,  | ||||
|       start_date,  | ||||
|       end_date, | ||||
|       min_amount, | ||||
|       max_amount | ||||
|     } = req.query; | ||||
|     try { | ||||
|         const agentId = req.agent.id; | ||||
|         const { | ||||
|             format = 'json', | ||||
|             search, | ||||
|             status, | ||||
|             type, | ||||
|             start_date, | ||||
|             end_date, | ||||
|             min_amount, | ||||
|             max_amount | ||||
|         } = req.query; | ||||
|  | ||||
|     // 构建查询条件 | ||||
|     let whereConditions = ['(am1.agent_id = ? OR am2.agent_id = ?)']; | ||||
|     let queryParams = [agentId, agentId]; | ||||
|         // 构建查询条件 | ||||
|         let whereConditions = ['(am1.agent_id = ? OR am2.agent_id = ?)']; | ||||
|         let queryParams = [agentId, agentId]; | ||||
|  | ||||
|     if (search) { | ||||
|       whereConditions.push('(u1.username LIKE ? OR u1.real_name LIKE ? OR u2.username LIKE ? OR u2.real_name LIKE ?)'); | ||||
|       queryParams.push(`%${search}%`, `%${search}%`, `%${search}%`, `%${search}%`); | ||||
|         if (search) { | ||||
|             whereConditions.push('(u1.username LIKE ? OR u1.real_name LIKE ? OR u2.username LIKE ? OR u2.real_name LIKE ?)'); | ||||
|             queryParams.push(`%${search}%`, `%${search}%`, `%${search}%`, `%${search}%`); | ||||
|         } | ||||
|  | ||||
|         if (status) { | ||||
|             whereConditions.push('t.status = ?'); | ||||
|             queryParams.push(status); | ||||
|         } | ||||
|  | ||||
|         if (type) { | ||||
|             whereConditions.push('t.source_type = ?'); | ||||
|             queryParams.push(type); | ||||
|         } | ||||
|  | ||||
|         if (start_date) { | ||||
|             whereConditions.push('DATE(t.created_at) >= ?'); | ||||
|             queryParams.push(start_date); | ||||
|         } | ||||
|  | ||||
|         if (end_date) { | ||||
|             whereConditions.push('DATE(t.created_at) <= ?'); | ||||
|             queryParams.push(end_date); | ||||
|         } | ||||
|  | ||||
|         if (min_amount) { | ||||
|             whereConditions.push('t.amount >= ?'); | ||||
|             queryParams.push(parseFloat(min_amount)); | ||||
|         } | ||||
|  | ||||
|         if (max_amount) { | ||||
|             whereConditions.push('t.amount <= ?'); | ||||
|             queryParams.push(parseFloat(max_amount)); | ||||
|         } | ||||
|  | ||||
|         const whereClause = whereConditions.join(' AND '); | ||||
|  | ||||
|         // 查询转账记录 | ||||
|         const [transfers] = await getDB().execute(` | ||||
|             SELECT t.id, | ||||
|                    t.amount, | ||||
|                    t.source_type, | ||||
|                    t.status, | ||||
|                    t.description, | ||||
|                    t.matching_order_id, | ||||
|                    t.created_at, | ||||
|                    u1.username  as from_username, | ||||
|                    u1.real_name as from_real_name, | ||||
|                    u1.phone     as from_phone, | ||||
|                    u2.username  as to_username, | ||||
|                    u2.real_name as to_real_name, | ||||
|                    u2.phone     as to_phone | ||||
|             FROM transfers t | ||||
|                      LEFT JOIN users u1 ON t.from_user_id = u1.id | ||||
|                      LEFT JOIN users u2 ON t.to_user_id = u2.id | ||||
|                      LEFT JOIN agent_merchants am1 ON t.from_user_id = am1.merchant_id | ||||
|                      LEFT JOIN agent_merchants am2 ON t.to_user_id = am2.merchant_id | ||||
|             WHERE ${whereClause} | ||||
|             ORDER BY t.created_at DESC | ||||
|         `, queryParams); | ||||
|  | ||||
|         if (format === 'csv') { | ||||
|             // 生成CSV格式 | ||||
|             const csvHeader = 'ID,金额,类型,状态,描述,交易ID,转出用户,转出手机,转入用户,转入手机,创建时间\n'; | ||||
|             const csvData = transfers.map(transfer => { | ||||
|                 return [ | ||||
|                     transfer.id, | ||||
|                     transfer.amount, | ||||
|                     transfer.type || '', | ||||
|                     transfer.status || '', | ||||
|                     (transfer.description || '').replace(/,/g, ','), // 替换逗号避免CSV格式问题 | ||||
|                     transfer.matching_order_id || '', | ||||
|                     transfer.from_real_name || transfer.from_username || '', | ||||
|                     transfer.from_phone || '', | ||||
|                     transfer.to_real_name || transfer.to_username || '', | ||||
|                     transfer.to_phone || '', | ||||
|                     transfer.created_at || '' | ||||
|                 ].join(','); | ||||
|             }).join('\n'); | ||||
|  | ||||
|             res.setHeader('Content-Type', 'text/csv; charset=utf-8'); | ||||
|             res.setHeader('Content-Disposition', `attachment; filename="transfers_${Date.now()}.csv"`); | ||||
|             res.send(csvHeader + csvData); | ||||
|         } else { | ||||
|             // 默认JSON格式 | ||||
|             res.json({ | ||||
|                 success: true, | ||||
|                 data: transfers, | ||||
|                 exported_at: new Date().toISOString(), | ||||
|                 total: transfers.length | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|     } catch (error) { | ||||
|         logger.error('导出转账记录失败', { | ||||
|             error: error.message, | ||||
|             stack: error.stack, | ||||
|             agentId: req.agent?.id | ||||
|         }); | ||||
|  | ||||
|         res.status(500).json({ | ||||
|             success: false, | ||||
|             message: '导出转账记录失败' | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     if (status) { | ||||
|       whereConditions.push('t.status = ?'); | ||||
|       queryParams.push(status); | ||||
|     } | ||||
|  | ||||
|     if (type) { | ||||
|       whereConditions.push('t.source_type = ?'); | ||||
|       queryParams.push(type); | ||||
|     } | ||||
|  | ||||
|     if (start_date) { | ||||
|       whereConditions.push('DATE(t.created_at) >= ?'); | ||||
|       queryParams.push(start_date); | ||||
|     } | ||||
|  | ||||
|     if (end_date) { | ||||
|       whereConditions.push('DATE(t.created_at) <= ?'); | ||||
|       queryParams.push(end_date); | ||||
|     } | ||||
|  | ||||
|     if (min_amount) { | ||||
|       whereConditions.push('t.amount >= ?'); | ||||
|       queryParams.push(parseFloat(min_amount)); | ||||
|     } | ||||
|  | ||||
|     if (max_amount) { | ||||
|       whereConditions.push('t.amount <= ?'); | ||||
|       queryParams.push(parseFloat(max_amount)); | ||||
|     } | ||||
|  | ||||
|     const whereClause = whereConditions.join(' AND '); | ||||
|  | ||||
|     // 查询转账记录 | ||||
|     const [transfers] = await getDB().execute(` | ||||
|       SELECT  | ||||
|         t.id, | ||||
|         t.amount, | ||||
|         t.source_type, | ||||
|         t.status, | ||||
|         t.description, | ||||
|         t.matching_order_id, | ||||
|         t.created_at, | ||||
|         u1.username as from_username, | ||||
|         u1.real_name as from_real_name, | ||||
|         u1.phone as from_phone, | ||||
|         u2.username as to_username, | ||||
|         u2.real_name as to_real_name, | ||||
|         u2.phone as to_phone | ||||
|       FROM transfers t | ||||
|       LEFT JOIN users u1 ON t.from_user_id = u1.id | ||||
|       LEFT JOIN users u2 ON t.to_user_id = u2.id | ||||
|       LEFT JOIN agent_merchants am1 ON t.from_user_id = am1.merchant_id | ||||
|       LEFT JOIN agent_merchants am2 ON t.to_user_id = am2.merchant_id | ||||
|       WHERE ${whereClause} | ||||
|       ORDER BY t.created_at DESC | ||||
|     `, queryParams); | ||||
|  | ||||
|     if (format === 'csv') { | ||||
|       // 生成CSV格式 | ||||
|       const csvHeader = 'ID,金额,类型,状态,描述,交易ID,转出用户,转出手机,转入用户,转入手机,创建时间\n'; | ||||
|       const csvData = transfers.map(transfer => { | ||||
|         return [ | ||||
|           transfer.id, | ||||
|           transfer.amount, | ||||
|           transfer.type || '', | ||||
|           transfer.status || '', | ||||
|           (transfer.description || '').replace(/,/g, ','), // 替换逗号避免CSV格式问题 | ||||
|           transfer.matching_order_id || '', | ||||
|           transfer.from_real_name || transfer.from_username || '', | ||||
|           transfer.from_phone || '', | ||||
|           transfer.to_real_name || transfer.to_username || '', | ||||
|           transfer.to_phone || '', | ||||
|           transfer.created_at || '' | ||||
|         ].join(','); | ||||
|       }).join('\n'); | ||||
|  | ||||
|       res.setHeader('Content-Type', 'text/csv; charset=utf-8'); | ||||
|       res.setHeader('Content-Disposition', `attachment; filename="transfers_${Date.now()}.csv"`); | ||||
|       res.send(csvHeader + csvData); | ||||
|     } else { | ||||
|       // 默认JSON格式 | ||||
|       res.json({ | ||||
|         success: true, | ||||
|         data: transfers, | ||||
|         exported_at: new Date().toISOString(), | ||||
|         total: transfers.length | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|   } catch (error) { | ||||
|     logger.error('导出转账记录失败', { | ||||
|       error: error.message, | ||||
|       stack: error.stack, | ||||
|       agentId: req.agent?.id | ||||
|     }); | ||||
|      | ||||
|     res.status(500).json({ | ||||
|       success: false, | ||||
|       message: '导出转账记录失败' | ||||
|     }); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| module.exports = router; | ||||
							
								
								
									
										762
									
								
								routes/upload.js
									
									
									
									
									
								
							
							
						
						
									
										762
									
								
								routes/upload.js
									
									
									
									
									
								
							| @@ -1,76 +1,127 @@ | ||||
| const express = require('express'); | ||||
| const router = express.Router(); | ||||
| const multer = require('multer'); | ||||
| const path = require('path'); | ||||
| const fs = require('fs'); | ||||
| const { agentAuth } = require('../middleware/agentAuth'); | ||||
| const { logger } = require('../config/logger'); | ||||
| const { authenticateToken } = require('./auth'); | ||||
| const { auth } = require('../middleware/auth'); | ||||
| const minioService = require('../services/minioService'); | ||||
| const { initializeBuckets } = require('../config/minio'); | ||||
| // 初始化MinIO存储桶 | ||||
| // initializeBuckets().catch(console.error); | ||||
|  | ||||
| // 确保上传目录存在 | ||||
| const uploadDir = path.join(__dirname, '../uploads'); | ||||
| if (!fs.existsSync(uploadDir)) { | ||||
|   fs.mkdirSync(uploadDir, { recursive: true }); | ||||
| } | ||||
| /** | ||||
|  * @swagger | ||||
|  * tags: | ||||
|  *   name: Upload | ||||
|  *   description: 文件上传API | ||||
|  */ | ||||
|  | ||||
| // 配置multer存储 | ||||
| const storage = multer.diskStorage({ | ||||
|   destination: function (req, file, cb) { | ||||
|     // 根据文件类型创建不同的子目录 | ||||
|     let subDir = 'others'; | ||||
|      | ||||
|     if (file.fieldname === 'avatar') { | ||||
|       subDir = 'avatars'; | ||||
|     } else if (file.fieldname === 'qr_code') { | ||||
|       subDir = 'qrcodes'; | ||||
|     } else if (file.fieldname === 'id_card_front' || file.fieldname === 'id_card_back') { | ||||
|       subDir = 'idcards'; | ||||
|     } else if (file.fieldname === 'business_license') { | ||||
|       subDir = 'licenses'; | ||||
|     } | ||||
|      | ||||
|     const targetDir = path.join(uploadDir, subDir); | ||||
|     if (!fs.existsSync(targetDir)) { | ||||
|       fs.mkdirSync(targetDir, { recursive: true }); | ||||
|     } | ||||
|      | ||||
|     cb(null, targetDir); | ||||
|   }, | ||||
|   filename: function (req, file, cb) { | ||||
|     // 生成唯一文件名 | ||||
|     const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); | ||||
|     const ext = path.extname(file.originalname); | ||||
|     cb(null, file.fieldname + '-' + uniqueSuffix + ext); | ||||
|   } | ||||
| }); | ||||
| // 配置multer内存存储(用于MinIO上传) | ||||
| const storage = multer.memoryStorage(); | ||||
|  | ||||
| // 文件过滤器 | ||||
| // 文件过滤器 - 支持图片和视频 | ||||
| const fileFilter = (req, file, cb) => { | ||||
|   // 允许的图片类型 | ||||
|   const allowedImageTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; | ||||
|    | ||||
|   if (allowedImageTypes.includes(file.mimetype)) { | ||||
|   // 允许图片和视频文件 | ||||
|   if (file.mimetype.startsWith('image/') || file.mimetype.startsWith('video/')) { | ||||
|     cb(null, true); | ||||
|   } else { | ||||
|     cb(new Error('只允许上传图片文件 (JPEG, PNG, GIF, WebP)'), false); | ||||
|     cb(new Error('只能上传图片或视频文件'), false); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| // 配置multer | ||||
| // 单文件上传配置 | ||||
| const upload = multer({ | ||||
|   storage: storage, | ||||
|   fileFilter: fileFilter, | ||||
|   limits: { | ||||
|     fileSize: 5 * 1024 * 1024, // 5MB限制 | ||||
|     fileSize: 5 * 1024 * 1024, // 5MB | ||||
|     files: 1 // 一次只能上传一个文件 | ||||
|   } | ||||
| }); | ||||
|  | ||||
| // 多文件上传配置 | ||||
| const multiUpload = multer({ | ||||
|   storage: storage, | ||||
|   fileFilter: fileFilter, | ||||
|   limits: { | ||||
|     fileSize: 10 * 1024 * 1024, // 10MB (视频文件更大) | ||||
|     files: 10 // 最多10个文件 | ||||
|   } | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * 单文件上传 | ||||
|  * POST /api/upload/single | ||||
|  * @swagger | ||||
|  * /upload/image: | ||||
|  *   post: | ||||
|  *     summary: 上传图片 | ||||
|  *     tags: [Upload] | ||||
|  *     security: | ||||
|  *       - bearerAuth: [] | ||||
|  *     requestBody: | ||||
|  *       required: true | ||||
|  *       content: | ||||
|  *         multipart/form-data: | ||||
|  *           schema: | ||||
|  *             type: object | ||||
|  *             properties: | ||||
|  *               file: | ||||
|  *                 type: string | ||||
|  *                 format: binary | ||||
|  *                 description: 要上传的图片文件 | ||||
|  *               type: | ||||
|  *                 type: string | ||||
|  *                 enum: [avatar, product, document] | ||||
|  *                 default: document | ||||
|  *                 description: 上传文件类型 | ||||
|  *     responses: | ||||
|  *       200: | ||||
|  *         description: 图片上传成功 | ||||
|  *         content: | ||||
|  *           application/json: | ||||
|  *             schema: | ||||
|  *               type: object | ||||
|  *               properties: | ||||
|  *                 success: | ||||
|  *                   type: boolean | ||||
|  *                   example: true | ||||
|  *                 url: | ||||
|  *                   type: string | ||||
|  *                   description: 上传后的文件URL | ||||
|  *                 filename: | ||||
|  *                   type: string | ||||
|  *                   description: 上传后的文件名 | ||||
|  *       400: | ||||
|  *         description: 请求参数错误 | ||||
|  *       401: | ||||
|  *         description: 未授权 | ||||
|  *       500: | ||||
|  *         description: 服务器错误 | ||||
|  */ | ||||
| router.post('/single', agentAuth, upload.single('file'), async (req, res) => { | ||||
|   try { | ||||
| router.post('/image', authenticateToken, (req, res) => { | ||||
|   upload.single('file')(req, res, async (err) => { | ||||
|     if (err instanceof multer.MulterError) { | ||||
|       if (err.code === 'LIMIT_FILE_SIZE') { | ||||
|         return res.status(400).json({ | ||||
|           success: false, | ||||
|           message: '文件大小不能超过 5MB' | ||||
|         }); | ||||
|       } | ||||
|       if (err.code === 'LIMIT_FILE_COUNT') { | ||||
|         return res.status(400).json({ | ||||
|           success: false, | ||||
|           message: '一次只能上传一个文件' | ||||
|         }); | ||||
|       } | ||||
|       return res.status(400).json({ | ||||
|         success: false, | ||||
|         message: '文件上传失败:' + err.message | ||||
|       }); | ||||
|     } else if (err) { | ||||
|       return res.status(400).json({ | ||||
|         success: false, | ||||
|         message: err.message | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     if (!req.file) { | ||||
|       return res.status(400).json({ | ||||
|         success: false, | ||||
| @@ -78,51 +129,122 @@ router.post('/single', agentAuth, upload.single('file'), async (req, res) => { | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     const file = req.file; | ||||
|     const relativePath = path.relative(path.join(__dirname, '../'), file.path).replace(/\\/g, '/'); | ||||
|     const fileUrl = `/uploads/${relativePath.split('/').slice(1).join('/')}`; | ||||
|     try { | ||||
|       // 使用MinIO服务上传文件 | ||||
|       const type = req.body.type || 'document'; | ||||
|       const result = await minioService.uploadFile( | ||||
|         req.file.buffer, | ||||
|         req.file.originalname, | ||||
|         req.file.mimetype, | ||||
|         type | ||||
|       ); | ||||
|  | ||||
|     logger.info('文件上传成功', { | ||||
|       agentId: req.agent.id, | ||||
|       filename: file.filename, | ||||
|       originalname: file.originalname, | ||||
|       size: file.size, | ||||
|       path: fileUrl | ||||
|     }); | ||||
|  | ||||
|     res.json({ | ||||
|       success: true, | ||||
|       message: '文件上传成功', | ||||
|       data: { | ||||
|         filename: file.filename, | ||||
|         originalname: file.originalname, | ||||
|         size: file.size, | ||||
|         mimetype: file.mimetype, | ||||
|         url: fileUrl, | ||||
|         path: fileUrl // 兼容前端可能使用的字段名 | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|   } catch (error) { | ||||
|     logger.error('文件上传失败', { | ||||
|       error: error.message, | ||||
|       stack: error.stack, | ||||
|       agentId: req.agent?.id | ||||
|     }); | ||||
|      | ||||
|     res.status(500).json({ | ||||
|       success: false, | ||||
|       message: error.message || '文件上传失败' | ||||
|     }); | ||||
|   } | ||||
|       res.json({ | ||||
|         success: true, | ||||
|         message: '文件上传成功', | ||||
|         data: result.data | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       console.error('文件上传到MinIO失败:', error); | ||||
|       res.status(500).json({ | ||||
|         success: false, | ||||
|         message: error.message || '文件上传失败' | ||||
|       }); | ||||
|     } | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * 多文件上传 | ||||
|  * POST /api/upload/multiple | ||||
|  * @swagger | ||||
|  * /upload: | ||||
|  *   post: | ||||
|  *     summary: 多文件上传接口 (支持MediaUpload组件) | ||||
|  *     tags: [Upload] | ||||
|  *     security: | ||||
|  *       - bearerAuth: [] | ||||
|  *     requestBody: | ||||
|  *       required: true | ||||
|  *       content: | ||||
|  *         multipart/form-data: | ||||
|  *           schema: | ||||
|  *             type: object | ||||
|  *             properties: | ||||
|  *               files: | ||||
|  *                 type: array | ||||
|  *                 items: | ||||
|  *                   type: string | ||||
|  *                   format: binary | ||||
|  *                 description: 要上传的文件列表 | ||||
|  *               type: | ||||
|  *                 type: string | ||||
|  *                 enum: [avatar, product, document] | ||||
|  *                 default: document | ||||
|  *                 description: 上传文件类型 | ||||
|  *     responses: | ||||
|  *       200: | ||||
|  *         description: 文件上传成功 | ||||
|  *         content: | ||||
|  *           application/json: | ||||
|  *             schema: | ||||
|  *               type: object | ||||
|  *               properties: | ||||
|  *                 success: | ||||
|  *                   type: boolean | ||||
|  *                   example: true | ||||
|  *                 message: | ||||
|  *                   type: string | ||||
|  *                   example: 文件上传成功 | ||||
|  *                 data: | ||||
|  *                   type: array | ||||
|  *                   items: | ||||
|  *                     type: object | ||||
|  *                     properties: | ||||
|  *                       filename: | ||||
|  *                         type: string | ||||
|  *                       originalname: | ||||
|  *                         type: string | ||||
|  *                       mimetype: | ||||
|  *                         type: string | ||||
|  *                       size: | ||||
|  *                         type: integer | ||||
|  *                       path: | ||||
|  *                         type: string | ||||
|  *                       url: | ||||
|  *                         type: string | ||||
|  *       400: | ||||
|  *         description: 请求参数错误 | ||||
|  *       401: | ||||
|  *         description: 未授权 | ||||
|  *       500: | ||||
|  *         description: 服务器错误 | ||||
|  * @access Private | ||||
|  */ | ||||
| router.post('/multiple', agentAuth, upload.array('files', 10), async (req, res) => { | ||||
|   try { | ||||
| router.post('/', authenticateToken, (req, res) => { | ||||
|   multiUpload.array('file', 10)(req, res, async (err) => { | ||||
|     if (err instanceof multer.MulterError) { | ||||
|       if (err.code === 'LIMIT_FILE_SIZE') { | ||||
|         return res.status(400).json({ | ||||
|           success: false, | ||||
|           message: '文件大小不能超过 10MB' | ||||
|         }); | ||||
|       } | ||||
|       if (err.code === 'LIMIT_FILE_COUNT') { | ||||
|         return res.status(400).json({ | ||||
|           success: false, | ||||
|           message: '一次最多只能上传10个文件' | ||||
|         }); | ||||
|       } | ||||
|       return res.status(400).json({ | ||||
|         success: false, | ||||
|         message: '文件上传失败:' + err.message | ||||
|       }); | ||||
|     } else if (err) { | ||||
|       return res.status(400).json({ | ||||
|         success: false, | ||||
|         message: err.message | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     if (!req.files || req.files.length === 0) { | ||||
|       return res.status(400).json({ | ||||
|         success: false, | ||||
| @@ -130,345 +252,167 @@ router.post('/multiple', agentAuth, upload.array('files', 10), async (req, res) | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     const files = req.files.map(file => { | ||||
|       const relativePath = path.relative(path.join(__dirname, '../'), file.path).replace(/\\/g, '/'); | ||||
|       const fileUrl = `/uploads/${relativePath.split('/').slice(1).join('/')}`; | ||||
|        | ||||
|       return { | ||||
|         filename: file.filename, | ||||
|         originalname: file.originalname, | ||||
|         size: file.size, | ||||
|         mimetype: file.mimetype, | ||||
|         url: fileUrl, | ||||
|         path: fileUrl | ||||
|       }; | ||||
|     }); | ||||
|     try { | ||||
|       // 使用MinIO服务上传多个文件 | ||||
|       const type = req.body.type || 'document'; | ||||
|       const files = req.files.map(file => ({ | ||||
|         buffer: file.buffer, | ||||
|         originalName: file.originalname, | ||||
|         mimeType: file.mimetype | ||||
|       })); | ||||
|  | ||||
|     logger.info('多文件上传成功', { | ||||
|       agentId: req.agent.id, | ||||
|       count: files.length, | ||||
|       totalSize: req.files.reduce((sum, file) => sum + file.size, 0) | ||||
|     }); | ||||
|       const result = await minioService.uploadMultipleFiles(files, type); | ||||
|  | ||||
|     res.json({ | ||||
|       success: true, | ||||
|       message: `成功上传${files.length}个文件`, | ||||
|       data: files | ||||
|     }); | ||||
|  | ||||
|   } catch (error) { | ||||
|     logger.error('多文件上传失败', { | ||||
|       error: error.message, | ||||
|       stack: error.stack, | ||||
|       agentId: req.agent?.id | ||||
|     }); | ||||
|      | ||||
|     res.status(500).json({ | ||||
|       success: false, | ||||
|       message: error.message || '文件上传失败' | ||||
|     }); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * 头像上传 | ||||
|  * POST /api/upload/avatar | ||||
|  */ | ||||
| router.post('/avatar', agentAuth, upload.single('avatar'), async (req, res) => { | ||||
|   try { | ||||
|     if (!req.file) { | ||||
|       return res.status(400).json({ | ||||
|         success: false, | ||||
|         message: '请选择头像文件' | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     const file = req.file; | ||||
|     const relativePath = path.relative(path.join(__dirname, '../'), file.path).replace(/\\/g, '/'); | ||||
|     const fileUrl = `/uploads/${relativePath.split('/').slice(1).join('/')}`; | ||||
|  | ||||
|     logger.info('头像上传成功', { | ||||
|       agentId: req.agent.id, | ||||
|       filename: file.filename, | ||||
|       size: file.size | ||||
|     }); | ||||
|  | ||||
|     res.json({ | ||||
|       success: true, | ||||
|       message: '头像上传成功', | ||||
|       data: { | ||||
|         filename: file.filename, | ||||
|         originalname: file.originalname, | ||||
|         size: file.size, | ||||
|         url: fileUrl, | ||||
|         path: fileUrl | ||||
|       // 如果只上传了一个文件,返回单文件格式以保持兼容性 | ||||
|       if (result.data.files.length === 1) { | ||||
|         result.data.files.forEach(element => { | ||||
|           element.path = '/' + element.path | ||||
|         }); | ||||
|         res.json({ | ||||
|           success: true, | ||||
|           message: '文件上传成功', | ||||
|           data: { | ||||
|             ...result.data.files[0], | ||||
|             urls: result.data.urls // 同时提供urls数组格式 | ||||
|           } | ||||
|         }); | ||||
|       } else { | ||||
|         // 多文件返回数组格式 | ||||
|         res.json({ | ||||
|           success: true, | ||||
|           message: `成功上传${result.data.files.length}个文件`, | ||||
|           data: result.data | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|   } catch (error) { | ||||
|     logger.error('头像上传失败', { | ||||
|       error: error.message, | ||||
|       stack: error.stack, | ||||
|       agentId: req.agent?.id | ||||
|     }); | ||||
|      | ||||
|     res.status(500).json({ | ||||
|       success: false, | ||||
|       message: error.message || '头像上传失败' | ||||
|     }); | ||||
|   } | ||||
|     } catch (error) { | ||||
|       console.error('文件上传到MinIO失败:', error); | ||||
|       res.status(500).json({ | ||||
|         success: false, | ||||
|         message: error.message || '文件上传失败' | ||||
|       }); | ||||
|     } | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * 收款码上传 | ||||
|  * POST /api/upload/qrcode | ||||
|  * @swagger | ||||
|  * /upload/single: | ||||
|  *   post: | ||||
|  *     summary: 单文件上传接口(兼容性接口) | ||||
|  *     tags: [Upload] | ||||
|  *     security: | ||||
|  *       - bearerAuth: [] | ||||
|  *     requestBody: | ||||
|  *       required: true | ||||
|  *       content: | ||||
|  *         multipart/form-data: | ||||
|  *           schema: | ||||
|  *             type: object | ||||
|  *             properties: | ||||
|  *               file: | ||||
|  *                 type: string | ||||
|  *                 format: binary | ||||
|  *                 description: 要上传的文件 | ||||
|  *               type: | ||||
|  *                 type: string | ||||
|  *                 enum: [avatar, product, document] | ||||
|  *                 default: document | ||||
|  *                 description: 上传文件类型 | ||||
|  *     responses: | ||||
|  *       200: | ||||
|  *         description: 文件上传成功 | ||||
|  *         content: | ||||
|  *           application/json: | ||||
|  *             schema: | ||||
|  *               type: object | ||||
|  *               properties: | ||||
|  *                 success: | ||||
|  *                   type: boolean | ||||
|  *                   example: true | ||||
|  *                 message: | ||||
|  *                   type: string | ||||
|  *                   example: 文件上传成功 | ||||
|  *                 url: | ||||
|  *                   type: string | ||||
|  *                   description: 上传后的文件URL | ||||
|  *                 filename: | ||||
|  *                   type: string | ||||
|  *                   description: 上传后的文件名 | ||||
|  *                 originalname: | ||||
|  *                   type: string | ||||
|  *                   description: 原始文件名 | ||||
|  *                 size: | ||||
|  *                   type: integer | ||||
|  *                   description: 文件大小 | ||||
|  *       400: | ||||
|  *         description: 请求参数错误 | ||||
|  *       401: | ||||
|  *         description: 未授权 | ||||
|  *       500: | ||||
|  *         description: 服务器错误 | ||||
|  */ | ||||
| router.post('/qrcode', agentAuth, upload.single('qr_code'), async (req, res) => { | ||||
|   try { | ||||
| router.post('/single', auth, (req, res) => { | ||||
|   upload.single('file')(req, res, async (err) => { | ||||
|     if (err instanceof multer.MulterError) { | ||||
|       return res.status(400).json({ | ||||
|         success: false, | ||||
|         message: '文件上传失败:' + err.message | ||||
|       }); | ||||
|     } else if (err) { | ||||
|       return res.status(400).json({ | ||||
|         success: false, | ||||
|         message: err.message | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     if (!req.file) { | ||||
|       return res.status(400).json({ | ||||
|       return res.status(400).json({ success: false, message: '没有上传文件' }); | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       // 使用MinIO服务上传文件 | ||||
|       const type = req.body.type || 'document'; | ||||
|       const result = await minioService.uploadFile( | ||||
|         req.file.buffer, | ||||
|         req.file.originalname, | ||||
|         req.file.mimetype, | ||||
|         type | ||||
|       ); | ||||
|  | ||||
|       res.json({ | ||||
|         success: true, | ||||
|         message: '文件上传成功', | ||||
|         url: result.data.url, | ||||
|         filename: result.data.filename, | ||||
|         originalname: result.data.originalname, | ||||
|         size: result.data.size | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       console.error('文件上传到MinIO失败:', error); | ||||
|       res.status(500).json({ | ||||
|         success: false, | ||||
|         message: '请选择收款码文件' | ||||
|         message: error.message || '文件上传失败' | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     const file = req.file; | ||||
|     const relativePath = path.relative(path.join(__dirname, '../'), file.path).replace(/\\/g, '/'); | ||||
|     const fileUrl = `/uploads/${relativePath.split('/').slice(1).join('/')}`; | ||||
|  | ||||
|     logger.info('收款码上传成功', { | ||||
|       agentId: req.agent.id, | ||||
|       filename: file.filename, | ||||
|       size: file.size | ||||
|     }); | ||||
|  | ||||
|     res.json({ | ||||
|       success: true, | ||||
|       message: '收款码上传成功', | ||||
|       data: { | ||||
|         filename: file.filename, | ||||
|         originalname: file.originalname, | ||||
|         size: file.size, | ||||
|         url: fileUrl, | ||||
|         path: fileUrl | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|   } catch (error) { | ||||
|     logger.error('收款码上传失败', { | ||||
|       error: error.message, | ||||
|       stack: error.stack, | ||||
|       agentId: req.agent?.id | ||||
|     }); | ||||
|      | ||||
|     res.status(500).json({ | ||||
|       success: false, | ||||
|       message: error.message || '收款码上传失败' | ||||
|     }); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * 身份证上传 | ||||
|  * POST /api/upload/idcard | ||||
|  */ | ||||
| router.post('/idcard', agentAuth, upload.fields([ | ||||
|   { name: 'id_card_front', maxCount: 1 }, | ||||
|   { name: 'id_card_back', maxCount: 1 } | ||||
| ]), async (req, res) => { | ||||
|   try { | ||||
|     if (!req.files || (!req.files.id_card_front && !req.files.id_card_back)) { | ||||
|       return res.status(400).json({ | ||||
|         success: false, | ||||
|         message: '请选择身份证文件' | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     const result = {}; | ||||
|  | ||||
|     if (req.files.id_card_front) { | ||||
|       const file = req.files.id_card_front[0]; | ||||
|       const relativePath = path.relative(path.join(__dirname, '../'), file.path).replace(/\\/g, '/'); | ||||
|       const fileUrl = `/uploads/${relativePath.split('/').slice(1).join('/')}`; | ||||
|        | ||||
|       result.front = { | ||||
|         filename: file.filename, | ||||
|         originalname: file.originalname, | ||||
|         size: file.size, | ||||
|         url: fileUrl, | ||||
|         path: fileUrl | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     if (req.files.id_card_back) { | ||||
|       const file = req.files.id_card_back[0]; | ||||
|       const relativePath = path.relative(path.join(__dirname, '../'), file.path).replace(/\\/g, '/'); | ||||
|       const fileUrl = `/uploads/${relativePath.split('/').slice(1).join('/')}`; | ||||
|        | ||||
|       result.back = { | ||||
|         filename: file.filename, | ||||
|         originalname: file.originalname, | ||||
|         size: file.size, | ||||
|         url: fileUrl, | ||||
|         path: fileUrl | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     logger.info('身份证上传成功', { | ||||
|       agentId: req.agent.id, | ||||
|       hasFront: !!result.front, | ||||
|       hasBack: !!result.back | ||||
|     }); | ||||
|  | ||||
|     res.json({ | ||||
|       success: true, | ||||
|       message: '身份证上传成功', | ||||
|       data: result | ||||
|     }); | ||||
|  | ||||
|   } catch (error) { | ||||
|     logger.error('身份证上传失败', { | ||||
|       error: error.message, | ||||
|       stack: error.stack, | ||||
|       agentId: req.agent?.id | ||||
|     }); | ||||
|      | ||||
|     res.status(500).json({ | ||||
|       success: false, | ||||
|       message: error.message || '身份证上传失败' | ||||
|     }); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * 营业执照上传 | ||||
|  * POST /api/upload/license | ||||
|  */ | ||||
| router.post('/license', agentAuth, upload.single('business_license'), async (req, res) => { | ||||
|   try { | ||||
|     if (!req.file) { | ||||
|       return res.status(400).json({ | ||||
|         success: false, | ||||
|         message: '请选择营业执照文件' | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     const file = req.file; | ||||
|     const relativePath = path.relative(path.join(__dirname, '../'), file.path).replace(/\\/g, '/'); | ||||
|     const fileUrl = `/uploads/${relativePath.split('/').slice(1).join('/')}`; | ||||
|  | ||||
|     logger.info('营业执照上传成功', { | ||||
|       agentId: req.agent.id, | ||||
|       filename: file.filename, | ||||
|       size: file.size | ||||
|     }); | ||||
|  | ||||
|     res.json({ | ||||
|       success: true, | ||||
|       message: '营业执照上传成功', | ||||
|       data: { | ||||
|         filename: file.filename, | ||||
|         originalname: file.originalname, | ||||
|         size: file.size, | ||||
|         url: fileUrl, | ||||
|         path: fileUrl | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|   } catch (error) { | ||||
|     logger.error('营业执照上传失败', { | ||||
|       error: error.message, | ||||
|       stack: error.stack, | ||||
|       agentId: req.agent?.id | ||||
|     }); | ||||
|      | ||||
|     res.status(500).json({ | ||||
|       success: false, | ||||
|       message: error.message || '营业执照上传失败' | ||||
|     }); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * 删除文件 | ||||
|  * DELETE /api/upload/file | ||||
|  */ | ||||
| router.delete('/file', agentAuth, async (req, res) => { | ||||
|   try { | ||||
|     const { path: filePath } = req.body; | ||||
|      | ||||
|     if (!filePath) { | ||||
|       return res.status(400).json({ | ||||
|         success: false, | ||||
|         message: '请提供文件路径' | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     // 构建完整的文件路径 | ||||
|     const fullPath = path.join(__dirname, '../', filePath); | ||||
|      | ||||
|     // 检查文件是否存在 | ||||
|     if (!fs.existsSync(fullPath)) { | ||||
|       return res.status(404).json({ | ||||
|         success: false, | ||||
|         message: '文件不存在' | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     // 删除文件 | ||||
|     fs.unlinkSync(fullPath); | ||||
|  | ||||
|     logger.info('文件删除成功', { | ||||
|       agentId: req.agent.id, | ||||
|       filePath: filePath | ||||
|     }); | ||||
|  | ||||
|     res.json({ | ||||
|       success: true, | ||||
|       message: '文件删除成功' | ||||
|     }); | ||||
|  | ||||
|   } catch (error) { | ||||
|     logger.error('文件删除失败', { | ||||
|       error: error.message, | ||||
|       stack: error.stack, | ||||
|       agentId: req.agent?.id | ||||
|     }); | ||||
|      | ||||
|     res.status(500).json({ | ||||
|       success: false, | ||||
|       message: '文件删除失败' | ||||
|     }); | ||||
|   } | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| // 错误处理中间件 | ||||
| router.use((error, req, res, next) => { | ||||
|   if (error instanceof multer.MulterError) { | ||||
|     if (error.code === 'LIMIT_FILE_SIZE') { | ||||
|       return res.status(400).json({ | ||||
|         success: false, | ||||
|         message: '文件大小超过限制(最大5MB)' | ||||
|       }); | ||||
|       return res.status(400).json({ success: false, message: '文件大小不能超过10MB' }); | ||||
|     } | ||||
|     if (error.code === 'LIMIT_FILE_COUNT') { | ||||
|       return res.status(400).json({ | ||||
|         success: false, | ||||
|         message: '文件数量超过限制(最多10个)' | ||||
|       }); | ||||
|     } | ||||
|     if (error.code === 'LIMIT_UNEXPECTED_FILE') { | ||||
|       return res.status(400).json({ | ||||
|         success: false, | ||||
|         message: '意外的文件字段' | ||||
|       }); | ||||
|       return res.status(400).json({ success: false, message: '一次最多只能上传10个文件' }); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   res.status(500).json({ | ||||
|     success: false, | ||||
|     message: error.message || '文件上传失败' | ||||
|   }); | ||||
|  | ||||
|   if (error.message === '只能上传图片或视频文件') { | ||||
|     return res.status(400).json({ success: false, message: error.message }); | ||||
|   } | ||||
|  | ||||
|   res.status(500).json({ success: false, message: '上传失败' }); | ||||
| }); | ||||
|  | ||||
| module.exports = router; | ||||
							
								
								
									
										646
									
								
								routes/users.js
									
									
									
									
									
								
							
							
						
						
									
										646
									
								
								routes/users.js
									
									
									
									
									
								
							| @@ -1,344 +1,362 @@ | ||||
| const express = require('express'); | ||||
| const router = express.Router(); | ||||
| const { getDB } = require('../database'); | ||||
| const { agentAuth } = require('../middleware/agentAuth'); | ||||
| const { logger } = require('../config/logger'); | ||||
| const {getDB} = require('../database'); | ||||
| const {agentAuth} = require('../middleware/agentAuth'); | ||||
| const {logger} = require('../config/logger'); | ||||
| const bcrypt = require('bcryptjs'); | ||||
|  | ||||
| /** | ||||
|  * 获取代理下级用户列表 | ||||
|  * GET /api/users | ||||
|  */ | ||||
| router.get('/', agentAuth, async (req, res) => { | ||||
|   try { | ||||
|     const agentId = req.agent.id; | ||||
|     const {  | ||||
|       page = 1,  | ||||
|       limit = 20,  | ||||
|       search,  | ||||
|       role,  | ||||
|       sort_by = 'created_at',  | ||||
|       sort_order = 'desc', | ||||
|       city, | ||||
|       district | ||||
|     } = req.query; | ||||
|     try { | ||||
|         const agentId = req.agent.id; | ||||
|         const { | ||||
|             page = 1, | ||||
|             limit = 20, | ||||
|             search, | ||||
|             role, | ||||
|             sort_by = 'created_at', | ||||
|             sort_order = 'desc', | ||||
|             city, | ||||
|             district | ||||
|         } = req.query; | ||||
|  | ||||
|     const pageNum = parseInt(page) || 1; | ||||
|     const limitNum = parseInt(limit) || 20; | ||||
|     const offset = (pageNum - 1) * limitNum; | ||||
|         const pageNum = parseInt(page) || 1; | ||||
|         const limitNum = parseInt(limit) || 20; | ||||
|         const offset = (pageNum - 1) * limitNum; | ||||
|  | ||||
|     // 构建查询条件 | ||||
|     let whereConditions = ['am.agent_id = ?']; | ||||
|     let queryParams = [agentId]; | ||||
|         // 构建查询条件 | ||||
|         let whereConditions = ['am.agent_id = ?']; | ||||
|         let queryParams = [agentId]; | ||||
|  | ||||
|     if (search) { | ||||
|       whereConditions.push('(u.username LIKE ? OR u.real_name LIKE ? OR u.phone LIKE ?)'); | ||||
|       queryParams.push(`%${search}%`, `%${search}%`, `%${search}%`); | ||||
|         if (search) { | ||||
|             whereConditions.push('(u.username LIKE ? OR u.real_name LIKE ? OR u.phone LIKE ?)'); | ||||
|             queryParams.push(`%${search}%`, `%${search}%`, `%${search}%`); | ||||
|         } | ||||
|  | ||||
|         if (role) { | ||||
|             whereConditions.push('u.role = ?'); | ||||
|             queryParams.push(role); | ||||
|         } | ||||
|  | ||||
|         if (city) { | ||||
|             whereConditions.push('u.city = ?'); | ||||
|             queryParams.push(city); | ||||
|         } | ||||
|  | ||||
|         if (district) { | ||||
|             whereConditions.push('u.district_id = ?'); | ||||
|             queryParams.push(district); | ||||
|         } | ||||
|  | ||||
|         const whereClause = whereConditions.join(' AND '); | ||||
|  | ||||
|         // 验证排序字段 | ||||
|         const allowedSortFields = ['created_at', 'updated_at', 'balance', 'username', 'real_name']; | ||||
|         const sortBy = allowedSortFields.includes(sort_by) ? sort_by : 'created_at'; | ||||
|         const sortOrder = sort_order.toLowerCase() === 'asc' ? 'ASC' : 'DESC'; | ||||
|  | ||||
|         // 查询用户列表 | ||||
|         const usersQuery = ` | ||||
|             SELECT u.id, | ||||
|                    u.username, | ||||
|                    u.real_name, | ||||
|                    u.phone, | ||||
|                    u.email, | ||||
|                    u.avatar, | ||||
|                    u.role, | ||||
|                    u.city, | ||||
|                    u.balance, | ||||
|                    u.points, | ||||
|                    u.created_at, | ||||
|                    u.updated_at, | ||||
|                    am.created_at                        as join_date, | ||||
|                    (SELECT CAST(COALESCE(SUM(amount), 0) AS DECIMAL(10, 2)) | ||||
|                     FROM transfers | ||||
|                     WHERE from_user_id = u.id | ||||
|                       AND DATE(created_at) = CURDATE()) as today_transfer_out, | ||||
|                    (SELECT CAST(COALESCE(SUM(amount), 0) AS DECIMAL(10, 2)) | ||||
|                     FROM transfers | ||||
|                     WHERE to_user_id = u.id | ||||
|                       AND DATE(created_at) = CURDATE()) as today_transfer_in | ||||
|             FROM agent_merchants am | ||||
|                      LEFT JOIN users u ON am.merchant_id = u.id | ||||
|             WHERE ${whereClause} | ||||
|             ORDER BY u.${sortBy} ${sortOrder} | ||||
|             LIMIT ${limitNum} OFFSET ${offset} | ||||
|         `; | ||||
|  | ||||
|         const [users] = await getDB().execute(usersQuery, queryParams); | ||||
|  | ||||
|         // 查询总数 | ||||
|         const countQuery = ` | ||||
|             SELECT COUNT(*) as total | ||||
|             FROM agent_merchants am | ||||
|                      LEFT JOIN users u ON am.merchant_id = u.id | ||||
|             WHERE ${whereClause} | ||||
|         `; | ||||
|  | ||||
|         const [countResult] = await getDB().execute(countQuery, queryParams); | ||||
|         const total = countResult[0]?.total || 0; | ||||
|  | ||||
|         // 查询统计信息 | ||||
|         const [statsResult] = await getDB().execute(` | ||||
|             SELECT COUNT(*)                                                    as total_users, | ||||
|                    COUNT(CASE WHEN u.audit_status = 'approved' THEN 1 END)     as active_users, | ||||
|                    CAST(COALESCE(SUM(u.balance), 0) AS DECIMAL(10, 2))         as total_balance, | ||||
|                    COUNT(CASE WHEN DATE(am.created_at) = CURDATE() THEN 1 END) as today_new_users | ||||
|             FROM agent_merchants am | ||||
|                      LEFT JOIN users u ON am.merchant_id = u.id | ||||
|             WHERE am.agent_id = ? | ||||
|         `, [agentId]); | ||||
|  | ||||
|         const stats = statsResult[0] || { | ||||
|             total_users: 0, | ||||
|             active_users: 0, | ||||
|             total_balance: '0.00', | ||||
|             today_new_users: 0 | ||||
|         }; | ||||
|  | ||||
|         res.json({ | ||||
|             success: true, | ||||
|             data: { | ||||
|                 users, | ||||
|                 pagination: { | ||||
|                     current_page: pageNum, | ||||
|                     per_page: limitNum, | ||||
|                     total, | ||||
|                     total_pages: Math.ceil(total / limitNum) | ||||
|                 }, | ||||
|                 stats | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|     } catch (error) { | ||||
|         logger.error('获取用户列表失败', { | ||||
|             error: error.message, | ||||
|             stack: error.stack, | ||||
|             agentId: req.agent?.id | ||||
|         }); | ||||
|  | ||||
|         res.status(500).json({ | ||||
|             success: false, | ||||
|             message: '获取用户列表失败' | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     if (role) { | ||||
|       whereConditions.push('u.role = ?'); | ||||
|       queryParams.push(role); | ||||
|     } | ||||
|  | ||||
|     if (city) { | ||||
|       whereConditions.push('u.city = ?'); | ||||
|       queryParams.push(city); | ||||
|     } | ||||
|  | ||||
|     if (district) { | ||||
|       whereConditions.push('u.district = ?'); | ||||
|       queryParams.push(district); | ||||
|     } | ||||
|  | ||||
|     const whereClause = whereConditions.join(' AND '); | ||||
|  | ||||
|     // 验证排序字段 | ||||
|     const allowedSortFields = ['created_at', 'updated_at', 'balance', 'username', 'real_name']; | ||||
|     const sortBy = allowedSortFields.includes(sort_by) ? sort_by : 'created_at'; | ||||
|     const sortOrder = sort_order.toLowerCase() === 'asc' ? 'ASC' : 'DESC'; | ||||
|  | ||||
|     // 查询用户列表 | ||||
|     const usersQuery = ` | ||||
|       SELECT  | ||||
|         u.id, | ||||
|         u.username, | ||||
|         u.real_name, | ||||
|         u.phone, | ||||
|         u.email, | ||||
|         u.avatar, | ||||
|         u.role, | ||||
|         u.city, | ||||
|         u.balance, | ||||
|         u.points, | ||||
|         u.created_at, | ||||
|         u.updated_at, | ||||
|         am.created_at as join_date, | ||||
|         ( | ||||
|           SELECT CAST(COALESCE(SUM(amount), 0) AS DECIMAL(10,2)) | ||||
|           FROM transfers  | ||||
|           WHERE from_user_id = u.id  | ||||
|             AND DATE(created_at) = CURDATE() | ||||
|         ) as today_transfer_out, | ||||
|         ( | ||||
|           SELECT CAST(COALESCE(SUM(amount), 0) AS DECIMAL(10,2)) | ||||
|           FROM transfers  | ||||
|           WHERE to_user_id = u.id  | ||||
|             AND DATE(created_at) = CURDATE() | ||||
|         ) as today_transfer_in | ||||
|       FROM agent_merchants am | ||||
|       LEFT JOIN users u ON am.merchant_id = u.id | ||||
|       WHERE ${whereClause} | ||||
|       ORDER BY u.${sortBy} ${sortOrder} | ||||
|       LIMIT ${limitNum} OFFSET ${offset} | ||||
|     `; | ||||
|  | ||||
|     const [users] = await getDB().execute(usersQuery, queryParams); | ||||
|  | ||||
|     // 查询总数 | ||||
|     const countQuery = ` | ||||
|       SELECT COUNT(*) as total | ||||
|       FROM agent_merchants am | ||||
|       LEFT JOIN users u ON am.merchant_id = u.id | ||||
|       WHERE ${whereClause} | ||||
|     `; | ||||
|      | ||||
|     const [countResult] = await getDB().execute(countQuery, queryParams); | ||||
|     const total = countResult[0]?.total || 0; | ||||
|  | ||||
|     // 查询统计信息 | ||||
|     const [statsResult] = await getDB().execute(` | ||||
|       SELECT  | ||||
|         COUNT(*) as total_users, | ||||
|         COUNT(CASE WHEN u.audit_status = 'approved' THEN 1 END) as active_users, | ||||
|         CAST(COALESCE(SUM(u.balance), 0) AS DECIMAL(10,2)) as total_balance, | ||||
|         COUNT(CASE WHEN DATE(am.created_at) = CURDATE() THEN 1 END) as today_new_users | ||||
|       FROM agent_merchants am | ||||
|       LEFT JOIN users u ON am.merchant_id = u.id | ||||
|       WHERE am.agent_id = ? | ||||
|     `, [agentId]); | ||||
|  | ||||
|     const stats = statsResult[0] || { | ||||
|       total_users: 0, | ||||
|       active_users: 0, | ||||
|       total_balance: '0.00', | ||||
|       today_new_users: 0 | ||||
|     }; | ||||
|  | ||||
|     res.json({ | ||||
|       success: true, | ||||
|       data: { | ||||
|         users, | ||||
|         pagination: { | ||||
|           current_page: pageNum, | ||||
|           per_page: limitNum, | ||||
|           total, | ||||
|           total_pages: Math.ceil(total / limitNum) | ||||
|         }, | ||||
|         stats | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|   } catch (error) { | ||||
|     logger.error('获取用户列表失败', { | ||||
|       error: error.message, | ||||
|       stack: error.stack, | ||||
|       agentId: req.agent?.id | ||||
|     }); | ||||
|      | ||||
|     res.status(500).json({ | ||||
|       success: false, | ||||
|       message: '获取用户列表失败' | ||||
|     }); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * 获取单个用户详情 | ||||
|  * GET /api/users/:id | ||||
|  * 获取直营列表 | ||||
|  * | ||||
|  */ | ||||
| router.get('/:id', agentAuth, async (req, res) => { | ||||
|   try { | ||||
|     const agentId = req.agent.id; | ||||
|     const userId = req.params.id; | ||||
| router.get('/directly_operated', agentAuth, async (req, res) => { | ||||
|     try { | ||||
|         const agentId = req.agent.userId; // 修正为正确的字段名 | ||||
|         const { | ||||
|             page = 1, | ||||
|             size = 20, | ||||
|             search, | ||||
|             sort_by = 'created_at', | ||||
|             sort_order = 'desc', | ||||
|         } = req.query; | ||||
|  | ||||
|     // 验证用户是否属于当前代理 | ||||
|     const [users] = await getDB().execute(` | ||||
|       SELECT  | ||||
|         u.id, | ||||
|         u.username, | ||||
|         u.real_name, | ||||
|         u.phone, | ||||
|         u.email, | ||||
|         u.avatar, | ||||
|         u.role, | ||||
|         u.city, | ||||
|         u.district, | ||||
|         u.balance, | ||||
|         u.points, | ||||
|         u.id_card, | ||||
|         u.business_license, | ||||
|         u.payment_qr_code, | ||||
|         u.created_at, | ||||
|         u.updated_at, | ||||
|         am.created_at as join_date | ||||
|       FROM agent_merchants am | ||||
|       LEFT JOIN users u ON am.merchant_id = u.id | ||||
|       WHERE am.agent_id = ? AND u.id = ? | ||||
|     `, [agentId, userId]); | ||||
|         // 转换分页参数 | ||||
|         const pageNum = parseInt(page); | ||||
|         const pageSize = parseInt(size); | ||||
|         const offset = (pageNum - 1) * pageSize; | ||||
|  | ||||
|     if (users.length === 0) { | ||||
|       return res.status(404).json({ | ||||
|         success: false, | ||||
|         message: '用户不存在或不属于当前代理' | ||||
|       }); | ||||
|         // 构建查询条件 | ||||
|         let whereConditions = ['u.inviter = ?']; | ||||
|         let queryParams = [agentId]; | ||||
|  | ||||
|         // 添加搜索条件 | ||||
|         if (search) { | ||||
|             whereConditions.push('(u.username LIKE ? OR u.real_name LIKE ? OR u.phone LIKE ?)'); | ||||
|             const searchPattern = `%${search}%`; | ||||
|             queryParams.push(searchPattern, searchPattern, searchPattern); | ||||
|         } | ||||
|  | ||||
|         const whereClause = whereConditions.join(' AND '); | ||||
|  | ||||
|         // 验证排序字段和顺序 | ||||
|         const validSortFields = ['id', 'username', 'real_name', 'created_at', 'updated_at', 'balance', 'points']; | ||||
|         const validSortOrders = ['asc', 'desc']; | ||||
|         const sortField = validSortFields.includes(sort_by) ? sort_by : 'created_at'; | ||||
|         const sortOrder = validSortOrders.includes(sort_order.toLowerCase()) ? sort_order.toUpperCase() : 'DESC'; | ||||
|  | ||||
|         // 查询总数 | ||||
|         const [countResult] = await getDB().execute(` | ||||
|             SELECT COUNT(*) as total | ||||
|             FROM users u | ||||
|             WHERE ${whereClause} | ||||
|         `, queryParams); | ||||
|         const total = countResult[0].total; | ||||
|  | ||||
|         // 查询用户列表 | ||||
|         const [users] = await getDB().execute(` | ||||
|             SELECT u.* | ||||
|             FROM users u | ||||
|             WHERE ${whereClause} | ||||
|             ORDER BY u.${sortField} ${sortOrder} | ||||
|             LIMIT ${pageSize} OFFSET ${offset} | ||||
|         `, [...queryParams]); | ||||
|  | ||||
|         // 计算分页信息 | ||||
|         const totalPages = Math.ceil(total / pageSize); | ||||
|         const hasNextPage = pageNum < totalPages; | ||||
|         const hasPrevPage = pageNum > 1; | ||||
|  | ||||
|         res.json({ | ||||
|             success: true, | ||||
|             data: { | ||||
|                 users, | ||||
|                 pagination: { | ||||
|                     current_page: pageNum, | ||||
|                     per_page: pageSize, | ||||
|                     total, | ||||
|                     total_pages: totalPages, | ||||
|                     has_next_page: hasNextPage, | ||||
|                     has_prev_page: hasPrevPage | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } catch (error) { | ||||
|         logger.error('获取直营用户列表失败', { | ||||
|             error: error.message, | ||||
|             stack: error.stack, | ||||
|             agentId: req.agent?.id | ||||
|         }); | ||||
|  | ||||
|         res.status(500).json({ | ||||
|             success: false, | ||||
|             message: '获取直营用户列表失败' | ||||
|         }); | ||||
|     } | ||||
| }) | ||||
|  | ||||
|     const user = users[0]; | ||||
|  | ||||
|     // 获取用户转账统计 | ||||
|     const [transferStats] = await getDB().execute(` | ||||
|       SELECT  | ||||
|         COUNT(*) as total_transfers, | ||||
|         CAST(COALESCE(SUM(CASE WHEN from_user_id = ? THEN amount ELSE 0 END), 0) AS DECIMAL(10,2)) as total_transfer_out, | ||||
|         CAST(COALESCE(SUM(CASE WHEN to_user_id = ? THEN amount ELSE 0 END), 0) AS DECIMAL(10,2)) as total_transfer_in, | ||||
|         COUNT(CASE WHEN DATE(created_at) = CURDATE() THEN 1 END) as today_transfers, | ||||
|         CAST(COALESCE(SUM(CASE WHEN from_user_id = ? AND DATE(created_at) = CURDATE() THEN amount ELSE 0 END), 0) AS DECIMAL(10,2)) as today_transfer_out, | ||||
|         CAST(COALESCE(SUM(CASE WHEN to_user_id = ? AND DATE(created_at) = CURDATE() THEN amount ELSE 0 END), 0) AS DECIMAL(10,2)) as today_transfer_in | ||||
|       FROM transfers  | ||||
|       WHERE from_user_id = ? OR to_user_id = ? | ||||
|     `, [userId, userId, userId, userId, userId, userId]); | ||||
|  | ||||
|     user.transfer_stats = transferStats[0] || { | ||||
|       total_transfers: 0, | ||||
|       total_transfer_out: '0.00', | ||||
|       total_transfer_in: '0.00', | ||||
|       today_transfers: 0, | ||||
|       today_transfer_out: '0.00', | ||||
|       today_transfer_in: '0.00' | ||||
|     }; | ||||
|  | ||||
|     res.json({ | ||||
|       success: true, | ||||
|       data: user | ||||
|     }); | ||||
|  | ||||
|   } catch (error) { | ||||
|     logger.error('获取用户详情失败', { | ||||
|       error: error.message, | ||||
|       stack: error.stack, | ||||
|       agentId: req.agent?.id, | ||||
|       userId: req.params.id | ||||
|     }); | ||||
|      | ||||
|     res.status(500).json({ | ||||
|       success: false, | ||||
|       message: '获取用户详情失败' | ||||
|     }); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * 导出用户数据 | ||||
|  * GET /api/users/export | ||||
|  * 创建直营用户 | ||||
|  */ | ||||
| router.get('/export/data', agentAuth, async (req, res) => { | ||||
|   try { | ||||
|     const agentId = req.agent.id; | ||||
|     const { format = 'json', search, role, city, district } = req.query; | ||||
| router.post('/create', agentAuth, async (req, res) => { | ||||
|     try { | ||||
|         const db = getDB(); | ||||
|         const agentId = req.agent.userId; | ||||
|         const {username, real_name, phone, password, avatar} = req.body; | ||||
|  | ||||
|     // 构建查询条件 | ||||
|     let whereConditions = ['am.agent_id = ?']; | ||||
|     let queryParams = [agentId]; | ||||
|         // 验证必填字段 | ||||
|         if (!username || !real_name || !phone || !password) { | ||||
|             return res.status(400).json({ | ||||
|                 success: false, | ||||
|                 message: '请填写完整用户信息' | ||||
|             }); | ||||
|         } | ||||
|         // 检查用户是否已存在 | ||||
|         const [existingUsers] = await db.execute( | ||||
|             'SELECT id FROM users WHERE username = ? OR phone = ?', | ||||
|             [username, phone || null] | ||||
|         ); | ||||
|  | ||||
|     if (search) { | ||||
|       whereConditions.push('(u.username LIKE ? OR u.real_name LIKE ? OR u.phone LIKE ?)'); | ||||
|       queryParams.push(`%${search}%`, `%${search}%`, `%${search}%`); | ||||
|         if (existingUsers.length > 0) { | ||||
|             return res.status(400).json({success: false, message: '用户名或手机号已存在'}); | ||||
|         } | ||||
|         const hashedPassword = await bcrypt.hash(password, 10); | ||||
|         const [agent] = await db.execute('SELECT * FROM users WHERE id = ?', [agentId]); | ||||
|         console.log(agent, 'agent'); | ||||
|         const {city, district_id, province} = agent[0]; | ||||
|         const [result] = await db.execute( | ||||
|             'INSERT INTO users (username, phone, password, avatar, points, audit_status, city, district_id, payment_status, province, inviter,user_type,real_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, "paid", ?, ?,?,?)', | ||||
|             [username, phone, hashedPassword, avatar, 0, 'approved', city, district_id, province, agentId, 'directly_operated',real_name] | ||||
|         ); | ||||
|         const userId = result.insertId; | ||||
|         res.json({ | ||||
|             success: true, | ||||
|             message: '直营用户创建成功', | ||||
|             userId | ||||
|         }); | ||||
|     } catch (error) { | ||||
|         logger.error('创建直营用户失败', { | ||||
|             error: error.message, | ||||
|             stack: error.stack, | ||||
|             agentId: req.agent?.id | ||||
|         }); | ||||
|  | ||||
|         res.status(500).json({ | ||||
|             success: false, | ||||
|             message: '创建直营用户失败' | ||||
|         }); | ||||
|     } | ||||
| }) | ||||
|  | ||||
|     if (role) { | ||||
|       whereConditions.push('u.role = ?'); | ||||
|       queryParams.push(role); | ||||
| /** | ||||
|  * 直营用户提现 | ||||
|  * | ||||
|  */ | ||||
| router.post('/withdraw', agentAuth, async (req, res) => { | ||||
|     const db = getDB(); | ||||
|     try { | ||||
|         const agentId = req.agent.userId; | ||||
|         const {userId, amount} = req.body; | ||||
|  | ||||
|         // 验证必填字段 | ||||
|         if (!userId || !amount) { | ||||
|             return res.status(400).json({ | ||||
|                 success: false, | ||||
|                 message: '请填写完整提现信息' | ||||
|             }); | ||||
|         } | ||||
|         let [userInfo] = await db.execute( | ||||
|             `SELECT * | ||||
|              FROM users | ||||
|              WHERE id = ? | ||||
|                AND user_type = 'directly_operated'`, | ||||
|             [userId] | ||||
|         ) | ||||
|  | ||||
|         if (userInfo.length > 0) { | ||||
|             let balance = Math.abs(userInfo[0].balance) | ||||
|             if (balance >= amount) { | ||||
|                 await db.query('START TRANSACTION'); | ||||
|                 await db.execute(`UPDATE users | ||||
|                                   SET balance = balance + ? | ||||
|                                   WHERE id = ?`, [amount, userId]) | ||||
|                 await db.execute( | ||||
|                     'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', | ||||
|                     [agentId, userId, 'agent_to_operated', 'received', amount, '直营商户提现', 'agent'] | ||||
|                 ); | ||||
|                 // 提交事务 | ||||
|                 await db.query('COMMIT'); | ||||
|                 res.json({ | ||||
|                     success: true, | ||||
|                     message: '提现成功' | ||||
|                 }) | ||||
|             } else { | ||||
|                 res.status(400).json({ | ||||
|                     success: false, | ||||
|                     message: '用户余额不足' | ||||
|                 }) | ||||
|             } | ||||
|  | ||||
|         } else { | ||||
|             res.status(400).json({ | ||||
|                 success: false, | ||||
|                 message: '无此直营用户' | ||||
|             }) | ||||
|         } | ||||
|     } catch (error) { | ||||
|         await db.query('ROLLBACK'); | ||||
|         logger.error('直营用户提现失败', { | ||||
|             error: error.message, | ||||
|             stack: error.stack, | ||||
|             agentId: req.agent?.id | ||||
|         }); | ||||
|         res.status(500).json({ | ||||
|             success: false, | ||||
|             message: '系统错误请联系管理员' | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     if (city) { | ||||
|       whereConditions.push('u.city = ?'); | ||||
|       queryParams.push(city); | ||||
|     } | ||||
|  | ||||
|     if (district) { | ||||
|       whereConditions.push('u.district = ?'); | ||||
|       queryParams.push(district); | ||||
|     } | ||||
|  | ||||
|     const whereClause = whereConditions.join(' AND '); | ||||
|  | ||||
|     // 查询用户数据 | ||||
|     const [users] = await getDB().execute(` | ||||
|       SELECT  | ||||
|         u.id, | ||||
|         u.username, | ||||
|         u.real_name, | ||||
|         u.phone, | ||||
|         u.email, | ||||
|         u.role, | ||||
|         u.city, | ||||
|         u.balance, | ||||
|         u.points, | ||||
|         u.created_at, | ||||
|         am.created_at as join_date | ||||
|       FROM agent_merchants am | ||||
|       LEFT JOIN users u ON am.merchant_id = u.id | ||||
|       WHERE ${whereClause} | ||||
|       ORDER BY u.created_at DESC | ||||
|     `, queryParams); | ||||
|  | ||||
|     if (format === 'csv') { | ||||
|       // 生成CSV格式 | ||||
|       const csvHeader = 'ID,用户名,真实姓名,手机号,邮箱,角色,城市,地区,账户类型,余额,积分,状态,注册时间,加入时间\n'; | ||||
|       const csvData = users.map(user => { | ||||
|         return [ | ||||
|           user.id, | ||||
|           user.username || '', | ||||
|           user.real_name || '', | ||||
|           user.phone || '', | ||||
|           user.email || '', | ||||
|           user.role || '', | ||||
|           user.city || '', | ||||
|           user.district || '', | ||||
|           user.account_type || '', | ||||
|           user.balance || '0.00', | ||||
|           user.points || 0, | ||||
|           user.status || '', | ||||
|           user.created_at || '', | ||||
|           user.join_date || '' | ||||
|         ].join(','); | ||||
|       }).join('\n'); | ||||
|  | ||||
|       res.setHeader('Content-Type', 'text/csv; charset=utf-8'); | ||||
|       res.setHeader('Content-Disposition', `attachment; filename="users_${Date.now()}.csv"`); | ||||
|       res.send(csvHeader + csvData); | ||||
|     } else { | ||||
|       // 默认JSON格式 | ||||
|       res.json({ | ||||
|         success: true, | ||||
|         data: users, | ||||
|         exported_at: new Date().toISOString(), | ||||
|         total: users.length | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|   } catch (error) { | ||||
|     logger.error('导出用户数据失败', { | ||||
|       error: error.message, | ||||
|       stack: error.stack, | ||||
|       agentId: req.agent?.id | ||||
|     }); | ||||
|      | ||||
|     res.status(500).json({ | ||||
|       success: false, | ||||
|       message: '导出用户数据失败' | ||||
|     }); | ||||
|   } | ||||
| }); | ||||
| }) | ||||
|  | ||||
| module.exports = router; | ||||
		Reference in New Issue
	
	Block a user