docs/:系统梳理、架构优化方案、CR-02 三端功能设计修订(含 §15 三端一致性同步与平台统计分析)、公寓端/应用端/平台端分册、公寓统计分析与排名报表、公寓端小步迭代开发计划(IT-0~IT-8) dev/:RuoYi-Vue-master(后端)与 RuoYi-Vue2-master(前端)官方上游基线,未改动
12 KiB
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)。人工停止报警成功后:
- 调用
/jcgl/handleStopCalling; - 把该行所有 key 写入
dismissedAlarmEvents; ++callListRequestVersion使在途列表响应失效;- 立即
syncAlarmVoice(this.jcxxList)停音; - 再刷新列表。
这样即使后端状态尚未落库、旧列表又返回,也不会重新响铃。对应测试见 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):
- 以
title|content为 key 去重; - 已人工关闭过的 key(
syncDismissedNoticeKeys)直接丢弃; - 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 |