feat: OTP首次绑定 + 三端登录修复 + 管理端权限修复 (2026-07-08)

OTP首次绑定:
- 新增统一 OTP 路由 /auth/otp-* (otp.py + router.py)
- 坐席端 OTP 绑定面板 (OtpBindPanel.vue)
- 管理端 OTP 管理列表 (MfaManage.vue)
- agent_login 签发半认证 token 支持首次绑定流程

三端登录修复:
- 坐席/管理端去掉'返回扫码登录'按钮
- 管理端改为二维码始终可见+轮询扫码状态
- 员工端 /itdesk/ 改为 alias 直接服务 H5 (不再301重定向)
- docker-compose 添加 h5 volume 挂载

管理端权限修复:
- 扫码登录改用 get_user_roles() 替代写死 roles=['agent']
- get_user_roles() 增加 agents.role 回退
- 新增 GET /admin/roles/user-roles 端点
- 角色管理页加载用户角色分配数据

文档更新:
- OTP PRD + 系统设计文档
- 故障排查手册 v1.1 (新增6案例)
- nginx 生产基准配置
This commit is contained in:
Simon
2026-07-08 21:54:57 +08:00
parent 6f0fbbb066
commit 400ce3ddcb
27 changed files with 2822 additions and 312 deletions
@@ -0,0 +1,278 @@
# 增量 PRD — OTP 首次绑定流程及管理后台清除功能
> **文档版本**: v1.0(增量)
> **创建日期**: 2026-07-06
> **产品经理**: Alice
> **状态**: 待评审
> **关联文档**:
> - `docs/02-产品需求/04-增量PRD-三端认证重构.md` — 母 PRD(认证重构,AUTH-P1-3 预留"首次绑定OTP引导"
> - `docs/02-产品需求/02-产品需求文档PRD-v1.2-20260704.md` — 主 PRD
> - `backend/app/api/otp.py` — OTP 六个端点已实现
> - `backend/app/api/agents.py:274` — `mfa_enabled=False` 时直通登录(无绑定引导)
> - `frontend-agent/src/views/Login.vue` — OTP 输入框已实现 `v-if="requireOtp"`,无绑定流程
---
## 0. 文档目标
本增量 PRD 旨在为 **坐席端 OTP 首次绑定引导****管理后台 OTP 清除功能** 提供完整的交互定义。解决当前三大缺口:
1. 新坐席 `mfa_enabled=False` → 账密通过后直通工作台,无绑定引导途径
2. 坐席 OTP 丢失(换手机 / Authenticator 误删)后无法自助重新绑定
3. 管理端缺少「清除已绑定 OTP」管理入口(后端已有 `/otp-admin-reset`,前端未接入)
> **注意**:后端六个 OTP 端点(bind / verify / unbind / status / admin-reset / admin-users)已在 `backend/app/api/otp.py` 实现完成,本 PRD 聚焦**前端交互与流程设计**。
---
## 1. 项目信息
| 字段 | 值 |
|------|------|
| 项目名称 | `wecom_it_smart_desk` |
| 文档语言 | 中文 |
| Programming Language | Vite + Vue3 + ElementPlus(前端不变) |
| 原始需求 | 三端认证重构(PRD-04)已确定 OTP 接口与路由,本增量补充首次绑定引导与管理员重置的前端交互 |
---
## 2. 产品定义
### 2.1 产品目标
1. **坐席首次登录闭环**:新坐席账密验证通过后自动引导绑定 OTP,绑定成功后才能进入工作台——堵死"无 OTP 直通"漏洞
2. **丢失恢复路径**:坐席 OTP 丢失后,通过管理后台申请重置 → 重新走首次绑定流程
3. **管理员可见可控**:管理员可在后台查看全量坐席 OTP 绑定状态,并一键清除指定坐席的绑定
### 2.2 用户故事
| ID | 角色 | 用户故事 | 优先级 |
|----|------|----------|--------|
| US-OTP-1 | 新坐席 (new agent) | 作为首次登录的 IT 坐席,我希望账密验证通过后系统自动弹出二维码引导我绑定 OTP,扫码+输入验证码后即可进入工作台 | P0 |
| US-OTP-2 | 已有 OTP 的坐席 (agent) | 作为已绑定 OTP 的坐席,我登录时输入账密后直接弹出 OTP 输入框验证,不要展示首次绑定流程 | P0 |
| US-OTP-3 | OTP 丢失的坐席 (agent) | 作为手机丢失/Authenticator 误删的坐席,我希望先在管理后台申请重置 OTP,然后下次登录时重新走绑定流程,最后能自助解绑+重新绑定 | P1 |
| US-OTP-4 | 管理员 (admin) | 作为管理员,我希望在管理后台查看所有坐席的 OTP 绑定状态,并能在坐席丢失 OTP 时一键清除其绑定,使其下次登录强制重新绑定 | P0 |
| US-OTP-5 | 坐席 (agent) | 作为已绑定 OTP 的坐席,我希望在设置页面能自助解绑 OTP(需验证当前 OTP 码),解绑后下次登录直接进入首次绑定流程 | P1 |
---
## 3. 需求池
### 3.1 P0 — 必须实现
| ID | 需求 | 说明 / 验收标准 |
|----|------|-----------------|
| OTP-P0-1 | 首次登录 OTP 绑定引导 | 坐席账密验证通过 → 后端返回 `require_otp_bind: true`(而非 `require_otp: true`)→ 前端弹出 OTP 绑定面板(二维码 + secret 明文 + 6 位验证码输入框 + 确认按钮)→ 调用 `/api/auth/otp-bind` 获取 secret/QR → 用户扫码后输入 6 位码 → 调用 `/api/auth/otp-verify` → 验证通过后 `mfa_enabled=True` → 签发完整 token → 进入工作台 |
| OTP-P0-2 | 管理后台 OTP 绑定状态列表 | 管理后台新增「OTP 管理」页面,列表展示:坐席姓名、employee_id、OTP 绑定状态(已绑定/未绑定)、绑定时间、最后验证时间。调用 `GET /api/auth/otp-admin-users` |
| OTP-P0-3 | 管理后台清除 OTP 绑定 | 管理员可在 OTP 管理页面点击「清除绑定」按钮 → 二次确认弹窗 → 调用 `POST /api/auth/otp-admin-reset/{employee_id}` → 该坐席下次登录强制走首次绑定流程 |
| OTP-P0-4 | 后端区分"需要OTP验证"和"需要OTP绑定"两种状态 | `POST /agents/login``mfa_enabled=False` 时,不直接签发 token,而是返回 `require_otp_bind: true`;前端据此展示绑定面板。`mfa_enabled=True` 时保持现有 `require_otp: true` |
### 3.2 P1 — 应该实现
| ID | 需求 | 说明 / 验收标准 |
|----|------|-----------------|
| OTP-P1-1 | 坐席端自助解绑+重新绑定 | 坐席在「个人设置」页面看到 OTP 绑定状态,可点击「解绑 OTP」→ 输入当前 OTP 验证码 → 调用 `POST /api/auth/otp-unbind` → 解绑成功后 `mfa_enabled=False` → 下次登录自动进入首次绑定流程(复用 OTP-P0-1 面板) |
| OTP-P1-2 | 重新绑定流程 | 坐席已解绑后,登录时走 OTP-P0-1 绑定引导(与首次登录完全相同)。也可在设置页面提供「重新绑定」入口,主动触发绑定流程 |
| OTP-P1-3 | 绑定流程中的"跳过"选项 | 首次绑定向导中提供"暂不绑定,稍后设置"按钮 → 跳过绑定直接进入工作台 → 在导航栏/设置页显示警告徽章提醒完成绑定 → 限制跳过次数或有效期(如仅可跳过 1 次) |
### 3.3 P2 — 锦上添花
| ID | 需求 | 说明 / 验收标准 |
|----|------|-----------------|
| OTP-P2-1 | OTP 绑定操作日志 | 管理员可查看 OTP 绑定/解绑/清除操作记录(操作人、操作时间、操作类型、目标坐席),调用现有后端日志即可 |
| OTP-P2-2 | 批量清除 OTP | 管理员可多选坐席后批量清除 OTP 绑定 |
| OTP-P2-3 | 绑定截止日强制提醒 | 跳过绑定的坐席在 N 天后强制弹出绑定面板,不允许继续跳过 |
---
## 4. UI 设计稿说明
> 原型图目录:`docs/04-原型设计/prototypes-原型图/`
### 4.1 坐席端:首次绑定引导面板(新增)
**触发时机**`handleLogin` 收到 `require_otp_bind: true` 后展示。
**布局**(在现有 `agent-login-v1.html` 登录卡片内,替换 OTP 输入区):
```
┌──────────────────────────────────────┐
│ 🛠️ IT智能服务台 │
│ 坐席工作台 │
├──────────────────────────────────────┤
│ │
│ 🔐 首次登录 — 绑定 OTP 二次验证 │
│ │
│ ┌──────────────────────────────┐ │
│ │ │ │
│ │ [二维码 QR Code PNG] │ │
│ │ 200×200 px │ │
│ │ │ │
│ └──────────────────────────────┘ │
│ │
│ 请使用 Google Authenticator │
│ 或 Microsoft Authenticator 扫码 │
│ │
│ ── 或手动输入密钥 ── │
│ 密钥:XXXX XXXX XXXX XXXX │
│ [📋 复制] │
│ │
│ ┌────────────────────────────────┐ │
│ │ 输入 6 位验证码 │ │
│ └────────────────────────────────┘ │
│ │
│ [ 验证并完成绑定 ] ← 主 CTA │
│ [ 暂不绑定,稍后设置 ] ← 次要 │
│ │
└──────────────────────────────────────┘
```
### 4.2 坐席端:登录页 OTP 验证(已有,无需改动)
保持现有 `agent-login-v1.html` 的 OTP 输入框逻辑:
- `v-if="requireOtp"` 渲染 6 位 OTP 输入框
- 按键文案「验证 OTP」
### 4.3 管理端:OTP 管理页面(新增)
**入口**:「坐席管理」→ 新增 Tab「OTP 绑定状态」,或在左侧菜单新增「OTP 管理」。
**列表字段**
| 列名 | 数据来源 |
|------|----------|
| 坐席姓名 | `GET /otp-admin-users``name` |
| 企微 ID | `employee_id` |
| OTP 状态 | `mfa_enabled` → 「已绑定」/「未绑定」标签 |
| 绑定时间 | `mfa_bound_at` |
| 最后验证 | `mfa_last_verified_at` |
| 操作 | 「清除绑定」按钮(仅 `mfa_enabled=true` 时可用) |
**清除确认弹窗**
```
⚠️ 确认清除 OTP 绑定?
坐席「张三 (zhangsan)」的 OTP 二次验证将被清除。
该坐席下次登录时需重新绑定。
[取消] [确认清除]
```
### 4.4 坐席端:个人设置页 OTP 管理(P1,新增)
在坐席端「设置」页新增「OTP 二次验证」面板:
- 已绑定状态:显示「✅ 已绑定|绑定时间:xxxx-xx-xx」,提供「解绑 OTP」按钮
- 解绑操作:弹出输入框要求输入当前 6 位 OTP 验证码 → 确认后调用 `/api/auth/otp-unbind`
- 未绑定状态:显示「⚠️ 未绑定 — [立即绑定]」按钮 → 弹出与首次登录相同的绑定面板
---
## 5. 交互流程
### 5.1 首次绑定流程(P0 核心流程)
```
坐席打开 /itagent/login
→ 选择"账号密码登录"
→ 输入账号、密码 → 点击登录
→ POST /agents/login {user_id, password}
→ 后端返回:{ require_otp_bind: true, user_id, name }
→ 前端展示 OTP 绑定面板(隐藏账密表单)
→ 前端调用 POST /api/auth/otp-bind → 获得 { secret, otpauth_url, qr_code_base64 }
→ 渲染二维码 + secret 明文
→ 坐席用 Authenticator 扫码(或手动输入 secret
→ Authenticator 生成 6 位 TOTP 码
→ 坐席在输入框中输入 6 位码 → 点击"验证并完成绑定"
→ 前端调用 POST /api/auth/otp-verify { otp_code }
→ 后端:校验通过 → mfa_enabled=True, mfa_bound_at=now → 返回 { verified: true }
→ 前端再次调用 POST /agents/login {user_id, password}(或后端在 verify 成功后直接签发 token
→ 进入工作台
```
### 5.2 OTP 丢失恢复流程(P0 + P1
```
坐席发现手机丢失/Authenticator 误删
→ 联系管理员(企微/电话)
→ 管理员登录管理后台 → OTP 管理页面
→ 找到该坐席 → 点击"清除绑定" → 确认
→ POST /api/auth/otp-admin-reset/{employee_id}
→ 坐席下次登录时 mfa_enabled=False → 触发首次绑定流程(5.1)
```
### 5.3 坐席自助解绑+重新绑定流程(P1)
```
坐席正常登录进入工作台
→ 打开"设置" → OTP 二次验证
→ 点击"解绑 OTP"
→ 弹窗输入当前 6 位 OTP 验证码 → 确认
→ POST /api/auth/otp-unbind { otp_code }
→ 解绑成功 → mfa_enabled=False
→ 下次登录自动进入首次绑定流程(5.1)
```
---
## 6. 关键设计决策
### 6.1 后端响应区分 `require_otp` vs `require_otp_bind`
`POST /agents/login` 当前逻辑(`agents.py:274`):
```python
if agent.mfa_enabled:
if not body.otp_code:
return {"require_otp": True, ...} # 已有 OTP → 要求验证
else:
# 校验 OTP → 签发 token
# 当 mfa_enabled=False 时,直接 fall through 到签发 token ← 问题所在
```
**建议改为**
```python
if agent.mfa_enabled:
if not body.otp_code:
return {"require_otp": True, "message": "请输入OTP动态码", ...}
else:
# 校验 OTP → 签发 token
else:
# 未绑定 OTP → 引导绑定
return {"require_otp_bind": True, "message": "首次登录请先绑定OTP二次验证", "user_id": agent.user_id, "name": agent.name}
```
### 6.2 绑定面板是"替换登录表单"还是"新页面跳转"
**建议**:在登录卡片内**替换**(隐藏账密表单,显示绑定面板),保持用户在同一页面上下文内,避免跳转带来的 token 状态管理复杂度。绑定成功后直接跳转工作台。
### 6.3 OTP 绑定成功后是否需要重新调用 login
**建议**`POST /api/auth/otp-verify``mfa_enabled=False` 的绑定场景下,验证成功后**直接返回完整 token**(类似于注册+登录合并),避免前端二次调用 login。后端 `otp.py:verify_otp` 中当 `mfa_enabled` 从 False 变为 True 时,除了写 Redis 标记,还应签发 JWT token 一并返回。
---
## 7. 待确认问题
| # | 问题 | 建议 | 影响 |
|---|------|------|------|
| Q1 | 首次绑定能否"跳过"? | 建议允许跳过 1 次(P1),在导航栏持续提醒"请完成 OTP 绑定",3 天后强制弹出绑定面板。**也可 P0 阶段先不允许跳过**,简化首版逻辑。 | 用户体验 vs 安全刚性 |
| Q2 | 扫码登录的坐席是否需要 OTP? | 三端认证重构决策3 要求「所有登录均需 OTP」。如果是,扫码登录也需要区分 `require_otp` vs `require_otp_bind`。但扫码登录本身已是企微身份强验证,再叠加 OTP 可能过度。**建议与架构师确认**。 | 扫码登录流程复杂度 |
| Q3 | OTP verify 绑定场景是否直接返回 token? | 建议是(见 §6.3),避免前端二次调用 login 的状态同步问题。需要修改 `otp.py:verify_otp`:绑定场景(首次)额外返回 token。 | 后端改动量小 |
| Q4 | 管理后台的 OTP 清除操作是否需要审计日志? | 建议 P2 补充,P0 阶段先记 logger。后端已有 `logger.info`(如 `otp.py:327`)。 | 合规性 |
| Q5 | 绑定面板中 secret 是否明文展示? | 建议默认展示(带复制按钮),用折叠/展开切换"手动输入密钥"区域。这是标准 TOTP 实践,Google/Microsoft Authenticator 都支持手动输入。 | 无安全风险(secret 仅在绑定过程中临时展示) |
---
## 8. 与母 PRD 的衔接关系
| 母 PRD 条目 | 衔接 |
|-------------|------|
| AUTH-P1-3「首次绑定 OTP 引导」 | 本增量 PRD 将其从 P1 升级为 **P0**,并给出完整交互定义 |
| AUTH-P0-5 OTP 接口统一 | 复用已有 `/api/auth/otp-bind` / `otp-verify` / `otp-unbind` / `otp-status` |
| AUTH-P0-4 OTP 输入框渲染时机 | 补充:首次绑定时渲染的是**绑定面板**(二维码+输入框),而非仅 OTP 输入框 |
| CTRT-P0-1 响应契约 | 所有 OTP 接口均遵循统一信封,前端直接消费 inner data |
---
> **文档结束** — 本增量 PRD 作为三端认证重构的子增量,聚焦 OTP 生命周期完整体验。待评审确认待确认问题后,移交架构师进行前端方案设计。