diff --git a/addExpenseMenu.js b/addExpenseMenu.js new file mode 100644 index 0000000..d09638a --- /dev/null +++ b/addExpenseMenu.js @@ -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(); \ No newline at end of file diff --git a/app.js b/app.js index ec755bb..bdc0f4c 100644 --- a/app.js +++ b/app.js @@ -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(`服务器运行在 http://localhost:${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; \ No newline at end of file diff --git a/controllers/expenseController.js b/controllers/expenseController.js new file mode 100644 index 0000000..e40e84f --- /dev/null +++ b/controllers/expenseController.js @@ -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 +}; \ No newline at end of file diff --git a/controllers/statisticsController.js b/controllers/statisticsController.js index a77e103..f3ed0a6 100644 --- a/controllers/statisticsController.js +++ b/controllers/statisticsController.js @@ -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)); diff --git a/models/Expense.js b/models/Expense.js new file mode 100644 index 0000000..12df028 --- /dev/null +++ b/models/Expense.js @@ -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; \ No newline at end of file diff --git a/models/index.js b/models/index.js index b370abb..54b9e1f 100644 --- a/models/index.js +++ b/models/index.js @@ -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 }; \ No newline at end of file diff --git a/routes/expense.js b/routes/expense.js new file mode 100644 index 0000000..5932021 --- /dev/null +++ b/routes/expense.js @@ -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; \ No newline at end of file