feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复
== 已部署上线 (9项) == - 代办事项真实数据源集成 (企微审批API 8bug修复链) - H5/坐席端 Logo样式统一+绿色背景 - 视频引导页修复 (localStorage key v2) - 坐席端 v9 Vue版本修复 (ElMessage._context) - 截图按钮 v10 修复 (getDisplayMedia user gesture) - 扫码样式恢复+H5扫码登录跳转修复 - H5截图快捷键提示 == 代码完成待部署 (3项) == - 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查) - 会议室预定-小鱼易联终端 (40文件, 40/40测试通过) - IT资产升级审批推送 (asset_service.py) == 需求文档 (2项) == - 坐席端AI辅助消息框-PRD (4项新功能确认) - 坐席端布局优化建议 v2.0 (7天计划) == 新增文档 == - 日报-2026-07-11.md - 知识迭代Bug修复报告-20260711.md - 会议室预定-部署指南.md - CHANGELOG.md 更新 == 测试 == - test_todo_integration.py: 40/40 - test_meetingroom.py: 40/40 - test_bugfix_ki_suggestions.py: 21/21
This commit is contained in:
+513
-353
@@ -1,411 +1,571 @@
|
||||
# 系统架构设计 + 任务分解 — 三端认证重构(增量)
|
||||
# 系统设计文档 — 代办事项真实数据源集成
|
||||
|
||||
> 文档版本:v1.0(架构师交付稿)
|
||||
> 架构师:高见远 (software-architect)
|
||||
> 依据:增量 PRD `docs/02-产品需求/04-增量PRD-三端认证重构.md` + 已锁定决策(决策1–7)
|
||||
> 原则:基于现有代码的最小变更增量重构,不引入新框架
|
||||
> 项目:企微IT智能服务台 — 代办面板去mock改造
|
||||
> 版本:v1.0
|
||||
> 日期:2026-07-11
|
||||
|
||||
---
|
||||
|
||||
# Part A:系统设计
|
||||
## Part A: 系统设计
|
||||
|
||||
## 1. 实现方案 + 框架选型
|
||||
### 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.1 核心技术挑战
|
||||
|
||||
### 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 | 新增 |
|
||||
| 挑战 | 方案 |
|
||||
|------|------|
|
||||
| 两个异构数据源(企微审批 + ITSM 工单)聚合为统一列表 | 引入 `TodoAggregatorService`,通过抽象接口 `TodoSourceService` 统一两个数据源,`asyncio.gather` 并行查询,`return_exceptions=True` 容错 |
|
||||
| 企微审批需 getapprovaldata → getapprovaldetail 二次过滤当前审批人,性能开销大 | 先用 `getapprovaldata` 按 sp_status=1 + 18 个模板批量获取 sp_no_list,再 `asyncio.gather` 并发调用 `getapprovaldetail`,最后用 `_extract_current_approver` 过滤 |
|
||||
| ITSM 列表 API 尚未获取,签名认证方式特殊 | 设计 `ITSMService` 为可插拔实现,签名计算抽为独立工具 `itsm_signer.py`,列表方法暂返回空列表 + 日志告警,待 API 到位后填充 |
|
||||
| Redis 缓存需区分不同坐席 | 缓存 key 设计:`todo:cache:{agent_userid}:{type_filter}`,TTL 45 秒 |
|
||||
| 彻底去 mock(后端 MOCK_TODO_ITEMS + 前端 mockTodoListData + device 类型) | 全量删除 mock 数据,schema 中 `VALID_TODO_TYPES` 移除 device,前端移除 DeviceDetail 引用 |
|
||||
|
||||
### 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 快捷登录 / 智能检测自动跳转」分支。
|
||||
#### 1.2 框架与库选择
|
||||
|
||||
| 库 | 用途 | 说明 |
|
||||
|----|------|------|
|
||||
| `httpx` | 异步 HTTP 客户端 | 已用于 approval.py,复用 |
|
||||
| `redis.asyncio` | Redis 异步缓存 | 已用于 token 管理,复用 |
|
||||
| `asyncio` | 并发查询两个数据源 | Python 标准库,`asyncio.gather(return_exceptions=True)` |
|
||||
| `hashlib` | ITSM SHA1 签名 | Python 标准库 |
|
||||
| `urllib.parse.quote_plus` | ITSM 签名 URL 编码 | Python 标准库 |
|
||||
|
||||
**结论:无需新增任何第三方依赖。**
|
||||
|
||||
#### 1.3 架构模式
|
||||
|
||||
采用 **Service Layer + 策略模式**:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ API Layer (todo_items.py) │
|
||||
│ GET /api/todo-items │
|
||||
│ GET /api/todo-items/{id} │
|
||||
└──────────────────────┬──────────────────────────────────┘
|
||||
│ 调用
|
||||
┌──────────────────────▼──────────────────────────────────┐
|
||||
│ TodoAggregatorService │
|
||||
│ ┌─ Redis 缓存检查 ─────────────────────────────────┐ │
|
||||
│ │ 命中 → 直接返回 │ │
|
||||
│ │ 未命中 → asyncio.gather 并行查询 │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
│ ┌──────────────┬──────────────┐ │
|
||||
│ │ │ │ │
|
||||
│ ┌──────▼──────┐ ┌────▼─────────┐ │ │
|
||||
│ │ApprovalTodo │ │ ITSMService │ │ │
|
||||
│ │ Service │ │ (abstract) │ │ │
|
||||
│ └──────┬──────┘ └────┬─────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ ┌──────▼──────┐ ┌────▼─────────┐ │ │
|
||||
│ │ 企微审批API │ │ ITSM API │ │ │
|
||||
│ │ getapproval │ │ (待实现) │ │ │
|
||||
│ │ data/detail│ │ │ │ │
|
||||
│ └─────────────┘ └──────────────┘ │ │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 1.4 后端改造方案
|
||||
|
||||
1. **新增 Service 层**(3 个文件):
|
||||
- `TodoSourceService`(抽象基类)+ `ApprovalTodoService`(企微审批实现)
|
||||
- `ITSMService`(ITSM 工单实现,列表方法待 API 到位)
|
||||
- `TodoAggregatorService`(聚合服务,缓存 + 并行查询 + 容错)
|
||||
|
||||
2. **新增 ITSM 签名工具**:`itsm_signer.py`,独立封装 SHA1 签名计算
|
||||
|
||||
3. **重写 `todo_items.py`**:删除 `MOCK_TODO_ITEMS`,调用 `TodoAggregatorService` 获取真实数据
|
||||
|
||||
4. **补充 `approval.py`**:新增 `get_approval_data()` 函数调用企微 `getapprovaldata` API
|
||||
|
||||
5. **更新 `config.py`**:新增 `itsm_app_id`、`itsm_app_secret`、`itsm_base_url` 配置项
|
||||
|
||||
6. **更新 `schemas/todo_item.py`**:`VALID_TODO_TYPES` 移除 `device`
|
||||
|
||||
#### 1.5 前端改造方案
|
||||
|
||||
1. **`api/todo.ts`**:类型定义移除 `device`,新增 `type` 查询参数
|
||||
2. **`stores/todo.ts`**:删除 `mockTodoListData` import 和 catch 块 mock fallback
|
||||
3. **`mock/data.ts`**:删除 `mockTodoListData` 导出
|
||||
4. **`TodoPanel.vue`**:新增类型筛选 Tab(全部/审批/工单)+ 手动刷新按钮 + 移除 device 类型样式
|
||||
5. **`TaskDetailView.vue`**:移除 DeviceDetail 引用
|
||||
6. **`DeviceDetail.vue`**:标记为废弃(保留文件但不再引用)
|
||||
|
||||
---
|
||||
|
||||
## 2. 文件列表(标注【新增】/【修改】/【删除】)
|
||||
### 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` | 【复用】 | 非企微拦截页(已存在,无需新建) |
|
||||
| 文件路径 | 操作 | 说明 |
|
||||
|----------|------|------|
|
||||
| `backend/app/config.py` | 修改 | 新增 ITSM 配置项 |
|
||||
| `backend/app/schemas/todo_item.py` | 修改 | VALID_TODO_TYPES 移除 device |
|
||||
| `backend/app/models/todo_item.py` | 修改 | type 注释移除 device |
|
||||
| `backend/app/services/todo_source_service.py` | 新建 | 抽象基类 + ApprovalTodoService |
|
||||
| `backend/app/services/itsm_service.py` | 新建 | ITSM Service(签名认证 + 可插拔列表) |
|
||||
| `backend/app/services/todo_aggregator_service.py` | 新建 | 聚合服务(缓存 + 并行 + 容错) |
|
||||
| `backend/app/utils/itsm_signer.py` | 新建 | ITSM SHA1 签名工具 |
|
||||
| `backend/app/api/todo_items.py` | 修改 | 删除 mock,重写为真实数据聚合 |
|
||||
| `backend/app/api/approval.py` | 修改 | 新增 get_approval_data() |
|
||||
|
||||
### 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` 等 |
|
||||
| 文件路径 | 操作 | 说明 |
|
||||
|----------|------|------|
|
||||
| `frontend-agent/src/api/todo.ts` | 修改 | 移除 device 类型,新增 type 参数 |
|
||||
| `frontend-agent/src/stores/todo.ts` | 修改 | 移除 mock fallback |
|
||||
| `frontend-agent/src/mock/data.ts` | 修改 | 删除 mockTodoListData |
|
||||
| `frontend-agent/src/components/conversation/TodoPanel.vue` | 修改 | 新增 Tab + 刷新 + 移除 device |
|
||||
| `frontend-agent/src/components/chat/TaskDetailView.vue` | 修改 | 移除 DeviceDetail 引用 |
|
||||
| `frontend-agent/src/components/chat/task/DeviceDetail.vue` | 修改 | 废弃标记 |
|
||||
| `frontend-agent/src/components/chat/task/TicketDetail.vue` | 修改 | 适配真实 ITSM 数据结构 |
|
||||
| `frontend-agent/src/components/chat/task/ApprovalDetail.vue` | 修改 | 适配真实企微审批数据结构 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据结构和接口
|
||||
|
||||
### 3.1 类图(Mermaid)
|
||||
### 3. 数据结构和接口(类图)
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Employee {
|
||||
+str employee_id
|
||||
+str corp_id
|
||||
+str name
|
||||
+str department
|
||||
+str position
|
||||
+str avatar
|
||||
class TodoSourceService {
|
||||
<<abstract>>
|
||||
+agent_userid: str
|
||||
+redis: aioredis.Redis
|
||||
+get_todo_list() List~TodoItemData~*
|
||||
+get_todo_detail(item_id: str) TodoItemData*
|
||||
}
|
||||
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 ApprovalTodoService {
|
||||
-redis: aioredis.Redis
|
||||
-agent_userid: str
|
||||
+get_todo_list() List~TodoItemData~
|
||||
+get_todo_detail(sp_no: str) TodoItemData
|
||||
-_fetch_approval_sp_no_list() List~str~
|
||||
-_fetch_approval_details(sp_no_list: List~str~) List~dict~
|
||||
-_filter_by_current_approver(details: List~dict~) List~dict~
|
||||
-_map_to_todo_item(detail: dict) TodoItemData
|
||||
}
|
||||
class OtpSecret {
|
||||
<<值对象,内嵌于 Agent>>
|
||||
+str secret
|
||||
+bool enabled
|
||||
+datetime bound_at
|
||||
+datetime last_verified_at
|
||||
|
||||
class ITSMService {
|
||||
-base_url: str
|
||||
-app_id: str
|
||||
-app_secret: str
|
||||
-redis: aioredis.Redis
|
||||
-agent_userid: str
|
||||
+get_todo_list() List~TodoItemData~
|
||||
+get_todo_detail(workitem_id: str) TodoItemData
|
||||
-_do_post(url: str, body: dict) dict
|
||||
-_get_workitem_detail(process_instance_id: int, executor: str) dict
|
||||
}
|
||||
class Token {
|
||||
<<Redis 存储>>
|
||||
+str token
|
||||
+str employee_id
|
||||
+list roles
|
||||
+str current_role
|
||||
+str login_source
|
||||
+int ttl_seconds
|
||||
|
||||
class ITSMSigner {
|
||||
+compute_signature(app_id: str, timestamp: str, app_secret: str, biz_data: dict) str
|
||||
-_get_signature_compatible(params: dict) str
|
||||
}
|
||||
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 TodoAggregatorService {
|
||||
-redis: aioredis.Redis
|
||||
-cache_ttl: int
|
||||
+get_todo_list(agent_userid: str, todo_type: Optional~str~) dict
|
||||
+get_todo_detail(agent_userid: str, item_id: str, todo_type: str) dict
|
||||
-_get_from_cache(agent_userid: str, todo_type: Optional~str~) Optional~dict~
|
||||
-_set_to_cache(agent_userid: str, todo_type: Optional~str~, data: dict) void
|
||||
-_invalidate_cache(agent_userid: str) void
|
||||
}
|
||||
class TokenService {
|
||||
+create_token(employee_id, name, roles, ...) str
|
||||
+get_user_info(token) dict
|
||||
+refresh(token) bool
|
||||
+switch_role(token, role) bool
|
||||
|
||||
class TodoItemData {
|
||||
+id: str
|
||||
+type: str
|
||||
+title: str
|
||||
+priority: str
|
||||
+description: dict
|
||||
+status: str
|
||||
+assigned_agent_id: Optional~str~
|
||||
+corp_id: str
|
||||
+created_at: str
|
||||
+updated_at: str
|
||||
}
|
||||
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/*
|
||||
|
||||
TodoSourceService <|-- ApprovalTodoService
|
||||
TodoSourceService <|-- ITSMService
|
||||
TodoAggregatorService o-- ApprovalTodoService : creates
|
||||
TodoAggregatorService o-- ITSMService : creates
|
||||
ITSMService --> ITSMSigner : uses
|
||||
TodoSourceService ..> TodoItemData : returns
|
||||
```
|
||||
|
||||
### 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. 程序调用流程(时序图)
|
||||
|
||||
### 4.1 员工端 OAuth 静默授权(snsapi_base → `?token=` 镜像)
|
||||
#### 4.1 代办列表查询流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 员工(企微WebView)
|
||||
participant H5 as H5前端(/itdesk/)
|
||||
participant R as 路由守卫
|
||||
participant B as 后端(/api/h5)
|
||||
participant W as 企微OAuth
|
||||
participant FE as 前端 TodoPanel
|
||||
participant API as GET /api/todo-items
|
||||
participant Agg as TodoAggregatorService
|
||||
participant Cache as Redis Cache
|
||||
participant Appr as ApprovalTodoService
|
||||
participant Wecom as 企微审批API
|
||||
participant ITSM as ITSMService
|
||||
participant ITSM_API as ITSM OpenAPI
|
||||
|
||||
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
|
||||
FE->>API: GET /api/todo-items?type=approval
|
||||
API->>Agg: get_todo_list(agent_userid, type="approval")
|
||||
Agg->>Cache: GET todo:cache:{userid}:approval
|
||||
alt 缓存命中
|
||||
Cache-->>Agg: cached_data
|
||||
Agg-->>API: {items, total, cached:true}
|
||||
else 缓存未命中
|
||||
Agg->>Agg: asyncio.gather(approval_svc, itsm_svc, return_exceptions=True)
|
||||
par 并行查询审批
|
||||
Agg->>Appr: get_todo_list()
|
||||
Appr->>Wecom: getapprovaldata(sp_status=1, templates=18)
|
||||
Wecom-->>Appr: sp_no_list
|
||||
Appr->>Appr: asyncio.gather(getapprovaldetail × N)
|
||||
loop 每个sp_no并发获取详情
|
||||
Appr->>Wecom: getapprovaldetail(sp_no)
|
||||
Wecom-->>Appr: approval_detail
|
||||
end
|
||||
Appr->>Appr: _filter_by_current_approver(details)
|
||||
Appr->>Appr: _map_to_todo_item(detail)
|
||||
Appr-->>Agg: List[TodoItemData]
|
||||
and 并行查询工单
|
||||
Agg->>ITSM: get_todo_list()
|
||||
Note over ITSM: ITSM列表API待实现<br/>当前返回空列表+日志告警
|
||||
ITSM-->>Agg: List[TodoItemData] (空)
|
||||
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
|
||||
Agg->>Agg: merge + sort by priority
|
||||
Agg->>Cache: SET todo:cache:{userid}:approval TTL=45s
|
||||
Agg-->>API: {items, total, cached:false}
|
||||
end
|
||||
API-->>FE: {code:0, data:{items, total}}
|
||||
```
|
||||
|
||||
#### 4.2 代办详情查询流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant FE as 前端 TaskDetailView
|
||||
participant API as GET /api/todo-items/{id}
|
||||
participant Agg as TodoAggregatorService
|
||||
participant Appr as ApprovalTodoService
|
||||
participant ITSM as ITSMService
|
||||
participant Wecom as 企微审批API
|
||||
participant ITSM_API as ITSM OpenAPI
|
||||
|
||||
FE->>API: GET /api/todo-items/approval:{sp_no}
|
||||
API->>Agg: get_todo_detail(agent_userid, item_id, todo_type="approval")
|
||||
|
||||
alt type == "approval"
|
||||
Agg->>Appr: get_todo_detail(sp_no)
|
||||
Appr->>Wecom: getapprovaldetail(sp_no)
|
||||
Wecom-->>Appr: approval_detail
|
||||
Appr->>Appr: _map_to_todo_item(detail)
|
||||
Appr-->>Agg: TodoItemData
|
||||
else type == "ticket"
|
||||
Agg->>ITSM: get_todo_detail(workitem_id)
|
||||
ITSM->>ITSM_API: POST /openapi/v1/process/workitem/detail
|
||||
ITSM_API-->>ITSM: {code:20000, data:{workitem_detail}}
|
||||
ITSM->>ITSM: _map_to_todo_item(detail)
|
||||
ITSM-->>Agg: TodoItemData
|
||||
end
|
||||
|
||||
Agg-->>API: TodoItemData
|
||||
API-->>FE: {code:0, data:{item}}
|
||||
```
|
||||
|
||||
#### 4.3 ITSM 签名认证流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Svc as ITSMService
|
||||
participant Signer as ITSMSigner
|
||||
participant API as ITSM OpenAPI
|
||||
|
||||
Svc->>Svc: timestamp = str(int(time.time()*1000))
|
||||
Svc->>Signer: compute_signature(app_id, timestamp, app_secret, biz_data)
|
||||
Signer->>Signer: sign_params = {appId, timestamp, appSecret, bizData}
|
||||
Signer->>Signer: sort by key ASC
|
||||
Signer->>Signer: concat all values → canonicalized_str
|
||||
Signer->>Signer: quote_plus(canonicalized_str)
|
||||
Signer->>Signer: sha1(q2).hexdigest().upper()
|
||||
Signer-->>Svc: sign_str
|
||||
Svc->>API: POST url, headers={appId, timestamp, sign}, json=body
|
||||
API-->>Svc: {code:20000, data:{...}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 任务列表(有序、含依赖、按 AUTH-/CTRT- 分组)
|
||||
### 5. 待明确事项
|
||||
|
||||
> 说明:本重构涉及「后端 + 三前端」,按 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 闭环) |
|
||||
| # | 问题 | 当前假设 | 影响 |
|
||||
|---|------|---------|------|
|
||||
| 1 | ITSM 代办列表 API 端点和请求/响应格式 | 设计为抽象接口,列表方法暂返回空列表 | 待用户抓包获取后实现,不影响架构 |
|
||||
| 2 | ITSM app_id 和 app_secret | 需在 config.py 中新增配置项占位 | 部署时通过环境变量注入 |
|
||||
| 3 | 企微 getapprovaldata 单次查询上限(size 参数) | 假设每页100条,循环 cursor 分页 | 如上限更小需增加分页逻辑 |
|
||||
| 4 | 审批详情并发查询数量较多时的限流 | 使用 asyncio.Semaphore 限制并发数(默认10) | 防止企微 API 限流 |
|
||||
| 5 | 坐席身份标识(agent_userid)从哪里获取 | 假设从请求 header 或 JWT token 中提取 | 需确认认证中间件传递方式 |
|
||||
| 6 | ITSM 工单的优先级映射规则 | 假设 ITSM 有自己的优先级字段,需映射到 urgent/high/normal | 待 API 确认后调整映射逻辑 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 依赖包列表
|
||||
## Part B: 任务分解
|
||||
|
||||
- **后端**:**无新增**。`pyotp`(2.10.0)、`qrcode`、`redis`、`bcrypt`、`passlib`、`slowapi` 均已存在。
|
||||
- **前端**:**无新增**。`axios`、`vue-router`、`pinia`、`element-plus`(agent/admin)、`vant`(h5) 均已存在。
|
||||
### 6. 依赖包列表
|
||||
|
||||
**无新增第三方依赖。** 所有所需库已在项目中使用:
|
||||
- `httpx` — 已用于 approval.py 的企微 API 调用
|
||||
- `redis.asyncio` — 已用于 token 缓存管理
|
||||
- `hashlib` — Python 标准库(ITSM SHA1 签名)
|
||||
- `urllib.parse.quote_plus` — Python 标准库(ITSM 签名 URL 编码)
|
||||
- `asyncio` — Python 标准库(并行查询)
|
||||
|
||||
---
|
||||
|
||||
## 7. 共享知识(跨文件约定)
|
||||
### 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 <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 验证码错误。
|
||||
#### T01: 后端基础设施与数据模型
|
||||
**依赖**:无
|
||||
**优先级**:P0
|
||||
**文件**:
|
||||
- `backend/app/config.py`(修改)
|
||||
- `backend/app/schemas/todo_item.py`(修改)
|
||||
- `backend/app/models/todo_item.py`(修改)
|
||||
|
||||
**描述**:
|
||||
1. config.py 新增 ITSM 配置项:
|
||||
- `itsm_app_id: str = ""`
|
||||
- `itsm_app_secret: str = ""`
|
||||
- `itsm_base_url: str = "https://devops.dc.servyou-it.com/itsm"`(生产)
|
||||
- `itsm_test_base_url: str = "https://test-devops.dc.servyou-it.com/itsm"`(测试)
|
||||
2. schemas/todo_item.py:
|
||||
- `VALID_TODO_TYPES` 从 `{"ticket", "approval", "device"}` 改为 `{"ticket", "approval"}`
|
||||
- 更新所有字段描述中的类型注释
|
||||
3. models/todo_item.py:
|
||||
- type 字段注释从 `ticket/approval/device` 改为 `ticket/approval`
|
||||
|
||||
---
|
||||
|
||||
## 8. 待明确事项
|
||||
#### T02: 后端 Service 层 + ITSM 签名工具
|
||||
**依赖**:T01
|
||||
**优先级**:P0
|
||||
**文件**:
|
||||
- `backend/app/services/todo_source_service.py`(新建)
|
||||
- `backend/app/services/itsm_service.py`(新建)
|
||||
- `backend/app/services/todo_aggregator_service.py`(新建)
|
||||
- `backend/app/utils/itsm_signer.py`(新建)
|
||||
|
||||
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` 接口本设计**保留**(仅前端不再调用),避免影响其他潜在调用方;如需彻底删除请确认。
|
||||
**描述**:
|
||||
1. `todo_source_service.py`:
|
||||
- 定义抽象基类 `TodoSourceService`,含 `get_todo_list()` 和 `get_todo_detail()` 抽象方法
|
||||
- 实现 `ApprovalTodoService`:
|
||||
- `get_todo_list()`:调 `getapprovaldata`(sp_status=1, 18模板)→ 并发 `getapprovaldetail` → `_extract_current_approver` 过滤 → `_map_to_todo_item` 映射
|
||||
- `get_todo_detail(sp_no)`:调 `getapprovaldetail` → `_map_to_todo_item`
|
||||
- `_map_to_todo_item()`:企微审批详情 → TodoItemData 格式映射
|
||||
- 使用 `asyncio.Semaphore(10)` 限制并发详情查询
|
||||
2. `itsm_signer.py`:
|
||||
- `ITSMSigner.compute_signature(app_id, timestamp, app_secret, biz_data)` 静态方法
|
||||
- 实现 PRD 中的签名算法:sort → concat → quote_plus → sha1 → upper
|
||||
3. `itsm_service.py`:
|
||||
- 继承 `TodoSourceService`
|
||||
- `_do_post(url, body)`:签名 + 发送请求(复用 ITSMSigner)
|
||||
- `get_workitem_detail(process_instance_id, executor)`:调已知详情 API
|
||||
- `get_todo_list()`:**待 API 到位**,当前返回空列表 + `logger.warning`
|
||||
- `get_todo_detail(workitem_id)`:调 `get_workitem_detail` → 映射
|
||||
4. `todo_aggregator_service.py`:
|
||||
- `get_todo_list(agent_userid, todo_type)`:Redis 缓存 → `asyncio.gather(return_exceptions=True)` → 合并排序 → 写缓存
|
||||
- `get_todo_detail(agent_userid, item_id, todo_type)`:按类型路由到对应 Service
|
||||
- 缓存 key:`todo:cache:{agent_userid}:{todo_type or "all"}`,TTL 45s
|
||||
|
||||
---
|
||||
|
||||
> 附:类图见 `docs/class-diagram.mermaid`,时序图见 `docs/sequence-diagram.mermaid`。
|
||||
#### T03: 后端 API 改造
|
||||
**依赖**:T02
|
||||
**优先级**:P0
|
||||
**文件**:
|
||||
- `backend/app/api/todo_items.py`(修改)
|
||||
- `backend/app/api/approval.py`(修改)
|
||||
- `backend/app/api/router.py`(确认,无需修改路由注册)
|
||||
|
||||
**描述**:
|
||||
1. `todo_items.py`:
|
||||
- 删除 `MOCK_TODO_ITEMS`(全部20条硬编码数据)
|
||||
- 删除 `TodoItemResponse`、`TodoItemListResponse`(移至 schemas,复用已有)
|
||||
- `list_todo_items()`:新增 `type` 查询参数,调用 `TodoAggregatorService.get_todo_list()`
|
||||
- `get_todo_item()`:从 `item_id` 中解析类型前缀(如 `approval:{sp_no}` / `ticket:{workitem_id}`),调用 `TodoAggregatorService.get_todo_detail()`
|
||||
- `update_todo_item_status()`:保留但标记为"仅展示,不支持在服务台内操作"(按用户决策,交互方式为跳转原系统)
|
||||
- 从请求中获取 `agent_userid`(header 或 JWT)
|
||||
2. `approval.py`:
|
||||
- 新增 `get_approval_data(access_token, starttime, endtime, filters)` 异步函数
|
||||
- 调用企微 `POST /cgi-bin/oa/getapprovaldata` API
|
||||
- 支持 cursor 分页循环
|
||||
|
||||
---
|
||||
|
||||
#### T04: 前端数据层与 API 改造
|
||||
**依赖**:T03
|
||||
**优先级**:P0
|
||||
**文件**:
|
||||
- `frontend-agent/src/api/todo.ts`(修改)
|
||||
- `frontend-agent/src/stores/todo.ts`(修改)
|
||||
- `frontend-agent/src/mock/data.ts`(修改)
|
||||
|
||||
**描述**:
|
||||
1. `api/todo.ts`:
|
||||
- `TodoItemData.type` 注释移除 device
|
||||
- `getTodoItems()` 新增 `type` 参数:`type?: 'ticket' | 'approval'`
|
||||
- 新增 `refreshTodoItems()` 函数(带 `_force=1` 参数跳过缓存)
|
||||
2. `stores/todo.ts`:
|
||||
- 删除 `import { mockTodoListData } from '@/mock/data'`
|
||||
- `fetchTodoList()` catch 块:删除 mock fallback,改为 `todoList.value = []` + 错误日志
|
||||
- 新增 `activeType` ref:`'all' | 'approval' | 'ticket'`,传入 API type 参数
|
||||
- 新增 `refreshList()` 方法:强制刷新(调 `refreshTodoItems`)
|
||||
3. `mock/data.ts`:
|
||||
- 删除 `mockTodoListData` 常量和导出
|
||||
- 删除 `mockTodos` 数组(5条前端 mock 数据)
|
||||
|
||||
---
|
||||
|
||||
#### T05: 前端组件层改造
|
||||
**依赖**:T04
|
||||
**优先级**:P0(移除 device)/ P1(Tab+详情+刷新)/ P2(定时刷新+跳转)
|
||||
**文件**:
|
||||
- `frontend-agent/src/components/conversation/TodoPanel.vue`(修改)
|
||||
- `frontend-agent/src/components/chat/TaskDetailView.vue`(修改)
|
||||
- `frontend-agent/src/components/chat/task/DeviceDetail.vue`(修改/废弃)
|
||||
- `frontend-agent/src/components/chat/task/TicketDetail.vue`(修改)
|
||||
- `frontend-agent/src/components/chat/task/ApprovalDetail.vue`(修改)
|
||||
|
||||
**描述**:
|
||||
1. `TodoPanel.vue`(P0+P1+P2):
|
||||
- `typeLabel` 移除 `device: '设备'`
|
||||
- 新增类型筛选 Tab(全部/审批/工单),切换时更新 `todoStore.activeType` + 刷新列表
|
||||
- 新增刷新按钮(🔄 图标),点击调 `todoStore.refreshList()`
|
||||
- 新增定时刷新(P2):`setInterval(fetchTodoList, 60000)`,组件 `onUnmounted` 时清除
|
||||
- 新增跳转按钮(P2):每条待办条目增加"在原系统中打开"链接(审批跳企微审批URL / 工单跳ITSM URL)
|
||||
- 移除 `.todo-type-tag.type-device` CSS 样式
|
||||
2. `TaskDetailView.vue`(P0):
|
||||
- 移除 `import DeviceDetail from './task/DeviceDetail.vue'`
|
||||
- 移除 `v-else-if="todoItem.type === 'device'"` 条件渲染块
|
||||
- `typeLabelMap` 移除 `device: '🖥 设备异常'`
|
||||
- 移除 `.tdv-type-device` CSS 样式
|
||||
3. `DeviceDetail.vue`(P0):
|
||||
- 文件头注释标记为"已废弃 — v1.0 移除 device 类型"
|
||||
- 保留文件但不再被任何组件引用
|
||||
4. `TicketDetail.vue`(P1):
|
||||
- 适配真实 ITSM 工单数据结构(description 字段映射调整)
|
||||
- 底部操作按钮改为"在 ITSM 中打开"跳转链接(P2)
|
||||
5. `ApprovalDetail.vue`(P1):
|
||||
- 适配真实企微审批数据结构(description 字段映射调整)
|
||||
- 底部操作按钮改为"在企微审批中打开"跳转链接(P2)
|
||||
|
||||
---
|
||||
|
||||
### 8. 共享知识(跨文件约定)
|
||||
|
||||
#### 8.1 统一数据映射规则
|
||||
|
||||
**企微审批 → TodoItemData 映射**:
|
||||
```python
|
||||
{
|
||||
"id": f"approval:{sp_no}", # 前缀类型 + 原始ID
|
||||
"type": "approval",
|
||||
"title": sp_name, # 审批单名称
|
||||
"priority": "high", # 审批默认 high(企微无优先级概念)
|
||||
"description": {
|
||||
"sp_no": sp_no,
|
||||
"template_name": template_name,
|
||||
"applicant": applyer_userid,
|
||||
"apply_time": apply_time,
|
||||
"sp_status": sp_status,
|
||||
"current_approver": current_approver,
|
||||
"template_id": template_id,
|
||||
},
|
||||
"status": "pending", # 审批中统一映射为 pending
|
||||
"assigned_agent_id": current_approver,
|
||||
"corp_id": corp_id,
|
||||
"created_at": apply_time_iso, # apply_time 时间戳转 ISO
|
||||
"updated_at": apply_time_iso,
|
||||
}
|
||||
```
|
||||
|
||||
**ITSM 工单 → TodoItemData 映射**:
|
||||
```python
|
||||
{
|
||||
"id": f"ticket:{process_instance_id}", # 前缀类型 + 原始ID
|
||||
"type": "ticket",
|
||||
"title": title,
|
||||
"priority": itsm_priority_to_todo(priority), # ITSM 优先级 → urgent/high/normal
|
||||
"description": {
|
||||
"process_instance_id": process_instance_id,
|
||||
"executor": executor,
|
||||
"status": itsm_status,
|
||||
"creator": creator,
|
||||
# ... 其他 ITSM 字段
|
||||
},
|
||||
"status": "pending",
|
||||
"assigned_agent_id": agent_userid,
|
||||
"corp_id": "",
|
||||
"created_at": created_at_iso,
|
||||
"updated_at": updated_at_iso,
|
||||
}
|
||||
```
|
||||
|
||||
#### 8.2 ID 格式约定
|
||||
|
||||
所有 TodoItem 的 `id` 字段使用 `{type}:{原始ID}` 格式:
|
||||
- 审批:`approval:{sp_no}`(如 `approval:202607110001`)
|
||||
- 工单:`ticket:{process_instance_id}`(如 `ticket:12345`)
|
||||
|
||||
解析规则:`item_id.split(":", 1)` → `[type, original_id]`
|
||||
|
||||
#### 8.3 缓存 Key 设计
|
||||
|
||||
```
|
||||
todo:cache:{agent_userid}:{todo_type}
|
||||
```
|
||||
- `agent_userid`:坐席企微 userid
|
||||
- `todo_type`:`all` / `approval` / `ticket`
|
||||
- TTL:45 秒
|
||||
- 强制刷新:删除 key 后重新查询
|
||||
|
||||
#### 8.4 API 响应格式
|
||||
|
||||
统一使用 `{code: 0, data: {...}, message: "success"}` 格式(复用 `success_response`)。
|
||||
|
||||
#### 8.5 并发控制
|
||||
|
||||
- 企微审批详情并发查询:`asyncio.Semaphore(10)` 限制
|
||||
- 两个数据源并行查询:`asyncio.gather(return_exceptions=True)`
|
||||
- 任一数据源失败不影响另一个:`isinstance(result, Exception)` 检查后跳过
|
||||
|
||||
#### 8.6 前端类型约定
|
||||
|
||||
- `type` 字段:仅 `'ticket'` | `'approval'`(移除 `'device'`)
|
||||
- 前缀格式:前端通过 `id.includes('approval:')` 或 `id.includes('ticket:')` 判断类型
|
||||
|
||||
---
|
||||
|
||||
### 9. 任务依赖图
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
T01[T01: 后端基础设施与数据模型<br/>config.py + schemas + models]
|
||||
T02[T02: 后端Service层 + ITSM签名<br/>4个新文件]
|
||||
T03[T03: 后端API改造<br/>todo_items.py + approval.py]
|
||||
T04[T04: 前端数据层与API<br/>api/todo.ts + store + mock]
|
||||
T05[T05: 前端组件层<br/>TodoPanel + TaskDetailView + 3个子视图]
|
||||
|
||||
T01 --> T02
|
||||
T02 --> T03
|
||||
T03 --> T04
|
||||
T04 --> T05
|
||||
|
||||
style T01 fill:#4CAF50,color:#fff
|
||||
style T02 fill:#2196F3,color:#fff
|
||||
style T03 fill:#2196F3,color:#fff
|
||||
style T04 fill:#FF9800,color:#fff
|
||||
style T05 fill:#FF9800,color:#fff
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user