# 系统架构设计 + 任务分解 — 三端认证重构(增量) > 文档版本: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 { <> +str token +str employee_id +list roles +str current_role +str login_source +int ttl_seconds } class MFAService { <> +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 { <> +GET otp-status +POST otp-bind +POST otp-verify +POST otp-unbind +POST otp-admin-reset/{id} +GET otp-admin-users } class LoginRouter { <> +POST agents/login +POST auth_qrcode/create +GET auth_qrcode/poll/{ticket} +POST auth_qrcode/scan +POST auth_qrcode/confirm } class H5OAuthRouter { <
> +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/mock,DEV_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 = XXX;replaceState 清除 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_id(H5)、`user:token:{token}`→JSON(坐席/管理统一格式)、`agent:token:{token}`→user_id(旧格式兼容)。 - 请求头统一 `Authorization: Bearer `;**移除** `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`。