910 lines
25 KiB
Markdown
910 lines
25 KiB
Markdown
# IT智能服务台 — 统一入口(Portal)技术设计文档
|
||
|
||
**版本**: v1.1
|
||
**日期**: 2026-06-13
|
||
**作者**: 宋献
|
||
**状态**: 实施中
|
||
|
||
---
|
||
|
||
## 〇、测试环境说明
|
||
|
||
**⚠️ 重要约束**:
|
||
- 本地开发环境无法完成企微 OAuth2 认证
|
||
- 所有前端都通过企微认证,不支持独立登录页面
|
||
- **所有登录相关验证必须在生产服务器 `10.90.5.110` 上进行**
|
||
|
||
**测试流程**:
|
||
1. 将代码部署到生产服务器
|
||
2. 通过企微工作台访问应用
|
||
3. 完成 OAuth2 认证后验证功能
|
||
|
||
---
|
||
|
||
## 一、概述
|
||
|
||
### 1.1 背景
|
||
|
||
当前 IT智能服务台 存在三个独立入口:
|
||
- **用户端** `/itdesk/` — 员工提交工单、查看进度
|
||
- **坐席端** `/itagent/` — IT坐席处理会话、AI辅助
|
||
- **管理端** `/itadmin/` — 系统配置、数据分析
|
||
|
||
**问题**:
|
||
1. 各端认证方式不统一(用户端OAuth2、坐席端用户名+通讯录验证)
|
||
2. 公网可直接访问登录页面,存在暴力破解风险
|
||
3. 用户需要记住多个URL,体验不佳
|
||
|
||
### 1.2 方案目标
|
||
|
||
**统一入口架构**:
|
||
- 所有用户必须通过 **企微工作台 → IT智能服务台应用** 进入
|
||
- 进入时自动检测账户关联的角色
|
||
- 提供卡片选择页面,让用户选择进入哪个端
|
||
- 无坐席/管理角色的用户直接进入用户端
|
||
|
||
**安全目标**:
|
||
- 消除公网可直接访问的登录页面
|
||
- 统一使用企微 OAuth2 认证
|
||
- 管理端仅限内网/VPN访问
|
||
- API端点保留独立认证通道(API Key + IP白名单)
|
||
|
||
---
|
||
|
||
## 二、系统架构
|
||
|
||
### 2.1 整体架构图
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 用户访问流程 │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ 企微工作台 → IT智能服务台应用 │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌─────────────────┐ │
|
||
│ │ OAuth2 授权 │ ← 统一入口,唯一认证点 │
|
||
│ └────────┬────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌─────────────────┐ │
|
||
│ │ 角色检测 + 路由 │ ← 查询数据库中的角色 │
|
||
│ └────────┬────────┘ │
|
||
│ │ │
|
||
│ ┌─────┴─────┬──────────┐ │
|
||
│ ▼ ▼ ▼ │
|
||
│ ┌──────┐ ┌──────┐ ┌──────┐ │
|
||
│ │用户端 │ │坐席端│ │管理端│ │
|
||
│ └──────┘ └──────┘ └──────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 2.2 角色路由逻辑
|
||
|
||
```
|
||
OAuth2 授权完成
|
||
│
|
||
▼
|
||
查询角色列表: GET /api/portal/roles
|
||
│
|
||
├── 仅 user 角色 → 直接跳转 /itdesk/(不显示选择页)
|
||
│
|
||
├── user + agent → 显示选择页(2张卡片)
|
||
│
|
||
├── user + admin → 显示选择页(2张卡片)
|
||
│
|
||
└── user + agent + admin → 显示选择页(3张卡片)
|
||
```
|
||
|
||
### 2.3 URL 路径规划
|
||
|
||
| 端 | 路径 | 说明 |
|
||
|---|------|------|
|
||
| 统一入口 | `/itportal/` | 路由选择页 |
|
||
| 用户端 | `/itdesk/` | 员工提交工单、查看进度 |
|
||
| 坐席端 | `/itagent/` | IT坐席处理会话 |
|
||
| 管理端 | `/itadmin/` | 系统配置、数据分析 |
|
||
| API | `/api/` | 后端接口 |
|
||
|
||
---
|
||
|
||
## 三、数据库设计
|
||
|
||
### 3.1 角色表 (roles)
|
||
|
||
```sql
|
||
CREATE TABLE roles (
|
||
id SERIAL PRIMARY KEY,
|
||
name VARCHAR(50) NOT NULL UNIQUE, -- 角色标识: user/agent/admin
|
||
display_name VARCHAR(100) NOT NULL, -- 显示名称: 用户/坐席/管理员
|
||
description TEXT, -- 角色描述
|
||
permissions JSONB DEFAULT '[]', -- 权限列表(JSON数组)
|
||
is_default BOOLEAN DEFAULT FALSE, -- 是否默认角色(user=true)
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
-- 预置角色
|
||
INSERT INTO roles (name, display_name, description, is_default) VALUES
|
||
('user', '用户', '所有在职员工默认角色,可提交工单、查看进度、浏览知识库', TRUE),
|
||
('agent', '坐席', 'IT支持人员,可处理会话、使用AI辅助、管理工单', FALSE),
|
||
('admin', '管理员', '系统管理员,可配置系统、管理权限、查看数据分析', FALSE);
|
||
```
|
||
|
||
### 3.2 用户角色关联表 (user_roles)
|
||
|
||
```sql
|
||
CREATE TABLE user_roles (
|
||
id SERIAL PRIMARY KEY,
|
||
employee_id VARCHAR(100) NOT NULL, -- 企微 UserID
|
||
role_id INTEGER NOT NULL REFERENCES roles(id),
|
||
source VARCHAR(50) NOT NULL, -- 来源: auto/tag/ehr/manual
|
||
assigned_by VARCHAR(100), -- 分配者(手动分配时记录)
|
||
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
expires_at TIMESTAMP, -- 过期时间(可选)
|
||
UNIQUE(employee_id, role_id)
|
||
);
|
||
|
||
-- 索引
|
||
CREATE INDEX idx_user_roles_employee ON user_roles(employee_id);
|
||
CREATE INDEX idx_user_roles_role ON user_roles(role_id);
|
||
```
|
||
|
||
### 3.3 角色映射规则表 (role_mapping_rules)
|
||
|
||
```sql
|
||
CREATE TABLE role_mapping_rules (
|
||
id SERIAL PRIMARY KEY,
|
||
role_id INTEGER NOT NULL REFERENCES roles(id),
|
||
source_type VARCHAR(50) NOT NULL, -- 来源类型: wecom_tag/ehr_position/manual
|
||
source_value VARCHAR(200) NOT NULL, -- 来源值: 标签名/岗位关键词
|
||
priority INTEGER DEFAULT 0, -- 优先级(数值越大优先级越高)
|
||
is_active BOOLEAN DEFAULT TRUE,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
-- 预置映射规则
|
||
INSERT INTO role_mapping_rules (role_id, source_type, source_value, priority) VALUES
|
||
((SELECT id FROM roles WHERE name='agent'), 'wecom_tag', 'IT坐席', 10),
|
||
((SELECT id FROM roles WHERE name='agent'), 'ehr_position', 'IT支持', 10),
|
||
((SELECT id FROM roles WHERE name='agent'), 'ehr_position', 'IT运维', 10),
|
||
((SELECT id FROM roles WHERE name='agent'), 'ehr_position', '技术支持', 10);
|
||
```
|
||
|
||
### 3.4 Token 存储(Redis)
|
||
|
||
**统一 Token 格式**:
|
||
|
||
```
|
||
Key: user:token:{token}
|
||
Value: {
|
||
"employee_id": "zhangsan",
|
||
"name": "张三",
|
||
"department": "IT支持组",
|
||
"avatar": "https://...",
|
||
"roles": ["user", "agent"],
|
||
"current_role": "agent",
|
||
"login_source": "portal",
|
||
"created_at": "2026-06-12T22:00:00",
|
||
"last_active": "2026-06-12T22:30:00"
|
||
}
|
||
TTL: 8 小时(28800秒)
|
||
```
|
||
|
||
---
|
||
|
||
## 四、API 设计
|
||
|
||
### 4.1 Portal API
|
||
|
||
#### 4.1.1 获取当前用户角色
|
||
|
||
```
|
||
GET /api/portal/roles
|
||
Authorization: Bearer {token}
|
||
|
||
Response:
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"employee_id": "zhangsan",
|
||
"name": "张三",
|
||
"department": "IT支持组",
|
||
"avatar": "https://...",
|
||
"roles": [
|
||
{
|
||
"name": "user",
|
||
"display_name": "用户",
|
||
"description": "所有在职员工默认角色"
|
||
},
|
||
{
|
||
"name": "agent",
|
||
"display_name": "坐席",
|
||
"description": "IT支持人员"
|
||
}
|
||
],
|
||
"current_role": "user"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.1.2 切换当前角色
|
||
|
||
```
|
||
POST /api/portal/switch-role
|
||
Authorization: Bearer {token}
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"new_role": "agent"
|
||
}
|
||
|
||
Response:
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"current_role": "agent",
|
||
"redirect_url": "/itagent/"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.1.3 获取角色对应的入口 URL
|
||
|
||
```
|
||
GET /api/portal/entry/{role_name}
|
||
Authorization: Bearer {token}
|
||
|
||
Response:
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"role": "agent",
|
||
"url": "/itagent/",
|
||
"display_name": "坐席端"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.2 角色管理 API(管理端)
|
||
|
||
#### 4.2.1 获取所有角色
|
||
|
||
```
|
||
GET /api/admin/roles
|
||
Authorization: Bearer {token}
|
||
X-Forwarded-For: {内网IP}
|
||
|
||
Response:
|
||
{
|
||
"code": 0,
|
||
"data": [
|
||
{
|
||
"id": 1,
|
||
"name": "user",
|
||
"display_name": "用户",
|
||
"is_default": true,
|
||
"user_count": 1500
|
||
},
|
||
{
|
||
"id": 2,
|
||
"name": "agent",
|
||
"display_name": "坐席",
|
||
"is_default": false,
|
||
"user_count": 15
|
||
},
|
||
{
|
||
"id": 3,
|
||
"name": "admin",
|
||
"display_name": "管理员",
|
||
"is_default": false,
|
||
"user_count": 3
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### 4.2.2 手动分配角色
|
||
|
||
```
|
||
POST /api/admin/roles/assign
|
||
Authorization: Bearer {token}
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"employee_id": "zhangsan",
|
||
"role_name": "admin",
|
||
"reason": "新任IT组长"
|
||
}
|
||
|
||
Response:
|
||
{
|
||
"code": 0,
|
||
"message": "角色分配成功"
|
||
}
|
||
```
|
||
|
||
#### 4.2.3 撤销角色
|
||
|
||
```
|
||
POST /api/admin/roles/revoke
|
||
Authorization: Bearer {token}
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"employee_id": "zhangsan",
|
||
"role_name": "admin",
|
||
"reason": "岗位调整"
|
||
}
|
||
|
||
Response:
|
||
{
|
||
"code": 0,
|
||
"message": "角色撤销成功"
|
||
}
|
||
```
|
||
|
||
#### 4.2.4 获取角色映射规则
|
||
|
||
```
|
||
GET /api/admin/roles/mapping-rules
|
||
Authorization: Bearer {token}
|
||
|
||
Response:
|
||
{
|
||
"code": 0,
|
||
"data": [
|
||
{
|
||
"id": 1,
|
||
"role_name": "agent",
|
||
"source_type": "wecom_tag",
|
||
"source_value": "IT坐席",
|
||
"priority": 10,
|
||
"is_active": true
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### 4.2.5 创建/更新映射规则
|
||
|
||
```
|
||
POST /api/admin/roles/mapping-rules
|
||
Authorization: Bearer {token}
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"role_name": "agent",
|
||
"source_type": "wecom_tag",
|
||
"source_value": "IT运维",
|
||
"priority": 10,
|
||
"is_active": true
|
||
}
|
||
|
||
Response:
|
||
{
|
||
"code": 0,
|
||
"message": "映射规则创建成功",
|
||
"data": {
|
||
"id": 4
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.3 认证中间件改造
|
||
|
||
#### 4.3.1 统一认证依赖
|
||
|
||
```python
|
||
async def get_current_user(
|
||
token: str = Depends(oauth2_scheme),
|
||
redis_client: Redis = Depends(dep_redis)
|
||
) -> UserInfo:
|
||
"""统一认证中间件:从 Redis 获取用户信息和角色"""
|
||
data = await redis_client.get(f"user:token:{token}")
|
||
if not data:
|
||
raise AppException(1002, "Token 已过期或无效")
|
||
|
||
user_data = json.loads(data)
|
||
|
||
# 更新最后活跃时间
|
||
user_data["last_active"] = datetime.now().isoformat()
|
||
await redis_client.setex(
|
||
f"user:token:{token}",
|
||
TOKEN_TTL,
|
||
json.dumps(user_data)
|
||
)
|
||
|
||
return UserInfo(
|
||
employee_id=user_data["employee_id"],
|
||
name=user_data["name"],
|
||
department=user_data["department"],
|
||
roles=user_data["roles"],
|
||
current_role=user_data["current_role"]
|
||
)
|
||
```
|
||
|
||
#### 4.3.2 角色验证装饰器
|
||
|
||
```python
|
||
def require_role(*required_roles: str):
|
||
"""角色验证装饰器:检查用户是否拥有指定角色之一"""
|
||
def decorator(func):
|
||
@wraps(func)
|
||
async def wrapper(
|
||
*args,
|
||
current_user: UserInfo = Depends(get_current_user),
|
||
**kwargs
|
||
):
|
||
# 检查用户是否有任一所需角色
|
||
user_roles = set(current_user.roles)
|
||
required = set(required_roles)
|
||
|
||
if not user_roles.intersection(required):
|
||
raise AppException(
|
||
4003,
|
||
f"需要以下角色之一: {', '.join(required_roles)}"
|
||
)
|
||
|
||
return await func(*args, current_user=current_user, **kwargs)
|
||
return wrapper
|
||
return decorator
|
||
|
||
# 使用示例
|
||
@router.get("/api/agent/conversations")
|
||
@require_role("agent")
|
||
async def get_conversations(current_user: UserInfo = Depends(get_current_user)):
|
||
# 只有 agent 角色才能访问
|
||
pass
|
||
|
||
@router.get("/api/admin/dashboard")
|
||
@require_role("admin")
|
||
async def get_dashboard(current_user: UserInfo = Depends(get_current_user)):
|
||
# 只有 admin 角色才能访问
|
||
pass
|
||
|
||
@router.get("/api/h5/tickets")
|
||
@require_role("user", "agent", "admin")
|
||
async def get_tickets(current_user: UserInfo = Depends(get_current_user)):
|
||
# 所有角色都可以访问
|
||
pass
|
||
```
|
||
|
||
---
|
||
|
||
## 五、前端设计
|
||
|
||
### 5.1 Portal 前端应用
|
||
|
||
**项目结构**:
|
||
|
||
```
|
||
frontend-portal/
|
||
├── src/
|
||
│ ├── App.vue
|
||
│ ├── main.ts
|
||
│ ├── router/
|
||
│ │ └── index.ts
|
||
│ ├── stores/
|
||
│ │ └── portal.ts # Portal 状态管理
|
||
│ ├── api/
|
||
│ │ └── portal.ts # Portal API 调用
|
||
│ ├── views/
|
||
│ │ ├── PortalSelect.vue # 角色选择页
|
||
│ │ └── PortalLoading.vue # 加载页
|
||
│ └── components/
|
||
│ └── RoleCard.vue # 角色卡片组件
|
||
├── index.html
|
||
├── vite.config.ts
|
||
└── package.json
|
||
```
|
||
|
||
### 5.2 角色选择页 UI
|
||
|
||
**页面布局**:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ │
|
||
│ IT智能服务台 │
|
||
│ │
|
||
│ 选择您要进入的工作台 │
|
||
│ │
|
||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||
│ │ │ │ │ │ │ │
|
||
│ │ 👤 用户 │ │ 🎧 坐席 │ │ ⚙️ 管理 │ │
|
||
│ │ │ │ │ │ │ │
|
||
│ │ 提交工单 │ │ 处理会话 │ │ 系统配置 │ │
|
||
│ │ 查看进度 │ │ AI 辅助 │ │ 数据分析 │ │
|
||
│ │ 知识库 │ │ 知识库 │ │ 权限管理 │ │
|
||
│ │ │ │ │ │ │ │
|
||
│ │ [进入] │ │ [进入] │ │ [进入] │ │
|
||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||
│ │
|
||
│ 当前账号:张三 (IT支持组) │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**RoleCard 组件**:
|
||
|
||
```vue
|
||
<template>
|
||
<div
|
||
class="role-card"
|
||
:class="{ 'role-card--disabled': !available }"
|
||
@click="handleClick"
|
||
>
|
||
<div class="role-card__icon">
|
||
<el-icon :size="48">
|
||
<component :is="icon" />
|
||
</el-icon>
|
||
</div>
|
||
<div class="role-card__title">{{ role.display_name }}</div>
|
||
<div class="role-card__desc">{{ role.description }}</div>
|
||
<el-button
|
||
v-if="available"
|
||
type="primary"
|
||
class="role-card__btn"
|
||
>
|
||
进入
|
||
</el-button>
|
||
<el-tag v-else type="info">无权限</el-tag>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed } from 'vue'
|
||
|
||
interface Role {
|
||
name: string
|
||
display_name: string
|
||
description: string
|
||
}
|
||
|
||
const props = defineProps<{
|
||
role: Role
|
||
available: boolean
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'select', role: Role): void
|
||
}>()
|
||
|
||
const icon = computed(() => {
|
||
switch (props.role.name) {
|
||
case 'user': return 'User'
|
||
case 'agent': return 'Headset'
|
||
case 'admin': return 'Setting'
|
||
default: return 'User'
|
||
}
|
||
})
|
||
|
||
const handleClick = () => {
|
||
if (props.available) {
|
||
emit('select', props.role)
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### 5.3 坐席端改造
|
||
|
||
**认证流程改造**:
|
||
|
||
```typescript
|
||
// 坐席端路由守卫
|
||
router.beforeEach(async (to, from, next) => {
|
||
const agentStore = useAgentStore()
|
||
|
||
// 检查是否已认证
|
||
if (!agentStore.token) {
|
||
// 尝试从 URL 参数获取 token(从 Portal 跳转时传递)
|
||
const token = to.query.token as string
|
||
if (token) {
|
||
agentStore.setToken(token)
|
||
// 清除 URL 中的 token 参数
|
||
next({ ...to, query: {} })
|
||
return
|
||
}
|
||
|
||
// 未认证,跳转到 Portal
|
||
window.location.href = '/itportal/'
|
||
return
|
||
}
|
||
|
||
// 验证角色权限
|
||
try {
|
||
const userInfo = await agentStore.validateRole('agent')
|
||
if (!userInfo.roles.includes('agent')) {
|
||
// 无坐席角色,跳转到 Portal
|
||
window.location.href = '/itportal/'
|
||
return
|
||
}
|
||
next()
|
||
} catch (error) {
|
||
// Token 无效,跳转到 Portal
|
||
window.location.href = '/itportal/'
|
||
}
|
||
})
|
||
```
|
||
|
||
**角色切换入口**:
|
||
|
||
```vue
|
||
<!-- 坐席端右上角菜单 -->
|
||
<template>
|
||
<el-dropdown @command="handleCommand">
|
||
<div class="user-info">
|
||
<el-avatar :src="userStore.avatar" :size="32" />
|
||
<span class="user-name">{{ userStore.name }}</span>
|
||
<el-icon><ArrowDown /></el-icon>
|
||
</div>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item command="switch-user">
|
||
<el-icon><User /></el-icon>
|
||
切换到用户端
|
||
</el-dropdown-item>
|
||
<el-dropdown-item v-if="hasAdminRole" command="switch-admin">
|
||
<el-icon><Setting /></el-icon>
|
||
切换到管理端
|
||
</el-dropdown-item>
|
||
<el-dropdown-item divided command="logout">
|
||
<el-icon><SwitchButton /></el-icon>
|
||
退出登录
|
||
</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { useUserStore } from '@/stores/user'
|
||
|
||
const router = useRouter()
|
||
const userStore = useUserStore()
|
||
|
||
const hasAdminRole = computed(() => {
|
||
return userStore.roles.includes('admin')
|
||
})
|
||
|
||
const handleCommand = async (command: string) => {
|
||
switch (command) {
|
||
case 'switch-user':
|
||
await switchRole('user')
|
||
break
|
||
case 'switch-admin':
|
||
await switchRole('admin')
|
||
break
|
||
case 'logout':
|
||
await logout()
|
||
break
|
||
}
|
||
}
|
||
|
||
const switchRole = async (role: string) => {
|
||
try {
|
||
const { redirect_url } = await api.switchRole(role)
|
||
window.location.href = redirect_url
|
||
} catch (error) {
|
||
ElMessage.error('切换失败')
|
||
}
|
||
}
|
||
|
||
const logout = async () => {
|
||
userStore.clearAuth()
|
||
window.location.href = '/itportal/'
|
||
}
|
||
</script>
|
||
```
|
||
|
||
---
|
||
|
||
## 六、安全设计
|
||
|
||
### 6.1 认证安全
|
||
|
||
| 安全措施 | 说明 | 状态 |
|
||
|----------|------|------|
|
||
| OAuth2 静默授权 | scope=snsapi_base,用户无感知 | ✅ 已实现 |
|
||
| state 参数防 CSRF | 随机 state,回调时验证 | ✅ 已实现 |
|
||
| Token 密码学安全 | secrets.token_urlsafe(32) | ✅ 已实现 |
|
||
| Token TTL 8小时 | Redis 自动过期 | ✅ 已实现 |
|
||
| redirect_uri 白名单 | 生产环境仅允许正式域名 | ⚠️ 需收紧 |
|
||
| 企微 UA 检测 | 非企微 WebView 拒绝访问 | ✅ 已实现 |
|
||
|
||
### 6.2 角色安全
|
||
|
||
| 安全措施 | 说明 | 状态 |
|
||
|----------|------|------|
|
||
| 角色最小权限 | 默认仅 user 角色 | ✅ 设计完成 |
|
||
| 角色来源追溯 | user_roles 表记录 source 和 assigned_by | ✅ 设计完成 |
|
||
| 管理端 IP 白名单 | 仅内网/VPN 可访问 | ⚠️ 待配置 |
|
||
| 管理端紧急通道 | 管理员密码 + 二次验证 | ⚠️ 待设计 |
|
||
|
||
### 6.3 API 安全
|
||
|
||
| 安全措施 | 说明 | 状态 |
|
||
|----------|------|------|
|
||
| API Key 认证 | 外部系统独立认证通道 | ⚠️ 待实现 |
|
||
| API IP 白名单 | 外部系统限制来源 IP | ⚠️ 待实现 |
|
||
| 速率限制 | slowapi 中间件 | ✅ 已实现 |
|
||
|
||
### 6.4 管理端访问控制
|
||
|
||
**Nginx 配置**:
|
||
|
||
```nginx
|
||
# 管理端:仅限内网/VPN 访问
|
||
location /itadmin/ {
|
||
# 允许内网网段
|
||
allow 10.0.0.0/8;
|
||
allow 172.16.0.0/12;
|
||
allow 192.168.0.0/16;
|
||
# 允许 VPN 网段
|
||
allow 10.212.0.0/16;
|
||
# 拒绝其他来源
|
||
deny all;
|
||
|
||
# 反向指向前端应用
|
||
try_files $uri $uri/ /itadmin/index.html;
|
||
}
|
||
|
||
# 管理端 API:同样限制访问来源
|
||
location /api/admin/ {
|
||
allow 10.0.0.0/8;
|
||
allow 172.16.0.0/12;
|
||
allow 192.168.0.0/16;
|
||
allow 10.212.0.0/16;
|
||
deny all;
|
||
|
||
proxy_pass http://backend;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 七、角色映射机制
|
||
|
||
### 7.1 映射流程
|
||
|
||
```
|
||
用户 OAuth2 登录
|
||
│
|
||
▼
|
||
获取企微 access_token
|
||
│
|
||
▼
|
||
查询企微通讯录 → 获取标签、部门、岗位
|
||
│
|
||
▼
|
||
查询 role_mapping_rules 表
|
||
│
|
||
├─ 标签匹配 → agent
|
||
├─ 岗位匹配 → agent
|
||
├─ 数据库 user_roles 表有 admin 记录 → admin
|
||
└─ 默认 → user
|
||
│
|
||
▼
|
||
合并角色列表 → 存入 Redis Token
|
||
```
|
||
|
||
### 7.2 企微标签配置
|
||
|
||
**标签组**:`IT服务台角色`
|
||
|
||
**标签值**:
|
||
- `IT坐席` — 映射为 agent 角色
|
||
- `IT管理员` — 映射为 admin 角色(备用,实际以手动分配为准)
|
||
|
||
**配置步骤**:
|
||
1. 登录企微管理后台
|
||
2. 通讯录 → 标签 → 新建标签组
|
||
3. 添加标签值
|
||
4. 为坐席人员打上 `IT坐席` 标签
|
||
|
||
### 7.3 eHR 字段映射(后续阶段)
|
||
|
||
**映射规则**:
|
||
- 岗位包含 `IT支持`、`IT运维`、`技术支持` → agent
|
||
|
||
**实现方式**:
|
||
- 对接北森 eHR OAuth2 API
|
||
- 定时同步员工岗位信息
|
||
- 根据岗位关键词自动映射角色
|
||
|
||
---
|
||
|
||
## 八、实施计划
|
||
|
||
### 阶段一:角色系统 + 统一 Token(1.5 周)
|
||
|
||
| 序号 | 任务 | 说明 | 工时 |
|
||
|------|------|------|------|
|
||
| 1 | 创建角色表 | roles + user_roles + role_mapping_rules | 2h |
|
||
| 2 | 实现角色 CRUD API | 管理后台管理角色分配 | 4h |
|
||
| 3 | 实现角色映射服务 | 企微标签 → 角色 | 4h |
|
||
| 4 | 改造 Token 存储 | 统一为 user:token:{token} | 4h |
|
||
| 5 | 改造认证中间件 | 统一 get_current_user,支持角色验证 | 4h |
|
||
| 6 | 坐席端认证改造 | 使用统一的 get_current_user | 4h |
|
||
| 7 | 单元测试 | 角色系统测试 | 4h |
|
||
|
||
**小计**:约 26 小时
|
||
|
||
### 阶段二:路由选择页(1 周)
|
||
|
||
| 序号 | 任务 | 说明 | 工时 |
|
||
|------|------|------|------|
|
||
| 8 | 创建 Portal Vue 应用 | 前端项目初始化 | 2h |
|
||
| 9 | 实现角色选择 UI | 卡片选择组件 | 4h |
|
||
| 10 | 改造 OAuth2 回调 | 授权后跳转到 Portal | 4h |
|
||
| 11 | 实现角色切换 API | POST /api/portal/switch-role | 2h |
|
||
| 12 | 坐席端切换入口 | 右上角菜单切换角色 | 2h |
|
||
| 13 | 前端测试 | 端到端测试 | 4h |
|
||
|
||
**小计**:约 18 小时
|
||
|
||
### 阶段三:管理端访问控制(0.5 周)
|
||
|
||
| 序号 | 任务 | 说明 | 工时 |
|
||
|------|------|------|------|
|
||
| 14 | Nginx IP 白名单配置 | 限制 /itadmin/ 访问来源 | 2h |
|
||
| 15 | 管理端 OAuth2 接入 | 主通道:企微授权 | 4h |
|
||
| 16 | 管理员角色分配 | 初始管理员配置 | 1h |
|
||
| 17 | 测试验证 | 内网/外网访问测试 | 2h |
|
||
|
||
**小计**:约 9 小时
|
||
|
||
### 阶段四:安全加固 + 测试(1 周)
|
||
|
||
| 序号 | 任务 | 说明 | 工时 |
|
||
|------|------|------|------|
|
||
| 18 | 收紧 redirect_uri 白名单 | 生产环境仅允许正式域名 | 1h |
|
||
| 19 | 移除所有降级逻辑 | 生产环境禁用 Mock 登录等 | 2h |
|
||
| 20 | 端到端测试 | 完整流程测试 | 4h |
|
||
| 21 | 安全审计 | 第三方安全测试 | 4h |
|
||
| 22 | 文档更新 | 更新部署文档、用户手册 | 2h |
|
||
|
||
**小计**:约 13 小时
|
||
|
||
---
|
||
|
||
**总工时**:约 66 小时(约 2.5 周全职工作)
|
||
|
||
---
|
||
|
||
## 九、风险与缓解措施
|
||
|
||
| 风险 | 等级 | 缓解措施 |
|
||
|------|------|----------|
|
||
| 企微 OAuth2 故障 | 高 | 管理端保留紧急通道 |
|
||
| 企微标签配置错误 | 中 | 角色映射有日志,可追溯 |
|
||
| Token 切换时的竞态条件 | 低 | Redis 事务保证原子性 |
|
||
| 内网 IP 白名单误配 | 中 | 配置前在测试环境验证 |
|
||
| 现有坐席登录方式变更 | 中 | 变更前手动通知坐席 |
|
||
|
||
---
|
||
|
||
## 十、附录
|
||
|
||
### 10.1 术语表
|
||
|
||
| 术语 | 说明 |
|
||
|------|------|
|
||
| Portal | 统一入口,路由选择页 |
|
||
| OAuth2 | 开放授权协议,用于企微身份验证 |
|
||
| Bearer Token | 用于 API 认证的令牌 |
|
||
| RBAC | 基于角色的访问控制 |
|
||
| eHR | 人力资源管理系统(北森) |
|
||
|
||
### 10.2 参考文档
|
||
|
||
- [企业微信 OAuth2 开发文档](https://developer.work.weixin.qq.com/document/path/91022)
|
||
- [企业微信 通讯录管理](https://developer.work.weixin.qq.com/document/path/90193)
|
||
- [FastAPI 安全最佳实践](https://fastapi.tiangolo.com/tutorial/security/)
|