对齐讲解业务单元静态数据口径

- 刷新讲解静态数据包并保留完整 outline 树
- 按后台顶层业务单元口径归并讲解点统计
- 更新讲解静态数据刷新操作文档

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
lyf
2026-07-03 02:05:02 +08:00
parent 0cd8d43684
commit c146beba9e
14 changed files with 12266 additions and 8796 deletions

View File

@@ -1,4 +1,6 @@
import type {
ExplainBusinessUnit,
ExplainGuideStop,
ExplainTrack,
GuideLocationResolution,
GuideLocationResolutionStatus,
@@ -17,6 +19,7 @@ import type {
GuideStaticContentPayload,
GuideStaticDataset,
GuideStaticExplainDataset,
GuideStaticExplainNavigationDataset,
GuideStaticExhibitPayload,
GuideStaticHallPayload,
GuideStaticOutlinePayload,
@@ -32,11 +35,20 @@ export interface GuideContentDataAdapterResult {
mediaAssets: MediaAsset[]
}
export interface GuideExplainNavigationAdapterResult {
halls: MuseumHall[]
guideStops: ExplainGuideStop[]
businessUnitsByHallId: Record<string, ExplainBusinessUnit[]>
}
export interface GuideExplainDataAdapterResult {
halls: MuseumHall[]
exhibits: MuseumExhibit[]
tracks: ExplainTrack[]
mediaAssets: MediaAsset[]
guideStops: ExplainGuideStop[]
businessUnitsByHallId: Record<string, ExplainBusinessUnit[]>
exhibitByStopId: Record<string, MuseumExhibit>
}
const stringifyId = (value: string | number | null | undefined) => (
@@ -239,6 +251,176 @@ const toHallModels = (
}
})
const toExplainBusinessUnitsByHallId = (
stops: ExplainGuideStop[],
topOutlines: Array<{ id: string; name: string; hallId: string; sort?: number }> = []
) => {
const unitsByHallId: Record<string, ExplainBusinessUnit[]> = {}
const stopGroupsByHall = new Map<string, Map<string, ExplainGuideStop[]>>()
const unitNameByKey = new Map<string, string>()
const unitOrderByKey = new Map<string, number>()
topOutlines.forEach((outline) => {
const hallGroups = stopGroupsByHall.get(outline.hallId) || new Map<string, ExplainGuideStop[]>()
if (!hallGroups.has(outline.id)) {
hallGroups.set(outline.id, [])
}
stopGroupsByHall.set(outline.hallId, hallGroups)
unitNameByKey.set(`${outline.hallId}:${outline.id}`, outline.name)
unitOrderByKey.set(`${outline.hallId}:${outline.id}`, outline.sort ?? 0)
})
stops
.slice()
.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
.forEach((stop) => {
const hallId = stop.hallId || ''
if (!hallId) return
const unitId = stop.outlineId || `ungrouped-${hallId}`
const unitName = stop.outlineName || '其他讲解'
const hallGroups = stopGroupsByHall.get(hallId) || new Map<string, ExplainGuideStop[]>()
const groupStops = hallGroups.get(unitId) || []
groupStops.push(stop)
hallGroups.set(unitId, groupStops)
stopGroupsByHall.set(hallId, hallGroups)
if (!unitNameByKey.has(`${hallId}:${unitId}`)) {
unitNameByKey.set(`${hallId}:${unitId}`, unitName)
}
if (!unitOrderByKey.has(`${hallId}:${unitId}`)) {
unitOrderByKey.set(`${hallId}:${unitId}`, stop.sort ?? 0)
}
})
stopGroupsByHall.forEach((groupMap, hallId) => {
unitsByHallId[hallId] = Array.from(groupMap.entries())
.map(([unitId, groupStops]) => ({
id: unitId,
name: unitNameByKey.get(`${hallId}:${unitId}`) || '其他讲解',
hallId,
guideStopCount: groupStops.length,
stops: groupStops
}))
.sort((a, b) => (
(unitOrderByKey.get(`${hallId}:${a.id}`) ?? 0) - (unitOrderByKey.get(`${hallId}:${b.id}`) ?? 0)
))
})
return unitsByHallId
}
const createExplainNavigationAdapter = (
dataset: GuideStaticExplainNavigationDataset,
hallGuideCounts = new Map<string, number>(),
poiBridge: {
hallToNavPoiId?: Record<string, GuideStaticPoiBridgeEntryPayload>
sgsPoiToNavPoiId?: Record<string, GuideStaticPoiBridgeEntryPayload>
} = {}
): GuideExplainNavigationAdapterResult => {
const visibleHalls = visibleHallsFrom(dataset.halls)
const visibleOutlines = visibleOutlinesFrom(dataset.outlines)
const visibleStops = visibleStopsFrom(dataset.guideStops)
const hallById = new Map(visibleHalls.map((hall) => [stringifyId(hall.id), hall]))
const outlineById = new Map(visibleOutlines.map((outline) => [stringifyId(outline.id), outline]))
const hallBridgeById = poiBridge.hallToNavPoiId || {}
const sgsPoiBridgeById = poiBridge.sgsPoiToNavPoiId || {}
const findHallForOutline = (outline: GuideStaticOutlinePayload | undefined) => {
let current = outline
let guard = 0
while (current && guard < 16) {
const parentId = stringifyId(current.parentId)
if (parentId && hallById.has(parentId)) {
return hallById.get(parentId)
}
current = parentId ? outlineById.get(parentId) : undefined
guard += 1
}
return undefined
}
const findHallForStop = (stop: GuideStaticStopPayload | undefined) => (
findHallForOutline(outlineById.get(stringifyId(stop?.outlineId)))
)
const findTopOutlineForStop = (stop: GuideStaticStopPayload | undefined) => {
let current = outlineById.get(stringifyId(stop?.outlineId))
let guard = 0
while (current && guard < 16) {
const parentId = stringifyId(current.parentId)
if (parentId && hallById.has(parentId)) {
return current
}
current = parentId ? outlineById.get(parentId) : undefined
guard += 1
}
return outlineById.get(stringifyId(stop?.outlineId))
}
const topOutlines = visibleOutlines.flatMap((outline) => {
const id = stringifyId(outline.id)
const parentId = stringifyId(outline.parentId)
if (!id || !parentId || !hallById.has(parentId)) return []
return [{
id,
name: outline.name || `业务单元 ${id}`,
hallId: parentId,
sort: typeof outline.sort === 'number' ? outline.sort : undefined
}]
})
const resolveHallLocation = (hall: GuideStaticHallPayload | undefined) => {
const hallId = stringifyId(hall?.id)
const sourcePoiId = stringifyId(hall?.poiId)
const bridgedHall = hallId ? toLocationResolution(hallBridgeById[hallId], 'exact', sourcePoiId) : undefined
if (bridgedHall) return bridgedHall
return sourcePoiId ? toLocationResolution(sgsPoiBridgeById[sourcePoiId], 'exact', sourcePoiId) : undefined
}
const halls = toHallModels(visibleHalls, hallGuideCounts, hallBridgeById, sgsPoiBridgeById)
const guideStops: ExplainGuideStop[] = visibleStops.map((stop) => {
const topOutline = findTopOutlineForStop(stop)
const hall = findHallForStop(stop)
const location = resolveHallLocation(hall)
const floorId = location?.floorId || formatFloorId(hall, undefined) || stop.floorCode || stringifyId(stop.floorId)
return {
id: stringifyId(stop.id) || '',
name: firstText(stop.name) || `讲解点 ${stringifyId(stop.id) || ''}`,
hallId: stringifyId(hall?.id),
hallName: hall?.name || undefined,
floorId,
targetType: 'STOP',
targetId: stringifyId(stop.id) || '',
coverImageUrl: resolveImage(stop.coverImageUrl, hall?.coverImageUrl, hall?.galleryUrls),
description: firstText(stop.name),
hasAudio: Boolean(stop.audioUrl),
poiId: location?.poiId,
outlineId: stringifyId(topOutline?.id),
outlineName: topOutline?.name || undefined,
sort: typeof stop.sort === 'number' ? stop.sort : undefined
}
})
return {
halls,
guideStops,
businessUnitsByHallId: toExplainBusinessUnitsByHallId(guideStops, topOutlines)
}
}
export const createGuideExplainNavigationAdapter = (
dataset: GuideStaticExplainNavigationDataset
): GuideExplainNavigationAdapterResult => createExplainNavigationAdapter(dataset)
export const createGuideExplainDataAdapter = (dataset: GuideStaticExplainDataset): GuideExplainDataAdapterResult => {
const visibleHalls = visibleHallsFrom(dataset.halls)
const visibleOutlines = visibleOutlinesFrom(dataset.outlines)
@@ -272,6 +454,36 @@ export const createGuideExplainDataAdapter = (dataset: GuideStaticExplainDataset
findHallForOutline(outlineById.get(stringifyId(stop?.outlineId)))
)
const findTopOutlineForStop = (stop: GuideStaticStopPayload | undefined) => {
let current = outlineById.get(stringifyId(stop?.outlineId))
let guard = 0
while (current && guard < 16) {
const parentId = stringifyId(current.parentId)
if (parentId && hallById.has(parentId)) {
return current
}
current = parentId ? outlineById.get(parentId) : undefined
guard += 1
}
return outlineById.get(stringifyId(stop?.outlineId))
}
const topOutlines = visibleOutlines.flatMap((outline) => {
const id = stringifyId(outline.id)
const parentId = stringifyId(outline.parentId)
if (!id || !parentId || !hallById.has(parentId)) return []
return [{
id,
name: outline.name || `业务单元 ${id}`,
hallId: parentId,
sort: typeof outline.sort === 'number' ? outline.sort : undefined
}]
})
const resolveHallLocation = (
hall: GuideStaticHallPayload | undefined,
status: Exclude<GuideLocationResolutionStatus, 'unavailable'>
@@ -313,6 +525,36 @@ export const createGuideExplainDataAdapter = (dataset: GuideStaticExplainDataset
const halls = toHallModels(visibleHalls, hallGuideCounts, hallBridgeById, sgsPoiBridgeById)
const guideStops: ExplainGuideStop[] = visibleStops.map((stop) => {
const topOutline = findTopOutlineForStop(stop)
const hall = findHallForStop(stop)
const preciseLocation = resolveSgsPoiLocation([stop.poiId], 'exact')
const location = preciseLocation || resolveHallLocation(hall, 'hallFallback')
const floorId = location?.floorId || formatFloorId(hall, undefined) || stop.floorCode || stringifyId(stop.floorId)
const stopGuide = visibleGuides.find((guide) => (
guide.targetType?.toUpperCase() === 'STOP'
&& stringifyId(guide.targetId) === stringifyId(stop.id)
))
return {
id: stringifyId(stop.id) || '',
name: firstText(stop.name, stopGuide?.title) || `讲解点 ${stringifyId(stop.id) || ''}`,
hallId: stringifyId(hall?.id),
hallName: hall?.name || undefined,
floorId,
targetType: 'STOP',
targetId: stringifyId(stop.id) || '',
coverImageUrl: resolveImage(stop.coverImageUrl, stopGuide?.mediaGallery, hall?.coverImageUrl, hall?.galleryUrls),
description: firstText(stopGuide?.standardText, stopGuide?.extendedText, stop.name),
hasAudio: stopGuide ? Boolean(primaryAudioUrl(stopGuide)) : Boolean(stop.audioUrl),
poiId: location?.poiId,
outlineId: stringifyId(topOutline?.id),
outlineName: topOutline?.name || undefined,
sort: typeof stop.sort === 'number' ? stop.sort : undefined
}
})
const businessUnitsByHallId = toExplainBusinessUnitsByHallId(guideStops, topOutlines)
const mediaAssets: MediaAsset[] = visibleGuides.map((guide) => {
const id = `media-${stringifyId(guide.id) || 'unknown'}`
const url = primaryAudioUrl(guide)
@@ -374,6 +616,13 @@ export const createGuideExplainDataAdapter = (dataset: GuideStaticExplainDataset
}
})
const exhibitByStopId = exhibits.reduce<Record<string, MuseumExhibit>>((result, exhibit) => {
if (exhibit.playTargetType === 'STOP' && exhibit.playTargetId) {
result[exhibit.playTargetId] = exhibit
}
return result
}, {})
const tracks: ExplainTrack[] = visibleGuides.map((guide) => {
const id = stringifyId(guide.id) || ''
const exhibitId = stringifyId(guide.exhibitId) || `guide-${id}`
@@ -409,7 +658,10 @@ export const createGuideExplainDataAdapter = (dataset: GuideStaticExplainDataset
halls,
exhibits,
tracks,
mediaAssets
mediaAssets,
guideStops,
businessUnitsByHallId,
exhibitByStopId
}
}