接入H5讲解后端数据闭环
This commit is contained in:
213
src/data/providers/backendExplainContentProvider.ts
Normal file
213
src/data/providers/backendExplainContentProvider.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import type {
|
||||
ExplainTrack,
|
||||
MediaAsset,
|
||||
MuseumExhibit,
|
||||
MuseumHall
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
dataSourceConfig
|
||||
} from '@/config/dataSource'
|
||||
import type {
|
||||
ExplainContentProvider
|
||||
} from '@/data/providers/staticMuseumContentProvider'
|
||||
import {
|
||||
toBackendExplainTrack,
|
||||
toBackendHall,
|
||||
toBackendMediaAsset,
|
||||
toBackendMuseumExhibit,
|
||||
type BackendExhibit
|
||||
} from '@/data/adapters/backendExplainDataAdapter'
|
||||
|
||||
interface CommonResult<T> {
|
||||
code: number
|
||||
msg?: string
|
||||
data?: T
|
||||
}
|
||||
|
||||
const normalizeBaseUrl = (baseUrl: string) => baseUrl.replace(/\/+$/, '')
|
||||
|
||||
const resolveAppApiBaseUrl = () => {
|
||||
const baseUrl = normalizeBaseUrl(dataSourceConfig.apiBaseUrl)
|
||||
return baseUrl.endsWith('/app-api') ? baseUrl : `${baseUrl}/app-api`
|
||||
}
|
||||
|
||||
const parseJsonPayload = <T>(payload: unknown): T => {
|
||||
if (typeof payload === 'string') {
|
||||
return JSON.parse(payload) as T
|
||||
}
|
||||
|
||||
return payload as T
|
||||
}
|
||||
|
||||
const requestJson = <T>(url: string): Promise<T> => new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url,
|
||||
method: 'GET',
|
||||
timeout: 10000,
|
||||
success: (response) => {
|
||||
const statusCode = Number(response.statusCode || 0)
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
reject(new Error(`后端讲解接口请求失败: ${statusCode}`))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
resolve(parseJsonPayload<T>(response.data))
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
},
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
|
||||
const normalizeKeyword = (keyword = '') => keyword.trim()
|
||||
|
||||
export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
private readonly searchCache = new Map<string, MuseumExhibit[]>()
|
||||
private readonly searchInflight = new Map<string, Promise<MuseumExhibit[]>>()
|
||||
private readonly detailCache = new Map<string, MuseumExhibit>()
|
||||
private readonly detailInflight = new Map<string, Promise<MuseumExhibit | null>>()
|
||||
|
||||
constructor(private readonly fallbackProvider: ExplainContentProvider) {}
|
||||
|
||||
private async requestSearch(keyword = '') {
|
||||
const normalizedKeyword = normalizeKeyword(keyword)
|
||||
const cacheKey = normalizedKeyword.toLowerCase()
|
||||
const cached = this.searchCache.get(cacheKey)
|
||||
if (cached) return cached
|
||||
|
||||
const inflight = this.searchInflight.get(cacheKey)
|
||||
if (inflight) return inflight
|
||||
|
||||
const promise = (async () => {
|
||||
const params = new URLSearchParams({ keyword: normalizedKeyword })
|
||||
const url = `${resolveAppApiBaseUrl()}/gis/exhibit/search?${params.toString()}`
|
||||
const response = await requestJson<CommonResult<BackendExhibit[]>>(url)
|
||||
if (response.code !== 0 || !Array.isArray(response.data)) {
|
||||
throw new Error(response.msg || '后端讲解搜索失败')
|
||||
}
|
||||
|
||||
const fallbackHalls = await this.safeFallbackHalls()
|
||||
const fallbackHallById = new Map(fallbackHalls.map((hall) => [hall.id, hall]))
|
||||
const exhibits = response.data.map((item) => toBackendMuseumExhibit(
|
||||
item,
|
||||
fallbackHallById.get(String(item.hallId || '')),
|
||||
{ includeDetail: false }
|
||||
))
|
||||
|
||||
this.searchCache.set(cacheKey, exhibits)
|
||||
return exhibits
|
||||
})()
|
||||
|
||||
this.searchInflight.set(cacheKey, promise)
|
||||
try {
|
||||
return await promise
|
||||
} finally {
|
||||
this.searchInflight.delete(cacheKey)
|
||||
}
|
||||
}
|
||||
|
||||
private async requestDetail(id: string) {
|
||||
const cached = this.detailCache.get(id)
|
||||
if (cached) return cached
|
||||
|
||||
const inflight = this.detailInflight.get(id)
|
||||
if (inflight) return inflight
|
||||
|
||||
const promise = (async () => {
|
||||
const params = new URLSearchParams({ id })
|
||||
const url = `${resolveAppApiBaseUrl()}/gis/exhibit/get?${params.toString()}`
|
||||
const response = await requestJson<CommonResult<BackendExhibit>>(url)
|
||||
if (response.code !== 0 || !response.data) {
|
||||
throw new Error(response.msg || '后端讲解详情失败')
|
||||
}
|
||||
|
||||
const fallbackHalls = await this.safeFallbackHalls()
|
||||
const fallbackHall = fallbackHalls.find((hall) => hall.id === String(response.data?.hallId || ''))
|
||||
const detail = toBackendMuseumExhibit(response.data, fallbackHall, { includeDetail: true })
|
||||
this.detailCache.set(id, detail)
|
||||
return detail
|
||||
})().catch(async (error) => {
|
||||
console.warn('后端讲解详情加载失败,将使用静态讲解兜底:', error)
|
||||
return this.fallbackProvider.listExhibits()
|
||||
.then((items) => items.find((item) => item.id === id) || null)
|
||||
.catch(() => null)
|
||||
})
|
||||
|
||||
this.detailInflight.set(id, promise)
|
||||
try {
|
||||
return await promise
|
||||
} finally {
|
||||
this.detailInflight.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
private async safeFallbackHalls() {
|
||||
return this.fallbackProvider.listHalls().catch(() => [])
|
||||
}
|
||||
|
||||
async listExplainExhibits() {
|
||||
try {
|
||||
return await this.requestSearch('')
|
||||
} catch (error) {
|
||||
console.warn('后端讲解列表加载失败,将使用静态讲解兜底:', error)
|
||||
return this.fallbackProvider.listExplainExhibits()
|
||||
}
|
||||
}
|
||||
|
||||
listExhibits() {
|
||||
return this.listExplainExhibits()
|
||||
}
|
||||
|
||||
getExhibitById(id: string) {
|
||||
return this.requestDetail(id)
|
||||
}
|
||||
|
||||
async listHalls() {
|
||||
try {
|
||||
const exhibits = await this.requestSearch('')
|
||||
const fallbackHalls = await this.safeFallbackHalls()
|
||||
const fallbackHallById = new Map(fallbackHalls.map((hall) => [hall.id, hall]))
|
||||
const exhibitsByHallId = new Map<string, MuseumExhibit[]>()
|
||||
|
||||
exhibits.forEach((exhibit) => {
|
||||
const hallId = exhibit.hallId || 'unknown'
|
||||
const items = exhibitsByHallId.get(hallId) || []
|
||||
items.push(exhibit)
|
||||
exhibitsByHallId.set(hallId, items)
|
||||
})
|
||||
|
||||
return Array.from(exhibitsByHallId.entries()).map(([hallId, hallExhibits]) => (
|
||||
toBackendHall(hallId, hallExhibits, fallbackHallById.get(hallId))
|
||||
))
|
||||
} catch (error) {
|
||||
console.warn('后端展厅聚合加载失败,将使用静态展厅兜底:', error)
|
||||
return this.fallbackProvider.listHalls()
|
||||
}
|
||||
}
|
||||
|
||||
async listTracks() {
|
||||
const exhibits = await this.listExplainExhibits()
|
||||
return exhibits.map(toBackendExplainTrack)
|
||||
}
|
||||
|
||||
async getMediaById(id: string) {
|
||||
const normalizedId = id.replace(/^media-/, '')
|
||||
const cachedDetail = Array.from(this.detailCache.values()).find((exhibit) => (
|
||||
exhibit.id === normalizedId || exhibit.guideContentId === normalizedId
|
||||
))
|
||||
if (cachedDetail) return toBackendMediaAsset(cachedDetail)
|
||||
|
||||
const detail = await this.requestDetail(normalizedId).catch(() => null)
|
||||
return detail ? toBackendMediaAsset(detail) : null
|
||||
}
|
||||
|
||||
async getMediaForExplainTrack(trackId: string) {
|
||||
return this.getMediaById(`media-${trackId.replace(/^track-/, '')}`)
|
||||
}
|
||||
|
||||
searchExplainExhibits(keyword = '') {
|
||||
return this.requestSearch(keyword)
|
||||
}
|
||||
}
|
||||
@@ -25,14 +25,19 @@ import {
|
||||
type SgsSceneExhibitMock,
|
||||
type SgsSceneSpaceMock
|
||||
} from '@/data/mock/sgsSceneExplainData'
|
||||
import {
|
||||
BackendExplainContentProvider
|
||||
} from '@/data/providers/backendExplainContentProvider'
|
||||
|
||||
export interface MuseumContentProvider {
|
||||
listExhibits(): Promise<MuseumExhibit[]>
|
||||
getExhibitById?(id: string): Promise<MuseumExhibit | null>
|
||||
listHalls(): Promise<MuseumHall[]>
|
||||
}
|
||||
|
||||
export interface ExplainContentProvider extends MuseumContentProvider {
|
||||
listExplainExhibits(): Promise<MuseumExhibit[]>
|
||||
searchExplainExhibits?(keyword?: string): Promise<MuseumExhibit[]>
|
||||
listTracks(): Promise<ExplainTrack[]>
|
||||
getMediaById(id: string): Promise<MediaAsset | null>
|
||||
getMediaForExplainTrack(trackId: string): Promise<MediaAsset | null>
|
||||
@@ -232,7 +237,7 @@ export const createMuseumContentProvider = (): ExplainContentProvider => {
|
||||
}
|
||||
|
||||
if (isGuideContentRemoteMode()) {
|
||||
return new RemoteGuideContentProvider()
|
||||
return new BackendExplainContentProvider(new StaticGuideContentProvider())
|
||||
}
|
||||
|
||||
if (isGuideContentMockMode()) {
|
||||
|
||||
Reference in New Issue
Block a user