Files
wecom_it_smart_desk/docs/02-技术文档/01-架构设计/会议室预定-小鱼易联终端-架构设计.md
T
Simon facc04aa65 chore: docs 结构整改 + compose 双目录对齐(合并重建提交)
本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交
(5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。

一、docs 结构整改(整改 #14)
根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入,
随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录
树并存于 docs/,共 791 文件、双分类体系冲突。

修复动作:
- b2 同名异主题文件改名迁移保全 9 个
- C 类 39 个孤立文件按主题正确归类
- A/B1 类 222 个重复文件删除(新结构已有内容副本)
- 9 个旧独有空目录删除
- 270 处内部引用按 verified 映射改写
- 整改记录 #14 登记于 04-运维文档/部署运维

结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。
残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。

二、compose 双目录对齐(消除踩坑 A)
- docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist
  改为 src/frontend-*/dist(h5 / agent / admin / terminal)
- docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/
- 效果:本地 docker compose up 不再把根目录 stale dist 挂回,
  与线上一致,分叉隐患消除(已 docker compose config 校验通过)

防复发铁律:
- 重构须提交;仓库修复须 git stash -u 或先 commit
- 新结构须 git add 并提交,避免再次 untracked 复活
- H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
2026-08-07 22:31:32 +08:00

52 KiB
Raw Blame History

会议室预定 — 小鱼易联终端 系统架构设计

版本: v1.0 日期: 2026-07-15 作者: 高见远 (Gao) · 架构师 状态: 待评审 所属项目: wecom_it_smart_desk — IT智能服务台 上游PRD: docs/01-产品文档/08-集成生态/会议室预定-小鱼易联终端-PRD.md


目录


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_tokenTTL=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_secretxylink_enterprise_idxylink_client_idxylink_client_secretxylink_api_basexylink_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 导入并注册 TerminalRoomBindingMeetingroomBookingSnapshot 模型

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

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 消息格式

服务端 → 终端(推送消息):

{
  "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": "王五"
    }
  }
}

终端 → 服务端(客户端消息):

// 心跳
{"type": "ping"}

// 请求状态刷新(可选,通常由服务端主动推送)
{"type": "request_status", "data": {"meetingroom_id": 123}}

3.4 Service 层接口定义

MeetingroomService
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
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 终端页面加载流程

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 快速预定流程

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 + 企微回调)

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 企微扫码登录流程(终端适配)

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

{
  "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_secretxylink_enterprise_idxylink_client_idxylink_client_secretxylink_api_basexylink_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 错误处理规范

# 错误码分配(与现有体系一致: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 前端终端页面设计规范

/* 深色主题色板(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 数据库模型约定

# 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. 任务依赖图

graph TD
    T01[T01: 项目基础设施 + 数据层<br/>配置·模型·迁移·前端脚手架]
    T02[T02: 后端会议室API + Service层<br/>企微API对接·缓存·REST路由]
    T03[T03: 终端绑定管理 + WebSocket<br/>CRUD·WS终端连接池·回调适配]
    T04[T04: 终端前端页面<br/>状态展示·快速预定·扫码登录·WS同步]
    T05[T05: H5端会议室页面 + 集成调试<br/>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 是所有任务的基础(配置、模型、前端脚手架),必须最先完成
  • T02T03 仅依赖 T01,可并行开发(不同人员分别负责API层和WS层)
  • T04 仅依赖 T01(前端可基于API Mock先行开发),与 T02/T03 并行
  • T05 依赖 T02(H5端需要后端API)和 T04(集成调试需要终端页面),是最终集成阶段

建议开发顺序:

第1阶段: T011-2天)
第2阶段: T02 + T03 + T04 并行(3-4天)
第3阶段: T051-2天)
总计: 5-8天(P0核心功能)

附录: 与现有系统的集成点

A1. WecomService 扩展

在现有 WecomService 类中新增以下方法,复用现有的 Redis缓存+内存降级模式:

# 现有方法(不变):
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 类中新增终端连接池:

# 现有连接池(不变):
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 中新增:

# 会议室预定 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 中新增:

from app.models.terminal_room_binding import TerminalRoomBinding
from app.models.meetingroom_booking_snapshot import MeetingroomBookingSnapshot

__all__ = [
    # ... 现有模型 ...
    "TerminalRoomBinding",
    "MeetingroomBookingSnapshot",
]

A5. nginx 路由配置(部署时新增)

# 终端前端页面
location /terminal/ {
    alias /app/frontend-terminal/dist/;
    try_files $uri $uri/ /terminal/index.html;
}

# 终端WebSocket (与现有 /ws/ 一致,无需额外配置)
# /ws/terminal/{sn} 已被现有的 /ws/ location 覆盖