# 增量 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 生命周期完整体验。待评审确认待确认问题后,移交架构师进行前端方案设计。