Files
leaudit-platform-frontend/auth_doc/API_RESPONSE_EXAMPLES_V3.2.md
T
2025-12-05 00:09:32 +08:00

13 KiB
Raw Blame History

RBAC 路由权限 API 响应示例 v3.2

📋 文档说明

本文档展示 v3.2 版本 RBAC 路由权限接口的完整响应格式。

v3.2 核心变更

  • 使用 status 字段管理路由启用/禁用状态
  • 取消勾选后路由不删除,只是设置为 status=0
  • 接口返回 enabled 字段标识路由是否启用
  • 仅省级管理员可修改路由权限

1️⃣ GET /rbac/roles/{role_id}/routes

功能说明

获取角色关联的所有路由(包括禁用的),每个路由带 enabled 字段

请求示例

GET /rbac/roles/1/routes
Authorization: Bearer <token>

响应示例

{
  "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权限列表

前端使用示例

// 获取角色路由
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)
  • 仅省级管理员可调用

请求示例

PUT /rbac/roles/1/routes
Authorization: Bearer <provincial_admin_token>
Content-Type: application/json

{
  "route_ids": [31, 41],
  "permission": "RW"
}

响应示例(成功)

{
  "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列表

响应示例(权限不足)

{
  "code": 4003,
  "msg": "权限不足:仅省级管理员可以修改角色路由权限",
  "data": null
}

前端使用示例

// 保存路由权限
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 完整组件

<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更新)

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️⃣ 核心逻辑说明

状态管理逻辑

# 批量更新时的逻辑:
# 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)

查询逻辑

# 查询时返回所有路由(包括禁用的)
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
取消勾选行为 删除记录 设置 status=0
路由显示 取消勾选后消失 取消勾选后仍显示
数据库操作 DELETE UPDATE status
权限检查 仅省级管理员
返回字段 assigned_count, removed_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.2 最后更新: 2025-11-28 维护者: Backend Team