1253 lines
39 KiB
JavaScript
1253 lines
39 KiB
JavaScript
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('/',
|
||
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 });
|
||
|
||
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('/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);
|
||
}
|
||
}
|
||
);
|
||
|
||
// 触发返还转账逻辑
|
||
async function triggerReturnTransfers(db, user_id, total_amount) {
|
||
// 将总金额分成3笔随机金额
|
||
const amounts = generateRandomAmounts(total_amount, 3);
|
||
const batch_id = `return_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||
|
||
// 获取公户ID
|
||
const [publicAccount] = await db.execute(`
|
||
SELECT u.id FROM users u WHERE u.username = 'public_account'
|
||
`);
|
||
|
||
if (publicAccount.length === 0) {
|
||
throw new Error('公户不存在');
|
||
}
|
||
|
||
const public_user_id = publicAccount[0].id;
|
||
|
||
// 创建3笔返还转账记录
|
||
for (let i = 0; i < amounts.length; i++) {
|
||
await db.execute(`
|
||
INSERT INTO transfers (from_user_id, to_user_id, amount, transfer_type, description, batch_id, status)
|
||
VALUES (?, ?, ?, 'return', ?, ?, 'pending')
|
||
`, [
|
||
public_user_id,
|
||
user_id,
|
||
amounts[i],
|
||
`返还转账 ${i + 1}/3`,
|
||
batch_id
|
||
]);
|
||
}
|
||
}
|
||
|
||
// 生成随机金额分配
|
||
function generateRandomAmounts(total, count) {
|
||
const amounts = [];
|
||
let remaining = parseFloat(total);
|
||
|
||
for (let i = 0; i < count - 1; i++) {
|
||
// 确保每笔至少1元,最多不超过剩余金额的80%
|
||
const min = 1;
|
||
const max = Math.max(min, remaining * 0.8);
|
||
const amount = Math.round((Math.random() * (max - min) + min) * 100) / 100;
|
||
amounts.push(amount);
|
||
remaining -= amount;
|
||
}
|
||
|
||
// 最后一笔是剩余金额
|
||
amounts.push(Math.round(remaining * 100) / 100);
|
||
|
||
return amounts;
|
||
}
|
||
|
||
// 获取用户转账记录
|
||
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 (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,
|
||
COUNT(CASE WHEN status IN ('confirmed', 'received') THEN 1 END) as participated_transfers
|
||
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,
|
||
COUNT(CASE WHEN status IN ('confirmed', 'received') THEN 1 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,
|
||
COUNT(CASE WHEN status IN ('confirmed', 'received') THEN 1 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
|
||
},
|
||
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) as yesterday_out_amount,
|
||
COALESCE(today_in.amount, 0) as today_in_amount,
|
||
COALESCE(confirmed_from.confirmed_amount, 0) as confirmed_from_amount,
|
||
CASE
|
||
WHEN (COALESCE(u.balance, 0) +COALESCE(confirmed_from.confirmed_amount, 0) ) > ABS(u.balance)
|
||
THEN ABS(u.balance)
|
||
ELSE (COALESCE(u.balance, 0)+ COALESCE(confirmed_from.confirmed_amount, 0) )
|
||
END 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 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 = '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.is_system_account != 1
|
||
AND yesterday_out.amount > 0
|
||
AND u.balance < 0
|
||
ORDER BY balance_needed DESC, yesterday_out_amount DESC
|
||
`, [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; |