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

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

285 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
GuideRouteReadiness,
GuideRouteResult,
GuideRouteTarget
} from '@/domain/museum'
import {
NAV_ROUTE_READINESS
} from '@/domain/guideReadiness'
import {
isIndoorNavigableFloor
} from '@/domain/guideFloor'
import {
GuideRouteError
} from '@/repositories/GuideRouteRepository'
import {
defaultSgsSdkApiProvider,
type SgsFloorDiagnosticsPayload,
type SgsNavigablePlacePayload,
type SgsSdkApiProvider
} from '@/data/providers/sgsSdkApiProvider'
import {
toGuideRouteResultFromSgsPlan
} from '@/data/adapters/sgsSdkRouteAdapter'
import { stringifyId, formatSgsFloorLabel } from '@/data/adapters/sgsSdkGuideAdapter'
const createUnavailableReadiness = (message: string): GuideRouteReadiness => ({
ready: false,
message,
requiredData: ['route_graph', 'nav_data']
})
const logSdkRouteReadinessIssue = (message: string, payload: Record<string, unknown>) => {
console.warn(`[SDKRouteReadiness] ${message}`, payload)
}
const toNumber = (value: unknown) => (
Number.isFinite(Number(value)) ? Number(value) : undefined
)
const isPositiveNumber = (value: unknown) => {
const numberValue = toNumber(value)
return typeof numberValue === 'number' && numberValue > 0
}
const hasRouteNodeId = (place: SgsNavigablePlacePayload) => (
place.nodeId !== null && place.nodeId !== undefined && place.nodeId !== ''
)
const hasRouteGraphEvidence = (floor: Pick<SgsFloorDiagnosticsPayload, 'routePlanningReady' | 'routeNodeCount' | 'routeEdgeCount'>) => (
floor.routePlanningReady === true
|| (isPositiveNumber(floor.routeNodeCount) && isPositiveNumber(floor.routeEdgeCount))
)
const hasRouteTargetEvidence = (
floor: Pick<SgsFloorDiagnosticsPayload, 'navigablePlaceCount'>,
routeTargetCount?: number
) => {
if (typeof routeTargetCount === 'number') return routeTargetCount > 0
return floor.navigablePlaceCount === undefined || isPositiveNumber(floor.navigablePlaceCount)
}
const isRouteReadyFromDiagnostics = (
floor: Pick<SgsFloorDiagnosticsPayload, 'routePlanningReady' | 'routeNodeCount' | 'routeEdgeCount' | 'navigablePlaceCount'>,
routeTargetCount?: number
) => hasRouteGraphEvidence(floor) && hasRouteTargetEvidence(floor, routeTargetCount)
const positionFromPlace = (place: SgsNavigablePlacePayload): [number, number, number] | undefined => {
const source = place.position || {
x: place.x,
y: place.y,
z: place.z
}
const x = toNumber(source.x)
const z = toNumber(source.z)
if (typeof x !== 'number' || typeof z !== 'number') return undefined
return [
x,
toNumber(source.y) || 0,
z
]
}
export class SdkGuideRouteRepository {
private targetsCache: GuideRouteTarget[] | null = null
constructor(
private readonly sdkApiProvider: SgsSdkApiProvider = defaultSgsSdkApiProvider
) {}
async getRouteReadiness(): Promise<GuideRouteReadiness> {
try {
const diagnostics = await this.sdkApiProvider.getMapDiagnostics()
const floors = (diagnostics.floors || []).filter(isIndoorNavigableFloor)
if (!floors.length) {
return createUnavailableReadiness('SDK 地图楼层数据为空')
}
// 地图级 diagnostics 的楼层摘要只保证 routePlanningReady/navigablePlaceCount
// routeNodeCount/routeEdgeCount 可能只出现在 floor diagnostics 或 summary 中。
let readyFloors = floors.filter((floor) => isRouteReadyFromDiagnostics(floor))
let notReadyFloors = floors.filter((floor) => !isRouteReadyFromDiagnostics(floor))
if (readyFloors.length === 0) {
const floorEvidence = await Promise.all(
floors.map((floor) => this.loadFloorRouteEvidence(floor))
)
readyFloors = floorEvidence
.filter((item) => item.ready)
.map((item) => item.floor)
notReadyFloors = floorEvidence
.filter((item) => !item.ready)
.map((item) => item.floor)
}
// 没有楼层有路线数据时不可用
if (readyFloors.length === 0) {
logSdkRouteReadinessIssue('SDK 路线数据未达到位置关系生成条件', {
floorCount: floors.length,
readyFloorCount: readyFloors.length,
summary: diagnostics.summary,
floors: floors.map((floor) => ({
floorId: floor.floorId,
floorName: floor.floorName,
floorCode: floor.floorCode,
routePlanningReady: floor.routePlanningReady,
routeNodeCount: floor.routeNodeCount,
routeEdgeCount: floor.routeEdgeCount,
navigablePlaceCount: floor.navigablePlaceCount
}))
})
return createUnavailableReadiness('无法定位,当前仅支持点位位置预览')
}
// 如果有楼层未就绪,给出警告但仍然可用
if (notReadyFloors.length > 0) {
const floorLabels = notReadyFloors
.map((f) => f.floorName || f.floorCode || String(f.floorId))
.join('、')
return {
ready: true,
message: `部分楼层位置关系数据未就绪:${floorLabels},可查看已就绪楼层的位置关系`,
requiredData: []
}
}
return {
ready: true,
message: 'SDK 位置关系数据已接入,可进行位置预览',
requiredData: []
}
} catch (error) {
console.error('[SDKRouteReadiness] 读取 SDK 路线状态失败', error)
return createUnavailableReadiness(
error instanceof Error ? error.message : 'SDK 位置关系数据加载失败'
)
}
}
private async loadFloorRouteEvidence(floor: SgsFloorDiagnosticsPayload) {
const rawFloorId = stringifyId(floor.floorId)
const [floorDiagnostics, navigablePlaces] = await Promise.all([
this.sdkApiProvider.getFloorDiagnostics(rawFloorId).catch(() => null),
this.sdkApiProvider.getNavigablePlaces(rawFloorId).catch(() => [])
])
const routeTargetCount = navigablePlaces.filter(hasRouteNodeId).length
const evidence = floorDiagnostics || floor
return {
floor: {
...floor,
...floorDiagnostics
},
ready: isRouteReadyFromDiagnostics(evidence, routeTargetCount),
routeTargetCount
}
}
async listRouteTargets(): Promise<GuideRouteTarget[]> {
if (this.targetsCache) return this.targetsCache
const diagnostics = await this.sdkApiProvider.getMapDiagnostics()
const floors = (diagnostics.floors || []).filter(isIndoorNavigableFloor)
const targets: GuideRouteTarget[] = []
for (const floor of floors) {
// 使用原始 floorId 请求 APISDK API 需要原始 ID 如 2065808921272119298
const rawFloorId = stringifyId(floor.floorId)
try {
const places = await this.sdkApiProvider.getNavigablePlaces(rawFloorId)
for (const place of places) {
// 过滤掉没有 nodeId 的点位,这些无法用于路线规划
if (!hasRouteNodeId(place)) {
continue
}
const placeId = stringifyId(place.id)
const placeName = place.name || `目的地 ${placeId}`
const categoryLabel = place.category || place.typeName || place.typeCode || place.type || undefined
const positionGltf = positionFromPlace(place)
// 使用统一的楼层标签格式化函数,确保与 nav-assets 格式一致
const floorLabel = formatSgsFloorLabel(
place.floorCode || undefined,
place.floorName || floor.floorName || undefined
)
targets.push({
poiId: placeId,
name: placeName,
// 保留后端原始 floorId确保与 SDK 模型楼层和 ThreeMap 当前楼层一致。
floorId: stringifyId(place.floorId ?? rawFloorId),
floorLabel,
categoryLabel,
positionGltf,
// 保存原始 nodeId 用于 API 调用
routeNodeId: stringifyId(place.nodeId)
})
}
} catch {
// 忽略单个楼层的加载错误
}
}
this.targetsCache = targets
return targets
}
async findRoute(startPoiId: string, endPoiId: string): Promise<GuideRouteResult> {
const readiness = await this.getRouteReadiness()
if (!readiness.ready) {
throw new GuideRouteError(
'ROUTE_DATA_UNAVAILABLE',
readiness.message || NAV_ROUTE_READINESS.message
)
}
if (!startPoiId || !endPoiId) {
throw new GuideRouteError('ROUTE_TARGET_MISSING', '请选择起点和终点')
}
if (startPoiId === endPoiId) {
throw new GuideRouteError('ROUTE_TARGET_MISSING', '起点和终点不能相同')
}
const targets = await this.listRouteTargets()
const startTarget = targets.find((target) => target.poiId === startPoiId)
const endTarget = targets.find((target) => target.poiId === endPoiId)
if (!startTarget || !endTarget) {
throw new GuideRouteError('ROUTE_TARGET_MISSING', '起点或终点不在 SDK 可预览目的地列表中')
}
try {
if (!startTarget.positionGltf || !endTarget.positionGltf) {
throw new GuideRouteError('ROUTE_TARGET_UNANCHORED', '起点或终点缺少可规划坐标')
}
const route = await this.sdkApiProvider.planRoute({
startFloorId: startTarget.floorId,
startX: startTarget.positionGltf[0],
startY: startTarget.positionGltf[2],
startNodeId: startTarget.routeNodeId || null,
endFloorId: endTarget.floorId,
endX: endTarget.positionGltf[0],
endY: endTarget.positionGltf[2],
endNodeId: endTarget.routeNodeId || null,
wheelchair: 0
})
return toGuideRouteResultFromSgsPlan(route, startTarget, endTarget)
} catch (error) {
if (error instanceof GuideRouteError) throw error
throw new GuideRouteError(
'ROUTE_NOT_FOUND',
error instanceof Error ? error.message : 'SGS 位置关系接口生成失败'
)
}
}
}