This commit is contained in:
352
docs/Data/miniapp_audio_play_text_api_integration.md
Normal file
352
docs/Data/miniapp_audio_play_text_api_integration.md
Normal file
@@ -0,0 +1,352 @@
|
||||
# 小程序导览音频与讲解词接口对接说明
|
||||
|
||||
## 0. 当前结论
|
||||
|
||||
小程序播放导览音频时,先调用播放解析接口:
|
||||
|
||||
```http
|
||||
GET /app-api/gis/guide/audio/play-info?targetType=ITEM&targetId=1001&lang=zh-CN
|
||||
```
|
||||
|
||||
如果需要展示讲解词正文,再按需调用文本接口:
|
||||
|
||||
```http
|
||||
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` 解析。
|
||||
|
||||
```http
|
||||
GET /app-api/gis/guide/audio/play-info?targetType=ITEM&targetId=1001
|
||||
GET /app-api/gis/guide/audio/text-info?targetType=ITEM&targetId=1001
|
||||
```
|
||||
|
||||
等价于:
|
||||
|
||||
```http
|
||||
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 路径
|
||||
- 四通道 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. 播放解析接口
|
||||
|
||||
```http
|
||||
GET /app-api/gis/guide/audio/play-info?targetType=ITEM&targetId=1001&lang=zh-CN
|
||||
```
|
||||
|
||||
可播放响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"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` 表达:
|
||||
|
||||
```json
|
||||
{
|
||||
"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. 讲解词正文接口
|
||||
|
||||
```http
|
||||
GET /app-api/gis/guide/audio/text-info?targetType=ITEM&targetId=1001&lang=zh-CN
|
||||
```
|
||||
|
||||
可用响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"available": true,
|
||||
"targetType": "ITEM",
|
||||
"targetId": 1001,
|
||||
"lang": "zh-CN",
|
||||
"narrationTier": "STANDARD",
|
||||
"title": "青铜神树",
|
||||
"text": "青铜神树是三星堆遗址出土的重要青铜器...",
|
||||
"textLength": 1200,
|
||||
"textHash": "0b32a4..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
不可用响应同样返回 `code=0`:
|
||||
|
||||
```json
|
||||
{
|
||||
"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. 推荐调用流程
|
||||
|
||||
小程序点击播放时:
|
||||
|
||||
1. 调用 `play-info`。
|
||||
2. 如果 `playable=false`,展示不可播放原因,不调用 `text-info`。
|
||||
3. 如果 `playable=true`,立即设置 `audio.src = playUrl` 并开始播放。
|
||||
4. 如果当前页面要展示讲解词,且 `hasText=true`,再异步调用 `text-info`。
|
||||
5. `text-info` 成功后渲染正文;失败或 `available=false` 不影响音频播放。
|
||||
|
||||
示例:
|
||||
|
||||
```ts
|
||||
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 文本缓存。
|
||||
Reference in New Issue
Block a user