refactor: 重构数据层支持 SGS SDK 集成

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
lyf
2026-06-26 04:21:12 +08:00
parent a7413ee037
commit 15fbbec12d
16 changed files with 762 additions and 54 deletions

View File

@@ -4,6 +4,18 @@ import type {
GuideModelSource,
GuideRenderPoi
} from '@/domain/guideModel'
import {
dataSourceConfig
} from '@/config/dataSource'
import {
formatSgsFloorLabel,
toMuseumPoiFromSgs
} from '@/data/adapters/sgsSdkGuideAdapter'
import {
defaultSgsSdkApiProvider,
type SgsSdkApiProvider,
type SgsSdkFloorSummaryPayload
} from '@/data/providers/sgsSdkApiProvider'
import {
formatNavFloorLabel
} from '@/data/adapters/navAssetsAdapter'
@@ -24,6 +36,38 @@ const toRenderPoi = (poi: StaticNavPoiPayload): GuideRenderPoi => ({
sourceObjectName: poi.sourceObjectName
})
const toSgsRenderPoi = (
poi: ReturnType<typeof toMuseumPoiFromSgs>
): GuideRenderPoi => ({
id: poi.id,
name: poi.name,
floorId: poi.floorId,
primaryCategory: poi.primaryCategory.id,
primaryCategoryZh: poi.primaryCategory.label,
iconType: poi.primaryCategory.iconType || poi.primaryCategory.id,
positionGltf: poi.positionGltf,
sourceObjectName: poi.sourceObjectName
})
const trimTrailingSlash = (value: string) => value.replace(/\/+$/, '')
const resolveSgsAssetUrl = (url?: string | null) => {
const normalizedUrl = url?.trim()
if (!normalizedUrl) return ''
if (/^https?:\/\//i.test(normalizedUrl)) return normalizedUrl
if (!normalizedUrl.startsWith('/')) return normalizedUrl
const baseUrl = trimTrailingSlash(dataSourceConfig.sgsApiBaseUrl || dataSourceConfig.apiBaseUrl || '')
const origin = baseUrl.endsWith('/app-api')
? baseUrl.slice(0, -'/app-api'.length)
: baseUrl
return origin ? `${origin}${normalizedUrl}` : normalizedUrl
}
const sortSgsFloors = (floors: SgsSdkFloorSummaryPayload[]) => [...floors]
.sort((a, b) => Number(a.sortOrder || 0) - Number(b.sortOrder || 0))
export interface GuideModelRepository extends GuideModelSource {}
export class StaticGuideModelRepository implements GuideModelRepository {
@@ -63,4 +107,67 @@ export class StaticGuideModelRepository implements GuideModelRepository {
}
}
export const guideModelRepository = new StaticGuideModelRepository()
export class SgsSdkGuideModelRepository implements GuideModelRepository {
private floorsCache: SgsSdkFloorSummaryPayload[] | null = null
constructor(private readonly provider: SgsSdkApiProvider = defaultSgsSdkApiProvider) {}
async loadPackage(): Promise<GuideModelRenderPackage> {
const manifest = await this.provider.getManifest()
const floors = sortSgsFloors(manifest.floors)
this.floorsCache = floors
const bundles = await Promise.all(
floors.map((floor) => this.provider.getFloorBundle(String(floor.floorId)))
)
const floorAssets: GuideModelFloorAsset[] = floors
.map((floor, index) => {
const bundle = bundles[index]
const modelUrl = resolveSgsAssetUrl(bundle.model?.modelUrl || bundle.model?.fallbackModelUrl)
return {
floorId: String(floor.floorId),
label: formatSgsFloorLabel(floor.floorCode, floor.floorName),
order: Number(floor.sortOrder || 0),
modelUrl,
sharedModelAsset: false
}
})
.filter((asset) => asset.modelUrl)
const overviewFloorIndex = floors.findIndex((floor) => floor.floorCode === 'EXTERIOR')
const overviewModelUrl = floorAssets[overviewFloorIndex]?.modelUrl || floorAssets[0]?.modelUrl || ''
return {
overviewModelUrl,
floors: floorAssets
}
}
async loadFloorPois(floorId: string) {
const manifest = await this.provider.getManifest()
this.floorsCache = sortSgsFloors(manifest.floors)
const matchedFloor = this.floorsCache.find((floor) => (
String(floor.floorId) === floorId
|| floor.floorCode === floorId
|| formatSgsFloorLabel(floor.floorCode, floor.floorName) === floorId
))
if (!matchedFloor) return []
const pois = await this.provider.getFloorPois(String(matchedFloor.floorId))
return pois
.map((poi) => toSgsRenderPoi(toMuseumPoiFromSgs(poi, manifest.floors)))
.filter((poi) => Array.isArray(poi.positionGltf) && poi.positionGltf.length === 3)
}
}
export const createGuideModelRepository = (): GuideModelRepository => {
if (dataSourceConfig.mode === 'api' || dataSourceConfig.mode === 'sdk') {
return new SgsSdkGuideModelRepository()
}
return new StaticGuideModelRepository()
}
export const guideModelRepository = createGuideModelRepository()

View File

@@ -1,5 +1,8 @@
import type {
GuideDataIntegrityReport,
GuideFloorDetail,
GuideLocationPreview,
GuideMapDiagnostics,
GuideRouteReadiness,
MuseumFloor,
MuseumPoi
@@ -16,6 +19,20 @@ import {
defaultStaticNavAssetsProvider,
type StaticNavAssetsProvider
} from '@/data/providers/staticNavAssetsProvider'
import {
defaultSgsSdkApiProvider,
type SgsSdkApiProvider
} from '@/data/providers/sgsSdkApiProvider'
import {
buildSgsFloorAliases,
createGuideDataIntegrityReport,
toGuideFloorDetailFromSgs,
toGuideMapDiagnostics,
toLocationPreviewFromPoi,
toMuseumFloorFromSgs,
toMuseumPoiFromSgs,
toRouteReadinessFromSgsDiagnostics
} from '@/data/adapters/sgsSdkGuideAdapter'
const searchableCategories = new Set([
'touring_poi',
@@ -60,6 +77,9 @@ export interface GuideRepository {
searchPois(keyword?: string): Promise<MuseumPoi[]>
getLocationPreview(poiId: string): Promise<GuideLocationPreview | null>
getRouteReadiness(): Promise<GuideRouteReadiness>
getFloorDetail?(floorId: string): Promise<GuideFloorDetail | null>
getMapDiagnostics?(): Promise<GuideMapDiagnostics>
checkDataIntegrity?(): Promise<GuideDataIntegrityReport>
}
export class StaticGuideRepository implements GuideRepository {
@@ -120,6 +140,161 @@ export class StaticGuideRepository implements GuideRepository {
return this.provider.loadRouteReadiness()
.catch(() => NAV_ROUTE_READINESS)
}
async getFloorDetail(floorId: string) {
const floors = await this.getFloors()
const normalizedFloorId = this.normalizeFloorId(floorId)
const floor = floors.find((item) => item.id === normalizedFloorId)
return floor
? {
...floor,
warnings: []
}
: null
}
async getMapDiagnostics(): Promise<GuideMapDiagnostics> {
const [floors, pois] = await Promise.all([
this.getFloors(),
this.listPois()
])
return {
mapId: 'static',
mapName: '本地静态导览数据',
status: 'WARN',
floors: floors.map((floor) => ({
...floor,
poiCount: pois.filter((poi) => poi.floorId === floor.id).length,
warnings: []
})),
summary: {
floorCount: floors.length,
modelReadyFloorCount: floors.length,
poiCount: pois.length,
spaceCount: 0,
guideStopCount: 0,
navigablePlaceCount: 0,
routeNodeCount: 0,
routeEdgeCount: 0
},
warnings: ['static 模式仅提供本地导览资源基础检查']
}
}
async checkDataIntegrity() {
const [floors, pois, diagnostics] = await Promise.all([
this.getFloors(),
this.listPois(),
this.getMapDiagnostics()
])
return createGuideDataIntegrityReport(diagnostics, floors, pois)
}
}
export const guideRepository = new StaticGuideRepository()
export class SgsSdkGuideRepository implements GuideRepository {
private floorsCache: MuseumFloor[] | null = null
private poiCache: MuseumPoi[] | null = null
private floorAliases: Map<string, string> | null = null
constructor(private readonly provider: SgsSdkApiProvider = defaultSgsSdkApiProvider) {}
getAssetBaseUrl() {
return ''
}
async getFloors() {
if (this.floorsCache) return this.floorsCache
const manifest = await this.provider.getManifest()
this.floorAliases = buildSgsFloorAliases(manifest.floors)
this.floorsCache = [...manifest.floors]
.sort((a, b) => Number(a.sortOrder || 0) - Number(b.sortOrder || 0))
.map(toMuseumFloorFromSgs)
return this.floorsCache
}
normalizeFloorId(labelOrId: string) {
const normalized = labelOrId.trim()
const aliases = this.floorAliases
if (!aliases) return normalized
return aliases.get(normalized) || aliases.get(normalized.toLowerCase()) || normalized
}
async listPois() {
if (this.poiCache) return this.poiCache
const manifest = await this.provider.getManifest()
this.floorAliases = buildSgsFloorAliases(manifest.floors)
const poiGroups = await Promise.all(
manifest.floors.map((floor) => this.provider.getFloorPois(String(floor.floorId)))
)
this.poiCache = poiGroups
.flat()
.map((poi) => toMuseumPoiFromSgs(poi, manifest.floors))
.filter((poi) => poi.id && poi.name)
return this.poiCache
}
async getPoiById(id: string) {
const pois = await this.listPois()
return pois.find((poi) => poi.id === id) || null
}
async searchPois(keyword = '') {
const pois = await this.listPois()
const normalizedKeyword = keyword.trim().toLowerCase()
if (!normalizedKeyword) return pois
return pois.filter((poi) => toSearchText(poi).includes(normalizedKeyword))
}
async getLocationPreview(poiId: string) {
const poi = await this.getPoiById(poiId)
return poi ? toLocationPreviewFromPoi(poi) : null
}
async getRouteReadiness() {
return this.provider.getMapDiagnostics()
.then(toRouteReadinessFromSgsDiagnostics)
.catch(() => NAV_ROUTE_READINESS)
}
async getFloorDetail(floorId: string) {
await this.getFloors()
const normalizedFloorId = this.normalizeFloorId(floorId)
try {
const diagnostics = await this.provider.getFloorDiagnostics(normalizedFloorId)
return toGuideFloorDetailFromSgs(diagnostics)
} catch {
const manifest = await this.provider.getManifest()
const floor = manifest.floors.find((item) => String(item.floorId) === normalizedFloorId)
return floor ? toGuideFloorDetailFromSgs(floor) : null
}
}
async getMapDiagnostics() {
const [manifest, diagnostics] = await Promise.all([
this.provider.getManifest(),
this.provider.getMapDiagnostics()
])
return toGuideMapDiagnostics(diagnostics, manifest)
}
async checkDataIntegrity() {
const [floors, pois, diagnostics] = await Promise.all([
this.getFloors(),
this.listPois(),
this.getMapDiagnostics()
])
return createGuideDataIntegrityReport(diagnostics, floors, pois)
}
}

View File

@@ -5,9 +5,11 @@ import type {
SearchIndexItem
} from '@/domain/museum'
import {
guideRepository,
type GuideRepository
} from '@/repositories/GuideRepository'
import {
guideRepository
} from '@/repositories/createGuideRepository'
import {
staticMuseumContentProvider,
type MuseumContentProvider