# 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 ``` ### 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 ``` --- ## 六、安全设计 ### 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/)