This commit is contained in:
2025-09-10 18:09:38 +08:00
parent 141d1313d6
commit 185bc88e21
17 changed files with 2976 additions and 1275 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;