初次提交

This commit is contained in:
2025-09-04 10:49:10 +08:00
commit e704c8abca
26 changed files with 8917 additions and 0 deletions

294
routes/agent.js Normal file
View File

@@ -0,0 +1,294 @@
const express = require('express');
const router = express.Router();
const { getDB } = require('../database');
const { agentAuth } = require('../middleware/agentAuth');
const { logger } = require('../config/logger');
/**
* 获取代理统计数据
* GET /api/agent/stats
*/
router.get('/stats', agentAuth, async (req, res) => {
try {
const agentId = req.agent.id;
// 获取下级用户统计
const [userStats] = await getDB().execute(`
SELECT
COUNT(*) as total_users,
COUNT(CASE WHEN DATE(created_at) = CURDATE() THEN 1 END) as today_new_users,
COUNT(CASE WHEN last_login_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) THEN 1 END) as active_users,
CAST(COALESCE(SUM(balance), 0) AS DECIMAL(10,2)) as total_balance
FROM agent_merchants
WHERE agent_id = ?
`, [agentId]);
// 获取佣金统计
const [commissionStats] = await getDB().execute(`
SELECT
CAST(COALESCE(SUM(commission_amount), 0) AS DECIMAL(10,2)) as total_commission,
CAST(COALESCE(SUM(CASE WHEN DATE(created_at) = CURDATE() THEN commission_amount ELSE 0 END), 0) AS DECIMAL(10,2)) as today_commission,
CAST(COALESCE(SUM(CASE WHEN status = 'paid' THEN commission_amount ELSE 0 END), 0) AS DECIMAL(10,2)) as paid_commission,
CAST(COALESCE(SUM(CASE WHEN status = 'pending' THEN commission_amount ELSE 0 END), 0) AS DECIMAL(10,2)) as pending_commission
FROM agent_commission_records
WHERE agent_id = ?
`, [agentId]);
// 获取转账统计
const [transferStats] = await getDB().execute(`
SELECT
COUNT(*) as total_transfers,
COUNT(CASE WHEN DATE(created_at) = CURDATE() THEN 1 END) as today_transfers,
CAST(COALESCE(SUM(amount), 0) AS DECIMAL(10,2)) as total_amount,
CAST(COALESCE(SUM(CASE WHEN DATE(created_at) = CURDATE() THEN amount ELSE 0 END), 0) AS DECIMAL(10,2)) as today_amount
FROM transfers t
INNER JOIN agent_merchants am ON (t.from_user_id = am.merchant_id OR t.to_user_id = am.merchant_id)
WHERE am.agent_id = ?
`, [agentId]);
const stats = {
users: userStats[0] || {
total_users: 0,
today_new_users: 0,
active_users: 0,
total_balance: '0.00'
},
commissions: commissionStats[0] || {
total_commission: '0.00',
today_commission: '0.00',
paid_commission: '0.00',
pending_commission: '0.00'
},
transfers: transferStats[0] || {
total_transfers: 0,
today_transfers: 0,
total_amount: '0.00',
today_amount: '0.00'
}
};
res.json({
success: true,
data: stats
});
} catch (error) {
logger.error('获取代理统计数据失败', {
error: error.message,
stack: error.stack,
agentId: req.agent?.id
});
res.status(500).json({
success: false,
message: '获取统计数据失败'
});
}
});
/**
* 获取用户增长趋势数据
* GET /api/agent/user-growth-trend
*/
router.get('/user-growth-trend', agentAuth, async (req, res) => {
try {
const agentId = req.agent.id;
const { days = 7 } = req.query;
const [trendData] = await getDB().execute(`
SELECT
DATE(created_at) as date,
COUNT(*) as count
FROM agent_merchants
WHERE agent_id = ?
AND created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
GROUP BY DATE(created_at)
ORDER BY date ASC
`, [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: '获取趋势数据失败'
});
}
});
/**
* 获取佣金收入趋势数据
* GET /api/agent/commission-trend
*/
router.get('/commission-trend', agentAuth, async (req, res) => {
try {
const agentId = req.agent.id;
const { days = 7 } = req.query;
const [trendData] = await getDB().execute(`
SELECT
DATE(created_at) as date,
CAST(COALESCE(SUM(commission_amount), 0) AS DECIMAL(10,2)) as amount
FROM agent_commission_records
WHERE agent_id = ?
AND created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
GROUP BY DATE(created_at)
ORDER BY date ASC
`, [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: '获取趋势数据失败'
});
}
});
/**
* 获取佣金类型分布数据
* GET /api/agent/commission-distribution
*/
router.get('/commission-distribution', agentAuth, async (req, res) => {
try {
const agentId = req.agent.id;
const [distributionData] = await getDB().execute(`
SELECT
commission_type as type,
COUNT(*) as count,
CAST(COALESCE(SUM(commission_amount), 0) AS DECIMAL(10,2)) as amount
FROM agent_commission_records
WHERE agent_id = ?
GROUP BY commission_type
ORDER BY amount DESC
`, [agentId]);
res.json({
success: true,
data: distributionData
});
} catch (error) {
logger.error('获取佣金分布失败', {
error: error.message,
stack: error.stack,
agentId: req.agent?.id
});
res.status(500).json({
success: false,
message: '获取分布数据失败'
});
}
});
/**
* 获取最新下级用户
* GET /api/agent/recent-users
*/
router.get('/recent-users', agentAuth, async (req, res) => {
try {
const agentId = req.agent.id;
const { limit = 10 } = req.query;
const [recentUsers] = await getDB().execute(`
SELECT
u.id,
u.username,
u.real_name,
u.phone,
u.avatar,
u.balance,
u.created_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 = ?
ORDER BY am.created_at DESC
LIMIT ?
`, [agentId, parseInt(limit)]);
res.json({
success: true,
data: recentUsers
});
} catch (error) {
logger.error('获取最新用户失败', {
error: error.message,
stack: error.stack,
agentId: req.agent?.id
});
res.status(500).json({
success: false,
message: '获取最新用户失败'
});
}
});
/**
* 获取最新佣金记录
* GET /api/agent/recent-commissions
*/
router.get('/recent-commissions', agentAuth, async (req, res) => {
try {
const agentId = req.agent.id;
const { limit = 10 } = req.query;
const [recentCommissions] = await getDB().execute(`
SELECT
acr.id,
acr.commission_type,
acr.commission_amount,
acr.status,
acr.created_at,
u.username,
u.real_name
FROM agent_commission_records acr
LEFT JOIN users u ON acr.merchant_id = u.id
WHERE acr.agent_id = ?
ORDER BY acr.created_at DESC
LIMIT ?
`, [agentId, parseInt(limit)]);
res.json({
success: true,
data: recentCommissions
});
} catch (error) {
logger.error('获取最新佣金失败', {
error: error.message,
stack: error.stack,
agentId: req.agent?.id
});
res.status(500).json({
success: false,
message: '获取最新佣金失败'
});
}
});
module.exports = router;

226
routes/auth.js Normal file
View File

@@ -0,0 +1,226 @@
const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { getDB } = require('../database');
const { logger } = require('../config/logger');
// JWT密钥
const JWT_SECRET = process.env.JWT_SECRET || 'agent_jwt_secret_key_2024';
/**
* 代理登录
* POST /api/auth/login
*/
router.post('/login', async (req, res) => {
try {
const { phone, password } = req.body;
if (!phone || !password) {
return res.status(400).json({
success: false,
message: '请输入手机号和密码'
});
}
// 查询代理信息
const [agents] = await getDB().execute(`
SELECT
ra.id as agent_id,
ra.user_id,
ra.agent_code,
ra.status as agent_status,
ra.region_id,
u.id as user_id,
u.username,
u.phone,
u.password,
u.real_name,
u.avatar,
zr.city_name,
zr.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
WHERE u.phone = ? AND ra.status = 'active'
`, [phone]);
if (agents.length === 0) {
return res.status(401).json({
success: false,
message: '手机号不存在或代理账号未激活'
});
}
const agent = agents[0];
// 验证密码
const isPasswordValid = await bcrypt.compare(password, agent.password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: '密码错误'
});
}
// 生成JWT token
const token = jwt.sign(
{
userId: agent.user_id,
agentId: agent.agent_id,
phone: agent.phone,
role: 'agent'
},
JWT_SECRET,
{ expiresIn: '24h' }
);
// 记录登录日志
logger.info('代理登录成功', {
agentId: agent.agent_id,
phone: agent.phone,
ip: req.ip
});
// 返回登录成功信息
res.json({
success: true,
message: '登录成功',
data: {
token,
agent: {
id: agent.agent_id,
userId: agent.user_id,
agentCode: agent.agent_code,
phone: agent.phone,
realName: agent.real_name,
avatar: agent.avatar,
region: {
id: agent.region_id,
cityName: agent.city_name,
districtName: agent.district_name
}
}
}
});
} catch (error) {
logger.error('代理登录失败', {
error: error.message,
stack: error.stack,
ip: req.ip
});
res.status(500).json({
success: false,
message: '登录失败,请稍后重试'
});
}
});
/**
* 获取当前代理信息
* GET /api/auth/me
*/
router.get('/me', async (req, res) => {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
success: false,
message: '未提供认证令牌'
});
}
// 验证token
const decoded = jwt.verify(token, JWT_SECRET);
// 查询代理信息
const [agents] = await getDB().execute(`
SELECT
ra.id as agent_id,
ra.user_id,
ra.agent_code,
ra.status as agent_status,
ra.region_id,
u.phone,
u.real_name,
u.avatar,
zr.city_name,
zr.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
WHERE ra.id = ? AND ra.status = 'active'
`, [decoded.agentId]);
if (agents.length === 0) {
return res.status(401).json({
success: false,
message: '代理账号不存在或已被禁用'
});
}
const agent = agents[0];
res.json({
success: true,
data: {
agent: {
id: agent.agent_id,
userId: agent.user_id,
agentCode: agent.agent_code,
phone: agent.phone,
realName: agent.real_name,
avatar: agent.avatar,
region: {
id: agent.region_id,
cityName: agent.city_name,
districtName: agent.district_name
}
}
}
});
} catch (error) {
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
message: '无效的认证令牌'
});
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: '认证令牌已过期'
});
}
logger.error('获取代理信息失败', {
error: error.message,
stack: error.stack
});
res.status(500).json({
success: false,
message: '获取用户信息失败'
});
}
});
/**
* 代理登出
* POST /api/auth/logout
*/
router.post('/logout', (req, res) => {
// 由于使用JWT登出主要在前端处理删除token
// 这里只是提供一个标准的登出接口
res.json({
success: true,
message: '登出成功'
});
});
module.exports = router;

540
routes/commissions.js Normal file
View File

@@ -0,0 +1,540 @@
const express = require('express');
const router = express.Router();
const { getDB } = require('../database');
const { agentAuth } = require('../middleware/agentAuth');
const { logger } = require('../config/logger');
/**
* 获取代理佣金记录列表
* GET /api/commissions
*/
router.get('/', agentAuth, async (req, res) => {
try {
const agentId = req.agent.id;
const {
page = 1,
limit = 20,
search,
status,
commission_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;
// 构建查询条件
let whereConditions = ['acr.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 (status) {
whereConditions.push('acr.status = ?');
queryParams.push(status);
}
if (commission_type) {
whereConditions.push('acr.commission_type = ?');
queryParams.push(commission_type);
}
if (start_date) {
whereConditions.push('DATE(acr.created_at) >= ?');
queryParams.push(start_date);
}
if (end_date) {
whereConditions.push('DATE(acr.created_at) <= ?');
queryParams.push(end_date);
}
if (min_amount) {
whereConditions.push('acr.commission_amount >= ?');
queryParams.push(parseFloat(min_amount));
}
if (max_amount) {
whereConditions.push('acr.commission_amount <= ?');
queryParams.push(parseFloat(max_amount));
}
const whereClause = whereConditions.join(' AND ');
// 验证排序字段
const allowedSortFields = ['created_at', 'commission_amount', 'status', 'commission_type'];
const sortBy = allowedSortFields.includes(sort_by) ? sort_by : 'created_at';
const sortOrder = sort_order.toLowerCase() === 'asc' ? 'ASC' : 'DESC';
// 查询佣金记录列表
const commissionsQuery = `
SELECT
acr.id,
acr.agent_id,
acr.merchant_id,
acr.commission_type,
acr.commission_amount,
acr.status,
acr.description,
acr.reference_id,
acr.created_at,
acr.updated_at,
acr.paid_at,
u.username,
u.real_name,
u.phone,
u.avatar
FROM agent_commission_records acr
LEFT JOIN users u ON acr.merchant_id = u.id
WHERE ${whereClause}
ORDER BY acr.${sortBy} ${sortOrder}
LIMIT ${limitNum} OFFSET ${offset}
`;
const [commissions] = await getDB().execute(commissionsQuery, queryParams);
// 查询总数
const countQuery = `
SELECT COUNT(*) as total
FROM agent_commission_records acr
LEFT JOIN users u ON acr.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_commissions,
COUNT(CASE WHEN status = 'pending' THEN 1 END) as pending_commissions,
COUNT(CASE WHEN status = 'paid' THEN 1 END) as paid_commissions,
CAST(COALESCE(SUM(commission_amount), 0) AS DECIMAL(10,2)) as total_amount,
CAST(COALESCE(SUM(CASE WHEN status = 'pending' THEN commission_amount ELSE 0 END), 0) AS DECIMAL(10,2)) as pending_amount,
CAST(COALESCE(SUM(CASE WHEN status = 'paid' THEN commission_amount ELSE 0 END), 0) AS DECIMAL(10,2)) as paid_amount,
CAST(COALESCE(SUM(CASE WHEN DATE(created_at) = CURDATE() THEN commission_amount ELSE 0 END), 0) AS DECIMAL(10,2)) as today_amount
FROM agent_commission_records
WHERE agent_id = ?
`, [agentId]);
const stats = statsResult[0] || {
total_commissions: 0,
pending_commissions: 0,
paid_commissions: 0,
total_amount: '0.00',
pending_amount: '0.00',
paid_amount: '0.00',
today_amount: '0.00'
};
res.json({
success: true,
data: {
commissions,
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/commissions/:id
*/
router.get('/:id', agentAuth, async (req, res) => {
try {
const agentId = req.agent.id;
const commissionId = req.params.id;
// 查询佣金记录详情
const [commissions] = await getDB().execute(`
SELECT
acr.id,
acr.agent_id,
acr.merchant_id,
acr.commission_type,
acr.commission_amount,
acr.status,
acr.description,
acr.reference_id,
acr.created_at,
acr.updated_at,
acr.paid_at,
u.username,
u.real_name,
u.phone,
u.avatar,
u.city,
u.district
FROM agent_commission_records acr
LEFT JOIN users u ON acr.merchant_id = u.id
WHERE acr.id = ? AND acr.agent_id = ?
`, [commissionId, agentId]);
if (commissions.length === 0) {
return res.status(404).json({
success: false,
message: '佣金记录不存在或无权限查看'
});
}
const commission = commissions[0];
res.json({
success: true,
data: commission
});
} catch (error) {
logger.error('获取佣金记录详情失败', {
error: error.message,
stack: error.stack,
agentId: req.agent?.id,
commissionId: req.params.id
});
res.status(500).json({
success: false,
message: '获取佣金记录详情失败'
});
}
});
/**
* 申请佣金发放(单个)
* POST /api/commissions/:id/request-payment
*/
router.post('/:id/request-payment', agentAuth, async (req, res) => {
try {
const agentId = req.agent.id;
const commissionId = req.params.id;
// 检查佣金记录是否存在且属于当前代理
const [commissions] = await getDB().execute(`
SELECT id, status, commission_amount
FROM agent_commission_records
WHERE id = ? AND agent_id = ?
`, [commissionId, agentId]);
if (commissions.length === 0) {
return res.status(404).json({
success: false,
message: '佣金记录不存在或无权限操作'
});
}
const commission = commissions[0];
if (commission.status !== 'pending') {
return res.status(400).json({
success: false,
message: '只能申请待发放状态的佣金'
});
}
// 更新佣金状态为申请中
await getDB().execute(`
UPDATE agent_commission_records
SET status = 'requested', updated_at = NOW()
WHERE id = ?
`, [commissionId]);
logger.info('代理申请佣金发放', {
agentId,
commissionId,
amount: commission.commission_amount
});
res.json({
success: true,
message: '佣金发放申请已提交,请等待审核'
});
} catch (error) {
logger.error('申请佣金发放失败', {
error: error.message,
stack: error.stack,
agentId: req.agent?.id,
commissionId: req.params.id
});
res.status(500).json({
success: false,
message: '申请佣金发放失败'
});
}
});
/**
* 批量申请佣金发放
* POST /api/commissions/batch-request-payment
*/
router.post('/batch-request-payment', agentAuth, async (req, res) => {
try {
const agentId = req.agent.id;
const { commission_ids } = req.body;
if (!commission_ids || !Array.isArray(commission_ids) || commission_ids.length === 0) {
return res.status(400).json({
success: false,
message: '请选择要申请发放的佣金记录'
});
}
// 检查所有佣金记录是否存在且属于当前代理
const placeholders = commission_ids.map(() => '?').join(',');
const [commissions] = await getDB().execute(`
SELECT id, status, commission_amount
FROM agent_commission_records
WHERE id IN (${placeholders}) AND agent_id = ?
`, [...commission_ids, agentId]);
if (commissions.length !== commission_ids.length) {
return res.status(400).json({
success: false,
message: '部分佣金记录不存在或无权限操作'
});
}
// 检查状态
const invalidCommissions = commissions.filter(c => c.status !== 'pending');
if (invalidCommissions.length > 0) {
return res.status(400).json({
success: false,
message: '只能申请待发放状态的佣金'
});
}
// 批量更新状态
await getDB().execute(`
UPDATE agent_commission_records
SET status = 'requested', updated_at = NOW()
WHERE id IN (${placeholders}) AND agent_id = ?
`, [...commission_ids, agentId]);
const totalAmount = commissions.reduce((sum, c) => sum + parseFloat(c.commission_amount), 0);
logger.info('代理批量申请佣金发放', {
agentId,
commissionIds: commission_ids,
count: commission_ids.length,
totalAmount
});
res.json({
success: true,
message: `已提交${commission_ids.length}条佣金发放申请,总金额${totalAmount.toFixed(2)}`
});
} catch (error) {
logger.error('批量申请佣金发放失败', {
error: error.message,
stack: error.stack,
agentId: req.agent?.id
});
res.status(500).json({
success: false,
message: '批量申请佣金发放失败'
});
}
});
/**
* 获取佣金趋势数据
* GET /api/commissions/trend
*/
router.get('/trend/data', agentAuth, async (req, res) => {
try {
const agentId = req.agent.id;
const { days = 7, type = 'amount' } = req.query;
let selectField = 'CAST(COALESCE(SUM(commission_amount), 0) AS DECIMAL(10,2)) as value';
if (type === 'count') {
selectField = 'COUNT(*) as value';
}
const [trendData] = await getDB().execute(`
SELECT
DATE(created_at) as date,
${selectField}
FROM agent_commission_records
WHERE agent_id = ?
AND created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
GROUP BY DATE(created_at)
ORDER BY date ASC
`, [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: '获取佣金趋势失败'
});
}
});
/**
* 导出佣金记录
* GET /api/commissions/export
*/
router.get('/export/data', agentAuth, async (req, res) => {
try {
const agentId = req.agent.id;
const {
format = 'json',
search,
status,
commission_type,
start_date,
end_date,
min_amount,
max_amount
} = req.query;
// 构建查询条件
let whereConditions = ['acr.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 (status) {
whereConditions.push('acr.status = ?');
queryParams.push(status);
}
if (commission_type) {
whereConditions.push('acr.commission_type = ?');
queryParams.push(commission_type);
}
if (start_date) {
whereConditions.push('DATE(acr.created_at) >= ?');
queryParams.push(start_date);
}
if (end_date) {
whereConditions.push('DATE(acr.created_at) <= ?');
queryParams.push(end_date);
}
if (min_amount) {
whereConditions.push('acr.commission_amount >= ?');
queryParams.push(parseFloat(min_amount));
}
if (max_amount) {
whereConditions.push('acr.commission_amount <= ?');
queryParams.push(parseFloat(max_amount));
}
const whereClause = whereConditions.join(' AND ');
// 查询佣金记录
const [commissions] = await getDB().execute(`
SELECT
acr.id,
acr.commission_type,
acr.commission_amount,
acr.status,
acr.description,
acr.reference_id,
acr.created_at,
acr.paid_at,
u.username,
u.real_name,
u.phone
FROM agent_commission_records acr
LEFT JOIN users u ON acr.merchant_id = u.id
WHERE ${whereClause}
ORDER BY acr.created_at DESC
`, queryParams);
if (format === 'csv') {
// 生成CSV格式
const csvHeader = 'ID,佣金类型,佣金金额,状态,描述,关联ID,用户名,真实姓名,手机号,创建时间,发放时间\n';
const csvData = commissions.map(commission => {
return [
commission.id,
commission.commission_type || '',
commission.commission_amount,
commission.status || '',
(commission.description || '').replace(/,/g, ''), // 替换逗号避免CSV格式问题
commission.reference_id || '',
commission.username || '',
commission.real_name || '',
commission.phone || '',
commission.created_at || '',
commission.paid_at || ''
].join(',');
}).join('\n');
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="commissions_${Date.now()}.csv"`);
res.send(csvHeader + csvData);
} else {
// 默认JSON格式
res.json({
success: true,
data: commissions,
exported_at: new Date().toISOString(),
total: commissions.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;

434
routes/transfers.js Normal file
View File

@@ -0,0 +1,434 @@
const express = require('express');
const router = express.Router();
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;
const pageNum = parseInt(page) || 1;
const limitNum = parseInt(limit) || 20;
const offset = (pageNum - 1) * limitNum;
// 构建查询条件
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 (status) {
whereConditions.push('t.status = ?');
queryParams.push(status);
}
if (type) {
whereConditions.push('t.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.type,
t.status,
t.description,
t.transaction_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: '获取转账记录失败'
});
}
});
/**
* 获取单个转账记录详情
* GET /api/transfers/:id
*/
router.get('/:id', agentAuth, async (req, res) => {
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.type,
t.status,
t.description,
t.transaction_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: '转账记录不存在或无权限查看'
});
}
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: '获取转账记录详情失败'
});
}
});
/**
* 获取转账趋势数据
* 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;
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: '获取转账趋势失败'
});
}
});
/**
* 导出转账记录
* 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;
// 构建查询条件
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 (status) {
whereConditions.push('t.status = ?');
queryParams.push(status);
}
if (type) {
whereConditions.push('t.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.type,
t.status,
t.description,
t.transaction_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.transaction_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;

474
routes/upload.js Normal file
View File

@@ -0,0 +1,474 @@
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 uploadDir = path.join(__dirname, '../uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
// 配置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);
}
});
// 文件过滤器
const fileFilter = (req, file, cb) => {
// 允许的图片类型
const allowedImageTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
if (allowedImageTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('只允许上传图片文件 (JPEG, PNG, GIF, WebP)'), false);
}
};
// 配置multer
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB限制
files: 10 // 最多10个文件
}
});
/**
* 单文件上传
* POST /api/upload/single
*/
router.post('/single', agentAuth, upload.single('file'), 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,
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 || '文件上传失败'
});
}
});
/**
* 多文件上传
* POST /api/upload/multiple
*/
router.post('/multiple', agentAuth, upload.array('files', 10), async (req, res) => {
try {
if (!req.files || req.files.length === 0) {
return res.status(400).json({
success: false,
message: '请选择要上传的文件'
});
}
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
};
});
logger.info('多文件上传成功', {
agentId: req.agent.id,
count: files.length,
totalSize: req.files.reduce((sum, file) => sum + file.size, 0)
});
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
}
});
} 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/qrcode
*/
router.post('/qrcode', agentAuth, upload.single('qr_code'), 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 || '收款码上传失败'
});
}
});
/**
* 身份证上传
* 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'
});
}
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: '意外的文件字段'
});
}
}
res.status(500).json({
success: false,
message: error.message || '文件上传失败'
});
});
module.exports = router;

354
routes/users.js Normal file
View File

@@ -0,0 +1,354 @@
const express = require('express');
const router = express.Router();
const { getDB } = require('../database');
const { agentAuth } = require('../middleware/agentAuth');
const { logger } = require('../config/logger');
/**
* 获取代理下级用户列表
* 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;
const pageNum = parseInt(page) || 1;
const limitNum = parseInt(limit) || 20;
const offset = (pageNum - 1) * limitNum;
// 构建查询条件
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 (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.district,
u.account_type,
u.balance,
u.points,
u.status,
u.last_login_at,
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.status = 'active' 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;
// 验证用户是否属于当前代理
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.account_type,
u.balance,
u.points,
u.status,
u.id_card,
u.business_license,
u.payment_qr_code,
u.last_login_at,
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]);
if (users.length === 0) {
return res.status(404).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;
// 构建查询条件
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 (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 [users] = await getDB().execute(`
SELECT
u.id,
u.username,
u.real_name,
u.phone,
u.email,
u.role,
u.city,
u.district,
u.account_type,
u.balance,
u.points,
u.status,
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;