费用支出

This commit is contained in:
wangxiaoxian 2026-03-15 20:37:31 +08:00
parent 1787de99e2
commit f167a86fa5
7 changed files with 455 additions and 7 deletions

99
addExpenseMenu.js Normal file
View File

@ -0,0 +1,99 @@
const sequelize = require('./config/db');
// 添加费用支出管理菜单
const addExpenseMenu = async () => {
try {
console.log('开始添加费用支出管理菜单...');
// 检查费用支出菜单是否已存在
const [expenseMenus] = await sequelize.query("SELECT id FROM menus WHERE code = 'expenses' LIMIT 1");
let expenseMenuId;
if (expenseMenus.length > 0) {
expenseMenuId = expenseMenus[0].id;
console.log('费用支出菜单已存在ID:', expenseMenuId);
// 更新现有菜单为一级菜单
await sequelize.query(`
UPDATE menus SET
parentId = NULL,
path = '/expenses',
icon = 'Wallet'
WHERE id = ${expenseMenuId}
`);
console.log('费用支出菜单已更新为一级菜单');
} else {
// 直接创建费用支出作为一级菜单
console.log('正在添加费用支出管理菜单...');
const expenseMenu = await sequelize.query(`
INSERT INTO menus (parentId, name, code, type, path, component, icon, sort, visible, status, createdAt, updatedAt)
VALUES (NULL, '费用支出', 'expenses', 'menu', '/expenses', 'finance/Expenses', 'Wallet', 5, 'show', 'active', NOW(), NOW())
`, { type: sequelize.QueryTypes.INSERT });
expenseMenuId = expenseMenu[0];
console.log('费用支出菜单创建成功ID:', expenseMenuId);
}
// 检查按钮权限是否已存在
const [existingButtons] = await sequelize.query("SELECT code FROM menus WHERE parentId = :expenseMenuId AND type = 'button'", {
replacements: { expenseMenuId }
});
const existingButtonCodes = existingButtons.map(btn => btn.code);
const requiredButtons = [
{ name: '查看', code: 'expenses:view', sort: 1 },
{ name: '新增', code: 'expenses:add', sort: 2 },
{ name: '编辑', code: 'expenses:edit', sort: 3 },
{ name: '删除', code: 'expenses:delete', sort: 4 }
];
// 只添加不存在的按钮
for (const button of requiredButtons) {
if (!existingButtonCodes.includes(button.code)) {
await sequelize.query(`
INSERT INTO menus (parentId, name, code, type, path, icon, sort, visible, status, createdAt, updatedAt)
VALUES (${expenseMenuId}, '${button.name}', '${button.code}', 'button', '', '', ${button.sort}, 'show', 'active', NOW(), NOW())
`);
console.log(`按钮权限 ${button.name} 创建成功`);
}
}
// 为管理员角色分配权限
console.log('正在为管理员角色分配权限...');
const [adminRoles] = await sequelize.query("SELECT id FROM roles WHERE code = 'admin' LIMIT 1");
if (adminRoles.length > 0) {
const adminRoleId = adminRoles[0].id;
console.log('管理员角色ID:', adminRoleId);
// 获取所有相关菜单ID
const [menus] = await sequelize.query(`
SELECT id FROM menus WHERE code IN ('expenses', 'expenses:view', 'expenses:add', 'expenses:edit', 'expenses:delete')
`);
// 获取已分配的权限
const [existingPermissions] = await sequelize.query(`
SELECT menuId FROM role_menus WHERE roleId = ${adminRoleId}
`);
const existingMenuIds = existingPermissions.map(p => p.menuId);
// 只分配不存在的权限
for (const menu of menus) {
if (!existingMenuIds.includes(menu.id)) {
await sequelize.query(`
INSERT IGNORE INTO role_menus (roleId, menuId, createdAt, updatedAt)
VALUES (${adminRoleId}, ${menu.id}, NOW(), NOW())
`);
console.log(`菜单ID ${menu.id} 权限分配成功`);
}
}
console.log('管理员角色权限分配完成');
} else {
console.log('管理员角色不存在,跳过权限分配');
}
console.log('费用支出管理菜单添加完成');
process.exit(0);
} catch (error) {
console.error('添加费用支出管理菜单失败:', error);
process.exit(1);
}
};
addExpenseMenu();

28
app.js
View File

@ -19,6 +19,7 @@ const userRoutes = require('./routes/user');
const roleRoutes = require('./routes/role');
const menuRoutes = require('./routes/menu');
const logRoutes = require('./routes/log');
const expenseRoutes = require('./routes/expense');
const app = express();
const PORT = process.env.PORT || 3000;
@ -42,6 +43,7 @@ app.use('/api/users', authMiddleware, operationLogMiddleware({ module: '用户
app.use('/api/roles', authMiddleware, operationLogMiddleware({ module: '角色管理' }), roleRoutes);
app.use('/api/menus', authMiddleware, operationLogMiddleware({ module: '菜单管理' }), menuRoutes);
app.use('/api/logs', authMiddleware, logRoutes);
app.use('/api/expenses', authMiddleware, operationLogMiddleware({ module: '费用支出管理' }), expenseRoutes);
// 测试接口
app.get('/', (req, res) => {
@ -49,8 +51,32 @@ app.get('/', (req, res) => {
});
// 启动服务器
app.listen(PORT, () => {
console.log('正在启动服务器...');
console.log('使用端口:', PORT);
try {
const server = app.listen(PORT, '0.0.0.0', () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
console.log(`服务器PID: ${process.pid}`);
console.log('服务器已成功启动,正在监听请求...');
});
// 错误处理
server.on('error', (error) => {
console.error('服务器启动错误:', error);
process.exit(1);
});
// 进程终止处理
process.on('SIGINT', () => {
console.log('正在关闭服务器...');
server.close(() => {
console.log('服务器已关闭');
process.exit(0);
});
});
} catch (error) {
console.error('服务器启动异常:', error);
process.exit(1);
}
module.exports = app;

View File

@ -0,0 +1,208 @@
const models = require('../models');
const { Op } = require('sequelize');
// 格式化时间(考虑时区,转换为北京时间)
const formatDate = (date) => {
if (!date) return null;
const beijingDate = new Date(date.getTime() + 8 * 60 * 60 * 1000);
return beijingDate.toISOString().replace('T', ' ').slice(0, 19);
};
// 格式化日期(只返回年月日)
const formatDateOnly = (date) => {
if (!date) return null;
const beijingDate = new Date(date.getTime() + 8 * 60 * 60 * 1000);
return beijingDate.toISOString().split('T')[0];
};
// 格式化费用支出数据
const formatExpenseData = (expense) => {
return {
...expense.toJSON(),
date: formatDateOnly(expense.date),
createTime: formatDate(expense.createTime),
updateTime: formatDate(expense.updateTime)
};
};
// 获取所有费用支出(支持搜索和分页)
const getAllExpenses = async (req, res) => {
try {
const { category, startDate, endDate, page = 1, pageSize = 10 } = req.query;
// 构建查询条件
const where = { isDeleted: 0 };
if (category) {
where.category = {
[Op.like]: `%${category}%`
};
}
if (startDate) {
where.date = {
...where.date,
[Op.gte]: startDate
};
}
if (endDate) {
where.date = {
...where.date,
[Op.lte]: endDate
};
}
// 计算偏移量
const offset = (page - 1) * pageSize;
// 查询费用支出数据
const { count, rows } = await models.Expense.findAndCountAll({
where,
limit: parseInt(pageSize),
offset: parseInt(offset),
order: [['date', 'DESC']]
});
// 格式化数据
const formattedExpenses = rows.map(formatExpenseData);
// 返回结果
res.status(200).json({
data: formattedExpenses,
total: count,
page: parseInt(page),
pageSize: parseInt(pageSize)
});
} catch (error) {
res.status(500).json({ error: error.message });
}
};
// 获取单个费用支出
const getExpenseById = async (req, res) => {
try {
const { id } = req.params;
const expense = await models.Expense.findOne({
where: { id, isDeleted: 0 }
});
if (!expense) {
return res.status(404).json({ error: '费用支出不存在' });
}
const formattedExpense = formatExpenseData(expense);
res.status(200).json(formattedExpense);
} catch (error) {
res.status(500).json({ error: error.message });
}
};
// 创建费用支出
const createExpense = async (req, res) => {
try {
const { date, amount, category, remark } = req.body;
const expense = await models.Expense.create({
date,
amount,
category,
remark,
createBy: req.user.id,
updateBy: req.user.id
});
const formattedExpense = formatExpenseData(expense);
res.status(201).json(formattedExpense);
} catch (error) {
res.status(500).json({ error: error.message });
}
};
// 更新费用支出
const updateExpense = async (req, res) => {
try {
const { id } = req.params;
const { date, amount, category, remark } = req.body;
const expense = await models.Expense.findOne({
where: { id, isDeleted: 0 }
});
if (!expense) {
return res.status(404).json({ error: '费用支出不存在' });
}
await expense.update({
date,
amount,
category,
remark,
updateBy: req.user.id
});
const formattedExpense = formatExpenseData(expense);
res.status(200).json(formattedExpense);
} catch (error) {
res.status(500).json({ error: error.message });
}
};
// 删除费用支出(软删除)
const deleteExpense = async (req, res) => {
try {
const { id } = req.params;
const expense = await models.Expense.findOne({
where: { id, isDeleted: 0 }
});
if (!expense) {
return res.status(404).json({ error: '费用支出不存在' });
}
await expense.update({
isDeleted: 1,
updateBy: req.user.id
});
res.status(200).json({ message: '费用支出删除成功' });
} catch (error) {
res.status(500).json({ error: error.message });
}
};
// 获取所有费用支出(不分页)
const listExpenses = async (req, res) => {
try {
const { category, startDate, endDate } = req.query;
// 构建查询条件
const where = { isDeleted: 0 };
if (category) {
where.category = {
[Op.like]: `%${category}%`
};
}
if (startDate) {
where.date = {
...where.date,
[Op.gte]: startDate
};
}
if (endDate) {
where.date = {
...where.date,
[Op.lte]: endDate
};
}
// 查询费用支出数据
const expenses = await models.Expense.findAll({
where,
order: [['date', 'DESC']]
});
// 格式化数据
const formattedExpenses = expenses.map(formatExpenseData);
// 返回结果
res.status(200).json(formattedExpenses);
} catch (error) {
res.status(500).json({ error: error.message });
}
};
module.exports = {
getAllExpenses,
listExpenses,
getExpenseById,
createExpense,
updateExpense,
deleteExpense
};

View File

@ -1,4 +1,4 @@
const { Room, Rental, Apartment } = require('../models');
const { Room, Rental, Apartment, Expense } = require('../models');
const { Op } = require('sequelize');
// 租金统计
@ -16,7 +16,8 @@ const getRentStatistics = async (req, res) => {
monthlyRent[monthKey] = {
amount: 0,
depositReceived: 0,
depositRefunded: 0
depositRefunded: 0,
expense: 0
};
}
@ -60,13 +61,42 @@ const getRentStatistics = async (req, res) => {
}
});
// 从数据库查询过去12个月的费用支出记录排除已删除的
const expenses = await Expense.findAll({
where: {
date: {
[Op.gte]: startDate
},
isDeleted: 0
}
});
// 按月份统计费用支出
expenses.forEach(expense => {
if (expense.date) {
// 解析费用日期
const expenseDate = new Date(expense.date);
const monthKey = `${expenseDate.getFullYear()}-${(expenseDate.getMonth() + 1).toString().padStart(2, '0')}`;
// 如果该月份在我们的统计范围内
if (monthlyRent.hasOwnProperty(monthKey)) {
// 统计费用支出
const expenseAmount = parseFloat(expense.amount) || 0;
if (expenseAmount > 0) {
monthlyRent[monthKey].expense += expenseAmount;
}
}
}
});
// 转换为数组格式并按月份倒序排序
const rentStatistics = Object.entries(monthlyRent)
.map(([month, data]) => ({
month,
amount: Math.round(data.amount * 100) / 100, // 保留两位小数
depositReceived: Math.round(data.depositReceived * 100) / 100, // 保留两位小数
depositRefunded: Math.round(data.depositRefunded * 100) / 100 // 保留两位小数
depositRefunded: Math.round(data.depositRefunded * 100) / 100, // 保留两位小数
expense: Math.round(data.expense * 100) / 100 // 保留两位小数
}))
.sort((a, b) => b.month.localeCompare(a.month));

63
models/Expense.js Normal file
View File

@ -0,0 +1,63 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../config/db');
const Expense = sequelize.define('Expense', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
comment: '支出ID'
},
date: {
type: DataTypes.DATE,
allowNull: false,
comment: '支出日期'
},
amount: {
type: DataTypes.DECIMAL(10, 2),
allowNull: false,
comment: '支出金额'
},
category: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '支出类别'
},
remark: {
type: DataTypes.STRING(255),
allowNull: true,
comment: '备注'
},
createBy: {
type: DataTypes.INTEGER,
allowNull: true,
comment: '创建人ID'
},
createTime: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
comment: '创建时间'
},
updateBy: {
type: DataTypes.INTEGER,
allowNull: true,
comment: '修改人ID'
},
updateTime: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
onUpdate: DataTypes.NOW,
comment: '更新时间'
},
isDeleted: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '删除状态0未删除1已删除'
}
}, {
tableName: 'expenses',
timestamps: false
});
module.exports = Expense;

View File

@ -9,6 +9,7 @@ const Menu = require('./Menu');
const RoleMenu = require('./RoleMenu');
const OperationLog = require('./OperationLog');
const LoginLog = require('./LoginLog');
const Expense = require('./Expense');
// 关联关系
User.belongsTo(Role, { foreignKey: 'roleId', as: 'role' });
@ -33,5 +34,6 @@ module.exports = {
Menu,
RoleMenu,
OperationLog,
LoginLog
LoginLog,
Expense
};

20
routes/expense.js Normal file
View File

@ -0,0 +1,20 @@
const express = require('express');
const router = express.Router();
const expenseController = require('../controllers/expenseController');
// 获取所有费用支出
router.get('/', expenseController.getAllExpenses);
// 获取单个费用支出
router.get('/:id', expenseController.getExpenseById);
// 创建费用支出
router.post('/', expenseController.createExpense);
// 更新费用支出
router.put('/:id', expenseController.updateExpense);
// 删除费用支出
router.delete('/:id', expenseController.deleteExpense);
module.exports = router;