21 KiB
/guide/exhibits 与 miniapp 讲解接口数据源偏差诊断
诊断日期:2026-07-02
范围:只检查“讲解”业务线数据来源,不展开 SDK 地图渲染、楼层、POI、空间面、路网等地图业务。
本次结论基于两类证据:
- 本地源码:
E:\MyWork\深圳国际艺术馆\智慧导览\smart-navigation-system - 本项目源码:
E:\MyWork\深圳国际艺术馆\museum-guide\museum-guide-v4.0\frontend-miniapp - 真实 HTTP 接口:
http://1.92.206.90:3001
1. 核心结论
http://1.92.206.90:3001/guide/exhibits 管理端页面展示的数据,与 frontend-miniapp 当前讲解业务接口返回的数据不一致,主要不是因为数据库完全不同,而是因为:
- 页面和 miniapp 调用的接口不同。
- 接口读取的表层级不同。
- 同一张表上的过滤条件不同。
- 统计字段的计算口径不同。
管理端 /guide/exhibits 使用的是完整讲解业务数据链:
SGS_EXHIBITION_HALL
-> SGS_EXHIBIT_OUTLINE
-> SGS_EXHIBIT_ITEM / SGS_GUIDE_STOP
-> SGS_GUIDE_CONTENT
-> sgs_guide_audio_channel
miniapp 当前讲解页虽然已经接入部分 App 端讲解接口,但“业务单元 / 讲解点列表”实际走的是 SDK 地图点位接口:
GET /app-api/gis/sdk/halls/{hallId}/guide-stops
该接口只返回 SGS_GUIDE_STOP 中已启用且已标定 mapX/mapY 的地图点位子集,不等价于管理端 /guide/exhibits 所展示的完整讲解业务数据。
因此,当前偏差的根因可以概括为:
管理端读完整讲解业务结构;
miniapp 列表读 SDK 地图标定子集;
App 展厅统计又直接读 hall 表静态字段;
所以展厅统计、业务单元、讲解点数量、音频状态都会出现偏差。
2. 管理端 /guide/exhibits 页面实际数据来源
2.1 前端项目与页面源码
页面所属项目:
E:\MyWork\深圳国际艺术馆\智慧导览\smart-navigation-system\sgs-frontend-map
页面入口:
sgs-frontend-map/src/app/(main)/guide/exhibits/page.tsx
相关前端文件:
| 用途 | 文件 |
|---|---|
| 页面组件 | sgs-frontend-map/src/app/(main)/guide/exhibits/page.tsx |
| 展厅 / 业务单元 / 展品 API client | sgs-frontend-map/src/api/map/exhibit.ts |
| 讲解点 API client | sgs-frontend-map/src/api/map/space.ts |
| 左侧树组件 | sgs-frontend-map/src/components/v2/exhibits/V2ExhibitTreeBrowser.tsx |
| 讲解模式 / 讲解点面板 | sgs-frontend-map/src/components/v2/exhibits/GuideStopsPanel.tsx |
| Axios baseURL | sgs-frontend-map/src/lib/api.ts,默认 /admin-api |
2.2 页面实际调用接口
| 页面区域 | 前端方法 | 实际接口 |
|---|---|---|
| 展厅树 | ExhibitApi.getTree() |
GET /admin-api/guide/exhibits/tree |
| 业务单元子节点 | ExhibitApi.getChildren(nodeId) |
GET /admin-api/guide/exhibits/tree/{nodeId}/children |
| 树搜索 | ExhibitApi.searchTree() |
GET /admin-api/guide/exhibits/tree/search |
| 展品列表 | ExhibitApi.getPage(params) |
GET /admin-api/guide/exhibits/list |
| 讲解模式 / 讲解点 | StopApi.list(outlineId) |
GET /admin-api/gis/guide-stop/list?outlineId=... |
| 跨业务单元搜索讲解点 | StopApi.listByHallId(hallId) |
GET /admin-api/gis/guide-stop/list-by-hall?hallId=... |
2.3 管理端后端链路
| 接口 | Controller | Service / Mapper | 主要表 |
|---|---|---|---|
/guide/exhibits/tree |
V2ExhibitTreeController |
ExhibitHallMapper、SgsExhibitOutlineMapper、ExhibitItemMapper |
SGS_EXHIBITION_HALL、SGS_EXHIBIT_OUTLINE、SGS_EXHIBIT_ITEM、SGS_GUIDE_CONTENT |
/guide/exhibits/tree/{nodeId}/children |
V2ExhibitTreeController |
SgsExhibitOutlineMapper、ExhibitItemMapper |
SGS_EXHIBIT_OUTLINE、SGS_EXHIBIT_ITEM、SGS_GUIDE_CONTENT |
/guide/exhibits/list |
V2ExhibitCrudController |
ExhibitServiceImpl、ExhibitItemMapper、GuideContentMapper |
SGS_EXHIBIT_ITEM、SGS_GUIDE_CONTENT |
/gis/guide-stop/list |
GuideStopController |
SgsGuideStopServiceImpl、SgsGuideStopMapper |
SGS_GUIDE_STOP |
/gis/guide-stop/list-by-hall |
GuideStopController |
SgsGuideStopServiceImpl、SgsExhibitOutlineMapper、SgsGuideStopMapper |
SGS_EXHIBIT_OUTLINE、SGS_GUIDE_STOP |
管理端源码里已经明确说明:
展厅 — SGS_EXHIBITION_HALL
单元 — SGS_EXHIBIT_OUTLINE
展品 — SGS_EXHIBIT_ITEM
对应源码:
yudao-module-gis/src/main/java/cn/iocoder/yudao/module/gis/controller/admin/guide/V2ExhibitTreeController.java
2.4 管理端统计口径
管理端 /guide/exhibits/tree 不直接使用 SGS_EXHIBITION_HALL.exhibitCount 作为展示统计。
它会:
- 读取启用展厅:
SGS_EXHIBITION_HALL.status = 1 - 找到展厅下所有
SGS_EXHIBIT_OUTLINE后代节点。 - 按 outline 聚合
SGS_EXHIBIT_ITEM数量。 - 通过
SGS_GUIDE_CONTENT判断哪些展品有讲解内容。 - 汇总到展厅 / 业务单元节点展示。
因此管理端页面上的展厅数量、业务单元数量、讲解内容数量是“动态聚合结果”,不是 hall 表上一个静态字段。
3. miniapp 当前讲解页实际数据来源
3.1 当前配置
本项目:
E:\MyWork\深圳国际艺术馆\museum-guide\museum-guide-v4.0\frontend-miniapp
配置文件:
.env
src/config/dataSource.ts
当前配置:
VITE_DATA_SOURCE_MODE=sdk
VITE_GUIDE_CONTENT_SOURCE_MODE=remote
VITE_API_BASE_URL=/app-api
VITE_SGS_API_BASE_URL=/app-api
含义:
- 地图/导览运行模式是
sdk。 - 讲解内容数据源是
remote。 - App API 基础路径是
/app-api。 - 但
sdk模式不应被理解为讲解业务列表的数据源;SDK 只应服务地图渲染/点位层。
3.2 miniapp 前端方法与接口
| miniapp 展示数据 | 前端方法链路 | 实际接口 |
|---|---|---|
| 展厅列表 | ExplainUseCase.loadExplainHalls() -> ExplainRepository.listHalls() -> BackendExplainContentProvider.requestHallList() |
GET /app-api/gis/hall/list |
| 业务单元统计 | loadExplainHallSummaries() -> loadTemporaryBusinessUnitsByHall() -> groupGuideStopsByOutline() |
基于 SDK guide-stops 分组 |
| 讲解点列表 | BackendExplainContentProvider.listGuideStopsByHall() -> sgsSdkApiProvider.getGuideStopsByHall() |
GET /app-api/gis/sdk/halls/{hallId}/guide-stops |
| 讲解点详情 | AudioPlayInfoRepository.getStopInfo() |
GET /app-api/gis/guide/stop/info |
| 音频播放信息 | AudioPlayInfoRepository.getPlayInfo() |
GET /app-api/gis/guide/audio/play-info |
| 正文 | AudioPlayInfoRepository.getTextInfo() |
GET /app-api/gis/guide/audio/text-info |
关键问题:
miniapp 的“业务单元 / 讲解点列表”不是从讲解业务树接口读取,
而是从 SDK 地图点位接口读取。
4. App API 后端读取表与管理端差异
4.1 /app-api/gis/hall/list
Controller:
yudao-module-gis/src/main/java/cn/iocoder/yudao/module/gis/controller/app/guide/AppExhibitController.java
接口:
GET /app-api/gis/hall/list
读取:
SGS_EXHIBITION_HALL
过滤:
status = 1
返回 exhibitCount 的方式:
vo.setExhibitCount(h.getExhibitCount())
也就是说,App 端 hall/list 的 exhibitCount 直接来自 SGS_EXHIBITION_HALL.exhibitCount 字段。
这与管理端 /guide/exhibits/tree 动态聚合 SGS_EXHIBIT_OUTLINE + SGS_EXHIBIT_ITEM + SGS_GUIDE_CONTENT 的口径不同。
结论:
即使两边都读取 SGS_EXHIBITION_HALL,
展厅统计也可能不同。
管理端显示的是动态聚合数量;
App API 返回的是 hall 表静态 exhibitCount 字段。
4.2 /app-api/gis/zone/list-by-hall
Controller:
yudao-module-gis/src/main/java/cn/iocoder/yudao/module/gis/controller/app/guide/AppExhibitController.java
接口:
GET /app-api/gis/zone/list-by-hall?hallId=...
源码注释:
展区数据应该从 SGS_EXHIBIT_OUTLINE 读取,这里返回空列表
TODO: 如果需要展区功能,应该查询 SGS_EXHIBIT_OUTLINE 表
结论:
App 端目前没有真正提供与管理端业务单元一致的接口。
管理端业务单元来自 SGS_EXHIBIT_OUTLINE;
App 端 zone/list-by-hall 当前返回空数组。
4.3 /app-api/gis/sdk/halls/{hallId}/guide-stops
Controller:
yudao-module-gis/src/main/java/cn/iocoder/yudao/module/gis/controller/app/spatial/SdkMapController.java
Service:
yudao-module-gis/src/main/java/cn/iocoder/yudao/module/gis/service/sdk/SdkMapServiceImpl.java
接口:
GET /app-api/gis/sdk/halls/{hallId}/guide-stops
读取主表:
SGS_GUIDE_STOP
过滤条件:
status in GuideStopAvailability.AVAILABLE_STATUSES
mapX is not null
mapY is not null
其中 GuideStopAvailability.AVAILABLE_STATUSES = {0, 1},用于兼容历史数据中 0=启用、1=启用 两套状态。
源码注释明确说明:
SDK 只输出可落到地图上的讲解点;未标定的属于讲解内容,不进入点位层。
结论:
该接口是 SDK 地图点位接口,不是完整讲解业务列表接口。
它只返回已启用且已地图标定 mapX/mapY 的讲解点。
未标定但有讲解内容、有音频、有正文的讲解点,会被该接口过滤掉。
4.4 /app-api/gis/guide/stop/info
Controller:
yudao-module-gis/src/main/java/cn/iocoder/yudao/module/gis/controller/app/guide/AppGuideStopController.java
Service:
yudao-module-gis/src/main/java/cn/iocoder/yudao/module/gis/service/guide/AppGuideStopServiceImpl.java
接口:
GET /app-api/gis/guide/stop/info?targetType=STOP&targetId=...&lang=zh-CN
读取:
SGS_GUIDE_STOP
SGS_GUIDE_CONTENT
音频摘要复用:
GuideAudioPlayService
注意:单点详情不需要叠加 mapX/mapY 过滤。GuideStopAvailability.java 源码注释明确说明:
SDK 列表查询需要叠加 mapX/mapY 过滤,但单点 ID 查询(stop-info / play-info / text-info)不需要叠加。
结论:
同一个 stopId 可能不出现在 SDK 地图列表中,
但仍然可以通过 stop-info / play-info / text-info 查到讲解详情、音频、正文。
4.5 /app-api/gis/guide/audio/play-info 与 text-info
Controller:
yudao-module-gis/src/main/java/cn/iocoder/yudao/module/gis/controller/app/guide/AppGuideAudioController.java
Service:
yudao-module-gis/src/main/java/cn/iocoder/yudao/module/gis/service/guide/GuideAudioPlayServiceImpl.java
读取:
SGS_GUIDE_STOP
SGS_GUIDE_CONTENT
sgs_guide_audio_channel
主要逻辑:
play-info判断是否有可播放音频。text-info判断是否有同语言正文。- 播放状态以
SGS_GUIDE_CONTENT和sgs_guide_audio_channel的发布状态为准。 - 不以 SDK guide-stops 返回的
hasAudio为准。
结论:
SDK guide-stops 的 hasAudio 不等价于讲解播放服务的 hasAudio / playable。
SDK 列表的 hasAudio 主要看点位自身字段;
play-info 的 playable 看讲解内容和正式音频通道。
5. 真实接口测试结果
本轮复测基础地址:
http://1.92.206.90:3001
5.1 展厅列表
请求:
GET http://1.92.206.90:3001/app-api/gis/hall/list
结果:
HTTP 200
code = 0
msg = ""
data.length = 8
前 5 条:
| id | name | exhibitCount |
|---|---|---|
715792102100832258 |
宇宙厅 | 0 |
715792102100832257 |
地球厅 | 0 |
715792102100832259 |
演化厅 | 0 |
715792102100832260 |
恐龙厅 | 0 |
715792102100832256 |
人类厅 | 0 |
说明:
这里的 exhibitCount 来自 SGS_EXHIBITION_HALL.exhibitCount,
不能直接拿来对比管理端 /guide/exhibits 页面动态聚合出的展品/讲解数量。
5.2 宇宙厅 SDK guide-stops
取 hallId:
715792102100832258
请求:
GET http://1.92.206.90:3001/app-api/gis/sdk/halls/715792102100832258/guide-stops
结果:
HTTP 200
code = 0
msg = ""
data.length = 2
返回样例:
| id | name | targetType | targetId | outlineId | outlineName | hasAudio |
|---|---|---|---|---|---|---|
1823450596808612 |
古典星盘 讲解 | GUIDE_STOP |
1823450596808612 |
7467940240901013505 |
第一单元:仰望苍穹——人类对宇宙认识的历程 | false |
1823450596814245 |
火星提森特陨石 讲解 | GUIDE_STOP |
1823450596814245 |
7467940240901013507 |
第三单元:采石知天——行星科学与深空探测 | false |
说明:
这里的 2 条不是宇宙厅完整讲解点数量,
而是宇宙厅下已启用且已标定 mapX/mapY 的 SDK 地图点位数量。
5.3 stop-info
取 guideStop.id:
1823450596808612
请求:
GET http://1.92.206.90:3001/app-api/gis/guide/stop/info?targetType=STOP&targetId=1823450596808612&lang=zh-CN
结果:
HTTP 200
code = 0
msg = ""
关键字段:
| 字段 | 值 |
|---|---|
available |
true |
targetType |
STOP |
targetId |
1823450596808612 |
resolvedStopId |
1823450596808612 |
title |
古典星盘 讲解 |
playTargetType |
STOP |
playTargetId |
1823450596808612 |
hasAudio |
true |
hasText |
true |
audioStatus |
READY |
reason |
null |
5.4 play-info
请求:
GET http://1.92.206.90:3001/app-api/gis/guide/audio/play-info?targetType=STOP&targetId=1823450596808612&lang=zh-CN
结果:
HTTP 200
code = 0
msg = ""
关键字段:
| 字段 | 值 |
|---|---|
playable |
true |
playUrl |
存在 |
duration |
57 |
audioId |
1253 |
title |
[古典星盘] 标准解说 |
reason |
null |
5.5 text-info
请求:
GET http://1.92.206.90:3001/app-api/gis/guide/audio/text-info?targetType=STOP&targetId=1823450596808612&lang=zh-CN
结果:
HTTP 200
code = 0
msg = ""
关键字段:
| 字段 | 值 |
|---|---|
available |
true |
textLength |
269 |
title |
[古典星盘] 标准解说 |
reason |
null |
正文摘要:
这九个16至19世纪的金属星盘,是前卫星时代的“科学计算器”,集导航、计时、天文功能于一体……
6. 为什么 /guide/exhibits 与 miniapp 接口返回数据不一致
6.1 展厅列表看起来同源,但统计字段不同源
两边都可能读取 SGS_EXHIBITION_HALL,但:
| 端 | 展厅基础表 | 数量/统计口径 |
|---|---|---|
管理端 /guide/exhibits |
SGS_EXHIBITION_HALL |
通过 SGS_EXHIBIT_OUTLINE 后代聚合 SGS_EXHIBIT_ITEM、SGS_GUIDE_CONTENT |
miniapp /app-api/gis/hall/list |
SGS_EXHIBITION_HALL |
直接返回 SGS_EXHIBITION_HALL.exhibitCount |
所以线上 App API 返回的 exhibitCount=0,不能证明管理端页面也应该显示 0。
偏差原因:
管理端动态算;
App API 直接读静态字段。
6.2 业务单元不是同一个接口来源
| 端 | 接口 | 表 |
|---|---|---|
| 管理端 | /admin-api/guide/exhibits/tree/{hallId}/children |
SGS_EXHIBIT_OUTLINE |
| miniapp 当前 | /app-api/gis/zone/list-by-hall |
源码 TODO,当前返回空数组 |
| miniapp 实际展示分组 | 基于 /app-api/gis/sdk/halls/{hallId}/guide-stops 的 outlineId/outlineName 分组 |
SDK 点位子集 |
偏差原因:
管理端业务单元来自完整 SGS_EXHIBIT_OUTLINE;
miniapp 没有使用等价 App 端 outline 接口,
而是从 SDK guide-stops 返回的少量点位里反推业务单元。
这会导致:
- 没有已标定讲解点的业务单元不显示。
- 有业务单元但未落图的讲解点不显示。
- 业务单元数量比管理端少。
6.3 讲解点列表不是同一个业务口径
| 端 | 接口 | 表 | 过滤 |
|---|---|---|---|
| 管理端讲解模式面板 | /admin-api/gis/guide-stop/list?outlineId=... |
SGS_GUIDE_STOP |
按 outlineId |
| miniapp 当前列表 | /app-api/gis/sdk/halls/{hallId}/guide-stops |
SGS_GUIDE_STOP |
status in {0,1} 且 mapX/mapY 非空 |
偏差原因:
管理端展示讲解业务点;
miniapp 当前展示 SDK 地图可落点。
这两个集合的关系是:
SDK guide-stops ⊂ 管理端讲解业务 guide-stops
即 SDK guide-stops 通常是管理端讲解点中的一个子集。
6.4 音频状态字段不是同一个口径
真实接口已证明同一个 stopId=1823450596808612:
| 接口 | 字段 | 值 |
|---|---|---|
/app-api/gis/sdk/halls/{hallId}/guide-stops |
hasAudio |
false |
/app-api/gis/guide/stop/info |
hasAudio |
true |
/app-api/gis/guide/audio/play-info |
playable |
true |
/app-api/gis/guide/audio/text-info |
available |
true |
偏差原因:
SDK guide-stops 的 hasAudio 主要看 SGS_GUIDE_STOP.audioUrl;
讲解播放接口的 playable 看 SGS_GUIDE_CONTENT 与 sgs_guide_audio_channel。
所以 SDK 列表中的 hasAudio=false 不应作为讲解业务播放状态。
7. 与接口契约的关系
docs/miniapp_integration.md 中的 App 端讲解接口契约约定:
stop-info用于进入讲解页时获取标题、图片、绑定展品、当前语言音频/正文状态。play-info用于点击播放时获取唯一可播放playUrl。text-info用于展开正文时按需获取讲解词全文。available=false/playable=false是业务不可用,不一定代表 HTTP 失败。- 不可用响应仍可返回
code=0,客户端应看data.available、data.playable和reason。
这套契约解决的是“单个 ITEM / STOP 的展示、播放、正文”问题。
它没有解决:
按展厅获取完整业务单元树;
按业务单元获取完整讲解点列表;
按管理端相同口径统计展品数/讲解数。
因此当前 miniapp 需要补充“讲解业务列表 / 树”类 App API,而不是继续用 SDK 地图点位接口替代。
8. 推荐修复方向
8.1 后端补 App 端讲解业务接口
建议新增或实现以下接口之一:
GET /app-api/gis/guide/halls/{hallId}/outlines
GET /app-api/gis/guide/outlines/{outlineId}/stops
或组合接口:
GET /app-api/gis/guide/halls/{hallId}/explain-tree
接口读取口径应对齐管理端:
SGS_EXHIBITION_HALL
SGS_EXHIBIT_OUTLINE
SGS_GUIDE_STOP
SGS_EXHIBIT_ITEM
SGS_GUIDE_CONTENT
sgs_guide_audio_channel
要求:
- 不按
mapX/mapY过滤讲解点。 - 业务单元从
SGS_EXHIBIT_OUTLINE获取。 - 讲解点从
SGS_GUIDE_STOP获取。 - 展品绑定关系从
SGS_EXHIBIT_ITEM.stopId获取。 - 音频状态复用
GuideAudioPlayService的摘要口径。 - 展厅/业务单元统计口径对齐管理端
/guide/exhibits/tree。
8.2 前端替换列表数据源
miniapp 前端应调整:
BackendExplainContentProvider.listGuideStopsByHall()
BackendExplainContentProvider.listTemporaryBusinessUnitsByHall()
不要再从:
/app-api/gis/sdk/halls/{hallId}/guide-stops
生成完整讲解业务单元和讲解点列表。
SDK guide-stops 可以保留为“查看位置 / 地图落点预览”的辅助数据,但不能作为讲解业务完整列表数据源。
8.3 展厅统计修复
/app-api/gis/hall/list 的 exhibitCount 当前直接来自 SGS_EXHIBITION_HALL.exhibitCount。
如果 miniapp 需要展示与管理端一致的统计,应选择其一:
- 后端在
hall/list中动态聚合管理端同口径统计。 - 新增
guide/halls/{hallId}/explain-tree时返回统计。 - 定期维护
SGS_EXHIBITION_HALL.exhibitCount,并明确它与管理端动态统计一致。
推荐优先采用第 2 种:在讲解业务树接口中返回统计,避免让通用 hall/list 承担复杂业务聚合。
9. 最终判断
/guide/exhibits 页面展示数据和 frontend-miniapp 当前接口返回数据不一致,根因不是简单的“接口坏了”,而是:
管理端 /guide/exhibits 读取完整讲解业务表,并动态聚合统计;
miniapp 当前展厅列表读取 hall 表静态 exhibitCount;
miniapp 当前业务单元和讲解点列表读取 SDK 地图标定点位子集;
miniapp 单点详情/播放/正文又读取讲解业务内容表和音频通道表。
因此当前 miniapp 同时混用了:
- 讲解业务数据源:
SGS_EXHIBITION_HALL、SGS_GUIDE_STOP、SGS_GUIDE_CONTENT、sgs_guide_audio_channel - SDK 地图数据源/点位口径:
SGS_GUIDE_STOP中已标定mapX/mapY的子集 - 静态统计字段:
SGS_EXHIBITION_HALL.exhibitCount
正确方向是把“讲解业务列表 / 业务单元 / 讲解点数量 / 讲解模式”统一切回讲解业务接口,SDK 接口只作为地图落点和位置预览的辅助数据。