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:
+151
-3
@@ -95,10 +95,29 @@ async def websocket_endpoint(
|
||||
return
|
||||
|
||||
# 步骤3: 从 Redis 查询 token 对应的坐席信息
|
||||
# Redis 中存储格式: agent:token:{token} -> agent_user_id
|
||||
# (与坐席登录 API /api/agents/login 存储格式一致)
|
||||
# 支持两种格式:
|
||||
# 1. 新格式: user:token:{token} -> JSON {employee_id, roles, ...}
|
||||
# 2. 旧格式: agent:token:{token} -> employee_id
|
||||
try:
|
||||
stored_agent_id = await cache_service.get(f"agent:token:{token}")
|
||||
import json
|
||||
|
||||
# 先尝试新格式
|
||||
user_info = await cache_service.get(f"user:token:{token}")
|
||||
if user_info:
|
||||
try:
|
||||
user_data = json.loads(user_info)
|
||||
stored_agent_id = user_data.get("employee_id")
|
||||
roles = user_data.get("roles", [])
|
||||
if "agent" not in roles:
|
||||
# token 没有 agent 角色
|
||||
await websocket.accept()
|
||||
await websocket.close(code=WS_CLOSE_UNAUTHORIZED, reason="No agent role")
|
||||
return
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
stored_agent_id = None
|
||||
else:
|
||||
# 兼容旧格式
|
||||
stored_agent_id = await cache_service.get(f"agent:token:{token}")
|
||||
except Exception as e:
|
||||
# Redis 不可用时必须拒绝连接:token 验证依赖 Redis,无法验证身份
|
||||
# 如果降级放行,攻击者可在 Redis 故障时用任意 agent_id 冒充坐席
|
||||
@@ -319,3 +338,132 @@ async def h5_websocket_endpoint(
|
||||
# 其他异常
|
||||
ws_manager.disconnect_employee(employee_id)
|
||||
logger.warning(f"H5 WebSocket 异常断开: employee_id={employee_id}, error={e}")
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 终端 WebSocket 端点(小鱼易联终端大屏)
|
||||
# ==========================================================================
|
||||
|
||||
@router.websocket("/ws/terminal/{terminal_sn}")
|
||||
async def terminal_websocket_endpoint(
|
||||
websocket: WebSocket,
|
||||
terminal_sn: str,
|
||||
) -> None:
|
||||
"""终端 WebSocket 端点主循环(token可选认证)。
|
||||
|
||||
做什么:
|
||||
1. 从 subprotocol / header / query 获取 token(可选)
|
||||
2. token 存在时验证有效性,不存在时允许连接(仅接收推送)
|
||||
3. 注册到 ConnectionManager 的终端连接表
|
||||
4. 进入消息接收循环,处理心跳 ping 和 request_status
|
||||
5. 连接断开时清理注册信息
|
||||
|
||||
认证策略(与坐席/H5端不同):
|
||||
- 终端查看状态不需要登录(访客可查看)
|
||||
- token 可选:无token时仅接收状态推送,不能发预定指令
|
||||
- 有token时记录登录用户身份(可用于预定操作)
|
||||
|
||||
Args:
|
||||
websocket: FastAPI WebSocket 对象
|
||||
terminal_sn: 终端序列号(从 URL 路径参数获取)
|
||||
"""
|
||||
# ======================================================================
|
||||
# Token 认证(可选 — 无token也允许连接)
|
||||
# ======================================================================
|
||||
|
||||
# 从 subprotocol / header / query 获取 token
|
||||
subprotocol = websocket.headers.get("sec-websocket-protocol", "")
|
||||
if subprotocol.startswith("bearer."):
|
||||
token = subprotocol[7:]
|
||||
else:
|
||||
auth_header = websocket.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
token = auth_header[7:]
|
||||
else:
|
||||
token = websocket.query_params.get("token", "")
|
||||
|
||||
# token 可选:无token也允许连接(仅接收推送,不能发预定指令)
|
||||
# 有token时验证(用于后续预定操作的身份识别)
|
||||
is_authenticated = False
|
||||
if token:
|
||||
try:
|
||||
import json
|
||||
# 尝试新格式
|
||||
user_info = await cache_service.get(f"user:token:{token}")
|
||||
if user_info:
|
||||
try:
|
||||
user_data = json.loads(user_info)
|
||||
is_authenticated = True
|
||||
logger.debug(
|
||||
f"终端 WS 已认证: sn={terminal_sn}, "
|
||||
f"user={user_data.get('employee_id')}"
|
||||
)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
else:
|
||||
# 兼容旧格式
|
||||
stored_id = await cache_service.get(f"agent:token:{token}")
|
||||
if stored_id:
|
||||
is_authenticated = True
|
||||
except Exception as e:
|
||||
logger.warning(f"终端 WS token 验证失败(降级为未认证): sn={terminal_sn}, error={e}")
|
||||
|
||||
# ======================================================================
|
||||
# 建立连接(无论是否认证都接受)
|
||||
# ======================================================================
|
||||
|
||||
await ws_manager.connect_terminal(terminal_sn, websocket, subprotocol=subprotocol)
|
||||
auth_label = "已认证" if is_authenticated else "未认证(仅查看)"
|
||||
logger.info(f"终端 WebSocket 连接已建立: sn={terminal_sn}, 状态={auth_label}")
|
||||
|
||||
try:
|
||||
# 消息接收循环
|
||||
while True:
|
||||
data = await websocket.receive_json()
|
||||
|
||||
# 处理心跳 ping
|
||||
if data.get("type") == "ping":
|
||||
await websocket.send_json({"type": "pong"})
|
||||
logger.debug(f"终端 WebSocket 心跳: sn={terminal_sn}")
|
||||
|
||||
# 处理状态刷新请求
|
||||
elif data.get("type") == "request_status":
|
||||
meetingroom_id = data.get("data", {}).get("meetingroom_id")
|
||||
if meetingroom_id:
|
||||
# 获取最新状态并推送
|
||||
try:
|
||||
from app.services.meetingroom_service import MeetingroomService
|
||||
from app.services.wecom_service import WecomService
|
||||
from app.config import settings
|
||||
|
||||
redis_client = settings.create_redis_client()
|
||||
wecom_service = WecomService(redis_client)
|
||||
mr_service = MeetingroomService(wecom_service, redis_client)
|
||||
status_data = await mr_service.get_current_status(meetingroom_id)
|
||||
|
||||
await websocket.send_json({
|
||||
"type": "room_status_update",
|
||||
"data": {
|
||||
"meetingroom_id": meetingroom_id,
|
||||
"status": status_data.get("status"),
|
||||
"current_meeting": status_data.get("current_meeting"),
|
||||
"next_meeting": status_data.get("next_meeting"),
|
||||
"minutes_to_next": status_data.get("minutes_to_next"),
|
||||
},
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning(f"终端请求状态刷新失败: sn={terminal_sn}, error={e}")
|
||||
|
||||
else:
|
||||
logger.debug(
|
||||
f"终端 WebSocket 收到未知消息: sn={terminal_sn}, "
|
||||
f"type={data.get('type', 'unknown')}"
|
||||
)
|
||||
|
||||
except WebSocketDisconnect:
|
||||
ws_manager.disconnect_terminal(terminal_sn)
|
||||
logger.info(f"终端断开 WebSocket 连接: sn={terminal_sn}")
|
||||
|
||||
except Exception as e:
|
||||
ws_manager.disconnect_terminal(terminal_sn)
|
||||
logger.warning(f"终端 WebSocket 异常断开: sn={terminal_sn}, error={e}")
|
||||
|
||||
Reference in New Issue
Block a user