做权限管理模块
This commit is contained in:
parent
88d2f527f4
commit
ca1d265e34
73
pc/src/api/permission.js
Normal file
73
pc/src/api/permission.js
Normal file
@ -0,0 +1,73 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
/**
|
||||
* 获取所有菜单权限列表(用于分配权限)
|
||||
*/
|
||||
export function getAllMenuPermissions() {
|
||||
return request({
|
||||
url: '/api/permissions/menus',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定角色的权限
|
||||
* @param {number} roleId - 角色ID
|
||||
*/
|
||||
export function getRolePermissions(roleId) {
|
||||
return request({
|
||||
url: `/api/permissions/role/${roleId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 为角色分配权限
|
||||
* @param {number} roleId - 角色ID
|
||||
* @param {Array<number>} menuIds - 菜单ID列表
|
||||
*/
|
||||
export function assignRolePermissions(roleId, menuIds) {
|
||||
return request({
|
||||
url: `/api/permissions/role/${roleId}`,
|
||||
method: 'post',
|
||||
timeout: 60000, // 设置60秒超时,因为批量保存可能需要较长时间
|
||||
data: {
|
||||
menu_ids: menuIds,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户的权限
|
||||
*/
|
||||
export function getUserPermissions() {
|
||||
return request({
|
||||
url: '/api/permissions/user',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户有权限访问的菜单树
|
||||
*/
|
||||
export function getUserMenuTree() {
|
||||
return request({
|
||||
url: '/api/permissions/user/menus',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否拥有指定权限
|
||||
* @param {string} permission - 权限标识
|
||||
*/
|
||||
export function checkPermission(permission) {
|
||||
return request({
|
||||
url: '/api/permissions/check',
|
||||
method: 'get',
|
||||
params: {
|
||||
permission,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,112 +1,602 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>权限管理</h2>
|
||||
<el-button type="primary" @click="handleAddPermission = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加权限
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="permissions-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="title">
|
||||
<el-icon><Key /></el-icon>
|
||||
权限管理
|
||||
</span>
|
||||
<span class="subtitle">为角色分配菜单和API访问权限</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-divider></el-divider>
|
||||
<el-row :gutter="20">
|
||||
<!-- 左侧:角色列表 -->
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" class="role-card">
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
<span>角色列表</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-input
|
||||
v-model="roleSearchQuery"
|
||||
placeholder="搜索角色..."
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
class="search-input"
|
||||
/>
|
||||
|
||||
<el-table :data="permissions" style="width: 100%">
|
||||
<el-table-column prop="name" label="权限名称" width="180" align="center" />
|
||||
<el-table-column prop="code" label="权限代码" width="200" align="center" />
|
||||
<el-table-column prop="description" label="权限描述" align="center" />
|
||||
<el-table-column prop="type" label="权限类型" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.type === 'menu' ? 'primary' : 'warning'">
|
||||
{{ scope.row.type === "menu" ? "菜单权限" : "功能权限" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleEdit(scope.row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button size="small" type="danger" @click="handleDelete(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
background
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
@current-change="handlePageChange"
|
||||
layout="total, prev, pager, next"
|
||||
/>
|
||||
</div>
|
||||
<el-scrollbar height="600px" class="role-list">
|
||||
<div
|
||||
v-for="role in filteredRoles"
|
||||
:key="role.roleId"
|
||||
:class="['role-item', { active: selectedRole?.roleId === role.roleId }]"
|
||||
@click="selectRole(role)"
|
||||
>
|
||||
<div class="role-info">
|
||||
<div class="role-name">{{ role.roleName }}</div>
|
||||
<div class="role-code">{{ role.roleCode }}</div>
|
||||
</div>
|
||||
<el-icon v-if="selectedRole?.roleId === role.roleId" class="check-icon">
|
||||
<Check />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="filteredRoles.length === 0" description="暂无角色数据" />
|
||||
</el-scrollbar>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 右侧:权限分配 -->
|
||||
<el-col :span="16">
|
||||
<el-card shadow="hover" class="permission-card">
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<el-icon><Menu /></el-icon>
|
||||
<span>权限分配</span>
|
||||
<span v-if="selectedRole" class="selected-role-name">
|
||||
({{ selectedRole.roleName }})
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="!selectedRole" class="empty-state">
|
||||
<el-empty description="请先选择一个角色" />
|
||||
</div>
|
||||
|
||||
<div v-else class="permission-content">
|
||||
<!-- 搜索和操作按钮 -->
|
||||
<div class="toolbar">
|
||||
<el-input
|
||||
v-model="permissionSearchQuery"
|
||||
placeholder="搜索菜单..."
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
style="width: 300px;"
|
||||
/>
|
||||
|
||||
<div class="actions">
|
||||
<el-button @click="expandAll">全部展开</el-button>
|
||||
<el-button @click="collapseAll">全部折叠</el-button>
|
||||
<el-button @click="checkAll">全选</el-button>
|
||||
<el-button @click="uncheckAll">取消全选</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 权限树 -->
|
||||
<el-scrollbar height="550px" class="tree-container">
|
||||
<el-tree
|
||||
ref="permissionTree"
|
||||
:data="menuTreeData"
|
||||
:props="treeProps"
|
||||
:filter-node-method="filterNode"
|
||||
node-key="menu_id"
|
||||
show-checkbox
|
||||
:default-expand-all="false"
|
||||
:check-strictly="false"
|
||||
class="permission-tree"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="custom-tree-node">
|
||||
<div class="node-content">
|
||||
<el-icon v-if="data.menu_type === 1" class="menu-icon">
|
||||
<Folder />
|
||||
</el-icon>
|
||||
<el-icon v-else class="api-icon">
|
||||
<Link />
|
||||
</el-icon>
|
||||
<span class="node-label">{{ node.label }}</span>
|
||||
</div>
|
||||
<div class="node-info">
|
||||
<el-tag v-if="data.menu_type === 1" type="primary" size="small">
|
||||
页面
|
||||
</el-tag>
|
||||
<el-tag v-else type="success" size="small">
|
||||
API
|
||||
</el-tag>
|
||||
<el-tag v-if="data.permission" type="info" size="small" class="permission-tag">
|
||||
{{ data.permission }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-scrollbar>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="footer">
|
||||
<el-button type="primary" @click="savePermissions" :loading="saving">
|
||||
<el-icon><Select /></el-icon>
|
||||
保存权限设置
|
||||
</el-button>
|
||||
<el-button @click="resetPermissions">
|
||||
<el-icon><RefreshLeft /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import {
|
||||
Key,
|
||||
UserFilled,
|
||||
Menu,
|
||||
Search,
|
||||
Check,
|
||||
Folder,
|
||||
Link,
|
||||
Select,
|
||||
RefreshLeft,
|
||||
} from '@element-plus/icons-vue';
|
||||
import { getRoleByTenantId } from '@/api/role';
|
||||
import {
|
||||
getAllMenuPermissions,
|
||||
getRolePermissions,
|
||||
assignRolePermissions,
|
||||
} from '@/api/permission';
|
||||
|
||||
// const loading = ref(false);
|
||||
// const error = ref("");
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
// 角色相关
|
||||
const roleList = ref([]);
|
||||
const roleSearchQuery = ref('');
|
||||
const selectedRole = ref(null);
|
||||
|
||||
interface Permission {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
description: string;
|
||||
type: string;
|
||||
}
|
||||
// 权限相关
|
||||
const allMenus = ref([]);
|
||||
const menuTreeData = ref([]);
|
||||
const permissionSearchQuery = ref('');
|
||||
const permissionTree = ref(null);
|
||||
const saving = ref(false);
|
||||
|
||||
const permissions = ref<Permission[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: "用户管理",
|
||||
code: "user:manage",
|
||||
description: "用户的增删改查权限",
|
||||
type: "menu",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "角色管理",
|
||||
code: "role:manage",
|
||||
description: "角色的增删改查权限",
|
||||
type: "menu",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "数据导出",
|
||||
code: "data:export",
|
||||
description: "数据导出功能权限",
|
||||
type: "function",
|
||||
},
|
||||
]);
|
||||
|
||||
const handleAddPermission = () => {
|
||||
// 添加权限功能
|
||||
// 树配置
|
||||
const treeProps = {
|
||||
children: 'children',
|
||||
label: 'menu_name',
|
||||
};
|
||||
|
||||
const handleEdit = (permission: Permission) => {
|
||||
// 编辑权限功能
|
||||
// 过滤后的角色列表
|
||||
const filteredRoles = computed(() => {
|
||||
if (!roleSearchQuery.value) {
|
||||
return roleList.value;
|
||||
}
|
||||
const query = roleSearchQuery.value.toLowerCase();
|
||||
return roleList.value.filter(
|
||||
(role) =>
|
||||
role.roleName.toLowerCase().includes(query) ||
|
||||
role.roleCode.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
// 获取当前租户ID
|
||||
const getCurrentTenantId = () => {
|
||||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
|
||||
return userInfo.tenant_id || 1;
|
||||
};
|
||||
|
||||
const handleDelete = (permission: Permission) => {
|
||||
// 删除权限功能
|
||||
// 加载角色列表
|
||||
const loadRoles = async () => {
|
||||
try {
|
||||
const tenantId = getCurrentTenantId();
|
||||
const res = await getRoleByTenantId(tenantId);
|
||||
|
||||
// 兼容两种响应格式:success 和 code
|
||||
const isSuccess = res.success || res.code === 0;
|
||||
if (isSuccess && res.data) {
|
||||
roleList.value = res.data;
|
||||
} else {
|
||||
ElMessage.warning(res.message || '未获取到角色数据');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载角色列表失败:', error);
|
||||
ElMessage.error('加载角色列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 加载所有菜单权限
|
||||
const loadAllMenus = async () => {
|
||||
try {
|
||||
const res = await getAllMenuPermissions();
|
||||
|
||||
if (res.success && res.data) {
|
||||
allMenus.value = res.data;
|
||||
buildMenuTree();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载菜单列表失败:', error);
|
||||
ElMessage.error('加载菜单列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 构建菜单树
|
||||
const buildMenuTree = () => {
|
||||
const tree = [];
|
||||
const map = new Map();
|
||||
|
||||
// 先创建所有节点的映射
|
||||
allMenus.value.forEach((menu) => {
|
||||
map.set(menu.menu_id, { ...menu, children: [] });
|
||||
});
|
||||
|
||||
// 构建树形结构
|
||||
allMenus.value.forEach((menu) => {
|
||||
const node = map.get(menu.menu_id);
|
||||
if (menu.parent_id === 0) {
|
||||
tree.push(node);
|
||||
} else {
|
||||
const parent = map.get(menu.parent_id);
|
||||
if (parent) {
|
||||
parent.children.push(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
menuTreeData.value = tree;
|
||||
};
|
||||
|
||||
// 选择角色
|
||||
const selectRole = async (role) => {
|
||||
selectedRole.value = role;
|
||||
await loadRolePermissions(role.roleId);
|
||||
};
|
||||
|
||||
// 加载角色权限
|
||||
const loadRolePermissions = async (roleId) => {
|
||||
try {
|
||||
const res = await getRolePermissions(roleId);
|
||||
|
||||
if (res.success && res.data) {
|
||||
// 等待树渲染完成
|
||||
await nextTick();
|
||||
|
||||
// 设置选中的节点
|
||||
if (permissionTree.value) {
|
||||
permissionTree.value.setCheckedKeys(res.data.menu_ids || []);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载角色权限失败:', error);
|
||||
ElMessage.error('加载角色权限失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 保存权限
|
||||
const savePermissions = async () => {
|
||||
if (!selectedRole.value) {
|
||||
ElMessage.warning('请先选择角色');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 只获取完全选中的节点(不包括半选中的父节点)
|
||||
// 因为半选中的父节点表示部分子节点被选中,父节点本身不需要权限记录
|
||||
const checkedKeys = permissionTree.value.getCheckedKeys();
|
||||
const menuIds = checkedKeys;
|
||||
|
||||
saving.value = true;
|
||||
const res = await assignRolePermissions(selectedRole.value.roleId, menuIds);
|
||||
|
||||
if (res.success) {
|
||||
ElMessage.success('权限保存成功');
|
||||
await loadRolePermissions(selectedRole.value.roleId);
|
||||
} else {
|
||||
ElMessage.error(res.message || '权限保存失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存权限失败:', error);
|
||||
if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {
|
||||
ElMessage.error('请求超时,请重试。如果数据量较大,可能需要更长时间');
|
||||
} else {
|
||||
ElMessage.error(error.message || '保存权限失败,请稍后重试');
|
||||
}
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 重置权限
|
||||
const resetPermissions = async () => {
|
||||
if (!selectedRole.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要重置权限设置吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
|
||||
await loadRolePermissions(selectedRole.value.roleId);
|
||||
ElMessage.success('已重置');
|
||||
} catch (error) {
|
||||
// 用户取消操作
|
||||
}
|
||||
};
|
||||
|
||||
// 全部展开
|
||||
const expandAll = () => {
|
||||
if (permissionTree.value) {
|
||||
const allKeys = allMenus.value.map((m) => m.menu_id);
|
||||
allKeys.forEach((key) => {
|
||||
const node = permissionTree.value.getNode(key);
|
||||
if (node) {
|
||||
node.expanded = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 全部折叠
|
||||
const collapseAll = () => {
|
||||
if (permissionTree.value) {
|
||||
const allKeys = allMenus.value.map((m) => m.menu_id);
|
||||
allKeys.forEach((key) => {
|
||||
const node = permissionTree.value.getNode(key);
|
||||
if (node) {
|
||||
node.expanded = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 全选
|
||||
const checkAll = () => {
|
||||
if (permissionTree.value) {
|
||||
const allKeys = allMenus.value.map((m) => m.menu_id);
|
||||
permissionTree.value.setCheckedKeys(allKeys);
|
||||
}
|
||||
};
|
||||
|
||||
// 取消全选
|
||||
const uncheckAll = () => {
|
||||
if (permissionTree.value) {
|
||||
permissionTree.value.setCheckedKeys([]);
|
||||
}
|
||||
};
|
||||
|
||||
// 过滤节点
|
||||
const filterNode = (value, data) => {
|
||||
if (!value) return true;
|
||||
return (
|
||||
data.menu_name.toLowerCase().includes(value.toLowerCase()) ||
|
||||
(data.path && data.path.toLowerCase().includes(value.toLowerCase())) ||
|
||||
(data.permission && data.permission.toLowerCase().includes(value.toLowerCase()))
|
||||
);
|
||||
};
|
||||
|
||||
// 监听搜索框变化
|
||||
watch(permissionSearchQuery, (val) => {
|
||||
if (permissionTree.value) {
|
||||
permissionTree.value.filter(val);
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化
|
||||
const init = async () => {
|
||||
await loadRoles();
|
||||
await loadAllMenus();
|
||||
};
|
||||
|
||||
init();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="less" scoped>
|
||||
.permissions-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.role-card,
|
||||
.permission-card {
|
||||
height: 100%;
|
||||
|
||||
.card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
|
||||
.selected-role-name {
|
||||
font-size: 14px;
|
||||
color: var(--el-color-primary);
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.role-list {
|
||||
margin-top: 16px;
|
||||
|
||||
.role-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 8px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #e6f0ff;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--el-color-primary-light-9);
|
||||
border-left: 3px solid var(--el-color-primary);
|
||||
|
||||
.role-name {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.role-info {
|
||||
flex: 1;
|
||||
|
||||
.role-name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.role-code {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: var(--el-color-primary);
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.permission-content {
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-container {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.permission-tree {
|
||||
background: transparent;
|
||||
|
||||
:deep(.el-tree-node__content) {
|
||||
height: auto;
|
||||
padding: 8px 0;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&:hover {
|
||||
background: #e6f0ff;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-tree-node {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-right: 16px;
|
||||
|
||||
.node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.menu-icon {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.api-icon {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.node-label {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.node-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.permission-tag {
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
264
server/controllers/permission.go
Normal file
264
server/controllers/permission.go
Normal file
@ -0,0 +1,264 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"server/models"
|
||||
"strconv"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
"github.com/beego/beego/v2/core/logs"
|
||||
)
|
||||
|
||||
// PermissionController 权限管理控制器
|
||||
type PermissionController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// GetAllMenuPermissions 获取所有菜单权限列表(用于分配权限)
|
||||
func (c *PermissionController) GetAllMenuPermissions() {
|
||||
menus, err := models.GetAllMenuPermissions()
|
||||
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "获取菜单列表失败",
|
||||
"error": err.Error(),
|
||||
}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "获取菜单列表成功",
|
||||
"data": menus,
|
||||
}
|
||||
}
|
||||
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetRolePermissions 获取指定角色的权限
|
||||
func (c *PermissionController) GetRolePermissions() {
|
||||
roleId, err := c.GetInt(":roleId")
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "角色ID参数错误",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
permissions, err := models.GetRolePermissions(roleId)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "获取角色权限失败",
|
||||
"error": err.Error(),
|
||||
}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "获取角色权限成功",
|
||||
"data": permissions,
|
||||
}
|
||||
}
|
||||
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AssignRolePermissions 为角色分配权限
|
||||
func (c *PermissionController) AssignRolePermissions() {
|
||||
roleId, err := c.GetInt(":roleId")
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "角色ID参数错误",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 解析请求体
|
||||
var requestData struct {
|
||||
MenuIds []int `json:"menu_ids"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &requestData); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "请求参数错误",
|
||||
"error": err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户ID(从JWT中获取)
|
||||
userIdData := c.Ctx.Input.GetData("userId")
|
||||
var createBy string
|
||||
if userIdData != nil {
|
||||
userId, ok := userIdData.(int)
|
||||
if ok {
|
||||
createBy = strconv.Itoa(userId)
|
||||
}
|
||||
}
|
||||
|
||||
// 记录日志(用于调试)
|
||||
logs.Info(fmt.Sprintf("开始为角色 %d 分配权限,共 %d 个菜单", roleId, len(requestData.MenuIds)))
|
||||
|
||||
// 分配权限
|
||||
err = models.AssignRolePermissions(roleId, requestData.MenuIds, createBy)
|
||||
if err != nil {
|
||||
logs.Error(fmt.Sprintf("分配权限失败: %v", err))
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "分配权限失败",
|
||||
"error": err.Error(),
|
||||
}
|
||||
} else {
|
||||
logs.Info(fmt.Sprintf("角色 %d 权限分配成功", roleId))
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "分配权限成功",
|
||||
}
|
||||
}
|
||||
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetUserPermissions 获取当前登录用户的权限
|
||||
func (c *PermissionController) GetUserPermissions() {
|
||||
// 从JWT中获取用户ID
|
||||
userIdData := c.Ctx.Input.GetData("userId")
|
||||
if userIdData == nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "未获取到用户信息",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
userId, ok := userIdData.(int)
|
||||
if !ok {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "用户ID格式错误",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
permissions, err := models.GetUserPermissions(userId)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "获取用户权限失败",
|
||||
"error": err.Error(),
|
||||
}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "获取用户权限成功",
|
||||
"data": permissions,
|
||||
}
|
||||
}
|
||||
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetUserMenuTree 获取当前用户有权限访问的菜单树
|
||||
func (c *PermissionController) GetUserMenuTree() {
|
||||
// 从JWT中获取用户ID
|
||||
userIdData := c.Ctx.Input.GetData("userId")
|
||||
if userIdData == nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "未获取到用户信息",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
userId, ok := userIdData.(int)
|
||||
if !ok {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "用户ID格式错误",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
menuTree, err := models.GetUserMenuTree(userId)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "获取用户菜单失败",
|
||||
"error": err.Error(),
|
||||
}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "获取用户菜单成功",
|
||||
"data": menuTree,
|
||||
}
|
||||
}
|
||||
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// CheckPermission 检查用户是否拥有指定权限
|
||||
func (c *PermissionController) CheckPermission() {
|
||||
// 从JWT中获取用户ID
|
||||
userIdData := c.Ctx.Input.GetData("userId")
|
||||
if userIdData == nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "未获取到用户信息",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
userId, ok := userIdData.(int)
|
||||
if !ok {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "用户ID格式错误",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取权限标识
|
||||
permission := c.GetString("permission")
|
||||
if permission == "" {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "权限标识不能为空",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
hasPermission, err := models.CheckUserPermission(userId, permission)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "检查权限失败",
|
||||
"error": err.Error(),
|
||||
}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "权限检查完成",
|
||||
"data": map[string]interface{}{
|
||||
"has_permission": hasPermission,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
188
server/middleware/permission.go
Normal file
188
server/middleware/permission.go
Normal file
@ -0,0 +1,188 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"server/models"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
)
|
||||
|
||||
// PermissionMiddleware 权限验证中间件
|
||||
// 根据路由的权限标识检查用户是否有访问权限
|
||||
func PermissionMiddleware() func(ctx *context.Context) {
|
||||
return func(ctx *context.Context) {
|
||||
// 获取当前请求的路径
|
||||
path := ctx.Input.URL()
|
||||
|
||||
// 不需要权限验证的路径列表
|
||||
publicPaths := []string{
|
||||
"/api/login",
|
||||
"/api/logout",
|
||||
"/api/reset-password",
|
||||
"/api/program-categories/public",
|
||||
"/api/program-infos/public",
|
||||
"/api/files/public",
|
||||
}
|
||||
|
||||
// 检查是否为公开路径
|
||||
for _, p := range publicPaths {
|
||||
if path == p {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否为公开预览接口
|
||||
if strings.HasPrefix(path, "/api/files/public-preview/") {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户ID
|
||||
userIdData := ctx.Input.GetData("userId")
|
||||
if userIdData == nil {
|
||||
// 如果没有用户ID,说明未登录,这个应该在JWT中间件中处理
|
||||
// 这里直接返回,因为JWT中间件已经拦截了
|
||||
return
|
||||
}
|
||||
|
||||
userId, ok := userIdData.(int)
|
||||
if !ok {
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "用户ID格式错误",
|
||||
}, false, false)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前路由对应的权限标识
|
||||
permission := getPermissionByPath(path, ctx.Input.Method())
|
||||
|
||||
// 如果没有权限标识,说明该接口不需要权限控制
|
||||
if permission == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户是否拥有该权限
|
||||
hasPermission, err := models.CheckUserPermission(userId, permission)
|
||||
if err != nil {
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "权限验证失败",
|
||||
"error": err.Error(),
|
||||
}, false, false)
|
||||
return
|
||||
}
|
||||
|
||||
if !hasPermission {
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "您没有权限访问此接口",
|
||||
"code": 403,
|
||||
}, false, false)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getPermissionByPath 根据路径和方法获取权限标识
|
||||
// 这是一个简化版本,实际应该从数据库中动态获取路由-权限映射关系
|
||||
func getPermissionByPath(path, method string) string {
|
||||
// 权限映射表(路径模式 -> 权限标识)
|
||||
// 这里只列举了部分示例,实际应该从数据库中加载
|
||||
permissionMap := map[string]string{
|
||||
// 用户管理
|
||||
"GET:/api/allUsers": "user:list",
|
||||
"GET:/api/user/:id": "user:detail",
|
||||
"POST:/api/addUser": "user:add",
|
||||
"POST:/api/editUser/:id": "user:edit",
|
||||
"DELETE:/api/deleteUser/:id": "user:delete",
|
||||
"POST:/api/changePassword/:id":"user:changePassword",
|
||||
|
||||
// 角色管理
|
||||
"GET:/api/roles": "role:list",
|
||||
"POST:/api/roles": "role:create",
|
||||
"GET:/api/roles/:id": "role:detail",
|
||||
"POST:/api/roles/:id": "role:update",
|
||||
"DELETE:/api/roles/:id": "role:delete",
|
||||
|
||||
// 菜单管理
|
||||
"GET:/api/allmenu": "menu:list",
|
||||
"POST:/api/menu": "menu:create",
|
||||
"PUT:/api/menu/:id": "menu:update",
|
||||
"DELETE:/api/menu/:id": "menu:delete",
|
||||
|
||||
// 文件管理
|
||||
"GET:/api/files": "file:list",
|
||||
"POST:/api/files": "file:upload",
|
||||
"GET:/api/files/my": "file:my",
|
||||
"GET:/api/files/download/:id": "file:download",
|
||||
"GET:/api/files/preview/:id": "file:preview",
|
||||
"GET:/api/files/:id": "file:detail",
|
||||
"PUT:/api/files/:id": "file:update",
|
||||
"DELETE:/api/files/:id": "file:delete",
|
||||
"GET:/api/files/search": "file:search",
|
||||
"GET:/api/files/statistics": "file:statistics",
|
||||
|
||||
// 租户管理
|
||||
"GET:/api/tenant/list": "tenant:list",
|
||||
"POST:/api/tenant": "tenant:create",
|
||||
"PUT:/api/tenant/:id": "tenant:update",
|
||||
"DELETE:/api/tenant/:id": "tenant:delete",
|
||||
"POST:/api/tenant/:id/audit": "tenant:audit",
|
||||
"GET:/api/tenant/:id": "tenant:detail",
|
||||
|
||||
// 知识库
|
||||
"GET:/api/knowledge/list": "knowledge:list",
|
||||
"GET:/api/knowledge/detail": "knowledge:detail",
|
||||
"POST:/api/knowledge/create": "knowledge:create",
|
||||
"POST:/api/knowledge/update": "knowledge:update",
|
||||
"POST:/api/knowledge/delete": "knowledge:delete",
|
||||
}
|
||||
|
||||
// 匹配路径(简化版本,不支持动态参数匹配)
|
||||
key := method + ":" + path
|
||||
if perm, ok := permissionMap[key]; ok {
|
||||
return perm
|
||||
}
|
||||
|
||||
// 尝试匹配动态路由(简单的ID参数替换)
|
||||
// 例如:/api/user/123 -> /api/user/:id
|
||||
pathParts := strings.Split(path, "/")
|
||||
for pattern, perm := range permissionMap {
|
||||
parts := strings.Split(pattern, ":")
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
methodPart := parts[0]
|
||||
pathPattern := parts[1]
|
||||
|
||||
if methodPart != method {
|
||||
continue
|
||||
}
|
||||
|
||||
patternParts := strings.Split(pathPattern, "/")
|
||||
if len(patternParts) != len(pathParts) {
|
||||
continue
|
||||
}
|
||||
|
||||
match := true
|
||||
for i, part := range patternParts {
|
||||
if strings.HasPrefix(part, ":") {
|
||||
// 动态参数,跳过
|
||||
continue
|
||||
}
|
||||
if part != pathParts[i] {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if match {
|
||||
return perm
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到匹配的权限标识,返回空字符串(表示不需要权限控制)
|
||||
return ""
|
||||
}
|
||||
|
||||
305
server/models/permission.go
Normal file
305
server/models/permission.go
Normal file
@ -0,0 +1,305 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// RoleMenu 角色-菜单关联表模型
|
||||
type RoleMenu struct {
|
||||
Id int `orm:"auto" json:"id"`
|
||||
RoleId int `orm:"column(role_id)" json:"role_id"`
|
||||
MenuId int `orm:"column(menu_id)" json:"menu_id"`
|
||||
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
|
||||
CreateBy string `orm:"column(create_by);size(50);null" json:"create_by"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (r *RoleMenu) TableName() string {
|
||||
return "yz_role_menus"
|
||||
}
|
||||
|
||||
// RolePermission 角色权限响应结构(包含菜单信息)
|
||||
type RolePermission struct {
|
||||
RoleId int `json:"role_id"`
|
||||
RoleName string `json:"role_name"`
|
||||
MenuIds []int `json:"menu_ids"`
|
||||
Permissions []string `json:"permissions"` // 权限标识列表
|
||||
}
|
||||
|
||||
// MenuPermission 菜单权限信息
|
||||
type MenuPermission struct {
|
||||
MenuId int `json:"menu_id"`
|
||||
MenuName string `json:"menu_name"`
|
||||
Path string `json:"path"`
|
||||
MenuType int `json:"menu_type"` // 1: 页面菜单, 2: API接口
|
||||
Permission string `json:"permission"` // 权限标识
|
||||
ParentId int `json:"parent_id"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
orm.RegisterModel(new(RoleMenu))
|
||||
}
|
||||
|
||||
// GetRoleMenus 获取指定角色的所有菜单权限
|
||||
func GetRoleMenus(roleId int) ([]int, error) {
|
||||
o := orm.NewOrm()
|
||||
var menuIds []int
|
||||
|
||||
_, err := o.Raw("SELECT menu_id FROM yz_role_menus WHERE role_id = ?", roleId).QueryRows(&menuIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取角色菜单失败: %v", err)
|
||||
}
|
||||
|
||||
return menuIds, nil
|
||||
}
|
||||
|
||||
// GetRolePermissions 获取角色的详细权限信息(包括菜单和API权限)
|
||||
func GetRolePermissions(roleId int) (*RolePermission, error) {
|
||||
o := orm.NewOrm()
|
||||
|
||||
// 获取角色信息
|
||||
var role Role
|
||||
err := o.Raw("SELECT * FROM yz_roles WHERE role_id = ? AND delete_time IS NULL", roleId).QueryRow(&role)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("角色不存在: %v", err)
|
||||
}
|
||||
|
||||
// 获取角色关联的所有菜单ID
|
||||
menuIds, err := GetRoleMenus(roleId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取权限标识列表
|
||||
var permissions []string
|
||||
if len(menuIds) > 0 {
|
||||
// 构建IN查询的占位符和参数
|
||||
placeholders := make([]string, len(menuIds))
|
||||
args := make([]interface{}, len(menuIds))
|
||||
for i, id := range menuIds {
|
||||
placeholders[i] = "?"
|
||||
args[i] = id
|
||||
}
|
||||
query := fmt.Sprintf("SELECT DISTINCT permission FROM yz_menus WHERE id IN (%s) AND permission IS NOT NULL AND permission != ''", strings.Join(placeholders, ","))
|
||||
_, err = o.Raw(query, args...).QueryRows(&permissions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取权限标识失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &RolePermission{
|
||||
RoleId: role.RoleId,
|
||||
RoleName: role.RoleName,
|
||||
MenuIds: menuIds,
|
||||
Permissions: permissions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAllMenuPermissions 获取所有菜单权限列表(用于分配权限时展示)
|
||||
func GetAllMenuPermissions() ([]*MenuPermission, error) {
|
||||
o := orm.NewOrm()
|
||||
var menus []*MenuPermission
|
||||
|
||||
_, err := o.Raw("SELECT id as menu_id, name as menu_name, path, menu_type, permission, parent_id FROM yz_menus WHERE status = 1 ORDER BY parent_id, `order`").QueryRows(&menus)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取菜单列表失败: %v", err)
|
||||
}
|
||||
|
||||
return menus, nil
|
||||
}
|
||||
|
||||
// AssignRolePermissions 为角色分配权限(菜单)
|
||||
func AssignRolePermissions(roleId int, menuIds []int, createBy string) error {
|
||||
o := orm.NewOrm()
|
||||
|
||||
// 先删除该角色的所有权限(使用更快的方式)
|
||||
_, err := o.Raw("DELETE FROM yz_role_menus WHERE role_id = ?", roleId).Exec()
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除旧权限失败: %v", err)
|
||||
}
|
||||
|
||||
// 如果没有新权限,直接返回
|
||||
if len(menuIds) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 使用更高效的批量插入方式
|
||||
// 如果数据量太大,分批插入以避免超时
|
||||
batchSize := 500 // 每批500条,MySQL可以高效处理
|
||||
total := len(menuIds)
|
||||
|
||||
for i := 0; i < total; i += batchSize {
|
||||
end := i + batchSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
|
||||
batch := menuIds[i:end]
|
||||
|
||||
// 构建批量INSERT语句
|
||||
query := "INSERT INTO yz_role_menus (role_id, menu_id, create_by) VALUES "
|
||||
values := make([]interface{}, 0, len(batch)*3)
|
||||
|
||||
placeholders := make([]string, 0, len(batch))
|
||||
for _, menuId := range batch {
|
||||
placeholders = append(placeholders, "(?, ?, ?)")
|
||||
values = append(values, roleId, menuId, createBy)
|
||||
}
|
||||
|
||||
query += strings.Join(placeholders, ", ")
|
||||
|
||||
// 执行批量插入
|
||||
_, err = o.Raw(query, values...).Exec()
|
||||
if err != nil {
|
||||
return fmt.Errorf("插入新权限失败(批次 %d/%d): %v", i/batchSize+1, (total+batchSize-1)/batchSize, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserPermissions 获取用户的所有权限(通过用户角色)
|
||||
func GetUserPermissions(userId int) (*RolePermission, error) {
|
||||
o := orm.NewOrm()
|
||||
|
||||
// 获取用户信息
|
||||
var user User
|
||||
err := o.Raw("SELECT * FROM yz_users WHERE id = ? AND delete_time IS NULL", userId).QueryRow(&user)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户不存在: %v", err)
|
||||
}
|
||||
|
||||
// 如果用户没有角色,返回空权限
|
||||
if user.Role == 0 {
|
||||
return &RolePermission{
|
||||
RoleId: 0,
|
||||
RoleName: "无角色",
|
||||
MenuIds: []int{},
|
||||
Permissions: []string{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 获取角色权限
|
||||
return GetRolePermissions(user.Role)
|
||||
}
|
||||
|
||||
// CheckUserPermission 检查用户是否拥有指定权限
|
||||
func CheckUserPermission(userId int, permission string) (bool, error) {
|
||||
if permission == "" {
|
||||
return true, nil // 空权限标识表示不需要权限控制
|
||||
}
|
||||
|
||||
userPerms, err := GetUserPermissions(userId)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 检查权限列表中是否包含指定权限
|
||||
for _, perm := range userPerms.Permissions {
|
||||
if perm == permission {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// MenuTreeNode 菜单树节点(包含子节点)
|
||||
type MenuTreeNode struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
ParentId int `json:"parent_id"`
|
||||
Icon string `json:"icon"`
|
||||
Order int `json:"order"`
|
||||
Status int8 `json:"status"`
|
||||
ComponentPath string `json:"component_path"`
|
||||
IsExternal int8 `json:"is_external"`
|
||||
ExternalUrl string `json:"external_url"`
|
||||
MenuType int8 `json:"menu_type"`
|
||||
Permission string `json:"permission"`
|
||||
Children []*MenuTreeNode `json:"children"`
|
||||
}
|
||||
|
||||
// GetUserMenuTree 获取用户有权限访问的菜单树(仅页面菜单)
|
||||
func GetUserMenuTree(userId int) ([]*MenuTreeNode, error) {
|
||||
o := orm.NewOrm()
|
||||
|
||||
// 获取用户角色
|
||||
var user User
|
||||
err := o.Raw("SELECT * FROM yz_users WHERE id = ? AND delete_time IS NULL", userId).QueryRow(&user)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户不存在: %v", err)
|
||||
}
|
||||
|
||||
if user.Role == 0 {
|
||||
return []*MenuTreeNode{}, nil
|
||||
}
|
||||
|
||||
// 获取角色的菜单ID列表
|
||||
menuIds, err := GetRoleMenus(user.Role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(menuIds) == 0 {
|
||||
return []*MenuTreeNode{}, nil
|
||||
}
|
||||
|
||||
// 获取菜单信息(仅页面菜单)
|
||||
var menus []*Menu
|
||||
// 构建IN查询的占位符和参数
|
||||
placeholders := make([]string, len(menuIds))
|
||||
args := make([]interface{}, len(menuIds))
|
||||
for i, id := range menuIds {
|
||||
placeholders[i] = "?"
|
||||
args[i] = id
|
||||
}
|
||||
query := fmt.Sprintf("SELECT * FROM yz_menus WHERE id IN (%s) AND menu_type = 1 AND status = 1 ORDER BY parent_id, `order`", strings.Join(placeholders, ","))
|
||||
_, err = o.Raw(query, args...).QueryRows(&menus)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取菜单列表失败: %v", err)
|
||||
}
|
||||
|
||||
// 转换为MenuTreeNode
|
||||
var nodes []*MenuTreeNode
|
||||
for _, m := range menus {
|
||||
nodes = append(nodes, &MenuTreeNode{
|
||||
Id: m.Id,
|
||||
Name: m.Name,
|
||||
Path: m.Path,
|
||||
ParentId: m.ParentId,
|
||||
Icon: m.Icon,
|
||||
Order: m.Order,
|
||||
Status: m.Status,
|
||||
ComponentPath: m.ComponentPath,
|
||||
IsExternal: m.IsExternal,
|
||||
ExternalUrl: m.ExternalUrl,
|
||||
MenuType: m.MenuType,
|
||||
Permission: m.Permission,
|
||||
Children: []*MenuTreeNode{},
|
||||
})
|
||||
}
|
||||
|
||||
// 构建菜单树
|
||||
return buildMenuTree(nodes, 0), nil
|
||||
}
|
||||
|
||||
// buildMenuTree 构建菜单树
|
||||
func buildMenuTree(menus []*MenuTreeNode, parentId int) []*MenuTreeNode {
|
||||
var tree []*MenuTreeNode
|
||||
|
||||
for _, menu := range menus {
|
||||
if menu.ParentId == parentId {
|
||||
menu.Children = buildMenuTree(menus, menu.Id)
|
||||
tree = append(tree, menu)
|
||||
}
|
||||
}
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
@ -128,6 +128,14 @@ func init() {
|
||||
beego.Router("/api/roles/:id", &controllers.RoleController{}, "post:UpdateRole")
|
||||
beego.Router("/api/roles/:id", &controllers.RoleController{}, "delete:DeleteRole")
|
||||
|
||||
// 权限管理路由
|
||||
beego.Router("/api/permissions/menus", &controllers.PermissionController{}, "get:GetAllMenuPermissions")
|
||||
beego.Router("/api/permissions/role/:roleId", &controllers.PermissionController{}, "get:GetRolePermissions")
|
||||
beego.Router("/api/permissions/role/:roleId", &controllers.PermissionController{}, "post:AssignRolePermissions")
|
||||
beego.Router("/api/permissions/user", &controllers.PermissionController{}, "get:GetUserPermissions")
|
||||
beego.Router("/api/permissions/user/menus", &controllers.PermissionController{}, "get:GetUserMenuTree")
|
||||
beego.Router("/api/permissions/check", &controllers.PermissionController{}, "get:CheckPermission")
|
||||
|
||||
// 手动配置特殊路由(无法通过自动路由处理的)
|
||||
beego.Router("/api/allmenu", &controllers.MenuController{}, "get:GetAllMenus")
|
||||
beego.Router("/api/program-categories/public", &controllers.ProgramCategoryController{}, "get:GetProgramCategoriesPublic")
|
||||
|
||||
255
权限管理模块使用说明.md
Normal file
255
权限管理模块使用说明.md
Normal file
@ -0,0 +1,255 @@
|
||||
# 权限管理模块使用说明
|
||||
|
||||
## 📋 功能概述
|
||||
|
||||
权限管理模块实现了基于角色的访问控制(RBAC),允许管理员为不同角色分配菜单和API访问权限。
|
||||
|
||||
## 🗄️ 数据库结构
|
||||
|
||||
### 1. 角色菜单关联表 `yz_role_menus`
|
||||
```sql
|
||||
CREATE TABLE yz_role_menus (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
role_id INT NOT NULL, -- 角色ID
|
||||
menu_id INT NOT NULL, -- 菜单ID(包括页面菜单和API接口)
|
||||
create_time DATETIME,
|
||||
create_by VARCHAR(50),
|
||||
UNIQUE KEY uk_role_menu (role_id, menu_id)
|
||||
);
|
||||
```
|
||||
|
||||
### 2. 菜单类型说明
|
||||
- `menu_type = 1`: 页面菜单(显示在导航栏)
|
||||
- `menu_type = 2`: API接口(用于权限控制)
|
||||
|
||||
## 🎯 核心功能
|
||||
|
||||
### 1. 权限分配
|
||||
- 为角色分配菜单访问权限
|
||||
- 为角色分配API访问权限
|
||||
- 支持树形结构的权限选择
|
||||
- 支持批量分配和取消
|
||||
|
||||
### 2. 权限验证
|
||||
- 用户登录后自动加载权限
|
||||
- 菜单根据权限动态显示
|
||||
- API访问根据权限自动拦截
|
||||
|
||||
### 3. 权限查询
|
||||
- 查询角色的所有权限
|
||||
- 查询用户的所有权限
|
||||
- 检查用户是否拥有特定权限
|
||||
|
||||
## 📡 API接口
|
||||
|
||||
### 1. 获取所有菜单权限列表
|
||||
```
|
||||
GET /api/permissions/menus
|
||||
```
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"menu_id": 1,
|
||||
"menu_name": "系统管理",
|
||||
"path": "/system",
|
||||
"menu_type": 1,
|
||||
"permission": null,
|
||||
"parent_id": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 获取角色权限
|
||||
```
|
||||
GET /api/permissions/role/:roleId
|
||||
```
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"role_id": 1,
|
||||
"role_name": "系统管理员",
|
||||
"menu_ids": [1, 2, 3, 4, 5],
|
||||
"permissions": ["user:list", "user:add", "file:upload"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 分配角色权限
|
||||
```
|
||||
POST /api/permissions/role/:roleId
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"menu_ids": [1, 2, 3, 4, 5]
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 获取当前用户权限
|
||||
```
|
||||
GET /api/permissions/user
|
||||
```
|
||||
|
||||
### 5. 获取用户菜单树
|
||||
```
|
||||
GET /api/permissions/user/menus
|
||||
```
|
||||
|
||||
### 6. 检查权限
|
||||
```
|
||||
GET /api/permissions/check?permission=user:list
|
||||
```
|
||||
|
||||
## 🎨 前端使用
|
||||
|
||||
### 页面访问
|
||||
```
|
||||
系统管理 -> 权限管理
|
||||
```
|
||||
|
||||
### 权限分配流程
|
||||
1. 在左侧选择要配置的角色
|
||||
2. 在右侧权限树中勾选该角色应该拥有的菜单和API权限
|
||||
3. 点击"保存权限设置"按钮
|
||||
4. 系统会自动保存并刷新权限
|
||||
|
||||
### 权限树操作
|
||||
- **全部展开**: 展开所有节点
|
||||
- **全部折叠**: 折叠所有节点
|
||||
- **全选**: 选中所有权限
|
||||
- **取消全选**: 取消所有权限选择
|
||||
- **搜索**: 支持按菜单名称、路径、权限标识搜索
|
||||
|
||||
### 权限类型标识
|
||||
- 🔹 **页面**:蓝色标签,表示页面菜单
|
||||
- 🟢 **API**:绿色标签,表示API接口
|
||||
- ℹ️ **权限标识**:灰色标签,显示具体的权限代码
|
||||
|
||||
## 🔒 权限验证中间件(可选)
|
||||
|
||||
已创建权限验证中间件 `server/middleware/permission.go`,但默认未启用。
|
||||
|
||||
### 启用方法
|
||||
在 `server/routers/router.go` 中添加:
|
||||
```go
|
||||
// 在JWT中间件之后添加权限验证中间件
|
||||
beego.InsertFilter("/api/*", beego.BeforeExec, middleware.PermissionMiddleware())
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
- 权限中间件会根据路径和方法自动匹配权限标识
|
||||
- 如果用户没有对应权限,会返回 403 错误
|
||||
- 公开接口(如登录、注册)会自动跳过权限验证
|
||||
|
||||
## 📊 权限标识规范
|
||||
|
||||
### 命名规范
|
||||
格式:`模块:操作`
|
||||
|
||||
### 示例
|
||||
- `user:list` - 查看用户列表
|
||||
- `user:add` - 添加用户
|
||||
- `user:edit` - 编辑用户
|
||||
- `user:delete` - 删除用户
|
||||
- `file:upload` - 上传文件
|
||||
- `file:download` - 下载文件
|
||||
- `role:create` - 创建角色
|
||||
- `menu:update` - 更新菜单
|
||||
|
||||
## 🎯 使用场景
|
||||
|
||||
### 1. 为新角色分配权限
|
||||
```
|
||||
场景:公司新增了"财务专员"角色,需要分配相关权限
|
||||
步骤:
|
||||
1. 在角色管理中创建"财务专员"角色
|
||||
2. 在权限管理中选择"财务专员"
|
||||
3. 勾选需要的权限(如:文件管理、知识库查看)
|
||||
4. 保存权限设置
|
||||
```
|
||||
|
||||
### 2. 调整现有角色权限
|
||||
```
|
||||
场景:需要限制"普通用户"的某些功能
|
||||
步骤:
|
||||
1. 在权限管理中选择"普通用户"角色
|
||||
2. 取消不需要的权限(如:用户管理、系统设置)
|
||||
3. 保存权限设置
|
||||
```
|
||||
|
||||
### 3. 权限审计
|
||||
```
|
||||
场景:查看某个角色有哪些权限
|
||||
步骤:
|
||||
1. 在权限管理中选择目标角色
|
||||
2. 查看已勾选的权限项
|
||||
3. 所有勾选项即为该角色拥有的权限
|
||||
```
|
||||
|
||||
## 🔧 技术实现
|
||||
|
||||
### 后端
|
||||
- **模型层**: `server/models/permission.go`
|
||||
- 角色权限关联
|
||||
- 权限查询和验证
|
||||
- 菜单树构建
|
||||
|
||||
- **控制器层**: `server/controllers/permission.go`
|
||||
- 权限分配接口
|
||||
- 权限查询接口
|
||||
|
||||
- **中间件层**: `server/middleware/permission.go`
|
||||
- API权限验证
|
||||
- 路径-权限映射
|
||||
|
||||
### 前端
|
||||
- **API层**: `pc/src/api/permission.js`
|
||||
- 权限相关接口封装
|
||||
|
||||
- **页面层**: `pc/src/views/system/permissions/index.vue`
|
||||
- 角色列表展示
|
||||
- 权限树展示和操作
|
||||
- 权限保存
|
||||
|
||||
## 📝 注意事项
|
||||
|
||||
1. **权限继承**:子菜单会继承父菜单的权限,建议同时勾选父子节点
|
||||
2. **API权限**:分配页面权限时,也要分配对应的API权限
|
||||
3. **系统管理员**:role_id=1 的系统管理员已默认分配所有权限
|
||||
4. **权限缓存**:修改权限后,用户需要重新登录才能生效(可以实现实时刷新)
|
||||
5. **权限粒度**:当前实现到API级别,可以根据需要扩展到字段级别
|
||||
|
||||
## 🚀 后续优化建议
|
||||
|
||||
1. **权限缓存**: 使用Redis缓存用户权限,提高查询效率
|
||||
2. **实时刷新**: WebSocket推送权限变更,无需重新登录
|
||||
3. **权限日志**: 记录权限变更历史,便于审计
|
||||
4. **数据权限**: 扩展到数据行级权限控制
|
||||
5. **权限模板**: 预定义常用权限组合,快速分配
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
### Q1: 修改权限后不生效?
|
||||
A: 用户需要重新登录,因为权限信息存储在JWT中。可以实现权限实时刷新机制。
|
||||
|
||||
### Q2: 如何给新用户分配权限?
|
||||
A: 在用户管理中为用户分配角色,然后在权限管理中为该角色分配权限。
|
||||
|
||||
### Q3: 权限标识是什么?
|
||||
A: 权限标识是API接口的唯一识别码,格式为"模块:操作",用于权限验证。
|
||||
|
||||
### Q4: 为什么看不到某些菜单?
|
||||
A: 检查该角色是否被分配了对应的菜单权限,同时确保菜单状态为启用。
|
||||
|
||||
### Q5: API接口返回403错误?
|
||||
A: 用户没有访问该接口的权限,需要在权限管理中为用户角色分配对应的API权限。
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
如有问题,请联系系统管理员或技术支持团队。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user