19 KiB
SGS Map SDK 数据层接入操作手册
创建日期:2026-06-25
适用项目:frontend-miniapp H5 导览业务
目标:通过后端 SGS Map SDK 数据接口接入真实楼层、POI、空间面和导航目的地数据,同时保持展示层只消费领域模型。
1. 接入原则
SGS Map SDK 接入分为两条边界:
| 边界 | 职责 | 允许接触 SDK/后端字段的位置 | 禁止事项 |
|---|---|---|---|
| 数据层 | 拉取楼层、POI、空间面、导航目的地、诊断信息,并转换为 MuseumFloor、MuseumPoi、GuideLocationPreview 等领域模型 |
src/data/providers、src/data/adapters、src/repositories |
页面和组件直接请求 /app-api/gis/sdk/* |
| 渲染层 | 加载 SGS H5 Engine、管理 iframe/SDK 生命周期、执行切楼层和聚焦命令 | src/services/sgs、未来独立 renderer |
把后端数据解析逻辑写进 renderer |
展示层必须只通过以下入口拿数据:
src/usecases/guideUseCase.tssrc/repositories/GuideRepository.ts暴露的GuideRepository合同src/domain/museum.ts中的领域模型
展示层禁止直接依赖:
SGSMapSDK/app-api/gis/sdk/*响应结构floorCode、typeName、position.x/y/z等后端原始字段manifest.poiCount、manifest.spaceCount等不可靠摘要字段
2. 当前服务地址
部署信息:
| 服务 | 地址 | 当前验证结果 |
|---|---|---|
| 后端直连 | http://1.92.206.90:48080/yudao-server/app-api |
当前开发机访问超时,需服务端排查防火墙/安全组/监听地址 |
| 前端代理 | http://1.92.206.90:3001/app-api |
可访问,返回后端 CommonResult code=0 |
| H5 SDK Engine | http://1.92.206.90:3001/engine/index.html |
需部署并按 HELLO -> ENGINE_READY 验证,HTTP 200 不足以证明健康 |
| SDK 脚本 | http://1.92.206.90:3001/sdk/sgs-map-sdk.min.js |
可访问 |
本项目推荐环境变量:
VITE_DATA_SOURCE_MODE=sdk
VITE_API_BASE_URL=/app-api
VITE_SGS_SDK_SCRIPT_URL=/sdk/sgs-map-sdk.min.js
VITE_SGS_H5_ENGINE_URL=/engine/index.html
VITE_SGS_SDK_ORIGIN=http://1.92.206.90:3001
VITE_SGS_SDK_TIMEOUT_MS=10000
本地直连联调时可临时使用:
VITE_API_BASE_URL=http://1.92.206.90:3001/app-api
VITE_SGS_H5_ENGINE_URL=http://1.92.206.90:3001/engine/index.html
VITE_SGS_SDK_SCRIPT_URL=http://1.92.206.90:3001/sdk/sgs-map-sdk.min.js
VITE_SGS_SDK_ORIGIN=http://1.92.206.90:3001
不要在页面或组件里读取这些环境变量。统一通过 src/config/dataSource.ts 暴露配置。
3. 后端接口清单
SDK 数据接口走 App API 通道,控制器为:
smart-navigation-system/yudao-module-gis/src/main/java/cn/iocoder/yudao/module/gis/controller/app/spatial/SdkMapController.java- 请求前缀:
/app-api/gis/sdk
必接接口:
| 用途 | 方法 | 路径 | 数据层使用方式 |
|---|---|---|---|
| 地图 Manifest | GET | /gis/sdk/maps/{mapId}/manifest |
只用于楼层列表、版本、能力声明;不要信任 poiCount/spaceCount |
| 楼层完整包 | GET | /gis/sdk/floors/{floorId}/bundle |
单层详情、模型信息、POI、空间面、路网摘要 |
| 楼层 POI | GET | /gis/sdk/floors/{floorId}/pois |
POI 主数据 |
| 楼层空间面 | GET | /gis/sdk/floors/{floorId}/spaces |
展厅/空间/服务空间 |
| 可导航目的地 | GET | /gis/sdk/floors/{floorId}/navigable-places |
起终点候选和空间门点 |
| 地图诊断 | GET | /gis/sdk/maps/{mapId}/diagnostics |
QA、健康检查、路网就绪判断 |
| 楼层诊断 | GET | /gis/sdk/floors/{floorId}/diagnostics |
单层数据完整性与路网就绪 |
后端原始 App 口径,可用于交叉核验:
| 用途 | 方法 | 路径 |
|---|---|---|
| 楼层主表 | GET | /gis/floor/list |
| 原始 POI | GET | /gis/poi/list-by-floor?floorId={floorId} |
| C 端精简 POI | GET | /gis/sgs-poi/list-by-floor?floorId={floorId} |
当前已知数据风险:
manifest.floors[].poiCount当前全部为0,实际后端 POI 为162。manifest.floors[].spaceCount当前与实际空间面数量不一致。L-1有 POI 和路网,但navigable-places=0。EXTERIOR路网节点和边为0,不能用于路径规划。- 逐层
guide-stops合计为0,地图 diagnostics 中guideStopCount=1,聚合口径不一致。 - SDK POI 当前全部
status=INACTIVE,anchorNodeName、description、iconUrl为空。数据层不要因为INACTIVE自动丢弃全部 POI,除非后端明确状态语义。
4. 推荐落地文件
只新增或修改数据边界文件,展示层不动。
src/config/dataSource.ts
继续作为 mode/api/sdk/url 配置唯一入口
src/data/providers/sgsSdkApiProvider.ts
新增:负责请求 /app-api/gis/sdk/*
src/data/adapters/sgsSdkGuideAdapter.ts
新增:负责 SDK 响应 -> MuseumFloor/MuseumPoi/GuideLocationPreview/GuideRouteReadiness
src/repositories/GuideRepository.ts
增加 ApiGuideRepository 或 SgsSdkGuideRepository
保持 GuideRepository interface 不变
src/repositories/createGuideRepository.ts
可选新增:根据 dataSourceConfig.mode 选择 StaticGuideRepository 或 SgsSdkGuideRepository
src/usecases/guideUseCase.ts
原则上不改业务方法,只替换 repository 注入来源
不要把以下逻辑写进组件:
src/pages/**
src/components/navigation/**
src/components/search/**
src/components/map/SgsMapRenderer.vue
SgsMapRenderer.vue 只负责 SDK iframe/渲染生命周期。它可以接收领域模型里来的 floorId、poiId、positionGltf,但不能自己请求 POI 列表或解析后端字段。
5. Provider 实现要求
sgsSdkApiProvider.ts 只做网络请求和 CommonResult 解包,不做领域转换。
建议接口:
export interface SgsSdkApiProvider {
getManifest(mapId?: string): Promise<SgsSdkManifestPayload>
getMapDiagnostics(mapId?: string): Promise<SgsMapDiagnosticsPayload>
getFloorDiagnostics(floorId: string): Promise<SgsFloorDiagnosticsPayload>
getFloorBundle(floorId: string): Promise<SgsFloorBundlePayload>
getFloorPois(floorId: string): Promise<SgsPoiPayload[]>
getFloorSpaces(floorId: string): Promise<SgsSpacePayload[]>
getNavigablePlaces(floorId: string): Promise<SgsNavigablePlacePayload[]>
}
请求实现规则:
- 使用
dataSourceConfig.apiBaseUrl作为基地址。 - H5 下可继续用
uni.request,不要在组件中使用fetch。 - 统一处理
{ code, data, msg },code !== 0必须抛出带路径的错误。 - 请求超时和网络错误要带 endpoint 信息,方便 QA 判断是后端还是代理问题。
- 可以在 Provider 内做短生命周期缓存,避免每次搜索重复拉全量 POI。
- Provider 返回类型命名使用
Payload后缀,提醒调用者这是后端形状,不是领域模型。
示例骨架:
import { dataSourceConfig } from '@/config/dataSource'
const normalizeBaseUrl = (baseUrl: string) => baseUrl.replace(/\/+$/, '')
const requestJson = <T>(path: string): Promise<T> => new Promise((resolve, reject) => {
const baseUrl = normalizeBaseUrl(dataSourceConfig.apiBaseUrl || '/app-api')
uni.request({
url: `${baseUrl}${path}`,
method: 'GET',
success: (response) => {
const statusCode = Number(response.statusCode || 0)
if (statusCode < 200 || statusCode >= 300) {
reject(new Error(`SGS 后端接口请求失败: ${statusCode} ${path}`))
return
}
const body = typeof response.data === 'string'
? JSON.parse(response.data)
: response.data
if (!body || body.code !== 0) {
reject(new Error(`SGS 后端接口业务失败: ${path} code=${body?.code} msg=${body?.msg || ''}`))
return
}
resolve(body.data as T)
},
fail: (error) => reject(new Error(`SGS 后端接口网络失败: ${path} ${JSON.stringify(error)}`))
})
})
6. Adapter 映射规则
sgsSdkGuideAdapter.ts 是阻断展示层污染的关键。所有后端字段都在这里转换。
6.1 楼层映射
后端字段:
{
floorId: '2065808921272119298',
floorCode: 'L1',
floorName: '1.0层',
sortOrder: 10
}
领域模型:
MuseumFloor {
id: floorId,
label: readableFloorLabel(floorCode, floorName),
order: sortOrder
}
建议显示标签:
| 后端 floorCode | 领域 label |
|---|---|
EXTERIOR |
馆外 |
L-2 |
B2 |
L-1 |
B1 |
L1 |
1F |
L1.5 |
1.5F |
L2 |
2F |
normalizeFloorId(labelOrId) 必须同时支持:
- 后端长 ID,例如
2065808921272119298 - 后端
floorCode,例如L1 - 前端显示标签,例如
1F
6.2 POI 映射
后端字段:
{
id: '5576',
name: '服务台',
type: 'service_desk',
typeName: '服务台',
floorId: '2065808921272119298',
floorCode: 'L1',
position: { x: -59.617386, y: 0.658652, z: 16.816816 },
status: 'INACTIVE',
anchorNodeName: null,
description: null,
iconUrl: null
}
领域模型:
MuseumPoi {
id: String(id),
name,
floorId: String(floorId),
floorLabel,
primaryCategory: mapSgsPoiCategory(type, typeName),
categories: [primaryCategory],
positionGltf: [position.x, position.y, position.z],
sourceConfidence: 'backend-sgs-sdk',
navigationReadiness,
accessible
}
坐标规则:
- 后端 SDK 文档声明坐标系为
GLB_METER。 position.x/y/z进入领域层统一保存为positionGltf: [x, y, z]。- 不要在展示层重排坐标轴。
- 如果某个接口只返回
x/y/z平铺字段,也在 Adapter 内统一转为positionGltf。
类型映射建议:
| SGS type | 领域分类 id | label | accessible |
|---|---|---|---|
toilet |
basic_service_facility |
卫生间 |
false |
accessible_toilet |
accessibility_special_service |
无障碍卫生间 |
true |
elevator |
transport_circulation |
电梯 |
true |
stairs |
transport_circulation |
楼梯 |
false |
escalator |
transport_circulation |
扶梯 |
false |
entrance_exit |
transport_circulation |
出入口 |
false |
service_desk |
basic_service_facility |
服务台 |
false |
mother_baby_room |
basic_service_facility |
母婴室 |
true |
locker |
basic_service_facility |
存包处 |
false |
rental_service |
basic_service_facility |
租赁服务 |
true |
ticket_office |
basic_service_facility |
售票处 |
false |
不要让组件判断 type === 'toilet'。组件只看领域层的 primaryCategory、accessible、floorLabel。
6.3 导航目的地映射
navigable-places 不等同于 POI,它可能是空间门点。
建议在数据层使用独立内部类型,例如:
interface SgsNavigablePlaceDomain {
id: string
name: string
floorId: string
floorLabel: string
category: 'poi' | 'door' | 'space' | 'guide'
positionGltf: [number, number, number]
ownerName?: string
}
当前 GuideRepository 合同没有暴露导航目的地列表。如果近期只做位置预览,可暂时只把 POI 映射到 MuseumPoi。如果要启用真实路线选择,再扩展 GuideRouteRepository 或新增 route use case,避免把导航目的地塞进 POI 列表污染搜索。
6.4 路线就绪映射
GuideRepository.getRouteReadiness() 应基于 diagnostics,而不是静态常量。
建议规则:
mapDiagnostics.status === 'OK'
且所有馆内可导航楼层 routePlanningReady=true
且 routeNodeCount > 0
且 routeEdgeCount > 0
且关键楼层 navigablePlaceCount > 0
=> ready=true
否则 ready=false,并把 warnings / requiredData 写入 GuideRouteReadiness
当前线上数据必须返回 ready=false 或“实验性路线预览”,因为:
EXTERIOR路径规划未就绪。L-1无可导航目的地。- guideStop 聚合口径不一致。
7. Repository 接入方式
保持现有 GuideRepository interface 不变:
export interface GuideRepository {
getAssetBaseUrl(): string
getFloors(): Promise<MuseumFloor[]>
normalizeFloorId(labelOrId: string): string
listPois(): Promise<MuseumPoi[]>
getPoiById(id: string): Promise<MuseumPoi | null>
searchPois(keyword?: string): Promise<MuseumPoi[]>
getLocationPreview(poiId: string): Promise<GuideLocationPreview | null>
getRouteReadiness(): Promise<GuideRouteReadiness>
}
新增 SgsSdkGuideRepository:
export class SgsSdkGuideRepository implements GuideRepository {
private floorsCache: MuseumFloor[] | null = null
private poiCache: MuseumPoi[] | null = null
constructor(private readonly provider: SgsSdkApiProvider = defaultSgsSdkApiProvider) {}
getAssetBaseUrl() {
return ''
}
async getFloors() {
if (this.floorsCache) return this.floorsCache
const manifest = await this.provider.getManifest('1')
this.floorsCache = manifest.floors
.map(toMuseumFloor)
.sort((a, b) => a.order - b.order)
return this.floorsCache
}
async listPois() {
if (this.poiCache) return this.poiCache
const manifest = await this.provider.getManifest('1')
const poisByFloor = await Promise.all(
manifest.floors.map((floor) => this.provider.getFloorPois(String(floor.floorId)))
)
this.poiCache = poisByFloor.flat().map((poi) => toMuseumPoiFromSgs(poi, manifest.floors))
return this.poiCache
}
}
Repository 规则:
listPois()可以跨楼层聚合,但只返回领域模型。getPoiById()从listPois()缓存查找,避免重复请求。searchPois()使用MuseumPoi字段构建搜索文本,不能搜索后端 raw JSON。getLocationPreview()只返回GuideLocationPreview。getRouteReadiness()只返回GuideRouteReadiness,不要把 diagnostics 原样传给 UI。
8. 模式切换
当前 src/config/dataSource.ts 已有:
export type DataSourceMode = 'static' | 'api' | 'sdk'
export const isSgsSdkMode = () => dataSourceConfig.mode === 'sdk'
推荐新增仓库工厂:
import { dataSourceConfig } from '@/config/dataSource'
import { StaticGuideRepository } from '@/repositories/GuideRepository'
import { SgsSdkGuideRepository } from '@/repositories/SgsSdkGuideRepository'
export const createGuideRepository = () => {
if (dataSourceConfig.mode === 'sdk' || dataSourceConfig.mode === 'api') {
return new SgsSdkGuideRepository()
}
return new StaticGuideRepository()
}
然后 guideUseCase.ts 只依赖工厂返回的 GuideRepository,页面不用知道当前是 static/api/sdk。
推荐模式语义:
| mode | 数据来源 | 渲染器 |
|---|---|---|
static |
本地 clean nav-assets | ThreeMap |
api |
后端 SGS App API | 仍可用 ThreeMap 或本地渲染 |
sdk |
后端 SGS App API | 当前仍为 ThreeMap;SDK iframe renderer 尚未接入 |
不要把 sdk 理解成“页面直接调用 SGSMapSDK.getFloorPois()”。数据仍从 Repository 进来,SDK 只做地图渲染和交互命令。
9. 展示层改造边界
允许展示层做的事:
- 调用
guideUseCase.getFloors() - 调用
guideUseCase.searchPois(keyword) - 调用
guideUseCase.getPoiById(id) - 传
GuideLocationPreview.positionGltf给地图聚焦 - 当前
GuideMapShell固定使用ThreeMap;启用 SDK iframe renderer 需要单独功能任务
禁止展示层做的事:
// 禁止:页面直接请求后端
uni.request({ url: '/app-api/gis/sdk/floors/xxx/pois' })
// 禁止:组件直接判断后端字段
if (poi.type === 'accessible_toilet') { ... }
// 禁止:组件直接拼后端楼层 ID 显示
text = `${raw.floorCode}-${raw.floorName}`
// 禁止:SgsMapRenderer 拉取 POI 列表
await service.getFloorPois(floorId)
如果组件需要新增展示信息,先判断是否属于领域模型:
- 属于稳定导览语义:加到
src/domain/museum.ts。 - 属于后端传输字段:留在 Provider/Adapter,禁止穿透。
- 属于 SDK 渲染命令结果:放到
src/services/sgs/SgsMapEventAdapter.ts转成 UI 事件。
10. 推荐实施步骤
- 新增
src/data/providers/sgsSdkApiProvider.ts。 - 新增
src/data/adapters/sgsSdkGuideAdapter.ts。 - 新增
SgsSdkGuideRepository,实现现有GuideRepositoryinterface。 - 新增仓库工厂,根据
dataSourceConfig.mode选择 static 或 SGS 后端数据。 - 让
guideUseCase使用仓库工厂,不改页面调用方式。 - 保持当前
GuideMapShell使用ThreeMap;若产品启用 iframe renderer,单独实施并保持 POI/楼层数据来自GuideUseCase。 - 增加数据健康检查脚本或开发命令,校验楼层、POI、空间面、导航目的地计数。
- 通过 H5 浏览器检查搜索、楼层切换、POI 聚焦、位置预览。
11. 验收检查清单
数据接口检查:
$base = "http://1.92.206.90:3001/app-api"
Invoke-RestMethod "$base/gis/floor/list"
Invoke-RestMethod "$base/gis/sdk/maps/1/manifest"
Invoke-RestMethod "$base/gis/sdk/maps/1/diagnostics"
Invoke-RestMethod "$base/gis/sdk/floors/2065808921272119298/pois"
Invoke-RestMethod "$base/gis/sdk/floors/2065808921272119298/navigable-places"
必须满足:
/gis/floor/list与 SDK manifest 楼层 ID 一致。/gis/poi/list-by-floor与/gis/sdk/floors/{id}/poisPOI ID 一致。- POI 无重复 ID。
- POI 无缺失
id/name/type/floorId/position。 - POI
floorId与请求楼层一致。 - 空间面无缺失
id/name/type/floorId/boundaryWkt。 GuideRepository.listPois()只返回MuseumPoi[]。- 页面源码中不能出现
/app-api/gis/sdk。 - 页面源码中不能出现
SGSMapSDK。
代码扫描:
rg -n "/app-api/gis/sdk|SGSMapSDK|getFloorPois|getManifest|getNavigablePlaces" src/pages src/components
预期:
src/components/map/SgsMapRenderer.vue可以出现 SDK 渲染相关服务调用。- 其它页面和组件不应直接出现后端 SDK 数据接口或 SDK 全局对象。
构建验证:
pnpm type-check
pnpm lint
pnpm build:h5
浏览器验证:
static模式仍可加载本地 3D/POI。api模式能展示后端楼层和 POI,渲染器不变。- 当前
sdk模式仍以ThreeMap渲染,验证后端数据与本地三维模型正常显示。 - SDK Engine 仅在未来 iframe renderer 接入时验证:
/engine/index.html不是 SPA fallback、资源可加载、并完成HELLO->ENGINE_READY。 - 顶部 tabs、搜索、楼层控件、详情卡片不被 canvas 遮挡;未来 iframe renderer 也必须满足该约束。
12. 上线前阻断项
以下问题未解决前,不建议对用户宣称“正式馆内导航”:
L-1无可导航目的地。EXTERIOR路网未就绪。manifest计数与实际明细不一致。guideStopCount聚合口径不一致。- POI 全部
status=INACTIVE的业务语义未确认。
用户文案应保持为:
位置预览查看三维位置路线预览导航数据准备中
不要使用:
开始馆内导航实时导航到达引导精准路径规划
13. 维护约定
- 后端接口字段变化先改 Provider Payload 类型。
- 领域含义变化再改 Adapter。
- UI 需要新信息时先扩展领域模型,再通过 Repository 暴露。
- 不要为了一个页面临时把 raw SDK 字段传透到组件。
- 每次更新 SDK 后,重新跑楼层/POI/空间面/导航目的地完整性检查。
- 每次启用真实路线能力前,重新审核
GuideRouteReadiness,保持不夸大导航能力。