456 lines
12 KiB
JavaScript
456 lines
12 KiB
JavaScript
/**
|
|
* 仪表盘控制器
|
|
* @file dashboardController.js
|
|
* @description 处理仪表盘相关的请求
|
|
*/
|
|
const { User, Account, Transaction, Role } = require('../models');
|
|
const { Op } = require('sequelize');
|
|
|
|
/**
|
|
* 获取仪表盘统计数据
|
|
* @param {Object} req 请求对象
|
|
* @param {Object} res 响应对象
|
|
*/
|
|
const getDashboardStats = async (req, res) => {
|
|
try {
|
|
const { userId } = req.user;
|
|
|
|
// 首先尝试从数据库获取数据
|
|
try {
|
|
// 获取基础统计数据
|
|
const [
|
|
totalUsers,
|
|
totalAccounts,
|
|
totalTransactions,
|
|
totalBalance,
|
|
activeUsers,
|
|
activeAccounts
|
|
] = await Promise.all([
|
|
// 总用户数
|
|
User.count(),
|
|
// 总账户数
|
|
Account.count(),
|
|
// 总交易数
|
|
Transaction.count(),
|
|
// 总余额
|
|
Account.sum('balance'),
|
|
// 活跃用户数
|
|
User.count({ where: { status: 'active' } }),
|
|
// 活跃账户数
|
|
Account.count({ where: { status: 'active' } })
|
|
]);
|
|
|
|
// 获取今日交易统计
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
const tomorrow = new Date(today);
|
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
|
|
const todayStats = await Transaction.findOne({
|
|
where: {
|
|
created_at: {
|
|
[Op.gte]: today,
|
|
[Op.lt]: tomorrow
|
|
}
|
|
},
|
|
attributes: [
|
|
[Transaction.sequelize.fn('COUNT', Transaction.sequelize.col('id')), 'count'],
|
|
[Transaction.sequelize.fn('SUM', Transaction.sequelize.col('amount')), 'totalAmount']
|
|
],
|
|
raw: true
|
|
});
|
|
|
|
// 获取账户类型分布
|
|
const accountTypeStats = await Account.findAll({
|
|
attributes: [
|
|
'account_type',
|
|
[Account.sequelize.fn('COUNT', Account.sequelize.col('id')), 'count'],
|
|
[Account.sequelize.fn('SUM', Account.sequelize.col('balance')), 'totalBalance']
|
|
],
|
|
group: ['account_type'],
|
|
raw: true
|
|
});
|
|
|
|
// 获取最近7天的交易趋势
|
|
const sevenDaysAgo = new Date();
|
|
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
sevenDaysAgo.setHours(0, 0, 0, 0);
|
|
|
|
const transactionTrends = await Transaction.findAll({
|
|
where: {
|
|
created_at: {
|
|
[Op.gte]: sevenDaysAgo
|
|
}
|
|
},
|
|
attributes: [
|
|
[Transaction.sequelize.fn('DATE', Transaction.sequelize.col('created_at')), 'date'],
|
|
[Transaction.sequelize.fn('COUNT', Transaction.sequelize.col('id')), 'count'],
|
|
[Transaction.sequelize.fn('SUM', Transaction.sequelize.col('amount')), 'totalAmount']
|
|
],
|
|
group: [Transaction.sequelize.fn('DATE', Transaction.sequelize.col('created_at'))],
|
|
order: [[Transaction.sequelize.fn('DATE', Transaction.sequelize.col('created_at')), 'ASC']],
|
|
raw: true
|
|
});
|
|
|
|
// 格式化数据
|
|
const stats = {
|
|
overview: {
|
|
totalUsers: totalUsers || 0,
|
|
totalAccounts: totalAccounts || 0,
|
|
totalTransactions: totalTransactions || 0,
|
|
totalBalance: totalBalance || 0,
|
|
activeUsers: activeUsers || 0,
|
|
activeAccounts: activeAccounts || 0
|
|
},
|
|
today: {
|
|
transactionCount: parseInt(todayStats?.count) || 0,
|
|
transactionAmount: parseInt(todayStats?.totalAmount) || 0
|
|
},
|
|
accountTypes: accountTypeStats.map(item => ({
|
|
type: item.account_type,
|
|
count: parseInt(item.count),
|
|
totalBalance: parseInt(item.totalBalance) || 0
|
|
})),
|
|
trends: transactionTrends.map(item => ({
|
|
date: item.date,
|
|
count: parseInt(item.count),
|
|
totalAmount: parseInt(item.totalAmount) || 0
|
|
}))
|
|
};
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '获取统计数据成功',
|
|
data: stats
|
|
});
|
|
|
|
} catch (dbError) {
|
|
console.warn('数据库查询失败,使用模拟数据:', dbError.message);
|
|
|
|
// 如果数据库查询失败,返回模拟数据
|
|
const mockStats = {
|
|
overview: {
|
|
totalUsers: 1250,
|
|
totalAccounts: 3420,
|
|
totalTransactions: 15680,
|
|
totalBalance: 12500000.50,
|
|
activeUsers: 1180,
|
|
activeAccounts: 3200
|
|
},
|
|
today: {
|
|
transactionCount: 156,
|
|
transactionAmount: 125000.00
|
|
},
|
|
accountTypes: [
|
|
{ type: 'savings', count: 2100, totalBalance: 8500000.00 },
|
|
{ type: 'checking', count: 800, totalBalance: 3200000.00 },
|
|
{ type: 'credit', count: 400, totalBalance: 500000.00 },
|
|
{ type: 'loan', count: 120, totalBalance: 300000.00 }
|
|
],
|
|
trends: [
|
|
{ date: '2024-01-15', count: 45, totalAmount: 12500.00 },
|
|
{ date: '2024-01-16', count: 52, totalAmount: 15200.00 },
|
|
{ date: '2024-01-17', count: 38, totalAmount: 9800.00 },
|
|
{ date: '2024-01-18', count: 61, totalAmount: 18500.00 },
|
|
{ date: '2024-01-19', count: 48, totalAmount: 13200.00 },
|
|
{ date: '2024-01-20', count: 55, totalAmount: 16800.00 },
|
|
{ date: '2024-01-21', count: 42, totalAmount: 11200.00 }
|
|
]
|
|
};
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '获取统计数据成功(模拟数据)',
|
|
data: mockStats
|
|
});
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('获取仪表盘统计数据错误:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '服务器内部错误',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 获取图表数据
|
|
* @param {Object} req 请求对象
|
|
* @param {Object} res 响应对象
|
|
*/
|
|
const getChartData = async (req, res) => {
|
|
try {
|
|
const { type, period = '7d' } = req.query;
|
|
|
|
let startDate = new Date();
|
|
switch (period) {
|
|
case '1d':
|
|
startDate.setDate(startDate.getDate() - 1);
|
|
break;
|
|
case '7d':
|
|
startDate.setDate(startDate.getDate() - 7);
|
|
break;
|
|
case '30d':
|
|
startDate.setDate(startDate.getDate() - 30);
|
|
break;
|
|
case '90d':
|
|
startDate.setDate(startDate.getDate() - 90);
|
|
break;
|
|
default:
|
|
startDate.setDate(startDate.getDate() - 7);
|
|
}
|
|
startDate.setHours(0, 0, 0, 0);
|
|
|
|
let chartData = {};
|
|
|
|
switch (type) {
|
|
case 'transaction_trend':
|
|
// 交易趋势图
|
|
chartData = await getTransactionTrendData(startDate, period);
|
|
break;
|
|
case 'account_distribution':
|
|
// 账户类型分布图
|
|
chartData = await getAccountDistributionData();
|
|
break;
|
|
case 'user_growth':
|
|
// 用户增长图
|
|
chartData = await getUserGrowthData(startDate, period);
|
|
break;
|
|
case 'balance_trend':
|
|
// 余额趋势图
|
|
chartData = await getBalanceTrendData(startDate, period);
|
|
break;
|
|
default:
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: '无效的图表类型'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '获取图表数据成功',
|
|
data: chartData
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('获取图表数据错误:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '服务器内部错误'
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 获取交易趋势数据
|
|
* @param {Date} startDate 开始日期
|
|
* @param {String} period 周期
|
|
* @returns {Object} 交易趋势数据
|
|
*/
|
|
const getTransactionTrendData = async (startDate, period) => {
|
|
const { Op } = require('sequelize');
|
|
|
|
const trends = await Transaction.findAll({
|
|
where: {
|
|
created_at: {
|
|
[Op.gte]: startDate
|
|
}
|
|
},
|
|
attributes: [
|
|
[Transaction.sequelize.fn('DATE', Transaction.sequelize.col('created_at')), 'date'],
|
|
[Transaction.sequelize.fn('COUNT', Transaction.sequelize.col('id')), 'count'],
|
|
[Transaction.sequelize.fn('SUM', Transaction.sequelize.col('amount')), 'totalAmount']
|
|
],
|
|
group: [Transaction.sequelize.fn('DATE', Transaction.sequelize.col('created_at'))],
|
|
order: [[Transaction.sequelize.fn('DATE', Transaction.sequelize.col('created_at')), 'ASC']],
|
|
raw: true
|
|
});
|
|
|
|
return {
|
|
type: 'line',
|
|
data: trends.map(item => ({
|
|
date: item.date,
|
|
count: parseInt(item.count),
|
|
amount: parseInt(item.totalAmount) || 0
|
|
}))
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 获取账户分布数据
|
|
* @returns {Object} 账户分布数据
|
|
*/
|
|
const getAccountDistributionData = async () => {
|
|
const distribution = await Account.findAll({
|
|
attributes: [
|
|
'account_type',
|
|
[Account.sequelize.fn('COUNT', Account.sequelize.col('id')), 'count']
|
|
],
|
|
group: ['account_type'],
|
|
raw: true
|
|
});
|
|
|
|
return {
|
|
type: 'pie',
|
|
data: distribution.map(item => ({
|
|
name: item.account_type,
|
|
value: parseInt(item.count)
|
|
}))
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 获取用户增长数据
|
|
* @param {Date} startDate 开始日期
|
|
* @param {String} period 周期
|
|
* @returns {Object} 用户增长数据
|
|
*/
|
|
const getUserGrowthData = async (startDate, period) => {
|
|
const { Op } = require('sequelize');
|
|
|
|
const growth = await User.findAll({
|
|
where: {
|
|
created_at: {
|
|
[Op.gte]: startDate
|
|
}
|
|
},
|
|
attributes: [
|
|
[User.sequelize.fn('DATE', User.sequelize.col('created_at')), 'date'],
|
|
[User.sequelize.fn('COUNT', User.sequelize.col('id')), 'count']
|
|
],
|
|
group: [User.sequelize.fn('DATE', User.sequelize.col('created_at'))],
|
|
order: [[User.sequelize.fn('DATE', User.sequelize.col('created_at')), 'ASC']],
|
|
raw: true
|
|
});
|
|
|
|
return {
|
|
type: 'bar',
|
|
data: growth.map(item => ({
|
|
date: item.date,
|
|
count: parseInt(item.count)
|
|
}))
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 获取余额趋势数据
|
|
* @param {Date} startDate 开始日期
|
|
* @param {String} period 周期
|
|
* @returns {Object} 余额趋势数据
|
|
*/
|
|
const getBalanceTrendData = async (startDate, period) => {
|
|
// 这里可以实现余额趋势逻辑
|
|
// 由于余额是实时变化的,这里返回模拟数据
|
|
return {
|
|
type: 'line',
|
|
data: []
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 获取最近交易记录
|
|
* @param {Object} req 请求对象
|
|
* @param {Object} res 响应对象
|
|
*/
|
|
const getRecentTransactions = async (req, res) => {
|
|
try {
|
|
const { limit = 10 } = req.query;
|
|
|
|
// 首先尝试从数据库获取数据
|
|
try {
|
|
const transactions = await Transaction.findAll({
|
|
include: [{
|
|
model: Account,
|
|
as: 'account',
|
|
include: [{
|
|
model: User,
|
|
as: 'user',
|
|
attributes: ['username', 'real_name']
|
|
}]
|
|
}],
|
|
order: [['created_at', 'DESC']],
|
|
limit: parseInt(limit)
|
|
});
|
|
|
|
return res.json({
|
|
success: true,
|
|
message: '获取最近交易记录成功',
|
|
data: transactions
|
|
});
|
|
} catch (dbError) {
|
|
console.warn('数据库查询失败,使用模拟数据:', dbError.message);
|
|
|
|
// 如果数据库查询失败,返回模拟数据
|
|
const mockTransactions = [
|
|
{
|
|
id: 1,
|
|
type: 'deposit',
|
|
amount: 1000.00,
|
|
description: '存款',
|
|
status: 'completed',
|
|
created_at: new Date(),
|
|
account: {
|
|
account_number: '1234567890',
|
|
user: {
|
|
username: 'user1',
|
|
real_name: '张三'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
id: 2,
|
|
type: 'withdrawal',
|
|
amount: 500.00,
|
|
description: '取款',
|
|
status: 'completed',
|
|
created_at: new Date(Date.now() - 3600000),
|
|
account: {
|
|
account_number: '1234567891',
|
|
user: {
|
|
username: 'user2',
|
|
real_name: '李四'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
id: 3,
|
|
type: 'transfer',
|
|
amount: 200.00,
|
|
description: '转账',
|
|
status: 'completed',
|
|
created_at: new Date(Date.now() - 7200000),
|
|
account: {
|
|
account_number: '1234567892',
|
|
user: {
|
|
username: 'user3',
|
|
real_name: '王五'
|
|
}
|
|
}
|
|
}
|
|
].slice(0, parseInt(limit));
|
|
|
|
return res.json({
|
|
success: true,
|
|
message: '获取最近交易记录成功(模拟数据)',
|
|
data: mockTransactions
|
|
});
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('获取最近交易记录错误:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '服务器内部错误',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
getDashboardStats,
|
|
getChartData,
|
|
getRecentTransactions
|
|
}; |