@@ -53,7 +53,7 @@ const requestJson = <T>(url: string): Promise<T> => new Promise((resolve, reject
|
||||
uni.request({
|
||||
url,
|
||||
method: 'GET',
|
||||
timeout: dataSourceConfig.sgsSdkTimeoutMs,
|
||||
timeout: dataSourceConfig.explainApiTimeoutMs,
|
||||
success: (response) => {
|
||||
const statusCode = Number(response.statusCode || 0)
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
@@ -76,6 +76,13 @@ const normalizeKeyword = (keyword = '') => keyword.trim()
|
||||
const catalogLang = () => dataSourceConfig.audioLanguage
|
||||
|
||||
const cacheKey = (...parts: string[]) => [catalogLang(), ...parts].join(':')
|
||||
const CATALOG_CACHE_TTL_MS = 60_000
|
||||
const CATALOG_CACHE_MAX_ENTRIES = 80
|
||||
|
||||
interface TimedCacheEntry<T> {
|
||||
value: T
|
||||
expiresAt: number
|
||||
}
|
||||
|
||||
const requireArrayData = <T>(response: CommonResult<T[]>, message: string) => {
|
||||
if (response.code !== 0) {
|
||||
@@ -86,22 +93,45 @@ const requireArrayData = <T>(response: CommonResult<T[]>, message: string) => {
|
||||
}
|
||||
|
||||
export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
private readonly explainExhibitCache = new Map<string, MuseumExhibit[]>()
|
||||
private readonly explainExhibitCache = new Map<string, TimedCacheEntry<MuseumExhibit[]>>()
|
||||
private readonly explainExhibitInflight = new Map<string, Promise<MuseumExhibit[]>>()
|
||||
private readonly detailCache = new Map<string, MuseumExhibit>()
|
||||
private readonly hallListCache = new Map<string, MuseumHall[]>()
|
||||
private readonly detailCache = new Map<string, TimedCacheEntry<MuseumExhibit>>()
|
||||
private readonly hallListCache = new Map<string, TimedCacheEntry<MuseumHall[]>>()
|
||||
private readonly hallListInflight = new Map<string, Promise<MuseumHall[]>>()
|
||||
private readonly guideStopCache = new Map<string, ExplainGuideStop[]>()
|
||||
private readonly guideStopCache = new Map<string, TimedCacheEntry<ExplainGuideStop[]>>()
|
||||
private readonly guideStopInflight = new Map<string, Promise<ExplainGuideStop[]>>()
|
||||
private readonly outlineCache = new Map<string, BackendCatalogOutlineItem[]>()
|
||||
private readonly outlineCache = new Map<string, TimedCacheEntry<BackendCatalogOutlineItem[]>>()
|
||||
private readonly outlineInflight = new Map<string, Promise<BackendCatalogOutlineItem[]>>()
|
||||
private readonly businessUnitCache = new Map<string, ExplainBusinessUnit[]>()
|
||||
private readonly businessUnitCache = new Map<string, TimedCacheEntry<ExplainBusinessUnit[]>>()
|
||||
private readonly businessUnitInflight = new Map<string, Promise<ExplainBusinessUnit[]>>()
|
||||
|
||||
constructor(
|
||||
private readonly fallbackProvider?: ExplainContentProvider
|
||||
) {}
|
||||
|
||||
private getCached<T>(cache: Map<string, TimedCacheEntry<T>>, key: string): T | null {
|
||||
const entry = cache.get(key)
|
||||
if (!entry) return null
|
||||
if (entry.expiresAt <= Date.now()) {
|
||||
cache.delete(key)
|
||||
return null
|
||||
}
|
||||
// Read promotes LRU position.
|
||||
cache.delete(key)
|
||||
cache.set(key, entry)
|
||||
return entry.value
|
||||
}
|
||||
|
||||
private setCached<T>(cache: Map<string, TimedCacheEntry<T>>, key: string, value: T) {
|
||||
cache.delete(key)
|
||||
cache.set(key, { value, expiresAt: Date.now() + CATALOG_CACHE_TTL_MS })
|
||||
while (cache.size > CATALOG_CACHE_MAX_ENTRIES) {
|
||||
const oldestKey = cache.keys().next().value
|
||||
if (!oldestKey) break
|
||||
cache.delete(oldestKey)
|
||||
}
|
||||
}
|
||||
|
||||
private async safeFallbackHalls() {
|
||||
if (!this.isStaticFallbackEnabled()) return []
|
||||
|
||||
@@ -128,7 +158,7 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
|
||||
private async requestCatalogHalls() {
|
||||
const key = cacheKey('halls')
|
||||
const cached = this.hallListCache.get(key)
|
||||
const cached = this.getCached(this.hallListCache, key)
|
||||
if (cached) return cached
|
||||
|
||||
const inflight = this.hallListInflight.get(key)
|
||||
@@ -149,7 +179,7 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
))
|
||||
.filter((hall) => hall.id)
|
||||
|
||||
this.hallListCache.set(key, halls)
|
||||
this.setCached(this.hallListCache, key, halls)
|
||||
return halls
|
||||
})()
|
||||
|
||||
@@ -166,7 +196,7 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
if (!normalizedHallId) return []
|
||||
|
||||
const key = cacheKey('hall', normalizedHallId, 'outlines')
|
||||
const cached = this.outlineCache.get(key)
|
||||
const cached = this.getCached(this.outlineCache, key)
|
||||
if (cached) return cached
|
||||
|
||||
const inflight = this.outlineInflight.get(key)
|
||||
@@ -180,7 +210,7 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
const url = `${resolveAppApiBaseUrl()}/gis/guide/catalog/halls/${encodeURIComponent(normalizedHallId)}/outlines?${params.toString()}`
|
||||
const response = await requestJson<CommonResult<BackendCatalogOutlineItem[]>>(url)
|
||||
const outlines = requireArrayData(response, '讲解单元目录加载失败')
|
||||
this.outlineCache.set(key, outlines)
|
||||
this.setCached(this.outlineCache, key, outlines)
|
||||
return outlines
|
||||
})()
|
||||
|
||||
@@ -197,7 +227,7 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
if (!normalizedHallId) return []
|
||||
|
||||
const key = cacheKey('hall', normalizedHallId, 'stops')
|
||||
const cached = this.guideStopCache.get(key)
|
||||
const cached = this.getCached(this.guideStopCache, key)
|
||||
if (cached) return cached
|
||||
|
||||
const inflight = this.guideStopInflight.get(key)
|
||||
@@ -226,7 +256,7 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
})
|
||||
.filter(Boolean) as ExplainGuideStop[]
|
||||
|
||||
this.guideStopCache.set(key, stops)
|
||||
this.setCached(this.guideStopCache, key, stops)
|
||||
return stops
|
||||
})()
|
||||
|
||||
@@ -244,13 +274,13 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
if (!normalizedOutlineId) return []
|
||||
|
||||
const hallStopsKey = cacheKey('hall', normalizedHallId, 'stops')
|
||||
const cachedHallStops = this.guideStopCache.get(hallStopsKey)
|
||||
const cachedHallStops = this.getCached(this.guideStopCache, hallStopsKey)
|
||||
if (cachedHallStops) {
|
||||
return cachedHallStops.filter((stop) => stop.outlineId === normalizedOutlineId)
|
||||
}
|
||||
|
||||
const key = cacheKey('outline', normalizedOutlineId, 'stops')
|
||||
const cached = this.guideStopCache.get(key)
|
||||
const cached = this.getCached(this.guideStopCache, key)
|
||||
if (cached) return cached
|
||||
|
||||
const inflight = this.guideStopInflight.get(key)
|
||||
@@ -276,7 +306,7 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
: null))
|
||||
.filter(Boolean) as ExplainGuideStop[]
|
||||
|
||||
this.guideStopCache.set(key, stops)
|
||||
this.setCached(this.guideStopCache, key, stops)
|
||||
return stops
|
||||
})()
|
||||
|
||||
@@ -290,7 +320,7 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
|
||||
private async requestCatalogExhibits() {
|
||||
const key = cacheKey('exhibits')
|
||||
const cached = this.explainExhibitCache.get(key)
|
||||
const cached = this.getCached(this.explainExhibitCache, key)
|
||||
if (cached) return cached
|
||||
|
||||
const inflight = this.explainExhibitInflight.get(key)
|
||||
@@ -303,9 +333,9 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
return stops.map((stop) => toCatalogMuseumExhibitFromStop(stop, hall))
|
||||
}))
|
||||
const exhibits = stopsByHall.flat()
|
||||
this.explainExhibitCache.set(key, exhibits)
|
||||
this.setCached(this.explainExhibitCache, key, exhibits)
|
||||
exhibits.forEach((exhibit) => {
|
||||
this.detailCache.set(exhibit.id, exhibit)
|
||||
this.setCached(this.detailCache, exhibit.id, exhibit)
|
||||
})
|
||||
return exhibits
|
||||
})()
|
||||
@@ -350,26 +380,17 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
if (!normalizedHallId) return []
|
||||
|
||||
const key = cacheKey('hall', normalizedHallId, 'units')
|
||||
const cached = this.businessUnitCache.get(key)
|
||||
const cached = this.getCached(this.businessUnitCache, key)
|
||||
if (cached) return cached
|
||||
|
||||
const inflight = this.businessUnitInflight.get(key)
|
||||
if (inflight) return inflight
|
||||
|
||||
const promise = (async () => {
|
||||
const [outlines, stops] = await Promise.all([
|
||||
this.requestCatalogOutlinesByHall(normalizedHallId),
|
||||
this.requestCatalogStopsByHall(normalizedHallId)
|
||||
])
|
||||
const units = toCatalogBusinessUnits(normalizedHallId, outlines, stops)
|
||||
|
||||
await Promise.all(units.map(async (unit) => {
|
||||
if (unit.stops.length) return
|
||||
unit.stops = await this.requestCatalogStopsByOutline(normalizedHallId, unit.id).catch(() => [])
|
||||
unit.guideStopCount = unit.stops.length || unit.guideStopCount
|
||||
}))
|
||||
|
||||
this.businessUnitCache.set(key, units)
|
||||
const outlines = await this.requestCatalogOutlinesByHall(normalizedHallId)
|
||||
// The outline response carries authoritative counts. Stops are loaded only after unit selection.
|
||||
const units = toCatalogBusinessUnits(normalizedHallId, outlines, [])
|
||||
this.setCached(this.businessUnitCache, key, units)
|
||||
return units
|
||||
})().catch((error) => {
|
||||
return this.fallbackOrThrow(error, '讲解单元目录加载失败', async () => (
|
||||
@@ -395,6 +416,19 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
}
|
||||
}
|
||||
|
||||
async listExplainExhibitsByHall(hallId: string) {
|
||||
const [halls, stops] = await Promise.all([
|
||||
this.listHalls(),
|
||||
this.listGuideStopsByHall(hallId)
|
||||
])
|
||||
const hall = halls.find((item) => item.id === hallId) || null
|
||||
return stops.map((stop) => toCatalogMuseumExhibitFromStop(stop, hall || undefined))
|
||||
}
|
||||
|
||||
listGuideStopsByBusinessUnit(hallId: string, unitId: string) {
|
||||
return this.requestCatalogStopsByOutline(hallId, unitId)
|
||||
}
|
||||
|
||||
listExhibits() {
|
||||
return this.listExplainExhibits()
|
||||
}
|
||||
@@ -403,7 +437,7 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
const normalizedId = id.trim()
|
||||
if (!normalizedId) return null
|
||||
|
||||
const cached = this.detailCache.get(normalizedId)
|
||||
const cached = this.getCached(this.detailCache, normalizedId)
|
||||
if (cached) return cached
|
||||
|
||||
const catalogExhibits = await this.listExplainExhibits().catch(() => [])
|
||||
@@ -414,7 +448,7 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
|| exhibit.linkedExhibits?.some((linked) => linked.id === normalizedId)
|
||||
))
|
||||
if (matched) {
|
||||
this.detailCache.set(normalizedId, matched)
|
||||
this.setCached(this.detailCache, normalizedId, matched)
|
||||
return matched
|
||||
}
|
||||
|
||||
|
||||
@@ -41,8 +41,10 @@ export interface MuseumContentProvider {
|
||||
|
||||
export interface ExplainContentProvider extends MuseumContentProvider {
|
||||
listExplainExhibits(): Promise<MuseumExhibit[]>
|
||||
listExplainExhibitsByHall?(hallId: string): Promise<MuseumExhibit[]>
|
||||
searchExplainExhibits?(keyword?: string): Promise<MuseumExhibit[]>
|
||||
listGuideStopsByHall?(hallId: string): Promise<ExplainGuideStop[]>
|
||||
listGuideStopsByBusinessUnit?(hallId: string, unitId: string): Promise<ExplainGuideStop[]>
|
||||
listTemporaryBusinessUnitsByHall?(hallId: string): Promise<ExplainBusinessUnit[]>
|
||||
listTracks(): Promise<ExplainTrack[]>
|
||||
getMediaById(id: string): Promise<MediaAsset | null>
|
||||
|
||||
Reference in New Issue
Block a user