Files
frontend-miniapp/docs/Data/miniapp_audio_play_text_api_integration.md
lyf 8fed715235
Some checks failed
CI / verify (push) Has been cancelled
chore: sync latest project updates
2026-07-03 14:42:38 +08:00

11 KiB
Raw Permalink Blame History

小程序导览音频与讲解词接口对接说明

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. 默认参数规则

targetTypetargetId 必须传,不做默认值。

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 / extended
  • zh / en 作为正式对接语言码
  • businessId
  • MinIO 路径
  • 四通道 URLstandardAudioUrlextendedAudioUrl

2. ITEM / STOP 语义

STOP 是真实讲解播放单元。

ITEM 是小程序兼容入口。

场景 推荐调用
小程序只有展品 ID targetType=ITEM&targetId=展品ID
小程序已经拿到讲解点 ID targetType=STOP&targetId=讲解点ID

规则:

  • ITEMtargetId 必须是展品 ID不要传讲解点 ID。
  • STOPtargetId 必须是讲解点 ID不要传展品 ID。
  • ITEM 请求内部会根据展品 stopId 找到讲解点,但响应里的 targetTypetargetId 仍保持小程序请求值,便于客户端按请求维度缓存。

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-infoplay-info 使用同一套语言、版本和目标解析逻辑。也就是说,小程序不需要自己判断标准版/拓展版;文本接口会返回与当前后端播放解析一致的正文版本。

5. 推荐调用流程

小程序点击播放时:

  1. 调用 play-info
  2. 如果 playable=false,展示不可播放原因,不调用 text-info
  3. 如果 playable=true,立即设置 audio.src = playUrl 并开始播放。
  4. 如果当前页面要展示讲解词,且 hasText=true,再异步调用 text-info
  5. 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 小程序本地缓存建议

播放信息本地缓存:

  • keytargetType + targetId + lang
  • 语言切换后清理旧语言缓存。
  • 播放失败、403、404 或 URL 过期时,重新请求一次 play-info

正文文本本地缓存:

  • keytargetType + targetId + lang
  • valuetext + textHash
  • 若重新请求 text-infotextHash 变化,替换本地正文。
  • 不要在小程序侧长期永久缓存正文;建议按会话或短期持久化缓存即可。

8. 2 万游客/天的性能口径

按一天 2 万游客访问,播放解析接口和文本接口都应该由 Redis 承接热点请求,但系统瓶颈不能放在 Spring Boot 音频流上。

当前职责分工:

  • play-info轻量解析Redis 短 TTL 缓存。
  • text-info:热门正文 Redis 缓存LRU 控制容量。
  • 音频文件:对象存储 / CDN / HTTPS 直出。

这样即使热门展品被大量点击:

  • 播放地址解析大多命中 play-info 缓存。
  • 正文展示大多命中 text-info 热文本缓存。
  • 最大流量的 MP3 文件不经过业务后端。

后续如果真实压测显示 Redis 或数据库仍有压力,再考虑:

  • 对展厅/热门展品做预热缓存。
  • play-infotext-info 增加批量预取,但播放前仍以单个 play-info 为准。
  • 将音频和文本静态化到 CDN 边缘,但仍由后端接口返回当前可用地址。

9. 联调检查清单

  • 不传 langplay-info 默认返回中文标准版解析结果。
  • 不传 langtext-info 默认返回中文标准版正文。
  • 中文可播放:playable=trueplayUrl 可直接播放,hasText 正确。
  • 中文有正文:text-info 返回 available=truetexttextLengthtextHash
  • 英文缺音频:play-info 返回 NO_PUBLISHED_AUDIO,不自动播放中文。
  • 英文缺正文:text-info 返回 available=false,不影响中文和音频播放。
  • 展品未绑定讲解点:NO_GUIDE_STOP
  • 讲解点无讲解词:NO_GUIDE_CONTENT
  • 播放 URL 失效:小程序重新请求一次 play-info
  • 后台发布/刷新音频后:对应 play-infotext-info 缓存应失效。
  • 热门正文多次请求:第二次开始命中 Redis 文本缓存。