Files
wecom_it_smart_desk/docs/02-产品需求/05-增量PRD-OTP首次绑定与重置.md
Simon 400ce3ddcb 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 生产基准配置
2026-07-08 21:54:57 +08:00

279 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 增量 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 生命周期完整体验。待评审确认待确认问题后,移交架构师进行前端方案设计。