Files
wecom_it_smart_desk/docs/system_design.md
T

412 lines
24 KiB
Markdown
Raw 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.
# 系统架构设计 + 任务分解 — 三端认证重构(增量)
> 文档版本:v1.0(架构师交付稿)
> 架构师:高见远 (software-architect)
> 依据:增量 PRD `docs/02-产品需求/04-增量PRD-三端认证重构.md` + 已锁定决策(决策1–7)
> 原则:基于现有代码的最小变更增量重构,不引入新框架
---
# Part A:系统设计
## 1. 实现方案 + 框架选型
### 1.1 技术栈(沿用,不新增框架)
- **后端**FastAPI + SQLAlchemy(async) + Redis + Pydantic。认证分层沿用 `api`(路由) / `services`(逻辑) / `models`(数据) 结构。
- **前端**:三端独立 Vue3 + Vite SPA`frontend-h5` / `frontend-agent` / `frontend-admin`),各自 Axios 实例 + 响应拦截器。
- **OTP 算法**`pyotp`**已安装**于 `backend/venv`,版本 2.10.0)、`qrcode` 已存在。**无需新增任何依赖**,直接复用 `backend/app/services/mfa_service.py``MFAService` 封装(TOTP secret 生成 / 校验 / 二维码 / Redis 标记)。
### 1.2 本次增量改动点(对照 PRD)
| 决策 | 改动点 | 类型 |
|------|--------|------|
| 1 三端独立入口 | 移除 `/itportal/``/api/portal/*`;清理 `portal_token` 引用 | 删除 |
| 2 员工端唯一认证 | H5 仅 snsapi_base 静默授权 → 直进;Token 经 `?token=` 镜像 `h5_token`;非 wxwork UA 跳拦截页(仅生产) | 改 |
| 3 坐席/管理两方式并列 | 移除「企微已登录+角色→免密直接进入」分支;保留 ①扫码 ②账密+OTP | 删/改 |
| 4 OTP 输入框延迟渲染 | 账密验证通过后才渲染 OTP(前端修正,原型已对齐) | 改 |
| 5 OTP 接口统一 | 新增 `/api/auth/otp-*`,作废 `/api/mfa/*``/api/agents/otp-*` | 新增/删 |
| 6 管理端 IP 白名单 | 新增后端中间件,仅生产启用,非白名单 403 | 新增 |
| 7 响应契约方案A | 三端拦截器统一:成功返回内层 `data`,失败抛 `{code,message}` | 改 |
| 8 env-gating | UA 校验 / IP 白名单 / 真实企微 OAuth 仅生产启用;本地走 dev/mock | 新增 |
### 1.3 关键设计决策(基于代码核实)
- **OTP 逻辑复用**:既有 `backend/app/api/mfa.py``MFAService`(干净);而 `backend/app/api/agents.py``/agents/otp-*` 用**内联 pyotp 重复实现**。新端点统一落到 **新增 `backend/app/api/otp.py`**(前缀 `/auth`),复用 `MFAService``agents.mfa_*` 字段。Redis 复用标记 key 维持 `mfa:verified:{employee_id}`TTL 1800s),**`dependencies.require_high_risk_otp` 无需改动**。
- **员工 `?token=` 镜像已具备**`frontend-h5/src/router/index.ts` 已读取 URL `?token=` → 写 `localStorage.h5_token`Bug#4 修复)。本次仅把后端 OAuth 回调从「code→前端 POST 取 token」改为「code→后端 302 `/itdesk/?token=XXX`」,复用既有镜像逻辑,更贴合决策2。
- **管理端 IP 白名单**:以 FastAPI 中间件实现,仅对 `/api/admin/*``/api/auth/otp-admin-*` 生效(生产环境 `APP_ENV` 判定),非白名单返回 `{code:4004, message:"无访问权限"}`,前端据此展示无权限拦截页。
- **免密分支移除**`agents.py agent_login``wecom_verified + role → skip_otp=True` 整段删除;前端 `Login.vue` 移除「企微免密登录 / JS-SDK 快捷登录 / 智能检测自动跳转」分支。
---
## 2. 文件列表(标注【新增】/【修改】/【删除】)
### 2.1 后端
| 路径 | 操作 | 说明 |
|------|------|------|
| `backend/app/config.py` | 【修改】 | 新增 `app_env`(默认 `"dev"`)、`admin_allowed_ips`(默认 `"117.147.35.138,218.75.34.87,10.240.0.0/16"`);复用 `wecom_corp_id` |
| `backend/app/utils/env_gating.py` | 【新增】 | `is_production()`(按 `app_env`)、`ip_in_whitelist(client_ip, allowed)`(支持 CIDR);统一 env-gating 判定 |
| `backend/app/middleware/admin_ip_whitelist.py` | 【新增】 | `AdminIPWhitelistMiddleware`:仅 `app_env==production` 且路径命中 admin 前缀时校验客户端 IP |
| `backend/app/main.py` | 【修改】 | 注册 `AdminIPWhitelistMiddleware`(在 CORS 之后、路由之前) |
| `backend/app/api/otp.py` | 【新增】 | 统一 OTP 路由(前缀 `/auth`):`otp-status` / `otp-bind` / `otp-verify` / `otp-unbind` / `otp-admin-reset/{id}` / `otp-admin-users` |
| `backend/app/api/router.py` | 【修改】 | 注册 `otp_router`;注销 `portal_router` / `mfa_router` / `agents` 内 otp 端点 |
| `backend/app/api/mfa.py` | 【删除】 | 作废 `/api/mfa/*``/api/admin/mfa/*` |
| `backend/app/api/agents.py` | 【修改】 | 删除 `/agents/otp-bind` / `/agents/otp-verify` / `/agents/otp-unbind` 三端点;`agent_login` 移除 `skip_otp` 免密分支 |
| `backend/app/api/h5.py` | 【修改】 | 修 `get_oauth_authorize_url``redirect_uri``/itportal/``/itdesk/`(现网bug);新增 `GET /h5/oauth/sns-callback` 302 带 `?token=``_require_wework_ua` 改用 `env_gating.is_production()` |
| `backend/app/api/auth_wecom_sso.py` | 【修改】 | `sso_verify``success_response` 包裹(一致性);备注 env-gating |
| `backend/app/api/dev_auth.py` | 【修改】 | 三端点用 `success_response` 包裹(CTRT-P1-1 一致性,dev 仅本地) |
| `backend/app/api/portal.py` | 【删除】 | 作废 `/api/portal/*` |
| `backend/app/dependencies.py` | 【修改】 | 文档更新(`mfa:verified:` key 不变,仅端点路径变) |
### 2.2 前端 H5`frontend-h5`
| 路径 | 操作 | 说明 |
|------|------|------|
| `src/api/index.ts` | 【修改】 | 拦截器统一方案A(成功返回内层 `data`);移除非 wxwork→`/login` 的 dev 兜底,生产改 `/wework-only`;清理 `X-Employee-Id` 遗留头 |
| `src/router/index.ts` | 【修改】 | 生产非 wxwork UA → 跳转 `/wework-only`(沿用现有 `?token=` 镜像分支不动) |
| `src/api/employee.ts` | 【修改】 | 调用点适配内层 `data``response.data``response` |
| `src/api/conversation.ts` 等全部 `src/api/*.ts` | 【修改】 | CTRT-P0-2 全量回归(约 8 个文件) |
| `src/views/WeworkOnly.vue` | 【复用】 | 非企微拦截页(已存在,无需新建) |
### 2.3 前端坐席(`frontend-agent`
| 路径 | 操作 | 说明 |
|------|------|------|
| `src/api/index.ts` | 【修改】 | 拦截器统一方案A;移除 `portal_token` 清理遗留 |
| `src/views/Login.vue` | 【修改】 | 移除「企微免密登录 / JS-SDK 快捷登录 / 智能检测三选项 / onMounted 自动 sso 跳转」;保留 ①扫码 ②账密+OTP(OTP 已 `v-if="requireOtp"` 延迟渲染) |
| `src/api/mfa.ts` | 【修改】 | 路径改 `/api/auth/otp-*`;调用点适配内层 `data` |
| `src/api/*.ts`qrcode.ts、conversation.ts、message.ts 等) | 【修改】 | CTRT-P0-2 全量回归 |
### 2.4 前端管理(`frontend-admin`
| 路径 | 操作 | 说明 |
|------|------|------|
| `src/api/index.ts` | 【修改】 | 拦截器统一方案A(与坐席同形态;admin 仍无静默刷新,401/1002 清 `admin_token``/login` |
| `src/views/Login.vue` | 【修改】 | 移除「企微免密登录」按钮与 JS-SDK 检测分支;保留 ①扫码 ②账密+OTP;非白名单 403 → 无权限页 |
| `src/views/NoPermission.vue` | 【新增】 | 管理端「无访问权限」拦截页(对应 PRD 原型 note 4 |
| `src/api/mfa.ts` | 【修改】 | `/admin/mfa/users``/api/auth/otp-admin-users``/admin/mfa/reset/{id}``/api/auth/otp-admin-reset/{id}`;适配内层 `data` |
| `src/api/*.ts` | 【修改】 | CTRT-P0-2 全量回归 |
### 2.5 待清理工程
| 路径 | 操作 | 说明 |
|------|------|------|
| `frontend-portal/`(整个工程) | 【删除】 | 统一入口 Portal 前端(决策1/C8),含 `QrcodeLogin.vue` / `PortalSelect.vue` / `stores/portal.ts` 等 |
---
## 3. 数据结构和接口
### 3.1 类图(Mermaid
```mermaid
classDiagram
class Employee {
+str employee_id
+str corp_id
+str name
+str department
+str position
+str avatar
}
class Agent {
+str user_id
+str name
+str role
+str status
+str mfa_secret
+bool mfa_enabled
+datetime mfa_bound_at
+datetime mfa_last_verified_at
+str password_hash
+int current_load
+int max_load
}
class OtpSecret {
<<值对象,内嵌于 Agent>>
+str secret
+bool enabled
+datetime bound_at
+datetime last_verified_at
}
class Token {
<<Redis 存储>>
+str token
+str employee_id
+list roles
+str current_role
+str login_source
+int ttl_seconds
}
class MFAService {
<<static 封装 pyotp>>
+generate_secret() str
+build_provisioning_uri(secret, id) str
+render_qrcode_base64(uri) str
+verify_code(secret, code) bool
+mark_verified(redis, id, ttl)
+is_verified(redis, id) bool
}
class TokenService {
+create_token(employee_id, name, roles, ...) str
+get_user_info(token) dict
+refresh(token) bool
+switch_role(token, role) bool
}
class OtpRouter {
<<FastAPI 前缀 /api/auth>>
+GET otp-status
+POST otp-bind
+POST otp-verify
+POST otp-unbind
+POST otp-admin-reset/{id}
+GET otp-admin-users
}
class LoginRouter {
<<agents/login + auth_qrcode>>
+POST agents/login
+POST auth_qrcode/create
+GET auth_qrcode/poll/{ticket}
+POST auth_qrcode/scan
+POST auth_qrcode/confirm
}
class H5OAuthRouter {
<<h5 OAuth>>
+GET h5/oauth/authorize
+GET h5/oauth/sns-callback
+POST h5/oauth/callback
}
class AdminIPWhitelistMiddleware {
+is_production 门控
+ip_in_whitelist(ip) bool
}
Agent "1" *-- "1" OtpSecret : 内嵌 mfa_*
OtpRouter ..> MFAService : 复用
OtpRouter ..> Agent : 读写 mfa_*
OtpRouter ..> Token : 依赖 Bearer 鉴权
LoginRouter ..> TokenService : 签发 token
LoginRouter ..> MFAService : agents/login 内联校验
H5OAuthRouter ..> TokenService : 签发 employee token
TokenService ..> Token : 存 Redis(user/employee/agent)
AdminIPWhitelistMiddleware ..> LoginRouter : 守卫 /api/admin/*
```
### 3.2 OTP 接口契约(前缀 `/api/auth`,全部走统一信封)
| 方法 & 路径 | 鉴权 | 请求体 | 成功响应 `data` | 说明 |
|------------|------|--------|----------------|------|
| `GET /otp-status` | 登录用户 | — | `{bound, enabled, last_verified_at}` | 路由守卫用 |
| `POST /otp-bind` | 登录用户 | — | `{secret, otpauth_url, qr_code_base64}` | 生成 secret 存 `mfa_secret`(enabled=False);已 enabled 拒绝 |
| `POST /otp-verify` | 登录用户 | `{otp_code}` | `{verified, bound, expires_in}` | 未启用→确认绑定(set enabled+bound_at);写 `mfa:verified:`;用于 bind 闭环 + 高危操作 |
| `POST /otp-unbind` | 登录用户 | `{otp_code}` | `{success}` | 校验后清空 `mfa_secret/enabled` |
| `POST /otp-admin-reset/{employee_id}` | admin 角色 | — | `{success}` | 丢手机兜底,无 OTP 直接清空 |
| `GET /otp-admin-users` | admin 角色 | `?keyword&bound&page&page_size` | `{items,total,page,page_size}` | 管理页用户 MFA 列表(保留管理页) |
### 3.3 登录接口契约
| 方法 & 路径 | 请求体 | 响应 `data` |
|------------|--------|------------|
| `POST /api/agents/login` | `{user_id, name?, password, otp_code?}` | 成功:`{token, user_id, name, role, ...}`mfa_enabled 且无 otp_code`{require_otp:true, user_id, name, role}`**不带 token** |
| `POST /api/auth_qrcode/create` | — | `{ticket, qrcode_url, qrcode_png_base64, expires_in, expires_at}` |
| `GET /api/auth_qrcode/poll/{ticket}` | — | `{status, employee_id, name, token}`status: waiting/scanned/confirmed/expired |
| `POST /api/auth_qrcode/confirm` | `{ticket, otp_code?}` | `{token, employee_id, name, roles, require_otp}` |
| `GET /api/h5/oauth/authorize` | `?redirect_uri` | `{authorize_url}`prod 强制 wxwork UA,否则 4003 |
| `GET /api/h5/oauth/sns-callback` | `?code` | **302 → `/itdesk/?token=XXX`** |
| `POST /api/h5/oauth/callback` | `{code}` | `{token, employee_id, ...}`dev 兜底,保留) |
| `GET /api/dev/login` | `?userid&role=user` | `{token, user}`dev/mockDEV_MODE 启用) |
| `POST /api/auth/refresh` | `?token` | `{token, expires_in}`(坐席静默刷新;H5/admin 不刷新) |
> 管理端登录**复用** `POST /api/agents/login`(同契约,role=admin)。
---
## 4. 程序调用流程(Mermaid 时序图)
### 4.1 员工端 OAuth 静默授权(snsapi_base → `?token=` 镜像)
```mermaid
sequenceDiagram
participant U as 员工(企微WebView)
participant H5 as H5前端(/itdesk/)
participant R as 路由守卫
participant B as 后端(/api/h5)
participant W as 企微OAuth
U->>H5: 打开 /itdesk/
H5->>R: beforeEach 守卫
R->>R: 读 ?token=(无) / ?code=(无) / 无 token
R->>B: GET /h5/oauth/authorize (prod 校验 wxwork UA)
B-->>R: {authorize_url}
R->>W: 302 跳转企微授权页
W-->>H5: 回调 redirect_uri?code=CODE
H5->>B: GET /h5/oauth/sns-callback?code=CODE
B->>W: code 换 userid + 用户信息
B->>B: 生成 employee token 存 Redis(employee:token:)
B-->>H5: 302 /itdesk/?token=XXX
H5->>R: 守卫读 ?token=XXX
R->>R: localStorage.h5_token = XXXreplaceState 清除 URL
R->>B: 携带 Bearer 拉取用户信息
B-->>H5: 工作台数据(内层 data
```
### 4.2 坐席/管理 扫码登录(auth_qrcode
```mermaid
sequenceDiagram
participant A as 坐席/管理员
participant FE as 前端登录页
participant B as 后端(/api/auth_qrcode)
participant WX as 企微App(扫码确认)
participant R as Redis
A->>FE: 点击「企微扫码登录」
FE->>B: POST /auth_qrcode/create
B->>R: 写 ticket(120s) + OAuth URL
B-->>FE: {ticket, qrcode_png_base64}
FE->>FE: 展示二维码 + 2s 轮询
loop 轮询
FE->>B: GET /auth_qrcode/poll/{ticket}
B-->>FE: {status: waiting/scanned}
end
WX->>B: GET /auth_qrcode/scan?code&state=ticket (企微OAuth回调)
B->>R: 写 scan:{ticket}
A->>WX: 在企微点「确认登录」
WX->>B: POST /auth_qrcode/confirm {ticket}
B->>B: 校验身份→签发 token(agent/admin)
B->>R: 写 confirm:{ticket}=token
FE->>B: GET /poll/{ticket} → {status:confirmed, token}
FE->>FE: localStorage.agent_token/admin_token = token
FE->>FE: 跳 /workspace 或 /
```
### 4.3 坐席/管理 账号密码 + OTP
```mermaid
sequenceDiagram
participant A as 坐席/管理员
participant FE as 前端登录页
participant B as 后端(/api/agents/login)
participant M as MFAService/Redis
A->>FE: 输入账号+密码,点登录
FE->>B: POST /agents/login {user_id, password}
alt 已绑定 MFA 且无 otp_code
B-->>FE: {require_otp:true, user_id, name, role}(无 token
FE->>FE: 渲染 OTP 输入框(v-if requireOtp)
A->>FE: 输入 6 位 OTP
FE->>B: POST /agents/login {user_id, password, otp_code}
B->>M: verify_code(mfa_secret, otp_code)
M-->>B: True
B->>B: 签发 token
B-->>FE: {token, user_id, name, role}
else 未绑定 MFA
B-->>FE: {token, ...} 直接登录
end
FE->>FE: localStorage.agent_token/admin_token = token;跳主页
```
### 4.4 令牌过期 / 401 处理
```mermaid
sequenceDiagram
participant FE as 三端前端
participant I as 响应拦截器
participant B as 后端
participant R as Redis
FE->>B: 业务请求(Bearer token)
B-->>FE: 401 / {code:1002}
alt H5 员工端
I->>I: 清 h5_token
alt 生产(有 CorpId)
I->>B: 重走 OAuth 重定向(带防循环计数)
else Mock(dev)
I->>FE: 跳 /itdesk/login
end
else 坐席端
I->>B: POST /api/auth/refresh?token=(静默)
B->>R: 延长 user:token TTL
alt 刷新成功
I->>FE: 重放原请求
else 失败
I->>I: 清 agent_token(不再清 portal_token
I->>FE: 跳 /login
end
else 管理端
I->>I: 清 admin_token
I->>FE: 跳 /login
end
```
---
## 5. 任务列表(有序、含依赖、按 AUTH-/CTRT- 分组)
> 说明:本重构涉及「后端 + 三前端」,按 PRD 交付要求拆为**可独立执行的细粒度任务**,按 AUTH(认证)/ CTRT(契约)分组,标注依赖与可并行项。前端契约任务(CTRT)与后端任务可并行启动。
### 5.1 认证类(AUTH-
| ID | 任务 | 涉及文件 | 依赖 | 可并行 | 验收标准 |
|----|------|----------|------|--------|----------|
| AUTH-01 | 环境门控与配置 | `backend/app/config.py`【改】、`backend/app/utils/env_gating.py`【新】 | 无 | 是(与 CTRT-01 并行) | `is_production()``ip_in_whitelist()` 单测通过;`app_env`/`admin_allowed_ips` 可由环境变量注入 |
| AUTH-02 | 管理端 IP 白名单中间件 | `backend/app/middleware/admin_ip_whitelist.py`【新】、`backend/app/main.py`【改】 | AUTH-01 | 否 | 仅 prod + `/api/admin/*``/api/auth/otp-admin-*` 命中;非白名单返回 `{code:4004}`dev 关闭不拦截 |
| AUTH-03 | 统一 OTP 路由(复用 MFAService | `backend/app/api/otp.py`【新】 | AUTH-01 | 是(与 AUTH-05 并行) | 6 端点齐备;复用 `mfa:verified:` key`otp-bind→otp-verify` 闭环、admin-reset 生效 |
| AUTH-04 | 路由收口与旧端点清理 | `backend/app/api/router.py`【改】、`backend/app/api/mfa.py`【删】、`backend/app/api/agents.py`【改】 | AUTH-03 | 否 | router 注册 otp、注销 portal/mfa/agents-otp`/agents/otp-*``/api/mfa/*` 不可达;`agent_login``skip_otp` 免密分支 |
| AUTH-05 | H5 OAuth 修 bug + `?token=` 重定向 | `backend/app/api/h5.py`【改】 | AUTH-01 | 是(与 AUTH-03 并行) | authorize `redirect_uri=/itdesk/``sns-callback` 302 带 `?token=``wxwork` UA 校验仅 prod |
| AUTH-06 | 移除 Portal 工程与 portal_token | `backend/app/api/portal.py`【删】、`frontend-portal/`【删】、`frontend-agent/src/api/index.ts`【改】 | AUTH-04 | 否 | `/api/portal/*` 不可达;`frontend-portal` 已删;三端无 `portal_token` 引用 |
| AUTH-07 | 后端信封一致性审计 | `backend/app/api/dev_auth.py`【改】、`backend/app/api/auth_wecom_sso.py`【改】、其余路由抽查 | 无 | 是(并行) | 全路由经 `success_response`/`AppException`;dev/sso 端点用信封包裹;无裸 dict 直返 |
| AUTH-08 | H5 员工端登录页/拦截 | `frontend-h5/src/router/index.ts`【改】、`frontend-h5/src/views/WeworkOnly.vue`【复用】 | CTRT-01 | 否 | prod 非 wxwork → `/wework-only``?token=` 镜像保留;mock 走 `/login` |
| AUTH-09 | 坐席登录页清理 | `frontend-agent/src/views/Login.vue`【改】 | CTRT-01 | 否 | 仅 ①扫码 ②账密+OTP;无免密/JS-SDK 分支;无 onMounted 自动 sso 跳转;OTP 仍延迟渲染 |
| AUTH-10 | 管理登录页清理 + IP 拦截页 | `frontend-admin/src/views/Login.vue`【改】、`frontend-admin/src/views/NoPermission.vue`【新】 | AUTH-02, CTRT-01 | 否 | 移除免密按钮;非白名单 403 → 无权限页;扫码+账密+OTP 保留 |
| AUTH-11 | 前端 OTP API 模块迁移 | `frontend-agent/src/api/mfa.ts`【改】、`frontend-admin/src/api/mfa.ts`【改】 | AUTH-03, CTRT-02 | 否 | 路径改 `/api/auth/otp-*`;调用点适配内层 `data`admin 列表/重置端点对齐 |
### 5.2 契约类(CTRT-
| ID | 任务 | 涉及文件 | 依赖 | 可并行 | 验收标准 |
|----|------|----------|------|--------|----------|
| CTRT-01 | 三端拦截器统一方案A(响应+请求) | `frontend-h5/src/api/index.ts`【改】、`frontend-agent/src/api/index.ts`【改】、`frontend-admin/src/api/index.ts`【改】 | 无 | 是(与 AUTH-01/03/05/07 并行) | 成功返回内层 `data`;失败抛 `{code,message}`;请求统一 `Authorization:Bearer`;清 `X-Employee-Id`/`portal_token` 遗留 |
| CTRT-02 | 三端调用点全量回归 | 三端 `src/api/*.ts`employee/conversation/mfa/qrcode/message 等约 15 文件) | CTRT-01 | 否 | H5 `response.data``response`agent/admin `response.data.data``response`;编译+核心链路无字段错取 |
| CTRT-03 | 401/1002 上报形态统一 | 三端 `src/api/index.ts`(含 CTRT-01 | 无 | 是 | 三端 catch 到统一 `{code,message}`;各自重授权逻辑符合 PRD §3「401 处理约定」表 |
### 5.3 联调与验证
| ID | 任务 | 涉及文件 | 依赖 | 可并行 | 验收标准 |
|----|------|----------|------|--------|----------|
| VERIFY-01 | env-gating 与本地 dev/mock 联调 | 全端 | 全部 AUTH/CTRT | 否 | 本地 `VITE_WECOM_CORP_ID` 空 → mock 登录链路通;真实 OAuth 仅 staging/prod |
| VERIFY-02 | 三端认证回归测试 | — | VERIFY-01 | 否 | 满足 PRD §5.3 五条自动化断言(dev/mock 返回 code:0+合法 token;失效 token 触发各端重授权;拦截器统一契约;OTP 闭环) |
---
## 6. 依赖包列表
- **后端****无新增**。`pyotp`(2.10.0)、`qrcode``redis``bcrypt``passlib``slowapi` 均已存在。
- **前端****无新增**。`axios``vue-router``pinia``element-plus`(agent/admin)、`vant`(h5) 均已存在。
---
## 7. 共享知识(跨文件约定)
1. **Token 键名**
- localStorage`h5_token` / `agent_token` / `admin_token`
- Redis`employee:token:{token}`→employee_idH5)、`user:token:{token}`→JSON(坐席/管理统一格式)、`agent:token:{token}`→user_id(旧格式兼容)。
- 请求头统一 `Authorization: Bearer <token>`**移除** `X-Employee-Id` 明文头与 `portal_token`
2. **拦截器返回形态(方案A**:成功返回**内层 `data`**;失败 `reject` 标准化错误对象 `{code, message}`。三端一致。
3. **环境变量**
- 后端:`APP_ENV`(production/staging/dev/test,默认 dev)、`WECOM_CORP_ID``ADMIN_ALLOWED_IPS`("117.147.35.138,218.75.34.87,10.240.0.0/16")、`DEV_MODE``MOCK_LOGIN_ENABLED``WECOM_SSO_ENABLED`
- 前端:`VITE_WECOM_CORP_ID`**空 = dev/mock**)、`VITE_APP_ENV`(可选,区分 prod/dev)。
4. **env-gating 矩阵**(仅生产启用):UA 校验 / IP 白名单 / 真实企微 OAuth;本地/测试跳过,走 dev/mock。
5. **401 / 1002 处理约定**
- H5:清 `h5_token`;prod→重走 OAuth(带防循环计数,上限 3);mock→跳 `/itdesk/login`
- 坐席:先静默 `POST /api/auth/refresh`;失败清 `agent_token``/login`**不再清 portal_token**)。
- 管理:清 `admin_token``/login`
6. **OTP Redis 标记**key `mfa:verified:{employee_id}`TTL 1800s,由 `MFAService` 读写;`require_high_risk_otp` 依赖此 key(端点改名不影响)。
7. **三端入口**`/itdesk/`(H5) / `/itagent/`(坐席) / `/itadmin/`(管理)**移除** `/itportal/`
8. **错误码**`1002`=未授权;`4003`=非企微环境/无权限;`4004`=管理端 IP 无权限(**新增**);`1006`=OTP 验证码错误。
---
## 8. 待明确事项
1. **`10.240.0.0` 网段掩码**PRD 写「10.240.0.0(内网/VPN 网段)」,本设计按 `/16` CIDR 处理,请确认精确掩码(如 `/12` / `/16`)。
2. **管理端 MFA 用户列表端点**`/admin/mfa/users` 是否随 PRD 作废?本设计**默认迁移**为 `GET /api/auth/otp-admin-users` 以保留管理页功能;若产品决定下线该管理页,则可一并删除。
3. **`agents/login` 内联 pyotp 校验**:本次保持最小变更(不重构为复用 `MFAService`);如需消除重复实现,列为可选优化(不影响功能)。
4. **员工端 OAuth 跳转方式**:本设计采用「后端 `sns-callback` 302 带 `?token=`」(贴合决策2,复用现有 `?token=` 镜像);旧的「前端 code→POST `/h5/oauth/callback` 取 token」路径**保留为 dev 兜底**。如坚持完全走 `?token=`,可删除旧 POST 回调。
5. **`wecom_jsdk_login` 接口**:决策4 移除前端「免密」入口,但后端 `/api/auth_wecom/jsdk-login` 接口本设计**保留**(仅前端不再调用),避免影响其他潜在调用方;如需彻底删除请确认。
---
> 附:类图见 `docs/class-diagram.mermaid`,时序图见 `docs/sequence-diagram.mermaid`。