feat: 临时改造讲解页方案 B 链路
接入展厅列表、展厅讲解点分组生成临时业务单元,并迁移讲解详情播放正文到新接口链路。
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import type {
|
||||
AudioPlayTargetType,
|
||||
ExplainBusinessUnit,
|
||||
ExplainGuideStop,
|
||||
ExplainTrack,
|
||||
MediaAsset,
|
||||
MuseumExhibit,
|
||||
@@ -17,10 +19,15 @@ import {
|
||||
import {
|
||||
audioPlayInfoRepository,
|
||||
audioReasonToText,
|
||||
type AudioLanguage,
|
||||
type AudioPlayInfo,
|
||||
type AudioPlayInfoRepository,
|
||||
type GuideStopInfoRequest,
|
||||
type AudioTextInfo
|
||||
} from '@/repositories/AudioPlayInfoRepository'
|
||||
import type {
|
||||
GuideStopInfo
|
||||
} from '@/data/adapters/guideStopInfoAdapter'
|
||||
import {
|
||||
publishedExhibitAudioRepository,
|
||||
type PublishedExhibitAudioRepository
|
||||
@@ -38,6 +45,19 @@ export interface ExplainAudioSelection {
|
||||
playInfo?: AudioPlayInfo
|
||||
}
|
||||
|
||||
export interface ExplainDetailEntryRequest {
|
||||
exhibitId: string
|
||||
targetType?: AudioPlayTargetType
|
||||
targetId?: string
|
||||
}
|
||||
|
||||
export interface ExplainTextSelection {
|
||||
exhibit: MuseumExhibit
|
||||
textInfo: AudioTextInfo
|
||||
available: boolean
|
||||
unavailableMessage?: string
|
||||
}
|
||||
|
||||
export interface ExhibitDetailAudioOptions {
|
||||
enrichAudio?: boolean
|
||||
includeText?: boolean
|
||||
@@ -76,6 +96,110 @@ export class ExplainUseCase {
|
||||
return { targetType, targetId }
|
||||
}
|
||||
|
||||
private resolveStopInfoAudioTarget(stopInfo: GuideStopInfo) {
|
||||
return {
|
||||
targetType: stopInfo.playTargetType || stopInfo.targetType,
|
||||
targetId: stopInfo.playTargetId || stopInfo.targetId
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveDetailEntryTarget(request: ExplainDetailEntryRequest): Promise<Required<GuideStopInfoRequest>> {
|
||||
const lang = dataSourceConfig.audioLanguage as AudioLanguage
|
||||
if (request.targetType && request.targetId) {
|
||||
return {
|
||||
targetType: request.targetType,
|
||||
targetId: request.targetId,
|
||||
lang
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
targetType: 'ITEM',
|
||||
targetId: request.exhibitId,
|
||||
lang
|
||||
}
|
||||
}
|
||||
|
||||
private toExhibitFromStopInfo(
|
||||
stopInfo: GuideStopInfo,
|
||||
fallback?: MuseumExhibit | null
|
||||
): MuseumExhibit {
|
||||
const linkedPrimary = stopInfo.linkedExhibits[0]
|
||||
const coverImage = stopInfo.coverImageUrl || fallback?.image
|
||||
const description = stopInfo.description || fallback?.description || '该讲解暂无简介。'
|
||||
const audioTarget = this.resolveStopInfoAudioTarget(stopInfo)
|
||||
const audioAvailable = stopInfo.audioStatus === 'READY'
|
||||
|
||||
return {
|
||||
...(fallback || {}),
|
||||
id: fallback?.id || linkedPrimary?.id || stopInfo.targetId,
|
||||
name: stopInfo.title || fallback?.name || linkedPrimary?.name || '讲解内容',
|
||||
hallId: fallback?.hallId,
|
||||
hallName: fallback?.hallName,
|
||||
floorId: fallback?.floorId,
|
||||
floorLabel: fallback?.floorLabel,
|
||||
image: coverImage,
|
||||
description,
|
||||
poiId: fallback?.poiId,
|
||||
sourcePoiId: fallback?.sourcePoiId,
|
||||
location: fallback?.location,
|
||||
year: fallback?.year,
|
||||
material: fallback?.material,
|
||||
size: fallback?.size,
|
||||
tags: fallback?.tags,
|
||||
guideTitle: stopInfo.title || fallback?.guideTitle,
|
||||
guideText: stopInfo.description || fallback?.guideText || fallback?.description,
|
||||
audioUrl: undefined,
|
||||
audioDuration: undefined,
|
||||
audioLanguage: stopInfo.lang,
|
||||
audioHasText: stopInfo.hasText,
|
||||
audioText: undefined,
|
||||
audioTextLength: undefined,
|
||||
audioTextHash: undefined,
|
||||
audioNarrationTier: undefined,
|
||||
audioUnavailableReason: audioAvailable
|
||||
? undefined
|
||||
: audioReasonToText(stopInfo.reason || (stopInfo.audioStatus === 'MISSING' ? 'NO_PUBLISHED_AUDIO' : undefined)),
|
||||
audioAvailable,
|
||||
audioStatus: stopInfo.audioStatus,
|
||||
supportedLanguages: stopInfo.supportedLanguages,
|
||||
imageStatus: stopInfo.imageStatus,
|
||||
imageSource: stopInfo.imageSource,
|
||||
galleryUrls: stopInfo.galleryUrls,
|
||||
linkedExhibits: stopInfo.linkedExhibits,
|
||||
stopInfoAvailable: stopInfo.available,
|
||||
stopInfoReason: stopInfo.reason,
|
||||
resolvedStopId: stopInfo.resolvedStopId,
|
||||
playTargetType: audioTarget.targetType,
|
||||
playTargetId: audioTarget.targetId
|
||||
}
|
||||
}
|
||||
|
||||
private toExhibitFromUnavailableStopInfo(
|
||||
fallback: MuseumExhibit,
|
||||
entryTarget: Required<GuideStopInfoRequest>
|
||||
): MuseumExhibit {
|
||||
return {
|
||||
...fallback,
|
||||
guideText: fallback.guideText || fallback.description,
|
||||
audioUrl: undefined,
|
||||
audioDuration: undefined,
|
||||
audioLanguage: entryTarget.lang,
|
||||
audioHasText: false,
|
||||
audioText: undefined,
|
||||
audioTextLength: undefined,
|
||||
audioTextHash: undefined,
|
||||
audioNarrationTier: undefined,
|
||||
audioUnavailableReason: '讲解详情服务暂不可用,当前提供图文讲解',
|
||||
audioAvailable: false,
|
||||
audioStatus: 'SERVICE_ERROR',
|
||||
stopInfoAvailable: false,
|
||||
stopInfoReason: 'SERVICE_ERROR',
|
||||
playTargetType: entryTarget.targetType,
|
||||
playTargetId: entryTarget.targetId
|
||||
}
|
||||
}
|
||||
|
||||
private applyPlayInfo(
|
||||
exhibit: MuseumExhibit,
|
||||
playInfo: AudioPlayInfo,
|
||||
@@ -229,14 +353,107 @@ export class ExplainUseCase {
|
||||
})
|
||||
}
|
||||
|
||||
async enterExplainDetail(request: ExplainDetailEntryRequest) {
|
||||
const entryTarget = await this.resolveDetailEntryTarget(request)
|
||||
const [stopInfoResult, fallbackExhibit] = await Promise.all([
|
||||
this.audioPlayInfo.getStopInfo(entryTarget)
|
||||
.then((stopInfo) => ({ stopInfo, error: null }))
|
||||
.catch((error) => ({ stopInfo: null, error })),
|
||||
this.explain.getExhibitById(request.exhibitId)
|
||||
.catch(() => this.explain.listExplainExhibits()
|
||||
.then((items) => items.find((item) => item.id === request.exhibitId) || null)
|
||||
.catch(() => null))
|
||||
])
|
||||
|
||||
if (stopInfoResult.stopInfo) {
|
||||
return this.toExhibitFromStopInfo(stopInfoResult.stopInfo, fallbackExhibit)
|
||||
}
|
||||
|
||||
if (fallbackExhibit) {
|
||||
return this.toExhibitFromUnavailableStopInfo(fallbackExhibit, entryTarget)
|
||||
}
|
||||
|
||||
throw stopInfoResult.error || new Error('讲解详情加载失败')
|
||||
}
|
||||
|
||||
async loadExplainDetailText(exhibit: MuseumExhibit): Promise<ExplainTextSelection> {
|
||||
const targetType = exhibit.playTargetType || 'ITEM'
|
||||
const targetId = exhibit.playTargetId || exhibit.id
|
||||
|
||||
try {
|
||||
const textInfo = await this.audioPlayInfo.getTextInfo({
|
||||
targetType,
|
||||
targetId,
|
||||
lang: (exhibit.audioLanguage as AudioLanguage) || (dataSourceConfig.audioLanguage as AudioLanguage)
|
||||
})
|
||||
const nextExhibit = this.applyTextInfo(exhibit, textInfo)
|
||||
|
||||
return {
|
||||
exhibit: nextExhibit,
|
||||
textInfo,
|
||||
available: textInfo.available === true && Boolean(textInfo.text),
|
||||
unavailableMessage: textInfo.available ? undefined : audioReasonToText(textInfo.reason || 'NO_TEXT')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('讲解词正文解析失败:', error)
|
||||
return {
|
||||
exhibit,
|
||||
textInfo: {
|
||||
available: false,
|
||||
targetType,
|
||||
targetId,
|
||||
lang: dataSourceConfig.audioLanguage as AudioLanguage,
|
||||
reason: 'SERVICE_ERROR'
|
||||
},
|
||||
available: false,
|
||||
unavailableMessage: '讲解词服务暂不可用,请稍后重试'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
listHalls(): Promise<MuseumHall[]> {
|
||||
return this.explain.listHalls()
|
||||
}
|
||||
|
||||
loadExplainHalls(): Promise<MuseumHall[]> {
|
||||
return this.listHalls()
|
||||
}
|
||||
|
||||
getHallById(id: string) {
|
||||
return this.explain.getHallById(id)
|
||||
}
|
||||
|
||||
loadTemporaryBusinessUnitsByHall(hallId: string): Promise<ExplainBusinessUnit[]> {
|
||||
return this.explain.listTemporaryBusinessUnitsByHall(hallId)
|
||||
}
|
||||
|
||||
async selectHall(hallId: string) {
|
||||
const [hall, units] = await Promise.all([
|
||||
this.getHallById(hallId),
|
||||
this.loadTemporaryBusinessUnitsByHall(hallId)
|
||||
])
|
||||
|
||||
return { hall, units }
|
||||
}
|
||||
|
||||
async selectBusinessUnit(hallId: string, unitId: string) {
|
||||
const units = await this.loadTemporaryBusinessUnitsByHall(hallId)
|
||||
return units.find((unit) => unit.id === unitId) || null
|
||||
}
|
||||
|
||||
async listGuideStopsByBusinessUnit(hallId: string, unitId: string): Promise<ExplainGuideStop[]> {
|
||||
const unit = await this.selectBusinessUnit(hallId, unitId)
|
||||
return unit?.stops || []
|
||||
}
|
||||
|
||||
openGuideStopDetail(stopId: string) {
|
||||
return this.enterExplainDetail({
|
||||
exhibitId: stopId,
|
||||
targetType: 'STOP',
|
||||
targetId: stopId
|
||||
})
|
||||
}
|
||||
|
||||
async listExhibitsByHallId(hallId: string) {
|
||||
const exhibits = await this.listExhibits()
|
||||
return exhibits.filter((exhibit) => exhibit.hallId === hallId)
|
||||
@@ -387,6 +604,64 @@ export class ExplainUseCase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async selectAudioForExplainDetail(exhibit: MuseumExhibit): Promise<ExplainAudioSelection> {
|
||||
const targetType = exhibit.playTargetType || 'ITEM'
|
||||
const targetId = exhibit.playTargetId || exhibit.id
|
||||
|
||||
if (exhibit.audioStatus && exhibit.audioStatus !== 'READY') {
|
||||
return {
|
||||
exhibit,
|
||||
track: null,
|
||||
media: null,
|
||||
playable: false,
|
||||
unavailableMessage: audioReasonToText(exhibit.stopInfoReason || 'NO_PUBLISHED_AUDIO')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const playInfo = await this.audioPlayInfo.getPlayInfo({
|
||||
targetType,
|
||||
targetId,
|
||||
lang: (exhibit.audioLanguage as AudioLanguage) || (dataSourceConfig.audioLanguage as AudioLanguage)
|
||||
})
|
||||
|
||||
if (!playInfo.playable || !playInfo.playUrl) {
|
||||
return {
|
||||
exhibit,
|
||||
track: null,
|
||||
media: null,
|
||||
playable: false,
|
||||
playInfo,
|
||||
unavailableMessage: audioReasonToText(playInfo.reason)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
exhibit,
|
||||
track: null,
|
||||
media: {
|
||||
id: `play-${playInfo.audioId || `${targetType}-${targetId}`}`,
|
||||
type: 'audio',
|
||||
url: playInfo.playUrl,
|
||||
duration: typeof playInfo.duration === 'number' ? playInfo.duration : undefined,
|
||||
language: playInfo.lang,
|
||||
available: true
|
||||
},
|
||||
playable: true,
|
||||
playInfo
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('语音播放解析失败:', error)
|
||||
return {
|
||||
exhibit,
|
||||
track: null,
|
||||
media: null,
|
||||
playable: false,
|
||||
unavailableMessage: '语音服务暂不可用,请稍后重试'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const explainUseCase = new ExplainUseCase()
|
||||
|
||||
Reference in New Issue
Block a user