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 生命周期完整体验。待评审确认待确认问题后,移交架构师进行前端方案设计。
@@ -0,0 +1,512 @@
# 系统架构设计 — OTP 首次绑定与重置
> **文档版本**: v1.0
> **创建日期**: 2026-07-06
> **架构师**: Bob(高见远)
> **关联 PRD**: `docs/02-产品需求/05-增量PRD-OTP首次绑定与重置.md`
> **关联母文档**: `docs/02-产品需求/04-增量PRD-三端认证重构.md`
---
## Part A: 系统设计
### 1. 实现方案与框架选型
#### 1.1 核心技术挑战
| 挑战 | 描述 | 策略 |
|------|------|------|
| **登录态区分** | 后端需区分"需要 OTP 验证"(已绑定)和"需要 OTP 绑定"(未绑定)两种状态 | `agents.py` 新增 `else` 分支,返回 `require_otp_bind: true` |
| **绑定即登录** | OTP 首次绑定成功后,用户不应重新输入账密 | `otp-verify` 绑定场景下直接签发 token 返回 |
| **前端 API 迁移** | 现有坐席/管理端 API 适配层使用旧端点 `/mfa/*``/admin/mfa/*`,需迁移到统一 `/auth/otp-*` | 逐步迁移,保留旧端点兼容过渡期 |
| **绑定面板嵌入** | PRD 要求绑定面板嵌入登录卡片内(非独立页面) | 新建 `OtpBindPanel.vue` 组件,在 `Login.vue``v-if="requireOtpBind"` 条件渲染 |
#### 1.2 框架与库(沿用现有栈,无新增)
| 层级 | 技术选型 | 说明 |
|------|----------|------|
| 后端框架 | FastAPI + SQLAlchemy Async | 已有,不新增 |
| OTP 服务 | `pyotp` + `qrcode` | 已有 `MFAService`,不新增 |
| 前端坐席端 | Vite + Vue 3 + Element Plus + Pinia | 已有,不新增 |
| 前端管理端 | Vite + Vue 3 + Element Plus + Pinia | 已有,不新增 |
| Redis | `redis.asyncio` | 已有,用于 MFA 验证状态缓存 |
#### 1.3 架构模式
```
┌─────────────────────────────────────────────────────────┐
│ nginx (反向代理) │
│ /api/auth/otp-* → backend │
└────────────┬────────────────────────────┬────────────────┘
│ │
┌────────▼────────┐ ┌────────▼────────┐
│ frontend-agent │ │ frontend-admin │
│ (坐席端 SPA) │ │ (管理端 SPA) │
│ │ │ │
│ Login.vue │ │ MfaManage.vue │
│ ├─ 账密表单 │ │ ├─ 列表+搜索 │
│ ├─ OTP 输入框 │ │ ├─ 清除绑定按钮 │
│ └─ OtpBindPanel │ │ └─ 确认弹窗 │
│ (新增组件) │ │ │
└────────┬─────────┘ └────────┬─────────┘
│ │
└──────────┬──────────────────┘
┌─────────▼──────────┐
│ backend (FastAPI) │
│ │
│ agents.py │
│ └─ agent_login: │
│ mfa_enabled? │
│ ├─ True+无码 │
│ │ → require_otp│
│ ├─ True+有码 │
│ │ → 验证→token │
│ └─ False │
│ → require_ │
│ otp_bind ★ │
│ │
│ otp.py │
│ ├─ otp-bind │
│ ├─ otp-verify ★ │
│ │ (绑定场景返token)│
│ ├─ otp-unbind │
│ ├─ otp-status │
│ ├─ otp-admin-users│
│ └─ otp-admin-reset│
└────────────────────┘
```
---
### 2. 文件列表
#### 2.1 后端
| 相对路径 | 操作 | 说明 |
|----------|------|------|
| `backend/app/api/agents.py` | **【修改】** | `agent_login` 增加 `else` 分支:`mfa_enabled=False``require_otp_bind: true` |
| `backend/app/api/otp.py` | **【修改】** | `verify_otp` 绑定场景(`mfa_enabled=False`)校验成功后设置 `mfa_enabled=True``mfa_bound_at=now`,并签发返回 token |
| `backend/app/schemas/mfa.py` | **【修改】** | 新增 `MFAVerifyBindResponse`(含 `token` 字段) |
#### 2.2 前端 — 坐席端
| 相对路径 | 操作 | 说明 |
|----------|------|------|
| `frontend-agent/src/api/otp.ts` | **【新增】** | 统一 OTP API 适配层(bind / verify / unbind / status),使用新端点 `/auth/otp-*` |
| `frontend-agent/src/stores/agent.ts` | **【修改】** | `login()` 方法处理 `require_otp_bind` 响应(不再 throw Error,返回标记供 UI 消费) |
| `frontend-agent/src/views/Login.vue` | **【修改】** | 新增 `requireOtpBind` 状态 + `OtpBindPanel` 组件渲染 |
| `frontend-agent/src/components/OtpBindPanel.vue` | **【新增】** | 可复用 OTP 绑定面板(二维码 + secret + 验证码输入 + 确认按钮 + "暂不绑定"按钮) |
| `frontend-agent/src/views/Settings.vue` | **【新增】** | 坐席设置页,含 OTP 管理面板(状态显示 + 解绑 + 重新绑定)— P1 |
| `frontend-agent/src/router/index.ts` | **【修改】** | 新增 `/settings` 路由 — P1 |
#### 2.3 前端 — 管理端
| 相对路径 | 操作 | 说明 |
|----------|------|------|
| `frontend-admin/src/api/mfa.ts` | **【修改】** | 切换 API 路径到 `/auth/otp-admin-users``/auth/otp-admin-reset`,更新 TS 类型字段名 |
| `frontend-admin/src/views/MfaManage.vue` | **【修改】** | 更新字段映射(`mfa_enabled`/`mfa_bound_at`/`mfa_last_verified_at`),"重置 MFA"→"清除绑定",更新确认弹窗文案 |
| `frontend-admin/src/components/Sidebar.vue` | **【修改】** | 在"运营管理"分区下新增「OTP 管理」菜单项 |
| `frontend-admin/src/router/index.ts` | **【修改】** | 路由标题更新为「OTP 管理」 |
---
### 3. 数据结构与接口
#### 3.1 后端 Schema 变更
```mermaid
classDiagram
class MFAVerifyRequest {
+str otp_code
}
class MFAVerifyResponse {
+bool verified
+int expires_in
}
class MFAVerifyBindResponse {
+bool verified
+int expires_in
+str token
+str user_id
+str name
+str role
}
class MFABindStartResponse {
+str secret
+str otpauth_url
+str qr_code_base64
}
class MFAStatusResponse {
+bool bound
+bool enabled
+datetime last_verified_at
}
MFAVerifyRequest --> MFAVerifyResponse : "mfa_enabled=True → 仅验证"
MFAVerifyRequest --> MFAVerifyBindResponse : "mfa_enabled=False → 绑定+签发token"
```
#### 3.2 核心 API 契约
| 端点 | 方法 | 鉴权 | 说明 | 变更 |
|------|------|------|------|------|
| `/api/auth/otp-status` | GET | `get_current_user` | 查询绑定状态 | 无变更 |
| `/api/auth/otp-bind` | POST | `get_current_user` | 生成 secret + QR | 无变更 |
| `/api/auth/otp-verify` | POST | `get_current_user` | 校验 OTP → **绑定场景额外返回 token** | **★ 行为变更** |
| `/api/auth/otp-unbind` | POST | `get_current_user` | 用户主动解绑 | 无变更 |
| `/api/auth/otp-admin-users` | GET | `require_role("admin")` | 管理员查看全量坐席 OTP 状态 | 无变更 |
| `/api/auth/otp-admin-reset/{employee_id}` | POST | `require_role("admin")` | 管理员清除指定坐席 OTP | 无变更 |
| `/api/agents/login` | POST | 无(登录接口) | **mfa_enabled=False → require_otp_bind** | **★ 行为变更** |
#### 3.3 `POST /api/agents/login` 响应契约变更
**变更前**(当前 `agents.py:274`):
```json
// mfa_enabled=True 且未传 otp_code
{ "code": 0, "data": { "require_otp": true, "message": "请输入OTP动态码", "user_id": "...", "name": "...", "role": "..." } }
// mfa_enabled=False → 直接签发 token(问题)
{ "code": 0, "data": { "token": "...", "user_id": "...", ... } }
```
**变更后**
```json
// mfa_enabled=True 且未传 otp_code — 不变
{ "code": 0, "data": { "require_otp": true, "message": "请输入OTP动态码", "user_id": "...", "name": "...", "role": "..." } }
// mfa_enabled=False — 新增 ★
{ "code": 0, "data": { "require_otp_bind": true, "message": "首次登录请先绑定OTP二次验证", "user_id": "...", "name": "...", "role": "..." } }
```
#### 3.4 `POST /api/auth/otp-verify` 响应契约变更
**变更前**(当前 `otp.py:verify_otp`):
```json
// mfa_enabled=False → 直接返回 verified=false
{ "code": 0, "data": { "verified": false, "expires_in": 0 } }
```
**变更后**
```json
// mfa_enabled=False + 校验通过 → 绑定并签发 token ★
{ "code": 0, "data": { "verified": true, "expires_in": 1800, "token": "...", "user_id": "...", "name": "...", "role": "..." } }
// mfa_enabled=True + 校验通过 → 不变
{ "code": 0, "data": { "verified": true, "expires_in": 1800 } }
```
---
### 4. 程序调用流程
#### 4.1 首次登录 OTP 绑定流程(P0 核心)
```mermaid
sequenceDiagram
participant User as 坐席
participant LoginVue as Login.vue
participant AgentStore as agentStore
participant OtpBind as OtpBindPanel.vue
participant API as apiClient
participant Backend as FastAPI Backend
participant Redis as Redis
User->>LoginVue: 输入账号密码 → 点击登录
LoginVue->>AgentStore: login(userId, password, undefined)
AgentStore->>API: POST /api/agents/login {user_id, password}
API->>Backend: agent_login()
Note over Backend: agent.mfa_enabled == False
Backend-->>API: { require_otp_bind: true, user_id, name, role }
API-->>AgentStore: { require_otp_bind: true, ... }
AgentStore-->>LoginVue: return { require_otp_bind: true }
Note over LoginVue: 隐藏账密表单<br/>显示 OtpBindPanel
LoginVue->>OtpBind: requireOtpBind = true
OtpBind->>API: POST /api/auth/otp-bind
API->>Backend: bind_otp()
Note over Backend: 生成 secret + QR
Backend-->>API: { secret, otpauth_url, qr_code_base64 }
API-->>OtpBind: { secret, otpauth_url, qr_code_base64 }
Note over OtpBind: 渲染二维码 + secret 明文
User->>User: 用 Authenticator 扫码(或手动输入 secret
User->>OtpBind: 输入 6 位验证码 → 点击"验证并完成绑定"
OtpBind->>API: POST /api/auth/otp-verify { otp_code }
API->>Backend: verify_otp()
Note over Backend: mfa_enabled=False (绑定场景)<br/>校验 TOTP → 通过<br/>设置 mfa_enabled=True<br/>设置 mfa_bound_at=now
Backend->>Redis: mark_verified(employee_id, TTL=1800)
Backend->>Backend: 签发 JWT token
Backend-->>API: { verified: true, token, user_id, name, role }
API-->>OtpBind: { verified: true, token, ... }
OtpBind-->>LoginVue: emit('bind-success', { token, user_id, name })
LoginVue->>AgentStore: 保存 token → 设置 agentInfo
LoginVue->>LoginVue: router.push('/workspace')
```
#### 4.2 管理后台清除 OTP 绑定流程
```mermaid
sequenceDiagram
participant Admin as 管理员
participant MfaVue as MfaManage.vue
participant API as apiClient
participant Backend as FastAPI Backend
participant DB as PostgreSQL
participant Redis as Redis
Admin->>MfaVue: 访问 OTP 管理页面
MfaVue->>API: GET /api/auth/otp-admin-users
API->>Backend: admin_list_otp_users()
Backend->>DB: SELECT * FROM agents
DB-->>Backend: agent rows
Backend-->>API: [{employee_id, name, mfa_enabled, mfa_bound_at, ...}]
API-->>MfaVue: 渲染表格
Admin->>MfaVue: 点击某坐席的「清除绑定」
MfaVue->>MfaVue: ElMessageBox 二次确认弹窗
Admin->>MfaVue: 确认清除
MfaVue->>API: POST /api/auth/otp-admin-reset/{employee_id}
API->>Backend: admin_reset_otp(employee_id)
Backend->>DB: UPDATE agents SET mfa_secret=NULL, mfa_enabled=False, mfa_bound_at=NULL
Backend->>Redis: clear_verified(employee_id)
Backend-->>API: { success: true }
API-->>MfaVue: ElMessage.success('已清除')
MfaVue->>API: 重新加载列表
```
#### 4.3 坐席自助解绑流程(P1)
```mermaid
sequenceDiagram
participant User as 坐席
participant Settings as Settings.vue
participant API as apiClient
participant Backend as FastAPI Backend
participant DB as PostgreSQL
participant Redis as Redis
User->>Settings: 打开设置页 → OTP 管理面板
Settings->>API: GET /api/auth/otp-status
API->>Backend: get_otp_status()
Backend-->>API: { bound: true, enabled: true, last_verified_at }
API-->>Settings: 显示「✅ 已绑定」
User->>Settings: 点击「解绑 OTP」
Settings->>Settings: 弹窗:输入当前 OTP 验证码
User->>Settings: 输入 6 位码 → 确认
Settings->>API: POST /api/auth/otp-unbind { otp_code }
API->>Backend: unbind_otp()
Backend->>Backend: MFAService.verify_code() → 通过
Backend->>DB: UPDATE SET mfa_secret=NULL, mfa_enabled=False, mfa_bound_at=NULL
Backend->>Redis: clear_verified(employee_id)
Backend-->>API: { success: true }
API-->>Settings: ElMessage.success('OTP 已解绑')
Settings->>Settings: 刷新状态 → 显示「⚠️ 未绑定 — 立即绑定」
```
---
### 5. 待确认问题与假设
| # | 问题 | 假设 | 影响 |
|---|------|------|------|
| Q1 | 首次绑定能否"跳过" | **P0 阶段不允许跳过**。简化首版逻辑,堵死无 OTP 直通漏洞。P1 阶段再引入"暂不绑定"按钮。 | 首次绑定向导无跳过按钮 |
| Q2 | 扫码登录的坐席是否需要 OTP? | **P0 阶段暂不处理**。扫码登录已有企微身份强验证,且当前 `agent_login` 中对已绑定 OTP 的坐席仍要求 OTP。本增量聚焦账密登录的绑定引导。 | 扫码登录不做额外改动 |
| Q3 | OTP verify 绑定场景是否直接返回 token? | **是**。修改 `otp.py:verify_otp`,绑定场景额外返回 token。避免前端二次 login。 | 后端改动量小,前端只需一次请求 |
| Q4 | 管理后台旧端点 `/admin/mfa/*` 是否需要保留? | **保留兼容过渡期**。前端切换到新端点,旧端点暂时保留不删,待下个迭代清理。 | 无风险 |
| Q5 | `OtpBindPanel.vue` 与已有 `MfaBind.vue` 的关系? | `OtpBindPanel` 是轻量嵌入组件(嵌入登录卡片内),`MfaBind.vue` 是独立全页组件(`/mfa-bind` 路由)。绑定流程 UI 逻辑可复用,但布局不同。**建议新建 `OtpBindPanel` 组件,从 `MfaBind.vue` 提取共享的二维码渲染 + 验证逻辑**。 | 两个组件共存,按场景使用 |
---
## Part B: 任务分解
### 6. 所需依赖包
**无新增依赖**。所有功能基于已有技术栈:
```
- Vue 3 + Element Plus(前端已有)
- Pinia(状态管理已有)
- pyotp + qrcode(后端已有 MFAService
- FastAPI + SQLAlchemy Async(后端已有)
- redis.asyncioRedis 客户端已有)
```
### 7. 任务列表(按依赖关系排序)
| Task ID | 任务名称 | 源文件 | 依赖 | 优先级 |
|---------|----------|--------|------|--------|
| **T01** | 后端 OTP 绑定流程改造 | `backend/app/api/agents.py`【修改】<br/>`backend/app/api/otp.py`【修改】<br/>`backend/app/schemas/mfa.py`【修改】 | 无 | **P0** |
| **T02** | 管理后台 OTP 管理页完整更新 | `frontend-admin/src/api/mfa.ts`【修改】<br/>`frontend-admin/src/views/MfaManage.vue`【修改】<br/>`frontend-admin/src/components/Sidebar.vue`【修改】<br/>`frontend-admin/src/router/index.ts`【修改】 | T01(需后端端点就绪) | **P0** |
| **T03** | 坐席端登录页 OTP 绑定面板 | `frontend-agent/src/api/otp.ts`【新增】<br/>`frontend-agent/src/stores/agent.ts`【修改】<br/>`frontend-agent/src/views/Login.vue`【修改】<br/>`frontend-agent/src/components/OtpBindPanel.vue`【新增】 | T01(需后端端点就绪) | **P0** |
| **T04** | 坐席端设置页 OTP 管理 | `frontend-agent/src/views/Settings.vue`【新增】<br/>`frontend-agent/src/router/index.ts`【修改】<br/>`frontend-agent/src/api/otp.ts`【修改】 | T03(依赖 otp.ts API 适配层) | **P1** |
#### T01 详细说明 — 后端 OTP 绑定流程改造
**修改点 1**`backend/app/api/agents.py:274``agent_login` 函数):
```python
# 当前:只有 if agent.mfa_enabled: ...
# 改造后:添加 else 分支
if agent.mfa_enabled:
if not body.otp_code:
return success_response(data={
"require_otp": True,
"message": "请输入OTP动态码",
"user_id": agent.user_id,
"name": agent.name,
"role": agent.role,
})
else:
if not MFAService.verify_code(agent.mfa_secret, body.otp_code, valid_window=1):
raise AppException(1006, "OTP验证码错误,请重新输入")
else:
# ★ 新增:未绑定 OTP → 引导绑定
return success_response(data={
"require_otp_bind": True,
"message": "首次登录请先绑定OTP二次验证",
"user_id": agent.user_id,
"name": agent.name,
"role": agent.role,
})
```
**修改点 2**`backend/app/api/otp.py:verify_otp`(第 213-218 行):
```python
# 当前:mfa_enabled=False → 直接返回 verified=false
# 改造后:mfa_enabled=False 且 mfa_secret 存在(已调用过 otp-bind)→ 走绑定校验逻辑
if not agent.mfa_enabled and agent.mfa_secret:
# ★ 绑定场景:校验 OTP → 设置 enabled + bound_at → 签发 token
if not MFAService.verify_code(agent.mfa_secret, body.otp_code):
logger.warning(f"OTP bind verify 验证码错误: agent={agent.user_id}")
return success_response(data=MFAVerifyResponse(verified=False, expires_in=0).model_dump())
now = datetime.now()
agent.mfa_enabled = True
agent.mfa_bound_at = now
agent.mfa_last_verified_at = now
db.add(agent)
await db.flush()
await MFAService.mark_verified(redis, agent.user_id, MFA_VERIFIED_TTL_SECONDS)
# 签发 token
from app.services.token_service import TokenService
from app.dependencies import get_redis as _get_redis_dep
redis_client = await _get_redis_dep()
token_service = TokenService(redis_client)
token = await token_service.create_token(
employee_id=agent.user_id,
name=getattr(agent, 'name', '') or '',
roles=[agent.role] if agent.role else [],
login_source="agent",
)
logger.info(f"OTP bind+verify 成功: agent={agent.user_id}")
return success_response(data={
"verified": True,
"expires_in": MFA_VERIFIED_TTL_SECONDS,
"token": token,
"user_id": agent.user_id,
"name": getattr(agent, 'name', '') or '',
"role": agent.role or '',
})
# 原有逻辑:已绑定场景保持不变
if not agent.mfa_enabled or not agent.mfa_secret:
return success_response(data=MFAVerifyResponse(verified=False, expires_in=0).model_dump())
# ... 后续不变
```
**修改点 3**`backend/app/schemas/mfa.py`:新增 Schema(可选,也可用 dict 直接返回)。
#### T02 详细说明 — 管理后台 OTP 管理页完整更新
1. **`mfa.ts`**API 路径从 `/admin/mfa/users``/auth/otp-admin-users``/admin/mfa/reset/{id}``/auth/otp-admin-reset/{id}`。TS 类型字段名从 `bound`/`bound_at` 改为 `mfa_enabled`/`mfa_bound_at`
2. **`MfaManage.vue`**:字段映射 `row.bound``row.mfa_enabled``row.bound_at``row.mfa_bound_at`;按钮文案"重置 MFA"→"清除绑定";确认弹窗文案按 PRD §4.3 更新。
3. **`Sidebar.vue`**:在"📋 运营管理"分组添加 `<el-menu-item index="/mfa-manage">` 菜单项。
4. **`router/index.ts`**`meta.title` 更新为 `'OTP 管理'`
#### T03 详细说明 — 坐席端登录页 OTP 绑定面板
1. **`otp.ts`**:新建 API 适配层,封装 `bindOtp()` / `verifyOtp()` / `getOtpStatus()` / `unbindOtp()`,使用新端点 `/auth/otp-*`
2. **`agent.ts:login()`**:处理 `require_otp_bind` 响应——不再 throw Error,返回 `{ require_otp_bind: true, user_id, name, role }` 供 Login.vue 消费。
3. **`Login.vue`**:新增 `requireOtpBind` ref;当 `result.require_otp_bind` 为 true 时,隐藏账密表单 + OTP 输入框,显示 `<OtpBindPanel>`;监听 `bind-success` 事件保存 token 并跳转。
4. **`OtpBindPanel.vue`**:新建可复用组件,包含:二维码渲染(`<img :src="data:image/png;base64,...">`)、secret 明文展示 + 复制按钮、6 位验证码输入框、"验证并完成绑定"主按钮、"暂不绑定,稍后设置"次要按钮(P0 阶段隐藏)。
#### T04 详细说明 — 坐席端设置页 OTP 管理(P1)
1. **`Settings.vue`**:新建设置页面,含「OTP 二次验证」面板:已绑定时显示状态+解绑按钮;未绑定时显示警告+绑定按钮。解绑流程:弹窗输入 OTP → `POST /auth/otp-unbind`
2. **`router/index.ts`**:新增路由 `{ path: '/settings', component: () => import('@/views/Settings.vue'), meta: { title: '个人设置', requiresAuth: true } }`
3. **`otp.ts`**:补充 `unbindOtp(otpCode)``getOtpStatus()` 的完整调用。
### 8. 共享知识
以下约定适用于所有任务、所有文件:
```
## 响应契约
- 所有 API 响应统一格式:{ code: 0, data: {...}, message: "success" }
- apiClient 拦截器已自动解包 data 层(Scheme A),前端直接消费 inner data
- 错误:{ code: 非0, message: "错误描述" } → 前端 catch 中取 error.message
## OTP 状态常量
- require_otp: true → 已绑定 OTP,需要输入 6 位码验证
- require_otp_bind: true → 未绑定 OTP,需要展示绑定面板(二维码 + 输入验证码)
- mfa_enabled: true/false → 数据库字段,标记 OTP 是否已启用
- mfa_bound_at: datetime → 首次绑定成功时间(NULL 表示从未绑定)
- mfa_secret: string/NULL → TOTP 密钥(仅绑定过程中临时存储,解绑后清空)
## 端点路径规范
- 坐席端/管理端统一使用 /auth/otp-* 前缀(新端点)
- 旧端点 /mfa/* 、/admin/mfa/* 保留兼容但前端不再调用
- nginx 剥离 /api 前缀后,对外即 /api/auth/otp-*
## 前端命名约定
- OtpBindPanel.vue: 嵌入登录卡片内的轻量绑定面板(P0)
- MfaBind.vue: 独立全页绑定向导(已有,保留用于 /mfa-bind 路由)
- 按钮文案:后端用"清除绑定",坐席端用"解绑 OTP"
## 错误处理
- OTP 验证码错误 → 返回 verified=false(不抛异常),前端显示红色提示,允许重试
- 解绑时 OTP 错误 → 后端抛 AppException(INVALID_PARAMETER),前端显示错误消息
- 已绑定用户重复调用 otp-bind → 后端拒绝,前端需先判断状态
## Redis Key
- mfa:verified:{employee_id} = "1"TTL = 1800s30 分钟验证窗口)
- 绑定成功后写该 key(等同于已验证)
- 解绑/管理员清除时删除该 key
```
### 9. 任务依赖图
```mermaid
graph TD
T01["T01: 后端 OTP 绑定流程改造<br/>(agents.py + otp.py + schemas/mfa.py)<br/>P0"]
T02["T02: 管理后台 OTP 管理页<br/>(mfa.ts + MfaManage.vue + Sidebar.vue + router)<br/>P0"]
T03["T03: 坐席端登录 OTP 绑定面板<br/>(otp.ts + agent.ts + Login.vue + OtpBindPanel.vue)<br/>P0"]
T04["T04: 坐席端设置页 OTP 管理<br/>(Settings.vue + router + otp.ts)<br/>P1"]
T01 --> T02
T01 --> T03
T03 --> T04
```
> **说明**:T02 和 T03 可并行开发(均依赖 T01,互不依赖)。T04 依赖 T03(需要 `otp.ts` API 适配层)。
---
> **文档结束** — 请工程师按 T01 → T02/T03(并行) → T04 顺序实施。
@@ -1,7 +1,10 @@
# 00 · 标准故障排查手册
> **版本**: v1.0 | **日期**: 2026-07-07 | **维护人**: 宋献 / 助理
> **定位**: 所有故障排查前**首先查看本手册**。本手册整合了原先散落的快速诊断、服务器端诊断、故障排查指南、4 份修复记录、通讯链路诊断、deploy/02 手册、调试验证指南。
> **版本**: v1.1 | **日期**: 2026-07-08 | **维护人**: 宋献 / 助理
> **定位**: 所有故障排查前**首先查看本手册**。
> **最新**: 新增 CASE-20260708-01~06OTP 路由404 / nginx 404 / 扫码角色 / 用户角色 / 员工端路由 / OTP列表结构)+ nginx 配错急救流程
| v1.1 | 2026-07-08 | 新增 6 天 7.8 案例 + nginx 急救流程 + 端到端验证更新 |
> **前置阅读**: [运维手册(部署/回滚/备份/应急)](../01-项目总览/01-智能IT服务系统运维手册-20260704.md) · [SOP-04 应急响应](../10-项目管理/SOPs-标准流程/SOP-04-应急响应.md)
---
@@ -163,6 +166,63 @@ docker logs wecom_it_backend | grep -i websocket
---
### CASE-20260708-01 · 后端路由 404 — OTP 统一路由未注册 ⭐
- **现象**`POST /api/auth/otp-bind` 返回 404;前端 OTP 绑定面板密钥和二维码不显示。
- **根因**:部署 `otp.py` 时漏部署 `router.py``api_router.include_router(otp_router)` 未执行。本地 `router.py` 包含服务器不存在的模块(`knowledge_iteration` / `approval_queue` / `vision` / `ragflow_ingestion` / `automation`),导入失败导致整个 `router.py` 加载失败。
- **修复**:上传 `router.py`,注释掉服务器上不存在的模块导入;同时修复 `otp.py``@require_role("admin")` 装饰器与显式 `current_user` 参数的冲突(服务器旧版 `require_role` 会自动追加 `current_user`)。
- **教训**:修改路由时务必同步部署 `router.py`,否则新端点虽然代码存在但永远不会注册。
### CASE-20260708-02 · 管理端 API 全部 404 — nginx 正则 location proxy_pass 缺 rewrite
- **现象**`/api/admin/roles``/api/admin/dashboard/overview` 等全部返回 404。
- **根因**nginx 配置中 `location ~ ^/api/admin/`(正则匹配)内 `proxy_pass http://backend_api;` 不带尾部斜杠,导致 `/api/` 前缀未剥离,后端收到 `/api/admin/roles` 而非 `/admin/roles`。带尾部斜杠又会报错 `"proxy_pass" cannot have URI part in location given by regular expression`
- **修复**:去掉嵌套正则 location,改为统一 `location /api/``proxy_pass http://backend_api/;`(尾部斜杠剥离 /api/ 前缀)。
- **教训**nginx 中正则 location 不能直接用 `proxy_pass` 带 URI;需要时用 `rewrite` 剥离前缀。
### CASE-20260708-03 · sxn 扫码登录无 admin 权限 — QR 扫描写死 `roles=["agent"]`
- **现象**:管理后台扫码登录后,OTP 管理/角色管理返回 403/无权限提示。`sxn` 账密登录正常。
- **根因**`auth_qrcode.py` 扫码自动确认逻辑中,`create_token` 写死了 `roles=["agent"]`,未调用 `get_user_roles()`
- **修复**:扫码确认改为调用 `RoleMappingService.get_user_roles()` 获取真实角色。
- **教训**:所有登录路径(账密/扫码/OAuth)的角色获取必须统一走 `get_user_roles()`
### CASE-20260708-04 · 用户角色列表为空 — `get_user_roles()` 只查 `user_roles` 表
- **现象**:管理后台角色管理页"用户角色分配"表格为空;OTP 管理 API 403。
- **根因**`get_user_roles()` 仅查询 `user_roles` 表,但旧数据(包括 sxn 的 admin)只存在于 `agents.role` 字段。`user_roles` 表为空时返回 `["user"]`
- **修复**`get_user_roles()` 增加 `agents.role` 回退查询;新增 `GET /admin/roles/user-roles` 端点;前端 `Roles.vue` 加载用户角色分配数据。
- **教训**:新旧数据迁移时需确保角色数据完整同步到 `user_roles` 表。
### CASE-20260708-05 · 企微工作台点"IT支持服务"进坐席登录页 — 员工端口缺失
- **现象**:企微工作台 → IT支持服务 → 显示坐席扫码登录页,而非员工 H5 页面。
- **根因**(1) nginx `/itdesk/` 被错误配置为 301 重定向到 `/itagent/`(2) H5 构建文件未挂载到容器(`docker-compose.yml` 缺少 `./html/h5` 挂载);(3) 改重定向到 `/h5/` 后仍失败,因为 H5 `vite base``/itdesk/`OAuth 回调依赖此路径。
- **修复**`/itdesk/` 直接 alias 到 H5 构建目录(`/usr/share/nginx/html/h5/`),不再 301 跳转;`docker-compose.yml` 添加 h5 volume 挂载;根路径 `/` 改为 302 → `/h5/`
- **nginx 关键配置**alias + try_files SPA 模式):
```
location /itdesk/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri $uri/ /index.html;
}
```
注意:fallback 用 `/index.html` 而非 `/itdesk/index.html`——alias 会自动映射。
- **教训**:前端 `vite base` 路径必须与 nginx 服务路径一致;OAuth 回调路径不能用 301 重定向。
### CASE-20260708-06 · 管理端 OTP 用户列表返回错误结构
- **现象**:OTP 管理页面提示加载失败,`/auth/otp-admin-users` 返回 403。
- **根因**(1) `admin_list_otp_users` 返回普通数组,前端期望 `{total, items}` 结构;(2) `@require_role("admin")` 因 token 不含 admin 角色返回 403(见 CASE-03/04)。
- **修复**:后端改为分页查询返回 `{total, items}`;增加 keyword/bound/page/page_size 参数支持。
### 附:nginx 配错急救流程(2026-07-08 实战总结)
| 步骤 | 操作 |
|------|------|
| 1 | 备份:`sudo cp /opt/wecom-it-desk/nginx/nginx.conf /tmp/nginx.bak` |
| 2 | 修改宿主文件后必须 `docker stop nginx && docker start nginx``reload` 有时不生效) |
| 3 | 验证容器内配置已同步:`docker exec wecom_it_nginx grep 关键字 /etc/nginx/nginx.conf` |
| 4 | 确认语法:`docker exec wecom_it_nginx nginx -t` |
| 5 | 检查容器状态:`docker ps --filter name=wecom_it_nginx` |
| 6 | 用 `agent-browser` 打开 URL 做端到端验证 |
| 7 | 避免用 sed 修改 nginx 配置——`$uri`/`$host` 等变量会被 shell 解释;用 Python 脚本或直接上传文件 |
---
## 5 端到端验证完成标准(原《调试验证指南》整合)
> 宣布完成前,按 §0.2 提供真实证据。
@@ -0,0 +1,185 @@
# =============================================================================
# 企微智能IT支持服务台 — Nginx 配置(生产环境 — 2026-07-08 三端正常基准)
# =============================================================================
# 状态:坐席端 /itagent/ + 管理端 /itadmin/ + 员工端 /itdesk/ 三端正常
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
set_real_ip_from 10.212.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 50m;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript
application/javascript application/xml+rss
application/json application/ld+json;
upstream backend_api {
server backend:8000;
}
server {
listen 80;
server_name itsupport.servyou.com.cn;
location /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
}
location /h5/ {
root /usr/share/nginx/html;
index index.html;
try_files $uri /h5/index.html;
}
location /h5/api/ {
proxy_pass http://backend:8000/;
proxy_http_version 1.1;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
http2 on;
server_name itsupport.servyou.com.cn;
ssl_certificate /etc/nginx/ssl/itsupport.servyou.com.cn.crt;
ssl_certificate_key /etc/nginx/ssl/itsupport.servyou.com.cn.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
server_tokens off;
location = /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# === 员工端 — H5 直接服务,不能 301 重定向(OAuth 回调依赖此路径)===
location /itdesk/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri $uri/ /index.html;
}
# === 坐席工作台 ===
location /itagent/ {
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
add_header Pragma "no-cache" always;
add_header Expires "0" always;
alias /usr/share/nginx/html/itagent/;
index index.html;
try_files $uri $uri/ /index.html;
}
# === 管理后台 ===
location /itadmin/ {
alias /usr/share/nginx/html/itadmin/;
index index.html;
try_files $uri /itadmin/index.html;
}
# === 统一入口(已弃用)===
location /itportal/ {
alias /usr/share/nginx/html/itportal/;
index index.html;
try_files $uri /itportal/index.html;
}
# === 后端 API — /api/ 前缀由 proxy_pass 尾部斜杠剥离 ===
location /api/ {
proxy_pass http://backend_api/;
proxy_http_version 1.1;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
# === WebSocket — 不能带尾部斜杠 ===
location /ws/ {
access_log off;
proxy_pass http://backend_api;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 86400s;
}
# === H5 静态文件 ===
location /h5/ {
root /usr/share/nginx/html;
index index.html;
try_files $uri /h5/index.html;
}
# === H5 API 代理 ===
location /h5/api/ {
proxy_pass http://backend:8000/;
proxy_http_version 1.1;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
# === 根路径 → H5 员工端 ===
location = / {
return 302 /h5/;
}
}
}