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.

442 lines
12 KiB

1 month ago
# 角色管理 — 前端对接文档
> 遵循《业务中台产品接口说明书》全局契约
> 接口基础路径:`/api/roles`
> 权限控制:纯 `hasAuthority`,权限码由 `DataInitializer` 种子化到权限资源树
---
## 1. 接口总览
| 方法 | 路径 | 用途 | 权限码 |
|------|------|------|--------|
| `POST` | `/api/roles/page` | 分页查询角色列表 | `crm:role:list` |
| `POST` | `/api/roles/saveOrUpdate` | 新增或编辑角色 | `crm:role:save` |
| `GET` | `/api/roles/detail` | 角色详情(含已授权资源集合) | `crm:role:detail` |
| `POST` | `/api/roles/assign-resources` | 分配角色权限资源(祖先补全 + 全量替换) | `crm:role:assign` |
| `POST` | `/api/roles/delete` | 删除角色(级联清理关联表) | `crm:role:delete` |
**认证方式**:所有接口需在请求头携带 `Authorization: Bearer {token}`,且 token 对应用户必须拥有对应权限码。
**请求格式**:
- `GET` 请求:参数放 URL query
- `POST` 请求:`Content-Type: application/x-www-form-urlencoded`,参数为表单字段
> ⚠️ 注意:POST 请求不使用 JSON body,复杂字段(对象/数组)各自 `JSON.stringify` 后作为普通表单字段传递。
---
## 2. 数据模型
### 2.1 SysRole(列表/编辑)
```typescript
interface SysRole {
id: string; // 角色 ID(字符串,雪花 ID)
roleName: string; // 角色名称
roleCode: string; // 角色编码(如 ROLE_SALES)
dataScope: number; // 数据范围:1=本人 2=本部门 3=本部门及子部门 4=全部
sort: number; // 排序号,值越小越靠前
remark: string | null; // 备注
builtin: boolean; // 是否内置角色(true=系统内置,不可删除、不可改编码)
}
```
### 2.2 RoleDetailVO(详情接口返回)
```typescript
interface RoleDTO {
1 month ago
id: string; // 角色 ID
roleName: string; // 角色名称
roleCode: string; // 角色编码
dataScope: number; // 数据范围:1=本人 2=本部门 3=本部门及子部门 4=全部
sort: number; // 排序
remark: string | null; // 备注
builtin: boolean; // 是否内置角色
resourceIds: string[]; // 已授权的资源 ID 集合(存储态,已含祖先补全)
}
```
> ⚠️ **ID 一律按字符串收发**。后端返回的 id 是字符串(雪花 ID 超出 JS Number 安全范围),前端回传原样传字符串即可,不要转 Number。
### 2.3 dataScope 枚举
| 值 | 含义 | 说明 |
|----|------|------|
| 1 | 仅本人 | 只能看到自己创建的数据 |
| 2 | 本部门 | 能看到本部门所有成员的数据 |
| 3 | 本部门及子部门 | 能看到本部门及其下属部门的数据 |
| 4 | 全部数据 | 能看到所有数据 |
> 注:值 5(自定义)存在于枚举中,但角色接口不允许设置,仅 1-4 合法。
---
## 3. 统一响应结构
所有接口返回统一结构:
```typescript
interface Result<T> {
code: number; // 0=成功,非 0=失败
success: boolean; // 是否成功
message: string; // 错误信息(成功时为 null)
data: T; // 业务数据(无数据时为 null)
}
```
分页接口的 `data``PageResult`
```typescript
interface PageResult<T> {
content: T[]; // 数据列表
total: number; // 总条数
size: number; // 每页条数
current: number; // 当前页码(从 1 开始)
pages: number; // 总页数
empty: boolean; // 是否为空结果
}
```
---
## 4. 接口详情
### 4.1 分页查询角色列表
```
POST /api/roles/page
```
**权限码**:`crm:role:list`
**请求参数**(表单字段):
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `current` | number | 否 | 1 | 当前页码 |
| `size` | number | 否 | 10 | 每页条数 |
| `keyword` | string | 否 | - | 按角色名称模糊搜索 |
**请求示例**:
```
POST /api/roles/page
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {token}
current=1&size=10&keyword=销售
```
**响应示例**:
```json
{
"code": 0,
"success": true,
"data": {
"content": [
{
"id": "1",
"roleName": "管理员",
"roleCode": "ROLE_ADMIN",
"dataScope": 4,
"sort": 0,
"remark": null,
"builtin": true
},
{
"id": "2",
"roleName": "销售",
"roleCode": "ROLE_SALES",
"dataScope": 2,
"sort": 1,
"remark": "销售团队角色",
"builtin": false
}
],
"total": 2,
"size": 10,
"current": 1,
"pages": 1,
"empty": false
}
}
```
---
### 4.2 新增或编辑角色
```
POST /api/roles/saveOrUpdate
```
**权限码**:`crm:role:save`
**请求参数**(表单字段):
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `id` | string | 否 | - | 不传=新增,传=编辑 |
| `roleName` | string | 是 | - | 角色名称 |
| `roleCode` | string | 是 | - | 角色编码(编辑内置角色时不可修改) |
| `dataScope` | number | 否 | 1 | 数据范围 1-4 |
| `sort` | number | 否 | 0 | 排序号 |
| `remark` | string | 否 | - | 备注 |
> ⚠️ `builtin` 字段不接受前端传入。新建角色固定 `builtin=false`,内置角色由系统初始化创建。
**校验规则**:
| 规则 | 触发条件 | 错误码 | 错误信息 |
|------|----------|--------|----------|
| 角色名称为空 | `roleName` 为空 | 61010 | 角色名称不能为空 |
| 角色编码为空 | `roleCode` 为空 | 61010 | 角色编码不能为空 |
| 数据范围不合法 | `dataScope` 不在 1-4 | 61010 | 数据范围不合法 |
| 编码已存在 | `roleCode` 与其他角色重复 | 61011 | 角色编码已存在:{roleCode} |
| 内置角色改编码 | 编辑内置角色时修改了 `roleCode` | 61012 | 内置角色的编码不可修改 |
| 非法创建内置角色 | 尝试设置 `builtin=true` | 61012 | 不允许通过接口创建内置角色 |
**新增请求示例**:
```
POST /api/roles/saveOrUpdate
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {token}
roleName=销售&roleCode=ROLE_SALES&dataScope=2&sort=1&remark=销售团队角色
```
**编辑请求示例**:
```
POST /api/roles/saveOrUpdate
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {token}
id=2&roleName=销售&roleCode=ROLE_SALES&dataScope=3&sort=1&remark=改为本部门及子部门
```
**响应**:
```json
{
"code": 0,
"success": true,
"data": null
}
```
---
### 4.3 角色详情
```
GET /api/roles/detail
```
**权限码**:`crm:role:detail`
**请求参数**(URL query):
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `roleId` | string | 是 | 角色 ID |
**请求示例**:
```
GET /api/roles/detail?roleId=2
Authorization: Bearer {token}
```
**响应示例**:
```json
{
"code": 0,
"success": true,
"data": {
"id": "2",
"roleName": "销售",
"roleCode": "ROLE_SALES",
"dataScope": 2,
"sort": 1,
"remark": "销售团队角色",
"builtin": false,
"resourceIds": ["1", "10", "100", "200"]
}
}
```
> `resourceIds` 是存储态的完整授权集合(已含祖先补全)。前端在回显权限树勾选状态时,需要用这个集合与资源树做交集——但只需勾选叶子节点,祖先节点会自动包含在集合中但不需要在前端勾选。
---
### 4.4 分配角色权限资源
```
POST /api/roles/assign-resources
```
**权限码**:`crm:role:assign`
**请求参数**(表单字段):
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `roleId` | string | 是 | 角色 ID |
| `resourceIds` | string | 否 | 逗号分隔的资源 ID,不传或空字符串=清空权限 |
> ⚠️ **前端只需传用户勾选的叶子节点 ID**。后端会自动沿 parentId 向上遍历补全所有祖先节点(ADR-0005),确保菜单可见性链路完整。
> 例如:前端勾选了叶子节点 `100`,其祖先为 `10` → `1`,后端会自动补全为 `{1, 10, 100}` 写入数据库。
**请求示例**:
```
POST /api/roles/assign-resources
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {token}
roleId=2&resourceIds=100,200,300
```
**清空权限示例**:
```
POST /api/roles/assign-resources
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {token}
roleId=2&resourceIds=
```
**响应**:
```json
{
"code": 0,
"success": true,
"data": null
}
```
**实现说明**:
1. 后端调用 `getAncestorIds` 补全全部祖先 ID
2. 先删除该角色的所有旧关联(全量替换,非增量)
3. 批量插入补全后的完整 ID 集合
4. 整个操作在 `@Transactional` 事务中执行
---
### 4.5 删除角色
```
POST /api/roles/delete
```
**权限码**:`crm:role:delete`
**请求参数**(表单字段):
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | string | 是 | 角色 ID |
**请求示例**:
```
POST /api/roles/delete
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {token}
id=2
```
**响应**:
```json
{
"code": 0,
"success": true,
"data": null
}
```
**级联清理说明**:
| 步骤 | 操作 | 说明 |
|------|------|------|
| 1 | 内置角色检查 | `builtin=true` 的角色拒绝删除,返回错误码 61012 |
| 2 | 清理 `sys_role_menu` | 删除该角色与所有菜单/权限点的关联 |
| 3 | 清理 `sys_user_role` | 解除该角色与所有用户的绑定 |
| 4 | 删除角色记录 | 从 `sys_role` 表删除 |
| 5 | 全部在事务中执行 | 任一步骤失败则回滚 |
**内置角色删除失败示例**:
```json
{
"code": 61012,
"success": false,
"message": "内置角色不允许删除"
}
```
---
## 5. 错误码
| 错误码 | 含义 | 触发场景 |
|--------|------|----------|
| 61010 | 角色参数非法 | 角色名称/编码为空、数据范围不合法 |
| 61011 | 角色编码已存在 | 新建或编辑时 roleCode 与其他角色重复 |
| 61012 | 内置角色保护 | 删除内置角色、修改内置角色编码、尝试创建内置角色 |
---
## 6. 权限码与前端按钮控制
角色管理页面的按钮需要根据权限码控制显示:
| 权限码 | 对应按钮 | 说明 |
|--------|----------|------|
| `crm:role:list` | 角色列表页面访问 | 控制菜单可见性 |
| `crm:role:detail` | 查看/编辑按钮 | 点击后调用详情接口 |
| `crm:role:save` | 新增/编辑保存按钮 | 弹窗保存时调用 |
| `crm:role:assign` | 权限分配按钮 | 打开权限树弹窗时调用 |
| `crm:role:delete` | 删除按钮 | 列表行操作列 |
> 这些权限码在系统启动时由 `DataInitializer` 自动创建为 button 类型权限点,并分配给管理员角色。其他角色需要通过权限分配接口授予。
---
## 7. 前端典型流程
### 7.1 角色列表页
```
1. 进入页面 → POST /api/roles/page?current=1&size=10
2. 输入关键词搜索 → POST /api/roles/page?current=1&size=10&keyword=销售
3. 点击"新增" → 弹窗填写 → POST /api/roles/saveOrUpdate
4. 点击"编辑" → GET /api/roles/detail?roleId=xxx → 回填表单 → POST /api/roles/saveOrUpdate
5. 点击"删除" → 确认 → POST /api/roles/delete?id=xxx
```
### 7.2 权限分配弹窗
```
1. 打开弹窗 → 调用 /api/resources/list 获取资源树
2. 回显已选 → GET /api/roles/detail?roleId=xxx → 取 resourceIds 与资源树做交集
3. 用户勾选/取消勾选叶子节点
4. 点击"保存" → POST /api/roles/assign-resources?roleId=xxx&resourceIds=100,200,300
```
> 前端只需提交用户勾选的叶子节点 ID。后端自动补全祖先,无需前端处理。
### 7.3 内置角色处理
- 管理员角色(`builtin=true`)在列表中展示时,删除按钮应置灰或隐藏
- 编辑内置角色时,角色编码字段应设为只读(后端会拒绝修改)