Files
leaudit-platform-frontend/auth_doc/API_RESPONSE_EXAMPLES_V3.3.md
2026-05-06 09:42:29 +08:00

533 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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