# 会议室预定 — 小鱼易联终端 系统架构设计
> **版本**: v1.0
> **日期**: 2026-07-15
> **作者**: 高见远 (Gao) · 架构师
> **状态**: 待评审
> **所属项目**: `wecom_it_smart_desk` — IT智能服务台
> **上游PRD**: `docs/01-产品文档/08-集成生态/会议室预定-小鱼易联终端-PRD.md`
---
## 目录
- [Part A: System Design](#part-a-system-design)
- [1. 实现方案 + 框架选型](#1-实现方案--框架选型)
- [2. 文件列表及相对路径](#2-文件列表及相对路径)
- [3. 数据结构和接口](#3-数据结构和接口)
- [4. 程序调用流程](#4-程序调用流程)
- [5. 待明确事项](#5-待明确事项)
- [Part B: Task Decomposition](#part-b-task-decomposition)
- [6. 依赖包列表](#6-依赖包列表)
- [7. 任务列表](#7-任务列表)
- [8. 共享知识](#8-共享知识)
- [9. 任务依赖图](#9-任务依赖图)
---
## Part A: System Design
### 1. 实现方案 + 框架选型
#### 1.1 核心技术挑战
| # | 挑战 | 分析 | 方案 |
|---|------|------|------|
| C1 | **企微会议室 access_token 独立管理** | 企微会议室API需要独立的"会议室"secret获取token,不同于现有应用secret和通讯录secret。现有 `WecomService` 已有 `get_access_token()` 和 `get_contact_access_token()` 双token模式 | 在 `WecomService` 中新增 `get_meetingroom_access_token()` 方法,复用 Redis缓存+内存降级模式,缓存key=`wecom:meetingroom_access_token`,TTL=6900秒 |
| C2 | **企微API为唯一数据源 + 本地不维护预定数据** | 所有预定状态查询、预定、取消都直接调用企微API,后端仅做代理+缓存+鉴权。这与现有系统中本地维护会话/消息数据的模式不同 | 后端 `MeetingroomService` 作为薄代理层:所有写操作直接转发企微API,读操作加Redis短缓存(30s)减少API调用。`terminal_room_binding` 表仅存终端↔会议室映射关系,不存预定数据 |
| C3 | **终端页面横屏大屏适配 + 深色主题** | 小鱼易联终端是物理大屏设备(1920×1080横屏),需要大字号触控交互,与现有H5端(Vant4移动端)和坐席端(Element Plus桌面端)UI风格完全不同 | 新建独立前端项目 `frontend-terminal`,使用 Vue3 + Tailwind CSS(不引入Vant/Element),自定义深色主题组件 |
| C4 | **终端身份识别 — 企微扫码登录** | 终端是公共设备,员工预定时需要身份识别。现有系统已有企微OAuth扫码登录流程(`auth_wecom_sso.py`),但终端场景需要适配(终端无企微内置浏览器UA) | 复用现有扫码登录 API (`/api/auth/qrcode`),终端页面通过轮询获取扫码结果。token存储在 Redis key=`terminal:token:{token}`,TTL=8小时 |
| C5 | **WebSocket 实时同步 + 轮询降级** | 终端需要实时显示会议室状态变更。现有WS有 agent/employee 双连接池,但终端是第三种连接类型 | 在 `ConnectionManager` 中新增 `terminal_connections` 字典,新增 `/ws/terminal/{terminal_sn}` 端点。WS断线时终端自动降级为30秒轮询 |
| C6 | **小鱼易联终端管理API集成(方案A为主,方案B预留)** | 用户确认终端型号/环境不确定,架构按方案A(浏览器全屏访问URL)设计,同时预留方案B(API推送URL)的接口 | `XyLinkService` 封装终端管理API,提供 `push_url_to_terminal()` 和 `get_terminal_status()` 方法。P0阶段终端直接浏览器访问URL,P1阶段可通过API推送 |
#### 1.2 框架与库选型
**后端(复用现有技术栈)**:
| 组件 | 选型 | 理由 |
|------|------|------|
| Web框架 | FastAPI (现有) | 复用现有架构,异步支持好,自动生成OpenAPI文档 |
| ORM | SQLAlchemy 2.0 + async (现有) | 复用现有 `Mapped` / `mapped_column` 模式 |
| 数据库 | PostgreSQL (现有) | 复用现有数据库连接 |
| 缓存 | Redis (现有) | 复用 `settings.create_redis_client()` 和 `cache_service` |
| HTTP客户端 | httpx.AsyncClient (现有) | 与 `WecomService` 一致的异步HTTP调用模式 |
| 数据库迁移 | Alembic (现有) | 新增 `011_` 迁移脚本 |
**前端终端页面(新建项目)**:
| 组件 | 选型 | 理由 |
|------|------|------|
| 框架 | Vue 3.4+ | 与现有前端技术栈一致,团队熟悉 |
| 构建工具 | Vite 5+ | 与现有前端一致,HMR开发体验好 |
| CSS方案 | Tailwind CSS 3+ | 深色主题 + 大字号自定义灵活,不需要UI组件库的样式约束 |
| 状态管理 | Pinia 2+ | 与现有前端一致 |
| 路由 | Vue Router 4+ | 与现有前端一致 |
| HTTP客户端 | Axios 1+ | 与现有前端一致 |
| TypeScript | TypeScript 5+ | 类型安全,与现有前端一致 |
**前端H5端(现有项目扩展)**:
| 组件 | 选型 | 理由 |
|------|------|------|
| UI组件库 | Vant 4 (现有) | 复用现有组件 |
| 其他 | 同现有 frontend-h5 | 仅新增页面和路由 |
#### 1.3 架构模式
采用**分层架构 + 中间层代理模式**:
```
┌──────────────────────────────────────────────────────────────────────┐
│ 系统架构分层 │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────────────────┐ │
│ │ 终端前端 │ │ H5前端(扩展) │ │ 管理后台(扩展) │ │
│ │ Vue3+Tailwind│ │ Vue3+Vant4 │ │ Vue3+Element+Tailwind │ │
│ │ frontend- │ │ frontend-h5/ │ │ frontend-admin/ │ │
│ │ terminal/ │ │ (现有) │ │ (现有) │ │
│ └──────┬───────┘ └──────┬───────┘ └───────────┬───────────────┘ │
│ │ │ │ │
│ │ REST API │ REST API │ REST API │
│ │ WebSocket │ │ │
│ └────────┬────────┴───────────────────────┘ │
│ │ │
│ ┌───────────────▼─────────────────────────────────────────────────┐ │
│ │ 后端 API 层 (FastAPI) │ │
│ │ app/api/meetingroom.py — 会议室预定REST API │ │
│ │ app/api/admin/terminal_binding.py — 终端绑定管理CRUD │ │
│ │ app/api/ws.py (扩展) — WebSocket终端端点 │ │
│ └───────────────┬─────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────▼─────────────────────────────────────────────────┐ │
│ │ Service 业务逻辑层 │ │
│ │ app/services/meetingroom_service.py — 会议室预定业务(缓存+代理) │ │
│ │ app/services/xylink_service.py — 小鱼易联终端管理 │ │
│ │ app/services/wecom_service.py (扩展) — 企微API封装+token管理 │ │
│ │ app/services/ws_manager.py (扩展) — WS连接管理(新增终端池) │ │
│ └───────────────┬─────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────▼─────────────────────────────────────────────────┐ │
│ │ Data 数据层 │ │
│ │ app/models/terminal_room_binding.py — 终端绑定模型 │ │
│ │ app/models/meetingroom_booking_snapshot.py (P2) — 预定快照模型 │ │
│ │ Redis: 缓存 + token + WS会话 │ │
│ │ PostgreSQL: terminal_room_binding 表 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────▼─────────────────────────────────────────────────┐ │
│ │ 外部 API 层 (External APIs) │ │
│ │ 企微会议室API (meetingroom/list, book, cancel, get_booking_info)│ │
│ │ 小鱼易联API (终端管理, Web SDK) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
```
**关键设计决策**:
1. **企微API为唯一数据源**:不在本地维护独立的预定数据。`MeetingroomService` 作为薄代理层,所有写操作直接转发企微API,读操作加Redis短缓存。
2. **终端绑定关系本地存储**:`terminal_room_binding` 表存储 terminal_sn ↔ meetingroom_id 映射,这是企微API不提供的关系数据。
3. **P0降级策略**:P0阶段终端使用30秒轮询保证基本实时性;P1阶段升级为WebSocket+企微回调。
4. **前端独立项目**:终端页面UI需求(横屏大屏、深色主题、大字号触控)与现有H5/管理后台差异大,新建独立项目 `frontend-terminal`。
---
### 2. 文件列表及相对路径
> **路径约定**:后端代码物理路径为 `backend/app/xxx`,但 Python 模块导入路径为 `from app.xxx import yyy`(运行目录为 `backend/`)。下方"模块路径"列标注导入路径。
#### 2.1 后端 — 新建文件
| # | 物理路径 | 模块路径 | 职责 |
|---|---------|---------|------|
| 1 | `backend/app/models/terminal_room_binding.py` | `app.models.terminal_room_binding` | 终端-会议室绑定模型(SQLAlchemy ORM) |
| 2 | `backend/app/models/meetingroom_booking_snapshot.py` | `app.models.meetingroom_booking_snapshot` | 预定记录快照模型(P2统计用) |
| 3 | `backend/app/schemas/meetingroom.py` | `app.schemas.meetingroom` | Pydantic 请求/响应模型定义 |
| 4 | `backend/app/services/meetingroom_service.py` | `app.services.meetingroom_service` | 会议室预定业务逻辑(缓存+企微API代理) |
| 5 | `backend/app/services/xylink_service.py` | `app.services.xylink_service` | 小鱼易联终端管理服务 |
| 6 | `backend/app/api/meetingroom.py` | `app.api.meetingroom` | 会议室预定REST API路由 |
| 7 | `backend/app/api/admin/terminal_binding.py` | `app.api.admin.terminal_binding` | 终端绑定管理CRUD路由 |
| 8 | `backend/alembic/versions/011_add_meetingroom_tables.py` | — | Alembic迁移:创建 terminal_room_binding 表 |
#### 2.2 后端 — 修改文件
| # | 物理路径 | 修改内容 |
|---|---------|---------|
| 9 | `backend/app/config.py` | 新增 `wecom_meetingroom_secret`、`xylink_enterprise_id`、`xylink_client_id`、`xylink_client_secret`、`xylink_api_base`、`xylink_ext_id` 配置项 |
| 10 | `backend/app/services/wecom_service.py` | 新增 `get_meetingroom_access_token()` 方法 + `get_meetingroom_list()` / `get_booking_info()` / `book_meetingroom()` / `cancel_booking()` / `get_booking_detail()` 方法 |
| 11 | `backend/app/services/ws_manager.py` | 新增 `terminal_connections` 字典 + `connect_terminal()` / `disconnect_terminal()` / `send_to_terminal()` 方法 |
| 12 | `backend/app/api/ws.py` | 新增 `/ws/terminal/{terminal_sn}` WebSocket端点 |
| 13 | `backend/app/api/router.py` | 注册 meetingroom 路由和 terminal_binding 路由 |
| 14 | `backend/app/models/__init__.py` | 导入并注册 `TerminalRoomBinding` 和 `MeetingroomBookingSnapshot` 模型 |
#### 2.3 前端终端页面 — 新建文件
| # | 路径 | 职责 |
|---|------|------|
| 15 | `frontend-terminal/package.json` | 依赖声明(Vue3, Vite, Tailwind, Pinia, Vue Router, Axios) |
| 16 | `frontend-terminal/vite.config.ts` | Vite配置(base=/terminal/, proxy, port=5176) |
| 17 | `frontend-terminal/tailwind.config.js` | Tailwind配置(深色主题色板、大字号断点) |
| 18 | `frontend-terminal/tsconfig.json` | TypeScript配置 |
| 19 | `frontend-terminal/postcss.config.js` | PostCSS配置(Tailwind + Autoprefixer) |
| 20 | `frontend-terminal/index.html` | HTML入口 |
| 21 | `frontend-terminal/src/main.ts` | 应用入口(挂载Vue + Pinia + Router) |
| 22 | `frontend-terminal/src/App.vue` | 根组件 |
| 23 | `frontend-terminal/src/router/index.ts` | 路由配置(/terminal/:sn → StatusView) |
| 24 | `frontend-terminal/src/stores/terminal.ts` | Pinia状态管理(会议室状态、登录状态、WS连接) |
| 25 | `frontend-terminal/src/api/meetingroom.ts` | API调用封装(Axios) |
| 26 | `frontend-terminal/src/composables/useWebSocket.ts` | WebSocket连接管理(自动重连+降级轮询) |
| 27 | `frontend-terminal/src/composables/useAuth.ts` | 扫码登录逻辑(创建二维码+轮询状态) |
| 28 | `frontend-terminal/src/views/StatusView.vue` | 终端状态页(主页面) |
| 29 | `frontend-terminal/src/views/BookingView.vue` | 快速预定弹窗 |
| 30 | `frontend-terminal/src/components/Timeline.vue` | 时间轴组件(横向滚动,当前时间标记) |
| 31 | `frontend-terminal/src/components/StatusBadge.vue` | 状态标识组件(空闲/使用中/即将开始) |
| 32 | `frontend-terminal/src/components/QrLogin.vue` | 扫码登录组件(显示二维码+轮询) |
| 33 | `frontend-terminal/src/styles/main.css` | 全局样式(Tailwind指令 + 深色主题变量) |
| 34 | `frontend-terminal/src/types/meetingroom.ts` | TypeScript类型定义 |
#### 2.4 前端H5端 — 新建/修改文件
| # | 路径 | 职责 |
|---|------|------|
| 35 | `frontend-h5/src/views/MeetingroomView.vue` | H5端会议室预定页面(新建) |
| 36 | `frontend-h5/src/api/meetingroom.ts` | H5端会议室API调用(新建) |
| 37 | `frontend-h5/src/router/index.ts` | 添加会议室路由(修改) |
---
### 3. 数据结构和接口
#### 3.1 类图(Mermaid classDiagram)
```mermaid
classDiagram
direction TB
%% ===== 数据模型层 =====
class TerminalRoomBinding {
+Integer id
+String terminal_sn
+String terminal_name
+Integer meetingroom_id
+String meetingroom_name
+String location
+Boolean is_active
+DateTime created_at
+DateTime updated_at
}
class MeetingroomBookingSnapshot {
+Integer id
+Integer meetingroom_id
+String booking_id
+String subject
+String booker
+DateTime start_time
+DateTime end_time
+Integer status
+Date snapshot_date
+DateTime created_at
}
%% ===== Service 层 =====
class WecomService {
-Optional~Redis~ redis
-AsyncClient client
-str _token_cache
-str _contact_token_cache
-str _meetingroom_token_cache
+get_access_token() str
+get_contact_access_token() str
+get_meetingroom_access_token() str
+get_meetingroom_list(city, building, floor) list
+get_booking_info(meetingroom_id, date) list
+book_meetingroom(meetingroom_id, subject, start, end, booker, attendees) dict
+cancel_booking(booking_id) dict
+get_booking_detail(meetingroom_id, booking_id) dict
}
class MeetingroomService {
-WecomService wecom_service
-Redis redis
+get_room_list(city, building, floor) list
+get_room_status(meetingroom_id, date) list
+book_room(meetingroom_id, subject, start, end, booker, attendees) dict
+cancel_booking(booking_id, meetingroom_id) dict
+get_booking_detail(meetingroom_id, booking_id) dict
+invalidate_room_cache(meetingroom_id) void
+get_current_status(meetingroom_id) dict
}
class XyLinkService {
-str enterprise_id
-str client_id
-str client_secret
-str api_base
-AsyncClient client
+get_terminal_status(sn) dict
+push_url_to_terminal(sn, url) dict
+list_terminals() list
+generate_signature(params, secret) str
}
class ConnectionManager {
+Dict active_connections
+Dict employee_connections
+Dict terminal_connections
+connect(agent_id, ws) void
+connect_employee(employee_id, ws) void
+connect_terminal(terminal_sn, ws) void
+disconnect_terminal(terminal_sn) void
+send_to_terminal(terminal_sn, data) void
+broadcast_to_terminals(terminal_sns, data) void
}
%% ===== API Schema 层 =====
class MeetingroomListResponse {
+int meetingroom_id
+String name
+int capacity
+String location
+List devices
+int need_approval
}
class BookingInfoResponse {
+String booking_id
+String subject
+String booker
+String start_time
+String end_time
+int status
}
class BookRequest {
+int meetingroom_id
+String subject
+String start_time
+String end_time
+String booker
+List attendees
}
class TerminalBindingCreate {
+String terminal_sn
+String terminal_name
+int meetingroom_id
+String meetingroom_name
+String location
}
class TerminalBindingResponse {
+int id
+String terminal_sn
+String terminal_name
+int meetingroom_id
+String meetingroom_name
+String location
+bool is_active
+String created_at
+String updated_at
}
%% ===== 关系 =====
TerminalRoomBinding <|-- MeetingroomService : uses
MeetingroomService --> WecomService : delegates API calls
MeetingroomService --> ConnectionManager : triggers WS push
XyLinkService --> TerminalRoomBinding : manages terminals
ConnectionManager --> TerminalRoomBinding : routes by terminal_sn
```
#### 3.2 REST API 接口定义
> **路由前缀约定**:会议室API使用 `/itportal/meetingroom/`,终端绑定管理API使用 `/itportal/admin/terminal-bindings/`,与现有系统 `/itportal/automation/` 保持一致。
##### 3.2.1 会议室预定 API
| 方法 | 路径 | 说明 | 请求参数 | 响应 |
|------|------|------|---------|------|
| GET | `/itportal/meetingroom/list` | 会议室列表 | `?city=&building=&floor=` (可选过滤) | `{code:0, data:{rooms:[...]}}` |
| GET | `/itportal/meetingroom/{meetingroom_id}/booking` | 预定状态查询 | `?date=YYYY-MM-DD` | `{code:0, data:{bookings:[...]}}` |
| GET | `/itportal/meetingroom/{meetingroom_id}/status` | 当前实时状态 | — | `{code:0, data:{status:"free"/"busy", current_meeting:{...}, next_meeting:{...}}}` |
| POST | `/itportal/meetingroom/book` | 预定会议室 | `{meetingroom_id, subject, start_time, end_time, booker, attendees?}` | `{code:0, data:{booking_id:"..."}}` |
| DELETE | `/itportal/meetingroom/booking/{booking_id}` | 取消预定 | `?meetingroom_id=` | `{code:0, data:{}}` |
| GET | `/itportal/meetingroom/booking/{booking_id}/detail` | 预定详情 | `?meetingroom_id=` | `{code:0, data:{subject, booker, attendees, start_time, end_time}}` |
| GET | `/itportal/meetingroom/terminal/{terminal_sn}/binding` | 根据终端SN查询绑定 | — | `{code:0, data:{meetingroom_id, meetingroom_name, ...}}` |
##### 3.2.2 终端绑定管理 API
| 方法 | 路径 | 说明 | 请求参数 | 响应 |
|------|------|------|---------|------|
| GET | `/itportal/admin/terminal-bindings` | 绑定列表 | `?page=&size=&keyword=` | `{code:0, data:{list:[...], total:N}}` |
| POST | `/itportal/admin/terminal-bindings` | 新增绑定 | `{terminal_sn, terminal_name, meetingroom_id, meetingroom_name, location}` | `{code:0, data:{id:N}}` |
| PUT | `/itportal/admin/terminal-bindings/{id}` | 更新绑定 | `{terminal_name?, meetingroom_id?, location?}` | `{code:0, data:{}}` |
| DELETE | `/itportal/admin/terminal-bindings/{id}` | 删除绑定 | — | `{code:0, data:{}}` |
##### 3.2.3 小鱼易联终端管理 API
| 方法 | 路径 | 说明 | 请求参数 | 响应 |
|------|------|------|---------|------|
| GET | `/itportal/admin/xylink/terminals/{sn}/status` | 查询终端状态 | — | `{code:0, data:{online:bool, ...}}` |
| POST | `/itportal/admin/xylink/terminals/{sn}/push-url` | 推送页面URL | `{url}` | `{code:0, data:{}}` |
#### 3.3 WebSocket 接口定义
##### 3.3.1 终端 WebSocket 端点
```
端点: /ws/terminal/{terminal_sn}
认证: Sec-WebSocket-Protocol: bearer.{token} (与现有坐席/H5端一致)
或 Authorization: Bearer {token}
或 ?token={token} (向后兼容)
注意: 终端WS的token认证使用 terminal:token:{token} Redis key 查询
终端状态查看不需要登录(访客可查看),但预定操作需要登录
WS连接时token可选,无token时仅允许接收状态推送,不允许发送预定指令
```
##### 3.3.2 WebSocket 消息格式
**服务端 → 终端(推送消息)**:
```json
{
"type": "room_status_update",
"data": {
"meetingroom_id": 123,
"status": "free",
"current_meeting": null,
"next_meeting": {
"booking_id": "xxx",
"subject": "技术方案讨论",
"start_time": "2026-07-15T15:00:00+08:00",
"end_time": "2026-07-15T16:00:00+08:00",
"booker": "王五"
}
}
}
```
**终端 → 服务端(客户端消息)**:
```json
// 心跳
{"type": "ping"}
// 请求状态刷新(可选,通常由服务端主动推送)
{"type": "request_status", "data": {"meetingroom_id": 123}}
```
#### 3.4 Service 层接口定义
##### MeetingroomService
```python
class MeetingroomService:
"""会议室预定业务服务 — 企微API代理 + Redis缓存。"""
# Redis 缓存 key 前缀
CACHE_KEY_ROOM_LIST = "meetingroom:room_list" # TTL=600s
CACHE_KEY_BOOKING_INFO = "meetingroom:booking:{room_id}:{date}" # TTL=30s
CACHE_KEY_BOOKING_DETAIL = "meetingroom:detail:{booking_id}" # TTL=300s
async def get_room_list(self, city=None, building=None, floor=None) -> list[dict]:
"""获取会议室列表(缓存10分钟)。"""
async def get_room_status(self, meetingroom_id: int, date: str) -> list[dict]:
"""获取指定日期的预定状态(缓存30秒)。"""
async def get_current_status(self, meetingroom_id: int) -> dict:
"""获取当前实时状态(综合判断空闲/使用中/即将开始)。"""
async def book_room(self, meetingroom_id, subject, start_time, end_time,
booker, attendees=None) -> dict:
"""预定会议室(成功后清除该会议室的预定状态缓存)。"""
async def cancel_booking(self, booking_id: str, meetingroom_id: int) -> dict:
"""取消预定(成功后清除缓存)。"""
async def get_booking_detail(self, meetingroom_id: int, booking_id: str) -> dict:
"""获取预定详情(缓存5分钟)。"""
async def invalidate_room_cache(self, meetingroom_id: int, date: str = None) -> None:
"""清除指定会议室的缓存(预定/取消后调用)。"""
async def notify_terminal_update(self, terminal_sn: str, meetingroom_id: int) -> None:
"""状态变更后通过WebSocket通知终端。"""
```
##### XyLinkService
```python
class XyLinkService:
"""小鱼易联终端管理服务。"""
def __init__(self, enterprise_id, client_id, client_secret, api_base, ext_id):
"""初始化小鱼易联API客户端。"""
def _generate_signature(self, params: dict, timestamp: str) -> str:
"""生成API签名(支持签名1.0和2.0)。"""
async def get_terminal_status(self, sn: str) -> dict:
"""查询终端在线状态。"""
async def push_url_to_terminal(self, sn: str, url: str) -> dict:
"""推送自定义页面URL到终端(方案B预留)。"""
async def list_terminals(self) -> list[dict]:
"""查询终端列表。"""
```
---
### 4. 程序调用流程
#### 4.1 终端页面加载流程
```mermaid
sequenceDiagram
participant Terminal as 终端浏览器
participant Backend as FastAPI后端
participant Redis as Redis
participant WecomAPI as 企微API
Terminal->>Terminal: 访问 /terminal/{sn}
Terminal->>Terminal: Vue Router 解析 sn 参数
Terminal->>Backend: GET /itportal/meetingroom/terminal/{sn}/binding
activate Backend
Backend->>Backend: 查询 TerminalRoomBinding (by terminal_sn)
alt 绑定存在
Backend-->>Terminal: {code:0, data:{meetingroom_id, meetingroom_name, location}}
Terminal->>Backend: GET /itportal/meetingroom/{meetingroom_id}/status
activate Backend
Backend->>Redis: GET meetingroom:booking:{room_id}:{today}
alt 缓存命中
Redis-->>Backend: 预定列表
else 缓存未命中
Backend->>WecomAPI: POST /cgi-bin/oa/meetingroom/get_booking_info
activate WecomAPI
WecomAPI-->>Backend: 预定列表
deactivate WecomAPI
Backend->>Redis: SETEX meetingroom:booking:{room_id}:{today} 30
end
Backend->>Backend: 计算当前状态(free/busy/starting_soon)
Backend-->>Terminal: {code:0, data:{status, current_meeting, next_meeting, bookings}}
deactivate Backend
Terminal->>Terminal: 渲染 StatusView (状态+时间轴)
Terminal->>Backend: WS /ws/terminal/{sn} (建立WebSocket连接)
activate Backend
Backend-->>Terminal: WS连接已建立
deactivate Backend
else 绑定不存在
Backend-->>Terminal: {code:0, data:null}
Terminal->>Terminal: 显示"请联系IT管理员绑定会议室"提示页
end
deactivate Backend
```
#### 4.2 快速预定流程
```mermaid
sequenceDiagram
participant User as 用户(终端)
participant Terminal as 终端页面
participant Backend as FastAPI后端
participant Redis as Redis
participant WecomAPI as 企微API
participant WS as WebSocket管理器
User->>Terminal: 点击时间轴空闲时段
Terminal->>Terminal: 检查登录状态(token)
alt 未登录
Terminal->>Backend: POST /api/auth/qrcode (创建扫码票据)
activate Backend
Backend->>Redis: SET terminal:qrcode:{ticket} 300s
Backend-->>Terminal: {ticket, qr_url}
deactivate Backend
Terminal->>Terminal: 显示QrLogin组件(二维码)
loop 轮询(每2秒)
Terminal->>Backend: GET /api/auth/scan/status?ticket={ticket}
Backend->>Redis: GET terminal:qrcode:{ticket}
alt 已扫码确认
Backend-->>Terminal: {status:"confirmed", token:"xxx", userid:"zhangsan"}
Terminal->>Terminal: 保存token, 关闭二维码
else 等待中
Backend-->>Terminal: {status:"waiting"}
end
end
end
Terminal->>Terminal: 弹出 BookingView (输入主题+选择时长)
User->>Terminal: 输入会议主题, 选择时长, 点击确认
Terminal->>Backend: POST /itportal/meetingroom/book
activate Backend
Note over Backend: Headers: Authorization: Bearer {token}
Backend->>Redis: 验证 terminal:token:{token} → userid
Backend->>Backend: 时间按30分钟取整
Backend->>WecomAPI: POST /cgi-bin/oa/meetingroom/book
activate WecomAPI
alt 预定成功
WecomAPI-->>Backend: {errcode:0, booking_id:"xxx"}
Backend->>Redis: DEL meetingroom:booking:{room_id}:{today}
Backend->>WS: send_to_terminal(sn, {type:"room_status_update"})
Backend-->>Terminal: {code:0, data:{booking_id:"xxx"}}
Terminal->>Terminal: 显示"预定成功"动画 → 3秒后返回状态页
else 时间冲突
WecomAPI-->>Backend: {errcode:xxx, errmsg:"时间冲突"}
Backend-->>Terminal: {code:3001, message:"该时段已被预定"}
Terminal->>Terminal: 显示错误提示 → 返回预定弹窗
end
deactivate WecomAPI
deactivate Backend
```
#### 4.3 实时同步流程(P1 — WebSocket + 企微回调)
```mermaid
sequenceDiagram
participant WecomApp as 企微客户端
participant WecomCallback as 企微回调
participant Backend as FastAPI后端
participant Redis as Redis
participant WS as WebSocket管理器
participant Terminal as 终端页面
Note over WecomApp,Terminal: 场景:员工在企微客户端预定/取消会议室
WecomApp->>WecomApp: 用户在企微客户端预定会议室
WecomApp->>WecomCallback: 企微触发预定变更事件回调
activate WecomCallback
WecomCallback->>Backend: POST /api/wecom/callback (事件回调)
activate Backend
Backend->>Backend: 解析回调事件类型(meetingroom_booked/cancelled)
Backend->>Redis: DEL meetingroom:booking:{room_id}:{today}
Backend->>Backend: 查询 TerminalRoomBinding (by meetingroom_id)
alt 终端已绑定
Backend->>WS: send_to_terminal(terminal_sn, room_status_update)
activate WS
WS->>Terminal: WS推送 {type:"room_status_update", data:{...}}
deactivate WS
Terminal->>Terminal: 更新状态页显示
end
Backend-->>WecomCallback: {code:0}
deactivate Backend
deactivate WecomCallback
Note over Terminal: 终端收到WS推送后自动刷新
alt WS断线降级
Terminal->>Terminal: WS连接断开 → 启动30秒轮询
loop 每30秒
Terminal->>Backend: GET /itportal/meetingroom/{room_id}/status
Backend-->>Terminal: 最新状态
end
end
```
#### 4.4 企微扫码登录流程(终端适配)
```mermaid
sequenceDiagram
participant User as 用户手机
participant WecomApp as 企微App
participant Terminal as 终端页面
participant Backend as FastAPI后端
participant Redis as Redis
participant WecomAPI as 企微API
Note over Terminal: 终端是公共设备,无企微内置浏览器UA
Terminal->>Backend: POST /api/auth/qrcode (创建扫码登录票据)
activate Backend
Backend->>Backend: 生成 ticket + state
Backend->>Redis: SET terminal:qrcode:{ticket} {state, status:"waiting"} TTL=300s
Backend->>Backend: 构建企微OAuth2授权URL
Backend-->>Terminal: {ticket, qr_url, oauth_url}
deactivate Backend
Terminal->>Terminal: QrLogin组件显示二维码(qr_url)
User->>WecomApp: 企微App扫描二维码
WecomApp->>WecomAPI: OAuth2授权(用户确认)
WecomAPI->>Backend: GET /api/auth/oauth2/callback?code=xxx&state=xxx
activate Backend
Backend->>WecomAPI: 用code换userid
activate WecomAPI
WecomAPI-->>Backend: {userid:"zhangsan"}
deactivate WecomAPI
Backend->>Redis: SET terminal:qrcode:{ticket} {status:"confirmed", userid:"zhangsan"} TTL=60s
Backend->>Redis: SET terminal:token:{token} {userid, name, dept} TTL=28800s (8h)
Backend-->>WecomAPI: 302 重定向到终端页面
deactivate Backend
loop 轮询(每2秒)
Terminal->>Backend: GET /api/auth/scan/status?ticket={ticket}
activate Backend
Backend->>Redis: GET terminal:qrcode:{ticket}
alt status == "confirmed"
Backend-->>Terminal: {status:"confirmed", token:"xxx", userid:"zhangsan", name:"张三"}
Terminal->>Terminal: 保存token到Pinia store + localStorage
Terminal->>Terminal: 关闭QrLogin组件, 打开BookingView
else status == "waiting"
Backend-->>Terminal: {status:"waiting"}
end
deactivate Backend
end
```
---
### 5. 待明确事项
| # | 问题 | 影响范围 | 当前假设 | 建议确认方式 |
|---|------|---------|---------|-------------|
| U1 | **小鱼易联终端型号及浏览器能力** | 终端页面技术选型、WebView兼容性 | 按方案A(浏览器全屏访问URL)设计,假设终端内置浏览器支持现代ES模块和WebSocket | 实际终端上测试页面渲染效果;若浏览器版本过低,考虑加polyfill或降级为iframe方案 |
| U2 | **企微会议室API的errcode错误码映射** | 错误处理和用户提示 | 假设时间冲突返回特定errcode,需实际调用确认 | 开发阶段用企微API测试工具验证各场景的errcode |
| U3 | **企微回调事件的数据结构** | P1企微回调实现 | 假设企微会议室预定变更会触发事件回调,回调数据包含meetingroom_id和booking_id | 查阅企微官方文档确认回调事件类型和字段;若无回调能力,P1仅依赖定时刷新+WS |
| U4 | **小鱼易联API签名的具体算法** | XyLinkService实现 | 假设支持HMAC-SHA256签名1.0和2.0,但具体参数拼接方式未确认 | 参考小鱼易联官方文档 https://openapi.xylink.com/ 的签名说明 |
| U5 | **终端页面的nginx路由配置** | 部署方案 | 假设在现有nginx中新增 `location /terminal/` 指向 `frontend-terminal/dist` | 部署时在nginx配置中添加路由规则 |
| U6 | **终端WS认证策略** | 安全性 | 假设终端查看状态不需要token认证(访客可查看),但预定操作需要token。WS连接时token可选 | 确认是否允许未认证终端建立WS连接(仅接收推送) |
| U7 | **多终端绑定同一会议室** | 实时同步推送 | 假设一个会议室可绑定多个终端(如门口+室内各一个),WS推送时需遍历所有绑定该会议室的终端 | 确认是否需要支持多终端绑定同一会议室 |
| U8 | **H5端会议室入口的权限控制** | H5端页面 | 假设所有企微员工都可以通过H5端预定会议室,无需额外角色权限 | 确认是否需要限制预定权限(如仅特定部门可预定) |
---
## Part B: Task Decomposition
### 6. 依赖包列表
#### 6.1 Python 后端(新增到 `backend/requirements.txt`)
```
# 以下包均已存在于现有项目中,无需新增安装:
# - fastapi (现有)
# - uvicorn (现有)
# - sqlalchemy[asyncio] (现有)
# - asyncpg (现有)
# - redis[asyncio] (现有)
# - httpx (现有)
# - pydantic-settings (现有)
# - alembic (现有)
# 无需新增 Python 依赖包 — 所有功能均可通过现有依赖实现
```
#### 6.2 前端终端页面(`frontend-terminal/package.json`)
```json
{
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.3.0",
"pinia": "^2.1.0",
"axios": "^1.7.0"
},
"devDependencies": {
"vite": "^5.3.0",
"@vitejs/plugin-vue": "^5.0.0",
"typescript": "^5.5.0",
"vue-tsc": "^2.0.0",
"tailwindcss": "^3.4.0",
"postcss": "^8.4.0",
"autoprefixer": "^10.4.0",
"@types/node": "^20.0.0"
}
}
```
#### 6.3 前端H5端(新增到 `frontend-h5/package.json`)
```
# 无需新增 npm 依赖包 — H5端会议室页面使用现有 Vue3 + Vant4 + Axios 技术栈即可实现
```
---
### 7. 任务列表
> **分组原则**:按功能模块/层次分组,每个任务包含3+相关文件。
> **依赖原则**:尽量仅依赖T01,减少线性依赖链。
#### T01: 项目基础设施 + 数据层
| 项 | 内容 |
|----|------|
| **任务ID** | T01 |
| **任务名称** | 项目基础设施 + 数据层(配置 + 模型 + 迁移 + 前端脚手架) |
| **优先级** | P0 |
| **依赖** | 无 |
**涉及文件**:
后端:
- `backend/app/config.py` (修改) — 新增 `wecom_meetingroom_secret`、`xylink_enterprise_id`、`xylink_client_id`、`xylink_client_secret`、`xylink_api_base`、`xylink_ext_id` 配置项
- `backend/app/models/terminal_room_binding.py` (新建) — TerminalRoomBinding 模型
- `backend/app/models/meetingroom_booking_snapshot.py` (新建) — MeetingroomBookingSnapshot 模型(P2)
- `backend/app/models/__init__.py` (修改) — 注册新模型
- `backend/alembic/versions/011_add_meetingroom_tables.py` (新建) — 创建 terminal_room_binding 表
前端:
- `frontend-terminal/package.json` (新建)
- `frontend-terminal/vite.config.ts` (新建) — base=`/terminal/`, port=5176
- `frontend-terminal/tailwind.config.js` (新建) — 深色主题色板
- `frontend-terminal/tsconfig.json` (新建)
- `frontend-terminal/postcss.config.js` (新建)
- `frontend-terminal/index.html` (新建)
- `frontend-terminal/src/main.ts` (新建)
- `frontend-terminal/src/App.vue` (新建)
- `frontend-terminal/src/styles/main.css` (新建) — Tailwind指令 + 深色主题变量
- `frontend-terminal/src/types/meetingroom.ts` (新建) — TypeScript类型定义
**验收标准**:
- 后端启动无报错,新配置项可从环境变量读取
- `alembic upgrade head` 成功创建 `terminal_room_binding` 表
- `frontend-terminal` 项目 `pnpm dev` 可正常启动,显示空白Vue页面
- Tailwind CSS 生效(深色背景)
---
#### T02: 后端会议室API + Service层
| 项 | 内容 |
|----|------|
| **任务ID** | T02 |
| **任务名称** | 后端会议室预定API + Service层 + 企微API对接 |
| **优先级** | P0 |
| **依赖** | T01 |
**涉及文件**:
- `backend/app/services/wecom_service.py` (修改) — 新增 `get_meetingroom_access_token()` + 会议室API方法(list/booking_info/book/cancel/detail)
- `backend/app/services/meetingroom_service.py` (新建) — 会议室预定业务逻辑(缓存管理 + 企微API代理 + 状态计算)
- `backend/app/services/xylink_service.py` (新建) — 小鱼易联终端管理服务(签名 + 终端状态 + 推送URL)
- `backend/app/schemas/meetingroom.py` (新建) — Pydantic请求/响应模型
- `backend/app/api/meetingroom.py` (新建) — 会议室预定REST API路由
- `backend/app/api/router.py` (修改) — 注册 meetingroom 路由
**验收标准**:
- `GET /itportal/meetingroom/list` 返回企微会议室列表
- `GET /itportal/meetingroom/{id}/booking?date=YYYY-MM-DD` 返回预定状态
- `GET /itportal/meetingroom/{id}/status` 返回当前实时状态(free/busy/starting_soon)
- `POST /itportal/meetingroom/book` 成功预定并清除缓存
- `DELETE /itportal/meetingroom/booking/{id}` 成功取消并清除缓存
- `GET /itportal/meetingroom/terminal/{sn}/binding` 返回终端绑定的会议室信息
- access_token 使用 Redis 缓存(key=`wecom:meetingroom_access_token`),不重复请求
- Swagger 文档中可见所有新接口
---
#### T03: 终端绑定管理 + WebSocket实时同步
| 项 | 内容 |
|----|------|
| **任务ID** | T03 |
| **任务名称** | 终端绑定管理CRUD + WebSocket终端连接池 + 企微回调适配 |
| **优先级** | P0(绑定管理) / P1(WS+回调) |
| **依赖** | T01 |
**涉及文件**:
- `backend/app/api/admin/terminal_binding.py` (新建) — 终端绑定CRUD路由
- `backend/app/api/admin/__init__.py` (修改) — 注册 terminal_binding 子路由
- `backend/app/api/router.py` (修改) — 注册 terminal_binding 路由
- `backend/app/services/ws_manager.py` (修改) — 新增 `terminal_connections` + `connect_terminal()` / `disconnect_terminal()` / `send_to_terminal()` / `broadcast_to_terminals()`
- `backend/app/api/ws.py` (修改) — 新增 `/ws/terminal/{terminal_sn}` WebSocket端点
- `backend/app/services/meetingroom_service.py` (修改) — 新增 `notify_terminal_update()` 方法(状态变更后WS推送)
**验收标准**:
- `GET/POST/PUT/DELETE /itportal/admin/terminal-bindings` CRUD完整可用
- 终端可建立 `/ws/terminal/{sn}` WebSocket连接
- 预定/取消后,绑定的终端通过WS收到 `room_status_update` 推送
- WS断线后不影响后端服务
- 终端SN查询绑定关系接口可用
---
#### T04: 终端前端页面
| 项 | 内容 |
|----|------|
| **任务ID** | T04 |
| **任务名称** | 终端前端页面(状态展示 + 快速预定 + 扫码登录 + WS实时同步) |
| **优先级** | P0(状态+预定) / P1(WS) |
| **依赖** | T01 |
**涉及文件**:
- `frontend-terminal/src/router/index.ts` (新建) — 路由配置
- `frontend-terminal/src/stores/terminal.ts` (新建) — Pinia状态管理
- `frontend-terminal/src/api/meetingroom.ts` (新建) — API调用封装
- `frontend-terminal/src/composables/useWebSocket.ts` (新建) — WS连接管理(自动重连+降级轮询)
- `frontend-terminal/src/composables/useAuth.ts` (新建) — 扫码登录逻辑
- `frontend-terminal/src/views/StatusView.vue` (新建) — 终端状态页(主页面)
- `frontend-terminal/src/views/BookingView.vue` (新建) — 快速预定弹窗
- `frontend-terminal/src/components/Timeline.vue` (新建) — 时间轴组件
- `frontend-terminal/src/components/StatusBadge.vue` (新建) — 状态标识组件
- `frontend-terminal/src/components/QrLogin.vue` (新建) — 扫码登录组件
**验收标准**:
- 终端页面在1920×1080横屏下正常渲染,深色主题
- 页面加载3秒内显示会议室状态(空闲/使用中)+ 时间轴
- 点击空闲时段弹出预定弹窗
- 未登录时显示企微扫码二维码,扫码后自动登录
- 预定成功后显示成功动画,3秒后返回状态页
- WebSocket连接建立后,状态变更实时更新(P1)
- WS断线后自动降级为30秒轮询(P1)
- 当前时间用竖线标记在时间轴上
---
#### T05: H5端会议室页面 + 集成调试
| 项 | 内容 |
|----|------|
| **任务ID** | T05 |
| **任务名称** | H5端会议室预定页面 + nginx路由配置 + 端到端集成调试 |
| **优先级** | P1(H5页面) / P0(集成) |
| **依赖** | T02, T04 |
**涉及文件**:
- `frontend-h5/src/views/MeetingroomView.vue` (新建) — H5端会议室预定页面
- `frontend-h5/src/api/meetingroom.ts` (新建) — H5端API调用
- `frontend-h5/src/router/index.ts` (修改) — 添加会议室路由
- `frontend-h5/src/views/HomeView.vue` (修改) — 首页新增「会议室」入口卡片
**集成调试范围**:
- 终端页面 → 后端API → 企微API 全链路验证
- H5端预定 → 终端页面实时更新验证
- 终端预定 → 企微客户端显示验证
- nginx `/terminal/` 路由配置验证
**验收标准**:
- H5端可查看所有会议室列表和状态
- H5端可选择会议室 → 查看时间轴 → 预定空闲时段
- H5端预定后,终端页面在30秒内(P0轮询)/ 5秒内(P1 WS)显示更新
- 终端预定后,H5端刷新可见最新状态
- nginx正确路由 `/terminal/` 到终端前端静态文件
---
### 8. 共享知识
#### 8.1 API 路由前缀约定
```
# 现有路由前缀(不变):
/api/ — 坐席端、H5端、管理后台共用API
/itportal/automation/ — 自动化闭环API
# 新增路由前缀:
/itportal/meetingroom/ — 会议室预定API(终端+H5共用)
/itportal/admin/terminal-bindings/ — 终端绑定管理API(管理后台)
/itportal/admin/xylink/ — 小鱼易联终端管理API(管理后台)
/ws/terminal/{terminal_sn} — 终端WebSocket端点
# 注意:nginx 已经 strip /api 前缀,后端路由不包含 /api。
# /itportal/ 前缀的请求,nginx 直接透传给后端(与 /itportal/automation/ 一致)。
```
#### 8.2 Redis 缓存 key 命名规范
```
# Token 缓存(与现有模式一致)
wecom:access_token — 普通应用token (TTL=6900s)
wecom:contact_access_token — 通讯录token (TTL=6900s)
wecom:meetingroom_access_token — 会议室token (TTL=6900s) 【新增】
# 会议室数据缓存
meetingroom:room_list — 会议室列表缓存 (TTL=600s)
meetingroom:room_list:{city}:{building} — 按位置过滤的列表缓存 (TTL=600s)
meetingroom:booking:{room_id}:{date} — 指定会议室指定日期的预定状态 (TTL=30s)
meetingroom:detail:{booking_id} — 预定详情缓存 (TTL=300s)
meetingroom:status:{room_id} — 当前实时状态缓存 (TTL=10s,极短缓存防刷)
# 终端认证缓存
terminal:token:{token} — 终端登录token → {userid, name, dept} (TTL=28800s/8h)
terminal:qrcode:{ticket} — 扫码登录票据 → {status, userid} (TTL=300s)
```
#### 8.3 错误处理规范
```python
# 错误码分配(与现有体系一致:1000+通用/2000+企微/3000+业务)
# 会议室模块使用 3100-3199 段:
3101: "会议室不存在"
3102: "该时段已被预定(时间冲突)"
3103: "会议室需要审批,暂不支持快速预定"
3104: "预定开始时间已过,无法预定"
3105: "非预定人无法取消"
3106: "终端未绑定会议室"
3107: "终端绑定不存在"
3201: "小鱼易联API调用失败"
3202: "终端不在线"
2001: "企微API返回错误" # 复用现有
2002: "企微access_token获取失败" # 复用现有
# 响应格式(与现有一致):
# 成功: {"code": 0, "data": {...}, "message": "success"}
# 失败: {"code": 3102, "data": null, "message": "该时段已被预定"}
```
#### 8.4 前后端接口契约
**终端页面 → 后端**:
| 场景 | 方法 | 路径 | 认证 | 备注 |
|------|------|------|------|------|
| 页面加载 | GET | `/itportal/meetingroom/terminal/{sn}/binding` | 无需认证 | 访客可查看 |
| 状态查询 | GET | `/itportal/meetingroom/{id}/status` | 无需认证 | 访客可查看 |
| 预定列表 | GET | `/itportal/meetingroom/{id}/booking?date=` | 无需认证 | 访客可查看 |
| 预定详情 | GET | `/itportal/meetingroom/booking/{id}/detail?meetingroom_id=` | 无需认证 | 访客可查看 |
| 预定 | POST | `/itportal/meetingroom/book` | Bearer token | 需扫码登录 |
| 取消 | DELETE | `/itportal/meetingroom/booking/{id}?meetingroom_id=` | Bearer token | 需扫码登录 |
| 扫码登录 | POST | `/api/auth/qrcode` | 无需认证 | 创建票据 |
| 扫码轮询 | GET | `/api/auth/scan/status?ticket=` | 无需认证 | 轮询状态 |
| WebSocket | WS | `/ws/terminal/{sn}` | token可选 | 无token仅接收推送 |
**H5端 → 后端**:
| 场景 | 方法 | 路径 | 认证 | 备注 |
|------|------|------|------|------|
| 会议室列表 | GET | `/itportal/meetingroom/list` | H5 token | 现有H5认证 |
| 预定状态 | GET | `/itportal/meetingroom/{id}/booking?date=` | H5 token | |
| 预定 | POST | `/itportal/meetingroom/book` | H5 token | |
| 取消 | DELETE | `/itportal/meetingroom/booking/{id}?meetingroom_id=` | H5 token | |
#### 8.5 前端终端页面设计规范
```css
/* 深色主题色板(Tailwind 自定义颜色) */
:root {
--color-bg-primary: #1a1a2e; /* 主背景 */
--color-bg-card: #16213e; /* 卡片背景 */
--color-bg-card-hover: #1a2744; /* 卡片悬停 */
--color-status-free: #07C160; /* 空闲(企微绿) */
--color-status-busy: #FF6B6B; /* 使用中(红色) */
--color-status-soon: #FFA502; /* 即将开始(橙色) */
--color-text-primary: #FFFFFF; /* 主文本 */
--color-text-secondary: #A0A0B0; /* 次要文本 */
}
/* 字号规范(大屏适配) */
.text-status-main { font-size: 96px; } /* 当前状态 */
.text-room-name { font-size: 36px; } /* 会议室名称 */
.text-timeline { font-size: 20px; } /* 时间轴 */
.text-button { font-size: 24px; } /* 操作按钮 */
/* 终端页面部署路径 */
/* 生产: https://itsupport.servyou.com.cn/terminal/{sn} */
/* 开发: http://localhost:5176/terminal/{sn} */
```
#### 8.6 数据库模型约定
```python
# TerminalRoomBinding 模型定义规范
# - 表名: terminal_room_binding(单数,与现有 agents/conversations 等一致)
# - 主键: Integer 自增(与现有 system_configs 等一致,非UUID)
# - terminal_sn: 唯一索引(一个终端只能绑定一个会议室)
# - meetingroom_id: 普通索引(一个会议室可被多个终端绑定)
# - created_at/updated_at: 服务器默认时间(与现有模型一致)
# - 使用 SQLAlchemy 2.0 Mapped 语法(与现有 Agent 等模型一致)
# Alembic 迁移编号: 011_add_meetingroom_tables
# down_revision: 010_add_agent_otp
```
---
### 9. 任务依赖图
```mermaid
graph TD
T01[T01: 项目基础设施 + 数据层
配置·模型·迁移·前端脚手架]
T02[T02: 后端会议室API + Service层
企微API对接·缓存·REST路由]
T03[T03: 终端绑定管理 + WebSocket
CRUD·WS终端连接池·回调适配]
T04[T04: 终端前端页面
状态展示·快速预定·扫码登录·WS同步]
T05[T05: H5端会议室页面 + 集成调试
H5预定页面·nginx·端到端验证]
T01 --> T02
T01 --> T03
T01 --> T04
T02 --> T05
T04 --> T05
style T01 fill:#07C160,color:#fff
style T05 fill:#FFA502,color:#fff
```
**依赖说明**:
- **T01** 是所有任务的基础(配置、模型、前端脚手架),必须最先完成
- **T02** 和 **T03** 仅依赖 T01,可并行开发(不同人员分别负责API层和WS层)
- **T04** 仅依赖 T01(前端可基于API Mock先行开发),与 T02/T03 并行
- **T05** 依赖 T02(H5端需要后端API)和 T04(集成调试需要终端页面),是最终集成阶段
**建议开发顺序**:
```
第1阶段: T01(1-2天)
第2阶段: T02 + T03 + T04 并行(3-4天)
第3阶段: T05(1-2天)
总计: 5-8天(P0核心功能)
```
---
## 附录: 与现有系统的集成点
### A1. WecomService 扩展
在现有 `WecomService` 类中新增以下方法,复用现有的 Redis缓存+内存降级模式:
```python
# 现有方法(不变):
get_access_token() → cache_key = "wecom:access_token"
get_contact_access_token() → cache_key = "wecom:contact_access_token"
# 新增方法:
get_meetingroom_access_token() → cache_key = "wecom:meetingroom_access_token"
→ 使用 settings.wecom_meetingroom_secret
→ TTL = 6900s (提前300s刷新)
→ 内存降级: self._meetingroom_token_cache
get_meetingroom_list() → POST /cgi-bin/oa/meetingroom/list
get_booking_info() → POST /cgi-bin/oa/meetingroom/get_booking_info
book_meetingroom() → POST /cgi-bin/oa/meetingroom/book
cancel_booking() → POST /cgi-bin/oa/meetingroom/cancel_book
get_booking_detail() → POST /cgi-bin/oa/meetingroom/bookinfo/get
```
### A2. ConnectionManager 扩展
在现有 `ConnectionManager` 类中新增终端连接池:
```python
# 现有连接池(不变):
active_connections: Dict[str, WebSocket] # agent_id → WS
employee_connections: Dict[str, WebSocket] # employee_id → WS
# 新增连接池:
terminal_connections: Dict[str, WebSocket] # terminal_sn → WS
# 新增方法:
connect_terminal(terminal_sn, ws) # 接受终端WS连接
disconnect_terminal(terminal_sn) # 清理终端WS连接
send_to_terminal(terminal_sn, data) # 向指定终端推送
broadcast_to_terminals(terminal_sns, data) # 向多终端推送
```
### A3. 路由注册
在 `app/api/router.py` 中新增:
```python
# 会议室预定 API
from app.api.meetingroom import router as meetingroom_router
api_router.include_router(meetingroom_router, tags=["会议室预定"])
# 终端绑定管理 API
from app.api.admin.terminal_binding import router as terminal_binding_router
api_router.include_router(terminal_binding_router, tags=["终端绑定管理"])
```
在 `app/main.py` 中无需修改 — WS路由已在 `ws.py` 中定义,`ws_router` 已被 include。
### A4. 模型注册
在 `app/models/__init__.py` 中新增:
```python
from app.models.terminal_room_binding import TerminalRoomBinding
from app.models.meetingroom_booking_snapshot import MeetingroomBookingSnapshot
__all__ = [
# ... 现有模型 ...
"TerminalRoomBinding",
"MeetingroomBookingSnapshot",
]
```
### A5. nginx 路由配置(部署时新增)
```nginx
# 终端前端页面
location /terminal/ {
alias /app/frontend-terminal/dist/;
try_files $uri $uri/ /terminal/index.html;
}
# 终端WebSocket (与现有 /ws/ 一致,无需额外配置)
# /ws/terminal/{sn} 已被现有的 /ws/ location 覆盖
```