Files
jurong_circle_black/routes/transfers.js
2025-09-26 14:40:02 +08:00

1257 lines
47 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const express = require('express');
const transferService = require('../services/transferService');
const {auth: authenticateToken} = require('../middleware/auth');
const {validate, validateQuery, transferSchemas, commonSchemas} = require('../middleware/validation');
const {logger} = require('../config/logger');
const {HTTP_STATUS} = require('../config/constants');
const {getDB} = require('../database');
const multer = require('multer');
const path = require('path');
const dayjs = require('dayjs');
const router = express.Router();
// router.get('/tmp', async (req, res) => {
// const db = getDB();
// // 1. 查询所有转账记录,按 id 升序
// let [transfers] = await db.execute(`
// SELECT *
// FROM transfers
// WHERE status='received' or status='confirmed'
// ORDER BY id ASC`);
//
// // 2. 用对象维护每个用户的最新余额
// const userBalances = {};
//
// // 3. 遍历每条转账记录,计算余额
// for (const trx of transfers) {
// const fromId = trx.from_user_id;
// const toId = trx.to_user_id;
// const amount = Number(trx.amount);
//
// if (!(fromId in userBalances)) userBalances[fromId] = 0;
// if (!(toId in userBalances)) userBalances[toId] = 0;
//
// const fromBalance = userBalances[fromId] - amount;
// const toBalance = userBalances[toId] + amount;
//
// userBalances[fromId] = fromBalance;
// userBalances[toId] = toBalance;
//
// const balanceDiff = toBalance - fromBalance;
//
// // 4. 更新当前行的字段
// await db.execute(
// `UPDATE transfers
// SET from_user_balance = ?,
// to_user_balance = ?,
// balance_diff = ?
// WHERE id = ?`,
// [fromBalance, toBalance, amount, trx.id]
// );
//
// }
// res.json('更新成功')
// })
router.get('/',
authenticateToken,
validateQuery(transferSchemas.query),
async (req, res, next) => {
try {
const {page, limit, status, start_date, end_date, search, sort, order} = req.query;
const filters = {
status,
start_date,
end_date,
search
};
// 非管理员只能查看自己相关的转账
if (req.user.role !== 'admin') {
filters.user_id = req.user.id;
}
const result = await transferService.getTransfers(filters, {page, limit, sort, order});
logger.info('Transfer list requested', {
userId: req.user.id,
filters,
resultCount: result.transfers.length
});
res.json({
success: true,
data: result
});
} catch (error) {
next(error);
}
}
);
router.get('/history', authenticateToken, async (req, res, next) => {
try {
const {page, limit, start_date, end_date, search, sort, order} = req.query;
const filters = {
start_date,
end_date,
search
};
// 非管理员只能查看自己相关的转账
if (req.user.role !== 'admin') {
filters.user_id = req.user.id;
}
const result = await transferService.getTransfersHistory(filters, {page, limit, sort, order});
res.json({
success: true,
data: result
});
} catch (error) {
next(error);
}
})
router.get('/list',
authenticateToken,
validateQuery(transferSchemas.query),
async (req, res, next) => {
try {
const {page, limit, status, transfer_type, start_date, end_date, sort, order} = req.query;
const filters = {
status,
transfer_type,
start_date,
end_date
};
// 非管理员只能查看自己相关的转账
if (req.user.role !== 'admin') {
filters.user_id = req.user.id;
}
const result = await transferService.getTransfers(filters, {page, limit, sort, order});
logger.info('Transfer list requested', {
userId: req.user.id,
filters,
resultCount: result.transfers.length
});
res.json({
success: true,
data: result
});
} catch (error) {
next(error);
}
}
);
router.get('/public-account', authenticateToken, async (req, res) => {
try {
const db = getDB();
const [publicUser] = await db.execute(`
SELECT id, username, real_name, balance
FROM users
WHERE username = 'public_account'
AND is_system_account = TRUE
`);
if (publicUser.length === 0) {
return res.status(404).json({success: false, message: '公户不存在'});
}
res.json({success: true, data: publicUser[0]});
} catch (error) {
console.error('获取公户信息失败:', error);
res.status(500).json({success: false, message: '服务器错误'});
}
});
router.post('/create',
authenticateToken,
validate(transferSchemas.create),
async (req, res, next) => {
try {
const result = await transferService.createTransfer(req.user.id, req.body);
logger.info('Transfer creation requested', {
userId: req.user.id,
transferId: result.transfer_id,
amount: req.body.amount
});
res.status(HTTP_STATUS.CREATED).json({
success: true,
message: '转账记录创建成功,等待确认',
data: result
});
} catch (error) {
next(error);
}
}
);
router.post('/admin/create',
authenticateToken,
async (req, res, next) => {
try {
// 检查管理员权限
if (req.user.role !== 'admin') {
return res.status(403).json({success: false, message: '权限不足'});
}
const {from_user_id, to_user_id, amount, transfer_type, description} = req.body;
// 验证必填字段
if (!from_user_id || !to_user_id || !amount || !transfer_type) {
return res.status(400).json({success: false, message: '缺少必填字段'});
}
const result = await transferService.createTransfer(from_user_id, {
to_user_id,
amount,
transfer_type,
description: description || '管理员分配转账'
});
logger.info('Admin transfer creation requested', {
adminId: req.user.id,
fromUserId: from_user_id,
toUserId: to_user_id,
transferId: result.transfer_id,
amount: amount
});
res.status(HTTP_STATUS.CREATED).json({
success: true,
message: '转账分配成功',
data: result
});
} catch (error) {
next(error);
}
}
);
// 确认转账
router.post('/confirm',
authenticateToken,
validate(transferSchemas.confirm),
async (req, res, next) => {
try {
const {transfer_id, note} = req.body;
await transferService.confirmTransfer(transfer_id, note, req.user.id);
logger.info('Transfer confirmed', {
transferId: transfer_id,
operatorId: req.user.id
});
res.json({
success: true,
message: '转账确认成功'
});
} catch (error) {
next(error);
}
}
);
// 确认转账(路径参数形式)
router.post('/confirm/:id',
authenticateToken,
async (req, res, next) => {
try {
const transfer_id = req.params.id;
const {action, note} = req.body;
// 验证action参数
if (action !== 'confirm') {
return res.status(400).json({
success: false,
message: 'action参数必须为confirm'
});
}
await transferService.confirmTransfer(transfer_id, note, req.user.id);
logger.info('Transfer confirmed via path param', {
transferId: transfer_id,
operatorId: req.user.id
});
res.json({
success: true,
message: '转账确认成功'
});
} catch (error) {
next(error);
}
}
);
// 拒绝转账
router.post('/reject',
authenticateToken,
validate(transferSchemas.reject),
async (req, res, next) => {
try {
const {transfer_id, note = ''} = req.body;
await transferService.rejectTransfer(transfer_id, note, req.user.id);
logger.info('Transfer rejected', {
transferId: transfer_id,
operatorId: req.user.id
});
res.json({
success: true,
message: '转账已拒绝'
});
} catch (error) {
next(error);
}
}
);
// 用户确认收到转账
router.post('/confirm-received',
authenticateToken,
async (req, res, next) => {
try {
const {transfer_id} = req.body;
if (!transfer_id) {
return res.status(400).json({success: false, message: '缺少转账ID'});
}
await transferService.confirmReceived(transfer_id, req.user.id);
logger.info('Transfer received confirmed by user', {
transferId: transfer_id,
userId: req.user.id
});
res.json({
success: true,
message: '已确认收到转账,余额已更新'
});
} catch (error) {
next(error);
}
}
);
// 用户确认未收到转账
router.post('/confirm-not-received',
authenticateToken,
async (req, res, next) => {
try {
const {transfer_id} = req.body;
if (!transfer_id) {
return res.status(400).json({success: false, message: '缺少转账ID'});
}
await transferService.confirmNotReceived(transfer_id, req.user.id);
logger.info('Transfer not received confirmed by user', {
transferId: transfer_id,
userId: req.user.id
});
res.json({
success: true,
message: '已确认未收到转账'
});
} catch (error) {
next(error);
}
}
);
// 获取用户转账记录
router.get('/user/:userId', authenticateToken, async (req, res) => {
try {
const userId = req.params.userId;
const {page = 1, limit = 10, status} = req.query;
// 检查权限(只能查看自己的记录或管理员查看所有)
// if (req.user.id != userId && req.user.role !== 'admin') {
// return res.status(403).json({ success: false, message: '权限不足' });
// }
const db = getDB();
// 确保参数为有效数字
const pageNum = Math.max(1, parseInt(page) || 1);
const limitNum = Math.max(1, Math.min(100, parseInt(limit) || 10));
const offset = Math.max(0, (pageNum - 1) * limitNum);
let whereClause = `WHERE source_type='manual' AND (t.from_user_id = ? OR t.to_user_id = ?)`;
const userIdInt = parseInt(userId);
let listParams = [userIdInt, userIdInt];
let countParams = [userIdInt, userIdInt];
if (status) {
whereClause += ' AND t.status = ?';
listParams.push(status);
countParams.push(status);
}
// 添加分页参数
listParams.push(limitNum.toString(), offset.toString());
const [transfers] = await db.execute(`
SELECT t.*,
from_user.username as from_username,
from_user.real_name as from_real_name,
to_user.username as to_username,
to_user.real_name as to_real_name
FROM transfers t
LEFT JOIN users from_user ON t.from_user_id = from_user.id
LEFT JOIN users to_user ON t.to_user_id = to_user.id
${whereClause}
ORDER BY t.created_at
DESC
LIMIT ${limitNum}
OFFSET ${offset}
`, countParams);
const [countResult] = await db.execute(`
SELECT COUNT(*) as total
FROM transfers t ${whereClause}
`, countParams);
res.json({
success: true,
data: {
transfers,
pagination: {
page: pageNum,
limit: limitNum,
total: countResult[0].total,
pages: Math.ceil(countResult[0].total / limitNum)
}
}
});
} catch (error) {
console.error('获取转账记录失败:', error);
res.status(500).json({success: false, message: '服务器错误'});
}
});
// 获取转账统计信息
router.get('/stats', authenticateToken, async (req, res) => {
try {
const userId = req.user.id;
const isAdmin = req.user.role === 'admin';
const db = getDB();
let stats = {};
if (isAdmin) {
// 管理员可以查看全局统计
const [totalStats] = await db.execute(`
SELECT COUNT(*) as total_transfers,
SUM(amount) as total_flow_amount,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count,
SUM(CASE WHEN status = 'confirmed' THEN 1 ELSE 0 END) as confirmed_count,
SUM(CASE WHEN status = 'received' THEN 1 ELSE 0 END) as received_count,
SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected_count,
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_count,
SUM(CASE WHEN status = 'not_received' THEN 1 ELSE 0 END) as not_received_count,
SUM(CASE WHEN is_overdue = 1 THEN 1 ELSE 0 END) as overdue_count,
SUM(CASE WHEN is_bad_debt = 1 THEN 1 ELSE 0 END) as bad_debt_count,
SUM(CASE WHEN status = 'confirmed' THEN amount ELSE 0 END) as total_amount,
SUM(CASE WHEN is_bad_debt = 1 THEN amount ELSE 0 END) as bad_debt_amount,
SUM(CASE
WHEN transfer_type = 'initial' AND status = 'confirmed' THEN amount
ELSE 0 END) as initial_amount,
SUM(CASE
WHEN transfer_type = 'return' AND status = 'confirmed' THEN amount
ELSE 0 END) as return_amount,
SUM(CASE
WHEN transfer_type = 'user_to_user' AND status = 'confirmed' THEN amount
ELSE 0 END) as user_to_user_amount,
(SELECT SUM(balance)
FROM users
WHERE role = 'user'
AND is_system_account = 1) as total_merchant_balance,
(SELECT SUM(balance)
FROM users
WHERE role = 'user'
AND is_system_account != 1) as total_user_balance,
SUM(CASE WHEN source_type IN ('system') THEN amount END) as participated_transfers,
SUM(CASE WHEN source_type IN ('agent') THEN amount END) as agent_total,
SUM(CASE WHEN source_type IN ('operated_agent') THEN amount END) as operated_agent_total
FROM transfers
`);
const todayStr = dayjs().format('YYYY-MM-DD');
const currentYear = dayjs().year();
const currentMonth = dayjs().month() + 1;
console.log(todayStr, 'todayStr');
const [todayStats] = await db.execute(`
SELECT COUNT(*) as today_transfers,
(
COALESCE((SELECT SUM(amount)
FROM transfers
WHERE DATE (created_at) = ?
AND to_user_id IN (SELECT id FROM users WHERE is_system_account = 1)
AND status = 'received'), 0) -
COALESCE((SELECT SUM(amount)
FROM transfers
WHERE DATE (created_at) = ?
AND from_user_id IN (SELECT id FROM users WHERE is_system_account = 1)
AND status = 'received'), 0)
) as today_amount
FROM transfers
WHERE DATE (created_at) = ?
`, [todayStr, todayStr, todayStr]);
const [monthlyStats] = await db.execute(`
SELECT COUNT(*) as monthly_transfers,
SUM(CASE WHEN status = 'received' THEN amount ELSE 0 END) as monthly_amount,
SUM(CASE WHEN source_type IN ('system') THEN amount END) as monthly_participated_transfers
FROM transfers
WHERE YEAR (created_at) = ?
AND MONTH (created_at) = ?
`, [currentYear, currentMonth]);
// 获取上月统计数据用于对比
const lastMonth = currentMonth === 1 ? 12 : currentMonth - 1;
const lastMonthYear = currentMonth === 1 ? currentYear - 1 : currentYear;
const [lastMonthStats] = await db.execute(`
SELECT COUNT(*) as last_monthly_transfers,
SUM(CASE WHEN status = 'confirmed' THEN amount ELSE 0 END) as last_monthly_amount,
SUM(CASE WHEN source_type IN ('system') THEN amount END) as last_monthly_participated_transfers
FROM transfers
WHERE YEAR (created_at) = ?
AND MONTH (created_at) = ?
`, [lastMonthYear, lastMonth]);
stats = {
total: {
transfers: totalStats[0].total_transfers || 0,
pending: parseFloat(totalStats[0].total_flow_amount || 0),
pending_count: totalStats[0].pending_count || 0,
confirmed: totalStats[0].confirmed_count || 0,
received_count: totalStats[0].received_count || 0,
rejected: totalStats[0].rejected_count || 0,
cancelled_count: totalStats[0].cancelled_count || 0,
not_received_count: totalStats[0].not_received_count || 0,
overdue: totalStats[0].overdue_count || 0,
bad_debt: totalStats[0].bad_debt_count || 0,
amount: parseFloat(totalStats[0].total_amount || 0),
bad_debt_amount: parseFloat(totalStats[0].bad_debt_amount || 0),
total_merchant_balance: parseFloat(totalStats[0].total_merchant_balance || 0),
initial_amount: parseFloat(totalStats[0].initial_amount || 0),
return_amount: parseFloat(totalStats[0].return_amount || 0),
user_to_user_amount: parseFloat(totalStats[0].user_to_user_amount || 0),
participated_transfers: totalStats[0].participated_transfers || 0,
total_user_balance: totalStats[0].total_user_balance || 0,
agent_total: totalStats[0].agent_total || 0,//代理收入
operated_agent_total: totalStats[0].operated_agent_total || 0,//直营代理收入
},
today: {
transfers: todayStats[0].today_transfers || 0,
amount: parseFloat(todayStats[0].today_amount || 0)
},
monthly: {
transfers: monthlyStats[0].monthly_transfers || 0,
amount: parseFloat(monthlyStats[0].monthly_amount || 0),
participated_transfers: monthlyStats[0].monthly_participated_transfers || 0
},
lastMonth: {
transfers: lastMonthStats[0].last_monthly_transfers || 0,
amount: parseFloat(lastMonthStats[0].last_monthly_amount || 0),
participated_transfers: lastMonthStats[0].last_monthly_participated_transfers || 0
}
};
} else {
// 普通用户只能查看自己的统计
const [userStats] = await db.execute(`
SELECT COUNT(*) as total_transfers,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count,
SUM(CASE WHEN status = 'confirmed' THEN 1 ELSE 0 END) as confirmed_count,
SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected_count,
SUM(CASE WHEN status = 'confirmed' AND from_user_id = ? THEN amount ELSE 0 END) as sent_amount,
SUM(CASE WHEN status = 'confirmed' AND to_user_id = ? THEN amount ELSE 0 END) as received_amount
FROM transfers
WHERE from_user_id = ?
OR to_user_id = ?
`, [userId, userId, userId, userId]);
const todayStr = dayjs().format('YYYY-MM-DD');
const [todayStats] = await db.execute(`
SELECT COUNT(*) as today_transfers,
SUM(CASE WHEN status = 'confirmed' AND from_user_id = ? THEN amount ELSE 0 END) as today_sent,
SUM(CASE WHEN status = 'confirmed' AND to_user_id = ? THEN amount ELSE 0 END) as today_received
FROM transfers
WHERE (from_user_id = ? OR to_user_id = ?)
AND DATE (created_at) = ?
`, [userId, userId, userId, userId, todayStr]);
stats = {
total: {
transfers: userStats[0].total_transfers || 0,
pending: userStats[0].pending_count || 0,
confirmed: userStats[0].confirmed_count || 0,
rejected: userStats[0].rejected_count || 0,
sent_amount: parseFloat(userStats[0].sent_amount || 0),
received_amount: parseFloat(userStats[0].received_amount || 0)
},
today: {
transfers: todayStats[0].today_transfers || 0,
sent_amount: parseFloat(todayStats[0].today_sent || 0),
received_amount: parseFloat(todayStats[0].today_received || 0)
}
};
}
res.json({success: true, data: stats});
} catch (error) {
console.error('获取转账统计失败:', error);
res.status(500).json({success: false, message: '获取转账统计失败'});
}
});
// 获取待确认的转账
router.get('/pending', authenticateToken, async (req, res) => {
try {
const userId = parseInt(req.user.id);
const db = getDB();
const [transfers] = await db.execute(`
SELECT t.*,
from_user.username as from_username,
from_user.real_name as from_real_name
FROM transfers t
LEFT JOIN users from_user ON t.from_user_id = from_user.id
WHERE t.to_user_id = ?
AND t.status = 'pending'
ORDER BY t.created_at DESC
`, [userId]);
res.json({success: true, data: transfers});
} catch (error) {
console.error('获取待确认转账失败:', error);
res.status(500).json({success: false, message: '服务器错误'});
}
});
// 获取当前用户账户信息不需要传递用户ID
router.get('/account', authenticateToken, async (req, res) => {
try {
const userId = req.user.id;
const db = getDB();
const [user] = await db.execute(`
SELECT id, username, real_name, balance, created_at, updated_at, points
FROM users
WHERE id = ?
`, [userId]);
if (user.length === 0) {
return res.status(404).json({success: false, message: '用户不存在'});
}
// 返回用户账户信息,格式与原来的 accounts 表保持一致
const accountData = {
id: user[0].id,
user_id: user[0].id,
account_type: 'user',
balance: user[0].balance,
username: user[0].username,
real_name: user[0].real_name,
created_at: user[0].created_at,
updated_at: user[0].updated_at,
points: user[0].points
};
res.json({success: true, data: accountData});
} catch (error) {
console.error('获取账户信息失败:', error);
res.status(500).json({success: false, message: '服务器错误'});
}
});
// 获取指定用户账户信息(管理员权限或用户本人)
router.get('/account/:userId', authenticateToken, async (req, res) => {
try {
const userId = req.params.userId;
// 检查权限
if (req.user.id != userId && req.user.role !== 'admin') {
return res.status(403).json({success: false, message: '权限不足'});
}
const db = getDB();
const [user] = await db.execute(`
SELECT id, username, real_name, balance, created_at, updated_at
FROM users
WHERE id = ?
`, [userId]);
if (user.length === 0) {
return res.status(404).json({success: false, message: '用户不存在'});
}
// 返回用户账户信息,格式与原来的 accounts 表保持一致
const accountData = {
id: user[0].id,
user_id: user[0].id,
account_type: 'user',
balance: user[0].balance,
username: user[0].username,
real_name: user[0].real_name,
created_at: user[0].created_at,
updated_at: user[0].updated_at
};
res.json({success: true, data: accountData});
} catch (error) {
console.error('获取账户信息失败:', error);
res.status(500).json({success: false, message: '服务器错误'});
}
});
// 获取转账趋势数据(管理员权限)
router.get('/trend', authenticateToken, async (req, res) => {
try {
// 检查管理员权限
if (req.user.role !== 'admin') {
return res.status(403).json({success: false, message: '权限不足'});
}
const db = getDB();
const {days = 7} = req.query;
const daysNum = Math.min(30, Math.max(1, parseInt(days) || 7));
// 首先获取数据库中最早和最晚的转账日期
const [dateRange] = await db.execute(`
SELECT MIN(DATE (created_at)) as min_date,
MAX(DATE (created_at)) as max_date,
COUNT(*) as total_count
FROM transfers
`);
if (dateRange[0].total_count === 0) {
// 如果没有转账记录,返回空数据
const result = [];
const now = new Date();
for (let i = daysNum - 1; i >= 0; i--) {
const date = dayjs().subtract(i, 'day');
result.push({
date: date.format('MM-DD'),
count: 0,
amount: 0
});
}
return res.json({
success: true,
data: result
});
}
// 获取最近的转账数据(基于实际数据的最大日期)
const maxDate = dayjs(dateRange[0].max_date);
// 获取指定天数内的转账趋势(从最大日期往前推)
const [trendData] = await db.execute(`
SELECT DATE (created_at) as date, COUNT (*) as count, SUM (amount) as amount
FROM transfers
WHERE DATE (created_at) >= DATE_SUB(?
, INTERVAL ? DAY)
AND status IN ('confirmed'
, 'received')
GROUP BY DATE (created_at)
ORDER BY date ASC
`, [dateRange[0].max_date, daysNum - 1]);
// 填充缺失的日期转账数为0
const result = [];
for (let i = daysNum - 1; i >= 0; i--) {
const date = maxDate.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,
amount: existingData ? parseFloat(existingData.amount) : 0
});
}
res.json({
success: true,
data: result
});
} catch (error) {
console.error('获取转账趋势错误:', error);
res.status(500).json({success: false, message: '获取转账趋势失败'});
}
});
// 管理员解除坏账(管理员权限)
router.post('/remove-bad-debt/:transferId', authenticateToken, async (req, res) => {
try {
// 检查管理员权限
if (req.user.role !== 'admin') {
return res.status(403).json({success: false, message: '权限不足'});
}
const {transferId} = req.params;
const {reason} = req.body;
const adminId = req.user.id;
// 验证转账ID
if (!transferId || isNaN(transferId)) {
return res.status(400).json({success: false, message: '无效的转账ID'});
}
const result = await transferService.removeBadDebt(transferId, adminId, reason);
res.json({
success: true,
message: '坏账标记已解除',
data: result
});
} catch (error) {
console.error('解除坏账失败:', error);
if (error.statusCode) {
return res.status(error.statusCode).json({
success: false,
message: error.message
});
}
res.status(500).json({success: false, message: '解除坏账失败'});
}
});
// 强制变更转账状态(管理员权限)
router.post('/force-change-status/:transferId', authenticateToken, async (req, res) => {
try {
// 检查管理员权限
if (req.user.role !== 'admin') {
return res.status(403).json({success: false, message: '权限不足'});
}
const {transferId} = req.params;
const {newStatus, status, reason, adjust_balance = false} = req.body;
console.log('newStatus:', newStatus);
console.log('status:', status);
console.log('reason:', reason);
console.log('adjust_balance:', adjust_balance);
// 兼容两种参数名newStatus 和 status
const actualNewStatus = newStatus || status;
const adminId = req.user.id;
// 验证转账ID
if (!transferId || isNaN(transferId)) {
return res.status(400).json({success: false, message: '无效的转账ID'});
}
// 验证必填参数
if (!actualNewStatus) {
return res.status(400).json({success: false, message: '新状态不能为空'});
}
// if (!reason) {
// return res.status(400).json({ success: false, message: '变更原因不能为空' });
// }
const result = await transferService.forceChangeTransferStatus(
transferId,
actualNewStatus,
reason,
adminId,
adjust_balance
);
res.json({
success: true,
message: `转账状态已从 ${result.oldStatus} 变更为 ${result.newStatus}`,
data: result
});
} catch (error) {
console.error('强制变更转账状态失败:', error);
if (error.statusCode) {
return res.status(error.statusCode).json({
success: false,
message: error.message
});
}
res.status(500).json({success: false, message: '变更转账状态失败'});
}
});
// 管理员查看数据库连接状态
router.get('/admin/database/status', authenticateToken, async (req, res) => {
try {
// 检查管理员权限
if (req.user.role !== 'admin') {
return res.status(403).json({success: false, message: '权限不足'});
}
const dbMonitor = require('../db-monitor');
const diagnosis = await dbMonitor.diagnose();
res.json({
success: true,
data: diagnosis
});
} catch (error) {
logger.error('Get database status failed', {
adminId: req.user.id,
error: error.message
});
res.status(500).json({
success: false,
message: '获取数据库状态失败: ' + error.message
});
}
});
// 管理员获取数据库监控报告
router.get('/admin/database/report', authenticateToken, async (req, res) => {
try {
// 检查管理员权限
if (req.user.role !== 'admin') {
return res.status(403).json({success: false, message: '权限不足'});
}
const dbMonitor = require('../db-monitor');
const report = await dbMonitor.generateReport();
res.json({
success: true,
data: {
report,
timestamp: new Date().toISOString()
}
});
} catch (error) {
logger.error('Get database report failed', {
adminId: req.user.id,
error: error.message
});
res.status(500).json({
success: false,
message: '获取数据库报告失败: ' + error.message
});
}
});
/**
* 获取待处理的匹配转账订单
* @param {number} page - 页码
* @param {number} limit - 每页数量
* @param {string} status - 状态过滤
* @param {string} search - 搜索关键词(用户名或真实姓名)
* @param {string} sort - 排序字段
* @param {string} order - 排序方向asc/desc
*/
router.get('/pending-allocations',
authenticateToken,
async (req, res, next) => {
try {
// 检查管理员权限
if (req.user.role !== 'admin') {
return res.status(403).json({success: false, message: '权限不足'});
}
const {
page = 1,
limit = 20,
status = '',
search = '',
sort = 'created_at',
order = 'desc'
} = req.query;
const db = getDB();
const offset = (parseInt(page) - 1) * parseInt(limit);
// 构建查询条件
let whereConditions = [];
let queryParams = [];
// 状态过滤
if (status) {
whereConditions.push('oa.status = ?');
queryParams.push(status);
}
// 搜索过滤(用户名或真实姓名)
if (search) {
whereConditions.push('(uf.username LIKE ? OR uf.real_name LIKE ? OR ut.username LIKE ? OR ut.real_name LIKE ?)');
const searchPattern = `%${search}%`;
queryParams.push(searchPattern, searchPattern, searchPattern, searchPattern);
}
const whereClause = whereConditions.length > 0 ? 'WHERE ' + whereConditions.join(' AND ') : '';
// 验证排序字段
const allowedSortFields = ['created_at', 'amount', 'status', 'cycle_number'];
const sortField = allowedSortFields.includes(sort) ? sort : 'created_at';
const sortOrder = order.toLowerCase() === 'asc' ? 'ASC' : 'DESC';
// 获取总数
const countQuery = `
SELECT COUNT(*) as total
FROM transfers oa
JOIN users uf ON oa.from_user_id = uf.id
JOIN users ut ON oa.to_user_id = ut.id
JOIN matching_orders mo ON oa.id = mo.id
${whereClause}
`;
const [countResult] = await db.execute(countQuery, queryParams);
const total = countResult[0].total;
// 使用 query 方法避免 LIMIT/OFFSET 参数问题
const dataQuery = `
SELECT oa.id,
oa.from_user_id,
oa.to_user_id,
oa.amount,
oa.cycle_number,
oa.status,
oa.outbound_date,
oa.return_date,
oa.can_return_after,
oa.confirmed_at,
oa.created_at,
oa.updated_at,
uf.username as from_username,
uf.real_name as from_real_name,
ut.username as to_username,
ut.real_name as to_real_name,
mo.amount as order_total_amount,
mo.status as order_status,
mo.matching_type,
t.status as transfer_status,
t.voucher_url
FROM transfers oa
JOIN users uf ON oa.from_user_id = uf.id
JOIN users ut ON oa.to_user_id = ut.id
JOIN matching_orders mo ON oa.id = mo.id
${whereClause}
ORDER BY oa.${sortField} ${sortOrder}
LIMIT ${parseInt(limit)}
OFFSET ${parseInt(offset)}
`;
const [allocations] = queryParams.length > 0
? await db.execute(dataQuery, queryParams)
: await db.query(dataQuery);
// 计算分页信息
const totalPages = Math.ceil(total / parseInt(limit));
logger.info('Pending allocations list requested', {
userId: req.user.id,
page: parseInt(page),
limit: parseInt(limit),
total,
resultCount: allocations.length
});
res.json({
success: true,
data: {
allocations,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
totalPages
}
}
});
} catch (error) {
logger.error('Get pending allocations failed', {
userId: req.user.id,
error: error.message
});
next(error);
}
}
);
/**
* 获取待处理匹配订单的统计信息
*/
router.get('/pending-allocations/stats',
authenticateToken,
async (req, res, next) => {
try {
// 检查管理员权限
if (req.user.role !== 'admin') {
return res.status(403).json({success: false, message: '权限不足'});
}
const db = getDB();
// 获取统计数据
const [stats] = await db.execute(`
SELECT COUNT(*) as total_allocations,
COUNT(CASE WHEN oa.status = 'pending' THEN 1 END) as pending_count,
COUNT(CASE WHEN oa.status = 'confirmed' THEN 1 END) as confirmed_count,
COUNT(CASE WHEN oa.status = 'completed' THEN 1 END) as completed_count,
SUM(oa.amount) as total_amount,
SUM(CASE WHEN oa.status = 'pending' THEN oa.amount ELSE 0 END) as pending_amount
FROM transfers oa
JOIN matching_orders mo ON oa.id = mo.id
WHERE mo.status != 'cancelled'
`);
res.json({
success: true,
data: stats[0]
});
} catch (error) {
logger.error('Get pending allocations stats failed', {
userId: req.user.id,
error: error.message
});
next(error);
}
}
);
/**
* 获取昨日用户转账统计列表
*/
router.get('/daily-stats',
authenticateToken,
async (req, res, next) => {
try {
// 检查管理员权限
if (req.user.role !== 'admin') {
return res.status(403).json({success: false, message: '权限不足'});
}
const db = getDB();
// 获取昨日和今日的日期范围从0点开始计算
// 今日0点到23:59:59
const todayStart = dayjs().startOf('day');
const todayEnd = dayjs().endOf('day');
// 昨日0点到23:59:59
const yesterdayStart = dayjs().subtract(1, 'day').startOf('day');
const yesterdayEnd = dayjs().subtract(1, 'day').endOf('day');
// 转换为MySQL兼容的字符串格式
const todayStartStr = todayStart.format('YYYY-MM-DD HH:mm:ss');
const todayEndStr = todayEnd.format('YYYY-MM-DD HH:mm:ss');
const yesterdayStartStr = yesterdayStart.format('YYYY-MM-DD HH:mm:ss');
const yesterdayEndStr = yesterdayEnd.format('YYYY-MM-DD HH:mm:ss');
// 使用dayjs格式化日期字符串用于返回
const todayStr = todayStart.format('YYYY-MM-DD');
const yesterdayStr = yesterdayStart.format('YYYY-MM-DD');
// 获取所有用户的昨日转出和今日入账统计
let [userStats] = await db.execute(`
SELECT u.id as user_id,
u.username,
u.real_name,
u.phone,
u.balance,
COALESCE(yesterday_out.amount, 0) -
COALESCE(yesterday_system_num.amount, 0) as yesterday_out_amount,
COALESCE(today_in.amount, 0) as today_in_amount,
COALESCE(confirmed_from.confirmed_amount, 0) as confirmed_from_amount,
COALESCE(u.balance, 0) + COALESCE(confirmed_from.confirmed_amount, 0) as balance_needed
FROM users u
LEFT JOIN (SELECT from_user_id,
SUM(amount) as amount
FROM transfers
WHERE created_at >= ?
AND created_at <= ?
AND status IN ('confirmed', 'received')
GROUP BY from_user_id) yesterday_out ON u.id = yesterday_out.from_user_id
LEFT JOIN (SELECT to_user_id,
SUM(amount) as amount
FROM transfers
WHERE created_at >= ?
AND created_at <= ?
AND transfer_type != 'user_to_user'
AND status IN ('received')
GROUP BY to_user_id) yesterday_system_num
ON u.id = yesterday_system_num.to_user_id
LEFT JOIN (SELECT to_user_id,
SUM(amount) as amount
FROM transfers
WHERE created_at >= ?
AND created_at <= ?
AND status IN ('confirmed', 'received')
GROUP BY to_user_id) today_in ON u.id = today_in.to_user_id
left join (select from_user_id,
sum(amount) as confirmed_amount
from transfers
WHERE status IN ('confirmed', 'received')
AND created_at >= ?
AND created_at <= ?
GROUP BY from_user_id) as confirmed_from on u.id = confirmed_from.from_user_id
WHERE u.role != 'admin' AND u.user_type !='directly_operated'
AND u.is_system_account != 1
AND yesterday_out.amount > 0
AND u.balance < 0
ORDER BY balance_needed DESC, yesterday_out_amount DESC
`, [yesterdayStartStr, yesterdayEndStr, yesterdayStartStr, yesterdayEndStr,todayStartStr, todayEndStr, todayStartStr, todayEndStr]);
// userStats = userStats.filter(item=>item.balance_needed > 100)
userStats.forEach(item => {
item.balance_needed = Math.abs(item.balance_needed)
})
res.json({
success: true,
data: {
date: {
yesterday: yesterdayStr,
today: todayStr
},
users: userStats
}
});
} catch (error) {
logger.error('Get daily transfer stats failed', {
userId: req.user.id,
error: error.message
});
next(error);
}
}
);
module.exports = router;