一个「调通旧功能」如何变成四层重建——语音播报链路断裂复盘

任务原文写的是「调试旧版语音播报是否可用」。预期是「旧版已有,调通即可」。实际逐层调试后结论很反常:除了最底层的设备能力可复用,上面四层——Feign、触发、配置、素材持久化——全部断裂。这不是「调通旧功能」,而是在底层能力之上重建四层链路。这篇记录每一层的现状、证据和误判修正。

背景

AI 视频分析闭环里有三个联动:平台弹窗、信息屏发布、语音播报。语音播报子任务原文:

旧版本已有相关功能,需要调试是否可用。

预期「旧版已有,调通即可」。实际调试结论:旧版只有一副骨架,四截全断,需要补开发。

四层链路总览

语音播报从平台底层能力到用户听到声音,逻辑上分四层:

1
2
3
4
5
6
7
8
9
10
flowchart TD
L1["第 1 层 · 设备能力<br/>视频平台语音广播接口"] --> L2["第 2 层 · Feign 客户端<br/>跨服务调用入口"]
L2 --> L3["第 3 层 · 触发层<br/>告警事件驱动调用"]
L3 --> L4["第 4 层 · 配置层<br/>设备/素材绑定与保存"]
L4 --> L5["附 · 素材持久化<br/>audioUrl 稳定地址"]
style L1 fill:#c8e6c9
style L2 fill:#ffcdd2
style L3 fill:#ffcdd2
style L4 fill:#ffd6a0
style L5 fill:#ffcdd2

绿色是唯一不用动的,红色是断裂的,橙色是半成品。

四层断裂
图 1:四层叠放——底层设备能力完好,上面三层全断。

第 1 层 · 设备能力——唯一可复用

integrate-iotPlfVideoClientImpl 真实调用了视频平台的语音广播接口。

能力 接口 实现
开始广播 POST /protocol/ProtocolApiService/requestVoiceBroadcas 真实调用
停止广播 POST /protocol/ProtocolApiService/stopVoice 真实调用
Provider 暴露 /platformVideo/broadcast/broadcastStart/broadcastStop 已暴露

参数:mainId(设备主 Id)+ subId(设备子 Id)+ audioUrl(语音文件地址;不传则返回 websocket 地址,推 opus 音频流)。

这是整条链路里唯一不用动的部分。底层调用和 Provider 都在,能复用。问题全在它之上。

证据:PlfVideoClientImpl.java:870(broadcastStart)、:889(broadcastStop);IotPlatformVideoProvider.java:727:742

底层探活的一个间接证据:实时监控能看画面(走同一个视频平台 PlfVideoClient.playWsFlvUrl),说明 host 配的对、网络通、登录鉴权通。广播接口和视频流接口共一套 prepareToken 鉴权,大概率也活。

第 2 层 · Feign——断裂

门开了但没修路
图 2:Provider 把接口的「门」打开了,但 Feign 客户端这层「路」没修——别的服务调不到。

IotPlatformVideoClient(Feign 客户端)只有摄像头 / 预置位方法,没有 broadcast 方法

Provider 已经把广播接口暴露在 /platformVideo/broadcast/*,但 Feign 客户端里没有对应方法——其他服务(warning)调不到。等于门开了,但没修路。

附带问题:该 Feign 客户端的 fallbackFactory 指向了通用的 IotPlatformFallbackFactory,而项目里有一个专门的 IotPlatformVideoFallbackFactory 没被用上——熔断兜底也指错了

要修三件事:

  • broadcastStart / broadcastStop 两个 Feign 方法;
  • fallbackFactory 改指 IotPlatformVideoFallbackFactory
  • iot-api 是共享模块,改完要 mvn install + 重编依赖它的服务。

证据:IotPlatformVideoClient.java:29(fallbackFactory 指错),全文件无 broadcast 方法。

这一层和第 1 层合起来是一个经典陷阱:Provider 暴露了接口不等于别的服务调得到。中间隔着 Feign 客户端这一层「路」,路没修,门开了也走不通。

第 3 层 · 触发——空 stub

warning 服务的 AlarmingEvenSinkServiceImpl.pushBroadcastDevice()空方法——查出 broadcastIds 之后方法直接结束,既不调 Feign,也没有定时停止

1
2
3
4
5
6
7
8
9
10
11
// 现状:查出列表就 return,什么都没推
private void pushBroadcastDevice(String processorId) {
AlarmingPushBroadcastDTO apbdto = new AlarmingPushBroadcastDTO();
apbdto.setProcessorId(processorId);
List<AlarmingPushBroadcastDTO> apbList =
alarmingPushBroadcastService.listByCondition(apbdto);
List<String> broadcastIds = apbList.stream()
.map(AlarmingPushBroadcastDTO::getBroadcastId)
.collect(Collectors.toList());
// ← 到此为止,broadcastIds 算出来就丢了,没有任何后续动作
}

一个被二次核对纠正的误判

最初以为这个 stub 会把告警记录的 broadcast_push_flag 置成「已推送」,造成「显示推了实际没推」。核对常量后纠正:

  • BROADCAST_PUSH_FLAG_YES="0"语义和直觉相反,”0” 才是「是」);
  • 真正的执行状态字段是 broadcast_push_status(默认 1 = 未推送)。

stub 不会误标已推送,但也没有任何真实执行结果落库

要修:完整实现——遍历配置 → 校验 mainId/subId/audioUrl 非空(缺则跳过 + 记日志,不能传空参给平台)→ 调 Feign broadcastStart异步按插播时长调 broadcastStop(禁止 Thread.sleep 阻塞告警线程)→ 失败 / 部分成功处理 + 状态落 broadcast_push_status

第 4 层 · 配置——半成品,两个子问题叠加

这一层问题最隐蔽。

4a. 设备 / 素材选择器数据源错误

告警配置页绑定广播时,「广播设备」和「广播素材」两个选择弹窗,拉的居然是 through 服务的通行通道(passagewayInfo)——风马牛不相及,是复制粘贴留下的半成品。

4b. 保存根本没调用接口

alarmConfig.vuesubmitBroadcast() 方法只关闭弹窗

1
2
3
4
// 现状:只关弹窗,没调保存接口
submitBroadcast() {
this.broadcastDialogVisible = false
}

updatealarmingPushBroadcast 虽然 import 了,但从未被调用——用户在弹窗里选完点确定,配置根本不会保存

要修:

  • 设备选择器换数据源(视频平台设备列表,带 mainId/subId);
  • 素材选择器换数据源(真实音频素材,拿 audioUrl);
  • submitBroadcast 补上调 updatealarmingPushBroadcast
  • 广播配置需能存 mainId/subId/audioUrl(对应数据库新增字段)。

附 · 素材持久化——缺失

广播 API 要 audioUrl(语音文件地址),但现有配置只存 broadcast_material_id(一个没映射关系的 id),没有稳定的 audioUrl 持久化链路

  • audio_url 必须是视频平台或 IoT 服务能访问的绝对地址,不能是浏览器相对路径;
  • 音频素材的上传(走附件服务传 mp3)、查询、URL 持久化,目前只有通用上传能力,没有完整链路

工作量汇总

要做什么 后端 / 前端 工作量
1 设备能力 无(可复用) 0
2 Feign 加 2 个方法 + 修 fallback + install 后端 0.5 天
3 触发 实现 stub(异步停播 + 状态落库 + 失败处理) 后端 1~1.5 天
4a 选择器 设备 / 素材选择器换数据源 前端 0.5~1 天
4b 保存 submitBroadcast 接通保存接口 前端 0.5 天
附 素材 音频上传 + audioUrl 持久化 后端 + 前端 0.5~1 天
联调 真实视频平台 + 带扬声器设备 联调 0.5~1 天(看环境)

工作量误判风险

最初估「后端 12 天」只够 happy path(Feign + stub 接通)。加上 mainId/subId 映射、素材 URL 链路、状态落库、前端三个修复点、联调,**完整闭环远超 12 天**。前端「0.5~1 天」只有在设备树和素材 URL 接口确定后才成立。

未确认的外部依赖(主要风险)

以下项不确认,做完代码也联调不了:

  1. 哪些设备支持语音广播(摄像头自带扬声器,还是外接音柱?)——设备侧未确认;
  2. 音频格式要求(opus / mp3?)——视频平台文档未拿到;
  3. mainId/subId 从哪来(设备同步还是手工配置?)——无广播设备同步逻辑;
  4. 视频平台能否访问附件 URL(网络可达性)——未测。

已完成的前置改动

四层里最底层、不依赖任何外部确认的两步已经做完,为后续补开发铺好地基:

① Feign 方法(第 2 层)——IotPlatformVideoClient 新增 broadcastStart / broadcastStopfallbackFactory 修正为 IotPlatformVideoFallbackFactoryiot-api 编译并 install。

② 数据承载(第 4 层的数据基础)——AlarmingPushBroadcast 实体新增 mainId / subId / audioUrl(允许为空,兼容旧数据),Flyway 迁移 V20260801.22.45__alter_alarming_push_broadcast_add_broadcast_device.sqlAlarmingPushBroadcastMapper.xml 三字段覆盖全部 12 处(resultMap / 列清单 / 条件块 / 增改)。

小结

语音播报的工作量被任务描述(「调试是否可用」)严重低估。实际是:底层能力可复用,但 Feign、触发、配置、素材持久化四层都要重建,且联调依赖设备侧能力确认。

几个值得带走的判断方法:

  • 逐层探活:从底层设备能力往上,一层一层确认通断,比笼统说「不能用」精准得多。
  • Provider 暴露 ≠ 调得到:中间隔着 Feign 客户端这层「路」。
  • 布尔常量不能按常理猜YES="0" / NO="1" 这种反直觉取值,看到任何标记先 grep 常量类确认。
  • 空 stub 不等于「标记已执行」:要分清「配置 flag」和「执行状态字段」是两回事。
  • 保存按钮只关弹窗:是最隐蔽的「功能不可用」,UI 看着正常,配置根本没落库。

建议视设备侧确认进度,决定本阶段是否纳入后续开发,或整体后移与设备接入同步推进。