34 KiB
权限系统增强实现计划
面向 AI 代理的工作者: 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(
- [ ])语法来跟踪进度。
目标: 实现完整的权限系统增强,包括 Permission Store、v-permission 指令、动态菜单渲染和 API 权限检查。
架构: 使用 Pinia 统一管理权限数据,localStorage 持久化存储;通过 v-permission 指令实现按钮级权限控制;从后端 API 获取菜单数据动态渲染;在请求拦截器中添加 API 权限检查。
技术栈: Vue 3, Pinia, TypeScript, Element Plus, jwt-decode
文件结构
新增文件
novalon-manage-web/src/
├── stores/
│ └── permission.ts # Permission Store
├── directives/
│ └── permission.ts # v-permission 指令
├── components/
│ └── MenuItem.vue # 递归菜单组件
├── utils/
│ └── permission-check.ts # API 权限检查工具
└── __tests__/
├── stores/
│ └── permission.test.ts # Permission Store 测试
├── directives/
│ └── permission.test.ts # v-permission 指令测试
├── components/
│ └── MenuItem.test.ts # MenuItem 组件测试
└── utils/
└── permission-check.test.ts # API 权限检查测试
修改文件
novalon-manage-web/src/
├── main.ts # 注册权限指令
├── views/system/Login.vue # 集成 Permission Store
├── layouts/DefaultLayout.vue # 使用动态菜单
└── utils/request.ts # 添加 API 权限检查
任务 1:创建 Permission Store
文件:
-
创建:
novalon-manage-web/src/stores/permission.ts -
测试:
novalon-manage-web/src/__tests__/stores/permission.test.ts -
步骤 1:编写 Permission Store 测试 - 基础功能
// novalon-manage-web/src/__tests__/stores/permission.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { usePermissionStore } from '@/stores/permission'
describe('Permission Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
localStorage.clear()
})
describe('基础功能', () => {
it('应该正确初始化状态', () => {
const store = usePermissionStore()
expect(store.roles).toEqual([])
expect(store.permissions).toEqual([])
expect(store.menus).toEqual([])
expect(store.loaded).toBe(false)
})
it('应该正确设置权限数据', () => {
const store = usePermissionStore()
store.setPermissionData({
roles: ['admin'],
permissions: ['user:read', 'user:delete'],
menus: [
{
id: 1,
name: '仪表盘',
path: '/dashboard',
icon: 'Odometer',
sort: 1
}
]
})
expect(store.roles).toEqual(['admin'])
expect(store.permissions).toEqual(['user:read', 'user:delete'])
expect(store.menus).toHaveLength(1)
expect(store.loaded).toBe(true)
})
it('应该正确清除权限数据', () => {
const store = usePermissionStore()
store.setPermissionData({
roles: ['admin'],
permissions: ['user:read'],
menus: []
})
store.clearPermissionData()
expect(store.roles).toEqual([])
expect(store.permissions).toEqual([])
expect(store.menus).toEqual([])
expect(store.loaded).toBe(false)
})
})
})
- 步骤 2:运行测试验证失败
运行:cd novalon-manage-web && pnpm test src/__tests__/stores/permission.test.ts
预期:FAIL,报错 "Cannot find module '@/stores/permission'"
- 步骤 3:创建 Permission Store 基础结构
// novalon-manage-web/src/stores/permission.ts
import { defineStore } from 'pinia'
export interface MenuItem {
id: number
name: string
path: string
icon?: string
parentId?: number
sort: number
children?: MenuItem[]
}
interface PermissionState {
roles: string[]
permissions: string[]
menus: MenuItem[]
loaded: boolean
}
export const usePermissionStore = defineStore('permission', {
state: (): PermissionState => ({
roles: [],
permissions: [],
menus: [],
loaded: false
}),
actions: {
setPermissionData(data: {
roles: string[]
permissions: string[]
menus: MenuItem[]
}) {
this.roles = data.roles
this.permissions = data.permissions
this.menus = data.menus
this.loaded = true
this.saveToStorage()
},
clearPermissionData() {
this.roles = []
this.permissions = []
this.menus = []
this.loaded = false
localStorage.removeItem('permission')
},
saveToStorage() {
const data = {
roles: this.roles,
permissions: this.permissions,
menus: this.menus
}
localStorage.setItem('permission', JSON.stringify(data))
},
initFromStorage() {
const stored = localStorage.getItem('permission')
if (stored) {
try {
const data = JSON.parse(stored)
this.roles = data.roles || []
this.permissions = data.permissions || []
this.menus = data.menus || []
this.loaded = true
} catch (error) {
console.error('从 localStorage 恢复权限数据失败:', error)
}
}
}
}
})
- 步骤 4:运行测试验证通过
运行:cd novalon-manage-web && pnpm test src/__tests__/stores/permission.test.ts
预期:PASS
- 步骤 5:编写 Permission Store 测试 - 权限检查方法
// 在 novalon-manage-web/src/__tests__/stores/permission.test.ts 中添加
describe('权限检查方法', () => {
it('应该正确检查单个角色', () => {
const store = usePermissionStore()
store.setPermissionData({
roles: ['admin', 'user'],
permissions: [],
menus: []
})
expect(store.hasRole('admin')).toBe(true)
expect(store.hasRole('manager')).toBe(false)
})
it('应该正确检查多个角色(满足任一即可)', () => {
const store = usePermissionStore()
store.setPermissionData({
roles: ['user'],
permissions: [],
menus: []
})
expect(store.hasRole(['admin', 'user'])).toBe(true)
expect(store.hasRole(['admin', 'manager'])).toBe(false)
})
it('应该正确检查单个权限', () => {
const store = usePermissionStore()
store.setPermissionData({
roles: [],
permissions: ['user:read', 'user:delete'],
menus: []
})
expect(store.hasPermission('user:read')).toBe(true)
expect(store.hasPermission('user:create')).toBe(false)
})
it('应该正确检查多个权限(满足任一即可)', () => {
const store = usePermissionStore()
store.setPermissionData({
roles: [],
permissions: ['user:read'],
menus: []
})
expect(store.hasPermission(['user:read', 'user:create'])).toBe(true)
expect(store.hasPermission(['user:create', 'user:update'])).toBe(false)
})
})
- 步骤 6:运行测试验证失败
运行:cd novalon-manage-web && pnpm test src/__tests__/stores/permission.test.ts
预期:FAIL,报错 "store.hasRole is not a function"
- 步骤 7:添加权限检查方法
// 在 novalon-manage-web/src/stores/permission.ts 中添加 getters
export const usePermissionStore = defineStore('permission', {
state: (): PermissionState => ({
roles: [],
permissions: [],
menus: [],
loaded: false
}),
getters: {
hasRole: (state) => (role: string | string[]) => {
if (Array.isArray(role)) {
return role.some(r => state.roles.includes(r))
}
return state.roles.includes(role)
},
hasPermission: (state) => (permission: string | string[]) => {
if (Array.isArray(permission)) {
return permission.some(p => state.permissions.includes(p))
}
return state.permissions.includes(permission)
}
},
actions: {
// ... 已有的 actions
}
})
- 步骤 8:运行测试验证通过
运行:cd novalon-manage-web && pnpm test src/__tests__/stores/permission.test.ts
预期:PASS
- 步骤 9:编写 Permission Store 测试 - localStorage 持久化
// 在 novalon-manage-web/src/__tests__/stores/permission.test.ts 中添加
describe('localStorage 持久化', () => {
it('应该正确保存到 localStorage', () => {
const store = usePermissionStore()
store.setPermissionData({
roles: ['admin'],
permissions: ['user:read'],
menus: [
{
id: 1,
name: '仪表盘',
path: '/dashboard',
sort: 1
}
]
})
const stored = localStorage.getItem('permission')
expect(stored).toBeTruthy()
const data = JSON.parse(stored!)
expect(data.roles).toEqual(['admin'])
expect(data.permissions).toEqual(['user:read'])
expect(data.menus).toHaveLength(1)
})
it('应该正确从 localStorage 恢复', () => {
localStorage.setItem('permission', JSON.stringify({
roles: ['user'],
permissions: ['user:read:self'],
menus: []
}))
const store = usePermissionStore()
store.initFromStorage()
expect(store.roles).toEqual(['user'])
expect(store.permissions).toEqual(['user:read:self'])
expect(store.loaded).toBe(true)
})
it('清除数据时应该同时清除 localStorage', () => {
const store = usePermissionStore()
store.setPermissionData({
roles: ['admin'],
permissions: [],
menus: []
})
store.clearPermissionData()
expect(localStorage.getItem('permission')).toBeNull()
})
})
- 步骤 10:运行测试验证通过
运行:cd novalon-manage-web && pnpm test src/__tests__/stores/permission.test.ts
预期:PASS
- 步骤 11:Commit
cd novalon-manage-web
git add src/stores/permission.ts src/__tests__/stores/permission.test.ts
git commit -m "feat: 添加 Permission Store 实现权限数据管理"
任务 2:创建 v-permission 指令
文件:
-
创建:
novalon-manage-web/src/directives/permission.ts -
测试:
novalon-manage-web/src/__tests__/directives/permission.test.ts -
步骤 1:编写 v-permission 指令测试 - 角色检查
// novalon-manage-web/src/__tests__/directives/permission.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { permissionDirective } from '@/directives/permission'
import { usePermissionStore } from '@/stores/permission'
describe('v-permission 指令', () => {
beforeEach(() => {
setActivePinia(createPinia())
localStorage.clear()
})
describe('角色检查', () => {
it('有角色时应该显示元素', () => {
const store = usePermissionStore()
store.setPermissionData({
roles: ['admin'],
permissions: [],
menus: []
})
const wrapper = mount({
template: '<button v-permission:role="\'admin\'">管理员按钮</button>',
directives: {
permission: permissionDirective
}
})
expect(wrapper.find('button').isVisible()).toBe(true)
})
it('无角色时应该隐藏元素', () => {
const store = usePermissionStore()
store.setPermissionData({
roles: ['user'],
permissions: [],
menus: []
})
const wrapper = mount({
template: '<button v-permission:role="\'admin\'">管理员按钮</button>',
directives: {
permission: permissionDirective
}
})
expect(wrapper.find('button').isVisible()).toBe(false)
})
it('支持数组参数(满足任一即可)', () => {
const store = usePermissionStore()
store.setPermissionData({
roles: ['user'],
permissions: [],
menus: []
})
const wrapper = mount({
template: '<button v-permission:role="[\'admin\', \'user\']">按钮</button>',
directives: {
permission: permissionDirective
}
})
expect(wrapper.find('button').isVisible()).toBe(true)
})
})
})
- 步骤 2:运行测试验证失败
运行:cd novalon-manage-web && pnpm test src/__tests__/directives/permission.test.ts
预期:FAIL,报错 "Cannot find module '@/directives/permission'"
- 步骤 3:创建 v-permission 指令基础结构
// novalon-manage-web/src/directives/permission.ts
import type { Directive, DirectiveBinding } from 'vue'
import { usePermissionStore } from '@/stores/permission'
export const permissionDirective: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const permissionStore = usePermissionStore()
const { arg, value } = binding
const checkType = arg || 'permission'
if (!value) {
console.warn('v-permission 指令需要提供权限值')
el.style.display = 'none'
return
}
let hasAccess = false
if (checkType === 'role') {
hasAccess = permissionStore.hasRole(value)
} else if (checkType === 'permission') {
hasAccess = permissionStore.hasPermission(value)
} else {
console.warn(`未知的权限检查类型: ${checkType}`)
el.style.display = 'none'
return
}
if (!hasAccess) {
el.style.display = 'none'
}
}
}
- 步骤 4:运行测试验证通过
运行:cd novalon-manage-web && pnpm test src/__tests__/directives/permission.test.ts
预期:PASS
- 步骤 5:编写 v-permission 指令测试 - 权限检查
// 在 novalon-manage-web/src/__tests__/directives/permission.test.ts 中添加
describe('权限检查', () => {
it('有权限时应该显示元素', () => {
const store = usePermissionStore()
store.setPermissionData({
roles: [],
permissions: ['user:delete'],
menus: []
})
const wrapper = mount({
template: '<button v-permission:permission="\'user:delete\'">删除用户</button>',
directives: {
permission: permissionDirective
}
})
expect(wrapper.find('button').isVisible()).toBe(true)
})
it('无权限时应该隐藏元素', () => {
const store = usePermissionStore()
store.setPermissionData({
roles: [],
permissions: ['user:read'],
menus: []
})
const wrapper = mount({
template: '<button v-permission:permission="\'user:delete\'">删除用户</button>',
directives: {
permission: permissionDirective
}
})
expect(wrapper.find('button').isVisible()).toBe(false)
})
it('支持简写形式(默认权限检查)', () => {
const store = usePermissionStore()
store.setPermissionData({
roles: [],
permissions: ['user:create'],
menus: []
})
const wrapper = mount({
template: '<button v-permission="\'user:create\'">创建用户</button>',
directives: {
permission: permissionDirective
}
})
expect(wrapper.find('button').isVisible()).toBe(true)
})
})
- 步骤 6:运行测试验证通过
运行:cd novalon-manage-web && pnpm test src/__tests__/directives/permission.test.ts
预期:PASS
- 步骤 7:在 main.ts 中注册指令
// novalon-manage-web/src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css'
import router from './router'
import App from './App.vue'
import './assets/styles.css'
import { permissionDirective } from './directives/permission'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.use(ElementPlus, {
locale: zhCn,
})
app.directive('permission', permissionDirective)
app.mount('#app')
- 步骤 8:Commit
cd novalon-manage-web
git add src/directives/permission.ts src/__tests__/directives/permission.test.ts src/main.ts
git commit -m "feat: 添加 v-permission 指令实现按钮级权限控制"
任务 3:集成 Permission Store 到登录流程
文件:
-
修改:
novalon-manage-web/src/views/system/Login.vue -
步骤 1:修改 Login.vue 集成 Permission Store
// novalon-manage-web/src/views/system/Login.vue
// 在 <script setup lang="ts"> 部分修改
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import request from '@/utils/request'
import { onMounted } from 'vue'
import { jwtDecode } from 'jwt-decode'
import { usePermissionStore } from '@/stores/permission'
const router = useRouter()
const loading = ref(false)
const permissionStore = usePermissionStore()
const formState = reactive({
username: '',
password: ''
})
onMounted(() => {
document.title = '登录 - Novalon 管理系统'
})
interface JwtPayload {
userId: number
username: string
roles: string[]
exp: number
iat: number
}
const onFinish = async () => {
loading.value = true
try {
const res: any = await request.post('/auth/login', formState)
if (!res || !res.token) {
ElMessage.error('登录失败:未收到有效响应')
return
}
localStorage.setItem('token', res.token)
if (res.userId) {
localStorage.setItem('userId', String(res.userId))
}
if (res.username) {
localStorage.setItem('username', res.username)
}
try {
const decoded = jwtDecode<JwtPayload>(res.token)
if (decoded.roles && Array.isArray(decoded.roles)) {
localStorage.setItem('roles', JSON.stringify(decoded.roles))
}
} catch (decodeError) {
console.warn('解析Token中的角色信息失败:', decodeError)
}
try {
await permissionStore.fetchUserMenus()
} catch (fetchError) {
console.warn('获取用户菜单失败:', fetchError)
}
ElMessage.success('登录成功')
await router.push('/')
} catch (error: any) {
console.error('登录错误:', error)
ElMessage.error(error.response?.data?.message || error.message || '登录失败')
} finally {
loading.value = false
}
}
- 步骤 2:在 Permission Store 中添加 fetchUserMenus 方法
// 在 novalon-manage-web/src/stores/permission.ts 中添加
import request from '@/utils/request'
export const usePermissionStore = defineStore('permission', {
// ... 已有的 state, getters
actions: {
// ... 已有的 actions
async fetchUserMenus() {
try {
const res: any = await request.get('/menus/user')
if (res && res.data) {
this.setPermissionData({
roles: JSON.parse(localStorage.getItem('roles') || '[]'),
permissions: res.data.permissions || [],
menus: res.data.menus || []
})
}
} catch (error) {
console.error('获取用户菜单失败:', error)
throw error
}
}
}
})
- 步骤 3:Commit
cd novalon-manage-web
git add src/views/system/Login.vue src/stores/permission.ts
git commit -m "feat: 集成 Permission Store 到登录流程"
任务 4:创建递归菜单组件
文件:
-
创建:
novalon-manage-web/src/components/MenuItem.vue -
测试:
novalon-manage-web/src/__tests__/components/MenuItem.test.ts -
步骤 1:编写 MenuItem 组件测试
// novalon-manage-web/src/__tests__/components/MenuItem.test.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import MenuItem from '@/components/MenuItem.vue'
describe('MenuItem 组件', () => {
it('应该正确渲染无子菜单的菜单项', () => {
const menu = {
id: 1,
name: '仪表盘',
path: '/dashboard',
icon: 'Odometer',
sort: 1
}
const wrapper = mount(MenuItem, {
props: { menu }
})
expect(wrapper.find('.el-menu-item').exists()).toBe(true)
expect(wrapper.text()).toContain('仪表盘')
})
it('应该正确渲染有子菜单的菜单项', () => {
const menu = {
id: 2,
name: '系统管理',
path: '/system',
icon: 'Setting',
sort: 2,
children: [
{
id: 3,
name: '用户管理',
path: '/users',
sort: 1
}
]
}
const wrapper = mount(MenuItem, {
props: { menu }
})
expect(wrapper.find('.el-sub-menu').exists()).toBe(true)
expect(wrapper.text()).toContain('系统管理')
expect(wrapper.text()).toContain('用户管理')
})
})
- 步骤 2:运行测试验证失败
运行:cd novalon-manage-web && pnpm test src/__tests__/components/MenuItem.test.ts
预期:FAIL,报错 "Cannot find module '@/components/MenuItem.vue'"
- 步骤 3:创建 MenuItem 组件
<!-- novalon-manage-web/src/components/MenuItem.vue -->
<template>
<el-sub-menu
v-if="menu.children && menu.children.length > 0"
:index="String(menu.id)"
>
<template #title>
<el-icon v-if="menu.icon">
<component :is="menu.icon" />
</el-icon>
<span>{{ menu.name }}</span>
</template>
<menu-item
v-for="child in menu.children"
:key="child.id"
:menu="child"
/>
</el-sub-menu>
<el-menu-item
v-else
:index="menu.path"
>
<el-icon v-if="menu.icon">
<component :is="menu.icon" />
</el-icon>
<span>{{ menu.name }}</span>
</el-menu-item>
</template>
<script setup lang="ts">
import type { MenuItem as MenuItemType } from '@/stores/permission'
defineProps<{
menu: MenuItemType
}>()
</script>
- 步骤 4:运行测试验证通过
运行:cd novalon-manage-web && pnpm test src/__tests__/components/MenuItem.test.ts
预期:PASS
- 步骤 5:Commit
cd novalon-manage-web
git add src/components/MenuItem.vue src/__tests__/components/MenuItem.test.ts
git commit -m "feat: 添加递归菜单组件 MenuItem"
任务 5:修改 DefaultLayout 使用动态菜单
文件:
-
修改:
novalon-manage-web/src/layouts/DefaultLayout.vue -
步骤 1:修改 DefaultLayout.vue
<!-- novalon-manage-web/src/layouts/DefaultLayout.vue -->
<template>
<el-container class="default-layout">
<el-aside
:width="collapsed ? '64px' : '200px'"
class="aside"
>
<div class="logo">
<span v-if="!collapsed">Novalon</span>
<span v-else>N</span>
</div>
<el-menu
:default-active="activeMenu"
class="menu"
:collapse="collapsed"
background-color="#f5f7fa"
text-color="#606266"
active-text-color="#409eff"
router
>
<menu-item
v-for="menu in menuTree"
:key="menu.id"
:menu="menu"
/>
</el-menu>
</el-aside>
<el-container>
<el-header class="header">
<el-icon
class="trigger"
@click="collapsed = !collapsed"
>
<Fold v-if="!collapsed" />
<Expand v-else />
</el-icon>
<div class="header-right">
<el-dropdown @command="handleCommand">
<el-avatar :size="32">
{{ username }}
</el-avatar>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">
个人中心
</el-dropdown-item>
<el-dropdown-item
command="logout"
divided
>
退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<el-main class="content">
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { Fold, Expand } from '@element-plus/icons-vue'
import { usePermissionStore } from '@/stores/permission'
import MenuItem from '@/components/MenuItem.vue'
const router = useRouter()
const route = useRoute()
const collapsed = ref(false)
const username = ref(localStorage.getItem('username') || 'Admin')
const permissionStore = usePermissionStore()
const activeMenu = computed(() => route.path)
const menuTree = computed(() => permissionStore.menus)
const handleCommand = (command: string) => {
if (command === 'profile') {
router.push('/profile')
} else if (command === 'logout') {
permissionStore.clearPermissionData()
localStorage.clear()
router.push('/login')
}
}
onMounted(() => {
const token = localStorage.getItem('token')
if (!token) {
router.push('/login')
} else if (!permissionStore.loaded) {
permissionStore.initFromStorage()
}
})
</script>
<style scoped lang="css">
.default-layout {
min-height: 100vh;
}
.aside {
background-color: #f5f7fa;
transition: width 0.3s;
overflow: hidden;
}
.logo {
height: 64px;
display: flex;
align-items: center;
justify-content: center;
color: #303133;
font-size: 20px;
font-weight: bold;
}
.menu {
border-right: none;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px;
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
.trigger {
font-size: 18px;
cursor: pointer;
transition: color 0.3s;
&:hover { color: #409eff; }
}
.header-right {
display: flex;
align-items: center;
}
}
.content {
margin: 16px;
padding: 16px;
background: #fff;
min-height: calc(100vh - 96px);
}
</style>
- 步骤 2:Commit
cd novalon-manage-web
git add src/layouts/DefaultLayout.vue
git commit -m "feat: 修改 DefaultLayout 使用动态菜单渲染"
任务 6:创建 API 权限检查工具
文件:
-
创建:
novalon-manage-web/src/utils/permission-check.ts -
测试:
novalon-manage-web/src/__tests__/utils/permission-check.test.ts -
步骤 1:编写 API 权限检查测试
// novalon-manage-web/src/__tests__/utils/permission-check.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { usePermissionStore } from '@/stores/permission'
import { canAccessApi } from '@/utils/permission-check'
describe('API 权限检查', () => {
beforeEach(() => {
setActivePinia(createPinia())
localStorage.clear()
})
it('有权限时应该返回 true', () => {
const store = usePermissionStore()
store.setPermissionData({
roles: [],
permissions: ['user:read'],
menus: []
})
expect(canAccessApi('/api/users', 'GET')).toBe(true)
})
it('无权限时应该返回 false', () => {
const store = usePermissionStore()
store.setPermissionData({
roles: [],
permissions: ['user:read'],
menus: []
})
expect(canAccessApi('/api/users', 'POST')).toBe(false)
})
it('未定义权限要求的 API 应该默认允许', () => {
const store = usePermissionStore()
store.setPermissionData({
roles: [],
permissions: [],
menus: []
})
expect(canAccessApi('/api/unknown', 'GET')).toBe(true)
})
it('应该正确匹配通配符路径', () => {
const store = usePermissionStore()
store.setPermissionData({
roles: [],
permissions: ['user:update'],
menus: []
})
expect(canAccessApi('/api/users/123', 'PUT')).toBe(true)
})
})
- 步骤 2:运行测试验证失败
运行:cd novalon-manage-web && pnpm test src/__tests__/utils/permission-check.test.ts
预期:FAIL,报错 "Cannot find module '@/utils/permission-check'"
- 步骤 3:创建 API 权限检查工具
// novalon-manage-web/src/utils/permission-check.ts
import { usePermissionStore } from '@/stores/permission'
const apiPermissionMap: Record<string, string> = {
'/api/users:GET': 'user:read',
'/api/users:POST': 'user:create',
'/api/users/*:PUT': 'user:update',
'/api/users/*:DELETE': 'user:delete',
'/api/roles:GET': 'role:read',
'/api/roles:POST': 'role:create',
'/api/roles/*:PUT': 'role:update',
'/api/roles/*:DELETE': 'role:delete',
'/api/menus:GET': 'menu:read',
'/api/menus:POST': 'menu:create',
'/api/menus/*:PUT': 'menu:update',
'/api/menus/*:DELETE': 'menu:delete',
'/api/config:GET': 'config:read',
'/api/config:POST': 'config:create',
'/api/config/*:PUT': 'config:update',
'/api/config/*:DELETE': 'config:delete',
'/api/dict:GET': 'dict:read',
'/api/dict:POST': 'dict:create',
'/api/dict/*:PUT': 'dict:update',
'/api/dict/*:DELETE': 'dict:delete',
'/api/files:GET': 'file:read',
'/api/files:POST': 'file:create',
'/api/files/*:DELETE': 'file:delete',
'/api/notices:GET': 'notice:read',
'/api/notices:POST': 'notice:create',
'/api/notices/*:PUT': 'notice:update',
'/api/notices/*:DELETE': 'notice:delete',
'/api/logs/login:GET': 'log:read',
'/api/logs/operation:GET': 'log:read',
'/api/logs/exception:GET': 'log:read'
}
function findRequiredPermission(path: string, method: string): string | null {
const exactKey = `${path}:${method}`
if (apiPermissionMap[exactKey]) {
return apiPermissionMap[exactKey]
}
for (const [key, permission] of Object.entries(apiPermissionMap)) {
const [pattern, reqMethod] = key.split(':')
if (reqMethod !== method) continue
const regex = new RegExp('^' + pattern.replace('*', '.*') + '$')
if (regex.test(path)) {
return permission
}
}
return null
}
export function canAccessApi(path: string, method: string): boolean {
const permissionStore = usePermissionStore()
const required = findRequiredPermission(path, method)
if (!required) {
return true
}
return permissionStore.hasPermission(required)
}
- 步骤 4:运行测试验证通过
运行:cd novalon-manage-web && pnpm test src/__tests__/utils/permission-check.test.ts
预期:PASS
- 步骤 5:Commit
cd novalon-manage-web
git add src/utils/permission-check.ts src/__tests__/utils/permission-check.test.ts
git commit -m "feat: 添加 API 权限检查工具"
任务 7:集成 API 权限检查到请求拦截器
文件:
-
修改:
novalon-manage-web/src/utils/request.ts -
步骤 1:修改 request.ts 添加权限检查
// novalon-manage-web/src/utils/request.ts
import axios, { AxiosRequestConfig } from 'axios'
import { generateSignatureHeaders } from './signature'
import { canAccessApi } from './permission-check'
const request = axios.create({
baseURL: '/api',
timeout: 10000
})
request.interceptors.request.use(
(config: AxiosRequestConfig) => {
const path = config.url || ''
const method = config.method?.toUpperCase() || 'GET'
if (!canAccessApi(path, method)) {
console.warn(`无权限访问 API: ${method} ${path}`)
return Promise.reject(new Error(`无权限访问此 API: ${method} ${path}`))
}
const token = localStorage.getItem('token')
if (token) {
config.headers = config.headers || {}
config.headers.Authorization = `Bearer ${token}`
}
const methodForSignature = config.method?.toUpperCase() || 'GET'
let url = config.url || ''
const body = config.data
if (config.params && Object.keys(config.params).length > 0) {
const queryParams = new URLSearchParams()
Object.entries(config.params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
queryParams.append(key, String(value))
}
})
const queryString = queryParams.toString()
if (queryString) {
url += (url.includes('?') ? '&' : '?') + queryString
}
}
const fullPath = `/api${url.startsWith('/') ? url : '/' + url}`
const signatureHeaders = generateSignatureHeaders(methodForSignature, fullPath, body)
config.headers = config.headers || {}
Object.assign(config.headers, signatureHeaders)
return config
},
(error) => Promise.reject(error)
)
request.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('token')
if (!window.location.pathname.includes('/login')) {
window.location.href = '/login'
}
}
return Promise.reject(error)
}
)
export default request
- 步骤 2:Commit
cd novalon-manage-web
git add src/utils/request.ts
git commit -m "feat: 集成 API 权限检查到请求拦截器"
任务 8:运行完整测试套件
- 步骤 1:运行所有单元测试
运行:cd novalon-manage-web && pnpm test
预期:所有测试通过
- 步骤 2:运行 TypeScript 类型检查
运行:cd novalon-manage-web && pnpm type-check
预期:类型检查通过(忽略已存在的其他文件错误)
- 步骤 3:最终 Commit
cd novalon-manage-web
git add .
git commit -m "feat: 完成权限系统增强功能实现"
后端 API 实现说明
本计划需要后端新增以下 API:
接口: GET /api/menus/user
功能: 获取当前登录用户可访问的菜单和权限
实现要点:
- 从 token 获取用户 ID
- 查询用户角色
- 根据角色查询菜单和权限
- 构建菜单树结构
- 返回菜单和权限列表
响应格式:
{
"code": 200,
"data": {
"menus": [
{
"id": 1,
"name": "仪表盘",
"path": "/dashboard",
"icon": "Odometer",
"parentId": null,
"sort": 1
}
],
"permissions": [
"user:read",
"user:create"
]
}
}
后端实现不在本计划范围内,需要单独开发。