Browse Source
- GET /api/resources → GET /api/resources/list - POST /api/resources + @RequestBody → POST /api/resources/saveOrUpdate + 表单字段 - DELETE /api/resources/{id} → POST /api/resources/delete + @RequestParam - 前端对接文档同步更新master
2 changed files with 623 additions and 5 deletions
@ -0,0 +1,617 @@ |
|||||
|
# 菜单/权限点管理 — 前端对接文档 |
||||
|
|
||||
|
> 遵循《业务中台产品接口说明书》全局契约 |
||||
|
> 接口基础路径:`/api/resources` |
||||
|
> 权限要求:**管理员角色**(`ROLE_ADMIN`) |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 1. 接口总览 |
||||
|
|
||||
|
| 方法 | 路径 | 用途 | |
||||
|
|------|------|------| |
||||
|
| `GET` | `/api/resources/list` | 获取全量资源树(扁平数组) | |
||||
|
| `POST` | `/api/resources/saveOrUpdate` | 新增或编辑节点 | |
||||
|
| `POST` | `/api/resources/delete` | 删除节点 | |
||||
|
| `POST` | `/api/resources/icon/upload` | 上传图标 | |
||||
|
|
||||
|
**认证方式**:所有接口需在请求头携带 `Authorization: Bearer {token}`,且 token 对应用户必须具有 `ROLE_ADMIN` 角色。 |
||||
|
|
||||
|
**请求格式**: |
||||
|
- `GET` 请求:参数放 URL query |
||||
|
- `POST` 请求:`Content-Type: application/x-www-form-urlencoded`,参数为表单字段 |
||||
|
- 文件上传:`Content-Type: multipart/form-data` |
||||
|
|
||||
|
> ⚠️ 注意:POST 请求不使用 JSON body,复杂字段(对象/数组)各自 `JSON.stringify` 后作为普通表单字段传递。 |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 2. 数据模型:ResourceNode |
||||
|
|
||||
|
```typescript |
||||
|
interface ResourceNode { |
||||
|
id: string | null; // 节点 ID(字符串),新增时不传,编辑时必传 |
||||
|
parentId: string | null; // 父节点 ID(字符串),根节点传 null 或 "0" |
||||
|
type: 'CATALOG' | 'MENU' | 'BUTTON'; // 节点类型 |
||||
|
name: string; // 显示名称(必填) |
||||
|
sort: number; // 排序号,值越小越靠前(必填) |
||||
|
description?: string; // 描述/备注 |
||||
|
|
||||
|
// —— menu 类型专用 —— |
||||
|
route?: string; // 前端路由路径,menu 必填 |
||||
|
|
||||
|
// —— button 类型专用 —— |
||||
|
perms?: string; // 权限码,格式 crm:<module>:<action> |
||||
|
denyBehavior?: string; // 无权限时表现:'hide' | 'disable' |
||||
|
apiUrl?: string; // 按钮对应的后端接口路径 |
||||
|
status?: string; // 权限点状态:'enabled' | 'disabled' |
||||
|
|
||||
|
// —— catalog/menu 专用 —— |
||||
|
icon?: string; // 图标预览 URL |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
> ⚠️ **ID 一律按字符串收发**。后端返回的 id 是字符串(雪花 ID 超出 JS Number 安全范围),前端回传原样传字符串即可,不要转 Number。 |
||||
|
|
||||
|
### 节点类型说明 |
||||
|
|
||||
|
| 类型 | 枚举值 | 说明 | 可挂子节点 | |
||||
|
|------|--------|------|-----------| |
||||
|
| 菜单分组 | `CATALOG` | 收纳容器,不承载页面 | 只能挂 `MENU` | |
||||
|
| 菜单页面 | `MENU` | 可导航的页面 | 只能挂 `BUTTON` | |
||||
|
| 按钮/权限点 | `BUTTON` | 叶子节点,承载权限码 | 不可挂子节点 | |
||||
|
|
||||
|
### 层级约束 |
||||
|
|
||||
|
``` |
||||
|
根节点(parentId=null/"0") |
||||
|
├── CATALOG(菜单分组) |
||||
|
│ └── MENU(菜单页面) |
||||
|
│ └── BUTTON(按钮/权限点) |
||||
|
└── MENU(菜单页面,一级菜单) |
||||
|
└── BUTTON(按钮/权限点) |
||||
|
``` |
||||
|
|
||||
|
**规则**: |
||||
|
- 根节点只能挂 `CATALOG` 或 `MENU` |
||||
|
- `CATALOG` 下只能挂 `MENU`(不能嵌套分组) |
||||
|
- `MENU` 下只能挂 `BUTTON` |
||||
|
- `BUTTON` 是叶子节点,不可再挂子节点 |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 3. 接口详情 |
||||
|
|
||||
|
### 3.1 获取全量资源树 |
||||
|
|
||||
|
``` |
||||
|
GET /api/resources/list |
||||
|
Authorization: Bearer {token} |
||||
|
``` |
||||
|
|
||||
|
**响应示例**: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"code": 0, |
||||
|
"success": true, |
||||
|
"message": "success", |
||||
|
"data": [ |
||||
|
{ |
||||
|
"id": "1", |
||||
|
"parentId": "0", |
||||
|
"type": "CATALOG", |
||||
|
"name": "系统管理", |
||||
|
"sort": 1, |
||||
|
"description": null, |
||||
|
"icon": "https://cdn.example.com/icons/system.png" |
||||
|
}, |
||||
|
{ |
||||
|
"id": "10", |
||||
|
"parentId": "1", |
||||
|
"type": "MENU", |
||||
|
"name": "用户管理", |
||||
|
"sort": 1, |
||||
|
"route": "/system/user", |
||||
|
"icon": null |
||||
|
}, |
||||
|
{ |
||||
|
"id": "100", |
||||
|
"parentId": "10", |
||||
|
"type": "BUTTON", |
||||
|
"name": "查询用户", |
||||
|
"sort": 1, |
||||
|
"perms": "crm:user:list", |
||||
|
"denyBehavior": "hide", |
||||
|
"apiUrl": "/api/system/users/page", |
||||
|
"status": "enabled", |
||||
|
"description": "查询用户列表" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**前端处理**:后端返回扁平数组,前端通过 `parentId` 自行组树。 |
||||
|
|
||||
|
```typescript |
||||
|
// 组树示例 |
||||
|
function buildTree(nodes: ResourceNode[]): TreeNode[] { |
||||
|
const map = new Map<string, TreeNode>(); |
||||
|
const roots: TreeNode[] = []; |
||||
|
|
||||
|
nodes.forEach(node => { |
||||
|
map.set(node.id!, { ...node, children: [] }); |
||||
|
}); |
||||
|
|
||||
|
nodes.forEach(node => { |
||||
|
const treeNode = map.get(node.id!)!; |
||||
|
const parentId = node.parentId || "0"; |
||||
|
if (parentId === "0") { |
||||
|
roots.push(treeNode); |
||||
|
} else { |
||||
|
const parent = map.get(parentId); |
||||
|
parent?.children.push(treeNode); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return roots.sort((a, b) => a.sort - b.sort); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 3.2 新增/编辑节点 |
||||
|
|
||||
|
``` |
||||
|
POST /api/resources/saveOrUpdate |
||||
|
Content-Type: application/x-www-form-urlencoded |
||||
|
Authorization: Bearer {token} |
||||
|
``` |
||||
|
|
||||
|
**表单参数**: |
||||
|
|
||||
|
| 参数 | 类型 | 必填 | 说明 | |
||||
|
|------|------|------|------| |
||||
|
| id | string | 编辑时必传 | 不传为新增,传为编辑 | |
||||
|
| parentId | string | 是 | 父节点 ID,根节点传 `"0"` | |
||||
|
| type | string | 是 | `CATALOG` / `MENU` / `BUTTON` | |
||||
|
| name | string | 是 | 显示名称 | |
||||
|
| sort | number | 是 | 排序号 | |
||||
|
| description | string | 否 | 描述/备注 | |
||||
|
| route | string | menu 必填 | 前端路由路径 | |
||||
|
| perms | string | button 必填 | 权限码 | |
||||
|
| denyBehavior | string | button 必填 | `hide` / `disable` | |
||||
|
| apiUrl | string | button 必填 | 后端接口路径 | |
||||
|
| status | string | button 必填 | `enabled` / `disabled` | |
||||
|
| icon | string | catalog/menu 可选 | 图标预览 URL | |
||||
|
|
||||
|
**请求示例(新增菜单分组)**: |
||||
|
|
||||
|
```http |
||||
|
POST /api/resources/saveOrUpdate |
||||
|
Content-Type: application/x-www-form-urlencoded |
||||
|
Authorization: Bearer eyJhbGciOi... |
||||
|
|
||||
|
parentId=0&type=CATALOG&name=业务管理&sort=10&description=CRM核心业务模块 |
||||
|
``` |
||||
|
|
||||
|
**请求示例(新增按钮/权限点)**: |
||||
|
|
||||
|
```http |
||||
|
POST /api/resources/saveOrUpdate |
||||
|
Content-Type: application/x-www-form-urlencoded |
||||
|
Authorization: Bearer eyJhbGciOi... |
||||
|
|
||||
|
parentId=1002&type=BUTTON&name=删除客户&sort=3&perms=crm:customer:delete&denyBehavior=disable&apiUrl=/api/crm/customer/delete&status=enabled&description=删除客户及其关联数据 |
||||
|
``` |
||||
|
|
||||
|
**请求示例(编辑节点 — 改名)**: |
||||
|
|
||||
|
```http |
||||
|
POST /api/resources/saveOrUpdate |
||||
|
Content-Type: application/x-www-form-urlencoded |
||||
|
Authorization: Bearer eyJhbGciOi... |
||||
|
|
||||
|
id=10&parentId=1&type=MENU&name=客户管理改&sort=1&route=/customer/list |
||||
|
``` |
||||
|
|
||||
|
**响应**:返回保存后的完整节点。 |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"code": 0, |
||||
|
"success": true, |
||||
|
"message": "success", |
||||
|
"data": { |
||||
|
"id": "11", |
||||
|
"parentId": "1", |
||||
|
"type": "MENU", |
||||
|
"name": "客户管理", |
||||
|
"sort": 2, |
||||
|
"route": "/customer/list" |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**字段必填规则**(按类型): |
||||
|
|
||||
|
| 字段 | CATALOG | MENU | BUTTON | |
||||
|
|------|---------|------|--------| |
||||
|
| name | ✅ 必填 | ✅ 必填 | ✅ 必填 | |
||||
|
| sort | ✅ 必填 | ✅ 必填 | ✅ 必填 | |
||||
|
| route | ❌ | ✅ 必填 | ❌ | |
||||
|
| perms | ❌ | ❌ | ✅ 必填 | |
||||
|
| denyBehavior | ❌ | ❌ | ✅ 必填 | |
||||
|
| apiUrl | ❌ | ❌ | ✅ 必填 | |
||||
|
| status | ❌ | ❌ | ✅ 必填 | |
||||
|
| icon | 可选 | 可选 | ❌ | |
||||
|
| description | 可选 | 可选 | 可选 | |
||||
|
|
||||
|
**编辑限制**: |
||||
|
- `type` 字段创建后不可变更(编辑时传不同的 type 会被拒绝) |
||||
|
- 编辑时 `route` 可不传,后端保留原有值(除非传新值覆盖) |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 3.3 删除节点 |
||||
|
|
||||
|
``` |
||||
|
POST /api/resources/delete |
||||
|
Content-Type: application/x-www-form-urlencoded |
||||
|
Authorization: Bearer {token} |
||||
|
``` |
||||
|
|
||||
|
**表单参数**: |
||||
|
|
||||
|
| 参数 | 类型 | 必填 | 说明 | |
||||
|
|------|------|------|------| |
||||
|
| id | string | 是 | 要删除的节点 ID | |
||||
|
|
||||
|
**请求示例**: |
||||
|
|
||||
|
```http |
||||
|
POST /api/resources/delete |
||||
|
Content-Type: application/x-www-form-urlencoded |
||||
|
Authorization: Bearer eyJhbGciOi... |
||||
|
|
||||
|
id=100 |
||||
|
``` |
||||
|
|
||||
|
**校验规则**: |
||||
|
- 节点必须存在 |
||||
|
- 节点下不能有子节点(有子节点会返回错误,需先删除子节点) |
||||
|
- 删除时自动清理各角色对该节点的授权引用 |
||||
|
|
||||
|
**成功响应**: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"code": 0, |
||||
|
"success": true, |
||||
|
"message": "success" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**失败响应示例**(有子节点): |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"code": 61009, |
||||
|
"success": false, |
||||
|
"message": "节点下有 3 个子节点,请先删除子节点" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 3.4 上传图标 |
||||
|
|
||||
|
``` |
||||
|
POST /api/resources/icon/upload |
||||
|
Content-Type: multipart/form-data |
||||
|
Authorization: Bearer {token} |
||||
|
``` |
||||
|
|
||||
|
**参数**: |
||||
|
|
||||
|
| 参数 | 类型 | 必填 | 说明 | |
||||
|
|------|------|------|------| |
||||
|
| file | File | ✅ | 图片文件 | |
||||
|
|
||||
|
**限制**: |
||||
|
- 格式:`png` / `jpg` / `jpeg` / `gif` / `svg` / `webp` |
||||
|
- 大小:≤ 500KB |
||||
|
|
||||
|
**请求示例**: |
||||
|
|
||||
|
```http |
||||
|
POST /api/resources/icon/upload |
||||
|
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary |
||||
|
Authorization: Bearer eyJhbGciOi... |
||||
|
|
||||
|
------WebKitFormBoundary |
||||
|
Content-Disposition: form-data; name="file"; filename="icon.png" |
||||
|
Content-Type: image/png |
||||
|
|
||||
|
(二进制内容) |
||||
|
------WebKitFormBoundary-- |
||||
|
``` |
||||
|
|
||||
|
**响应**: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"code": 0, |
||||
|
"success": true, |
||||
|
"message": "success", |
||||
|
"data": { |
||||
|
"fileId": "1234567890", |
||||
|
"originalName": "icon.png", |
||||
|
"size": 12345, |
||||
|
"contentType": "image/png", |
||||
|
"bizDomain": "resources/icon" |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**前端处理流程**: |
||||
|
|
||||
|
```typescript |
||||
|
async function uploadIcon(file: File): Promise<string> { |
||||
|
// 1. 上传图标,获取 fileId |
||||
|
const formData = new FormData(); |
||||
|
formData.append('file', file); |
||||
|
|
||||
|
const uploadRes = await fetch('/api/resources/icon/upload', { |
||||
|
method: 'POST', |
||||
|
headers: { Authorization: `Bearer ${token}` }, |
||||
|
body: formData, // multipart/form-data 由浏览器自动设置 |
||||
|
}); |
||||
|
const { data: fileInfo } = await uploadRes.json(); |
||||
|
|
||||
|
// 2. 通过 fileId 获取预览 URL |
||||
|
const previewRes = await fetch(`/api/file/preview-url?fileId=${fileInfo.fileId}`, { |
||||
|
headers: { Authorization: `Bearer ${token}` }, |
||||
|
}); |
||||
|
const { data: previewUrl } = await previewRes.json(); |
||||
|
|
||||
|
// 3. 返回预览 URL,存入节点的 icon 字段 |
||||
|
return previewUrl; |
||||
|
} |
||||
|
|
||||
|
// 使用示例 |
||||
|
const iconUrl = await uploadIcon(selectedFile); |
||||
|
// 然后在保存节点时将 iconUrl 作为 icon 表单字段传入 |
||||
|
await saveNode({ ...node, icon: iconUrl }); |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 4. 停用与启用权限点 |
||||
|
|
||||
|
停用/启用权限点**没有单独的接口**,通过编辑节点(`POST /api/resources/saveOrUpdate`)更新 `status` 字段实现。 |
||||
|
|
||||
|
### 操作方式 |
||||
|
|
||||
|
```http |
||||
|
POST /api/resources/saveOrUpdate |
||||
|
Content-Type: application/x-www-form-urlencoded |
||||
|
Authorization: Bearer eyJhbGciOi... |
||||
|
|
||||
|
id=1003&parentId=1002&type=BUTTON&name=删除客户&sort=3&perms=crm:customer:delete&denyBehavior=disable&apiUrl=/api/crm/customer/delete&status=disabled |
||||
|
``` |
||||
|
|
||||
|
| 操作 | status 值 | 说明 | |
||||
|
|------|-----------|------| |
||||
|
| 停用 | `disabled` | 权限点全局失效,所有用户均无法使用该权限 | |
||||
|
| 启用 | `enabled` | 恢复权限点生效,原角色授权自动恢复 | |
||||
|
|
||||
|
### 业务语义 |
||||
|
|
||||
|
- **停用**:即使角色已勾选授予该权限点,权限解析引擎也不将其 `perms` 纳入用户的权限码并集——相当于**全局断路**,无需逐角色取消授权 |
||||
|
- **启用**:重新启用后,原授权**自动恢复生效**,无需管理员重新到角色授权中勾选 |
||||
|
- **数据影响**:停用/启用只修改 `status` 字段,不涉及角色授权关系(`sys_role_menu`)的变动 |
||||
|
|
||||
|
### 前端处理建议 |
||||
|
|
||||
|
```typescript |
||||
|
// 权限点状态切换 |
||||
|
async function togglePermissionStatus(nodeId: string, enabled: boolean) { |
||||
|
const node = await getNode(nodeId); |
||||
|
const params = new URLSearchParams(); |
||||
|
params.append('id', node.id); |
||||
|
params.append('parentId', node.parentId); |
||||
|
params.append('type', node.type); |
||||
|
params.append('name', node.name); |
||||
|
params.append('sort', node.sort); |
||||
|
params.append('perms', node.perms); |
||||
|
params.append('denyBehavior', node.denyBehavior); |
||||
|
params.append('apiUrl', node.apiUrl); |
||||
|
params.append('status', enabled ? 'enabled' : 'disabled'); |
||||
|
|
||||
|
await fetch('/api/resources/saveOrUpdate', { |
||||
|
method: 'POST', |
||||
|
headers: { |
||||
|
Authorization: `Bearer ${token}`, |
||||
|
'Content-Type': 'application/x-www-form-urlencoded', |
||||
|
}, |
||||
|
body: params, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 在 UI 上显示停用状态 |
||||
|
<Switch |
||||
|
checked={node.status === 'enabled'} |
||||
|
onChange={(checked) => togglePermissionStatus(node.id, checked)} |
||||
|
/> |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 5. 错误处理 |
||||
|
|
||||
|
所有错误响应格式统一: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"code": 61009, |
||||
|
"success": false, |
||||
|
"message": "具体错误信息" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**判定规则**:判成功只看 `success === true`;判失败后的分支处理(跳登录、回填表单等)再看 `code`。 |
||||
|
|
||||
|
**HTTP 状态码**:业务失败 HTTP 状态码仍是 200,通过 `success`/`code` 判断。仅以下场景 HTTP 状态码非 200: |
||||
|
|
||||
|
| HTTP | 场景 | 响应体 code | |
||||
|
|------|------|-------------| |
||||
|
| 401 | 未登录 / 登录过期 | 40103 | |
||||
|
| 403 | 已登录但无权限 | 40301 | |
||||
|
| 404 | 接口路径不存在 | 40401 | |
||||
|
| 500 | 系统内部异常 | 50001 | |
||||
|
|
||||
|
即使 HTTP 状态码非 200,响应体仍然是统一信封结构。 |
||||
|
|
||||
|
**资源管理接口常见错误**: |
||||
|
|
||||
|
| 消息 | 原因 | |
||||
|
|------|------| |
||||
|
| `节点名称不能为空` | name 字段为空 | |
||||
|
| `节点类型不能为空` | type 字段为空 | |
||||
|
| `menu 类型必须提供路由(route)` | MENU 类型缺少 route | |
||||
|
| `button 类型必须提供权限码(perms)` | BUTTON 类型缺少 perms | |
||||
|
| `button 类型必须提供无权限行为(denyBehavior)` | BUTTON 类型缺少 denyBehavior | |
||||
|
| `button 类型必须提供接口 URL(apiUrl)` | BUTTON 类型缺少 apiUrl | |
||||
|
| `button 类型必须提供状态(status)` | BUTTON 类型缺少 status | |
||||
|
| `根节点不允许添加 button 类型` | 违反层级约束 | |
||||
|
| `catalog 节点下只能添加 menu 类型` | 违反层级约束 | |
||||
|
| `menu 节点下只能添加 button 类型` | 违反层级约束 | |
||||
|
| `button 是叶子节点,不可添加子节点` | 违反层级约束 | |
||||
|
| `节点类型不可变更` | 编辑时修改了 type | |
||||
|
| `节点下有 N 个子节点,请先删除子节点` | 删除非叶子节点 | |
||||
|
| `不支持的图标格式` | 上传非法格式 | |
||||
|
| `图标大小不能超过 500KB` | 文件过大 | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 6. 联调指南 |
||||
|
|
||||
|
### 6.1 前置条件 |
||||
|
|
||||
|
1. 确保后端服务已启动(Base URL 由后端另行下发) |
||||
|
2. 使用管理员账号通过钉钉扫码登录获取 token |
||||
|
3. 请求头统一添加 `Authorization: Bearer {token}` |
||||
|
4. token 由后端滑动续期,前端无需定时刷新;收到 401(code=40103)时清除 token 跳登录页 |
||||
|
|
||||
|
### 6.2 快速验证 |
||||
|
|
||||
|
```bash |
||||
|
# 1. 获取全量资源树 |
||||
|
curl -X GET "{baseUrl}/api/resources/list" \ |
||||
|
-H "Authorization: Bearer YOUR_TOKEN" |
||||
|
|
||||
|
# 2. 新增一个菜单分组 |
||||
|
curl -X POST "{baseUrl}/api/resources/saveOrUpdate" \ |
||||
|
-H "Authorization: Bearer YOUR_TOKEN" \ |
||||
|
-H "Content-Type: application/x-www-form-urlencoded" \ |
||||
|
-d "parentId=0&type=CATALOG&name=测试分组&sort=99" |
||||
|
|
||||
|
# 3. 在分组下新增菜单 |
||||
|
curl -X POST "{baseUrl}/api/resources/saveOrUpdate" \ |
||||
|
-H "Authorization: Bearer YOUR_TOKEN" \ |
||||
|
-H "Content-Type: application/x-www-form-urlencoded" \ |
||||
|
-d "parentId={上一步返回的id}&type=MENU&name=测试菜单&sort=1&route=/test/menu" |
||||
|
|
||||
|
# 4. 在菜单下新增按钮 |
||||
|
curl -X POST "{baseUrl}/api/resources/saveOrUpdate" \ |
||||
|
-H "Authorization: Bearer YOUR_TOKEN" \ |
||||
|
-H "Content-Type: application/x-www-form-urlencoded" \ |
||||
|
-d "parentId={菜单id}&type=BUTTON&name=测试按钮&sort=1&perms=crm:test:action&denyBehavior=hide&apiUrl=/api/test/action&status=enabled" |
||||
|
|
||||
|
# 5. 删除节点 |
||||
|
curl -X POST "{baseUrl}/api/resources/delete" \ |
||||
|
-H "Authorization: Bearer YOUR_TOKEN" \ |
||||
|
-H "Content-Type: application/x-www-form-urlencoded" \ |
||||
|
-d "id={节点id}" |
||||
|
``` |
||||
|
|
||||
|
### 6.3 前端开发建议 |
||||
|
|
||||
|
1. **树形组件**:推荐使用 `parentId` 在前端组树,后端返回扁平数组性能更好 |
||||
|
2. **表单提交**:使用 `URLSearchParams` 或 `qs` 库构造表单字段,不要用 `JSON.stringify` 整体打包 |
||||
|
3. **ID 处理**:所有 id 字段按字符串处理,不要 `parseInt` / `Number()` |
||||
|
4. **表单校验**:根据节点类型动态显示必填字段 |
||||
|
- CATALOG:name, sort |
||||
|
- MENU:name, sort, route |
||||
|
- BUTTON:name, sort, perms, denyBehavior, apiUrl, status |
||||
|
5. **类型锁定**:编辑时 type 字段应置灰不可修改 |
||||
|
6. **图标上传**:先上传获取 URL,再保存节点 |
||||
|
7. **权限码格式**:建议提供下拉选择或自动补全,格式为 `crm:<module>:<action>` |
||||
|
- module: `lead`, `opportunity`, `customer`, `project` |
||||
|
- action: `add`, `edit`, `delete`, `export`, `transfer` 等 |
||||
|
|
||||
|
### 6.4 调试技巧 |
||||
|
|
||||
|
- 每个响应都带响应头 `X-Trace-Id`,报障时附上此 ID |
||||
|
- 使用浏览器开发者工具 Network 面板查看请求/响应 |
||||
|
- 后端日志会打印保存的节点信息,便于排查 |
||||
|
- 403 错误检查 token 是否过期、用户是否有 ADMIN 角色 |
||||
|
- 61009 错误查看 message 字段获取具体原因 |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 7. 权限码并集说明 |
||||
|
|
||||
|
用户登录后,后端会自动计算其**权限码并集**(所有角色授权的启用状态按钮的 perms),注入到 Spring Security 上下文中。 |
||||
|
|
||||
|
**前端使用**: |
||||
|
- 登录后从 `GET /api/auth/me` 获取用户信息及 `authorities` 列表 |
||||
|
- 根据 `authorities` 控制按钮显示/隐藏 |
||||
|
|
||||
|
```typescript |
||||
|
// 示例:检查是否有某权限 |
||||
|
const hasPermission = (perm: string) => { |
||||
|
return userAuthorities.includes(perm); |
||||
|
}; |
||||
|
|
||||
|
// 使用 |
||||
|
{hasPermission('crm:customer:delete') && <DeleteButton />} |
||||
|
``` |
||||
|
|
||||
|
**停用权限点**: |
||||
|
- 当按钮的 `status` 设为 `disabled` 时,即使角色已授权,该权限码也不会出现在用户权限并集中 |
||||
|
- 相当于全局断路,无需逐角色取消授权 |
||||
|
- 重新启用后原授权自动恢复 |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 8. 与角色授权的集成 |
||||
|
|
||||
|
资源树维护完成后,在**角色管理 → 授权**页面使用: |
||||
|
|
||||
|
1. 调用 `GET /api/resources/list` 获取全量资源树 |
||||
|
2. 渲染为勾选树(checkbox tree) |
||||
|
3. 勾选节点即授予该角色对应权限 |
||||
|
4. 勾选菜单节点时,前端自动联动勾选其全部祖先节点(确保菜单可见) |
||||
|
|
||||
|
```typescript |
||||
|
// 联动勾选祖先 |
||||
|
function collectAncestorIds(nodeId: string, tree: TreeNode[]): string[] { |
||||
|
const ancestors: string[] = []; |
||||
|
const findPath = (nodes: TreeNode[], target: string, path: string[]): boolean => { |
||||
|
for (const node of nodes) { |
||||
|
if (node.id === target) { |
||||
|
ancestors.push(...path); |
||||
|
return true; |
||||
|
} |
||||
|
if (findPath(node.children, target, [...path, node.id!])) { |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
return false; |
||||
|
}; |
||||
|
findPath(tree, nodeId, []); |
||||
|
return ancestors; |
||||
|
} |
||||
|
``` |
||||
Loading…
Reference in new issue