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 } 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 } 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 { code: number msg?: string data?: T } 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 getGuideStops(floorId: string): Promise getGuideStopsByHall(hallId: string): Promise getNavigablePlaces(floorId: string): Promise getFloorBusinessPois( floorId: string, options?: { types?: string[]; keyword?: string; limit?: number } ): Promise queryPois(params: SgsPoiQueryParamsPayload): Promise getFeaturedRoutes( mapId?: string, options?: { limit?: number } ): Promise getFeaturedRoute(routeId: string): Promise planRoute(payload: SgsRoutePlanRequestPayload): Promise } 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 | 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 = (payload: unknown, requestUrl: string, contentType: string): T => { if (typeof payload === 'string') { const trimmedPayload = payload.trim() const looksLikeHtml = /^]/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 = ( path: string, options: { method?: 'GET' | 'POST'; data?: unknown } = {} ): Promise => 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 | 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>(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).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() const mapDiagnosticsCache = new Map() const floorDiagnosticsCache = new Map() const floorBundleCache = new Map() const floorPoiCache = new Map() const floorSpaceCache = new Map() const floorGuideStopCache = new Map() const hallGuideStopCache = new Map() const navigablePlaceCache = new Map() const floorBusinessPoiCache = new Map() const poiQueryCache = new Map() const featuredRoutesCache = new Map() const featuredRouteDetailCache = new Map() 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( `/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( `/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( `/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( `/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( `/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( `/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( `/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( `/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( `/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( `/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( `/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( `/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( `/gis/sdk/featured-routes/${encodeURIComponent(routeId)}` ) featuredRouteDetailCache.set(routeId, route) return route }, async planRoute(payload) { return requestJson( '/gis/sdk/routes/plan', { method: 'POST', data: payload } ) } } } export const defaultSgsSdkApiProvider = createSgsSdkApiProvider()