Files
frontend-miniapp/src/data/providers/sgsSdkApiProvider.ts
lyf 7cda427de9 修复 SGS SDK 导览数据源与模型加载
升级 SDK API 数据契约,补齐 guideStops、业务 POI、诊断与路线数据接口。

修复 SDK 模式下 POI/模型/楼层混用静态数据的问题,并为 ThreeMap 模型加载增加短退避重试。
2026-07-02 10:26:52 +08:00

601 lines
18 KiB
TypeScript

import {
dataSourceConfig
} from '@/config/dataSource'
export type SgsDiagnosticsStatusPayload = 'OK' | 'WARN' | 'ERROR'
export interface SgsSdkFloorSummaryPayload {
floorId: string | number
floorCode?: string | null
floorName?: string | null
sortOrder?: number | null
modelSizeBytes?: number | null
poiCount?: number | null
spaceCount?: number | null
}
export interface SgsSdkManifestPayload {
mapId: string | number
mapName?: string | null
sdkVersion?: string | null
protocolVersion?: number | null
dataVersion?: string | null
updatedAt?: string | null
coordinateSystem?: string | null
floors: SgsSdkFloorSummaryPayload[]
capabilities?: Record<string, boolean | undefined>
}
export type SgsPoiGroupPayload = 'SERVICE' | 'BUSINESS' | 'OTHER'
export type SgsBusinessPoiTypePayload =
| 'shop'
| 'restaurant'
| 'cafe'
| 'vending'
| 'bookstore'
| 'cultural_shop'
| 'photo_spot'
| string
export interface SgsPoiQueryParamsPayload {
mapId?: string | number
floorId?: string | number
group?: SgsPoiGroupPayload
types?: string[]
keyword?: string
limit?: number
}
export interface SgsPositionPayload {
x?: number | null
y?: number | null
z?: number | null
}
export interface SgsPoiPayload {
id: string | number
name?: string | null
type?: string | null
typeName?: string | null
floorCode?: string | null
floorId?: string | number | null
position?: SgsPositionPayload | null
x?: number | null
y?: number | null
z?: number | null
status?: string | null
anchorNodeName?: string | null
description?: string | null
iconUrl?: string | null
poiGroup?: SgsPoiGroupPayload | string | null
businessType?: SgsBusinessPoiTypePayload | null
coverImageUrl?: string | null
phone?: string | null
businessHours?: string | null
sortOrder?: number | null
spatialAreaId?: string | number | null
spatialAreaName?: string | null
}
export interface SgsSpacePayload {
id: string | number
name?: string | null
type?: string | null
floorId?: string | number | null
boundaryWkt?: string | null
center?: SgsPositionPayload | null
sourceNodeName?: string | null
status?: string | null
colorHex?: string | null
}
export interface SgsNavigablePlacePayload {
id?: string | number | null
name?: string | null
category?: string | null
type?: string | null
typeCode?: string | null
typeName?: string | null
floorId?: string | number | null
floorCode?: string | null
floorName?: string | null
position?: SgsPositionPayload | null
x?: number | null
y?: number | null
z?: number | null
nodeId?: string | number | null
ownerName?: string | null
}
export interface SgsGuideStopPayload {
id: string | number
name?: string | null
type?: string | null
typeName?: string | null
floorId?: string | number | null
position?: SgsPositionPayload | null
x?: number | null
y?: number | null
z?: number | null
targetType?: string | null
targetId?: string | number | null
audioUrl?: string | null
coverImageUrl?: string | null
description?: string | null
status?: string | null
located?: boolean | null
hasAudio?: boolean | null
poiId?: string | number | null
outlineId?: string | number | null
outlineName?: string | null
hallId?: string | number | null
hallName?: string | null
sort?: number | null
routeId?: string | number | null
routeName?: string | null
seqOrder?: number | null
stayMinutes?: number | null
}
export interface SgsFloorDiagnosticsPayload {
floorId: string | number
floorCode?: string | null
floorName?: string | null
status: SgsDiagnosticsStatusPayload
modelReady?: boolean
poiCount?: number
spaceCount?: number
guideStopCount?: number
navigablePlaceCount?: number
routeNodeCount?: number
routeEdgeCount?: number
routePlanningReady?: boolean
warnings?: string[]
}
export interface SgsFloorDiagnosticsSummaryPayload extends SgsFloorDiagnosticsPayload {}
export interface SgsMapDiagnosticsPayload {
mapId: string | number
mapName?: string | null
sdkVersion?: string | null
status: SgsDiagnosticsStatusPayload
floors: SgsFloorDiagnosticsSummaryPayload[]
summary?: {
floorCount?: number
modelReadyFloorCount?: number
poiCount?: number
spaceCount?: number
guideStopCount?: number
navigablePlaceCount?: number
routeNodeCount?: number
routeEdgeCount?: number
}
warnings?: string[]
}
export interface SgsFloorBundlePayload {
floor?: SgsSdkFloorSummaryPayload | null
model?: SgsModelInfoPayload | null
pois?: SgsPoiPayload[]
spaces?: SgsSpacePayload[]
guideStops?: SgsGuideStopPayload[]
routeSummary?: {
hasRouteNetwork?: boolean
nodeCount?: number
edgeCount?: number
} | null
hiddenSceneNodeNames?: string[]
dataVersion?: string | null
updatedAt?: string | null
}
export interface SgsModelInfoPayload {
modelUrl?: string | null
fallbackModelUrl?: string | null
compressionType?: string | null
modelVersion?: string | null
nodeCount?: number | null
sizeBytes?: number | null
originSizeBytes?: number | null
}
export interface SgsRoutePlanRequestPayload {
startFloorId: number | string
startX: number
startY: number
startNodeId?: number | string | null
endFloorId: number | string
endX: number
endY: number
endNodeId?: number | string | null
wheelchair: 0 | 1
}
export interface SgsRouteStepNodePayload {
id?: string | number | null
nodeName?: string | null
floorId?: string | number | null
nodeType?: string | null
x?: number | null
y?: number | null
z?: number | null
}
export interface SgsRoutePathPointPayload {
x?: number | null
y?: number | null
z?: number | null
}
export interface SgsRouteSegmentPayload {
floorId?: string | number | null
floorCode?: string | null
floorName?: string | null
startNodeId?: string | number | null
endNodeId?: string | number | null
startNodeName?: string | null
endNodeName?: string | null
segmentType?: string | null
type?: string | null
transferType?: string | null
distance?: number | null
duration?: number | null
pathGeoJson?: string | null
pathPoints?: SgsRoutePathPointPayload[]
nodePathIds?: Array<string | number | null>
}
export interface SgsRoutePlanResponsePayload {
distance?: number | null
duration?: number | null
pathGeoJson?: string | null
nodePaths?: SgsRouteStepNodePayload[]
segments?: SgsRouteSegmentPayload[]
}
export interface SgsRouteEndpointPayload {
id?: string | number | null
name?: string | null
floorId?: string | number | null
floorCode?: string | null
floorName?: string | null
x?: number | null
y?: number | null
z?: number | null
}
export interface SgsFeaturedRouteSummaryPayload {
id: string | number
mapId?: string | number | null
routeName?: string | null
routeNameEn?: string | null
routeType?: string | null
theme?: string | null
description?: string | null
coverImageUrl?: string | null
estimatedDuration?: number | null
distanceMeters?: number | null
waypointCount?: number | null
sortOrder?: number | null
status?: string | number | null
}
export interface SgsFeaturedRouteWaypointPayload {
order?: number | null
spaceId?: string | number | null
spaceName?: string | null
guideStopId?: string | number | null
guideStopName?: string | null
floorId?: string | number | null
floorCode?: string | null
floorName?: string | null
x?: number | null
y?: number | null
z?: number | null
stayMinutes?: number | null
}
export interface SgsFeaturedRouteDetailPayload extends SgsFeaturedRouteSummaryPayload {
startEntrance?: SgsRouteEndpointPayload | null
endEntrance?: SgsRouteEndpointPayload | null
waypoints?: SgsFeaturedRouteWaypointPayload[]
}
interface CommonResult<T> {
code: number
msg?: string
data?: T
}
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[]>
getGuideStops(floorId: string): Promise<SgsGuideStopPayload[]>
getGuideStopsByHall(hallId: string): Promise<SgsGuideStopPayload[]>
getNavigablePlaces(floorId: string): Promise<SgsNavigablePlacePayload[]>
getFloorBusinessPois(
floorId: string,
options?: { types?: string[]; keyword?: string; limit?: number }
): Promise<SgsPoiPayload[]>
queryPois(params: SgsPoiQueryParamsPayload): Promise<SgsPoiPayload[]>
getFeaturedRoutes(
mapId?: string,
options?: { limit?: number }
): Promise<SgsFeaturedRouteSummaryPayload[]>
getFeaturedRoute(routeId: string): Promise<SgsFeaturedRouteDetailPayload>
planRoute(payload: SgsRoutePlanRequestPayload): Promise<SgsRoutePlanResponsePayload>
}
const normalizeBaseUrl = (baseUrl: string) => baseUrl.replace(/\/+$/, '')
const resolveAppApiBaseUrl = () => {
const baseUrl = normalizeBaseUrl(dataSourceConfig.sgsApiBaseUrl || dataSourceConfig.apiBaseUrl || '/app-api')
return baseUrl.endsWith('/app-api') ? baseUrl : `${baseUrl}/app-api`
}
const getHeaderValue = (
header: Record<string, string> | undefined,
key: string
) => {
if (!header) return ''
const matchedKey = Object.keys(header).find((item) => item.toLowerCase() === key.toLowerCase())
return matchedKey ? String(header[matchedKey] || '') : ''
}
const previewPayload = (payload: unknown) => {
const text = typeof payload === 'string' ? payload : JSON.stringify(payload) || ''
return text.replace(/\s+/g, ' ').trim().slice(0, 120)
}
const parseJsonPayload = <T>(payload: unknown, requestUrl: string, contentType: string): T => {
if (typeof payload === 'string') {
const trimmedPayload = payload.trim()
const looksLikeHtml = /^<!doctype\s+html/i.test(trimmedPayload) || /^<html[\s>]/i.test(trimmedPayload)
if (looksLikeHtml || contentType.toLowerCase().includes('text/html')) {
throw new Error(`SGS 数据接口返回非 JSON: ${requestUrl} content-type=${contentType || 'unknown'} body="${previewPayload(payload)}"`)
}
try {
return JSON.parse(trimmedPayload) as T
} catch {
throw new Error(`SGS 数据接口 JSON 解析失败: ${requestUrl} content-type=${contentType || 'unknown'} body="${previewPayload(payload)}"`)
}
}
return payload as T
}
const requestJson = <T>(
path: string,
options: { method?: 'GET' | 'POST'; data?: unknown } = {}
): Promise<T> => new Promise((resolve, reject) => {
const baseUrl = resolveAppApiBaseUrl()
const requestUrl = `${baseUrl}${path}`
uni.request({
url: requestUrl,
method: options.method || 'GET',
data: options.method === 'POST' ? JSON.stringify(options.data || {}) : undefined,
header: options.method === 'POST'
? {
'Content-Type': 'application/json'
}
: undefined,
timeout: dataSourceConfig.sgsSdkTimeoutMs,
success: (response) => {
const statusCode = Number(response.statusCode || 0)
const contentType = getHeaderValue(response.header as Record<string, string> | undefined, 'content-type')
if (statusCode < 200 || statusCode >= 300) {
reject(new Error(`SGS 数据接口请求失败: ${statusCode} ${requestUrl} content-type=${contentType || 'unknown'} body="${previewPayload(response.data)}"`))
return
}
try {
const body = parseJsonPayload<CommonResult<T>>(response.data, requestUrl, contentType)
if (!body || body.code !== 0) {
reject(new Error(`SGS 数据接口业务失败: ${requestUrl} code=${body?.code} msg=${body?.msg || ''}`))
return
}
resolve(body.data as T)
} catch (error) {
reject(error)
}
},
fail: (error) => {
reject(new Error(`SGS 数据接口网络失败: ${path} ${JSON.stringify(error)}`))
}
})
})
const buildQueryString = (params: object) => {
const query = new URLSearchParams()
Object.entries(params as Record<string, unknown>).forEach(([key, value]) => {
if (value === null || typeof value === 'undefined' || value === '') return
query.set(key, Array.isArray(value) ? value.join(',') : String(value))
})
const queryString = query.toString()
return queryString ? `?${queryString}` : ''
}
const stableCacheKey = (value: unknown) => JSON.stringify(value)
export const createSgsSdkApiProvider = (): SgsSdkApiProvider => {
const manifestCache = new Map<string, SgsSdkManifestPayload>()
const mapDiagnosticsCache = new Map<string, SgsMapDiagnosticsPayload>()
const floorDiagnosticsCache = new Map<string, SgsFloorDiagnosticsPayload>()
const floorBundleCache = new Map<string, SgsFloorBundlePayload>()
const floorPoiCache = new Map<string, SgsPoiPayload[]>()
const floorSpaceCache = new Map<string, SgsSpacePayload[]>()
const floorGuideStopCache = new Map<string, SgsGuideStopPayload[]>()
const hallGuideStopCache = new Map<string, SgsGuideStopPayload[]>()
const navigablePlaceCache = new Map<string, SgsNavigablePlacePayload[]>()
const floorBusinessPoiCache = new Map<string, SgsPoiPayload[]>()
const poiQueryCache = new Map<string, SgsPoiPayload[]>()
const featuredRoutesCache = new Map<string, SgsFeaturedRouteSummaryPayload[]>()
const featuredRouteDetailCache = new Map<string, SgsFeaturedRouteDetailPayload>()
const mapIdFor = (mapId?: string) => mapId || dataSourceConfig.sgsMapId
const floorIdFor = (floorId: string) => encodeURIComponent(floorId)
const hallIdFor = (hallId: string) => encodeURIComponent(hallId)
return {
async getManifest(mapId) {
const normalizedMapId = mapIdFor(mapId)
const cached = manifestCache.get(normalizedMapId)
if (cached) return cached
const manifest = await requestJson<SgsSdkManifestPayload>(
`/gis/sdk/maps/${encodeURIComponent(normalizedMapId)}/manifest`
)
manifestCache.set(normalizedMapId, manifest)
return manifest
},
async getMapDiagnostics(mapId) {
const normalizedMapId = mapIdFor(mapId)
const cached = mapDiagnosticsCache.get(normalizedMapId)
if (cached) return cached
const diagnostics = await requestJson<SgsMapDiagnosticsPayload>(
`/gis/sdk/maps/${encodeURIComponent(normalizedMapId)}/diagnostics`
)
mapDiagnosticsCache.set(normalizedMapId, diagnostics)
return diagnostics
},
async getFloorDiagnostics(floorId) {
const cached = floorDiagnosticsCache.get(floorId)
if (cached) return cached
const diagnostics = await requestJson<SgsFloorDiagnosticsPayload>(
`/gis/sdk/floors/${floorIdFor(floorId)}/diagnostics`
)
floorDiagnosticsCache.set(floorId, diagnostics)
return diagnostics
},
async getFloorBundle(floorId) {
const cached = floorBundleCache.get(floorId)
if (cached) return cached
const bundle = await requestJson<SgsFloorBundlePayload>(
`/gis/sdk/floors/${floorIdFor(floorId)}/bundle`
)
floorBundleCache.set(floorId, bundle)
return bundle
},
async getFloorPois(floorId) {
const cached = floorPoiCache.get(floorId)
if (cached) return cached
const pois = await requestJson<SgsPoiPayload[]>(
`/gis/sdk/floors/${floorIdFor(floorId)}/pois`
)
floorPoiCache.set(floorId, pois)
return pois
},
async getFloorSpaces(floorId) {
const cached = floorSpaceCache.get(floorId)
if (cached) return cached
const spaces = await requestJson<SgsSpacePayload[]>(
`/gis/sdk/floors/${floorIdFor(floorId)}/spaces`
)
floorSpaceCache.set(floorId, spaces)
return spaces
},
async getGuideStops(floorId) {
const cached = floorGuideStopCache.get(floorId)
if (cached) return cached
const guideStops = await requestJson<SgsGuideStopPayload[]>(
`/gis/sdk/floors/${floorIdFor(floorId)}/guide-stops`
)
floorGuideStopCache.set(floorId, guideStops)
return guideStops
},
async getGuideStopsByHall(hallId) {
const cached = hallGuideStopCache.get(hallId)
if (cached) return cached
const guideStops = await requestJson<SgsGuideStopPayload[]>(
`/gis/sdk/halls/${hallIdFor(hallId)}/guide-stops`
)
hallGuideStopCache.set(hallId, guideStops)
return guideStops
},
async getNavigablePlaces(floorId) {
const cached = navigablePlaceCache.get(floorId)
if (cached) return cached
const places = await requestJson<SgsNavigablePlacePayload[]>(
`/gis/sdk/floors/${floorIdFor(floorId)}/navigable-places`
)
navigablePlaceCache.set(floorId, places)
return places
},
async getFloorBusinessPois(floorId, options = {}) {
const cacheKey = `${floorId}:${stableCacheKey(options)}`
const cached = floorBusinessPoiCache.get(cacheKey)
if (cached) return cached
const pois = await requestJson<SgsPoiPayload[]>(
`/gis/sdk/floors/${floorIdFor(floorId)}/business-pois${buildQueryString(options)}`
)
floorBusinessPoiCache.set(cacheKey, pois)
return pois
},
async queryPois(params) {
const cacheKey = stableCacheKey(params)
const cached = poiQueryCache.get(cacheKey)
if (cached) return cached
const pois = await requestJson<SgsPoiPayload[]>(
`/gis/sdk/pois${buildQueryString(params)}`
)
poiQueryCache.set(cacheKey, pois)
return pois
},
async getFeaturedRoutes(mapId, options = {}) {
const normalizedMapId = mapIdFor(mapId)
const cacheKey = `${normalizedMapId}:${stableCacheKey(options)}`
const cached = featuredRoutesCache.get(cacheKey)
if (cached) return cached
const routes = await requestJson<SgsFeaturedRouteSummaryPayload[]>(
`/gis/sdk/maps/${encodeURIComponent(normalizedMapId)}/featured-routes${buildQueryString(options)}`
)
featuredRoutesCache.set(cacheKey, routes)
return routes
},
async getFeaturedRoute(routeId) {
const cached = featuredRouteDetailCache.get(routeId)
if (cached) return cached
const route = await requestJson<SgsFeaturedRouteDetailPayload>(
`/gis/sdk/featured-routes/${encodeURIComponent(routeId)}`
)
featuredRouteDetailCache.set(routeId, route)
return route
},
async planRoute(payload) {
return requestJson<SgsRoutePlanResponsePayload>(
'/gis/sdk/routes/plan',
{
method: 'POST',
data: payload
}
)
}
}
}
export const defaultSgsSdkApiProvider = createSgsSdkApiProvider()