import type { ExplainBusinessUnit, ExplainGuideStop, ExplainGuideStopPage, ExplainTrack, MuseumExhibit, MuseumHall, SearchIndexItem } from '@/domain/museum' import { mediaRepository, type MediaRepository } from '@/repositories/MediaRepository' import { museumContentRepository, type MuseumContentRepository } from '@/repositories/MuseumContentRepository' import { explainContentProvider, type ExplainContentProvider } from '@/data/providers/staticMuseumContentProvider' export interface ExplainRepository { listExplainExhibits(): Promise listExplainExhibitsByHall(hallId: string): Promise listExhibits(): Promise getExhibitById(id: string): Promise listHalls(): Promise getHallById(id: string): Promise listGuideStopsByHall(hallId: string): Promise listGuideStopsPageByHall(hallId: string, pageNo: number, pageSize: number): Promise listGuideStopsByBusinessUnit(hallId: string, unitId: string): Promise listTemporaryBusinessUnitsByHall(hallId: string): Promise listTracks(): Promise getTrackByExhibitId(exhibitId: string): Promise searchExplain(keyword?: string): Promise } const trackTitleFor = (exhibit: MuseumExhibit) => `${exhibit.name}讲解` export class DefaultExplainRepository implements ExplainRepository { constructor( private readonly content: MuseumContentRepository = museumContentRepository, private readonly media: MediaRepository = mediaRepository, private readonly explainContent: ExplainContentProvider = explainContentProvider ) {} listExplainExhibits() { return this.explainContent.listExplainExhibits() } async listExplainExhibitsByHall(hallId: string) { if (this.explainContent.listExplainExhibitsByHall) { return this.explainContent.listExplainExhibitsByHall(hallId) } const exhibits = await this.listExplainExhibits() return exhibits.filter((exhibit) => exhibit.hallId === hallId) } listExhibits() { return this.content.listExhibits() } getExhibitById(id: string) { return this.explainContent.getExhibitById?.(id) || this.content.getExhibitById(id) } listHalls() { return this.explainContent.listHalls() } async getHallById(id: string) { const halls = await this.explainContent.listHalls() return halls.find((hall) => hall.id === id || hall.poiId === id) || null } async listGuideStopsByHall(hallId: string) { const pageSize = 100 const items: ExplainGuideStop[] = [] const seen = new Set() for (let pageNo = 1; ; pageNo += 1) { const page = await this.listGuideStopsPageByHall(hallId, pageNo, pageSize) page.items.forEach((item) => { if (!seen.has(item.id)) { seen.add(item.id) items.push(item) } }) if (!page.hasMore || items.length >= page.total) return items } } async listGuideStopsPageByHall(hallId: string, pageNo: number, pageSize: number) { if (this.explainContent.listGuideStopsPageByHall) { return this.explainContent.listGuideStopsPageByHall(hallId, pageNo, pageSize) } const items = await this.explainContent.listGuideStopsByHall?.(hallId) || [] const normalizedPageNo = Math.max(1, Math.floor(pageNo) || 1) const normalizedPageSize = Math.max(1, Math.floor(pageSize) || 20) const offset = (normalizedPageNo - 1) * normalizedPageSize return { items: items.slice(offset, offset + normalizedPageSize), total: items.length, pageNo: normalizedPageNo, pageSize: normalizedPageSize, hasMore: offset + normalizedPageSize < items.length } } async listGuideStopsByBusinessUnit(hallId: string, unitId: string) { if (this.explainContent.listGuideStopsByBusinessUnit) { return this.explainContent.listGuideStopsByBusinessUnit(hallId, unitId) } const units = await this.listTemporaryBusinessUnitsByHall(hallId) return units.find((unit) => unit.id === unitId)?.stops || [] } async listTemporaryBusinessUnitsByHall(hallId: string) { if (this.explainContent.listTemporaryBusinessUnitsByHall) { return this.explainContent.listTemporaryBusinessUnitsByHall(hallId) } return [] } async listTracks() { const tracks = await this.explainContent.listTracks() if (tracks.length) return tracks const exhibits = await this.content.listExhibits() return Promise.all(exhibits.map(async (exhibit): Promise => { const media = await this.media.getMediaForExplainTrack(exhibit.id) return { id: `track-${exhibit.id}`, exhibitId: exhibit.id, hallId: exhibit.hallId, title: trackTitleFor(exhibit), summary: exhibit.description, mediaId: media?.id, coverImage: exhibit.image, poiId: exhibit.poiId, floorId: exhibit.floorId, available: media?.available === true } })) } async getTrackByExhibitId(exhibitId: string) { const detail = await this.getExhibitById(exhibitId).catch(() => null) if (detail) { return { id: `track-${detail.guideContentId || detail.id}`, exhibitId: detail.id, hallId: detail.hallId, title: detail.guideTitle || trackTitleFor(detail), summary: detail.guideText || detail.description, mediaId: detail.audioUrl ? `media-${detail.guideContentId || detail.id}` : undefined, coverImage: detail.image, poiId: detail.poiId, floorId: detail.floorId, available: Boolean(detail.audioUrl) || detail.audioAvailable === true, playTargetType: detail.playTargetType, playTargetId: detail.playTargetId } } const tracks = await this.listTracks() return tracks.find((track) => track.exhibitId === exhibitId) || null } async searchExplain(keyword = '') { if (this.explainContent.searchExplainExhibits) { const exhibits = await this.explainContent.searchExplainExhibits(keyword) return exhibits.map((exhibit) => ({ id: exhibit.id, name: exhibit.name, desc: `${exhibit.hallName || '展品讲解'} · ${exhibit.floorLabel || ''}`.trim(), type: 'exhibit', exhibitId: exhibit.id, hallId: exhibit.hallId, poiId: exhibit.poiId, floorId: exhibit.floorId })) } const normalizedKeyword = keyword.trim().toLowerCase() const [exhibits, halls] = await Promise.all([ this.explainContent.listExplainExhibits(), this.explainContent.listHalls() ]) const exhibitItems = exhibits .filter((exhibit) => !normalizedKeyword || [ exhibit.name, exhibit.hallName, exhibit.floorLabel, ...(exhibit.tags || []) ].filter(Boolean).join(' ').toLowerCase().includes(normalizedKeyword)) .map((exhibit) => ({ id: exhibit.id, name: exhibit.name, desc: `${exhibit.hallName || '展品讲解'} · ${exhibit.floorLabel || ''}`.trim(), type: 'exhibit', exhibitId: exhibit.id, hallId: exhibit.hallId, poiId: exhibit.poiId, floorId: exhibit.floorId })) const hallItems = halls .filter((hall) => !normalizedKeyword || [ hall.name, hall.floorLabel, hall.description ].filter(Boolean).join(' ').toLowerCase().includes(normalizedKeyword)) .map((hall) => ({ id: hall.id, name: hall.name, desc: `${hall.floorLabel || ''} · ${hall.exhibitCount || 0}个讲解内容`.trim(), type: 'hall', hallId: hall.id, poiId: hall.poiId, floorId: hall.floorId })) return [...exhibitItems, ...hallItems] } } export const explainRepository = new DefaultExplainRepository()