视频会议
This commit is contained in:
@@ -54,3 +54,16 @@ export function getMeetingStats() {
|
|||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制结束指定录制(admin 兜底操作)
|
||||||
|
* 适用:host 失联未点停止 / ffmpeg 异常但 webhook 失效 / 长时间停留 recording 状态
|
||||||
|
* 终态记录调用此接口幂等返回成功
|
||||||
|
* @param {number} recordingID
|
||||||
|
*/
|
||||||
|
export function forceStopRecording(recordingID) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/admin/meetings/recordings/${recordingID}/force-stop`,
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -208,6 +208,7 @@
|
|||||||
width="780px"
|
width="780px"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
class="detail-dialog"
|
class="detail-dialog"
|
||||||
|
@close="onDetailDialogClose"
|
||||||
>
|
>
|
||||||
<div v-loading="detailLoading" class="detail-content">
|
<div v-loading="detailLoading" class="detail-content">
|
||||||
<template v-if="currentMeeting">
|
<template v-if="currentMeeting">
|
||||||
@@ -356,7 +357,7 @@
|
|||||||
<template #default="{ row }">{{ formatBytes(row.size_bytes) }}</template>
|
<template #default="{ row }">{{ formatBytes(row.size_bytes) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="started_at" label="开始时间" width="170" align="center" />
|
<el-table-column prop="started_at" label="开始时间" width="170" align="center" />
|
||||||
<el-table-column label="文件" min-width="100" align="center">
|
<el-table-column label="文件" min-width="120" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<a
|
<a
|
||||||
v-if="row.status === 'ready' && row.file_url"
|
v-if="row.status === 'ready' && row.file_url"
|
||||||
@@ -367,6 +368,63 @@
|
|||||||
<span v-else-if="row.status === 'failed'" class="failure-text">
|
<span v-else-if="row.status === 'failed'" class="failure-text">
|
||||||
{{ row.failure_reason || '失败' }}
|
{{ row.failure_reason || '失败' }}
|
||||||
</span>
|
</span>
|
||||||
|
<!-- recording 兜底:host 失联/未点停止时由 admin 强制结束 -->
|
||||||
|
<el-button
|
||||||
|
v-else-if="row.status === 'recording'"
|
||||||
|
type="danger"
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
:loading="forceStopMap[row.id]"
|
||||||
|
@click="handleForceStopRecording(row)"
|
||||||
|
>强制结束</el-button>
|
||||||
|
<span v-else class="text-muted">—</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="转写" width="170" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<!-- 仅 ready 状态的录制可触发转写 -->
|
||||||
|
<template v-if="row.status === 'ready'">
|
||||||
|
<el-tag
|
||||||
|
v-if="transcriptMap[row.id]?.status === 'ready'"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
effect="plain"
|
||||||
|
class="transcript-tag"
|
||||||
|
@click="viewTranscript(row)"
|
||||||
|
>已就绪 · 查看</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-else-if="transcriptMap[row.id]?.status === 'running' || transcriptMap[row.id]?.status === 'pending'"
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
effect="plain"
|
||||||
|
>
|
||||||
|
<el-icon class="is-loading" style="margin-right:4px"><Loading /></el-icon>
|
||||||
|
转写中
|
||||||
|
</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-else-if="transcriptMap[row.id]?.status === 'failed'"
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
effect="plain"
|
||||||
|
@click="viewTranscript(row)"
|
||||||
|
>失败 · 查看</el-tag>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
:loading="transcribingMap[row.id]"
|
||||||
|
@click="handleTranscribe(row, false)"
|
||||||
|
>开始转写</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="transcriptMap[row.id]?.status === 'ready' || transcriptMap[row.id]?.status === 'failed'"
|
||||||
|
type="warning"
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
:loading="transcribingMap[row.id]"
|
||||||
|
@click="handleTranscribe(row, true)"
|
||||||
|
>重转</el-button>
|
||||||
|
</template>
|
||||||
<span v-else class="text-muted">—</span>
|
<span v-else class="text-muted">—</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -375,14 +433,69 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 转写文字稿弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="transcriptVisible"
|
||||||
|
:title="transcriptDialogTitle"
|
||||||
|
width="720px"
|
||||||
|
destroy-on-close
|
||||||
|
class="transcript-dialog"
|
||||||
|
>
|
||||||
|
<div v-if="currentTranscript" class="transcript-content">
|
||||||
|
<div class="transcript-meta">
|
||||||
|
<el-tag :type="transcriptStatusTag(currentTranscript.status)" size="small">
|
||||||
|
{{ currentTranscript.status_label }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-if="currentTranscript.provider_code" class="meta-pill">
|
||||||
|
提供商:{{ currentTranscript.provider_code }}
|
||||||
|
</span>
|
||||||
|
<span v-if="currentTranscript.model_code" class="meta-pill">
|
||||||
|
模型:{{ currentTranscript.model_code }}
|
||||||
|
</span>
|
||||||
|
<span v-if="currentTranscript.language" class="meta-pill">
|
||||||
|
语言:{{ currentTranscript.language }}
|
||||||
|
</span>
|
||||||
|
<span v-if="currentTranscript.duration_sec" class="meta-pill">
|
||||||
|
音频时长:{{ formatDuration(currentTranscript.duration_sec) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="currentTranscript.status === 'failed'" class="transcript-error">
|
||||||
|
<el-icon><CircleClose /></el-icon>
|
||||||
|
<span>{{ currentTranscript.error_msg || '转写失败' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="currentTranscript.segments && currentTranscript.segments.length > 0"
|
||||||
|
class="transcript-segments"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(seg, idx) in currentTranscript.segments"
|
||||||
|
:key="idx"
|
||||||
|
class="segment-row"
|
||||||
|
>
|
||||||
|
<span class="segment-time">{{ formatSegTime(seg.start) }}</span>
|
||||||
|
<span class="segment-text">{{ seg.text }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="currentTranscript.text" class="transcript-text">
|
||||||
|
{{ currentTranscript.text }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-empty v-else description="暂无文字稿内容" :image-size="60" />
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { Search } from '@element-plus/icons-vue'
|
import { Search, Loading, CircleClose } from '@element-plus/icons-vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { getMeetingList, getMeetingDetail, getMeetingStats } from '@/api/meeting'
|
import { getMeetingList, getMeetingDetail, getMeetingStats, forceStopRecording } from '@/api/meeting'
|
||||||
|
import { getTranscript, submitTranscribe } from '@/api/transcribe'
|
||||||
|
|
||||||
const AVATAR_COLORS = ['#2563EB', '#7C3AED', '#059669', '#D97706', '#DC2626', '#0891B2', '#4F46E5', '#BE185D']
|
const AVATAR_COLORS = ['#2563EB', '#7C3AED', '#059669', '#D97706', '#DC2626', '#0891B2', '#4F46E5', '#BE185D']
|
||||||
|
|
||||||
@@ -528,9 +641,13 @@ const viewDetail = async (row) => {
|
|||||||
detailVisible.value = true
|
detailVisible.value = true
|
||||||
detailLoading.value = true
|
detailLoading.value = true
|
||||||
currentMeeting.value = null
|
currentMeeting.value = null
|
||||||
|
// 切换会议时清空旧的转写状态 + 停止旧轮询,避免上一场会议的状态泄漏
|
||||||
|
resetTranscriptState()
|
||||||
try {
|
try {
|
||||||
const res = await getMeetingDetail(row.id)
|
const res = await getMeetingDetail(row.id)
|
||||||
currentMeeting.value = res.data
|
currentMeeting.value = res.data
|
||||||
|
// 详情拉到后,对每段已 ready 的录制并发拉一次转写状态
|
||||||
|
loadAllTranscripts()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error(e?.message || '获取会议详情失败')
|
ElMessage.error(e?.message || '获取会议详情失败')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -538,10 +655,179 @@ const viewDetail = async (row) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============== 转写:状态、API、轮询 ==============
|
||||||
|
|
||||||
|
// recording.id → transcript DTO(含 status/text/segments/...)
|
||||||
|
const transcriptMap = reactive({})
|
||||||
|
// recording.id → 是否正在调用 submit(按钮 loading 用)
|
||||||
|
const transcribingMap = reactive({})
|
||||||
|
// recording.id → 是否正在调用强制结束(按钮 loading 用)
|
||||||
|
const forceStopMap = reactive({})
|
||||||
|
// 当前展开的转写文字稿(用于弹窗)
|
||||||
|
const transcriptVisible = ref(false)
|
||||||
|
const currentTranscript = ref(null)
|
||||||
|
// 轮询定时器:recording.id → setInterval handle
|
||||||
|
const pollTimers = {}
|
||||||
|
|
||||||
|
const transcriptDialogTitle = computed(() => {
|
||||||
|
if (!currentTranscript.value) return '转写文字稿'
|
||||||
|
return `转写文字稿 - 录制 #${currentTranscript.value.recording_id}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const transcriptStatusTag = (s) => {
|
||||||
|
switch (s) {
|
||||||
|
case 'ready': return 'success'
|
||||||
|
case 'running':
|
||||||
|
case 'pending': return 'warning'
|
||||||
|
case 'failed': return 'danger'
|
||||||
|
default: return 'info'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0.0 → "00:00",秒级;STT segment 起止以浮点秒
|
||||||
|
const formatSegTime = (sec) => {
|
||||||
|
if (sec == null) return '00:00'
|
||||||
|
const total = Math.floor(sec)
|
||||||
|
const m = Math.floor(total / 60)
|
||||||
|
const s = total % 60
|
||||||
|
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拉取一段录制的转写状态;recording 未触发过转写时后端返回 null
|
||||||
|
const loadOneTranscript = async (recordingId) => {
|
||||||
|
try {
|
||||||
|
const res = await getTranscript(recordingId)
|
||||||
|
if (res?.data) {
|
||||||
|
transcriptMap[recordingId] = res.data
|
||||||
|
// 终态 → 停止该 id 的轮询
|
||||||
|
if (res.data.status === 'ready' || res.data.status === 'failed') {
|
||||||
|
stopPolling(recordingId)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 主动写一个 null,便于模板 v-if 判定为"尚未发起"
|
||||||
|
delete transcriptMap[recordingId]
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 静默:转写查询失败不影响列表展示
|
||||||
|
console.warn('[transcribe] 查询转写状态失败', recordingId, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadAllTranscripts = () => {
|
||||||
|
const recordings = currentMeeting.value?.recordings || []
|
||||||
|
recordings.forEach((r) => {
|
||||||
|
if (r.status === 'ready') {
|
||||||
|
loadOneTranscript(r.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动轮询:每 3 秒查一次,直到 ready/failed 或弹窗关闭
|
||||||
|
const startPolling = (recordingId) => {
|
||||||
|
stopPolling(recordingId)
|
||||||
|
pollTimers[recordingId] = setInterval(() => {
|
||||||
|
loadOneTranscript(recordingId)
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopPolling = (recordingId) => {
|
||||||
|
if (pollTimers[recordingId]) {
|
||||||
|
clearInterval(pollTimers[recordingId])
|
||||||
|
delete pollTimers[recordingId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopAllPolling = () => {
|
||||||
|
Object.keys(pollTimers).forEach((id) => stopPolling(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetTranscriptState = () => {
|
||||||
|
stopAllPolling()
|
||||||
|
Object.keys(transcriptMap).forEach((k) => delete transcriptMap[k])
|
||||||
|
Object.keys(transcribingMap).forEach((k) => delete transcribingMap[k])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发转写:force=true 表示已 ready 也强制重跑
|
||||||
|
const handleTranscribe = async (row, force) => {
|
||||||
|
if (transcribingMap[row.id]) return
|
||||||
|
transcribingMap[row.id] = true
|
||||||
|
try {
|
||||||
|
const res = await submitTranscribe(row.id, { force, language: '' })
|
||||||
|
transcriptMap[row.id] = res.data
|
||||||
|
if (res.data?.status === 'ready' || res.data?.status === 'failed') {
|
||||||
|
// 极少见:同步就完成(短音频缓存命中)
|
||||||
|
stopPolling(row.id)
|
||||||
|
} else {
|
||||||
|
ElMessage.success('转写任务已提交,正在处理中…')
|
||||||
|
startPolling(row.id)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 503 → STT 未配置;400 → 参数 / 录制状态错误;其他 → 默认提示
|
||||||
|
const status = e?.response?.status
|
||||||
|
const msg = e?.response?.data?.message || e?.message || '提交转写失败'
|
||||||
|
if (status === 503) {
|
||||||
|
ElMessage.warning('语音转写服务未配置,请联系管理员检查 LLM 配置中心')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(msg)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
transcribingMap[row.id] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制结束指定录制(admin 兜底)
|
||||||
|
// 触发场景:host 失联未点停止 / ffmpeg 异常但 webhook 失效 / 长时间停留 recording 状态
|
||||||
|
// 后端走完整 stop 状态机:media-server stop → MinIO 上传 → DB ready/failed
|
||||||
|
const handleForceStopRecording = async (row) => {
|
||||||
|
if (forceStopMap[row.id]) return
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确认强制结束录制 #${row.id}?将立即停止 ffmpeg 并尝试上传当前已写入的片段到 MinIO。`,
|
||||||
|
'强制结束录制',
|
||||||
|
{ type: 'warning', confirmButtonText: '确认结束', cancelButtonText: '取消' }
|
||||||
|
)
|
||||||
|
} catch (_) {
|
||||||
|
return // 用户取消
|
||||||
|
}
|
||||||
|
forceStopMap[row.id] = true
|
||||||
|
try {
|
||||||
|
await forceStopRecording(row.id)
|
||||||
|
ElMessage.success('已发起强制结束,正在收尾上传…')
|
||||||
|
// 后端 StopRecording 是同步上传 MinIO(Phase B1 取舍),返回时一般已是 ready/failed
|
||||||
|
// 重新拉取详情即可看到终态
|
||||||
|
if (currentMeeting.value?.id) {
|
||||||
|
const res = await getMeetingDetail(currentMeeting.value.id)
|
||||||
|
currentMeeting.value = res.data
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e?.response?.data?.message || e?.message || '强制结束失败'
|
||||||
|
ElMessage.error(msg)
|
||||||
|
} finally {
|
||||||
|
forceStopMap[row.id] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看转写文字稿
|
||||||
|
const viewTranscript = (row) => {
|
||||||
|
const t = transcriptMap[row.id]
|
||||||
|
if (!t) return
|
||||||
|
currentTranscript.value = t
|
||||||
|
transcriptVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 详情弹窗关闭时停掉所有轮询,防内存泄漏
|
||||||
|
const onDetailDialogClose = () => {
|
||||||
|
stopAllPolling()
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchMeetingList()
|
fetchMeetingList()
|
||||||
fetchStats()
|
fetchStats()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stopAllPolling()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -651,4 +937,49 @@ onMounted(() => {
|
|||||||
.file-link { color: #2563EB; font-size: 13px; text-decoration: none; }
|
.file-link { color: #2563EB; font-size: 13px; text-decoration: none; }
|
||||||
.file-link:hover { text-decoration: underline; }
|
.file-link:hover { text-decoration: underline; }
|
||||||
.failure-text { color: #DC2626; font-size: 12px; }
|
.failure-text { color: #DC2626; font-size: 12px; }
|
||||||
|
|
||||||
|
/* 转写状态 tag 可点击 */
|
||||||
|
.transcript-tag { cursor: pointer; }
|
||||||
|
.transcript-tag:hover { opacity: 0.85; }
|
||||||
|
|
||||||
|
/* 转写文字稿弹窗 */
|
||||||
|
.transcript-content { padding: 4px 0 8px; }
|
||||||
|
.transcript-meta {
|
||||||
|
display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
|
||||||
|
padding: 12px 16px; background: #F8FAFC; border-radius: 8px; border: 1px solid #E2E8F0;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.meta-pill {
|
||||||
|
font-size: 12px; color: #475569; background: #FFFFFF;
|
||||||
|
padding: 2px 10px; border-radius: 999px; border: 1px solid #E2E8F0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcript-error {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 16px; background: #FEF2F2; border: 1px solid #FECACA;
|
||||||
|
border-radius: 8px; color: #B91C1C; font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcript-segments {
|
||||||
|
max-height: 480px; overflow-y: auto; padding: 4px 6px 4px 0;
|
||||||
|
border: 1px solid #E2E8F0; border-radius: 8px; background: #FFFFFF;
|
||||||
|
}
|
||||||
|
.segment-row {
|
||||||
|
display: flex; gap: 12px; align-items: flex-start;
|
||||||
|
padding: 8px 14px; border-bottom: 1px dashed #F1F5F9;
|
||||||
|
}
|
||||||
|
.segment-row:last-child { border-bottom: none; }
|
||||||
|
.segment-time {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-family: 'SF Mono', 'Menlo', monospace;
|
||||||
|
font-size: 12px; color: #64748B; padding-top: 2px; min-width: 48px;
|
||||||
|
}
|
||||||
|
.segment-text { font-size: 14px; color: #1E293B; line-height: 1.6; flex: 1; }
|
||||||
|
|
||||||
|
.transcript-text {
|
||||||
|
padding: 16px; background: #FFFFFF; border: 1px solid #E2E8F0;
|
||||||
|
border-radius: 8px; font-size: 14px; color: #1E293B; line-height: 1.7;
|
||||||
|
white-space: pre-wrap; word-break: break-word;
|
||||||
|
max-height: 480px; overflow-y: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -59,5 +59,7 @@ func RegisterRoutes(
|
|||||||
adminGroup.GET("/meetings/stats", meetingManageCtrl.GetMeetingStats)
|
adminGroup.GET("/meetings/stats", meetingManageCtrl.GetMeetingStats)
|
||||||
adminGroup.GET("/meetings", meetingManageCtrl.GetMeetingList)
|
adminGroup.GET("/meetings", meetingManageCtrl.GetMeetingList)
|
||||||
adminGroup.GET("/meetings/:id", meetingManageCtrl.GetMeetingDetail)
|
adminGroup.GET("/meetings/:id", meetingManageCtrl.GetMeetingDetail)
|
||||||
|
// 录制兜底操作(admin 强制结束):用于 host 失联 / ffmpeg 崩 webhook 失效等场景
|
||||||
|
adminGroup.POST("/meetings/recordings/:id/force-stop", meetingManageCtrl.ForceStopRecording)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,8 +84,7 @@ func InitializeApp(cfg *config.Config) (*App, error) {
|
|||||||
messageManageService := service2.NewMessageManageService(messageManageDAO, userDAO, conversationDAO, pubSub)
|
messageManageService := service2.NewMessageManageService(messageManageDAO, userDAO, conversationDAO, pubSub)
|
||||||
messageManageController := controller2.NewMessageManageController(messageManageService)
|
messageManageController := controller2.NewMessageManageController(messageManageService)
|
||||||
meetingManageDAO := dao2.NewMeetingManageDAO(gormDB)
|
meetingManageDAO := dao2.NewMeetingManageDAO(gormDB)
|
||||||
meetingManageService := service2.NewMeetingManageService(meetingManageDAO)
|
// admin meetingManageService 依赖 meetingRecordingService(强制结束录制兜底),延后到 meetingRecordingService 构造后再实例化
|
||||||
meetingManageController := controller2.NewMeetingManageController(meetingManageService)
|
|
||||||
serverConfig := provideServerConfig(cfg)
|
serverConfig := provideServerConfig(cfg)
|
||||||
handler := ws.ProvideWSHandler(hub, pubSub, jwtConfig, serverConfig, onlineService, authService)
|
handler := ws.ProvideWSHandler(hub, pubSub, jwtConfig, serverConfig, onlineService, authService)
|
||||||
friendGroupDAO := dao3.NewFriendGroupDAO(gormDB)
|
friendGroupDAO := dao3.NewFriendGroupDAO(gormDB)
|
||||||
@@ -116,6 +115,9 @@ func InitializeApp(cfg *config.Config) (*App, error) {
|
|||||||
meetingService := service7.NewMeetingService(meetingRoomDAO, meetingParticipantDAO, meetingChatDAO, gormDB, client, meetingBroadcaster, notifyService, friendshipDAO, onlineService, httpMediaOrchestrator, meetingLifecycleService)
|
meetingService := service7.NewMeetingService(meetingRoomDAO, meetingParticipantDAO, meetingChatDAO, gormDB, client, meetingBroadcaster, notifyService, friendshipDAO, onlineService, httpMediaOrchestrator, meetingLifecycleService)
|
||||||
meetingSignalService := service7.NewMeetingSignalService(meetingRoomDAO, meetingParticipantDAO, client, meetingBroadcaster, httpMediaOrchestrator, meetingLifecycleService)
|
meetingSignalService := service7.NewMeetingSignalService(meetingRoomDAO, meetingParticipantDAO, client, meetingBroadcaster, httpMediaOrchestrator, meetingLifecycleService)
|
||||||
meetingRecordingService := service7.NewMeetingRecordingService(meetingRoomDAO, meetingParticipantDAO, meetingRecordingDAO, client, meetingBroadcaster, httpMediaOrchestrator, minioClient, minioConfig)
|
meetingRecordingService := service7.NewMeetingRecordingService(meetingRoomDAO, meetingParticipantDAO, meetingRecordingDAO, client, meetingBroadcaster, httpMediaOrchestrator, minioClient, minioConfig)
|
||||||
|
// admin 会议管理服务在 recording service 构造后再创建,从而支持"强制结束录制"兜底
|
||||||
|
meetingManageService := service2.NewMeetingManageService(meetingManageDAO, meetingRecordingService)
|
||||||
|
meetingManageController := controller2.NewMeetingManageController(meetingManageService)
|
||||||
meetingController := controller7.NewMeetingController(meetingService)
|
meetingController := controller7.NewMeetingController(meetingService)
|
||||||
meetingRecordingController := controller7.NewMeetingRecordingController(meetingRecordingService)
|
meetingRecordingController := controller7.NewMeetingRecordingController(meetingRecordingService)
|
||||||
meetingWSHandler := controller7.NewMeetingWSHandler(meetingSignalService, hub)
|
meetingWSHandler := controller7.NewMeetingWSHandler(meetingSignalService, hub)
|
||||||
|
|||||||
Reference in New Issue
Block a user