Files
jcgl/docs/frontend/05-实时通信与语音告警.md

12 KiB
Raw Permalink Blame History

05 实时通信与语音告警

前端有 5 条长连接:4 条业务 WebSocket + 1 条本机 SIP WebSocket。它们都不走 axios,地址在代码里硬编码,连接生命周期与页面绑定。

1. 连接清单

连接 地址 页面 用途 断线行为
叫乘推送 ws://{apiRoot}/yktgl/jcwebsocket/{gyid} JcXx.vue 叫乘状态、入寓人员、充值、同步提醒 location.reload()
交路推送 ws://{apiRoot}/yktgl/jlwebsocket/{gyid} JcXx.vue 交路更新提醒 location.reload()
人员/入住推送 ws://{apiRoot}/yktgl/rlywebsocket/test33 shijinpeng/main.vue 触发自助登记 location.reload()
食堂推送 ws://{apiRoot}/yktgl/MealsWebsocket/test33 MealsOrdering.vue、paymentPlatform.vue 刷脸/刷卡回调 location.reload()
SIP 客户端 ws://127.0.0.1:9004/sipclient/{gyid} JcXx.vue 注册、呼叫指令 仅日志,不重连

说明:

  • apiRoot = window.location.host,由 public/config.js 注入;地址里再拼 /yktgl 上下文。
  • 协议固定 ws://,没有使用 config.js 的 getWebSocketUrl()(该函数支持 wss 但无人调用)。
  • 业务 WebSocket 没有心跳、没有指数退避重连,关闭即整页刷新。
  • shijinpeng 与食堂页面把 username 固定为 test33,多终端同时连接时后端广播会给所有 test33 会话,无法区分终端。
  • 后端路由规则见 后端 07:jcwebsocket 只投递给 username == gyid 或 all 的会话;jlwebsocket、rlywebsocket、MealsWebsocket 为广播。

2. 叫乘推送协议(jcwebsocket)

消息体是 JSON 字符串,至少包含 lx(类型);JcXx.vue:2097 的 switch 分发:

lx 附带的字段 前端行为 当前状态
ry content 打开“入寓登记信息”弹窗并填充 启用
cz content、id、deviceCode 打开“现金充值”弹窗,记录申请 id 启用
cz_cancel id 仅当 id 与当前弹窗一致时关闭,并提示“乘务员已取消” 启用
zcxx photoBase64、deviceCode、zcid 打开“注册人员信息”,进入新增人员流程 启用
xgxx content、newPhotoBase64 打开“修改人员信息”,带原照片/新照片 启用
refresh — 重新拉取叫乘列表 启用
autoCallFailed content、roomNo 入同步提醒队列(标题“超时未叫乘提醒”)+ 刷新列表 启用
missedCallWarning content、roomNo 入同步提醒队列 启用
routeChangeWarning content、routeStatus 原为交路变更提醒 已注释禁用
routeExpiredWarning content、roomNo 原为运安交路超时提醒 已注释禁用
callTimeResetWarning content、roomNo 原为叫乘时间重置提醒 已注释禁用
其他 — 忽略 —

后端文档 06-接口清单 目前只列了前 5 类,cz_cancel、autoCallFailed、missedCallWarning 是前端实际已在处理、文档需补齐的消息。

2.1 人员登记链路

sequenceDiagram
  participant D as 自助终端/设备
  participant B as 后端
  participant W as JcXx 值班台
  D->>B: 刷脸/读卡事件
  B->>W: {"lx":"zcxx", photoBase64, zcid, deviceCode}
  W->>W: 打开注册人员弹窗,设置 ryxx_code='0'
  W->>B: GET /jcgl/getRyxxByRyId?ryid=...
  B-->>W: 人员信息(可能不存在)
  W->>B: POST /jcgl/modifyRyXx?code=0
  B-->>W: 保存结果

xgxx 走同一弹窗但 ryxx_code='1',提交时 code=1。

3. 交路推送(jlwebsocket)

jlwebsocketonmessage(JcXx.vue:2081)不解析消息内容,只做两件事:

this.visible_jlxxtx = true
this.playVoice()   // 播放 orderCompleteTip.mp3

页面右下角出现“交路信息已更新”的浮动提示(bottom-right-div),点击后打开交路弹窗。消息内容、变更类型完全依赖用户再查询。

4. 告警链路

4.1 报警音状态机

相关字段:

  • alarmVoiceActive:是否正在播放报警音;
  • dismissedAlarmEvents:已人工处理的事件 key 集合;
  • audioPlayerAlarm:<audio loop>,loop 绑定 alarmVoiceActive。

syncAlarmVoice(rows)(JcXx.vue:2742)每次列表刷新后执行:

flowchart TD
  A["收到 jcxxList"] --> B{"存在 ryzt=2/4 的行?"}
  B -- 否 --> C{"alarmVoiceActive?"}
  C -- 是 --> D["stopAlarmVoice()"]
  C -- 否 --> E["保持静音"]
  B -- 是 --> F{"该行有 alarmEventKey?"}
  F -- 是 --> G{"任一 key 未被 dismissed?"}
  F -- 否 --> H{"stoped != '1'?"}
  G -- 是 --> I["开始播放报警音"]
  H -- 是 --> I
  G -- 否 --> E
  H -- 否 --> E

事件 key 来自后端字段 alarmEventKey,可能是逗号分隔的多个 key(getAlarmEventKeys)。人工停止报警成功后:

  1. 调用 /jcgl/handleStopCalling;
  2. 把该行所有 key 写入 dismissedAlarmEvents;
  3. ++callListRequestVersion 使在途列表响应失效;
  4. 立即 syncAlarmVoice(this.jcxxList) 停音;
  5. 再刷新列表。

这样即使后端状态尚未落库、旧列表又返回,也不会重新响铃。对应测试见 tests/alarm-state.test.cjs。

4.2 停止报警的入口

入口 方法 条件
顶部“停止报警” stopCurrentAlarm 选中行必须是 ryzt=2/4
行内“暂停报警” handleStopCalling(row) 同上

非报警状态点击“暂停报警”只提示“当前状态停止报警无效”。

5. 同步提醒:弹窗队列 + TTS 队列

autoCallFailed、missedCallWarning 等消息不直接弹窗,而是走两级队列。

5.1 入队与合并

enqueueSyncNotice(title, content)(JcXx.vue:2765):

  1. 以 title|content 为 key 去重;
  2. 已人工关闭过的 key(syncDismissedNoticeKeys)直接丢弃;
  3. 800 ms 内的多条提醒合并到一次 flushSyncNotices()。

flushSyncNotices(:2782):

  • 按 title 分组,生成 sections: [{title, contents[]}];
  • 每个分组进入 TTS 队列;
  • 若当前已有提醒弹窗,则把新 section 合并进当前弹窗并重置 10 秒关闭计时;
  • 否则入 syncDialogQueue,由 showNextSyncDialog 依次展示;
  • 每次刷新都会调用 getJcXxList()。

5.2 弹窗行为

  • 标题固定“交路及叫乘提醒”,展示所有 section;
  • 10 秒无操作自动关闭(startSyncDialogCloseTimer);
  • 用户手动关闭时,handleSyncDialogBeforeClose 把当前所有 section 的 key 写入 syncDismissedNoticeKeys;
  • @closed 后展示队列中的下一条;
  • 关闭后同一内容不会再打扰,直到页面刷新(集合只在 destroyed 清空)。

5.3 TTS 播报

enqueueSyncSpeech 把同一 title 下的 contents 用“;”拼成一段文本。播放调度 playNextSyncSpeech(JcXx.vue:2917):

  • 使用浏览器 SpeechSynthesisUtterance,lang=zh-CN、rate=0.9;
  • 单条播完 500 ms 后播下一条;
  • 若 isBusinessVoiceBusy() 为真,则 2 秒后重试;
  • 业务语音(叫乘/通话/临叫/页面音频/其他 TTS)优先:getJcXxList 发现忙时调用 interruptSyncSpeechForBusinessVoice(),把当前播报退回队首并 cancel(),等业务语音结束再播。

isBusinessVoiceBusy(:2896)判定任意一条成立即忙:

  • 列表存在 ryzt=1/5;
  • activeTempCalls 非空(本页发起的临叫);
  • 4 个 <audio> 中任意一个正在播放;
  • 浏览器正在播其他 TTS 且不是同步提醒自己。

5.4 队列字段总览

字段 作用
syncNoticeQueue / syncNoticeTimer 800 ms 合并窗口
syncDialogQueue / syncCurrentDialog / syncDialogVisible 弹窗队列
syncDialogActive / syncDialogCloseTimer 弹窗互斥与 10 秒自动关闭
syncDismissedNoticeKeys 已关闭提醒去重
syncSpeechQueue / syncSpeechCurrent / syncSpeechRetryTimer TTS 队列与重试

6. 运安离线报警

getSynJcXxIsOffline 每 10 分钟调用 /jwDataLog/getSynJcXxIsOffline(JcXx.vue:4522):

if (res.data == '1') {
  this.visible_synJcXxIsOffline = true
  this.playOfflineAlarmVoice()
}
  • 离线弹窗标题“报警”,内容“运安计划数据同步异常,请联系技术人员处理!”;
  • 离线报警音复用 alarm.mp3,但不循环、不参与 alarmVoiceActive 状态机;
  • 只提示一次,不做持续告警,也不会自动关闭(需人工点关闭);
  • 未做去重,10 分钟一次会重复播放,直到后端恢复。

7. SIP 语音通道

initClientSocket(JcXx.vue:2019)连接 ws://127.0.0.1:9004/sipclient/{gyid},连接成功后设置 sipclient.connected = 1。

前端只发送两类报文:

7.1 选中行时注册状态

{
  "regSta": {
    "acc_no": 1,
    "reg_act": 0,
    "sta": 2,
    "code": 200,
    "expire": 180
  }
}

在 handleRowClick 中发送(JcXx.vue:3241),目的疑似让 SIP 客户端切换/刷新注册账号。

7.2 发起呼叫

{
  "reqAct": {
    "type": "call_make",
    "actId": "actid_12121212",
    "params": ["sip:1001@10.32.100.166"]
  }
}

在 handleRoomCallConfirm 中发送(:4268),随后才调用后端 /jcgl/handleRoomCall(2)。

问题:

  • 目标 sip:1001@10.32.100.166 与 actId 都是硬编码,换环境必须改代码;
  • SIP 发送与后端调用没有事务关系,任一方失败都会造成“界面显示已呼叫/实际未呼叫”或相反;
  • 停止呼叫只调后端,没有对应的 SIP call_end 报文;
  • sipclient.connected 只在首次 onopen 设置,断线后不清零,按钮仍可点。

8. 食堂与自助终端的推送

页面 消息处理 结果
MealsOrdering.vue messageObj.code === '5' && icTag === 1 调用 /Meals/consume 扣款,成功后 handleSuccess 建单;失败提示“卡内余额不足”
paymentPlatform.vue 同样的回调 initWebSocket() 在 mounted 被注释,回调不会触发
shijinpeng/main.vue 任意消息 重置人员信息、显示“入寓登记,请刷脸!”、开始录像
chefOrders.vue 不连 WebSocket 监听 BroadcastChannel("myChannel") 的 "2603" 消息刷新订单并响铃

详见 06-食堂与自助终端模块。

9. 连接生命周期与风险

sequenceDiagram
  participant P as JcXx.vue
  participant WS as 业务 WebSocket
  P->>WS: mounted 建立连接
  WS-->>P: onopen(仅日志)
  WS-->>P: onmessage(业务消息)
  alt 网络抖动/后端重启
    WS-->>P: onclose
    P->>P: location.reload()
    P->>WS: 重新加载页面后重连
  end
  alt 退出登录/路由离开
    P->>WS: close()
    WS-->>P: onclose
    P->>P: location.reload()(与方法内清理竞争)
  end
风险 说明
刷新风暴 后端重启时所有值班台同时 reload,重连后若仍失败会继续刷新
无重连退避 没有 setTimeout 重连、没有最大重试次数
退出竞争 handleLogout 先 close() 再清登录信息,onclose 的 reload 可能在不同时序触发
路由离开刷新 destroyed 直接调用 jlwebsocketclose()/jcwebsocketclose(),方法内 reload()
wss 不支持 HTTPS 部署时 ws:// 被浏览器拦截
固定 username 多终端共用 test33,消息无法定向
无消息校验 JSON.parse(e.data) 无 try/catch,非法消息会中断回调

10. 修改落点

需求 位置
新增叫乘消息类型 jcwebsocketonmessage :2097
改告警判定 syncAlarmVoice :2742、getAlarmEventKeys :2739
改提醒弹窗样式/时长 模板 1347–1378、startSyncDialogCloseTimer :2859
改 TTS 语速/优先级 playNextSyncSpeech :2917、isBusinessVoiceBusy :2896
改 WebSocket 地址/重连 initWebSocket :2005、initClientSocket :2019
改 SIP 报文 handleRowClick :3241、handleRoomCallConfirm :4263
改离线报警 getSynJcXxIsOffline :4522