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

365
docs/miniapp_integration.md Normal file
View File

@@ -0,0 +1,365 @@
# 小程序导览接口对接说明
> 统一维护约定:本文档是小程序对接的固定主文档。后续新增小程序 API、展示、播放、讲解词、异常处理和联调说明时统一修订本文档不再按单个功能新建小程序对接文档。
## 0. 快速接入
小程序导览页使用 3 个接口:
| 场景 | 接口 | 用途 |
| --- | --- | --- |
| 进入讲解页 | `stop-info` | 获取标题、简介、图片、绑定展品、当前语言音频/正文状态 |
| 点击播放 | `play-info` | 获取唯一可播放 `playUrl` |
| 展开正文 | `text-info` | 按需获取讲解词全文 |
推荐顺序:
1. 页面进入先调 `stop-info`
2. 根据 `stop-info.audioStatus` 决定播放按钮是否可点。
3. 用户点击播放时再调 `play-info`
4. 用户展开正文时再调 `text-info`
## 1. 通用参数
三个接口都使用同一组参数:
| 参数 | 是否必填 | 取值 | 说明 |
| --- | --- | --- | --- |
| `targetType` | 是 | `ITEM` / `STOP` | `ITEM` 表示展品入口,`STOP` 表示讲解点入口 |
| `targetId` | 是 | number | 必须和 `targetType` 匹配 |
| `lang` | 否 | `zh-CN` / `en-US` | 不传默认 `zh-CN` |
调用示例:
```http
GET /app-api/gis/guide/stop/info?targetType=ITEM&targetId=1001&lang=zh-CN
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` 时,`targetId` 传展品 ID。
- `targetType=STOP` 时,`targetId` 传讲解点 ID。
- 小程序建议始终显式传 `lang`,语言切换后所有请求使用新的 `lang`
- 不要传 `standard``extended``businessId`、MinIO 路径或四通道音频 URL。
## 2. stop-info讲解页展示信息
```http
GET /app-api/gis/guide/stop/info?targetType=ITEM&targetId=1001&lang=zh-CN
```
成功响应:
```json
{
"code": 0,
"data": {
"available": true,
"targetType": "ITEM",
"targetId": 1001,
"resolvedStopId": 2001,
"lang": "zh-CN",
"title": "青铜神树",
"description": "本讲解点介绍三星堆出土的青铜神树……",
"coverImageUrl": "https://cdn.example.com/cover.jpg",
"galleryUrls": "[\"https://cdn.example.com/a.jpg\",\"https://cdn.example.com/b.jpg\"]",
"imageStatus": "READY",
"imageSource": "STOP",
"linkedExhibits": [
{
"id": 1001,
"name": "青铜神树",
"nameEn": "Bronze Sacred Tree",
"exhibitCode": "EX-001",
"coverImageUrl": "https://cdn.example.com/exhibit-cover.jpg"
}
],
"playTargetType": "STOP",
"playTargetId": 2001,
"hasAudio": true,
"hasText": true,
"supportedLanguages": ["zh-CN", "en-US"],
"audioStatus": "READY",
"reason": null
}
}
```
不可用响应仍返回 `code=0`
```json
{
"code": 0,
"data": {
"available": false,
"targetType": "ITEM",
"targetId": 9999,
"lang": "zh-CN",
"imageStatus": "MISSING",
"imageSource": "NONE",
"linkedExhibits": [],
"hasAudio": false,
"hasText": false,
"supportedLanguages": [],
"audioStatus": "MISSING",
"reason": "NO_GUIDE_STOP"
}
}
```
字段说明:
| 字段 | 说明 |
| --- | --- |
| `available` | 是否有可展示的讲解点 |
| `resolvedStopId` | 最终解析到的讲解点 ID |
| `coverImageUrl` | 讲解点封面图 |
| `galleryUrls` | 讲解点图集JSON 字符串数组,客户端需安全解析 |
| `imageStatus` | `READY` / `MISSING` |
| `linkedExhibits` | 当前讲解点绑定的展品列表 |
| `playTargetType` / `playTargetId` | 调用 `play-info``text-info` 时推荐使用的目标 |
| `hasAudio` | 是否存在任一语言音频 |
| `supportedLanguages` | 有音频的语言列表 |
| `audioStatus` | 当前请求语言是否可播放:`READY` / `MISSING` |
| `hasText` | 当前语言是否有讲解词正文 |
| `reason` | 不可用原因 |
图片规则:
- `imageStatus=READY`:使用 `coverImageUrl``galleryUrls` 渲染讲解点图片。
- `imageStatus=MISSING`:展示占位图或隐藏图片区域。
- 不要用 `linkedExhibits[*].coverImageUrl` 兜底讲解点主图。
音频按钮规则:
- 播放按钮以 `audioStatus` 为准。
- `hasAudio=true` 只表示存在任一语言音频,不代表当前语言可播放。
- 如果 `audioStatus=MISSING`,当前语言播放按钮置灰。
## 3. play-info音频播放
```http
GET /app-api/gis/guide/audio/play-info?targetType=STOP&targetId=2001&lang=zh-CN
```
可播放响应:
```json
{
"code": 0,
"data": {
"playable": true,
"targetType": "STOP",
"targetId": 2001,
"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`
```json
{
"code": 0,
"data": {
"playable": false,
"targetType": "STOP",
"targetId": 2001,
"lang": "en-US",
"narrationTier": "STANDARD",
"hasText": false,
"fallback": false,
"reason": "NO_PUBLISHED_AUDIO"
}
}
```
字段说明:
| 字段 | 说明 |
| --- | --- |
| `playable` | 是否可播放 |
| `playUrl` | 可直接赋值给小程序音频组件的地址 |
| `expiresAt` | 播放地址过期时间;为空表示当前地址无短期过期限制 |
| `duration` | 音频时长,单位秒 |
| `format` | 音频格式 |
| `hasText` | 是否有同语言讲解词正文 |
| `reason` | 不可播放原因 |
播放失败处理:
- `playable=false`:不启动播放器,按 `reason` 展示提示。
- `playUrl` 播放失败、403、404 或疑似过期:重新请求一次 `play-info`
- 请求英文没有音频时,不要自动播放中文音频。
## 4. text-info讲解词正文
```http
GET /app-api/gis/guide/audio/text-info?targetType=STOP&targetId=2001&lang=zh-CN
```
可用响应:
```json
{
"code": 0,
"data": {
"available": true,
"targetType": "STOP",
"targetId": 2001,
"lang": "zh-CN",
"narrationTier": "STANDARD",
"title": "青铜神树",
"text": "青铜神树是三星堆遗址出土的重要青铜器...",
"textLength": 1200,
"textHash": "0b32a4..."
}
}
```
不可用响应仍返回 `code=0`
```json
{
"code": 0,
"data": {
"available": false,
"targetType": "STOP",
"targetId": 2001,
"lang": "en-US",
"narrationTier": "STANDARD",
"reason": "NO_TEXT"
}
}
```
字段说明:
| 字段 | 说明 |
| --- | --- |
| `available` | 是否有正文 |
| `text` | 讲解词正文 |
| `textLength` | 正文字数 |
| `textHash` | 正文版本标识,可用于本地缓存判断 |
| `reason` | 不可用原因 |
正文建议按需加载,不需要在页面进入时强制请求。
## 5. 页面推荐流程
```ts
async function enterGuidePage(targetType: "ITEM" | "STOP", targetId: number) {
const lang = getGlobalLanguage() || "zh-CN"
const stopInfo = await api.getGuideStopInfo({ targetType, targetId, lang })
if (!stopInfo.available) {
showStopUnavailable(stopInfo.reason)
return
}
renderStopPage({
title: stopInfo.title,
description: stopInfo.description,
cover: stopInfo.coverImageUrl,
gallery: parseJsonArraySafely(stopInfo.galleryUrls),
imageStatus: stopInfo.imageStatus,
linkedExhibits: stopInfo.linkedExhibits
})
setPlayButtonEnabled(stopInfo.audioStatus === "READY")
bindPlayButton(async () => {
if (stopInfo.audioStatus !== "READY") {
showToast("当前语言暂无可播放音频")
return
}
const playInfo = await api.getAudioPlayInfo({
targetType: stopInfo.playTargetType,
targetId: stopInfo.playTargetId,
lang
})
if (!playInfo.playable || !playInfo.playUrl) {
showToast(reasonToText(playInfo.reason))
return
}
audioContext.src = playInfo.playUrl
audioContext.title = playInfo.title || ""
audioContext.play()
})
bindExpandText(async () => {
if (!stopInfo.hasText) return
const textInfo = await api.getAudioTextInfo({
targetType: stopInfo.playTargetType,
targetId: stopInfo.playTargetId,
lang
})
if (textInfo.available) {
renderNarrationText(textInfo.text)
}
})
}
```
## 6. reason 处理建议
| reason | 建议提示 |
| --- | --- |
| `NO_GUIDE_STOP` | 该展品暂未配置讲解 |
| `NO_GUIDE_CONTENT` | 该讲解点暂无讲解内容 |
| `NO_PUBLISHED_AUDIO` | 当前语言暂无语音讲解 |
| `NO_TEXT` | 当前语言暂无讲解词 |
| `UNSUPPORTED_LANGUAGE` | 暂不支持该语言 |
| `UNSUPPORTED_TARGET_TYPE` | 暂不支持该目标类型 |
| `TARGET_NOT_FOUND` | 内容不存在或已下架 |
小程序可根据页面语气调整文案,但不要把以上业务空态当成系统异常弹窗。
## 7. 小程序本地缓存建议
播放信息:
- key`targetType + targetId + lang`
- 语言切换后清理旧语言缓存。
- 播放失败、403、404 或 URL 过期时,重新请求一次 `play-info`
讲解词正文:
- key`targetType + targetId + lang`
- value`text + textHash`
- 再次请求后如果 `textHash` 变化,替换本地正文。
- 不建议长期永久缓存正文。
## 8. 联调检查清单
- 不传 `lang`:三个接口默认按 `zh-CN` 返回。
- 进入讲解页:先调 `stop-info`,不要用 `play-info` 承担图片、简介、绑定展品展示。
- 图片存在:`imageStatus=READY`,使用讲解点图片渲染。
- 图片缺失:`imageStatus=MISSING`,展示占位或隐藏图片区。
- 当前语言可播放:`audioStatus=READY`,播放按钮可点。
- 只有其他语言音频:`hasAudio=true``audioStatus=MISSING`,当前语言播放按钮不可点。
- 点击播放:`play-info.playable=true` 时设置 `audio.src=playUrl`
- 英文缺音频:返回 `NO_PUBLISHED_AUDIO`,不自动播放中文。
- 展开正文:`text-info.available=true` 时渲染正文。
- 英文缺正文:返回 `NO_TEXT`,不影响音频播放。
- 展品未绑定讲解点:返回 `NO_GUIDE_STOP`
- 播放地址失效:重新请求一次 `play-info`