Merge branch 'Wren' into shiy-login

This commit is contained in:
2025-11-24 18:42:29 +08:00
5 changed files with 2802 additions and 174 deletions
+27 -1
View File
@@ -81,6 +81,7 @@ axiosInstance.interceptors.request.use(
(config) => { (config) => {
// 检查是否在白名单中 // 检查是否在白名单中
if (isInAuthWhitelist(config.url)) { if (isInAuthWhitelist(config.url)) {
console.log('🔓 [Request Interceptor] URL在白名单中,跳过Authorization:', config.url);
return config; return config;
} }
@@ -89,12 +90,24 @@ axiosInstance.interceptors.request.use(
const token = localStorage.getItem('access_token'); const token = localStorage.getItem('access_token');
if (token) { if (token) {
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`;
console.log('🔑 [Request Interceptor] 添加Authorization头:', {
url: config.url,
method: config.method,
hasToken: !!token,
tokenPreview: token.substring(0, 20) + '...'
});
} else {
console.warn('⚠️ [Request Interceptor] 没有找到access_token:', {
url: config.url,
localStorage: Object.keys(localStorage)
});
} }
} }
return config; return config;
}, },
(error) => { (error) => {
console.error('❌ [Request Interceptor] 请求拦截器错误:', error);
return Promise.reject(error); return Promise.reject(error);
} }
); );
@@ -114,9 +127,21 @@ export class AuthenticationError extends Error {
*/ */
axiosInstance.interceptors.response.use( axiosInstance.interceptors.response.use(
(response) => { (response) => {
console.log('✅ [Response Interceptor] 请求成功:', {
url: response.config.url,
status: response.status,
statusText: response.statusText
});
return response; return response;
}, },
(error) => { (error) => {
console.error('❌ [Response Interceptor] 请求失败:', {
url: error.config?.url,
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data
});
if (isAxiosError(error) && error.response?.status === 401) { if (isAxiosError(error) && error.response?.status === 401) {
// 检查是否在错误容忍白名单中 // 检查是否在错误容忍白名单中
const requestUrl = error.config?.url; const requestUrl = error.config?.url;
@@ -442,7 +467,8 @@ export async function apiRequest<T>(
// 检查API返回的状态码 // 检查API返回的状态码
const data = response.data; const data = response.data;
if (data && typeof data === 'object' && 'code' in data && data.code !== 0) { // 修复:支持code=0PostgREST)和code=200RBAC API)两种成功响应
if (data && typeof data === 'object' && 'code' in data && data.code !== 0 && data.code !== 200) {
const errorMessage = data.message || data.msg || '未知错误'; const errorMessage = data.message || data.msg || '未知错误';
console.error(`API请求失败: ${errorMessage} - ${url}`); console.error(`API请求失败: ${errorMessage} - ${url}`);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+224
View File
@@ -505,6 +505,230 @@
} }
} }
/* ==================== 表单样式 ==================== */
.role-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-size: 14px;
font-weight: 500;
color: #303133;
}
.form-group label.required::after {
content: ' *';
color: var(--color-error);
}
.form-input,
.form-textarea,
.form-select {
padding: 10px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
color: #303133;
transition: border-color 0.2s;
}
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(0, 104, 74, 0.1);
}
.form-input.error,
.form-textarea.error,
.form-select.error {
border-color: var(--color-error);
}
.form-input:disabled,
.form-textarea:disabled,
.form-select:disabled {
background: #f5f7fa;
cursor: not-allowed;
color: #909399;
}
.form-hint {
font-size: 12px;
color: #909399;
line-height: 1.5;
}
.form-error {
font-size: 12px;
color: var(--color-error);
line-height: 1.5;
}
.form-notice {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border-radius: 4px;
font-size: 14px;
background: #ecf5ff;
border: 1px solid #b3d8ff;
color: #606266;
}
.form-notice.warning {
background: #fef0f0;
border-color: #fbc4c4;
color: #606266;
}
.form-notice i {
font-size: 18px;
color: #409eff;
}
.form-notice.warning i {
color: var(--color-error);
}
/* 分配用户模态框 */
.assign-user-modal {
display: flex;
flex-direction: column;
gap: 16px;
}
/* 全选栏 */
.select-all-bar {
padding: 12px 16px;
background: #f5f7fa;
border-radius: 6px;
border: 1px solid #e4e7ed;
}
.select-all-bar .user-checkbox-item {
padding: 0;
margin: 0;
}
.select-all-bar .user-checkbox-item:hover {
background: transparent;
}
.select-all-bar .user-name {
font-weight: 600;
color: #303133;
}
/* 用户复选框列表 */
.users-checkbox-list {
max-height: 400px;
overflow-y: auto;
border: 1px solid #e4e7ed;
border-radius: 6px;
padding: 12px;
background: #fafbfc;
}
.user-checkbox-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s;
}
.user-checkbox-item:hover {
background: #e6e8eb;
}
.user-checkbox-item input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: var(--color-primary);
}
.user-checkbox-item .user-info {
flex: 1;
min-width: 0;
}
.user-checkbox-item .user-name {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 4px;
}
.user-checkbox-item .user-meta {
font-size: 12px;
color: #909399;
}
/* 搜索框 */
.search-box {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border: 1px solid #dcdfe6;
border-radius: 6px;
background: white;
margin-bottom: 16px;
}
.search-box i {
font-size: 18px;
color: #909399;
}
.search-box input {
flex: 1;
border: none;
outline: none;
font-size: 14px;
color: #303133;
}
.search-box input::placeholder {
color: #c0c4cc;
}
/* 无权限卡片 */
.no-permission-card {
max-width: 800px;
margin: 60px auto;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.no-permission-card .empty-state {
text-align: center;
}
.no-permission-card h2 {
margin: 0;
}
.no-permission-card p {
margin: 0;
}
/* ==================== 响应式布局 ==================== */
/* 响应式布局 */ /* 响应式布局 */
@media (max-width: 1200px) { @media (max-width: 1200px) {
.permissions-container { .permissions-container {
+709
View File
@@ -0,0 +1,709 @@
# 用户权限管理接口 - 前端对接文档
## 📋 文档信息
- **版本**: v2.0
- **更新日期**: 2025-01-24
- **API基础URL**: `http://YOUR_HOST:8000`
- **认证方式**: JWT Bearer Token
---
## ⚠️ 重要提示 - 前端对接必读
### 1. Token过期处理
**问题现象**: API返回 `code: 4001, msg: "token参数无效"`
**原因**: JWT Token已过期(默认有效期1小时)
**解决方案**:
```javascript
// 检查Token是否过期
const isTokenExpired = (token) => {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
return Date.now() >= payload.exp * 1000;
} catch {
return true;
}
};
// 在发送请求前检查
if (isTokenExpired(token)) {
await login(); // 重新登录获取新Token
}
```
### 2. 实际角色ID对应关系
⚠️ **数据库中的实际角色ID** (请以实际数据为准):
| 角色名称 | role_key | 实际role_id | 说明 |
|---------|----------|------------|------|
| 省级管理员 | provincial_admin | **52** | 拥有完整RBAC管理权限 |
| 市级管理员 | admin | **1** | 负责本地区业务管理 |
| 普通员工 | common | **2** | 只能查看和管理自己的数据 |
**调用示例**:
```javascript
// ✅ 正确:使用实际的role_id
const provincialAdminUsers = await getRoleUsers(52);
const adminUsers = await getRoleUsers(1);
const commonUsers = await getRoleUsers(2);
// ❌ 错误:使用Mock数据的ID
const wrongUsers = await getRoleUsers(3); // 数据库中不存在
```
### 3. 响应格式说明
所有接口统一返回格式:
```json
{
"code": 200,
"msg": "success",
"data": { ... }
}
```
部分接口(如用户列表)直接返回数据对象:
```json
{
"users": [...],
"total": 10
}
```
### 4. 权限要求
| 接口模块 | 权限要求 | 说明 |
|---------|----------|------|
| RBAC管理接口 | `system:rbac:manage` | 仅provincial_admin角色 |
| 用户管理接口 | JWT认证即可 | 所有登录用户 |
| 路由权限接口 | JWT认证即可 | 所有登录用户 |
---
## 📚 接口总览
### 一、RBAC管理接口(18个)
**基础路径**: `/api/v3/rbac`
#### 1.1 角色管理(5个接口)
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/roles` | 获取角色列表(支持分页、搜索) |
| GET | `/roles/{role_id}` | 获取角色详情 |
| POST | `/roles` | 创建角色 |
| PUT | `/roles/{role_id}` | 更新角色 |
| DELETE | `/roles/{role_id}` | 删除角色(支持force参数) |
#### 1.2 权限管理(5个接口)
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/permissions` | 获取权限列表(支持树形/平铺) |
| GET | `/permissions/{permission_id}` | 获取权限详情 |
| POST | `/permissions` | 创建权限 |
| PUT | `/permissions/{permission_id}` | 更新权限 |
| DELETE | `/permissions/{permission_id}` | 删除权限 |
#### 1.3 角色权限关联(4个接口)
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/roles/{role_id}/permissions` | 获取角色的所有权限 |
| POST | `/roles/{role_id}/permissions` | 批量分配权限给角色(支持替换/追加) |
| PUT | `/roles/{role_id}/permissions/{permission_id}` | 更新单个权限配置 |
| DELETE | `/roles/{role_id}/permissions/{permission_id}` | 移除角色权限 |
#### 1.4 用户角色管理(4个接口)
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/roles/{role_id}/users` | 获取拥有某角色的用户列表 |
| GET | `/users/{user_id}/roles` | 获取用户的所有角色 |
| POST | `/users/{user_id}/roles` | 为用户分配角色 |
| DELETE | `/users/{user_id}/roles/{role_id}` | 移除用户角色 |
### 二、用户管理接口(3个)
**基础路径**: `/admin/users`
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/users` | 获取用户列表(支持分页、搜索) |
| GET | `/organizations` | 获取组织架构(树形结构) |
| GET | `/organizations/flat` | 获取组织列表(扁平结构) |
### 三、路由权限接口(5个)
**基础路径**: `/rbac`
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/user/routes` | 获取当前用户可访问路由(树形) |
| GET | `/user/routes/flat` | 获取当前用户可访问路由(扁平) |
| GET | `/roles/{role_id}/routes` | 获取角色可访问路由 |
| PUT | `/roles/{role_id}/routes` | **批量更新角色路由权限** ⭐新增 |
| GET | `/check-route` | 检查路由访问权限 |
---
## 📖 详细接口说明
## 一、RBAC管理接口
### 1.1 获取角色列表
**接口**: `GET /api/v3/rbac/roles`
**Query参数**:
```javascript
{
page: 1, // 页码
page_size: 20, // 每页数量
role_key: "", // 角色标识过滤(可选)
role_name: "", // 角色名称模糊搜索(可选)
include_system: true // 是否包含系统角色(可选)
}
```
**响应示例**:
```json
{
"code": 200,
"message": "success",
"data": {
"total": 3,
"page": 1,
"page_size": 20,
"items": [
{
"id": 52,
"role_key": "provincial_admin",
"role_name": "省级管理员",
"description": "省级权限,可管理所有地区",
"data_scope": "ALL",
"is_system": true,
"user_count": 1,
"permission_count": 15
},
{
"id": 1,
"role_key": "admin",
"role_name": "市级管理员",
"data_scope": "DEPT",
"is_system": true,
"user_count": 6
}
]
}
}
```
---
### 1.2 创建角色
**接口**: `POST /api/v3/rbac/roles`
**请求体**:
```json
{
"role_key": "department_leader",
"role_name": "部门负责人",
"description": "负责部门日常管理",
"data_scope": "DEPT",
"metadata": {}
}
```
**响应示例**:
```json
{
"code": 200,
"message": "角色创建成功",
"data": {
"id": 6,
"role_key": "department_leader",
"role_name": "部门负责人",
"data_scope": "DEPT",
"is_system": false
}
}
```
---
### 1.3 获取角色的所有用户
**接口**: `GET /api/v3/rbac/roles/{role_id}/users`
**Query参数**:
```javascript
{
page: 1,
page_size: 20,
area: "梅州", // 按地区过滤(可选)
username: "admin" // 用户名模糊搜索(可选)
}
```
**响应示例**:
```json
{
"code": 200,
"message": "success",
"data": {
"total": 6,
"page": 1,
"page_size": 20,
"items": [
{
"user_id": 8,
"username": "梅州烟草",
"nick_name": "梅州烟草",
"area": "梅州管理员账号",
"ou_name": "梅州管理员账号",
"phone_number": null,
"email": null,
"assigned_at": "2025-11-18T01:40:25.030949+00:00"
}
]
}
}
```
---
### 1.4 批量分配权限给角色
**接口**: `POST /api/v3/rbac/roles/{role_id}/permissions`
**请求体**:
```json
{
"permissions": [
{
"permission_id": 10,
"grant_type": "GRANT",
"data_scope": "ALL"
},
{
"permission_id": 11,
"grant_type": "GRANT",
"data_scope": "DEPT"
}
],
"replace": true
}
```
**参数说明**:
- `replace`: true=替换全部权限,false=追加权限
- `grant_type`: GRANT(授予)或 DENY(拒绝)
- `data_scope`: ALL(全部数据)/ DEPT(本部门)/ SELF(仅自己)
**响应示例**:
```json
{
"code": 200,
"message": "权限分配成功",
"data": {
"role_id": 2,
"assigned_count": 2
}
}
```
---
### 1.5 为用户分配角色
**接口**: `POST /api/v3/rbac/users/{user_id}/roles`
**请求体**:
```json
{
"role_ids": [1, 2]
}
```
**响应示例**:
```json
{
"code": 200,
"message": "角色分配成功",
"data": {
"user_id": 20,
"username": "test_user",
"assigned_roles": [
{"role_id": 1, "role_name": "市级管理员"},
{"role_id": 2, "role_name": "普通员工"}
]
}
}
```
**注意事项**:
- ⚠️ 不能给自己分配角色(防止提权)
- ⚠️ provincial_admin角色只能由省级管理员分配
- ⚠️ admin角色只能给本地区用户分配角色
---
## 二、用户管理接口
### 2.1 获取用户列表
**接口**: `GET /admin/users/users`
**Query参数**:
```javascript
{
page: 1,
page_size: 20,
ou_id: "000", // 组织ID过滤(可选)
is_leader: true, // 是否领导过滤(可选)
search: "admin" // 搜索关键词(可选)
}
```
**响应示例**:
```json
{
"users": [
{
"id": 5,
"username": "admin",
"nick_name": "admin",
"ou_id": "000",
"ou_name": "test",
"is_leader": true,
"status": 0
}
],
"total": 10
}
```
⚠️ **已知问题**: 当前版本 `total` 字段返回0,请以 `users` 数组长度为准。
---
### 2.2 获取组织架构(树形)
**接口**: `GET /admin/users/organizations`
**Query参数**:
```javascript
{
include_users: true // 是否包含用户信息
}
```
**响应示例**:
```json
{
"organizations": [
{
"ou_id": "000",
"ou_name": "总部",
"parent_ou_id": null,
"level": 0,
"children": [
{
"ou_id": "0000000A1ML",
"ou_name": "梅州市局",
"parent_ou_id": "000",
"level": 1,
"users": [
{
"id": 8,
"username": "梅州烟草",
"nick_name": "梅州烟草",
"ou_id": "0000000A1ML",
"ou_name": "梅州市局"
}
]
}
],
"users": []
}
],
"total_organizations": 5,
"total_users": 10
}
```
---
## 三、路由权限接口
### 3.1 获取当前用户可访问路由
**接口**: `GET /rbac/user/routes`
**响应示例**:
```json
{
"code": 200,
"msg": "success",
"data": {
"user_id": 5,
"username": "admin",
"routes": [
{
"id": 1,
"route_path": "/",
"route_name": "Home",
"route_title": "首页",
"component": "Layout",
"parent_id": null,
"icon": "home",
"sort_order": 0,
"is_hidden": false,
"is_cache": true,
"meta": {},
"children": [
{
"id": 11,
"route_path": "/dashboard",
"route_name": "Dashboard",
"route_title": "工作台",
"parent_id": 1
}
]
}
],
"routes_flat": [
{"id": 1, "route_path": "/", "route_name": "Home"},
{"id": 11, "route_path": "/dashboard", "route_name": "Dashboard"}
]
}
}
```
**说明**:
- `routes`: 树形结构(用于构建菜单)
- `routes_flat`: 扁平结构(用于路由守卫权限检查)
---
### 3.2 获取角色可访问路由
**接口**: `GET /rbac/roles/{role_id}/routes`
**响应示例**:
```json
{
"code": 200,
"msg": "success",
"data": {
"role_id": 2,
"routes": [
{"id": 1, "route_path": "/", "route_name": "Home", "route_title": "首页"},
{"id": 11, "route_path": "/dashboard", "route_name": "Dashboard"}
]
}
}
```
---
### 3.3 批量更新角色路由权限 ⭐新增
**接口**: `PUT /rbac/roles/{role_id}/routes`
**功能说明**:
- 批量更新指定角色的路由权限
- 采用**替换模式**:先删除现有所有关联,再插入新关联
- 自动清除相关缓存(角色缓存 + 用户缓存)
**请求体**:
```json
{
"route_ids": [1, 11, 12, 3, 31, 32, 2, 21],
"permission": "RW"
}
```
**参数说明**:
- `route_ids`: 路由ID列表(必填)
- `permission`: 权限类型,可选值:
- `R`: 只读权限
- `W`: 只写权限
- `RW`: 读写权限(默认)
**响应示例**:
```json
{
"code": 200,
"msg": "success",
"data": {
"role_id": 2,
"assigned_count": 8,
"removed_count": 5,
"route_ids": [1, 11, 12, 3, 31, 32, 2, 21]
}
}
```
**前端调用示例**:
```javascript
const updateRoleRoutes = async (roleId, routeIds) => {
const response = await fetch(`/rbac/roles/${roleId}/routes`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
route_ids: routeIds,
permission: 'RW'
})
});
const result = await response.json();
if (result.code === 200) {
console.log(`成功分配 ${result.data.assigned_count} 个路由`);
console.log(`移除了 ${result.data.removed_count} 个旧路由`);
}
return result;
};
// 使用示例
await updateRoleRoutes(2, [1, 11, 12, 3, 31, 32, 2, 21]);
```
**注意事项**:
- ⚠️ 所有 `route_ids` 必须存在且未删除
- ⚠️ 使用事务确保数据一致性
- ⚠️ 会自动清除所有受影响用户的路由缓存
---
### 3.4 检查路由访问权限
**接口**: `GET /rbac/check-route`
**Query参数**:
```javascript
{
route_path: "/system/users"
}
```
**响应示例**:
```json
{
"code": 200,
"msg": "success",
"data": {
"route_path": "/system/users",
"has_access": true
}
}
```
---
## 🚨 错误码说明
### HTTP状态码
| 状态码 | 说明 | 处理建议 |
|--------|------|---------|
| 200 | 成功 | - |
| 400 | 请求参数错误 | 检查请求体格式和必填字段 |
| 401 | 未认证 | Token过期或无效,需要重新登录 |
| 403 | 无权限 | 显示权限不足提示 |
| 404 | 资源不存在 | 检查ID是否正确 |
| 409 | 冲突 | 如role_key重复 |
| 500 | 服务器错误 | 联系后端排查 |
### 常见错误处理
```javascript
// 统一错误处理
const handleError = (error) => {
if (error.response?.status === 401) {
// Token过期,跳转登录
localStorage.removeItem('token');
window.location.href = '/login';
} else if (error.response?.status === 403) {
alert('权限不足');
} else if (error.response?.data?.detail) {
alert(error.response.data.detail);
} else {
alert('操作失败,请稍后重试');
}
};
```
---
## 💡 常见问题FAQ
### Q1: 为什么所有接口返回 code: 4001?
**A**: Token已过期,请重新登录获取新Token
### Q2: role_id应该用哪个?文档里写的1,2,3还是52,1,2
**A**: 使用实际数据库中的ID:
- provincial_admin = **52**
- admin = **1**
- common = **2**
### Q3: 需要用Mock数据吗?
**A**: **不需要**,所有接口已完整实现,直接调用真实API
### Q4: 如何判断当前用户是否有RBAC管理权限?
```javascript
// 方式1:检查JWT中的user_role
const payload = JSON.parse(atob(token.split('.')[1]));
const isProvincialAdmin = payload.user_role === 'provincial_admin';
// 方式2:调用权限检查接口(推荐)
const routes = await fetch('/rbac/user/routes');
const hasRBACAccess = routes.data.routes.some(r => r.route_path === '/rbac');
```
### Q5: 如何实现角色权限页面的路由分配功能?
**A**: 使用新的批量更新接口 `PUT /rbac/roles/{role_id}/routes`:
1. 通过 `GET /rbac/roles/{role_id}/routes` 获取当前角色已有路由
2. 用户选择要分配的路由ID列表
3. 调用 `PUT /rbac/roles/{role_id}/routes` 提交更新
4. 接口会自动替换所有路由权限并清除缓存
### Q6: total字段为什么返回0
**A**: `/admin/users/users` 接口的 `total` 字段当前版本存在bug,请以 `users` 数组长度为准。该问题已记录,将在后续版本修复。
### Q7: 批量更新路由权限后,用户需要重新登录吗?
**A**: 不需要。接口会自动清除相关用户的路由缓存,用户刷新页面即可看到新权限。
---
## 📦 前端开发清单
- [ ] 移除所有RBAC相关的Mock数据
- [ ] 使用实际的role_id52, 1, 2
- [ ] 实现Token过期自动刷新机制
- [ ] 处理401/403错误(跳转登录/权限提示)
- [ ] 使用 `PUT /rbac/roles/{role_id}/routes` 实现路由权限分配
- [ ] 处理 `total` 字段为0的情况(用数组长度代替)
---
## 📞 技术支持
如有问题,请联系后端开发团队。
**文档版本**: v2.0
**最后更新**: 2025-01-24
**维护者**: Backend Team