# 系统设计文档 — 代办事项真实数据源集成 > 项目:企微IT智能服务台 — 代办面板去mock改造 > 版本:v1.0 > 日期:2026-07-11 --- ## Part A: 系统设计 ### 1. 实现方案 #### 1.1 核心技术挑战 | 挑战 | 方案 | |------|------| | 两个异构数据源(企微审批 + 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.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. 文件列表 #### 后端 | 文件路径 | 操作 | 说明 | |----------|------|------| | `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() | #### 前端 | 文件路径 | 操作 | 说明 | |----------|------|------| | `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. 数据结构和接口(类图) ```mermaid classDiagram class TodoSourceService { <> +agent_userid: str +redis: aioredis.Redis +get_todo_list() List~TodoItemData~* +get_todo_detail(item_id: str) TodoItemData* } 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 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 ITSMSigner { +compute_signature(app_id: str, timestamp: str, app_secret: str, biz_data: dict) str -_get_signature_compatible(params: dict) str } 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 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 } TodoSourceService <|-- ApprovalTodoService TodoSourceService <|-- ITSMService TodoAggregatorService o-- ApprovalTodoService : creates TodoAggregatorService o-- ITSMService : creates ITSMService --> ITSMSigner : uses TodoSourceService ..> TodoItemData : returns ``` --- ### 4. 程序调用流程(时序图) #### 4.1 代办列表查询流程 ```mermaid sequenceDiagram 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 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待实现
当前返回空列表+日志告警 ITSM-->>Agg: List[TodoItemData] (空) end 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. 待明确事项 | # | 问题 | 当前假设 | 影响 | |---|------|---------|------| | 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 确认后调整映射逻辑 | --- ## Part B: 任务分解 ### 6. 依赖包列表 **无新增第三方依赖。** 所有所需库已在项目中使用: - `httpx` — 已用于 approval.py 的企微 API 调用 - `redis.asyncio` — 已用于 token 缓存管理 - `hashlib` — Python 标准库(ITSM SHA1 签名) - `urllib.parse.quote_plus` — Python 标准库(ITSM 签名 URL 编码) - `asyncio` — Python 标准库(并行查询) --- ### 7. 任务列表 #### 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` --- #### 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. `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 --- #### 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: 后端基础设施与数据模型
config.py + schemas + models] T02[T02: 后端Service层 + ITSM签名
4个新文件] T03[T03: 后端API改造
todo_items.py + approval.py] T04[T04: 前端数据层与API
api/todo.ts + store + mock] T05[T05: 前端组件层
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 ```