From 6d462df53250c09f61bb8210121ca945411dcce6 Mon Sep 17 00:00:00 2001 From: wangxiaoxian <1094175543@qq.com> Date: Mon, 9 Mar 2026 00:28:33 +0800 Subject: [PATCH] =?UTF-8?q?=E7=99=BB=E5=BD=95=E6=9D=83=E9=99=90=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 33 +- controllers/apartmentController.js | 44 +-- controllers/authController.js | 296 ++++++++++++++ controllers/logController.js | 162 ++++++++ controllers/menuController.js | 563 ++++++++++++++++++++++++++ controllers/regionController.js | 125 ------ controllers/rentalController.js | 38 +- controllers/roleController.js | 319 +++++++++++++++ controllers/roomController.js | 12 +- controllers/statisticsController.js | 215 ++++------ controllers/userController.js | 587 ++++++++++++++++++++++++++++ controllers/waterBillController.js | 12 +- middleware/auth.js | 99 +++++ models/Apartment.js | 24 +- models/ElectricityBill.js | 10 + models/LoginLog.js | 58 +++ models/Menu.js | 88 +++++ models/OperationLog.js | 83 ++++ models/Region.js | 44 --- models/Rental.js | 10 + models/Role.js | 60 +++ models/RoleMenu.js | 26 ++ models/Room.js | 10 + models/User.js | 64 +++ models/WaterBill.js | 10 + models/index.js | 28 +- package-lock.json | 98 +++++ package.json | 2 + routes/auth.js | 18 + routes/log.js | 21 + routes/menu.js | 36 ++ routes/region.js | 13 - routes/role.js | 27 ++ routes/statistics.js | 3 +- routes/user.js | 32 ++ utils/logger.js | 157 ++++++++ 36 files changed, 3021 insertions(+), 406 deletions(-) create mode 100644 controllers/authController.js create mode 100644 controllers/logController.js create mode 100644 controllers/menuController.js delete mode 100644 controllers/regionController.js create mode 100644 controllers/roleController.js create mode 100644 controllers/userController.js create mode 100644 middleware/auth.js create mode 100644 models/LoginLog.js create mode 100644 models/Menu.js create mode 100644 models/OperationLog.js delete mode 100644 models/Region.js create mode 100644 models/Role.js create mode 100644 models/RoleMenu.js create mode 100644 models/User.js create mode 100644 routes/auth.js create mode 100644 routes/log.js create mode 100644 routes/menu.js delete mode 100644 routes/region.js create mode 100644 routes/role.js create mode 100644 routes/user.js create mode 100644 utils/logger.js diff --git a/app.js b/app.js index 684a2e9..ec755bb 100644 --- a/app.js +++ b/app.js @@ -1,15 +1,24 @@ const express = require('express'); const cors = require('cors'); const sequelize = require('./config/db'); +const { authMiddleware } = require('./middleware/auth'); +const { operationLogMiddleware } = require('./utils/logger'); + +// 加载模型关联关系 +require('./models'); // 导入路由 -const regionRoutes = require('./routes/region'); const apartmentRoutes = require('./routes/apartment'); const roomRoutes = require('./routes/room'); const rentalRoutes = require('./routes/rental'); const statisticsRoutes = require('./routes/statistics'); const waterBillRoutes = require('./routes/waterBill'); const electricityBillRoutes = require('./routes/electricityBill'); +const authRoutes = require('./routes/auth'); +const userRoutes = require('./routes/user'); +const roleRoutes = require('./routes/role'); +const menuRoutes = require('./routes/menu'); +const logRoutes = require('./routes/log'); const app = express(); const PORT = process.env.PORT || 3000; @@ -19,14 +28,20 @@ app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); -// 路由 -app.use('/api/regions', regionRoutes); -app.use('/api/apartments', apartmentRoutes); -app.use('/api/rooms', roomRoutes); -app.use('/api/rentals', rentalRoutes); -app.use('/api/statistics', statisticsRoutes); -app.use('/api/water-bills', waterBillRoutes); -app.use('/api/electricity-bills', electricityBillRoutes); +// 公开路由(不需要认证) +app.use('/api/auth', authRoutes); + +// 需要认证的路由 +app.use('/api/apartments', authMiddleware, operationLogMiddleware({ module: '公寓管理' }), apartmentRoutes); +app.use('/api/rooms', authMiddleware, operationLogMiddleware({ module: '房间管理' }), roomRoutes); +app.use('/api/rentals', authMiddleware, operationLogMiddleware({ module: '租房管理' }), rentalRoutes); +app.use('/api/statistics', authMiddleware, operationLogMiddleware({ module: '统计分析' }), statisticsRoutes); +app.use('/api/water-bills', authMiddleware, operationLogMiddleware({ module: '水费管理' }), waterBillRoutes); +app.use('/api/electricity-bills', authMiddleware, operationLogMiddleware({ module: '电费管理' }), electricityBillRoutes); +app.use('/api/users', authMiddleware, operationLogMiddleware({ module: '用户管理' }), userRoutes); +app.use('/api/roles', authMiddleware, operationLogMiddleware({ module: '角色管理' }), roleRoutes); +app.use('/api/menus', authMiddleware, operationLogMiddleware({ module: '菜单管理' }), menuRoutes); +app.use('/api/logs', authMiddleware, logRoutes); // 测试接口 app.get('/', (req, res) => { diff --git a/controllers/apartmentController.js b/controllers/apartmentController.js index d1245e5..0126d78 100644 --- a/controllers/apartmentController.js +++ b/controllers/apartmentController.js @@ -1,4 +1,4 @@ -const { Apartment, Region } = require('../models'); +const { Apartment } = require('../models'); const { Op } = require('sequelize'); // 格式化时间(考虑时区,转换为北京时间) @@ -11,22 +11,11 @@ const formatDate = (date) => { // 格式化公寓数据 const formatApartmentData = (apartment) => { - const formattedApartment = { + return { ...apartment.toJSON(), createTime: formatDate(apartment.createTime), updateTime: formatDate(apartment.updateTime) }; - - // 格式化关联数据 - if (formattedApartment.Region) { - formattedApartment.Region = { - ...formattedApartment.Region, - createTime: formatDate(formattedApartment.Region.createTime), - updateTime: formatDate(formattedApartment.Region.updateTime) - }; - } - - return formattedApartment; }; // 获取所有公寓(支持搜索和分页) @@ -51,7 +40,6 @@ const getAllApartments = async (req, res) => { // 查询公寓数据 const { count, rows } = await Apartment.findAndCountAll({ where, - include: [Region], limit: parseInt(pageSize), offset: parseInt(offset) }); @@ -76,8 +64,7 @@ const getApartmentById = async (req, res) => { try { const { id } = req.params; const apartment = await Apartment.findOne({ - where: { id, isDeleted: 0 }, - include: [Region] + where: { id, isDeleted: 0 } }); if (!apartment) { return res.status(404).json({ error: '公寓不存在' }); @@ -92,8 +79,13 @@ const getApartmentById = async (req, res) => { // 创建公寓 const createApartment = async (req, res) => { try { - const { regionId, name, address } = req.body; - const apartment = await Apartment.create({ regionId, name, address }); + const { name, address } = req.body; + const apartment = await Apartment.create({ + name, + address, + createBy: req.user.id, + updateBy: req.user.id + }); res.status(201).json(apartment); } catch (error) { res.status(500).json({ error: error.message }); @@ -104,14 +96,18 @@ const createApartment = async (req, res) => { const updateApartment = async (req, res) => { try { const { id } = req.params; - const { regionId, name, address } = req.body; + const { name, address } = req.body; const apartment = await Apartment.findOne({ where: { id, isDeleted: 0 } }); if (!apartment) { return res.status(404).json({ error: '公寓不存在' }); } - await apartment.update({ regionId, name, address }); + await apartment.update({ + name, + address, + updateBy: req.user.id + }); res.status(200).json(apartment); } catch (error) { res.status(500).json({ error: error.message }); @@ -128,7 +124,10 @@ const deleteApartment = async (req, res) => { if (!apartment) { return res.status(404).json({ error: '公寓不存在' }); } - await apartment.update({ isDeleted: 1 }); + await apartment.update({ + isDeleted: 1, + updateBy: req.user.id + }); res.status(200).json({ message: '公寓删除成功' }); } catch (error) { res.status(500).json({ error: error.message }); @@ -153,8 +152,7 @@ const listApartments = async (req, res) => { // 查询公寓数据 const apartments = await Apartment.findAll({ - where, - include: [Region] + where }); // 格式化数据 diff --git a/controllers/authController.js b/controllers/authController.js new file mode 100644 index 0000000..25fae0b --- /dev/null +++ b/controllers/authController.js @@ -0,0 +1,296 @@ +const bcrypt = require('bcryptjs'); +const User = require('../models/User'); +const Role = require('../models/Role'); +const Menu = require('../models/Menu'); +const { generateToken } = require('../middleware/auth'); +const { logLogin, getClientIp } = require('../utils/logger'); + +// 登录 +exports.login = async (req, res) => { + const clientIp = getClientIp(req); + const userAgent = req.headers['user-agent']; + let username = req.body.username; + + try { + const { password } = req.body; + + // 参数验证 + if (!username || !password) { + await logLogin({ + username, + loginType: 'login', + ip: clientIp, + userAgent, + status: 'fail', + message: '用户名和密码不能为空' + }); + return res.status(400).json({ + code: 400, + message: '用户名和密码不能为空' + }); + } + + // 查找用户 + const user = await User.findOne({ + where: { username }, + include: [{ + model: Role, + as: 'role' + }] + }); + + if (!user) { + await logLogin({ + username, + loginType: 'login', + ip: clientIp, + userAgent, + status: 'fail', + message: '用户名或密码错误' + }); + return res.status(401).json({ + code: 401, + message: '用户名或密码错误' + }); + } + + // 检查用户状态 + if (user.status === 'disabled') { + await logLogin({ + userId: user.id, + username: user.username, + loginType: 'login', + ip: clientIp, + userAgent, + status: 'fail', + message: '账号已被禁用' + }); + return res.status(401).json({ + code: 401, + message: '账号已被禁用' + }); + } + + // 验证密码 + const isPasswordValid = await bcrypt.compare(password, user.password); + if (!isPasswordValid) { + await logLogin({ + userId: user.id, + username: user.username, + loginType: 'login', + ip: clientIp, + userAgent, + status: 'fail', + message: '用户名或密码错误' + }); + return res.status(401).json({ + code: 401, + message: '用户名或密码错误' + }); + } + + // 生成 Token + const token = generateToken(user); + + // 获取用户菜单权限 + let menus = []; + // 超级管理员(isSuperAdmin为1或角色code为admin)返回所有菜单 + if (user.isSuperAdmin === 1 || (user.role && user.role.code === 'admin')) { + // 超级管理员返回所有菜单 + const allMenus = await Menu.findAll({ + where: { + isDeleted: false, + status: 'active', + visible: 'show' + }, + order: [['sort', 'ASC']] + }); + menus = buildMenuTree(allMenus); + } else if (user.roleId) { + // 普通用户返回角色分配的菜单 + const roleData = await Role.findByPk(user.roleId, { + include: [{ + model: Menu, + as: 'menus', + where: { + isDeleted: false, + status: 'active', + visible: 'show' + }, + through: { attributes: [] } + }] + }); + + if (roleData && roleData.menus) { + const sortedMenus = roleData.menus.sort((a, b) => a.sort - b.sort); + menus = buildMenuTree(sortedMenus); + } + } + + // 记录登录成功日志 + await logLogin({ + userId: user.id, + username: user.username, + loginType: 'login', + ip: clientIp, + userAgent, + status: 'success', + message: '登录成功' + }); + + // 返回用户信息和 Token + res.json({ + code: 200, + message: '登录成功', + data: { + token, + userInfo: { + id: user.id, + username: user.username, + nickname: user.nickname, + role: user.role + }, + menus + } + }); + } catch (error) { + console.error('登录错误:', error); + await logLogin({ + username, + loginType: 'login', + ip: clientIp, + userAgent, + status: 'fail', + message: error.message + }); + res.status(500).json({ + code: 500, + message: '登录失败', + error: error.message + }); + } +}; + +// 登出 +exports.logout = async (req, res) => { + try { + // 记录登出日志 + const user = req.user || {}; + await logLogin({ + userId: user.id, + username: user.username, + loginType: 'logout', + ip: getClientIp(req), + userAgent: req.headers['user-agent'], + status: 'success', + message: '登出成功' + }); + + res.json({ + code: 200, + message: '登出成功' + }); + } catch (error) { + console.error('登出错误:', error); + res.json({ + code: 200, + message: '登出成功' + }); + } +}; + +// 构建菜单树 +function buildMenuTree(menus, parentId = null) { + return menus + .filter(menu => menu.parentId === parentId) + .map(menu => ({ + id: menu.id, + name: menu.name, + code: menu.code, + type: menu.type, + path: menu.path, + component: menu.component, + icon: menu.icon, + sort: menu.sort, + visible: menu.visible, + children: buildMenuTree(menus, menu.id) + })); +} + +// 获取当前用户信息 +exports.getCurrentUser = async (req, res) => { + try { + // req.user 由 authMiddleware 附加 + res.json({ + code: 200, + message: '获取成功', + data: req.user + }); + } catch (error) { + console.error('获取用户信息错误:', error); + res.status(500).json({ + code: 500, + message: '获取用户信息失败', + error: error.message + }); + } +}; + +// 修改密码 +exports.changePassword = async (req, res) => { + try { + const { oldPassword, newPassword } = req.body; + const userId = req.user.id; + + // 参数验证 + if (!oldPassword || !newPassword) { + return res.status(400).json({ + code: 400, + message: '原密码和新密码不能为空' + }); + } + + if (newPassword.length < 6) { + return res.status(400).json({ + code: 400, + message: '新密码长度不能少于6位' + }); + } + + // 查找用户 + const user = await User.findByPk(userId); + if (!user) { + return res.status(404).json({ + code: 404, + message: '用户不存在' + }); + } + + // 验证原密码 + const isPasswordValid = await bcrypt.compare(oldPassword, user.password); + if (!isPasswordValid) { + return res.status(400).json({ + code: 400, + message: '原密码错误' + }); + } + + // 加密新密码 + const hashedPassword = await bcrypt.hash(newPassword, 10); + + // 更新密码 + await user.update({ password: hashedPassword }); + + res.json({ + code: 200, + message: '密码修改成功' + }); + } catch (error) { + console.error('修改密码错误:', error); + res.status(500).json({ + code: 500, + message: '修改密码失败', + error: error.message + }); + } +}; diff --git a/controllers/logController.js b/controllers/logController.js new file mode 100644 index 0000000..d84a992 --- /dev/null +++ b/controllers/logController.js @@ -0,0 +1,162 @@ +const { Op } = require('sequelize'); +const OperationLog = require('../models/OperationLog'); +const LoginLog = require('../models/LoginLog'); + +// 获取操作日志列表 +exports.getOperationLogs = async (req, res) => { + try { + const { page = 1, pageSize = 20, username, module, action, status, startTime, endTime } = req.query; + + // 构建查询条件 + const where = {}; + if (username) { + where.username = { [Op.like]: `%${username}%` }; + } + if (module) { + where.module = module; + } + if (action) { + where.action = action; + } + if (status) { + where.status = status; + } + if (startTime && endTime) { + where.createTime = { + [Op.between]: [new Date(startTime), new Date(endTime)] + }; + } + + // 查询日志列表 + const { count, rows } = await OperationLog.findAndCountAll({ + where, + order: [['createTime', 'DESC']], + offset: (page - 1) * pageSize, + limit: parseInt(pageSize) + }); + + res.json({ + code: 200, + message: '获取成功', + data: { + list: rows, + total: count, + page: parseInt(page), + pageSize: parseInt(pageSize) + } + }); + } catch (error) { + console.error('获取操作日志错误:', error); + res.status(500).json({ + code: 500, + message: '获取操作日志失败', + error: error.message + }); + } +}; + +// 获取登录日志列表 +exports.getLoginLogs = async (req, res) => { + try { + const { page = 1, pageSize = 20, username, loginType, status, startTime, endTime } = req.query; + + // 构建查询条件 + const where = {}; + if (username) { + where.username = { [Op.like]: `%${username}%` }; + } + if (loginType) { + where.loginType = loginType; + } + if (status) { + where.status = status; + } + if (startTime && endTime) { + where.createTime = { + [Op.between]: [new Date(startTime), new Date(endTime)] + }; + } + + // 查询日志列表 + const { count, rows } = await LoginLog.findAndCountAll({ + where, + order: [['createTime', 'DESC']], + offset: (page - 1) * pageSize, + limit: parseInt(pageSize) + }); + + res.json({ + code: 200, + message: '获取成功', + data: { + list: rows, + total: count, + page: parseInt(page), + pageSize: parseInt(pageSize) + } + }); + } catch (error) { + console.error('获取登录日志错误:', error); + res.status(500).json({ + code: 500, + message: '获取登录日志失败', + error: error.message + }); + } +}; + +// 清空操作日志 +exports.clearOperationLogs = async (req, res) => { + try { + const { startTime, endTime } = req.body; + const where = {}; + + if (startTime && endTime) { + where.createTime = { + [Op.between]: [new Date(startTime), new Date(endTime)] + }; + } + + await OperationLog.destroy({ where }); + + res.json({ + code: 200, + message: '清空成功' + }); + } catch (error) { + console.error('清空操作日志错误:', error); + res.status(500).json({ + code: 500, + message: '清空操作日志失败', + error: error.message + }); + } +}; + +// 清空登录日志 +exports.clearLoginLogs = async (req, res) => { + try { + const { startTime, endTime } = req.body; + const where = {}; + + if (startTime && endTime) { + where.createTime = { + [Op.between]: [new Date(startTime), new Date(endTime)] + }; + } + + await LoginLog.destroy({ where }); + + res.json({ + code: 200, + message: '清空成功' + }); + } catch (error) { + console.error('清空登录日志错误:', error); + res.status(500).json({ + code: 500, + message: '清空登录日志失败', + error: error.message + }); + } +}; diff --git a/controllers/menuController.js b/controllers/menuController.js new file mode 100644 index 0000000..2562588 --- /dev/null +++ b/controllers/menuController.js @@ -0,0 +1,563 @@ +const { Op } = require('sequelize'); +const Menu = require('../models/Menu'); +const RoleMenu = require('../models/RoleMenu'); +const { logOperation } = require('../utils/logger'); + +// 获取菜单树 +exports.getMenuTree = async (req, res) => { + try { + const { type, status } = req.query; + + const where = { isDeleted: false }; + if (type) { + where.type = type; + } + if (status) { + where.status = status; + } + + const menus = await Menu.findAll({ + where, + order: [['sort', 'ASC'], ['createdAt', 'ASC']] + }); + + const menuTree = buildTree(menus); + + res.json({ + code: 200, + message: '获取成功', + data: menuTree + }); + } catch (error) { + console.error('获取菜单树失败:', error); + res.status(500).json({ + code: 500, + message: '获取菜单树失败', + error: error.message + }); + } +}; + +// 获取菜单列表(平铺) +exports.getMenuList = async (req, res) => { + try { + const { page = 1, pageSize = 10, name, type, status } = req.query; + + const where = { isDeleted: false }; + if (name) { + where.name = { [Op.like]: `%${name}%` }; + } + if (type) { + where.type = type; + } + if (status) { + where.status = status; + } + + const { count, rows } = await Menu.findAndCountAll({ + where, + order: [['sort', 'ASC'], ['createdAt', 'ASC']], + offset: (page - 1) * pageSize, + limit: parseInt(pageSize) + }); + + res.json({ + code: 200, + message: '获取成功', + data: { + list: rows, + total: count, + page: parseInt(page), + pageSize: parseInt(pageSize) + } + }); + } catch (error) { + console.error('获取菜单列表失败:', error); + res.status(500).json({ + code: 500, + message: '获取菜单列表失败', + error: error.message + }); + } +}; + +// 获取菜单详情 +exports.getMenuById = async (req, res) => { + try { + const { id } = req.params; + + const menu = await Menu.findByPk(id, { + where: { isDeleted: false } + }); + + if (!menu) { + return res.status(404).json({ + code: 404, + message: '菜单不存在' + }); + } + + res.json({ + code: 200, + message: '获取成功', + data: menu + }); + } catch (error) { + console.error('获取菜单详情失败:', error); + res.status(500).json({ + code: 500, + message: '获取菜单详情失败', + error: error.message + }); + } +}; + +// 创建菜单 +exports.createMenu = async (req, res) => { + try { + const { parentId, name, code, type, path, component, icon, sort, visible, status } = req.body; + + if (!name || !code || !type) { + return res.status(400).json({ + code: 400, + message: '菜单名称、编码和类型不能为空' + }); + } + + if (type === 'menu' && !path) { + return res.status(400).json({ + code: 400, + message: '菜单类型必须填写路由路径' + }); + } + + const existingMenu = await Menu.findOne({ + where: { code, isDeleted: false } + }); + + if (existingMenu) { + return res.status(400).json({ + code: 400, + message: '菜单编码已存在' + }); + } + + if (parentId) { + const parentMenu = await Menu.findByPk(parentId, { + where: { isDeleted: false } + }); + if (!parentMenu) { + return res.status(400).json({ + code: 400, + message: '父菜单不存在' + }); + } + } + + const menu = await Menu.create({ + parentId: parentId || null, + name, + code, + type, + path: type === 'menu' ? path : null, + component, + icon, + sort: sort || 0, + visible: visible || 'show', + status: status || 'active', + createBy: req.user.id, + updateBy: req.user.id + }); + + await logOperation({ + userId: req.user.id, + username: req.user.username, + module: '菜单管理', + action: '创建', + description: `创建菜单: ${name}`, + method: req.method, + path: req.path, + ip: req.ip, + status: 'success' + }); + + res.json({ + code: 200, + message: '创建成功', + data: menu + }); + } catch (error) { + console.error('创建菜单失败:', error); + res.status(500).json({ + code: 500, + message: '创建菜单失败', + error: error.message + }); + } +}; + +// 更新菜单 +exports.updateMenu = async (req, res) => { + try { + const { id } = req.params; + const { parentId, name, code, type, path, component, icon, sort, visible, status } = req.body; + + const menu = await Menu.findByPk(id, { + where: { isDeleted: false } + }); + + if (!menu) { + return res.status(404).json({ + code: 404, + message: '菜单不存在' + }); + } + + if (code && code !== menu.code) { + const existingMenu = await Menu.findOne({ + where: { code, isDeleted: false, id: { [Op.ne]: id } } + }); + + if (existingMenu) { + return res.status(400).json({ + code: 400, + message: '菜单编码已存在' + }); + } + } + + if (parentId) { + if (parseInt(parentId) === parseInt(id)) { + return res.status(400).json({ + code: 400, + message: '不能将菜单设置为自己的子菜单' + }); + } + + const parentMenu = await Menu.findByPk(parentId, { + where: { isDeleted: false } + }); + if (!parentMenu) { + return res.status(400).json({ + code: 400, + message: '父菜单不存在' + }); + } + } + + const updateData = { + updateBy: req.user.id + }; + if (parentId !== undefined) updateData.parentId = parentId || null; + if (name !== undefined) updateData.name = name; + if (code !== undefined) updateData.code = code; + if (type !== undefined) updateData.type = type; + if (path !== undefined) updateData.path = path; + if (component !== undefined) updateData.component = component; + if (icon !== undefined) updateData.icon = icon; + if (sort !== undefined) updateData.sort = sort; + if (visible !== undefined) updateData.visible = visible; + if (status !== undefined) updateData.status = status; + + await menu.update(updateData); + + await logOperation({ + userId: req.user.id, + username: req.user.username, + module: '菜单管理', + action: '更新', + description: `更新菜单: ${menu.name}`, + method: req.method, + path: req.path, + ip: req.ip, + status: 'success' + }); + + res.json({ + code: 200, + message: '更新成功', + data: menu + }); + } catch (error) { + console.error('更新菜单失败:', error); + res.status(500).json({ + code: 500, + message: '更新菜单失败', + error: error.message + }); + } +}; + +// 删除菜单 +exports.deleteMenu = async (req, res) => { + try { + const { id } = req.params; + + const menu = await Menu.findByPk(id, { + where: { isDeleted: false } + }); + + if (!menu) { + return res.status(404).json({ + code: 404, + message: '菜单不存在' + }); + } + + const children = await Menu.findAll({ + where: { parentId: id, isDeleted: false } + }); + + if (children.length > 0) { + return res.status(400).json({ + code: 400, + message: '该菜单下有子菜单,无法删除' + }); + } + + await menu.update({ + isDeleted: true, + updateBy: req.user.id + }); + + await RoleMenu.destroy({ + where: { menuId: id } + }); + + await logOperation({ + userId: req.user.id, + username: req.user.username, + module: '菜单管理', + action: '删除', + description: `删除菜单: ${menu.name}`, + method: req.method, + path: req.path, + ip: req.ip, + status: 'success' + }); + + res.json({ + code: 200, + message: '删除成功' + }); + } catch (error) { + console.error('删除菜单失败:', error); + res.status(500).json({ + code: 500, + message: '删除菜单失败', + error: error.message + }); + } +}; + +// 获取角色的菜单权限 +exports.getRoleMenus = async (req, res) => { + try { + const { roleId } = req.params; + + const role = await Menu.findOne({ + where: { id: roleId, isDeleted: false } + }); + + if (!role) { + return res.status(404).json({ + code: 404, + message: '角色不存在' + }); + } + + const Role = require('../models/Role'); + const roleData = await Role.findByPk(roleId, { + include: [{ + model: Menu, + as: 'menus', + where: { isDeleted: false }, + through: { attributes: [] } + }] + }); + + const menus = roleData ? roleData.menus : []; + + res.json({ + code: 200, + message: '获取成功', + data: menus + }); + } catch (error) { + console.error('获取角色菜单失败:', error); + res.status(500).json({ + code: 500, + message: '获取角色菜单失败', + error: error.message + }); + } +}; + +// 分配菜单权限给角色 +exports.assignMenusToRole = async (req, res) => { + try { + const { roleId } = req.params; + const { menuIds } = req.body; + + if (!Array.isArray(menuIds)) { + return res.status(400).json({ + code: 400, + message: '菜单ID必须是数组' + }); + } + + const Role = require('../models/Role'); + const role = await Role.findByPk(roleId); + + if (!role) { + return res.status(404).json({ + code: 404, + message: '角色不存在' + }); + } + + await RoleMenu.destroy({ + where: { roleId } + }); + + if (menuIds.length > 0) { + const roleMenus = menuIds.map(menuId => ({ + roleId, + menuId + })); + await RoleMenu.bulkCreate(roleMenus); + } + + await logOperation({ + userId: req.user.id, + username: req.user.username, + module: '菜单管理', + action: '分配权限', + description: `为角色 ${role.name} 分配菜单权限`, + method: req.method, + path: req.path, + ip: req.ip, + status: 'success' + }); + + res.json({ + code: 200, + message: '分配成功' + }); + } catch (error) { + console.error('分配菜单权限失败:', error); + res.status(500).json({ + code: 500, + message: '分配菜单权限失败', + error: error.message + }); + } +}; + +// 获取当前用户的菜单权限 +exports.getUserMenus = async (req, res) => { + try { + const userId = req.user.id; + const roleId = req.user.roleId; + + // 获取用户角色 + const User = require('../models/User'); + const user = await User.findByPk(userId, { + include: [{ + model: require('../models/Role'), + as: 'role' + }] + }); + + // 超级管理员(isSuperAdmin为1或角色code为admin)返回所有菜单 + if (user.isSuperAdmin === 1 || (user.role && user.role.code === 'admin')) { + // 先获取所有可见的菜单 + const menuList = await Menu.findAll({ + where: { + isDeleted: false, + status: 'active', + visible: 'show' + }, + order: [['sort', 'ASC']] + }); + + // 再获取所有按钮 + const buttonList = await Menu.findAll({ + where: { + isDeleted: false, + status: 'active', + type: 'button' + }, + order: [['sort', 'ASC']] + }); + + // 合并菜单和按钮 + const allMenus = [...menuList, ...buttonList]; + const menuTree = buildTree(allMenus); + + return res.json({ + code: 200, + message: '获取成功', + data: menuTree + }); + } + + if (!user.role) { + return res.status(403).json({ + code: 403, + message: '用户没有分配角色' + }); + } + + // 普通用户返回角色分配的菜单 + const Role = require('../models/Role'); + const roleData = await Role.findByPk(roleId, { + include: [{ + model: Menu, + as: 'menus', + where: { + isDeleted: false, + status: 'active' + }, + through: { attributes: [] } + }] + }); + + if (!roleData || !roleData.menus) { + return res.json({ + code: 200, + message: '获取成功', + data: [] + }); + } + + // 按sort排序 + const menus = roleData.menus.sort((a, b) => a.sort - b.sort); + + // 构建菜单树 + const menuTree = buildTree(menus); + + res.json({ + code: 200, + message: '获取成功', + data: menuTree + }); + } catch (error) { + console.error('获取用户菜单失败:', error); + res.status(500).json({ + code: 500, + message: '获取用户菜单失败', + error: error.message + }); + } +}; + +// 构建菜单树 +function buildTree(menus, parentId = null) { + return menus + .filter(menu => menu.parentId === parentId) + .map(menu => ({ + ...menu.dataValues, + children: buildTree(menus, menu.id) + })); +} diff --git a/controllers/regionController.js b/controllers/regionController.js deleted file mode 100644 index 9cec70f..0000000 --- a/controllers/regionController.js +++ /dev/null @@ -1,125 +0,0 @@ -const { Region } = require('../models'); -const { Op } = require('sequelize'); - -// 格式化时间(考虑时区,转换为北京时间) -const formatDate = (date) => { - if (!date) return null; - // 创建一个新的Date对象,加上8小时的时区偏移 - const beijingDate = new Date(date.getTime() + 8 * 60 * 60 * 1000); - return beijingDate.toISOString().split('T')[0]; -}; - -// 格式化区域数据 -const formatRegionData = (region) => { - return { - ...region.toJSON(), - createTime: formatDate(region.createTime), - updateTime: formatDate(region.updateTime) - }; -}; - -// 获取所有区域 -const getAllRegions = async (req, res) => { - try { - const regions = await Region.findAll({ - where: { isDeleted: 0 } - }); - const formattedRegions = regions.map(formatRegionData); - res.status(200).json(formattedRegions); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}; - -// 获取单个区域 -const getRegionById = async (req, res) => { - try { - const { id } = req.params; - const region = await Region.findOne({ - where: { id, isDeleted: 0 } - }); - if (!region) { - return res.status(404).json({ error: '区域不存在' }); - } - const formattedRegion = formatRegionData(region); - res.status(200).json(formattedRegion); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}; - -// 创建区域 -const createRegion = async (req, res) => { - try { - const { name, description } = req.body; - const region = await Region.create({ name, description }); - res.status(201).json(region); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}; - -// 更新区域 -const updateRegion = async (req, res) => { - try { - const { id } = req.params; - const { name, description } = req.body; - const region = await Region.findOne({ - where: { id, isDeleted: 0 } - }); - if (!region) { - return res.status(404).json({ error: '区域不存在' }); - } - await region.update({ name, description }); - res.status(200).json(region); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}; - -// 删除区域(软删除) -const deleteRegion = async (req, res) => { - try { - const { id } = req.params; - const region = await Region.findOne({ - where: { id, isDeleted: 0 } - }); - if (!region) { - return res.status(404).json({ error: '区域不存在' }); - } - await region.update({ isDeleted: 1 }); - res.status(200).json({ message: '区域删除成功' }); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}; - -// 获取所有区域(不分页) -const listRegions = async (req, res) => { - try { - const { name } = req.query; - - // 构建查询条件 - const where = { isDeleted: 0 }; - if (name) { - where.name = { - [Op.like]: `%${name}%` - }; - } - - const regions = await Region.findAll({ where }); - const formattedRegions = regions.map(formatRegionData); - res.status(200).json(formattedRegions); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}; - -module.exports = { - getAllRegions, - listRegions, - getRegionById, - createRegion, - updateRegion, - deleteRegion -}; \ No newline at end of file diff --git a/controllers/rentalController.js b/controllers/rentalController.js index 6b6e17d..fd5062a 100644 --- a/controllers/rentalController.js +++ b/controllers/rentalController.js @@ -1,4 +1,4 @@ -const { Rental, Room, Apartment, Region } = require('../models'); +const { Rental, Room, Apartment } = require('../models'); const { Op } = require('sequelize'); // 格式化时间(考虑时区,转换为北京时间) @@ -122,13 +122,7 @@ const getAllRentals = async (req, res) => { include: [ { model: Apartment, - where: { isDeleted: 0 }, - include: [ - { - model: Region, - where: { isDeleted: 0 } - } - ] + where: { isDeleted: 0 } } ] } @@ -253,7 +247,9 @@ const createRental = async (req, res) => { endDate: body.endDate, rent: body.rent, deposit: deposit, - status: body.status || 'active' + status: body.status || 'active', + createBy: req.user.id, + updateBy: req.user.id }); console.log('租房记录:', rental); @@ -281,7 +277,16 @@ const updateRental = async (req, res) => { } // 处理押金,为空时设置为0 const updateDeposit = deposit || 0; - await rental.update({ roomId, tenantName, startDate, endDate, rent, deposit: updateDeposit, status }); + await rental.update({ + roomId, + tenantName, + startDate, + endDate, + rent, + deposit: updateDeposit, + status, + updateBy: req.user.id + }); res.status(200).json(rental); } catch (error) { res.status(500).json({ error: error.message }); @@ -298,7 +303,10 @@ const deleteRental = async (req, res) => { if (!rental) { return res.status(404).json({ error: '租房记录不存在' }); } - await rental.update({ isDeleted: 1 }); + await rental.update({ + isDeleted: 1, + updateBy: req.user.id + }); res.status(200).json({ message: '租房记录删除成功' }); } catch (error) { res.status(500).json({ error: error.message }); @@ -351,13 +359,7 @@ const listRentals = async (req, res) => { include: [ { model: Apartment, - where: { isDeleted: 0 }, - include: [ - { - model: Region, - where: { isDeleted: 0 } - } - ] + where: { isDeleted: 0 } } ] } diff --git a/controllers/roleController.js b/controllers/roleController.js new file mode 100644 index 0000000..3b9cf8e --- /dev/null +++ b/controllers/roleController.js @@ -0,0 +1,319 @@ +const { Op } = require('sequelize'); +const Role = require('../models/Role'); +const { logOperation } = require('../utils/logger'); + +// 获取角色列表 +exports.getRoles = async (req, res) => { + try { + const { page = 1, pageSize = 10, name, status } = req.query; + + const where = { isDeleted: 0 }; + if (name) where.name = { [Op.like]: `%${name}%` }; + if (status) where.status = status; + + const { count, rows: roles } = await Role.findAndCountAll({ + where, + limit: parseInt(pageSize), + offset: (parseInt(page) - 1) * parseInt(pageSize), + order: [['id', 'DESC']] + }); + + res.json({ + code: 200, + message: '获取角色列表成功', + data: { + list: roles, + total: count, + page: parseInt(page), + pageSize: parseInt(pageSize) + } + }); + + // 记录操作日志 + await logOperation({ + userId: req.user.id, + username: req.user.username, + module: '角色管理', + action: '查询', + description: '获取角色列表', + method: req.method, + path: req.path, + ip: req.ip, + status: 'success' + }); + + } catch (error) { + console.error('获取角色列表错误:', error); + res.status(500).json({ + code: 500, + message: '获取角色列表失败', + error: error.message + }); + } +}; + +// 获取角色详情 +exports.getRoleById = async (req, res) => { + try { + const { id } = req.params; + + const role = await Role.findByPk(id, { + where: { isDeleted: 0 } + }); + + if (!role) { + return res.status(404).json({ + code: 404, + message: '角色不存在' + }); + } + + res.json({ + code: 200, + message: '获取角色详情成功', + data: role + }); + + // 记录操作日志 + await logOperation({ + userId: req.user.id, + username: req.user.username, + module: '角色管理', + action: '查询', + description: `获取角色详情,ID: ${id}`, + method: req.method, + path: req.path, + ip: req.ip, + status: 'success' + }); + + } catch (error) { + console.error('获取角色详情错误:', error); + res.status(500).json({ + code: 500, + message: '获取角色详情失败', + error: error.message + }); + } +}; + +// 创建角色 +exports.createRole = async (req, res) => { + try { + const { name, code, description, permissions, status } = req.body; + + // 参数验证 + if (!name || !code) { + return res.status(400).json({ + code: 400, + message: '角色名称和编码不能为空' + }); + } + + // 检查角色编码是否已存在 + const existingRole = await Role.findOne({ + where: { code, isDeleted: 0 } + }); + + if (existingRole) { + return res.status(400).json({ + code: 400, + message: '角色编码已存在' + }); + } + + const role = await Role.create({ + name, + code, + description, + permissions, + status: status || 'active', + createBy: req.user.id, + updateBy: req.user.id + }); + + res.json({ + code: 200, + message: '创建角色成功', + data: role + }); + + // 记录操作日志 + await logOperation({ + userId: req.user.id, + username: req.user.username, + module: '角色管理', + action: '创建', + description: `创建角色: ${name}`, + method: req.method, + path: req.path, + ip: req.ip, + status: 'success' + }); + + } catch (error) { + console.error('创建角色错误:', error); + res.status(500).json({ + code: 500, + message: '创建角色失败', + error: error.message + }); + } +}; + +// 更新角色 +exports.updateRole = async (req, res) => { + try { + const { id } = req.params; + const { name, code, description, permissions, status } = req.body; + + // 查找角色 + const role = await Role.findByPk(id, { + where: { isDeleted: 0 } + }); + + if (!role) { + return res.status(404).json({ + code: 404, + message: '角色不存在' + }); + } + + // 检查角色编码是否已存在(排除当前角色) + if (code && code !== role.code) { + const existingRole = await Role.findOne({ + where: { code, isDeleted: 0, id: { [Op.ne]: id } } + }); + + if (existingRole) { + return res.status(400).json({ + code: 400, + message: '角色编码已存在' + }); + } + } + + await role.update({ + name: name || role.name, + code: code || role.code, + description: description !== undefined ? description : role.description, + permissions: permissions !== undefined ? permissions : role.permissions, + status: status || role.status, + updateBy: req.user.id + }); + + res.json({ + code: 200, + message: '更新角色成功', + data: role + }); + + // 记录操作日志 + await logOperation({ + userId: req.user.id, + username: req.user.username, + module: '角色管理', + action: '更新', + description: `更新角色: ${role.name}`, + method: req.method, + path: req.path, + ip: req.ip, + status: 'success' + }); + + } catch (error) { + console.error('更新角色错误:', error); + res.status(500).json({ + code: 500, + message: '更新角色失败', + error: error.message + }); + } +}; + +// 删除角色 +exports.deleteRole = async (req, res) => { + try { + const { id } = req.params; + + // 查找角色 + const role = await Role.findByPk(id, { + where: { isDeleted: 0 } + }); + + if (!role) { + return res.status(404).json({ + code: 404, + message: '角色不存在' + }); + } + + // 检查是否有用户使用此角色 + const userCount = await role.countUsers({ + where: { isDeleted: 0 } + }); + + if (userCount > 0) { + return res.status(400).json({ + code: 400, + message: '该角色下还有用户,无法删除' + }); + } + + // 软删除 + await role.update({ + isDeleted: 1, + updateBy: req.user.id + }); + + res.json({ + code: 200, + message: '删除角色成功' + }); + + // 记录操作日志 + await logOperation({ + userId: req.user.id, + username: req.user.username, + module: '角色管理', + action: '删除', + description: `删除角色: ${role.name}`, + method: req.method, + path: req.path, + ip: req.ip, + status: 'success' + }); + + } catch (error) { + console.error('删除角色错误:', error); + res.status(500).json({ + code: 500, + message: '删除角色失败', + error: error.message + }); + } +}; + +// 获取所有角色(用于下拉选择) +exports.getAllRoles = async (req, res) => { + try { + const roles = await Role.findAll({ + where: { isDeleted: 0, status: 'active' }, + attributes: ['id', 'name', 'code'] + }); + + res.json({ + code: 200, + message: '获取角色列表成功', + data: roles + }); + + } catch (error) { + console.error('获取角色列表错误:', error); + res.status(500).json({ + code: 500, + message: '获取角色列表失败', + error: error.message + }); + } +}; diff --git a/controllers/roomController.js b/controllers/roomController.js index 6a0df53..1c1dad3 100644 --- a/controllers/roomController.js +++ b/controllers/roomController.js @@ -239,7 +239,9 @@ const createRoom = async (req, res) => { yearlyPrice: yearlyPrice === '' ? null : yearlyPrice, status, subStatus, - otherStatus + otherStatus, + createBy: req.user.id, + updateBy: req.user.id }; const room = await Room.create(processedData); res.status(201).json(room); @@ -268,7 +270,8 @@ const updateRoom = async (req, res) => { yearlyPrice: yearlyPrice === '' ? null : yearlyPrice, status, subStatus, - otherStatus + otherStatus, + updateBy: req.user.id }; await room.update(processedData); res.status(200).json(room); @@ -287,7 +290,10 @@ const deleteRoom = async (req, res) => { if (!room) { return res.status(404).json({ error: '房间不存在' }); } - await room.update({ isDeleted: 1 }); + await room.update({ + isDeleted: 1, + updateBy: req.user.id + }); res.status(200).json({ message: '房间删除成功' }); } catch (error) { res.status(500).json({ error: error.message }); diff --git a/controllers/statisticsController.js b/controllers/statisticsController.js index d5ad0f8..e41405c 100644 --- a/controllers/statisticsController.js +++ b/controllers/statisticsController.js @@ -1,4 +1,4 @@ -const { Room, Rental, Apartment, Region } = require('../models'); +const { Room, Rental, Apartment } = require('../models'); const { Op } = require('sequelize'); // 租金统计 @@ -111,156 +111,14 @@ const getRoomStatusStatistics = async (req, res) => { } }; -// 区域房屋统计 -const getRegionHouseStatistics = async (req, res) => { - try { - const regions = await Region.findAll({ - where: { isDeleted: 0 }, - include: [ - { - model: Apartment, - where: { isDeleted: 0 }, - include: [{ - model: Room, - where: { isDeleted: 0 } - }] - } - ] - }); - - const regionHouseStatistics = regions.map(region => { - let empty = 0; - let reserved = 0; - let rented = 0; - let soon_expire = 0; - let expired = 0; - let cleaning = 0; - let maintenance = 0; - - region.Apartments.forEach(apartment => { - apartment.Rooms.forEach(room => { - if (room.status === 'empty') { - empty++; - } else if (room.status === 'reserved') { - reserved++; - } else if (room.status === 'rented') { - rented++; - // 统计附属状态 - if (room.subStatus === 'soon_expire') { - soon_expire++; - } else if (room.subStatus === 'expired') { - expired++; - } - // 统计其他状态 - if (room.otherStatus === 'cleaning') { - cleaning++; - } else if (room.otherStatus === 'maintenance') { - maintenance++; - } - } - }); - }); - - return { - region: region.name, - empty, - reserved, - rented, - soon_expire, - expired, - cleaning, - maintenance, - total: empty + reserved + rented - }; - }); - - res.status(200).json(regionHouseStatistics); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}; - -// 区域公寓房间状态分布 -const getRegionApartmentHouseStatistics = async (req, res) => { - try { - const regions = await Region.findAll({ - where: { isDeleted: 0 }, - include: [ - { - model: Apartment, - where: { isDeleted: 0 }, - include: [{ - model: Room, - where: { isDeleted: 0 } - }] - } - ] - }); - - const apartmentHouseStatistics = []; - - regions.forEach(region => { - region.Apartments.forEach(apartment => { - let empty = 0; - let reserved = 0; - let rented = 0; - let soon_expire = 0; - let expired = 0; - let cleaning = 0; - let maintenance = 0; - - apartment.Rooms.forEach(room => { - if (room.status === 'empty') { - empty++; - } else if (room.status === 'reserved') { - reserved++; - } else if (room.status === 'rented') { - rented++; - // 统计附属状态 - if (room.subStatus === 'soon_expire') { - soon_expire++; - } else if (room.subStatus === 'expired') { - expired++; - } - // 统计其他状态 - if (room.otherStatus === 'cleaning') { - cleaning++; - } else if (room.otherStatus === 'maintenance') { - maintenance++; - } - } - }); - - apartmentHouseStatistics.push({ - region: region.name, - apartment: apartment.name, - empty, - reserved, - rented, - soon_expire, - expired, - cleaning, - maintenance, - total: empty + reserved + rented - }); - }); - }); - - res.status(200).json(apartmentHouseStatistics); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}; - // Dashboard统计数据 const getDashboardStatistics = async (req, res) => { try { // 导入所有需要的模型 - const { Region, Apartment, Room, WaterBill, Rental } = require('../models'); + const { Apartment, Room, WaterBill, Rental } = require('../models'); // 并行查询所有统计数据 - const [regionCount, apartmentCount, roomCount, emptyRoomCount, reservedRoomCount, rentedRoomCount, soonExpireRoomCount, expiredRoomCount, collectedRentAmount, collectedWaterAmount] = await Promise.all([ - Region.count({ where: { isDeleted: 0 } }), + const [apartmentCount, roomCount, emptyRoomCount, reservedRoomCount, rentedRoomCount, soonExpireRoomCount, expiredRoomCount, collectedRentAmount, collectedWaterAmount] = await Promise.all([ Apartment.count({ where: { isDeleted: 0 } }), Room.count({ where: { isDeleted: 0 } }), Room.count({ where: { status: 'empty', isDeleted: 0 } }), @@ -276,7 +134,6 @@ const getDashboardStatistics = async (req, res) => { // 构造响应数据 const dashboardStatistics = { - regionCount, apartmentCount, roomCount, emptyRoomCount, @@ -295,10 +152,70 @@ const getDashboardStatistics = async (req, res) => { } }; +// 公寓房间状态分布统计 +const getApartmentRoomStatusStatistics = async (req, res) => { + try { + // 获取所有公寓(排除已删除的) + const apartments = await Apartment.findAll({ where: { isDeleted: 0 } }); + + // 获取所有房间(排除已删除的) + const rooms = await Room.findAll({ where: { isDeleted: 0 } }); + + // 构建公寓房间状态分布数据 + const apartmentRoomStatusStatistics = apartments.map(apartment => { + const apartmentRooms = rooms.filter(room => room.apartmentId === apartment.id); + + let empty = 0; + let reserved = 0; + let rented = 0; + let soon_expire = 0; + let expired = 0; + let cleaning = 0; + let maintenance = 0; + + apartmentRooms.forEach(room => { + if (room.status === 'empty') { + empty++; + } else if (room.status === 'reserved') { + reserved++; + } else if (room.status === 'rented') { + rented++; + if (room.subStatus === 'soon_expire') { + soon_expire++; + } else if (room.subStatus === 'expired') { + expired++; + } + if (room.otherStatus === 'cleaning') { + cleaning++; + } else if (room.otherStatus === 'maintenance') { + maintenance++; + } + } + }); + + return { + apartment: apartment.name, + empty, + reserved, + rented, + soon_expire, + expired, + cleaning, + maintenance, + total: empty + reserved + rented + }; + }); + + res.status(200).json(apartmentRoomStatusStatistics); + } catch (error) { + console.error('获取公寓房间状态分布数据时出错:', error); + res.status(500).json({ error: error.message }); + } +}; + module.exports = { getRentStatistics, getRoomStatusStatistics, - getRegionHouseStatistics, - getRegionApartmentHouseStatistics, - getDashboardStatistics + getDashboardStatistics, + getApartmentRoomStatusStatistics }; \ No newline at end of file diff --git a/controllers/userController.js b/controllers/userController.js new file mode 100644 index 0000000..c572976 --- /dev/null +++ b/controllers/userController.js @@ -0,0 +1,587 @@ +const bcrypt = require('bcryptjs'); +const { Op } = require('sequelize'); +const User = require('../models/User'); +const Role = require('../models/Role'); +const { logOperation } = require('../utils/logger'); + +// 获取用户列表 +exports.getUserList = async (req, res) => { + try { + const { page = 1, pageSize = 10, username, roleId, status } = req.query; + + // 构建查询条件 + const where = { isDeleted: 0 }; + if (username) { + where.username = { [Op.like]: `%${username}%` }; + } + if (roleId) { + where.roleId = roleId; + } + if (status) { + where.status = status; + } + + // 查询用户列表 + const { count, rows } = await User.findAndCountAll({ + where, + attributes: ['id', 'username', 'nickname', 'roleId', 'status', 'createdAt', 'updatedAt'], + include: [{ + model: Role, + as: 'role', + attributes: ['id', 'name', 'code'] + }], + order: [['createdAt', 'DESC']], + offset: (page - 1) * pageSize, + limit: parseInt(pageSize) + }); + + res.json({ + code: 200, + message: '获取成功', + data: { + list: rows, + total: count, + page: parseInt(page), + pageSize: parseInt(pageSize) + } + }); + } catch (error) { + console.error('获取用户列表错误:', error); + res.status(500).json({ + code: 500, + message: '获取用户列表失败', + error: error.message + }); + } +}; + +// 获取用户详情 +exports.getUserById = async (req, res) => { + try { + const { id } = req.params; + + const user = await User.findByPk(id, { + where: { isDeleted: 0 }, + attributes: ['id', 'username', 'nickname', 'roleId', 'status', 'createdAt', 'updatedAt'], + include: [{ + model: Role, + as: 'role', + attributes: ['id', 'name', 'code'] + }] + }); + + if (!user) { + return res.status(404).json({ + code: 404, + message: '用户不存在' + }); + } + + res.json({ + code: 200, + message: '获取成功', + data: user + }); + + // 记录操作日志 + await logOperation({ + userId: req.user.id, + username: req.user.username, + module: '用户管理', + action: '查询', + description: `获取用户详情,ID: ${id}`, + method: req.method, + path: req.path, + ip: req.ip, + status: 'success' + }); + } catch (error) { + console.error('获取用户详情错误:', error); + res.status(500).json({ + code: 500, + message: '获取用户详情失败', + error: error.message + }); + } +}; + +// 创建用户 +exports.createUser = async (req, res) => { + try { + const { username, password, nickname, roleId } = req.body; + + // 参数验证 + if (!username || !password) { + return res.status(400).json({ + code: 400, + message: '用户名和密码不能为空' + }); + } + + if (username.length < 3 || username.length > 20) { + return res.status(400).json({ + code: 400, + message: '用户名长度应在3-20个字符之间' + }); + } + + if (password.length < 6 || password.length > 20) { + return res.status(400).json({ + code: 400, + message: '密码长度应在6-20个字符之间' + }); + } + + if (!roleId) { + return res.status(400).json({ + code: 400, + message: '角色不能为空' + }); + } + + // 检查角色是否存在 + const role = await Role.findByPk(roleId, { + where: { isDeleted: 0, status: 'active' } + }); + if (!role) { + return res.status(400).json({ + code: 400, + message: '角色不存在或已禁用' + }); + } + + // 检查用户名是否已存在 + const existingUser = await User.findOne({ where: { username, isDeleted: 0 } }); + if (existingUser) { + return res.status(400).json({ + code: 400, + message: '用户名已存在' + }); + } + + // 加密密码 + const hashedPassword = await bcrypt.hash(password, 10); + + // 创建用户 + const user = await User.create({ + username, + password: hashedPassword, + nickname: nickname || null, + roleId, + createBy: req.user.id, + updateBy: req.user.id + }); + + res.json({ + code: 200, + message: '创建成功', + data: { + id: user.id, + username: user.username, + nickname: user.nickname, + roleId: user.roleId, + createdAt: user.createdAt + } + }); + + // 记录操作日志 + await logOperation({ + userId: req.user.id, + username: req.user.username, + module: '用户管理', + action: '创建', + description: `创建用户: ${username}`, + method: req.method, + path: req.path, + ip: req.ip, + status: 'success' + }); + } catch (error) { + console.error('创建用户错误:', error); + res.status(500).json({ + code: 500, + message: '创建用户失败', + error: error.message + }); + } +}; + +// 更新用户 +exports.updateUser = async (req, res) => { + try { + const { id } = req.params; + const { nickname, roleId, status } = req.body; + + // 查找用户 + const user = await User.findByPk(id, { + where: { isDeleted: 0 } + }); + if (!user) { + return res.status(404).json({ + code: 404, + message: '用户不存在' + }); + } + + // 检查角色是否存在 + if (roleId) { + const role = await Role.findByPk(roleId, { + where: { isDeleted: 0, status: 'active' } + }); + if (!role) { + return res.status(400).json({ + code: 400, + message: '角色不存在或已禁用' + }); + } + } + + // 构建更新数据 + const updateData = { + updateBy: req.user.id + }; + if (nickname !== undefined) updateData.nickname = nickname || null; + if (roleId !== undefined) updateData.roleId = roleId; + if (status !== undefined) updateData.status = status; + + // 更新用户 + await user.update(updateData); + + res.json({ + code: 200, + message: '更新成功', + data: { + id: user.id, + username: user.username, + nickname: user.nickname, + roleId: user.roleId, + status: user.status, + updatedAt: user.updatedAt + } + }); + + // 记录操作日志 + await logOperation({ + userId: req.user.id, + username: req.user.username, + module: '用户管理', + action: '更新', + description: `更新用户: ${user.username}`, + method: req.method, + path: req.path, + ip: req.ip, + status: 'success' + }); + } catch (error) { + console.error('更新用户错误:', error); + res.status(500).json({ + code: 500, + message: '更新用户失败', + error: error.message + }); + } +}; + +// 删除用户 +exports.deleteUser = async (req, res) => { + try { + const { id } = req.params; + + // 不能删除自己 + if (parseInt(id) === req.user.id) { + return res.status(400).json({ + code: 400, + message: '不能删除自己的账号' + }); + } + + // 查找用户 + const user = await User.findByPk(id, { + where: { isDeleted: 0 } + }); + if (!user) { + return res.status(404).json({ + code: 404, + message: '用户不存在' + }); + } + + // 软删除 + await user.update({ + isDeleted: 1, + updateBy: req.user.id + }); + + res.json({ + code: 200, + message: '删除成功' + }); + + // 记录操作日志 + await logOperation({ + userId: req.user.id, + username: req.user.username, + module: '用户管理', + action: '删除', + description: `删除用户: ${user.username}`, + method: req.method, + path: req.path, + ip: req.ip, + status: 'success' + }); + } catch (error) { + console.error('删除用户错误:', error); + res.status(500).json({ + code: 500, + message: '删除用户失败', + error: error.message + }); + } +}; + +// 重置用户密码 +exports.resetUserPassword = async (req, res) => { + try { + const { id } = req.params; + const defaultPassword = '123456'; + + // 查找用户 + const user = await User.findByPk(id, { + where: { isDeleted: 0 } + }); + if (!user) { + return res.status(404).json({ + code: 404, + message: '用户不存在' + }); + } + + // 加密默认密码 + const hashedPassword = await bcrypt.hash(defaultPassword, 10); + + // 更新密码 + await user.update({ + password: hashedPassword, + updateBy: req.user.id + }); + + res.json({ + code: 200, + message: '密码重置成功', + data: { + defaultPassword + } + }); + + // 记录操作日志 + await logOperation({ + userId: req.user.id, + username: req.user.username, + module: '用户管理', + action: '重置密码', + description: `重置用户密码: ${user.username}`, + method: req.method, + path: req.path, + ip: req.ip, + status: 'success' + }); + } catch (error) { + console.error('重置密码错误:', error); + res.status(500).json({ + code: 500, + message: '重置密码失败', + error: error.message + }); + } +}; + +// 获取用户列表(用于下拉选择) +exports.getAllUsers = async (req, res) => { + try { + const users = await User.findAll({ + where: { isDeleted: 0, status: 'active' }, + attributes: ['id', 'username', 'nickname'] + }); + + res.json({ + code: 200, + message: '获取用户列表成功', + data: users + }); + + } catch (error) { + console.error('获取用户列表错误:', error); + res.status(500).json({ + code: 500, + message: '获取用户列表失败', + error: error.message + }); + } +}; + +// 获取当前用户信息 +exports.getCurrentUserInfo = async (req, res) => { + try { + const user = await User.findByPk(req.user.id, { + attributes: ['id', 'username', 'nickname', 'status', 'createdAt', 'updatedAt'], + include: [{ + model: Role, + as: 'role', + attributes: ['id', 'name', 'code'] + }] + }); + + if (!user) { + return res.status(404).json({ + code: 404, + message: '用户不存在' + }); + } + + res.json({ + code: 200, + message: '获取用户信息成功', + data: user + }); + } catch (error) { + console.error('获取用户信息失败:', error); + res.status(500).json({ + code: 500, + message: '获取用户信息失败', + error: error.message + }); + } +}; + +// 更新个人资料 +exports.updateUserProfile = async (req, res) => { + try { + const { nickname } = req.body; + + // 验证昵称 + if (!nickname || nickname.trim().length === 0) { + return res.status(400).json({ + code: 400, + message: '昵称不能为空' + }); + } + + // 更新用户信息 + const user = await User.findByPk(req.user.id); + if (!user) { + return res.status(404).json({ + code: 404, + message: '用户不存在' + }); + } + + await user.update({ + nickname, + updateBy: req.user.id + }); + + // 记录操作日志 + await logOperation({ + userId: req.user.id, + username: req.user.username, + module: '个人中心', + action: '更新资料', + description: '更新个人资料', + method: req.method, + path: req.path, + ip: req.ip, + status: 'success' + }); + + res.json({ + code: 200, + message: '个人资料更新成功', + data: { + id: user.id, + username: user.username, + nickname: user.nickname, + status: user.status, + createdAt: user.createdAt, + updatedAt: user.updatedAt + } + }); + } catch (error) { + console.error('更新个人资料失败:', error); + res.status(500).json({ + code: 500, + message: '更新个人资料失败', + error: error.message + }); + } +}; + +// 修改密码 +exports.changePassword = async (req, res) => { + try { + const { oldPassword, newPassword } = req.body; + + // 验证参数 + if (!oldPassword || !newPassword) { + return res.status(400).json({ + code: 400, + message: '旧密码和新密码不能为空' + }); + } + + if (newPassword.length < 6) { + return res.status(400).json({ + code: 400, + message: '新密码长度至少6位' + }); + } + + // 验证旧密码 + const user = await User.findByPk(req.user.id); + if (!user) { + return res.status(404).json({ + code: 404, + message: '用户不存在' + }); + } + + const isPasswordValid = await bcrypt.compare(oldPassword, user.password); + if (!isPasswordValid) { + return res.status(400).json({ + code: 400, + message: '旧密码错误' + }); + } + + // 更新密码 + const hashedPassword = await bcrypt.hash(newPassword, 10); + await user.update({ + password: hashedPassword, + updateBy: req.user.id + }); + + // 记录操作日志 + await logOperation({ + userId: req.user.id, + username: req.user.username, + module: '个人中心', + action: '修改密码', + description: '修改个人密码', + method: req.method, + path: req.path, + ip: req.ip, + status: 'success' + }); + + res.json({ + code: 200, + message: '密码修改成功' + }); + } catch (error) { + console.error('修改密码失败:', error); + res.status(500).json({ + code: 500, + message: '修改密码失败', + error: error.message + }); + } +}; diff --git a/controllers/waterBillController.js b/controllers/waterBillController.js index a521d03..cc05782 100644 --- a/controllers/waterBillController.js +++ b/controllers/waterBillController.js @@ -134,7 +134,9 @@ const createWaterBill = async (req, res) => { usage, unitPrice, amount, - status: status || 'unpaid' + status: status || 'unpaid', + createBy: req.user.id, + updateBy: req.user.id }); const formattedBill = formatWaterBillData(bill); @@ -172,7 +174,8 @@ const updateWaterBill = async (req, res) => { usage, unitPrice, amount, - status: status !== undefined ? status : bill.status + status: status !== undefined ? status : bill.status, + updateBy: req.user.id }); const formattedBill = formatWaterBillData(bill); @@ -192,7 +195,10 @@ const deleteWaterBill = async (req, res) => { if (!bill) { return res.status(404).json({ error: '水费记录不存在' }); } - await bill.update({ isDeleted: 1 }); + await bill.update({ + isDeleted: 1, + updateBy: req.user.id + }); res.status(200).json({ message: '水费记录删除成功' }); } catch (error) { res.status(500).json({ error: error.message }); diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..63933d2 --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,99 @@ +const jwt = require('jsonwebtoken'); +const User = require('../models/User'); + +// JWT 密钥 +const JWT_SECRET = process.env.JWT_SECRET || 'rentease-secret-key'; + +// 生成 Token(2小时有效期) +const generateToken = (user) => { + return jwt.sign( + { + id: user.id, + username: user.username, + role: user.role?.code || user.role + }, + JWT_SECRET, + { expiresIn: '2h' } + ); +}; + +// 验证 Token 中间件 +const authMiddleware = async (req, res, next) => { + try { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ + code: 401, + message: '未提供认证令牌' + }); + } + + const token = authHeader.substring(7); + + // 验证 Token + const decoded = jwt.verify(token, JWT_SECRET); + + // 检查用户是否存在且状态正常 + const user = await User.findOne({ + where: { + id: decoded.id, + status: 'active' + }, + attributes: ['id', 'username', 'nickname', 'roleId', 'status'], + include: [{ + model: require('../models/Role'), + as: 'role', + attributes: ['id', 'name', 'code'] + }] + }); + + if (!user) { + return res.status(401).json({ + code: 401, + message: '用户不存在或已被禁用' + }); + } + + // 将用户信息附加到请求对象 + req.user = user; + next(); + } catch (error) { + if (error.name === 'TokenExpiredError') { + return res.status(401).json({ + code: 401, + message: '登录已过期,请重新登录' + }); + } + if (error.name === 'JsonWebTokenError') { + return res.status(401).json({ + code: 401, + message: '无效的认证令牌' + }); + } + return res.status(500).json({ + code: 500, + message: '认证失败', + error: error.message + }); + } +}; + +// 管理员权限检查中间件 +const adminMiddleware = (req, res, next) => { + const userRole = req.user.role?.code || req.user.role; + if (userRole !== 'admin') { + return res.status(403).json({ + code: 403, + message: '没有权限执行此操作' + }); + } + next(); +}; + +module.exports = { + generateToken, + authMiddleware, + adminMiddleware, + JWT_SECRET +}; diff --git a/models/Apartment.js b/models/Apartment.js index a9b9b16..1e216d7 100644 --- a/models/Apartment.js +++ b/models/Apartment.js @@ -1,6 +1,5 @@ const { DataTypes } = require('sequelize'); const sequelize = require('../config/db'); -const Region = require('./Region'); const Apartment = sequelize.define('Apartment', { id: { @@ -9,15 +8,6 @@ const Apartment = sequelize.define('Apartment', { autoIncrement: true, comment: '公寓ID' }, - regionId: { - type: DataTypes.INTEGER, - allowNull: false, - references: { - model: Region, - key: 'id' - }, - comment: '所属区域ID' - }, name: { type: DataTypes.STRING(50), allowNull: false, @@ -28,11 +18,21 @@ const Apartment = sequelize.define('Apartment', { 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, @@ -50,8 +50,4 @@ const Apartment = sequelize.define('Apartment', { timestamps: false }); -// 建立关联 -Apartment.belongsTo(Region, { foreignKey: 'regionId' }); -Region.hasMany(Apartment, { foreignKey: 'regionId' }); - module.exports = Apartment; \ No newline at end of file diff --git a/models/ElectricityBill.js b/models/ElectricityBill.js index 5933732..fbb795a 100644 --- a/models/ElectricityBill.js +++ b/models/ElectricityBill.js @@ -59,11 +59,21 @@ const ElectricityBill = sequelize.define('ElectricityBill', { defaultValue: 'unpaid', comment: '状态(unpaid:未支付,paid:已支付)' }, + 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, diff --git a/models/LoginLog.js b/models/LoginLog.js new file mode 100644 index 0000000..af5c3d0 --- /dev/null +++ b/models/LoginLog.js @@ -0,0 +1,58 @@ +const { DataTypes } = require('sequelize'); +const sequelize = require('../config/db'); + +const LoginLog = sequelize.define('LoginLog', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '日志ID' + }, + userId: { + type: DataTypes.INTEGER, + allowNull: true, + comment: '用户ID' + }, + username: { + type: DataTypes.STRING(50), + allowNull: true, + comment: '用户名' + }, + loginType: { + type: DataTypes.ENUM('login', 'logout'), + allowNull: false, + comment: '登录类型' + }, + ip: { + type: DataTypes.STRING(50), + allowNull: true, + comment: 'IP地址' + }, + userAgent: { + type: DataTypes.TEXT, + allowNull: true, + comment: '浏览器信息' + }, + status: { + type: DataTypes.ENUM('success', 'fail'), + allowNull: false, + defaultValue: 'success', + comment: '登录状态' + }, + message: { + type: DataTypes.STRING(255), + allowNull: true, + comment: '登录信息/失败原因' + }, + createTime: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + comment: '创建时间' + } +}, { + tableName: 'login_logs', + timestamps: false, + comment: '登录日志表' +}); + +module.exports = LoginLog; diff --git a/models/Menu.js b/models/Menu.js new file mode 100644 index 0000000..b88c8da --- /dev/null +++ b/models/Menu.js @@ -0,0 +1,88 @@ +const { DataTypes } = require('sequelize'); +const sequelize = require('../config/db'); + +const Menu = sequelize.define('Menu', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + parentId: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null, + comment: '父菜单ID,null表示顶级菜单' + }, + name: { + type: DataTypes.STRING(50), + allowNull: false, + comment: '菜单名称' + }, + code: { + type: DataTypes.STRING(50), + allowNull: false, + unique: true, + comment: '菜单编码' + }, + type: { + type: DataTypes.ENUM('menu', 'button'), + allowNull: false, + defaultValue: 'menu', + comment: '类型:menu-菜单,button-按钮' + }, + path: { + type: DataTypes.STRING(200), + allowNull: true, + comment: '路由路径' + }, + component: { + type: DataTypes.STRING(200), + allowNull: true, + comment: '组件路径' + }, + icon: { + type: DataTypes.STRING(50), + allowNull: true, + comment: '菜单图标' + }, + sort: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + comment: '排序号' + }, + visible: { + type: DataTypes.ENUM('show', 'hide'), + allowNull: false, + defaultValue: 'show', + comment: '是否显示:show-显示,hide-隐藏' + }, + status: { + type: DataTypes.ENUM('active', 'disabled'), + allowNull: false, + defaultValue: 'active', + comment: '状态:active-启用,disabled-禁用' + }, + createBy: { + type: DataTypes.INTEGER, + allowNull: true, + comment: '创建人ID' + }, + updateBy: { + type: DataTypes.INTEGER, + allowNull: true, + comment: '修改人ID' + }, + isDeleted: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + comment: '是否删除:true-已删除,false-未删除' + } +}, { + tableName: 'menus', + timestamps: true, + comment: '菜单表' +}); + +module.exports = Menu; diff --git a/models/OperationLog.js b/models/OperationLog.js new file mode 100644 index 0000000..b3558f9 --- /dev/null +++ b/models/OperationLog.js @@ -0,0 +1,83 @@ +const { DataTypes } = require('sequelize'); +const sequelize = require('../config/db'); + +const OperationLog = sequelize.define('OperationLog', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '日志ID' + }, + userId: { + type: DataTypes.INTEGER, + allowNull: true, + comment: '操作用户ID' + }, + username: { + type: DataTypes.STRING(50), + allowNull: true, + comment: '操作用户名' + }, + module: { + type: DataTypes.STRING(50), + allowNull: false, + comment: '操作模块' + }, + action: { + type: DataTypes.STRING(50), + allowNull: false, + comment: '操作类型' + }, + description: { + type: DataTypes.TEXT, + allowNull: true, + comment: '操作描述' + }, + method: { + type: DataTypes.STRING(10), + allowNull: true, + comment: '请求方法' + }, + url: { + type: DataTypes.STRING(500), + allowNull: true, + comment: '请求URL' + }, + ip: { + type: DataTypes.STRING(50), + allowNull: true, + comment: 'IP地址' + }, + params: { + type: DataTypes.TEXT, + allowNull: true, + comment: '请求参数' + }, + result: { + type: DataTypes.TEXT, + allowNull: true, + comment: '操作结果' + }, + status: { + type: DataTypes.ENUM('success', 'fail'), + allowNull: false, + defaultValue: 'success', + comment: '操作状态' + }, + duration: { + type: DataTypes.INTEGER, + allowNull: true, + comment: '执行时长(毫秒)' + }, + createTime: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + comment: '创建时间' + } +}, { + tableName: 'operation_logs', + timestamps: false, + comment: '操作日志表' +}); + +module.exports = OperationLog; diff --git a/models/Region.js b/models/Region.js deleted file mode 100644 index 60f02b9..0000000 --- a/models/Region.js +++ /dev/null @@ -1,44 +0,0 @@ -const { DataTypes } = require('sequelize'); -const sequelize = require('../config/db'); - -const Region = sequelize.define('Region', { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true, - comment: '区域ID' - }, - name: { - type: DataTypes.STRING(50), - allowNull: false, - unique: true, - comment: '区域名称' - }, - description: { - type: DataTypes.TEXT, - allowNull: true, - comment: '区域描述' - }, - createTime: { - type: DataTypes.DATE, - defaultValue: DataTypes.NOW, - comment: '创建时间' - }, - updateTime: { - type: DataTypes.DATE, - defaultValue: DataTypes.NOW, - onUpdate: DataTypes.NOW, - comment: '更新时间' - }, - isDeleted: { - type: DataTypes.INTEGER, - allowNull: false, - defaultValue: 0, - comment: '删除状态(0:未删除,1:已删除)' - } -}, { - tableName: 'regions', - timestamps: false -}); - -module.exports = Region; \ No newline at end of file diff --git a/models/Rental.js b/models/Rental.js index a7b6119..476605e 100644 --- a/models/Rental.js +++ b/models/Rental.js @@ -54,11 +54,21 @@ const Rental = sequelize.define('Rental', { 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, diff --git a/models/Role.js b/models/Role.js new file mode 100644 index 0000000..9651b64 --- /dev/null +++ b/models/Role.js @@ -0,0 +1,60 @@ +const { DataTypes } = require('sequelize'); +const sequelize = require('../config/db'); + +const Role = sequelize.define('Role', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + name: { + type: DataTypes.STRING(50), + allowNull: false, + unique: true, + comment: '角色名称' + }, + code: { + type: DataTypes.STRING(50), + allowNull: false, + unique: true, + comment: '角色编码' + }, + description: { + type: DataTypes.TEXT, + allowNull: true, + comment: '角色描述' + }, + permissions: { + type: DataTypes.JSON, + allowNull: true, + defaultValue: [], + comment: '角色权限' + }, + status: { + type: DataTypes.ENUM('active', 'disabled'), + defaultValue: 'active', + comment: '状态:active-启用,disabled-禁用' + }, + createBy: { + type: DataTypes.INTEGER, + allowNull: true, + comment: '创建人ID' + }, + updateBy: { + type: DataTypes.INTEGER, + allowNull: true, + comment: '修改人ID' + }, + isDeleted: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + comment: '删除状态(0:未删除,1:已删除)' + } +}, { + tableName: 'roles', + timestamps: true, + comment: '角色表' +}); + +module.exports = Role; diff --git a/models/RoleMenu.js b/models/RoleMenu.js new file mode 100644 index 0000000..2da6517 --- /dev/null +++ b/models/RoleMenu.js @@ -0,0 +1,26 @@ +const { DataTypes } = require('sequelize'); +const sequelize = require('../config/db'); + +const RoleMenu = sequelize.define('RoleMenu', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + roleId: { + type: DataTypes.INTEGER, + allowNull: false, + comment: '角色ID' + }, + menuId: { + type: DataTypes.INTEGER, + allowNull: false, + comment: '菜单ID' + } +}, { + tableName: 'role_menus', + timestamps: true, + comment: '角色菜单关联表' +}); + +module.exports = RoleMenu; diff --git a/models/Room.js b/models/Room.js index 5029ef8..12dad87 100644 --- a/models/Room.js +++ b/models/Room.js @@ -56,11 +56,21 @@ const Room = sequelize.define('Room', { defaultValue: '', comment: '其他状态(cleaning:打扫中,maintenance:维修中)' }, + 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, diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..58c5c11 --- /dev/null +++ b/models/User.js @@ -0,0 +1,64 @@ +const { DataTypes } = require('sequelize'); +const sequelize = require('../config/db'); + +const User = sequelize.define('User', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + username: { + type: DataTypes.STRING(50), + allowNull: false, + unique: true, + comment: '用户名' + }, + password: { + type: DataTypes.STRING(255), + allowNull: false, + comment: '密码(加密存储)' + }, + nickname: { + type: DataTypes.STRING(50), + allowNull: true, + comment: '昵称' + }, + roleId: { + type: DataTypes.INTEGER, + allowNull: true, + comment: '角色ID' + }, + isSuperAdmin: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + comment: '是否超级管理员(0:否,1:是)' + }, + status: { + type: DataTypes.ENUM('active', 'disabled'), + defaultValue: 'active', + comment: '状态:active-启用,disabled-禁用' + }, + createBy: { + type: DataTypes.INTEGER, + allowNull: true, + comment: '创建人ID' + }, + updateBy: { + type: DataTypes.INTEGER, + allowNull: true, + comment: '修改人ID' + }, + isDeleted: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + comment: '删除状态(0:未删除,1:已删除)' + } +}, { + tableName: 'users', + timestamps: true, + comment: '用户表' +}); + +module.exports = User; diff --git a/models/WaterBill.js b/models/WaterBill.js index 757a02c..ef21e76 100644 --- a/models/WaterBill.js +++ b/models/WaterBill.js @@ -59,11 +59,21 @@ const WaterBill = sequelize.define('WaterBill', { defaultValue: 'unpaid', comment: '状态(unbilled:未出账,unpaid:未支付,paid:已支付)' }, + 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, diff --git a/models/index.js b/models/index.js index bef706b..b370abb 100644 --- a/models/index.js +++ b/models/index.js @@ -1,15 +1,37 @@ -const Region = require('./Region'); const Apartment = require('./Apartment'); const Room = require('./Room'); const Rental = require('./Rental'); const WaterBill = require('./WaterBill'); const ElectricityBill = require('./ElectricityBill'); +const User = require('./User'); +const Role = require('./Role'); +const Menu = require('./Menu'); +const RoleMenu = require('./RoleMenu'); +const OperationLog = require('./OperationLog'); +const LoginLog = require('./LoginLog'); + +// 关联关系 +User.belongsTo(Role, { foreignKey: 'roleId', as: 'role' }); +Role.hasMany(User, { foreignKey: 'roleId', as: 'users' }); + +// 菜单自关联(父子菜单) +Menu.belongsTo(Menu, { foreignKey: 'parentId', as: 'parent' }); +Menu.hasMany(Menu, { foreignKey: 'parentId', as: 'children' }); + +// 角色与菜单多对多关联 +Role.belongsToMany(Menu, { through: RoleMenu, foreignKey: 'roleId', otherKey: 'menuId', as: 'menus' }); +Menu.belongsToMany(Role, { through: RoleMenu, foreignKey: 'menuId', otherKey: 'roleId', as: 'roles' }); module.exports = { - Region, Apartment, Room, Rental, WaterBill, - ElectricityBill + ElectricityBill, + User, + Role, + Menu, + RoleMenu, + OperationLog, + LoginLog }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4aa8d0b..8370c90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,10 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "bcryptjs": "^3.0.3", "cors": "^2.8.6", "express": "^4.17.1", + "jsonwebtoken": "^9.0.3", "mysql2": "^2.3.3", "sequelize": "^6.37.7" } @@ -58,6 +60,14 @@ "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.19.0.tgz", @@ -102,6 +112,11 @@ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/bytes": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.0.tgz", @@ -200,6 +215,14 @@ "resolved": "https://registry.npmmirror.com/dottie/-/dottie-2.0.7.tgz", "integrity": "sha512-7lAK2A0b3zZr3UC5aE69CPdCFR4RHW1o2Dr74TqFykxkUCBXSRJum/yPc7g8zRHJqWKomPLHwFLLoUnn8PXXRg==" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", @@ -385,11 +408,86 @@ "resolved": "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz", "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz", "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/long": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/long/-/long-4.0.0.tgz", diff --git a/package.json b/package.json index a28aea4..831cd69 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,10 @@ "license": "ISC", "description": "", "dependencies": { + "bcryptjs": "^3.0.3", "cors": "^2.8.6", "express": "^4.17.1", + "jsonwebtoken": "^9.0.3", "mysql2": "^2.3.3", "sequelize": "^6.37.7" } diff --git a/routes/auth.js b/routes/auth.js new file mode 100644 index 0000000..1800c44 --- /dev/null +++ b/routes/auth.js @@ -0,0 +1,18 @@ +const express = require('express'); +const router = express.Router(); +const authController = require('../controllers/authController'); +const { authMiddleware } = require('../middleware/auth'); + +// 登录(不需要认证) +router.post('/login', authController.login); + +// 登出(需要认证) +router.post('/logout', authMiddleware, authController.logout); + +// 获取当前用户信息(需要认证) +router.get('/user', authMiddleware, authController.getCurrentUser); + +// 修改密码(需要认证) +router.post('/change-password', authMiddleware, authController.changePassword); + +module.exports = router; diff --git a/routes/log.js b/routes/log.js new file mode 100644 index 0000000..92335f7 --- /dev/null +++ b/routes/log.js @@ -0,0 +1,21 @@ +const express = require('express'); +const router = express.Router(); +const logController = require('../controllers/logController'); +const { authMiddleware, adminMiddleware } = require('../middleware/auth'); + +// 所有日志接口都需要认证和管理员权限 +router.use(authMiddleware, adminMiddleware); + +// 获取操作日志列表 +router.get('/operation', logController.getOperationLogs); + +// 获取登录日志列表 +router.get('/login', logController.getLoginLogs); + +// 清空操作日志 +router.post('/operation/clear', logController.clearOperationLogs); + +// 清空登录日志 +router.post('/login/clear', logController.clearLoginLogs); + +module.exports = router; diff --git a/routes/menu.js b/routes/menu.js new file mode 100644 index 0000000..c14474e --- /dev/null +++ b/routes/menu.js @@ -0,0 +1,36 @@ +const express = require('express'); +const router = express.Router(); +const menuController = require('../controllers/menuController'); +const { authMiddleware, adminMiddleware } = require('../middleware/auth'); + +// 获取当前用户的菜单权限(只需要认证,不需要管理员权限) +router.get('/user/menus', authMiddleware, menuController.getUserMenus); + +// 所有菜单管理接口都需要认证和管理员权限 +router.use(authMiddleware, adminMiddleware); + +// 获取菜单树 +router.get('/tree', menuController.getMenuTree); + +// 获取菜单列表 +router.get('/', menuController.getMenuList); + +// 获取菜单详情 +router.get('/:id', menuController.getMenuById); + +// 创建菜单 +router.post('/', menuController.createMenu); + +// 更新菜单 +router.put('/:id', menuController.updateMenu); + +// 删除菜单 +router.delete('/:id', menuController.deleteMenu); + +// 获取角色的菜单权限 +router.get('/role/:roleId', menuController.getRoleMenus); + +// 分配菜单权限给角色 +router.post('/role/:roleId/assign', menuController.assignMenusToRole); + +module.exports = router; diff --git a/routes/region.js b/routes/region.js deleted file mode 100644 index fb79b83..0000000 --- a/routes/region.js +++ /dev/null @@ -1,13 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const regionController = require('../controllers/regionController'); - -// 路由 -router.get('/', regionController.getAllRegions); -router.get('/list', regionController.listRegions); -router.get('/:id', regionController.getRegionById); -router.post('/', regionController.createRegion); -router.put('/:id', regionController.updateRegion); -router.delete('/:id', regionController.deleteRegion); - -module.exports = router; \ No newline at end of file diff --git a/routes/role.js b/routes/role.js new file mode 100644 index 0000000..0693579 --- /dev/null +++ b/routes/role.js @@ -0,0 +1,27 @@ +const express = require('express'); +const router = express.Router(); +const roleController = require('../controllers/roleController'); +const { authMiddleware, adminMiddleware } = require('../middleware/auth'); + +// 所有角色管理接口都需要认证和管理员权限 +router.use(authMiddleware, adminMiddleware); + +// 获取角色列表 +router.get('/', roleController.getRoles); + +// 获取角色详情 +router.get('/:id', roleController.getRoleById); + +// 创建角色 +router.post('/', roleController.createRole); + +// 更新角色 +router.put('/:id', roleController.updateRole); + +// 删除角色 +router.delete('/:id', roleController.deleteRole); + +// 获取所有角色(用于下拉选择) +router.get('/all/list', roleController.getAllRoles); + +module.exports = router; diff --git a/routes/statistics.js b/routes/statistics.js index 4c56805..4740def 100644 --- a/routes/statistics.js +++ b/routes/statistics.js @@ -5,8 +5,7 @@ const statisticsController = require('../controllers/statisticsController'); // 路由 router.get('/rent', statisticsController.getRentStatistics); router.get('/room-status', statisticsController.getRoomStatusStatistics); -router.get('/region-house', statisticsController.getRegionHouseStatistics); -router.get('/region-apartment-house', statisticsController.getRegionApartmentHouseStatistics); router.get('/dashboard', statisticsController.getDashboardStatistics); +router.get('/apartment-room-status', statisticsController.getApartmentRoomStatusStatistics); module.exports = router; \ No newline at end of file diff --git a/routes/user.js b/routes/user.js new file mode 100644 index 0000000..9a6b3e7 --- /dev/null +++ b/routes/user.js @@ -0,0 +1,32 @@ +const express = require('express'); +const router = express.Router(); +const userController = require('../controllers/userController'); +const { authMiddleware, adminMiddleware } = require('../middleware/auth'); + +// 个人中心接口(只需要认证) +router.get('/info', authMiddleware, userController.getCurrentUserInfo); +router.put('/profile', authMiddleware, userController.updateUserProfile); +router.post('/change-password', authMiddleware, userController.changePassword); + +// 管理员权限接口 +router.use(authMiddleware, adminMiddleware); + +// 获取用户列表 +router.get('/', userController.getUserList); + +// 获取用户详情 +router.get('/:id', userController.getUserById); + +// 创建用户 +router.post('/', userController.createUser); + +// 更新用户 +router.put('/:id', userController.updateUser); + +// 删除用户 +router.delete('/:id', userController.deleteUser); + +// 重置用户密码 +router.post('/:id/reset-password', userController.resetUserPassword); + +module.exports = router; diff --git a/utils/logger.js b/utils/logger.js new file mode 100644 index 0000000..d95e781 --- /dev/null +++ b/utils/logger.js @@ -0,0 +1,157 @@ +const OperationLog = require('../models/OperationLog'); +const LoginLog = require('../models/LoginLog'); + +/** + * 记录操作日志 + * @param {Object} options 日志选项 + * @param {number} options.userId 用户ID + * @param {string} options.username 用户名 + * @param {string} options.module 操作模块 + * @param {string} options.action 操作类型 + * @param {string} options.description 操作描述 + * @param {string} options.method 请求方法 + * @param {string} options.url 请求URL + * @param {string} options.ip IP地址 + * @param {Object} options.params 请求参数 + * @param {Object} options.result 操作结果 + * @param {string} options.status 操作状态 success/fail + * @param {number} options.duration 执行时长(毫秒) + */ +async function logOperation(options) { + try { + await OperationLog.create({ + userId: options.userId, + username: options.username, + module: options.module, + action: options.action, + description: options.description, + method: options.method, + url: options.url, + ip: options.ip, + params: options.params ? JSON.stringify(options.params) : null, + result: options.result ? JSON.stringify(options.result) : null, + status: options.status || 'success', + duration: options.duration + }); + } catch (error) { + console.error('记录操作日志失败:', error); + } +} + +/** + * 记录登录日志 + * @param {Object} options 日志选项 + * @param {number} options.userId 用户ID + * @param {string} options.username 用户名 + * @param {string} options.loginType 登录类型 login/logout + * @param {string} options.ip IP地址 + * @param {string} options.userAgent 浏览器信息 + * @param {string} options.status 登录状态 success/fail + * @param {string} options.message 登录信息/失败原因 + */ +async function logLogin(options) { + try { + await LoginLog.create({ + userId: options.userId, + username: options.username, + loginType: options.loginType, + ip: options.ip, + userAgent: options.userAgent, + status: options.status || 'success', + message: options.message + }); + } catch (error) { + console.error('记录登录日志失败:', error); + } +} + +/** + * 获取客户端IP地址 + * @param {Object} req Express请求对象 + * @returns {string} IP地址 + */ +function getClientIp(req) { + return req.headers['x-forwarded-for'] || + req.headers['x-real-ip'] || + req.connection.remoteAddress || + req.socket.remoteAddress || + 'unknown'; +} + +/** + * 操作日志中间件 + * 自动记录API请求日志 + */ +function operationLogMiddleware(options = {}) { + const { module = '系统', excludePaths = ['/api/auth/login'] } = options; + + return async (req, res, next) => { + // 排除指定路径 + if (excludePaths.some(path => req.path.includes(path))) { + return next(); + } + + const startTime = Date.now(); + const originalSend = res.send; + + // 捕获响应数据 + res.send = function(data) { + res.responseData = data; + return originalSend.call(this, data); + }; + + res.on('finish', async () => { + const duration = Date.now() - startTime; + const user = req.user || {}; + + try { + await OperationLog.create({ + userId: user.id, + username: user.username, + module: module, + action: getActionFromMethod(req.method), + description: `${req.method} ${req.path}`, + method: req.method, + url: req.originalUrl, + ip: getClientIp(req), + params: JSON.stringify({ + body: req.body, + query: req.query, + params: req.params + }), + result: res.responseData ? res.responseData.substring(0, 2000) : null, + status: res.statusCode >= 200 && res.statusCode < 300 ? 'success' : 'fail', + duration: duration + }); + } catch (error) { + console.error('记录操作日志失败:', error); + } + }); + + next(); + }; +} + +/** + * 根据HTTP方法获取操作类型 + * @param {string} method HTTP方法 + * @returns {string} 操作类型 + */ +function getActionFromMethod(method) { + const actionMap = { + 'GET': '查询', + 'POST': '新增', + 'PUT': '修改', + 'PATCH': '修改', + 'DELETE': '删除' + }; + return actionMap[method] || '其他'; +} + +module.exports = { + logOperation, + logLogin, + getClientIp, + operationLogMiddleware, + getActionFromMethod +};