代理后端出版
This commit is contained in:
166
routes/agent.js
166
routes/agent.js
@@ -3,6 +3,7 @@ const router = express.Router();
|
||||
const { getDB } = require('../database');
|
||||
const { agentAuth } = require('../middleware/agentAuth');
|
||||
const { logger } = require('../config/logger');
|
||||
const dayjs = require('dayjs');
|
||||
|
||||
/**
|
||||
* 获取代理统计数据
|
||||
@@ -17,8 +18,8 @@ router.get('/stats', agentAuth, async (req, res) => {
|
||||
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
|
||||
COUNT(CASE WHEN updated_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) THEN 1 END) as active_users,
|
||||
0 as total_balance
|
||||
FROM agent_merchants
|
||||
WHERE agent_id = ?
|
||||
`, [agentId]);
|
||||
@@ -28,8 +29,8 @@ router.get('/stats', agentAuth, async (req, res) => {
|
||||
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
|
||||
CAST(COALESCE(SUM(commission_amount), 0) AS DECIMAL(10,2)) as paid_commission,
|
||||
0 as pending_commission
|
||||
FROM agent_commission_records
|
||||
WHERE agent_id = ?
|
||||
`, [agentId]);
|
||||
@@ -38,9 +39,9 @@ router.get('/stats', agentAuth, async (req, res) => {
|
||||
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
|
||||
COUNT(CASE WHEN DATE(t.created_at) = CURDATE() THEN 1 END) as today_transfers,
|
||||
CAST(COALESCE(SUM(t.amount), 0) AS DECIMAL(10,2)) as total_amount,
|
||||
CAST(COALESCE(SUM(CASE WHEN DATE(t.created_at) = CURDATE() THEN t.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 = ?
|
||||
@@ -78,7 +79,7 @@ router.get('/stats', agentAuth, async (req, res) => {
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取统计数据失败'
|
||||
@@ -105,10 +106,27 @@ router.get('/user-growth-trend', agentAuth, async (req, res) => {
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`, [agentId, parseInt(days)]);
|
||||
// 填充缺失的日期(注册数为0)
|
||||
const result = [];
|
||||
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = dayjs().subtract(i, 'day');
|
||||
const dateStr = date.format('YYYY-MM-DD');
|
||||
|
||||
// 修复日期比较:将数据库返回的Date对象转换为字符串进行比较
|
||||
const existingData = trendData.find(item => {
|
||||
const itemDateStr = dayjs(item.date).format('YYYY-MM-DD');
|
||||
return itemDateStr === dateStr;
|
||||
});
|
||||
|
||||
result.push({
|
||||
date: date.format('MM-DD'),
|
||||
count: existingData ? existingData.count : 0
|
||||
});
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
data: trendData
|
||||
data: result
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
@@ -117,7 +135,7 @@ router.get('/user-growth-trend', agentAuth, async (req, res) => {
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取趋势数据失败'
|
||||
@@ -145,9 +163,27 @@ router.get('/commission-trend', agentAuth, async (req, res) => {
|
||||
ORDER BY date ASC
|
||||
`, [agentId, parseInt(days)]);
|
||||
|
||||
// 填充缺失的日期(佣金为0)
|
||||
const result = [];
|
||||
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = dayjs().subtract(i, 'day');
|
||||
const dateStr = date.format('YYYY-MM-DD');
|
||||
|
||||
const existingData = trendData.find(item => {
|
||||
const itemDateStr = dayjs(item.date).format('YYYY-MM-DD');
|
||||
return itemDateStr === dateStr;
|
||||
});
|
||||
|
||||
result.push({
|
||||
date: date.format('MM-DD'),
|
||||
amount: existingData ? parseFloat(existingData.amount) : 0
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: trendData
|
||||
data: result
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
@@ -156,7 +192,7 @@ router.get('/commission-trend', agentAuth, async (req, res) => {
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取趋势数据失败'
|
||||
@@ -164,6 +200,95 @@ router.get('/commission-trend', agentAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取用户活跃度数据
|
||||
* GET /api/agent/user-activity
|
||||
*/
|
||||
router.get('/user-activity', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
const { days = 7 } = req.query;
|
||||
|
||||
// 获取活跃用户趋势(基于转账活动)
|
||||
const [activityTrend] = await getDB().execute(`
|
||||
SELECT
|
||||
DATE(t.created_at) as date,
|
||||
COUNT(DISTINCT CASE WHEN t.from_user_id = am.merchant_id THEN t.from_user_id END) as active_senders,
|
||||
COUNT(DISTINCT CASE WHEN t.to_user_id = am.merchant_id THEN t.to_user_id END) as active_receivers,
|
||||
COUNT(DISTINCT am.merchant_id) as total_active_users
|
||||
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 = ?
|
||||
AND t.created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
|
||||
AND t.status = 'completed'
|
||||
GROUP BY DATE(t.created_at)
|
||||
ORDER BY date ASC
|
||||
`, [agentId, parseInt(days)]);
|
||||
|
||||
// 获取用户活跃度统计
|
||||
const [activityStats] = await getDB().execute(`
|
||||
SELECT
|
||||
COUNT(DISTINCT am.merchant_id) as total_users,
|
||||
COUNT(DISTINCT CASE WHEN t.created_at >= DATE_SUB(NOW(), INTERVAL 1 DAY) THEN am.merchant_id END) as daily_active_users,
|
||||
COUNT(DISTINCT CASE WHEN t.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) THEN am.merchant_id END) as weekly_active_users,
|
||||
COUNT(DISTINCT CASE WHEN t.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN am.merchant_id END) as monthly_active_users,
|
||||
ROUND(
|
||||
COUNT(DISTINCT CASE WHEN t.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) THEN am.merchant_id END) * 100.0 /
|
||||
NULLIF(COUNT(DISTINCT am.merchant_id), 0), 2
|
||||
) as weekly_activity_rate
|
||||
FROM agent_merchants am
|
||||
LEFT JOIN transfers t ON (t.from_user_id = am.merchant_id OR t.to_user_id = am.merchant_id)
|
||||
AND t.status = 'completed'
|
||||
WHERE am.agent_id = ?
|
||||
`, [agentId]);
|
||||
|
||||
// 填充缺失的日期
|
||||
const trendResult = [];
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = dayjs().subtract(i, 'day');
|
||||
const dateStr = date.format('YYYY-MM-DD');
|
||||
|
||||
const existingData = activityTrend.find(item => {
|
||||
const itemDateStr = dayjs(item.date).format('YYYY-MM-DD');
|
||||
return itemDateStr === dateStr;
|
||||
});
|
||||
|
||||
trendResult.push({
|
||||
date: date.format('MM-DD'),
|
||||
active_users: existingData ? existingData.total_active_users : 0,
|
||||
active_senders: existingData ? existingData.active_senders : 0,
|
||||
active_receivers: existingData ? existingData.active_receivers : 0
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
trend: trendResult,
|
||||
stats: activityStats[0] || {
|
||||
total_users: 0,
|
||||
daily_active_users: 0,
|
||||
weekly_active_users: 0,
|
||||
monthly_active_users: 0,
|
||||
weekly_activity_rate: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} 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
|
||||
@@ -194,7 +319,7 @@ router.get('/commission-distribution', agentAuth, async (req, res) => {
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取分布数据失败'
|
||||
@@ -211,6 +336,7 @@ router.get('/recent-users', agentAuth, async (req, res) => {
|
||||
const agentId = req.agent.id;
|
||||
const { limit = 10 } = req.query;
|
||||
|
||||
const limitValue = Math.max(1, Math.min(100, parseInt(limit))); // 限制在1-100之间
|
||||
const [recentUsers] = await getDB().execute(`
|
||||
SELECT
|
||||
u.id,
|
||||
@@ -225,8 +351,8 @@ router.get('/recent-users', agentAuth, async (req, res) => {
|
||||
LEFT JOIN users u ON am.merchant_id = u.id
|
||||
WHERE am.agent_id = ?
|
||||
ORDER BY am.created_at DESC
|
||||
LIMIT ?
|
||||
`, [agentId, parseInt(limit)]);
|
||||
LIMIT ${limitValue}
|
||||
`, [agentId]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -239,7 +365,7 @@ router.get('/recent-users', agentAuth, async (req, res) => {
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取最新用户失败'
|
||||
@@ -256,12 +382,12 @@ router.get('/recent-commissions', agentAuth, async (req, res) => {
|
||||
const agentId = req.agent.id;
|
||||
const { limit = 10 } = req.query;
|
||||
|
||||
const limitValue = Math.max(1, Math.min(100, parseInt(limit))); // 限制在1-100之间
|
||||
const [recentCommissions] = await getDB().execute(`
|
||||
SELECT
|
||||
acr.id,
|
||||
acr.commission_type,
|
||||
acr.commission_amount,
|
||||
acr.status,
|
||||
acr.created_at,
|
||||
u.username,
|
||||
u.real_name
|
||||
@@ -269,8 +395,8 @@ router.get('/recent-commissions', agentAuth, async (req, res) => {
|
||||
LEFT JOIN users u ON acr.merchant_id = u.id
|
||||
WHERE acr.agent_id = ?
|
||||
ORDER BY acr.created_at DESC
|
||||
LIMIT ?
|
||||
`, [agentId, parseInt(limit)]);
|
||||
LIMIT ${limitValue}
|
||||
`, [agentId]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -283,7 +409,7 @@ router.get('/recent-commissions', agentAuth, async (req, res) => {
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取最新佣金失败'
|
||||
|
||||
@@ -14,9 +14,9 @@ const JWT_SECRET = process.env.JWT_SECRET || 'agent_jwt_secret_key_2024';
|
||||
*/
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const { phone, password } = req.body;
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!phone || !password) {
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请输入手机号和密码'
|
||||
@@ -43,7 +43,7 @@ router.post('/login', async (req, res) => {
|
||||
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]);
|
||||
`, [username]);
|
||||
|
||||
if (agents.length === 0) {
|
||||
return res.status(401).json({
|
||||
|
||||
220
routes/captcha.js
Normal file
220
routes/captcha.js
Normal file
@@ -0,0 +1,220 @@
|
||||
const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* 生成随机验证码字符串
|
||||
* @param {number} length 验证码长度
|
||||
* @returns {string} 验证码字符串
|
||||
*/
|
||||
function generateCaptchaText(length = 4) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成SVG验证码图片
|
||||
* @param {string} text 验证码文本
|
||||
* @returns {string} SVG字符串
|
||||
*/
|
||||
function generateCaptchaSVG(text) {
|
||||
const width = 120;
|
||||
const height = 40;
|
||||
const fontSize = 18;
|
||||
|
||||
// 生成随机颜色
|
||||
const getRandomColor = () => {
|
||||
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8'];
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
};
|
||||
|
||||
// 生成干扰线
|
||||
const generateNoise = () => {
|
||||
let noise = '';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const x1 = Math.random() * width;
|
||||
const y1 = Math.random() * height;
|
||||
const x2 = Math.random() * width;
|
||||
const y2 = Math.random() * height;
|
||||
noise += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${getRandomColor()}" stroke-width="1" opacity="0.3"/>`;
|
||||
}
|
||||
return noise;
|
||||
};
|
||||
|
||||
// 生成干扰点
|
||||
const generateDots = () => {
|
||||
let dots = '';
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const x = Math.random() * width;
|
||||
const y = Math.random() * height;
|
||||
const r = Math.random() * 2 + 1;
|
||||
dots += `<circle cx="${x}" cy="${y}" r="${r}" fill="${getRandomColor()}" opacity="0.4"/>`;
|
||||
}
|
||||
return dots;
|
||||
};
|
||||
|
||||
// 生成文字
|
||||
let textElements = '';
|
||||
const charWidth = width / text.length;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i];
|
||||
const x = charWidth * i + charWidth / 2;
|
||||
const y = height / 2 + fontSize / 3;
|
||||
const rotation = (Math.random() - 0.5) * 30; // 随机旋转角度
|
||||
const color = getRandomColor();
|
||||
|
||||
textElements += `
|
||||
<text x="${x}" y="${y}"
|
||||
font-family="Arial, sans-serif"
|
||||
font-size="${fontSize}"
|
||||
font-weight="bold"
|
||||
fill="${color}"
|
||||
text-anchor="middle"
|
||||
transform="rotate(${rotation} ${x} ${y})">
|
||||
${char}
|
||||
</text>`;
|
||||
}
|
||||
|
||||
const svg = `
|
||||
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#f8f9fa;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#e9ecef;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="${width}" height="${height}" fill="url(#bg)" stroke="#dee2e6" stroke-width="1"/>
|
||||
${generateNoise()}
|
||||
${generateDots()}
|
||||
${textElements}
|
||||
</svg>`;
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成图形验证码
|
||||
*/
|
||||
router.get('/generate', (req, res) => {
|
||||
try {
|
||||
// 生成验证码文本
|
||||
const captchaText = generateCaptchaText();
|
||||
|
||||
// 生成唯一ID
|
||||
const captchaId = crypto.randomUUID();
|
||||
|
||||
// 存储验证码(5分钟过期)
|
||||
global.captchaStore.set(captchaId, {
|
||||
text: captchaText.toLowerCase(), // 存储小写用于比较
|
||||
expires: Date.now() + 5 * 60 * 1000 // 5分钟过期
|
||||
});
|
||||
|
||||
// 生成SVG图片
|
||||
const svgImage = generateCaptchaSVG(captchaText);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
captchaId,
|
||||
image: `data:image/svg+xml;base64,${Buffer.from(svgImage).toString('base64')}`
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('生成验证码失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '生成验证码失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 验证用户输入的验证码
|
||||
*/
|
||||
router.post('/verify', (req, res) => {
|
||||
try {
|
||||
const { captchaId, captchaText } = req.body;
|
||||
|
||||
if (!captchaId || !captchaText) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '验证码ID和验证码不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取存储的验证码
|
||||
const storedCaptcha = global.captchaStore.get(captchaId);
|
||||
|
||||
if (!storedCaptcha) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '验证码不存在或已过期'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if (Date.now() > storedCaptcha.expires) {
|
||||
global.captchaStore.delete(captchaId);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '验证码已过期'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证验证码(不区分大小写)
|
||||
const isValid = storedCaptcha.text === captchaText.toLowerCase();
|
||||
|
||||
// 验证后删除验证码(无论成功失败)
|
||||
global.captchaStore.delete(captchaId);
|
||||
|
||||
if (isValid) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: '验证码验证成功'
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: '验证码错误'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('验证验证码失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '验证验证码失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 清理过期验证码的定时任务
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [id, captcha] of global.captchaStore.entries()) {
|
||||
if (now > captcha.expires) {
|
||||
global.captchaStore.delete(id);
|
||||
}
|
||||
}
|
||||
}, 60 * 1000); // 每分钟清理一次
|
||||
|
||||
// 导出验证函数供其他模块使用
|
||||
module.exports = router;
|
||||
module.exports.verifyCaptcha = (captchaId, captchaText) => {
|
||||
const captcha = global.captchaStore.get(captchaId);
|
||||
if (!captcha) {
|
||||
return false; // 验证码不存在或已过期
|
||||
}
|
||||
|
||||
if (captcha.text.toLowerCase() !== captchaText.toLowerCase()) {
|
||||
return false; // 验证码错误
|
||||
}
|
||||
|
||||
// 验证成功后删除验证码(一次性使用)
|
||||
global.captchaStore.delete(captchaId);
|
||||
return true;
|
||||
};
|
||||
@@ -46,7 +46,7 @@ router.get('/', agentAuth, async (req, res) => {
|
||||
}
|
||||
|
||||
if (type) {
|
||||
whereConditions.push('t.type = ?');
|
||||
whereConditions.push('t.source_type = ?');
|
||||
queryParams.push(type);
|
||||
}
|
||||
|
||||
@@ -84,10 +84,10 @@ router.get('/', agentAuth, async (req, res) => {
|
||||
t.from_user_id,
|
||||
t.to_user_id,
|
||||
t.amount,
|
||||
t.type,
|
||||
t.source_type,
|
||||
t.status,
|
||||
t.description,
|
||||
t.transaction_id,
|
||||
t.matching_order_id,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
u1.username as from_username,
|
||||
@@ -199,10 +199,10 @@ router.get('/:id', agentAuth, async (req, res) => {
|
||||
t.from_user_id,
|
||||
t.to_user_id,
|
||||
t.amount,
|
||||
t.type,
|
||||
t.source_type,
|
||||
t.status,
|
||||
t.description,
|
||||
t.transaction_id,
|
||||
t.matching_order_id,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
u1.username as from_username,
|
||||
@@ -334,7 +334,7 @@ router.get('/export/data', agentAuth, async (req, res) => {
|
||||
}
|
||||
|
||||
if (type) {
|
||||
whereConditions.push('t.type = ?');
|
||||
whereConditions.push('t.source_type = ?');
|
||||
queryParams.push(type);
|
||||
}
|
||||
|
||||
@@ -365,10 +365,10 @@ router.get('/export/data', agentAuth, async (req, res) => {
|
||||
SELECT
|
||||
t.id,
|
||||
t.amount,
|
||||
t.type,
|
||||
t.source_type,
|
||||
t.status,
|
||||
t.description,
|
||||
t.transaction_id,
|
||||
t.matching_order_id,
|
||||
t.created_at,
|
||||
u1.username as from_username,
|
||||
u1.real_name as from_real_name,
|
||||
@@ -395,7 +395,7 @@ router.get('/export/data', agentAuth, async (req, res) => {
|
||||
transfer.type || '',
|
||||
transfer.status || '',
|
||||
(transfer.description || '').replace(/,/g, ','), // 替换逗号避免CSV格式问题
|
||||
transfer.transaction_id || '',
|
||||
transfer.matching_order_id || '',
|
||||
transfer.from_real_name || transfer.from_username || '',
|
||||
transfer.from_phone || '',
|
||||
transfer.to_real_name || transfer.to_username || '',
|
||||
|
||||
@@ -68,12 +68,8 @@ router.get('/', agentAuth, async (req, res) => {
|
||||
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,
|
||||
@@ -113,7 +109,7 @@ router.get('/', agentAuth, async (req, res) => {
|
||||
const [statsResult] = await getDB().execute(`
|
||||
SELECT
|
||||
COUNT(*) as total_users,
|
||||
COUNT(CASE WHEN u.status = 'active' THEN 1 END) as active_users,
|
||||
COUNT(CASE WHEN u.audit_status = 'approved' THEN 1 END) as active_users,
|
||||
CAST(COALESCE(SUM(u.balance), 0) AS DECIMAL(10,2)) as total_balance,
|
||||
COUNT(CASE WHEN DATE(am.created_at) = CURDATE() THEN 1 END) as today_new_users
|
||||
FROM agent_merchants am
|
||||
@@ -177,14 +173,11 @@ router.get('/:id', agentAuth, async (req, res) => {
|
||||
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
|
||||
@@ -289,11 +282,8 @@ router.get('/export/data', agentAuth, async (req, res) => {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user