docs: refresh frontend integration notes

This commit is contained in:
wren
2026-05-06 09:42:29 +08:00
parent c54f84382b
commit b36e102aa0
9 changed files with 218 additions and 3970 deletions
-529
View File
@@ -1,529 +0,0 @@
# RBAC 路由权限 API 响应示例 v3.2
## 📋 文档说明
本文档展示 v3.2 版本 RBAC 路由权限接口的完整响应格式。
**v3.2 核心变更**
- ✅ 使用 `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 |
|------|------|------|
| 取消勾选行为 | 删除记录 | 设置 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
+1 -1
View File
@@ -89,7 +89,7 @@ Authorization: Bearer <token>
"id": 41,
"route_path": "/rules",
"route_name": "Rules",
"route_title": "评查规则库",
"route_title": "规则管理",
"parent_id": null,
"icon": "ri-book-3-line",
"sort_order": 3,
File diff suppressed because it is too large Load Diff
+172 -23
View File
@@ -1,6 +1,6 @@
# 入口模块管理 API 完整技术文档
> **供前端对接使用** | 更新时间:2025-11-28 | 版本:v2.0
> **供前端对接使用** | 更新时间:2025-01-29 | 版本:v2.1
## 📋 目录
@@ -390,7 +390,9 @@ LIMIT 20 OFFSET 0;
**错误响应(500**
```json
{
"detail": "查询入口模块列表失败: connection to server failed"
"code": 500,
"msg": "查询入口模块列表失败: connection to server failed",
"data": []
}
```
@@ -470,7 +472,9 @@ WHERE id = 1; -- $1 参数化查询
**错误响应(404**
```json
{
"detail": "入口模块不存在: id=999"
"code": 404,
"msg": "入口模块不存在: id=999",
"data": []
}
```
@@ -606,7 +610,9 @@ RETURNING id, name, description, path, areas, created_at, updated_at;
**错误响应(400- 名称重复**
```json
{
"detail": "模块名称已存在: name=合同管理"
"code": 400,
"msg": "模块名称已存在: name=合同管理",
"data": []
}
```
@@ -756,21 +762,27 @@ RETURNING id, name, description, path, areas, created_at, updated_at;
**错误响应(404- 模块不存在**
```json
{
"detail": "入口模块不存在: id=999"
"code": 404,
"msg": "入口模块不存在: id=999",
"data": []
}
```
**错误响应(400- 名称已存在**
```json
{
"detail": "模块名称已存在: name=案卷智能评查"
"code": 400,
"msg": "模块名称已存在: name=案卷智能评查",
"data": []
}
```
**错误响应(400- 没有更新字段**
```json
{
"detail": "没有要更新的字段"
"code": 400,
"msg": "没有要更新的字段",
"data": []
}
```
@@ -849,7 +861,9 @@ DELETE FROM entry_modules WHERE id = 1;
**错误响应(404**
```json
{
"detail": "入口模块不存在: id=999"
"code": 404,
"msg": "入口模块不存在: id=999",
"data": []
}
```
@@ -862,6 +876,73 @@ DELETE FROM entry_modules WHERE id = 1;
### 6. 上传入口模块图标
> ⚠️ **重要更新 (v2.1)**:图片上传功能已修复JWT拦截问题,静态资源现在可以无需Token访问。
#### 🔥 前端常见问题及解决方案
**问题1:上传时报错 "Field required - body.file"**
```json
{
"code": 3000,
"msg": "参数校验错误",
"data": [
{
"type": "missing",
"loc": ["body", "file"],
"msg": "Field required"
}
]
}
```
**原因**:字段名错误或没有使用FormData上传
**正确做法**
```javascript
// ✅ 正确
const formData = new FormData();
formData.append('file', imageFile); // 字段名必须是 "file"
await fetch(`/api/v3/entry-modules/${moduleId}/image`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
// ✅ 不要设置 Content-Type,让浏览器自动添加boundary
},
body: formData
});
// ❌ 错误示例
formData.append('image', imageFile); // 字段名错误
formData.append('picture', imageFile); // 字段名错误
body: JSON.stringify({file: base64}); // 不能用JSON
headers: {'Content-Type': 'multipart/form-data'} // 不要手动设置
```
**问题2:图片显示401 Unauthorized**
这是因为旧版本JWT中间件拦截了MinIO静态资源。
**已修复** ✅:
- MinIO路径(`/docauditai/`)已加入JWT白名单
- 图片现在可以**无需Token**直接访问
- 测试:`curl http://your-server/docauditai/documents/mz/static/img/entry_module_1.png`
**问题3:错误的图片访问路径**
```javascript
// ❌ 错误 - 硬编码路径
const imageUrl = `/docauditai/entryModule/${module.code}`;
// ✅ 正确 - 使用数据库path字段
const imageUrl = `http://172.16.0.81:9000/docauditai/${module.path}`;
// ✅ 或者使用上传接口返回的url字段(推荐)
const result = await uploadImage(moduleId, file);
const imageUrl = result.data.url;
```
#### 接口定义
```http
@@ -1067,28 +1148,52 @@ WHERE id = 1;
**错误响应(400- 不支持的格式**
```json
{
"detail": "不支持的图片格式: .pdf,支持的格式: .jpg, .jpeg, .png, .gif, .webp, .svg"
"code": 400,
"msg": "不支持的图片格式: .pdf,支持的格式: .jpg, .jpeg, .png, .gif, .webp, .svg",
"data": []
}
```
**错误响应(400- 文件为空**
```json
{
"detail": "文件内容为空"
"code": 400,
"msg": "文件内容为空",
"data": []
}
```
**错误响应(404- 模块不存在**
```json
{
"detail": "入口模块不存在: id=999"
"code": 404,
"msg": "入口模块不存在: id=999",
"data": []
}
```
**错误响应(422)- 缺少file字段** ⚠️ **常见错误**
```json
{
"code": 3000,
"msg": "参数校验错误",
"data": [
{
"type": "missing",
"loc": ["body", "file"],
"msg": "Field required",
"input": null
}
]
}
```
**错误响应(500- MinIO上传失败**
```json
{
"detail": "上传入口模块图片失败: S3 error: Access Denied"
"code": 500,
"msg": "上传入口模块图片失败: S3 error: Access Denied",
"data": []
}
```
@@ -1465,22 +1570,30 @@ api.interceptors.response.use(
(error) => {
if (error.response) {
const { status, data } = error.response;
// ⭐ v2.1更新:错误响应统一格式 {code, msg, data}
const errorMsg = data.msg || data.detail || '请求失败';
switch (status) {
case 401:
// Token过期,跳转登录
console.error('未授权:', errorMsg);
window.location.href = '/login';
break;
case 403:
// 无权限
console.error('权限不足');
console.error('权限不足:', errorMsg);
break;
case 404:
// 资源不存在
console.error(data.detail || '资源不存在');
console.error('资源不存在:', errorMsg);
break;
case 422:
// 参数校验错误
console.error('参数错误:', errorMsg, data.data);
break;
case 500:
// 服务器错误
console.error(data.detail || '服务器错误');
console.error('服务器错误:', errorMsg);
break;
}
}
@@ -1554,20 +1667,20 @@ export const entryModuleApi = {
/**
* 上传图标
* ⚠️ 注意:字段名必须是 "file",不要手动设置Content-Type
*/
uploadImage: (id: number, file: File) => {
const formData = new FormData();
formData.append('file', file);
formData.append('file', file); // ✅ 字段名必须是 "file"
return api.post<{
code: number;
msg: string;
data: ImageUploadResponse;
}>(`/entry-modules/${id}/image`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
}>(`/entry-modules/${id}/image`, formData
// ✅ 不要设置Content-Typeaxios会自动设置
// 如果手动设置会缺少boundary参数导致上传失败
);
}
};
```
@@ -1776,7 +1889,43 @@ WHERE module = 'entry_module';
---
**文档版本**v2.0
**最后更新**2025-11-28
## 📝 版本更新记录
### v2.1 (2025-01-29)
**重要更新**
1. ✅ 修复JWT拦截MinIO静态资源问题
- MinIO路径(`/docauditai/`)已加入JWT白名单
- 图片现在可以无需Token访问
2. ✅ 统一错误响应格式
- 旧格式:`{"detail": "错误信息"}`
- 新格式:`{"code": xxx, "msg": "错误信息", "data": []}`
3. ✅ 新增图片上传常见问题解决方案
- 详细说明了"Field required - body.file"错误的解决方法
- 提供了正确的FormData使用示例
- 强调字段名必须是 "file"
4. ✅ 创建测试工具
- 新增HTML测试工具:`scripts/test_entry_module_image_upload.html`
- 可视化测试界面,帮助调试图片上传功能
**前端重要修改**
- ⚠️ 删除手动设置 `Content-Type: multipart/form-data`
- ⚠️ 使用 `data.msg` 替代 `data.detail` 获取错误信息
- ⚠️ 图片访问不要硬编码路径,使用数据库path字段
### v2.0 (2025-11-28)
- 完整的API文档
- 数据库设计详解
- 前端对接指南
- TypeScript类型定义
---
**当前版本**v2.1
**最后更新**2025-01-29
**维护者**Backend Team
**反馈渠道**[提交Issue](http://git.7bm.co:1024/leke/docauditai/-/issues)
File diff suppressed because it is too large Load Diff