You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

18 KiB

菜单/权限点管理 — 前端对接文档

遵循《业务中台产品接口说明书》全局契约
接口基础路径:/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

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
  children?: ResourceNode[];    // 子节点列表(listAll 返回时填充,其他接口无此字段)

⚠️ ID 一律按字符串收发。后端返回的 id 是字符串(雪花 ID 超出 JS Number 安全范围),前端回传原样传字符串即可,不要转 Number。

节点类型说明

类型 枚举值 说明 可挂子节点
菜单分组 CATALOG 收纳容器,不承载页面 只能挂 MENU
菜单页面 MENU 可导航的页面 只能挂 BUTTON
按钮/权限点 BUTTON 叶子节点,承载权限码 不可挂子节点

层级约束

根节点(parentId=null/"0")
  ├── CATALOG(菜单分组)
  │     └── MENU(菜单页面)
  │           └── BUTTON(按钮/权限点)
  └── MENU(菜单页面,一级菜单)
        └── BUTTON(按钮/权限点)

规则

  • 根节点只能挂 CATALOGMENU
  • CATALOG 下只能挂 MENU(不能嵌套分组)
  • MENU 下只能挂 BUTTON
  • BUTTON 是叶子节点,不可再挂子节点

3. 接口详情

3.1 获取全量资源树

GET /api/resources/list
Authorization: Bearer {token}

响应示例(后端已组树,直接渲染即可):

{
  "code": 0,
  "success": true,
  "message": "success",
  "data": [
    {
      "id": "1",
      "parentId": "0",
      "type": "CATALOG",
      "name": "系统管理",
      "sort": 1,
      "icon": "https://cdn.example.com/icons/system.png",
      "children": [
        {
          "id": "10",
          "parentId": "1",
          "type": "MENU",
          "name": "用户管理",
          "sort": 1,
          "route": "/system/user",
          "children": [
            {
              "id": "100",
              "parentId": "10",
              "type": "BUTTON",
              "name": "查询用户",
              "sort": 1,
              "perms": "crm:user:list",
              "denyBehavior": "hide",
              "apiUrl": "/api/system/users/page",
              "status": "enabled",
              "description": "查询用户列表",
              "children": []
            }
          ]
        }
      ]
    }
  ]
}

前端处理:后端已通过 TreeUtils.buildTree 组好树,data 直接就是树形结构,无需前端再按 parentId 组树。直接渲染即可:

// 直接渲染后端返回的树形数据
const tree = response.data;  // 已是树形结构

<Tree treeData={tree} />

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

请求示例(新增菜单分组)

POST /api/resources/saveOrUpdate
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer eyJhbGciOi...

parentId=0&type=CATALOG&name=业务管理&sort=10&description=CRM核心业务模块

请求示例(新增按钮/权限点)

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=删除客户及其关联数据

请求示例(编辑节点 — 改名)

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

响应:返回保存后的完整节点。

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

请求示例

POST /api/resources/delete
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer eyJhbGciOi...

id=100

校验规则

  • 节点必须存在
  • 节点下不能有子节点(有子节点会返回错误,需先删除子节点)
  • 删除时自动清理各角色对该节点的授权引用

成功响应

{
  "code": 0,
  "success": true,
  "message": "success"
}

失败响应示例(有子节点):

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

请求示例

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--

响应

{
  "code": 0,
  "success": true,
  "message": "success",
  "data": {
    "fileId": "1234567890",
    "originalName": "icon.png",
    "size": 12345,
    "contentType": "image/png",
    "bizDomain": "resources/icon"
  }
}

前端处理流程

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 字段实现。

操作方式

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)的变动

前端处理建议

// 权限点状态切换
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. 错误处理

所有错误响应格式统一:

{
  "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 快速验证

# 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. 树形数据:后端已组好树并按 sort 排序,前端直接渲染 data 即可,无需自行组树
  2. 表单提交:使用 URLSearchParamsqs 库构造表单字段,不要用 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 控制按钮显示/隐藏
// 示例:检查是否有某权限
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. 勾选菜单节点时,前端自动联动勾选其全部祖先节点(确保菜单可见)
// 联动勾选祖先
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;
}