Files
frontend-miniapp/docs/Data/SGS_SDK_DATA_LAYER_INTEGRATION_GUIDE.md
lyf 72885b7f54
Some checks failed
CI / verify (push) Has been cancelled
升级 SGS Map SDK 至 2.5.0
2026-07-16 09:43:52 +08:00

19 KiB
Raw Permalink Blame History

SGS Map SDK 数据层接入操作手册

创建日期2026-06-25
适用项目:frontend-miniapp H5 导览业务
目标:通过后端 SGS Map SDK 数据接口接入真实楼层、POI、空间面和导航目的地数据同时保持展示层只消费领域模型。

1. 接入原则

SGS Map SDK 接入分为两条边界:

边界 职责 允许接触 SDK/后端字段的位置 禁止事项
数据层 拉取楼层、POI、空间面、导航目的地、诊断信息并转换为 MuseumFloorMuseumPoiGuideLocationPreview 等领域模型 src/data/providerssrc/data/adapterssrc/repositories 页面和组件直接请求 /app-api/gis/sdk/*
渲染层 加载 SGS H5 Engine、管理 iframe/SDK 生命周期、执行切楼层和聚焦命令 src/services/sgs、未来独立 renderer 把后端数据解析逻辑写进 renderer

展示层必须只通过以下入口拿数据:

  • src/usecases/guideUseCase.ts
  • src/repositories/GuideRepository.ts 暴露的 GuideRepository 合同
  • src/domain/museum.ts 中的领域模型

展示层禁止直接依赖:

  • SGSMapSDK
  • /app-api/gis/sdk/* 响应结构
  • floorCodetypeNameposition.x/y/z 等后端原始字段
  • manifest.poiCountmanifest.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=INACTIVEanchorNodeNamedescriptioniconUrl 为空。数据层不要因为 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/渲染生命周期。它可以接收领域模型里来的 floorIdpoiIdpositionGltf,但不能自己请求 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'。组件只看领域层的 primaryCategoryaccessiblefloorLabel

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 当前仍为 ThreeMapSDK 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. 推荐实施步骤

  1. 新增 src/data/providers/sgsSdkApiProvider.ts
  2. 新增 src/data/adapters/sgsSdkGuideAdapter.ts
  3. 新增 SgsSdkGuideRepository,实现现有 GuideRepository interface。
  4. 新增仓库工厂,根据 dataSourceConfig.mode 选择 static 或 SGS 后端数据。
  5. guideUseCase 使用仓库工厂,不改页面调用方式。
  6. 保持当前 GuideMapShell 使用 ThreeMap;若产品启用 iframe renderer单独实施并保持 POI/楼层数据来自 GuideUseCase
  7. 增加数据健康检查脚本或开发命令校验楼层、POI、空间面、导航目的地计数。
  8. 通过 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}/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

代码扫描:

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,保持不夸大导航能力。