chore: sync latest project updates
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-03 14:42:38 +08:00
parent 8b2c36677e
commit 8fed715235
106 changed files with 6030 additions and 121 deletions

View 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 文本缓存。