接入H5讲解后端数据闭环

This commit is contained in:
lyf
2026-06-30 17:22:20 +08:00
parent 1c2cc788d1
commit e3682ed3ec
13 changed files with 815 additions and 68 deletions

View File

@@ -18,12 +18,16 @@ import {
audioPlayInfoRepository,
audioReasonToText,
type AudioPlayInfo,
type AudioPlayInfoRepository
type AudioPlayInfoRepository,
type AudioTextInfo
} from '@/repositories/AudioPlayInfoRepository'
import {
publishedExhibitAudioRepository,
type PublishedExhibitAudioRepository
} from '@/repositories/PublishedExhibitAudioRepository'
import {
dataSourceConfig
} from '@/config/dataSource'
export interface ExplainAudioSelection {
exhibit: MuseumExhibit
@@ -34,6 +38,11 @@ export interface ExplainAudioSelection {
playInfo?: AudioPlayInfo
}
export interface ExhibitDetailAudioOptions {
enrichAudio?: boolean
includeText?: boolean
}
export class ExplainUseCase {
constructor(
private readonly explain: ExplainRepository = explainRepository,
@@ -56,6 +65,100 @@ export class ExplainUseCase {
}
}
private resolveAudioTarget(exhibit: MuseumExhibit, track?: ExplainTrack | null) {
const targetType: AudioPlayTargetType = track?.playTargetType
|| exhibit.playTargetType
|| 'ITEM'
const targetId = track?.playTargetId
|| exhibit.playTargetId
|| exhibit.id
return { targetType, targetId }
}
private applyPlayInfo(
exhibit: MuseumExhibit,
playInfo: AudioPlayInfo,
options: { preserveStaticFallback?: boolean } = {}
): MuseumExhibit {
const apiPlayable = playInfo.playable === true && Boolean(playInfo.playUrl)
const canUseStaticAudioFallback = !playInfo.reason || playInfo.reason === 'TARGET_NOT_FOUND'
const fallbackAudioUrl = options.preserveStaticFallback && canUseStaticAudioFallback && exhibit.audioUrl
? exhibit.audioUrl
: undefined
const nextAudioUrl = apiPlayable ? playInfo.playUrl || undefined : fallbackAudioUrl
const nextAudioDuration = typeof playInfo.duration === 'number'
? playInfo.duration
: exhibit.audioDuration
return {
...exhibit,
guideTitle: playInfo.title || exhibit.guideTitle,
audioUrl: nextAudioUrl,
audioDuration: nextAudioDuration,
audioLanguage: playInfo.lang || exhibit.audioLanguage,
audioSubtitleUrl: playInfo.subtitleUrl || exhibit.audioSubtitleUrl,
audioHasText: playInfo.hasText === true || exhibit.audioHasText,
audioNarrationTier: playInfo.narrationTier || exhibit.audioNarrationTier,
audioUnavailableReason: apiPlayable ? undefined : audioReasonToText(playInfo.reason),
audioAvailable: apiPlayable || Boolean(fallbackAudioUrl)
}
}
private applyTextInfo(exhibit: MuseumExhibit, textInfo: AudioTextInfo | null): MuseumExhibit {
if (!textInfo?.available || !textInfo.text) return exhibit
return {
...exhibit,
guideTitle: textInfo.title || exhibit.guideTitle,
guideText: textInfo.text,
audioText: textInfo.text,
audioTextLength: textInfo.textLength || exhibit.audioTextLength,
audioTextHash: textInfo.textHash || exhibit.audioTextHash,
audioLanguage: textInfo.lang || exhibit.audioLanguage,
audioNarrationTier: textInfo.narrationTier || exhibit.audioNarrationTier,
audioHasText: true
}
}
private async enrichExhibitAudio(
exhibit: MuseumExhibit,
options: {
includeText?: boolean
preserveStaticFallback?: boolean
track?: ExplainTrack | null
} = {}
): Promise<MuseumExhibit> {
const track = options.track ?? await this.explain.getTrackByExhibitId(exhibit.id).catch(() => null)
const baseExhibit = this.applyTrackAudioState(exhibit, track)
const { targetType, targetId } = this.resolveAudioTarget(baseExhibit, track)
try {
const playInfo = await this.audioPlayInfo.getPlayInfo({ targetType, targetId })
let nextExhibit = this.applyPlayInfo(baseExhibit, playInfo, {
preserveStaticFallback: options.preserveStaticFallback
})
if (options.includeText && playInfo.hasText) {
try {
const textInfo = await this.audioPlayInfo.getTextInfo({
targetType,
targetId,
lang: playInfo.lang
})
nextExhibit = this.applyTextInfo(nextExhibit, textInfo)
} catch (error) {
console.warn('讲解词正文加载失败,将使用静态讲解文稿:', error)
}
}
return nextExhibit
} catch (error) {
console.warn('讲解播放信息加载失败,将使用静态讲解数据:', error)
return baseExhibit
}
}
private async getTrackByExhibitIdMap() {
const tracks = await this.explain.listTracks()
const trackByExhibitId = new Map<string, ExplainTrack>()
@@ -97,19 +200,35 @@ export class ExplainUseCase {
}
}
async getExhibitById(id: string) {
async getExhibitById(id: string, options: ExhibitDetailAudioOptions = {}) {
const exhibit = await this.explain.getExhibitById(id)
if (!exhibit) return null
try {
const track = await this.explain.getTrackByExhibitId(id)
return this.applyTrackAudioState(exhibit, track)
const baseExhibit = this.applyTrackAudioState(exhibit, track)
if (!options.enrichAudio) {
return baseExhibit
}
return this.enrichExhibitAudio(exhibit, {
includeText: options.includeText,
preserveStaticFallback: true,
track
})
} catch (error) {
console.warn('讲解详情音频状态加载失败,将使用展项原始音频状态:', error)
return exhibit
}
}
async enrichExhibitDetailAudio(id: string) {
return this.getExhibitById(id, {
enrichAudio: true,
includeText: true
})
}
listHalls(): Promise<MuseumHall[]> {
return this.explain.listHalls()
}
@@ -130,15 +249,20 @@ export class ExplainUseCase {
async selectAudioForExhibit(exhibitId: string): Promise<ExplainAudioSelection | null> {
const track = await this.explain.getTrackByExhibitId(exhibitId)
const explainExhibits = await this.explain.listExplainExhibits()
const exhibit = explainExhibits.find((item) => item.id === exhibitId)
|| await this.explain.getExhibitById(exhibitId)
const summaryExhibit = explainExhibits.find((item) => item.id === exhibitId)
const detailExhibit = await this.explain.getExhibitById(exhibitId).catch(() => null)
const exhibit = detailExhibit || summaryExhibit
if (!exhibit) return null
const fallbackMedia = track?.mediaId
? await this.media.getMediaById(track.mediaId)
const fallbackMedia = await (track?.mediaId
? this.media.getMediaById(track.mediaId)
: exhibit.guideContentId
? await this.media.getMediaById(`media-${exhibit.guideContentId}`)
: null
? this.media.getMediaById(`media-${exhibit.guideContentId}`)
: Promise.resolve(null)
).catch((error) => {
console.warn('静态讲解媒体读取失败,将继续尝试播放接口:', error)
return null
})
const targetType: AudioPlayTargetType = track?.playTargetType
|| exhibit.playTargetType
|| 'ITEM'
@@ -203,6 +327,15 @@ export class ExplainUseCase {
}
}
const directMediaSelection = toDirectMediaSelection({
playable: false,
targetType,
targetId,
lang: dataSourceConfig.audioLanguage as AudioPlayInfo['lang'],
reason: 'DIRECT_MEDIA_FALLBACK'
})
if (directMediaSelection) return directMediaSelection
try {
const initialPlayInfo = await this.audioPlayInfo.getPlayInfo({ targetType, targetId })
@@ -222,9 +355,6 @@ export class ExplainUseCase {
}
}
}
const directMediaSelection = toDirectMediaSelection(initialPlayInfo)
if (directMediaSelection) return directMediaSelection
}
return {
@@ -237,13 +367,23 @@ export class ExplainUseCase {
}
} catch (error) {
console.error('语音播放解析失败:', error)
const serviceErrorPlayInfo: AudioPlayInfo = {
playable: false,
targetType,
targetId,
lang: dataSourceConfig.audioLanguage as AudioPlayInfo['lang'],
reason: 'SERVICE_ERROR'
}
const directMediaSelection = toDirectMediaSelection(serviceErrorPlayInfo)
if (directMediaSelection) return directMediaSelection
return {
exhibit,
track,
media: null,
playable: false,
unavailableMessage: '语音服务暂不可用,请稍后重试'
playInfo: serviceErrorPlayInfo,
unavailableMessage: '语音服务暂不可用,当前提供图文讲解'
}
}
}