费用支出
This commit is contained in:
parent
1787de99e2
commit
f167a86fa5
|
|
@ -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
28
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('正在启动服务器...');
|
||||
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;
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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;
|
||||
Loading…
Reference in New Issue