11 KiB
小程序导览音频与讲解词接口对接说明
0. 当前结论
小程序播放导览音频时,先调用播放解析接口:
GET /app-api/gis/guide/audio/play-info?targetType=ITEM&targetId=1001&lang=zh-CN
如果需要展示讲解词正文,再按需调用文本接口:
GET /app-api/gis/guide/audio/text-info?targetType=ITEM&targetId=1001&lang=zh-CN
两个接口刻意分开:
play-info只负责尽快返回唯一可播放playUrl,保证游客点击后音频优先播放。text-info只在页面确实要展示正文、字幕面板或讲解词详情时调用,避免每次播放都带上大段文本。- 音频播放失败不应被文本查询拖累;文本缺失也不影响音频播放。
- 两类数据缓存策略不同:播放解析是短 TTL 元信息缓存,讲解词是热门文本缓存 + LRU 淘汰。
1. 默认参数规则
targetType 和 targetId 必须传,不做默认值。
lang 可以不传,不传时后端默认按中文标准语言 zh-CN 解析。
GET /app-api/gis/guide/audio/play-info?targetType=ITEM&targetId=1001
GET /app-api/gis/guide/audio/text-info?targetType=ITEM&targetId=1001
等价于:
GET /app-api/gis/guide/audio/play-info?targetType=ITEM&targetId=1001&lang=zh-CN
GET /app-api/gis/guide/audio/text-info?targetType=ITEM&targetId=1001&lang=zh-CN
小程序仍建议显式传全局语言状态,尤其是用户切换英文后:
| 参数 | 是否必填 | 取值 | 说明 |
|---|---|---|---|
targetType |
是 | ITEM / STOP |
ITEM 是展品入口,STOP 是讲解点入口 |
targetId |
是 | number | 必须和 targetType 匹配 |
lang |
否 | zh-CN / en-US |
不传默认 zh-CN;兼容历史 zh / en 入参 |
不要传:
standard/extendedzh/en作为正式对接语言码businessId- MinIO 路径
- 四通道 URL,如
standardAudioUrl、extendedAudioUrl
2. ITEM / STOP 语义
STOP 是真实讲解播放单元。
ITEM 是小程序兼容入口。
| 场景 | 推荐调用 |
|---|---|
| 小程序只有展品 ID | targetType=ITEM&targetId=展品ID |
| 小程序已经拿到讲解点 ID | targetType=STOP&targetId=讲解点ID |
规则:
ITEM的targetId必须是展品 ID,不要传讲解点 ID。STOP的targetId必须是讲解点 ID,不要传展品 ID。ITEM请求内部会根据展品stopId找到讲解点,但响应里的targetType、targetId仍保持小程序请求值,便于客户端按请求维度缓存。
3. 播放解析接口
GET /app-api/gis/guide/audio/play-info?targetType=ITEM&targetId=1001&lang=zh-CN
可播放响应:
{
"code": 0,
"data": {
"playable": true,
"targetType": "ITEM",
"targetId": 1001,
"lang": "zh-CN",
"narrationTier": "STANDARD",
"audioId": 8912,
"title": "青铜神树",
"duration": 120,
"format": "mp3",
"playUrl": "https://cdn.museum.com/tts-audio/xxx.mp3",
"expiresAt": null,
"subtitleUrl": null,
"hasText": true,
"fallback": false,
"fallbackReason": null,
"reason": null
}
}
不可播放仍然返回 code=0,通过 playable=false 表达:
{
"code": 0,
"data": {
"playable": false,
"targetType": "ITEM",
"targetId": 1001,
"lang": "en-US",
"narrationTier": "STANDARD",
"hasText": false,
"fallback": false,
"reason": "NO_PUBLISHED_AUDIO"
}
}
字段说明:
| 字段 | 说明 |
|---|---|
playable |
是否可播放 |
playUrl |
可直接赋值给小程序音频组件的 HTTPS/CDN/对象存储地址 |
narrationTier |
后端解析出的版本:STANDARD / EXTENDED |
hasText |
当前语言和版本是否有讲解词正文;有正文时可按需调用 text-info |
fallback |
是否发生版本降级或历史音频回退 |
reason |
不可播放原因 |
常见 reason:
| reason | 小程序建议处理 |
|---|---|
NO_GUIDE_STOP |
展品未绑定讲解点 |
NO_GUIDE_CONTENT |
讲解点没有讲解词 |
NO_PUBLISHED_AUDIO |
当前语言没有已发布音频 |
UNSUPPORTED_LANGUAGE |
语言不支持 |
UNSUPPORTED_TARGET_TYPE |
targetType 不支持 |
TARGET_NOT_FOUND |
目标不存在 |
4. 讲解词正文接口
GET /app-api/gis/guide/audio/text-info?targetType=ITEM&targetId=1001&lang=zh-CN
可用响应:
{
"code": 0,
"data": {
"available": true,
"targetType": "ITEM",
"targetId": 1001,
"lang": "zh-CN",
"narrationTier": "STANDARD",
"title": "青铜神树",
"text": "青铜神树是三星堆遗址出土的重要青铜器...",
"textLength": 1200,
"textHash": "0b32a4..."
}
}
不可用响应同样返回 code=0:
{
"code": 0,
"data": {
"available": false,
"targetType": "ITEM",
"targetId": 1001,
"lang": "en-US",
"narrationTier": "STANDARD",
"reason": "NO_TEXT"
}
}
字段说明:
| 字段 | 说明 |
|---|---|
available |
是否有正文 |
text |
讲解词正文,available=false 时为空 |
textLength |
正文字数,按 Java 字符数统计 |
textHash |
正文 MD5,用于小程序本地缓存版本判断 |
reason |
不可用原因 |
text-info 和 play-info 使用同一套语言、版本和目标解析逻辑。也就是说,小程序不需要自己判断标准版/拓展版;文本接口会返回与当前后端播放解析一致的正文版本。
5. 推荐调用流程
小程序点击播放时:
- 调用
play-info。 - 如果
playable=false,展示不可播放原因,不调用text-info。 - 如果
playable=true,立即设置audio.src = playUrl并开始播放。 - 如果当前页面要展示讲解词,且
hasText=true,再异步调用text-info。 text-info成功后渲染正文;失败或available=false不影响音频播放。
示例:
async function playGuideAudio(targetType: "ITEM" | "STOP", targetId: number) {
const lang = getGlobalLanguage() || "zh-CN"
const playInfo = await api.getAudioPlayInfo({ targetType, targetId, lang })
if (!playInfo.playable || !playInfo.playUrl) {
showToast(reasonToText(playInfo.reason))
return
}
audioContext.stop()
audioContext.src = playInfo.playUrl
audioContext.title = playInfo.title || ""
audioContext.play()
if (playInfo.hasText && shouldShowNarrationText()) {
loadNarrationTextLazy(targetType, targetId, lang)
}
}
async function loadNarrationTextLazy(targetType: "ITEM" | "STOP", targetId: number, lang: string) {
const cacheKey = `${targetType}:${targetId}:${lang}`
const cached = narrationTextCache.get(cacheKey)
if (cached) {
renderNarrationText(cached.text)
return
}
const textInfo = await api.getAudioTextInfo({ targetType, targetId, lang })
if (!textInfo.available) {
return
}
narrationTextCache.set(cacheKey, {
text: textInfo.text,
textHash: textInfo.textHash
})
renderNarrationText(textInfo.text)
}
6. 为什么不在 play-info 里直接返回正文
从系统架构看,音频播放和正文展示是两个不同优先级的动作。
播放路径应该尽量短:
- 游客点击播放后的第一目标是尽快拿到
playUrl并开始播放。 - 大段正文会增加接口响应体,弱网下会拖慢播放启动。
- 很多播放场景只需要音频,不一定展开讲解词面板。
- 音频地址和正文的缓存策略不同,混在一个接口里会让缓存粒度变粗。
- 文本接口失败时,不应该影响音频播放。
因此当前设计是:
play-info:轻量、高频、短 TTL,返回播放必要信息。text-info:按需、可延迟、热门文本 LRU 缓存,返回正文。
这对小程序实现也比较直接:音频先播,文本异步补上。页面可以先显示标题、加载态或正文骨架,文本回来后再填充。
7. 缓存策略
7.1 后端播放解析缓存
play-info 使用 Redis 缓存轻量播放解析结果。
| 项 | 策略 |
|---|---|
| Redis key | gis:guide:play-info:{targetType}:{targetId}:{lang} |
| TTL | 5 分钟 |
| 缓存内容 | 播放元信息,不含音频二进制 |
| 失效时机 | 音频发布、撤回、通道快照刷新、全量快照重建 |
音频文件流量仍然走对象存储 / CDN / HTTPS 静态地址,业务后端不代理音频流。
7.2 后端热门文本缓存 + LRU 淘汰
text-info 使用 Redis 做热门文本缓存,只缓存 available=true 的正文响应。
| 项 | 策略 |
|---|---|
| Redis key | gis:guide:text-info:{targetType}:{targetId}:{lang}:{narrationTier} |
| LRU 索引 | gis:guide:text-info:lru |
| TTL | 60 分钟 |
| 最大条数 | 1000 条 |
| 淘汰方式 | Redis ZSet 记录最近访问时间,超过上限时删除最久未访问的 key |
| 大文本处理 | 响应 JSON 超过 2KB 时 gzip 后 base64 存储 |
| 负结果缓存 | 不缓存 available=false,避免后台补文本后长期命中旧缺失状态 |
textHash 返回给小程序,用于本地缓存版本判断;后端 Redis key 不把 textHash 放进去,因为那样会导致每次命中缓存前还要先查数据库计算 hash,反而失去缓存意义。
7.3 小程序本地缓存建议
播放信息本地缓存:
- key:
targetType + targetId + lang - 语言切换后清理旧语言缓存。
- 播放失败、403、404 或 URL 过期时,重新请求一次
play-info。
正文文本本地缓存:
- key:
targetType + targetId + lang - value:
text + textHash - 若重新请求
text-info后textHash变化,替换本地正文。 - 不要在小程序侧长期永久缓存正文;建议按会话或短期持久化缓存即可。
8. 2 万游客/天的性能口径
按一天 2 万游客访问,播放解析接口和文本接口都应该由 Redis 承接热点请求,但系统瓶颈不能放在 Spring Boot 音频流上。
当前职责分工:
play-info:轻量解析,Redis 短 TTL 缓存。text-info:热门正文 Redis 缓存,LRU 控制容量。- 音频文件:对象存储 / CDN / HTTPS 直出。
这样即使热门展品被大量点击:
- 播放地址解析大多命中
play-info缓存。 - 正文展示大多命中
text-info热文本缓存。 - 最大流量的 MP3 文件不经过业务后端。
后续如果真实压测显示 Redis 或数据库仍有压力,再考虑:
- 对展厅/热门展品做预热缓存。
- 对
play-info和text-info增加批量预取,但播放前仍以单个play-info为准。 - 将音频和文本静态化到 CDN 边缘,但仍由后端接口返回当前可用地址。
9. 联调检查清单
- 不传
lang:play-info默认返回中文标准版解析结果。 - 不传
lang:text-info默认返回中文标准版正文。 - 中文可播放:
playable=true,playUrl可直接播放,hasText正确。 - 中文有正文:
text-info返回available=true、text、textLength、textHash。 - 英文缺音频:
play-info返回NO_PUBLISHED_AUDIO,不自动播放中文。 - 英文缺正文:
text-info返回available=false,不影响中文和音频播放。 - 展品未绑定讲解点:
NO_GUIDE_STOP。 - 讲解点无讲解词:
NO_GUIDE_CONTENT。 - 播放 URL 失效:小程序重新请求一次
play-info。 - 后台发布/刷新音频后:对应
play-info和text-info缓存应失效。 - 热门正文多次请求:第二次开始命中 Redis 文本缓存。