初次提交
This commit is contained in:
294
routes/agent.js
Normal file
294
routes/agent.js
Normal 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
226
routes/auth.js
Normal 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
540
routes/commissions.js
Normal 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
434
routes/transfers.js
Normal 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
474
routes/upload.js
Normal 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
354
routes/users.js
Normal 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;
|
||||
Reference in New Issue
Block a user