479 lines
12 KiB
JavaScript
479 lines
12 KiB
JavaScript
const { Op } = require('sequelize');
|
||
const Menu = require('../models/Menu');
|
||
const RoleMenu = require('../models/RoleMenu');
|
||
const { logOperation } = require('../utils/logger');
|
||
const response = require('../utils/response');
|
||
|
||
// 格式化日期时间(年月日时分秒)
|
||
const formatDateTime = (date) => {
|
||
if (!date) return null;
|
||
const dateObj = date instanceof Date ? date : new Date(date);
|
||
if (isNaN(dateObj.getTime())) return null;
|
||
const beijingDate = new Date(dateObj.getTime() + 8 * 60 * 60 * 1000);
|
||
return beijingDate.toISOString().replace('T', ' ').slice(0, 19);
|
||
};
|
||
|
||
// 格式化菜单数据(递归处理children)
|
||
const formatMenuData = (menu) => {
|
||
const data = menu.toJSON ? menu.toJSON() : menu;
|
||
const result = {
|
||
...data,
|
||
createTime: formatDateTime(data.createTime),
|
||
updateTime: formatDateTime(data.updateTime)
|
||
};
|
||
if (data.children && Array.isArray(data.children)) {
|
||
result.children = data.children.map(formatMenuData);
|
||
}
|
||
return result;
|
||
};
|
||
|
||
// 获取菜单树
|
||
exports.getMenuTree = async (req, res) => {
|
||
try {
|
||
const { type, status } = req.query;
|
||
const userType = req.user.userType;
|
||
|
||
const where = { isDeleted: false };
|
||
if (type) {
|
||
where.type = type;
|
||
}
|
||
if (status) {
|
||
where.status = status;
|
||
}
|
||
|
||
// 普通管理员只能获取基础菜单
|
||
if (userType === 'tenant_admin') {
|
||
where.isBasic = 1;
|
||
}
|
||
|
||
const menus = await Menu.findAll({
|
||
where,
|
||
order: [['sort', 'ASC'], ['createTime', 'ASC']]
|
||
});
|
||
|
||
const menuTree = buildTree(menus);
|
||
|
||
response.success(res, '获取成功', menuTree);
|
||
} catch (error) {
|
||
console.error('获取菜单树失败:', error);
|
||
response.serverError(res, '获取菜单树失败', error);
|
||
}
|
||
};
|
||
|
||
// 获取菜单列表(平铺)
|
||
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'], ['createTime', 'ASC']],
|
||
offset: (page - 1) * pageSize,
|
||
limit: parseInt(pageSize)
|
||
});
|
||
|
||
response.success(res, '获取成功', {
|
||
list: rows.map(formatMenuData),
|
||
total: count,
|
||
page: parseInt(page),
|
||
pageSize: parseInt(pageSize)
|
||
});
|
||
} catch (error) {
|
||
console.error('获取菜单列表失败:', error);
|
||
response.serverError(res, '获取菜单列表失败', error);
|
||
}
|
||
};
|
||
|
||
// 获取菜单详情
|
||
exports.getMenuById = async (req, res) => {
|
||
try {
|
||
const { id } = req.params;
|
||
|
||
const menu = await Menu.findByPk(id, {
|
||
where: { isDeleted: false }
|
||
});
|
||
|
||
if (!menu) {
|
||
return response.notFound(res, '菜单不存在');
|
||
}
|
||
|
||
response.success(res, '获取成功', formatMenuData(menu));
|
||
} catch (error) {
|
||
console.error('获取菜单详情失败:', error);
|
||
response.serverError(res, '获取菜单详情失败', error);
|
||
}
|
||
};
|
||
|
||
// 创建菜单
|
||
exports.createMenu = async (req, res) => {
|
||
try {
|
||
const { parentId, name, code, type, path, component, icon, sort, visible, status } = req.body;
|
||
|
||
if (!name || !code || !type) {
|
||
return response.badRequest(res, '菜单名称、编码和类型不能为空');
|
||
}
|
||
|
||
if (type === 'menu' && !path) {
|
||
return response.badRequest(res, '菜单类型必须填写路由路径');
|
||
}
|
||
|
||
const existingMenu = await Menu.findOne({
|
||
where: { code, isDeleted: false }
|
||
});
|
||
|
||
if (existingMenu) {
|
||
return response.badRequest(res, '菜单编码已存在');
|
||
}
|
||
|
||
if (parentId) {
|
||
const parentMenu = await Menu.findByPk(parentId, {
|
||
where: { isDeleted: false }
|
||
});
|
||
if (!parentMenu) {
|
||
return response.badRequest(res, '父菜单不存在');
|
||
}
|
||
}
|
||
|
||
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'
|
||
});
|
||
|
||
response.success(res, '创建成功', formatMenuData(menu));
|
||
} catch (error) {
|
||
console.error('创建菜单失败:', error);
|
||
response.serverError(res, '创建菜单失败', error);
|
||
}
|
||
};
|
||
|
||
// 更新菜单
|
||
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 response.notFound(res, '菜单不存在');
|
||
}
|
||
|
||
if (code && code !== menu.code) {
|
||
const existingMenu = await Menu.findOne({
|
||
where: { code, isDeleted: false, id: { [Op.ne]: id } }
|
||
});
|
||
|
||
if (existingMenu) {
|
||
return response.badRequest(res, '菜单编码已存在');
|
||
}
|
||
}
|
||
|
||
if (parentId) {
|
||
if (parseInt(parentId) === parseInt(id)) {
|
||
return response.badRequest(res, '不能将菜单设置为自己的子菜单');
|
||
}
|
||
|
||
const parentMenu = await Menu.findByPk(parentId, {
|
||
where: { isDeleted: false }
|
||
});
|
||
if (!parentMenu) {
|
||
return response.badRequest(res, '父菜单不存在');
|
||
}
|
||
}
|
||
|
||
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'
|
||
});
|
||
|
||
response.success(res, '更新成功', formatMenuData(menu));
|
||
} catch (error) {
|
||
console.error('更新菜单失败:', error);
|
||
response.serverError(res, '更新菜单失败', error);
|
||
}
|
||
};
|
||
|
||
// 删除菜单
|
||
exports.deleteMenu = async (req, res) => {
|
||
try {
|
||
const { id } = req.params;
|
||
|
||
const menu = await Menu.findByPk(id, {
|
||
where: { isDeleted: false }
|
||
});
|
||
|
||
if (!menu) {
|
||
return response.notFound(res, '菜单不存在');
|
||
}
|
||
|
||
const children = await Menu.findAll({
|
||
where: { parentId: id, isDeleted: false }
|
||
});
|
||
|
||
if (children.length > 0) {
|
||
return response.badRequest(res, '该菜单下有子菜单,无法删除');
|
||
}
|
||
|
||
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'
|
||
});
|
||
|
||
response.success(res, '删除成功');
|
||
} catch (error) {
|
||
console.error('删除菜单失败:', error);
|
||
response.serverError(res, '删除菜单失败', error);
|
||
}
|
||
};
|
||
|
||
// 获取角色的菜单权限
|
||
exports.getRoleMenus = async (req, res) => {
|
||
try {
|
||
const { roleId } = req.params;
|
||
|
||
const Role = require('../models/Role');
|
||
|
||
// 先检查角色是否存在
|
||
const role = await Role.findOne({
|
||
where: { id: roleId, isDeleted: 0 }
|
||
});
|
||
|
||
if (!role) {
|
||
return response.notFound(res, '角色不存在');
|
||
}
|
||
|
||
// 获取角色的菜单权限
|
||
const roleData = await Role.findByPk(roleId, {
|
||
include: [{
|
||
model: Menu,
|
||
as: 'menus',
|
||
where: { isDeleted: 0 },
|
||
required: false,
|
||
through: { attributes: [] }
|
||
}]
|
||
});
|
||
|
||
const menus = roleData ? roleData.menus : [];
|
||
|
||
response.success(res, '获取成功', menus.map(formatMenuData));
|
||
} catch (error) {
|
||
console.error('获取角色菜单失败:', error);
|
||
response.serverError(res, '获取角色菜单失败', error);
|
||
}
|
||
};
|
||
|
||
// 分配菜单权限给角色
|
||
exports.assignMenusToRole = async (req, res) => {
|
||
try {
|
||
const { roleId } = req.params;
|
||
const { menuIds } = req.body;
|
||
|
||
if (!Array.isArray(menuIds)) {
|
||
return response.badRequest(res, '菜单ID必须是数组');
|
||
}
|
||
|
||
const Role = require('../models/Role');
|
||
const role = await Role.findByPk(roleId);
|
||
|
||
if (!role) {
|
||
return response.notFound(res, '角色不存在');
|
||
}
|
||
|
||
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'
|
||
});
|
||
|
||
response.success(res, '分配成功');
|
||
} catch (error) {
|
||
console.error('分配菜单权限失败:', error);
|
||
response.serverError(res, '分配菜单权限失败', error);
|
||
}
|
||
};
|
||
|
||
// 获取当前用户的菜单权限
|
||
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'
|
||
}]
|
||
});
|
||
|
||
// 系统管理员或租户管理员返回所有菜单
|
||
if (user.userType === 'super_admin' || user.userType === 'tenant_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 response.success(res, '获取成功', menuTree);
|
||
}
|
||
|
||
if (!user.role) {
|
||
return response.forbidden(res, '用户没有分配角色');
|
||
}
|
||
|
||
// 普通用户返回角色分配的菜单
|
||
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 response.success(res, '获取成功', []);
|
||
}
|
||
|
||
// 按sort排序
|
||
const menus = roleData.menus.sort((a, b) => a.sort - b.sort);
|
||
|
||
// 构建菜单树
|
||
const menuTree = buildTree(menus);
|
||
|
||
response.success(res, '获取成功', menuTree);
|
||
} catch (error) {
|
||
console.error('获取用户菜单失败:', error);
|
||
response.serverError(res, '获取用户菜单失败', error);
|
||
}
|
||
};
|
||
|
||
// 构建菜单树
|
||
function buildTree(menus, parentId = null) {
|
||
return menus
|
||
.filter(menu => menu.parentId === parentId)
|
||
.map(menu => {
|
||
// 获取菜单的原始数据
|
||
const menuData = formatMenuData(menu);
|
||
return {
|
||
...menuData,
|
||
children: buildTree(menus, menu.id)
|
||
};
|
||
});
|
||
}
|