Files
jurong_circle_black/routes/transfers.js

1627 lines
49 KiB
JavaScript
Raw Normal View History

2025-08-26 10:06:23 +08:00
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();
2025-08-28 09:14:56 +08:00
/**
* @swagger
* components:
* schemas:
* Transfer:
* type: object
* properties:
* id:
* type: integer
* description: 转账记录ID
* user_id:
* type: integer
* description: 用户ID
* recipient_id:
* type: integer
* description: 接收方用户ID
* amount:
* type: number
* format: float
* description: 转账金额
* status:
* type: string
* enum: [pending, completed, failed, cancelled]
* description: 转账状态
* transfer_type:
* type: string
* enum: [user_to_user, user_to_system, system_to_user]
* description: 转账类型
* voucher_image:
* type: string
* description: 转账凭证图片路径
* remark:
* type: string
* description: 转账备注
* created_at:
* type: string
* format: date-time
* description: 创建时间
* updated_at:
* type: string
* format: date-time
* description: 更新时间
* Pagination:
* type: object
* properties:
* total:
* type: integer
* description: 总记录数
* page:
* type: integer
* description: 当前页码
* limit:
* type: integer
* description: 每页记录数
* total_pages:
* type: integer
* description: 总页数
*/
2025-08-26 10:06:23 +08:00
// 配置文件上传
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/')
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9)
cb(null, 'voucher-' + uniqueSuffix + path.extname(file.originalname))
}
});
const upload = multer({
storage: storage,
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('只允许上传图片文件'));
}
},
limits: {
fileSize: 5 * 1024 * 1024 // 5MB
}
});
/**
2025-08-28 09:14:56 +08:00
* @swagger
* /transfers:
* get:
* summary: 获取转账列表
* tags: [Transfers]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: status
* schema:
* type: string
* description: 转账状态过滤
* - in: query
* name: transfer_type
* schema:
* type: string
* description: 转账类型过滤
* - in: query
* name: start_date
* schema:
* type: string
* format: date
* description: 开始日期过滤
* - in: query
* name: end_date
* schema:
* type: string
* format: date
* description: 结束日期过滤
* - in: query
* name: search
* schema:
* type: string
* description: 搜索关键词用户名或真实姓名
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: limit
* schema:
* type: integer
* default: 10
* description: 每页数量
* - in: query
* name: sort
* schema:
* type: string
* description: 排序字段
* - in: query
* name: order
* schema:
* type: string
* enum: [asc, desc]
* description: 排序方向
* responses:
* 200:
* description: 成功获取转账列表
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* transfers:
* type: array
* items:
* $ref: '#/components/schemas/Transfer'
* pagination:
* $ref: '#/components/schemas/Pagination'
* 401:
* description: 未授权
* 500:
* description: 服务器错误
2025-08-26 10:06:23 +08:00
*/
router.get('/',
authenticateToken,
validateQuery(transferSchemas.query),
async (req, res, next) => {
try {
const { page, limit, status, transfer_type, start_date, end_date, search, sort, order } = req.query;
const filters = {
status,
transfer_type,
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);
}
}
);
2025-08-28 09:14:56 +08:00
/**
* @swagger
* /transfers/list:
* get:
* summary: 获取转账记录列表
* tags: [Transfers]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: status
* schema:
* type: string
* description: 转账状态过滤
* - in: query
* name: transfer_type
* schema:
* type: string
* description: 转账类型过滤
* - in: query
* name: start_date
* schema:
* type: string
* format: date
* description: 开始日期过滤
* - in: query
* name: end_date
* schema:
* type: string
* format: date
* description: 结束日期过滤
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: limit
* schema:
* type: integer
* default: 10
* description: 每页数量
* - in: query
* name: sort
* schema:
* type: string
* description: 排序字段
* - in: query
* name: order
* schema:
* type: string
* enum: [asc, desc]
* description: 排序方向
* responses:
* 200:
* description: 成功获取转账记录列表
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* transfers:
* type: array
* items:
* $ref: '#/components/schemas/Transfer'
* pagination:
* $ref: '#/components/schemas/Pagination'
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
2025-08-26 10:06:23 +08:00
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);
}
}
);
2025-08-28 09:14:56 +08:00
/**
* @swagger
* /transfers/public-account:
* get:
* summary: 获取公户信息
* tags: [Transfers]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 成功获取公户信息
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: object
* properties:
* id:
* type: integer
* description: 公户ID
* username:
* type: string
* description: 公户用户名
* example: public_account
* real_name:
* type: string
* description: 公户名称
* balance:
* type: number
* format: float
* description: 公户余额
* 401:
* description: 未授权
* 404:
* description: 公户不存在
* 500:
* description: 服务器错误
*/
2025-08-26 10:06:23 +08:00
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: '服务器错误' });
}
});
2025-08-28 09:14:56 +08:00
/**
* @swagger
* /transfers/create:
* post:
* summary: 创建转账记录
* tags: [Transfers]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - to_user_id
* - amount
* - transfer_type
* properties:
* to_user_id:
* type: integer
* description: 接收方用户ID
* amount:
* type: number
* format: float
* description: 转账金额
* transfer_type:
* type: string
* enum: [user_to_user, user_to_system, system_to_user]
* description: 转账类型
* remark:
* type: string
* description: 转账备注
* responses:
* 201:
* description: 转账记录创建成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: 转账记录创建成功等待确认
* data:
* type: object
* properties:
* transfer_id:
* type: integer
* description: 转账记录ID
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
2025-08-26 10:06:23 +08:00
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);
}
}
);
2025-08-28 09:14:56 +08:00
/**
* @swagger
* /transfers/admin/create:
* post:
* summary: 管理员创建转账记录
* tags: [Transfers]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - from_user_id
* - to_user_id
* - amount
* - transfer_type
* properties:
* from_user_id:
* type: integer
* description: 发送方用户ID
* to_user_id:
* type: integer
* description: 接收方用户ID
* amount:
* type: number
* format: float
* description: 转账金额
* transfer_type:
* type: string
* enum: [user_to_user, user_to_system, system_to_user]
* description: 转账类型
* description:
* type: string
* description: 转账描述
* responses:
* 201:
* description: 转账记录创建成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: 转账记录创建成功
* data:
* type: object
* properties:
* transfer_id:
* type: integer
* description: 转账记录ID
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 500:
* description: 服务器错误
*/
2025-08-26 10:06:23 +08:00
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
2025-09-02 09:29:20 +08:00
LIMIT ${limitNum} OFFSET ${offset}
`, countParams);
2025-08-26 10:06:23 +08:00
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;
// 兼容两种参数名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,
2025-09-02 09:29:20 +08:00
COALESCE(confirmed_from.confirmed_amount, 0) as confirmed_from_amount,
2025-08-26 10:06:23 +08:00
CASE
2025-09-02 09:29:20 +08:00
WHEN (COALESCE(u.balance, 0) +COALESCE(confirmed_from.confirmed_amount, 0) ) > ABS(u.balance)
2025-08-26 10:06:23 +08:00
THEN ABS(u.balance)
2025-09-02 09:29:20 +08:00
ELSE (COALESCE(u.balance, 0)+ COALESCE(confirmed_from.confirmed_amount, 0) )
2025-08-26 10:06:23 +08:00
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
2025-09-02 09:29:20 +08:00
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
2025-08-26 10:06:23 +08:00
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
2025-09-02 09:29:20 +08:00
`, [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)
})
2025-08-26 10:06:23 +08:00
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;