all in
This commit is contained in:
@@ -0,0 +1,532 @@
|
||||
# RBAC 路由权限 API 响应示例 v3.3
|
||||
|
||||
## 📋 文档说明
|
||||
|
||||
本文档展示 v3.3 版本 RBAC 路由权限接口的完整响应格式。
|
||||
|
||||
**v3.3 核心变更**:
|
||||
- ✅ 层级权限控制:市级管理员可查看admin + common角色
|
||||
- ✅ 地区数据隔离:市级管理员只能查看/操作同地区用户
|
||||
- ✅ 使用 `status` 字段管理路由启用/禁用状态
|
||||
- ✅ 取消勾选后路由不删除,只是设置为 status=0
|
||||
- ✅ 接口返回 `enabled` 字段标识路由是否启用
|
||||
|
||||
---
|
||||
|
||||
## 1️⃣ GET /rbac/roles/{role_id}/routes
|
||||
|
||||
### 功能说明
|
||||
获取角色关联的所有路由(包括禁用的),每个路由带 `enabled` 字段
|
||||
|
||||
### 请求示例
|
||||
```bash
|
||||
GET /rbac/roles/1/routes
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### 响应示例
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"role_id": 1,
|
||||
"routes": [
|
||||
{
|
||||
"id": 31,
|
||||
"route_path": "/home",
|
||||
"route_name": "Home",
|
||||
"route_title": "系统概览",
|
||||
"parent_id": null,
|
||||
"icon": "ri-dashboard-line",
|
||||
"sort_order": 1,
|
||||
"is_hidden": false,
|
||||
"component": "views/Home.vue",
|
||||
"enabled": true,
|
||||
"permissions": [
|
||||
{
|
||||
"id": 10,
|
||||
"permission_key": "dashboard:stats:view",
|
||||
"display_name": "查看统计数据",
|
||||
"api_method": "GET",
|
||||
"api_path": "/api/v3/dashboard/stats"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"route_path": "/documents",
|
||||
"route_name": "Documents",
|
||||
"route_title": "文件管理",
|
||||
"parent_id": null,
|
||||
"icon": "ri-folder-3-line",
|
||||
"sort_order": 2,
|
||||
"is_hidden": false,
|
||||
"component": "views/documents/Index.vue",
|
||||
"enabled": false,
|
||||
"permissions": [
|
||||
{
|
||||
"id": 20,
|
||||
"permission_key": "document:list:read",
|
||||
"display_name": "查看文档列表",
|
||||
"api_method": "GET",
|
||||
"api_path": "/api/v2/documents"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"id": 59,
|
||||
"route_path": "/files/upload",
|
||||
"route_name": "FilesUpload",
|
||||
"route_title": "文件上传",
|
||||
"parent_id": 2,
|
||||
"enabled": false,
|
||||
"permissions": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"route_path": "/rules",
|
||||
"route_name": "Rules",
|
||||
"route_title": "评查规则库",
|
||||
"parent_id": null,
|
||||
"icon": "ri-book-3-line",
|
||||
"sort_order": 3,
|
||||
"is_hidden": false,
|
||||
"component": "views/rules/Index.vue",
|
||||
"enabled": true,
|
||||
"permissions": [
|
||||
{
|
||||
"id": 28,
|
||||
"permission_key": "evaluation_group:list:read",
|
||||
"display_name": "查看评查点分组列表",
|
||||
"api_method": "GET",
|
||||
"api_path": "/api/v3/evaluation-point-groups"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `enabled` | boolean | **true=启用(前端显示为勾选)**, **false=禁用(前端显示为未勾选)** |
|
||||
| `permissions` | array | 路由关联的API权限列表 |
|
||||
|
||||
### 前端使用示例
|
||||
|
||||
```javascript
|
||||
// 获取角色路由
|
||||
const response = await fetch('/rbac/roles/1/routes')
|
||||
const { routes } = response.data
|
||||
|
||||
// 提取启用的路由ID(用于Tree组件初始勾选)
|
||||
const checkedKeys = []
|
||||
const extractEnabled = (routeList) => {
|
||||
routeList.forEach(route => {
|
||||
if (route.enabled) {
|
||||
checkedKeys.push(route.id)
|
||||
}
|
||||
if (route.children) {
|
||||
extractEnabled(route.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
extractEnabled(routes)
|
||||
|
||||
// 设置Tree组件默认勾选
|
||||
treeRef.value.setCheckedKeys(checkedKeys)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2️⃣ PUT /rbac/roles/{role_id}/routes
|
||||
|
||||
### 功能说明
|
||||
批量更新角色路由权限(状态管理模式)
|
||||
- route_ids 中的路由设置为启用(status=1)
|
||||
- 不在 route_ids 中的路由设置为禁用(status=0)
|
||||
- **仅省级管理员可调用**
|
||||
|
||||
### 请求示例
|
||||
```bash
|
||||
PUT /rbac/roles/1/routes
|
||||
Authorization: Bearer <provincial_admin_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"route_ids": [31, 41],
|
||||
"permission": "RW"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应示例(成功)
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"role_id": 1,
|
||||
"enabled_count": 2,
|
||||
"disabled_count": 1,
|
||||
"inserted_count": 0,
|
||||
"route_ids": [31, 41]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `enabled_count` | int | 本次启用的路由数量 |
|
||||
| `disabled_count` | int | 本次禁用的路由数量 |
|
||||
| `inserted_count` | int | 本次新插入的路由数量 |
|
||||
| `route_ids` | array | 启用的路由ID列表 |
|
||||
|
||||
### 响应示例(权限不足)
|
||||
```json
|
||||
{
|
||||
"code": 4003,
|
||||
"msg": "权限不足:仅省级管理员可以修改角色路由权限",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 前端使用示例
|
||||
|
||||
```javascript
|
||||
// 保存路由权限
|
||||
const saveRoutePermissions = async (roleId) => {
|
||||
const checkedKeys = treeRef.value.getCheckedKeys()
|
||||
|
||||
try {
|
||||
const response = await fetch(`/rbac/roles/${roleId}/routes`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
route_ids: checkedKeys,
|
||||
permission: 'RW'
|
||||
})
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.code === 200) {
|
||||
ElMessage.success(
|
||||
`成功启用 ${result.data.enabled_count} 个路由,` +
|
||||
`禁用 ${result.data.disabled_count} 个路由`
|
||||
)
|
||||
|
||||
// 刷新路由列表以更新enabled状态
|
||||
await loadRoutes()
|
||||
} else if (result.code === 4003) {
|
||||
ElMessage.error('权限不足:仅省级管理员可以修改路由权限')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败:' + error.message)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3️⃣ 完整工作流程示例
|
||||
|
||||
### Vue 3 + Element Plus 完整组件
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="role-route-manager">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>角色路由权限管理 - {{ roleName }}</span>
|
||||
<el-tag v-if="!isProvincialAdmin" type="warning">
|
||||
仅查看模式(需要省级管理员权限)
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="routes"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:default-checked-keys="initialCheckedKeys"
|
||||
:props="{ label: 'route_title', children: 'children' }"
|
||||
:disabled="!isProvincialAdmin"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<span class="tree-node">
|
||||
<i :class="data.icon" style="margin-right: 8px"></i>
|
||||
<span>{{ data.route_title }}</span>
|
||||
<el-tag
|
||||
v-if="data.enabled"
|
||||
type="success"
|
||||
size="small"
|
||||
style="margin-left: 8px"
|
||||
>
|
||||
已启用
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-else
|
||||
type="info"
|
||||
size="small"
|
||||
style="margin-left: 8px"
|
||||
>
|
||||
已禁用
|
||||
</el-tag>
|
||||
<span
|
||||
v-if="data.permissions && data.permissions.length > 0"
|
||||
style="margin-left: 8px; color: #909399; font-size: 12px"
|
||||
>
|
||||
({{ data.permissions.length }}个权限)
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
|
||||
<div style="margin-top: 20px" v-if="isProvincialAdmin">
|
||||
<el-button type="primary" @click="savePermissions">
|
||||
保存权限
|
||||
</el-button>
|
||||
<el-button @click="loadRoutes">
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
roleId: { type: Number, required: true },
|
||||
roleName: { type: String, required: true }
|
||||
})
|
||||
|
||||
// 检查当前用户是否是省级管理员
|
||||
const currentUserRole = ref('')
|
||||
const isProvincialAdmin = computed(() => currentUserRole.value === 'provincial_admin')
|
||||
|
||||
const treeRef = ref()
|
||||
const routes = ref([])
|
||||
const initialCheckedKeys = ref([])
|
||||
|
||||
// 从Token解析用户角色
|
||||
const parseUserRole = () => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
const payload = JSON.parse(atob(token.split('.')[1]))
|
||||
currentUserRole.value = payload.user_role || ''
|
||||
}
|
||||
}
|
||||
|
||||
// 加载路由列表
|
||||
const loadRoutes = async () => {
|
||||
try {
|
||||
const response = await fetch(`/rbac/roles/${props.roleId}/routes`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.code === 200) {
|
||||
routes.value = result.data.routes
|
||||
|
||||
// 提取启用的路由ID
|
||||
initialCheckedKeys.value = []
|
||||
const extractEnabled = (routeList) => {
|
||||
routeList.forEach(route => {
|
||||
if (route.enabled) {
|
||||
initialCheckedKeys.value.push(route.id)
|
||||
}
|
||||
if (route.children) {
|
||||
extractEnabled(route.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
extractEnabled(result.data.routes)
|
||||
|
||||
// 等待DOM更新后设置勾选
|
||||
await nextTick()
|
||||
treeRef.value?.setCheckedKeys(initialCheckedKeys.value)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('加载路由失败:' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存权限
|
||||
const savePermissions = async () => {
|
||||
if (!isProvincialAdmin.value) {
|
||||
ElMessage.warning('权限不足:仅省级管理员可以修改路由权限')
|
||||
return
|
||||
}
|
||||
|
||||
const checkedKeys = treeRef.value.getCheckedKeys()
|
||||
|
||||
try {
|
||||
const response = await fetch(`/rbac/roles/${props.roleId}/routes`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
route_ids: checkedKeys,
|
||||
permission: 'RW'
|
||||
})
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.code === 200) {
|
||||
ElMessage.success(
|
||||
`权限保存成功!启用 ${result.data.enabled_count} 个路由,` +
|
||||
`禁用 ${result.data.disabled_count} 个路由`
|
||||
)
|
||||
await loadRoutes() // 刷新以更新enabled状态
|
||||
} else if (result.code === 4003) {
|
||||
ElMessage.error('权限不足:仅省级管理员可以修改路由权限')
|
||||
} else {
|
||||
ElMessage.error(result.msg || '保存失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败:' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
parseUserRole()
|
||||
loadRoutes()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4️⃣ 数据库表结构
|
||||
|
||||
### role_route 表(v3.2更新)
|
||||
|
||||
```sql
|
||||
CREATE TABLE role_route (
|
||||
id SERIAL PRIMARY KEY,
|
||||
role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
route_id INTEGER NOT NULL REFERENCES sys_routes(id) ON DELETE CASCADE,
|
||||
permission VARCHAR(10) DEFAULT 'RW',
|
||||
status SMALLINT DEFAULT 1 NOT NULL, -- ⭐新增:0=禁用,1=启用
|
||||
created_at TIMESTAMP(6) WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP(6) WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT role_route_role_id_route_id_key UNIQUE (role_id, route_id),
|
||||
CONSTRAINT chk_role_route_status CHECK (status IN (0, 1))
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX idx_role_route_status ON role_route(status);
|
||||
CREATE INDEX idx_role_route_role_status ON role_route(role_id, status);
|
||||
|
||||
-- 注释
|
||||
COMMENT ON COLUMN role_route.status IS '状态:0=禁用,1=启用';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5️⃣ 核心逻辑说明
|
||||
|
||||
### 状态管理逻辑
|
||||
|
||||
```python
|
||||
# 批量更新时的逻辑:
|
||||
# 1. 先将所有路由设置为禁用(status=0)
|
||||
UPDATE role_route SET status = 0 WHERE role_id = ?
|
||||
|
||||
# 2. 将勾选的路由设置为启用(status=1)
|
||||
UPDATE role_route SET status = 1 WHERE role_id = ? AND route_id IN (?)
|
||||
|
||||
# 3. 如果记录不存在,插入新记录(status默认为1)
|
||||
INSERT INTO role_route (role_id, route_id, permission, status)
|
||||
VALUES (?, ?, 'RW', 1)
|
||||
```
|
||||
|
||||
### 查询逻辑
|
||||
|
||||
```python
|
||||
# 查询时返回所有路由(包括禁用的)
|
||||
SELECT
|
||||
sr.*,
|
||||
rr.status as route_status
|
||||
FROM role_route rr
|
||||
JOIN sys_routes sr ON rr.route_id = sr.id
|
||||
WHERE rr.role_id = ? -- 不限制status,返回所有记录
|
||||
|
||||
# 将 status 字段映射为 enabled
|
||||
route['enabled'] = route['route_status'] == 1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 版本对比
|
||||
|
||||
| 特性 | v3.1 | v3.2 | v3.3 |
|
||||
|------|------|------|------|
|
||||
| 取消勾选行为 | 删除记录 | 设置 status=0 | 设置 status=0 |
|
||||
| 路由显示 | 取消勾选后消失 | 取消勾选后仍显示 | 取消勾选后仍显示 |
|
||||
| 数据库操作 | DELETE | UPDATE status | UPDATE status |
|
||||
| 权限检查 | 无 | 仅省级管理员 | 仅省级管理员 |
|
||||
| 层级权限控制 | 无 | 无 | ✅ 省级/市级分权 |
|
||||
| 地区数据隔离 | 无 | 无 | ✅ 市级管理员地区限制 |
|
||||
| 返回字段 | assigned_count, removed_count | enabled_count, disabled_count, inserted_count | enabled_count, disabled_count, inserted_count |
|
||||
|
||||
---
|
||||
|
||||
## 💡 常见问题
|
||||
|
||||
### Q1: 为什么取消勾选后路由还显示?
|
||||
**A**: v3.2 采用状态管理模式,取消勾选只是将 status 设置为 0(禁用),不会删除记录。这样可以保留历史配置,方便恢复。
|
||||
|
||||
### Q2: 非省级管理员调用修改接口会怎样?
|
||||
**A**: 返回 403 错误,提示"权限不足:仅省级管理员可以修改角色路由权限"
|
||||
|
||||
### Q3: enabled 和 assigned 有什么区别?
|
||||
**A**:
|
||||
- `enabled` (v3.2): 基于 role_route.status 字段,表示路由是否启用
|
||||
- `assigned` (v3.2旧版): 基于记录是否存在,已废弃
|
||||
|
||||
### Q4: 如何查看只启用的路由?
|
||||
**A**: 调用接口时传参 `include_disabled=false`(默认为 true,返回所有)
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: v3.3
|
||||
**最后更新**: 2025-11-28
|
||||
**维护者**: Backend Team
|
||||
Reference in New Issue
Block a user