Files
wecom_it_smart_desk/docs/02-技术文档/技术方案-REQ-用户-004-坐席在线状态查询.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

9.9 KiB
Raw Blame History

技术方案-REQ-用户-004-坐席在线状态查询

基本信息

字段 内容
需求编号 REQ-用户-004
版本 v1.2
日期 2026-07-25
状态 已实现(BUG-用户-001 已修复)
作者 Simon
关联文档 PRD-REQ-用户-004BUG-用户-001

1. 技术架构设计

1.1 接口设计

1.1.1 新增 API

路径GET /h5/agents/online-status

所属模块backend/app/api/h5.pyNginx 将 /h5/agents/* 代理到 FastAPI /h5/agents/*

认证方式:公开接口,无需认证

请求参数:无

响应格式

{
  "code": 0,
  "message": "success",
  "data": {
    "online": true
  }
}

响应字段说明

字段 类型 说明
code int 0=成功,非0=失败
message string 响应描述
data.online boolean 是否有在线坐席

1.2 数据流设计

┌─────────────┐         ┌─────────────┐         ┌─────────────┐
│   H5前端    │  HTTP   │  后端API   │  SQL   │  PostgreSQL │
│ (轮询30秒)  │────────▶│ /h5/agents │───────▶│  agents表  │
│             │         │ /online-status│        │            │
└─────────────┘         └─────────────┘         └─────────────┘
       │                                               │
       │  online: true/false                           │
       ▼                                               ▼
┌─────────────┐                                 ┌─────────────┐
│  标题栏状态 │                                 │ status='online'│
│  坐席在线   │                                 │ 的坐席记录   │
└─────────────┘                                 └─────────────┘

2. 后端实现

2.1 接口实现

文件backend/app/api/h5.py

@router.get("/h5/agents/online-status")
async def get_agents_online_status(db: AsyncSession = Depends(get_db)):
    """获取坐席在线状态(H5公开接口)。

    查询当前是否有在线坐席,返回 true/false。

    Returns:
        Dict: 统一响应格式,包含 online 字段
    """
    # 查询在线坐席数量
    stmt = select(func.count(Agent.id)).where(Agent.status == "online")
    result = await db.execute(stmt)
    count = result.scalar() or 0

    return success_response(data={"online": count > 0})

2.2 依赖导入

from sqlalchemy import select, func
from app.models.agent import Agent

2.3 路由注册

backend/app/api/router.py 中确认 h5 路由已注册:

# 确认已有
router.include_router(h5.router, prefix="/h5", tags=["H5"])

3. 前端实现

3.1 Store 修改

文件frontend-h5/src/stores/conversation.ts

修改内容

// 将硬编码改为从 API 获取
const agentOnline = ref<boolean>(false)

// 新增获取坐席在线状态的方法
async function fetchAgentOnlineStatus() {
  try {
    const res = await fetch('/api/h5/agents/online-status')
    const data = await res.json()
    if (data.code === 0) {
      agentOnline.value = data.data.online
    }
  } catch (e) {
    console.error('获取坐席在线状态失败:', e)
    // 失败时默认离线,避免误导
    agentOnline.value = false
  }
}

// 新增轮询定时器
let pollTimer: number | null = null

function startPolling() {
  if (pollTimer) return
  // 立即获取一次
  fetchAgentOnlineStatus()
  // 每30秒轮询
  pollTimer = window.setInterval(fetchAgentOnlineStatus, 30000)
}

function stopPolling() {
  if (pollTimer) {
    clearInterval(pollTimer)
    pollTimer = null
  }
}

// 页面可见性处理
if (typeof document !== 'undefined') {
  document.addEventListener('visibilitychange', () => {
    if (document.hidden) {
      stopPolling()
    } else {
      fetchAgentOnlineStatus()
      startPolling()
    }
  })
}

// 初始化时启动轮询
onMounted(() => {
  startPolling()
})

// 组件卸载时停止轮询
onUnmounted(() => {
  stopPolling()
})

3.2 模板修改

文件frontend-h5/src/components/chat/ChatPanel.vue

现有模板已支持:

  • v-if="store.agentOnline" 显示"坐席在线"
  • v-else 显示"坐席离线"

无需修改。


4. WebSocket 断连自动离线

4.1 背景

坐席端关闭浏览器窗口时,需要自动将坐席状态更新为 offline,否则 H5 端将始终显示"坐席在线"。

4.2 实现

文件backend/app/api/ws.py

agent_websocket_endpoint()finally 块中添加离线更新逻辑:

finally:
    # 更新坐席状态为 offline(finally 块确保断开时一定执行)
    try:
        session_factory = _get_session_factory()
        async with session_factory() as db:
            # ⚠️ agent_id 在 URL 中对应 Agent.user_id(不是 Agent.id
            stmt = select(Agent).where(Agent.user_id == agent_id)
            result = await db.execute(stmt)
            agent = result.scalar_one_or_none()
            if agent and agent.status != "offline":
                agent.status = "offline"
                await db.commit()
    except Exception as e:
        logger.warning(f"更新坐席状态失败: {e}")

4.3 Bug 根因(重要教训)

WS URL 参数 {agent_id} 实际传递的是企微 user_id(如 sxn),而非 DB 主键 Agent.id(如 agent-sxn-001)。最初代码错误使用 Agent.id == agent_id,导致每次都查不到坐席,离线状态从未生效。

字段 说明
Agent.id agent-sxn-001 DB 主键(UUID 格式)
Agent.user_id sxn 企微 userid
URL {agent_id} sxn 实际传递的是 user_id

修复Agent.idAgent.user_id


5. Nginx 路由配置

5.1 问题

Nginx 中 /h5/ 被配置为静态文件 alias,导致 /h5/agents/online-status 返回 H5 首页 HTML 而非 JSON。

5.2 解决方案

nginx.conflocation /h5/ 之前添加代理规则(nginx 最长前缀匹配优先):

# H5 坐席状态 API — /h5/agents/
location /h5/agents/ {
    proxy_pass http://backend_api/h5/agents/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

# H5 坐席队列状态 API — /h5/queue/
location /h5/queue/ {
    proxy_pass http://backend_api/h5/queue/;
    ...
}

# H5 用户端(兜底,静态文件)
location /h5/ {
    alias /usr/share/nginx/html/h5/;
    index index.html;
    try_files $uri /h5/index.html;
}

需要同时在 HTTPS (443) 和 HTTP (80) 两个 server block 中添加。


6. 部署与配置

6.1 无需新增配置

  • 不需要新的环境变量
  • 不需要新的数据库表
  • 不需要新的 Redis 缓存

6.2 部署顺序

  1. 部署后端(重启)
  2. 部署 H5 前端

7. 测试要点

5.1 后端测试

# 测试接口返回
curl http://localhost:8000/api/h5/agents/online-status

# 预期响应
{"code":0,"message":"success","data":{"online":true}}

5.2 前端测试

测试场景 预期结果
有在线坐席 显示"坐席在线"(绿色)
无在线坐席 显示"坐席离线"(灰色)
30秒后 状态自动刷新
切换浏览器标签页 暂停轮询
切回浏览器标签页 立即刷新一次

8. 风险与回滚

8.1 风险

风险 影响 缓解措施
轮询频率过高 增加数据库查询压力 限制30秒,且只查 count
接口被恶意调用 无(只读数据) -

8.2 回滚方案

  • 后端:回滚 h5.py 中的接口代码
  • 前端:恢复 agentOnline = true 硬编码

9. 变更记录

日期 版本 变更内容 变更人
2026-07-25 v1.0 初始版本 Simon
2026-07-25 v1.1 API 路径修正(/api/h5/ → /h5/);新增 §4 WS 断连自动离线(含 Agent.id→user_id bug 修复);新增 §5 Nginx 路由配置(/h5/agents/ 代理);状态更新为已实现 Simon
2026-07-25 v1.2 BUG-用户-001 修复:新增 §10 离线限制呼叫人工(后端 shake 加在线检查 + 前端 InputBar 加 agentOnline 判断) Simon

10. BUG-用户-001 补充:离线限制呼叫人工(v1.2)

10.1 问题

坐席离线时,用户点击"人工咨询"按钮,后端直接入队 queued,因无在线坐席永久无人接单。

10.2 后端修改

文件backend/app/api/h5.py — shake 接口

conversation.status = "queued" 之前增加坐席在线检查:

# 前置校验:必须有在线坐席
from app.models.agent import Agent
from sqlalchemy import select, func
online_count = await db.execute(
    select(func.count(Agent.id)).where(Agent.status == "online")
)
count = online_count.scalar() or 0
if count == 0:
    raise AppException(
        code=1003,
        message="暂无在线坐席,请稍后再试。Duckula(达寇拉)仍在努力为您提供帮助~"
    )

10.3 前端修改

文件frontend-h5/src/components/chat/InputBar.vue

callAgentState 计算属性首行增加坐席在线判断:

// 坐席离线 → 按钮禁用
if (!store.agentOnline) return 'disabled'

按钮提示文案调整:callAgentBtnTitle 中离线时显示"坐席离线,暂不可用"。

10.4 测试要点

场景 预期
坐席离线,AI ≥3次 按钮灰色,提示"坐席离线,暂不可用"
坐席上线(30s内)AI ≥3次 按钮绿色,可点击
离线时 curl shake { code: 1003, message: "暂无在线坐席..." }