459 lines
12 KiB
JavaScript
459 lines
12 KiB
JavaScript
|
|
/**
|
||
|
|
* 交易控制器
|
||
|
|
* @file transactionController.js
|
||
|
|
* @description 处理银行交易相关的请求
|
||
|
|
*/
|
||
|
|
const { Transaction, Account, User } = require('../models');
|
||
|
|
const { validationResult } = require('express-validator');
|
||
|
|
const { Op } = require('sequelize');
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 获取交易记录列表
|
||
|
|
* @param {Object} req 请求对象
|
||
|
|
* @param {Object} res 响应对象
|
||
|
|
*/
|
||
|
|
exports.getTransactions = async (req, res) => {
|
||
|
|
try {
|
||
|
|
const page = parseInt(req.query.page) || 1;
|
||
|
|
const limit = parseInt(req.query.limit) || 20;
|
||
|
|
const offset = (page - 1) * limit;
|
||
|
|
const {
|
||
|
|
account_id,
|
||
|
|
transaction_type,
|
||
|
|
status,
|
||
|
|
start_date,
|
||
|
|
end_date,
|
||
|
|
amount_min,
|
||
|
|
amount_max
|
||
|
|
} = req.query;
|
||
|
|
|
||
|
|
const whereClause = {};
|
||
|
|
|
||
|
|
// 普通用户只能查看自己账户的交易记录
|
||
|
|
if (req.user.role.name !== 'admin') {
|
||
|
|
const userAccounts = await Account.findAll({
|
||
|
|
where: { user_id: req.user.id },
|
||
|
|
attributes: ['id']
|
||
|
|
});
|
||
|
|
const accountIds = userAccounts.map(account => account.id);
|
||
|
|
whereClause.account_id = { [Op.in]: accountIds };
|
||
|
|
} else if (account_id) {
|
||
|
|
whereClause.account_id = account_id;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (transaction_type) {
|
||
|
|
whereClause.transaction_type = transaction_type;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (status) {
|
||
|
|
whereClause.status = status;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (start_date || end_date) {
|
||
|
|
whereClause.created_at = {};
|
||
|
|
if (start_date) {
|
||
|
|
whereClause.created_at[Op.gte] = new Date(start_date);
|
||
|
|
}
|
||
|
|
if (end_date) {
|
||
|
|
whereClause.created_at[Op.lte] = new Date(end_date);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (amount_min || amount_max) {
|
||
|
|
whereClause.amount = {};
|
||
|
|
if (amount_min) {
|
||
|
|
whereClause.amount[Op.gte] = Math.round(parseFloat(amount_min) * 100);
|
||
|
|
}
|
||
|
|
if (amount_max) {
|
||
|
|
whereClause.amount[Op.lte] = Math.round(parseFloat(amount_max) * 100);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const { count, rows } = await Transaction.findAndCountAll({
|
||
|
|
where: whereClause,
|
||
|
|
include: [{
|
||
|
|
model: Account,
|
||
|
|
as: 'account',
|
||
|
|
include: [{
|
||
|
|
model: User,
|
||
|
|
as: 'user',
|
||
|
|
attributes: ['id', 'username', 'real_name']
|
||
|
|
}]
|
||
|
|
}],
|
||
|
|
limit,
|
||
|
|
offset,
|
||
|
|
order: [['created_at', 'DESC']]
|
||
|
|
});
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: {
|
||
|
|
transactions: rows.map(transaction => ({
|
||
|
|
...transaction.getSafeInfo(),
|
||
|
|
amount_formatted: transaction.getAmountFormatted(),
|
||
|
|
balance_after_formatted: transaction.getBalanceAfterFormatted(),
|
||
|
|
type_description: transaction.getTypeDescription(),
|
||
|
|
status_description: transaction.getStatusDescription(),
|
||
|
|
is_income: transaction.isIncome(),
|
||
|
|
is_expense: transaction.isExpense()
|
||
|
|
})),
|
||
|
|
pagination: {
|
||
|
|
page,
|
||
|
|
limit,
|
||
|
|
total: count,
|
||
|
|
pages: Math.ceil(count / limit)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取交易记录错误:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '服务器内部错误'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 获取交易详情
|
||
|
|
* @param {Object} req 请求对象
|
||
|
|
* @param {Object} res 响应对象
|
||
|
|
*/
|
||
|
|
exports.getTransactionDetail = async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { transactionId } = req.params;
|
||
|
|
|
||
|
|
const transaction = await Transaction.findByPk(transactionId, {
|
||
|
|
include: [{
|
||
|
|
model: Account,
|
||
|
|
as: 'account',
|
||
|
|
include: [{
|
||
|
|
model: User,
|
||
|
|
as: 'user',
|
||
|
|
attributes: ['id', 'username', 'real_name']
|
||
|
|
}]
|
||
|
|
}]
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!transaction) {
|
||
|
|
return res.status(404).json({
|
||
|
|
success: false,
|
||
|
|
message: '交易记录不存在'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 检查权限
|
||
|
|
if (req.user.role.name !== 'admin' && transaction.account.user_id !== req.user.id) {
|
||
|
|
return res.status(403).json({
|
||
|
|
success: false,
|
||
|
|
message: '无权访问该交易记录'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: {
|
||
|
|
...transaction.getSafeInfo(),
|
||
|
|
amount_formatted: transaction.getAmountFormatted(),
|
||
|
|
balance_after_formatted: transaction.getBalanceAfterFormatted(),
|
||
|
|
type_description: transaction.getTypeDescription(),
|
||
|
|
status_description: transaction.getStatusDescription(),
|
||
|
|
is_income: transaction.isIncome(),
|
||
|
|
is_expense: transaction.isExpense(),
|
||
|
|
can_reverse: transaction.canReverse()
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取交易详情错误:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '服务器内部错误'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 转账
|
||
|
|
* @param {Object} req 请求对象
|
||
|
|
* @param {Object} res 响应对象
|
||
|
|
*/
|
||
|
|
exports.transfer = async (req, res) => {
|
||
|
|
try {
|
||
|
|
const errors = validationResult(req);
|
||
|
|
if (!errors.isEmpty()) {
|
||
|
|
return res.status(400).json({
|
||
|
|
success: false,
|
||
|
|
message: '输入数据验证失败',
|
||
|
|
errors: errors.array()
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const { from_account_id, to_account_number, amount, description } = req.body;
|
||
|
|
|
||
|
|
// 查找转出账户
|
||
|
|
const fromAccount = await Account.findByPk(from_account_id);
|
||
|
|
if (!fromAccount) {
|
||
|
|
return res.status(404).json({
|
||
|
|
success: false,
|
||
|
|
message: '转出账户不存在'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 检查转出账户权限
|
||
|
|
if (req.user.role.name !== 'admin' && fromAccount.user_id !== req.user.id) {
|
||
|
|
return res.status(403).json({
|
||
|
|
success: false,
|
||
|
|
message: '无权操作该账户'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 查找转入账户
|
||
|
|
const toAccount = await Account.findOne({
|
||
|
|
where: { account_number: to_account_number },
|
||
|
|
include: [{
|
||
|
|
model: User,
|
||
|
|
as: 'user',
|
||
|
|
attributes: ['id', 'username', 'real_name']
|
||
|
|
}]
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!toAccount) {
|
||
|
|
return res.status(404).json({
|
||
|
|
success: false,
|
||
|
|
message: '转入账户不存在'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 检查账户状态
|
||
|
|
if (!fromAccount.isActive() || !toAccount.isActive()) {
|
||
|
|
return res.status(400).json({
|
||
|
|
success: false,
|
||
|
|
message: '账户状态异常,无法进行转账操作'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const amountInCents = Math.round(amount * 100);
|
||
|
|
|
||
|
|
// 检查余额是否充足
|
||
|
|
if (!fromAccount.hasSufficientBalance(amountInCents)) {
|
||
|
|
return res.status(400).json({
|
||
|
|
success: false,
|
||
|
|
message: '账户余额不足'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 开始事务
|
||
|
|
const transaction = await sequelize.transaction();
|
||
|
|
|
||
|
|
try {
|
||
|
|
// 更新转出账户余额
|
||
|
|
await fromAccount.update({
|
||
|
|
balance: fromAccount.balance - amountInCents,
|
||
|
|
available_balance: fromAccount.available_balance - amountInCents
|
||
|
|
}, { transaction });
|
||
|
|
|
||
|
|
// 更新转入账户余额
|
||
|
|
await toAccount.update({
|
||
|
|
balance: toAccount.balance + amountInCents,
|
||
|
|
available_balance: toAccount.available_balance + amountInCents
|
||
|
|
}, { transaction });
|
||
|
|
|
||
|
|
const transactionNumber = await generateTransactionNumber();
|
||
|
|
|
||
|
|
// 创建转出交易记录
|
||
|
|
await Transaction.create({
|
||
|
|
transaction_number: transactionNumber,
|
||
|
|
account_id: fromAccount.id,
|
||
|
|
transaction_type: 'transfer_out',
|
||
|
|
amount: amountInCents,
|
||
|
|
balance_before: fromAccount.balance + amountInCents,
|
||
|
|
balance_after: fromAccount.balance,
|
||
|
|
counterparty_account: toAccount.account_number,
|
||
|
|
counterparty_name: toAccount.user.real_name,
|
||
|
|
description: description || `转账给${toAccount.user.real_name}`,
|
||
|
|
status: 'completed',
|
||
|
|
processed_at: new Date()
|
||
|
|
}, { transaction });
|
||
|
|
|
||
|
|
// 创建转入交易记录
|
||
|
|
await Transaction.create({
|
||
|
|
transaction_number: transactionNumber,
|
||
|
|
account_id: toAccount.id,
|
||
|
|
transaction_type: 'transfer_in',
|
||
|
|
amount: amountInCents,
|
||
|
|
balance_before: toAccount.balance - amountInCents,
|
||
|
|
balance_after: toAccount.balance,
|
||
|
|
counterparty_account: fromAccount.account_number,
|
||
|
|
counterparty_name: fromAccount.user.real_name,
|
||
|
|
description: description || `来自${fromAccount.user.real_name}的转账`,
|
||
|
|
status: 'completed',
|
||
|
|
processed_at: new Date()
|
||
|
|
}, { transaction });
|
||
|
|
|
||
|
|
await transaction.commit();
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
message: '转账成功',
|
||
|
|
data: {
|
||
|
|
transaction_number: transactionNumber,
|
||
|
|
amount: amount,
|
||
|
|
from_account: fromAccount.account_number,
|
||
|
|
to_account: toAccount.account_number,
|
||
|
|
to_account_holder: toAccount.user.real_name
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
await transaction.rollback();
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('转账错误:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '服务器内部错误'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 撤销交易
|
||
|
|
* @param {Object} req 请求对象
|
||
|
|
* @param {Object} res 响应对象
|
||
|
|
*/
|
||
|
|
exports.reverseTransaction = async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { transactionId } = req.params;
|
||
|
|
|
||
|
|
const transaction = await Transaction.findByPk(transactionId, {
|
||
|
|
include: [{
|
||
|
|
model: Account,
|
||
|
|
as: 'account'
|
||
|
|
}]
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!transaction) {
|
||
|
|
return res.status(404).json({
|
||
|
|
success: false,
|
||
|
|
message: '交易记录不存在'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 检查权限
|
||
|
|
if (req.user.role.name !== 'admin' && transaction.account.user_id !== req.user.id) {
|
||
|
|
return res.status(403).json({
|
||
|
|
success: false,
|
||
|
|
message: '无权操作该交易'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 检查是否可以撤销
|
||
|
|
if (!transaction.canReverse()) {
|
||
|
|
return res.status(400).json({
|
||
|
|
success: false,
|
||
|
|
message: '该交易无法撤销'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = await transaction.reverse();
|
||
|
|
if (result) {
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
message: '交易撤销成功'
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
res.status(400).json({
|
||
|
|
success: false,
|
||
|
|
message: '交易撤销失败'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('撤销交易错误:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '服务器内部错误'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 获取交易统计
|
||
|
|
* @param {Object} req 请求对象
|
||
|
|
* @param {Object} res 响应对象
|
||
|
|
*/
|
||
|
|
exports.getTransactionStats = async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { start_date, end_date, account_id } = req.query;
|
||
|
|
|
||
|
|
const whereClause = {};
|
||
|
|
|
||
|
|
// 普通用户只能查看自己账户的统计
|
||
|
|
if (req.user.role.name !== 'admin') {
|
||
|
|
const userAccounts = await Account.findAll({
|
||
|
|
where: { user_id: req.user.id },
|
||
|
|
attributes: ['id']
|
||
|
|
});
|
||
|
|
const accountIds = userAccounts.map(account => account.id);
|
||
|
|
whereClause.account_id = { [Op.in]: accountIds };
|
||
|
|
} else if (account_id) {
|
||
|
|
whereClause.account_id = account_id;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (start_date || end_date) {
|
||
|
|
whereClause.created_at = {};
|
||
|
|
if (start_date) {
|
||
|
|
whereClause.created_at[Op.gte] = new Date(start_date);
|
||
|
|
}
|
||
|
|
if (end_date) {
|
||
|
|
whereClause.created_at[Op.lte] = new Date(end_date);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 获取交易统计
|
||
|
|
const stats = await Transaction.findAll({
|
||
|
|
where: whereClause,
|
||
|
|
attributes: [
|
||
|
|
'transaction_type',
|
||
|
|
[sequelize.fn('COUNT', sequelize.col('id')), 'count'],
|
||
|
|
[sequelize.fn('SUM', sequelize.col('amount')), 'total_amount']
|
||
|
|
],
|
||
|
|
group: ['transaction_type'],
|
||
|
|
raw: true
|
||
|
|
});
|
||
|
|
|
||
|
|
// 获取总交易数
|
||
|
|
const totalCount = await Transaction.count({ where: whereClause });
|
||
|
|
|
||
|
|
// 获取总交易金额
|
||
|
|
const totalAmount = await Transaction.sum('amount', { where: whereClause });
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: {
|
||
|
|
total_count: totalCount,
|
||
|
|
total_amount: totalAmount ? (totalAmount / 100).toFixed(2) : '0.00',
|
||
|
|
by_type: stats.map(stat => ({
|
||
|
|
type: stat.transaction_type,
|
||
|
|
count: parseInt(stat.count),
|
||
|
|
total_amount: (parseInt(stat.total_amount) / 100).toFixed(2)
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取交易统计错误:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '服务器内部错误'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 生成交易流水号
|
||
|
|
* @returns {String} 交易流水号
|
||
|
|
*/
|
||
|
|
async function generateTransactionNumber() {
|
||
|
|
const timestamp = Date.now().toString();
|
||
|
|
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
|
||
|
|
return `TXN${timestamp}${random}`;
|
||
|
|
}
|