登录权限管理

This commit is contained in:
wangxiaoxian 2026-03-09 00:28:33 +08:00
parent d62de12d1f
commit 6d462df532
36 changed files with 3021 additions and 406 deletions

33
app.js
View File

@ -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) => {

View File

@ -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
});
// 格式化数据

View File

@ -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
});
}
};

View File

@ -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
});
}
};

View File

@ -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)
}));
}

View File

@ -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
};

View File

@ -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 }
}
]
}

View File

@ -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
});
}
};

View File

@ -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 });

View File

@ -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
};

View File

@ -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
});
}
};

View File

@ -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 });

99
middleware/auth.js Normal file
View File

@ -0,0 +1,99 @@
const jwt = require('jsonwebtoken');
const User = require('../models/User');
// JWT 密钥
const JWT_SECRET = process.env.JWT_SECRET || 'rentease-secret-key';
// 生成 Token2小时有效期
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
};

View File

@ -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;

View File

@ -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,

58
models/LoginLog.js Normal file
View File

@ -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;

88
models/Menu.js Normal file
View File

@ -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: '父菜单IDnull表示顶级菜单'
},
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;

83
models/OperationLog.js Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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,

60
models/Role.js Normal file
View File

@ -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;

26
models/RoleMenu.js Normal file
View File

@ -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;

View File

@ -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,

64
models/User.js Normal file
View File

@ -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: '是否超级管理员01'
},
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;

View File

@ -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,

View File

@ -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
};

98
package-lock.json generated
View File

@ -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",

View File

@ -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"
}

18
routes/auth.js Normal file
View File

@ -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;

21
routes/log.js Normal file
View File

@ -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;

36
routes/menu.js Normal file
View File

@ -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;

View File

@ -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;

27
routes/role.js Normal file
View File

@ -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;

View File

@ -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;

32
routes/user.js Normal file
View File

@ -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;

157
utils/logger.js Normal file
View File

@ -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
};