修复搜索与点位定位闭环
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-12 03:07:49 +08:00
parent e006333c0a
commit d8420fc7c2
18 changed files with 2961 additions and 621 deletions

View File

@@ -15,6 +15,9 @@ import {
isIndoorNavigableFloor,
isPoiOnIndoorNavigableFloor
} from '@/domain/guideFloor'
import {
warnPoiCollectionIssues
} from '@/domain/poiCategories'
import {
formatNavFloorLabel,
isStaticIndoorNavigableFloorId,
@@ -127,6 +130,91 @@ const dedupePoisById = (pois: MuseumPoi[]) => {
})
}
const getPoiSpaceIdentity = (poi: MuseumPoi) => poi.sourceSpaceId || poi.spaceId || ''
const mergeSearchablePois = (pois: MuseumPoi[], spacePoints: MuseumPoi[]) => {
const hallPoisBySpaceId = new Map(
pois
.map((poi) => [getPoiSpaceIdentity(poi), poi] as const)
.filter(([spaceId]) => Boolean(spaceId))
)
const spacePointsBySpaceId = new Map(
spacePoints
.map((poi) => [getPoiSpaceIdentity(poi), poi] as const)
.filter(([spaceId]) => Boolean(spaceId))
)
const filteredPois = pois.filter((poi) => {
const spaceId = getPoiSpaceIdentity(poi)
const matchingSpacePoint = spaceId ? spacePointsBySpaceId.get(spaceId) : null
if (!matchingSpacePoint) return true
return matchingSpacePoint.primaryCategory.id === 'exhibition_hall'
})
const filteredSpacePoints = spacePoints.filter((poi) => {
const spaceId = getPoiSpaceIdentity(poi)
const matchingHallPoi = spaceId ? hallPoisBySpaceId.get(spaceId) : null
if (!matchingHallPoi) return true
return poi.primaryCategory.id !== 'exhibition_hall'
})
return dedupePoisById([
...filteredPois,
...filteredSpacePoints
])
}
const getMedianPoiY = (pois: MuseumPoi[]) => {
const values = pois
.map((poi) => poi.positionGltf?.[1])
.filter((value): value is number => Number.isFinite(value))
.sort((left, right) => left - right)
if (!values.length) return undefined
const middle = Math.floor(values.length / 2)
return values.length % 2 === 0
? (values[middle - 1] + values[middle]) / 2
: values[middle]
}
type SgsPoiSourceName = 'pois' | 'businessPois' | 'spaces' | 'navigablePlaces'
interface SgsPoiSourceFailure {
floorId: string
source: SgsPoiSourceName
message: string
}
const poiDataLoadErrorMessage = '点位数据加载失败,请检查网络后重试'
const toErrorMessage = (reason: unknown) => (
reason instanceof Error ? reason.message : String(reason || '未知错误')
)
const collectSgsSourceResult = <T>(
result: PromiseSettledResult<T>,
floorId: string,
source: SgsPoiSourceName,
failures: SgsPoiSourceFailure[]
) => {
if (result.status === 'fulfilled') return result.value
failures.push({
floorId,
source,
message: toErrorMessage(result.reason)
})
return null
}
const warnSgsSourceFailures = (scope: string, failures: SgsPoiSourceFailure[]) => {
if (!import.meta.env.DEV || !failures.length) return
// 开发期保留来源与楼层信息,便于区分真实空结果和接口失败。
console.warn(`[SGS 点位数据] ${scope}`, failures)
}
export interface GuideRepository {
getAssetBaseUrl(): string
getFloors(): Promise<MuseumFloor[]>
@@ -171,9 +259,11 @@ export class StaticGuideRepository implements GuideRepository {
if (this.poiCache) return this.poiCache
const pois = await this.provider.loadPoiIndex()
this.poiCache = pois
.map(toMuseumPoi)
const adaptedPois = pois.map(toMuseumPoi)
warnPoiCollectionIssues('static-nav-assets', adaptedPois)
this.poiCache = dedupePoisById(adaptedPois
.filter(isSearchableIndoorPoi)
)
return this.poiCache
}
@@ -299,30 +389,55 @@ export class SgsSdkGuideRepository implements GuideRepository {
const indoorFloors = manifest.floors.filter(isIndoorNavigableFloor)
const indoorFloorIds = new Set(indoorFloors.map((floor) => String(floor.floorId)))
this.floorAliases = buildSgsFloorAliases(indoorFloors)
const failures: SgsPoiSourceFailure[] = []
let successfulCoreSourceCount = 0
const floorDataGroups = await Promise.all(
indoorFloors.map(async (floor) => {
const floorId = String(floor.floorId)
const [pois, businessPois, spaces, navigablePlaces] = await Promise.all([
this.provider.getFloorPois(floorId).catch(() => []),
this.provider.getFloorBusinessPois(floorId).catch(() => []),
this.provider.getFloorSpaces(floorId).catch(() => []),
this.provider.getNavigablePlaces(floorId).catch(() => [])
const [poisResult, businessPoisResult, spacesResult, navigablePlacesResult] = await Promise.allSettled([
this.provider.getFloorPois(floorId),
this.provider.getFloorBusinessPois(floorId),
this.provider.getFloorSpaces(floorId),
this.provider.getNavigablePlaces(floorId)
])
const pois = collectSgsSourceResult(poisResult, floorId, 'pois', failures) || []
const businessPois = collectSgsSourceResult(businessPoisResult, floorId, 'businessPois', failures) || []
const spaces = collectSgsSourceResult(spacesResult, floorId, 'spaces', failures) || []
const navigablePlaces = collectSgsSourceResult(
navigablePlacesResult,
floorId,
'navigablePlaces',
failures
) || []
if (poisResult.status === 'fulfilled') successfulCoreSourceCount += 1
if (spacesResult.status === 'fulfilled') successfulCoreSourceCount += 1
const adaptedPois = [
...pois,
...businessPois
].map((poi) => toMuseumPoiFromSgs(poi, manifest.floors))
const fallbackY = getMedianPoiY(adaptedPois)
return {
floorId,
pois: [
...pois,
...businessPois
],
hallPois: toMuseumHallPoisFromSgs(spaces, navigablePlaces, manifest.floors, floorId)
pois: adaptedPois,
hallPois: toMuseumHallPoisFromSgs(
spaces,
navigablePlaces,
manifest.floors,
floorId,
{ fallbackY }
)
}
})
)
warnSgsSourceFailures('楼层点位聚合', failures)
if (indoorFloors.length && successfulCoreSourceCount === 0) {
throw new Error(poiDataLoadErrorMessage)
}
const ordinaryPois = floorDataGroups
.flatMap((group) => group.pois)
.map((poi) => toMuseumPoiFromSgs(poi, manifest.floors))
.filter((poi) => poi.id && poi.name)
.filter((poi) => indoorFloorIds.has(String(poi.floorId)) || isPoiOnIndoorNavigableFloor(poi))
@@ -330,12 +445,18 @@ export class SgsSdkGuideRepository implements GuideRepository {
.flatMap((group) => group.hallPois)
.filter((poi) => indoorFloorIds.has(String(poi.floorId)) || isPoiOnIndoorNavigableFloor(poi))
this.poiCache = dedupePoisById([
const adaptedPois = [
...hallPois,
...ordinaryPois
])
]
warnPoiCollectionIssues('sgs-guide-pois-before-dedupe', adaptedPois)
const result = dedupePoisById(adaptedPois)
const hasCoreFailures = failures.some((failure) => (
failure.source === 'pois' || failure.source === 'spaces'
))
if (!hasCoreFailures) this.poiCache = result
return this.poiCache
return result
}
async listSpacePoints() {
@@ -345,48 +466,100 @@ export class SgsSdkGuideRepository implements GuideRepository {
const indoorFloors = manifest.floors.filter(isIndoorNavigableFloor)
const indoorFloorIds = new Set(indoorFloors.map((floor) => String(floor.floorId)))
this.floorAliases = buildSgsFloorAliases(indoorFloors)
const failures: SgsPoiSourceFailure[] = []
let successfulSpaceSourceCount = 0
const floorDataGroups = await Promise.all(
indoorFloors.map(async (floor) => {
const floorId = String(floor.floorId)
const spaces = await this.provider.getFloorSpaces(floorId).catch(() => [])
const [spacesResult, floorPoisResult] = await Promise.allSettled([
this.provider.getFloorSpaces(floorId),
this.provider.getFloorPois(floorId)
])
const spaces = collectSgsSourceResult(spacesResult, floorId, 'spaces', failures) || []
const floorPois = collectSgsSourceResult(floorPoisResult, floorId, 'pois', failures) || []
if (spacesResult.status === 'fulfilled') successfulSpaceSourceCount += 1
const fallbackY = getMedianPoiY(
floorPois.map((poi) => toMuseumPoiFromSgs(poi, manifest.floors))
)
return {
floorId,
spaces
spaces,
fallbackY
}
})
)
warnSgsSourceFailures('空间点位聚合', failures)
if (indoorFloors.length && successfulSpaceSourceCount === 0) {
throw new Error(poiDataLoadErrorMessage)
}
const spacePoints = floorDataGroups
.flatMap((group) => group.spaces.map((space) => toMuseumSpacePointFromSgs(space, manifest.floors, group.floorId)))
.flatMap((group) => group.spaces.map((space) => toMuseumSpacePointFromSgs(
space,
manifest.floors,
group.floorId,
{ fallbackY: group.fallbackY }
)))
.filter((poi): poi is MuseumPoi => Boolean(poi))
.filter((poi) => indoorFloorIds.has(String(poi.floorId)) || isPoiOnIndoorNavigableFloor(poi))
this.spacePointCache = dedupePoisById(spacePoints)
warnPoiCollectionIssues('sgs-space-points-before-dedupe', spacePoints)
const result = dedupePoisById(spacePoints)
const hasSpaceFailures = failures.some((failure) => failure.source === 'spaces')
const floorsWithSpaces = new Set(
floorDataGroups
.filter((group) => group.spaces.length > 0)
.map((group) => group.floorId)
)
const hasPoiHeightFallbackFailures = failures.some((failure) => (
failure.source === 'pois' && floorsWithSpaces.has(failure.floorId)
))
if (!result.length && floorsWithSpaces.size && hasPoiHeightFallbackFailures) {
throw new Error(poiDataLoadErrorMessage)
}
if (!hasSpaceFailures && !hasPoiHeightFallbackFailures) this.spacePointCache = result
return this.spacePointCache
return result
}
async getPoiById(id: string) {
const pois = await this.listPois()
const matchedPoi = pois.find((poi) => poi.id === id)
const pois = await this.searchPois()
return pois.find((poi) => poi.id === id || poi.spaceId === id || poi.sourceSpaceId === id)
|| findPoiByNavIdNameFallback(pois, id)
if (matchedPoi) return matchedPoi
const spacePoints = await this.listSpacePoints()
return spacePoints.find((poi) => poi.id === id || poi.spaceId === id || poi.sourceSpaceId === id)
|| null
}
async searchPois(keyword = '') {
const pois = await this.listPois()
const sourceFailures: unknown[] = []
let pois: MuseumPoi[] = []
let spacePoints: MuseumPoi[] = []
try {
pois = await this.listPois()
} catch (error) {
sourceFailures.push(error)
}
try {
spacePoints = await this.listSpacePoints()
} catch (error) {
sourceFailures.push(error)
}
const searchablePois = mergeSearchablePois(pois, spacePoints)
if (sourceFailures.length && !searchablePois.length) {
throw new Error(poiDataLoadErrorMessage)
}
if (import.meta.env.DEV && sourceFailures.length) {
console.warn('[SGS 点位数据] 搜索使用部分可用数据', sourceFailures.map(toErrorMessage))
}
const normalizedKeyword = keyword.trim().toLowerCase()
if (!normalizedKeyword) return pois
if (!normalizedKeyword) return searchablePois
return pois.filter((poi) => toSearchText(poi).includes(normalizedKeyword))
return searchablePois.filter((poi) => toSearchText(poi).includes(normalizedKeyword))
}
async getLocationPreview(poiId: string) {
@@ -425,7 +598,7 @@ export class SgsSdkGuideRepository implements GuideRepository {
async checkDataIntegrity() {
const [floors, pois, diagnostics] = await Promise.all([
this.getFloors(),
this.listPois(),
this.searchPois(),
this.getMapDiagnostics()
])