# 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 地图基座、管理 iframe/SDK 生命周期、执行切楼层和聚焦命令 | `src/services/sgs`、`src/components/map/SgsMapRenderer.vue` | 把后端数据解析逻辑写进 `SgsMapRenderer.vue` | 展示层必须只通过以下入口拿数据: - `src/usecases/guideUseCase.ts` - `src/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 基座 | `http://1.92.206.90:3001/h5-sdk` | 可访问 | | SDK 脚本 | `http://1.92.206.90:3001/sdk/sgs-map-sdk.min.js` | 可访问 | 本项目推荐环境变量: ```env 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=/h5-sdk VITE_SGS_SDK_ORIGIN=http://1.92.206.90:3001 VITE_SGS_SDK_TIMEOUT_MS=10000 ``` 本地直连联调时可临时使用: ```env VITE_API_BASE_URL=http://1.92.206.90:3001/app-api VITE_SGS_H5_ENGINE_URL=http://1.92.206.90:3001/h5-sdk 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. 推荐落地文件 只新增或修改数据边界文件,展示层不动。 ```text 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 注入来源 ``` 不要把以下逻辑写进组件: ```text 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 解包,不做领域转换。 建议接口: ```ts export interface SgsSdkApiProvider { getManifest(mapId?: string): Promise getMapDiagnostics(mapId?: string): Promise getFloorDiagnostics(floorId: string): Promise getFloorBundle(floorId: string): Promise getFloorPois(floorId: string): Promise getFloorSpaces(floorId: string): Promise getNavigablePlaces(floorId: string): Promise } ``` 请求实现规则: - 使用 `dataSourceConfig.apiBaseUrl` 作为基地址。 - H5 下可继续用 `uni.request`,不要在组件中使用 `fetch`。 - 统一处理 `{ code, data, msg }`,`code !== 0` 必须抛出带路径的错误。 - 请求超时和网络错误要带 endpoint 信息,方便 QA 判断是后端还是代理问题。 - 可以在 Provider 内做短生命周期缓存,避免每次搜索重复拉全量 POI。 - Provider 返回类型命名使用 `Payload` 后缀,提醒调用者这是后端形状,不是领域模型。 示例骨架: ```ts import { dataSourceConfig } from '@/config/dataSource' const normalizeBaseUrl = (baseUrl: string) => baseUrl.replace(/\/+$/, '') const requestJson = (path: string): Promise => 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 楼层映射 后端字段: ```ts { floorId: '2065808921272119298', floorCode: 'L1', floorName: '1.0层', sortOrder: 10 } ``` 领域模型: ```ts 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 映射 后端字段: ```ts { 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 } ``` 领域模型: ```ts 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,它可能是空间门点。 建议在数据层使用独立内部类型,例如: ```ts 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,而不是静态常量。 建议规则: ```text 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 不变: ```ts export interface GuideRepository { getAssetBaseUrl(): string getFloors(): Promise normalizeFloorId(labelOrId: string): string listPois(): Promise getPoiById(id: string): Promise searchPois(keyword?: string): Promise getLocationPreview(poiId: string): Promise getRouteReadiness(): Promise } ``` 新增 `SgsSdkGuideRepository`: ```ts 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` 已有: ```ts export type DataSourceMode = 'static' | 'api' | 'sdk' export const isSgsSdkMode = () => dataSourceConfig.mode === 'sdk' ``` 推荐新增仓库工厂: ```ts 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 | `SgsMapRenderer` | 不要把 `sdk` 理解成“页面直接调用 `SGSMapSDK.getFloorPois()`”。数据仍从 Repository 进来,SDK 只做地图渲染和交互命令。 ## 9. 展示层改造边界 允许展示层做的事: - 调用 `guideUseCase.getFloors()` - 调用 `guideUseCase.searchPois(keyword)` - 调用 `guideUseCase.getPoiById(id)` - 传 `GuideLocationPreview.positionGltf` 给地图聚焦 - 通过 `GuideMapShell` 选择 `ThreeMap` 或 `SgsMapRenderer` 禁止展示层做的事: ```ts // 禁止:页面直接请求后端 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. 推荐实施步骤 1. 新增 `src/data/providers/sgsSdkApiProvider.ts`。 2. 新增 `src/data/adapters/sgsSdkGuideAdapter.ts`。 3. 新增 `SgsSdkGuideRepository`,实现现有 `GuideRepository` interface。 4. 新增仓库工厂,根据 `dataSourceConfig.mode` 选择 static 或 SGS 后端数据。 5. 让 `guideUseCase` 使用仓库工厂,不改页面调用方式。 6. 在 `sdk` 模式下让 `GuideMapShell` 继续选择 `SgsMapRenderer`,但 POI/楼层数据仍来自 `GuideUseCase`。 7. 增加数据健康检查脚本或开发命令,校验楼层、POI、空间面、导航目的地计数。 8. 通过 H5 浏览器检查搜索、楼层切换、POI 聚焦、位置预览。 ## 11. 验收检查清单 数据接口检查: ```powershell $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}/pois` POI ID 一致。 - POI 无重复 ID。 - POI 无缺失 `id/name/type/floorId/position`。 - POI `floorId` 与请求楼层一致。 - 空间面无缺失 `id/name/type/floorId/boundaryWkt`。 - `GuideRepository.listPois()` 只返回 `MuseumPoi[]`。 - 页面源码中不能出现 `/app-api/gis/sdk`。 - 页面源码中不能出现 `SGSMapSDK`。 代码扫描: ```powershell rg -n "/app-api/gis/sdk|SGSMapSDK|getFloorPois|getManifest|getNavigablePlaces" src/pages src/components ``` 预期: - `src/components/map/SgsMapRenderer.vue` 可以出现 SDK 渲染相关服务调用。 - 其它页面和组件不应直接出现后端 SDK 数据接口或 SDK 全局对象。 构建验证: ```powershell pnpm type-check pnpm lint pnpm build:h5 ``` 浏览器验证: - `static` 模式仍可加载本地 3D/POI。 - `api` 模式能展示后端楼层和 POI,渲染器不变。 - `sdk` 模式能加载 SGS 地图基座,楼层切换与 POI 聚焦可用。 - SDK 失败时有错误态,不出现空白地图。 - 顶部 tabs、搜索、楼层控件、详情卡片不被 iframe/canvas 遮挡。 ## 12. 上线前阻断项 以下问题未解决前,不建议对用户宣称“正式馆内导航”: - `L-1` 无可导航目的地。 - `EXTERIOR` 路网未就绪。 - `manifest` 计数与实际明细不一致。 - `guideStopCount` 聚合口径不一致。 - POI 全部 `status=INACTIVE` 的业务语义未确认。 用户文案应保持为: - `位置预览` - `查看三维位置` - `路线预览` - `导航数据准备中` 不要使用: - `开始馆内导航` - `实时导航` - `到达引导` - `精准路径规划` ## 13. 维护约定 - 后端接口字段变化先改 Provider Payload 类型。 - 领域含义变化再改 Adapter。 - UI 需要新信息时先扩展领域模型,再通过 Repository 暴露。 - 不要为了一个页面临时把 raw SDK 字段传透到组件。 - 每次更新 SDK 后,重新跑楼层/POI/空间面/导航目的地完整性检查。 - 每次启用真实路线能力前,重新审核 `GuideRouteReadiness`,保持不夸大导航能力。