feat: adapt migration queue aggregate APIs
Build and Deploy Vue3 / build (push) Successful in 1m41s
Build and Deploy Vue3 / deploy (push) Failing after 53s

This commit is contained in:
shiran
2026-08-04 17:09:28 +08:00
parent 57fc4a3fcb
commit 6d47314a9a
3 changed files with 77 additions and 60 deletions
+12
View File
@@ -557,6 +557,18 @@ export const abortDataMigrate = (data) => {
})
}
/** 获取跨服务数据迁移聚合队列 */
export const getDataMigrateQueue = (params) => {
return http2.get('/api/v1/admin/server/host_service/point/vm/data_migrate/queue', { params })
}
/** 按迁移记录中断队列任务 */
export const abortDataMigrateQueue = (data) => {
return http2.post('/api/v1/admin/server/host_service/point/vm/data_migrate/queue/abort', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 获取跨服务数据迁移并发及队列状态 */
export const getDataMigrateConcurrency = () => {
return http2.get('/api/v1/admin/server/host_service/point/vm/data_migrate/concurrency')
@@ -191,6 +191,7 @@ const serviceInfo = ref({})
// Tab
const activeTab = ref('host')
provide('activeKvmDetailTab', activeTab)
const tabLoaded = reactive({
'host': true,
'image': false,
+64 -60
View File
@@ -33,14 +33,14 @@
<el-table :data="queueRecords" v-loading="loading" stripe>
<el-table-column label="队列" width="90" align="center">
<template #default="{ row }">
<el-tag v-if="row.queue_position > 0" type="info">#{{ row.queue_position }}</el-tag>
<el-tag v-if="row.global_queue_position > 0" type="info">#{{ row.global_queue_position }}</el-tag>
<el-tag v-else type="success">执行中</el-tag>
</template>
</el-table-column>
<el-table-column label="记录 / 状态" min-width="145">
<template #default="{ row }">
<div class="stack-cell">
<span>记录 #{{ row.id }}</span>
<span>记录 #{{ row.migration_record_id }}</span>
<el-tag :type="statusType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
</div>
</template>
@@ -83,15 +83,26 @@
</template>
</el-table-column>
<el-table-column label="创建时间" width="170">
<template #default="{ row }">{{ formatTime(row.started_at || row.CreatedAt) }}</template>
<template #default="{ row }">{{ formatTime(row.started_at || row.created_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button link type="danger" :loading="abortingId === row.id" :disabled="row.status === 'aborting'" @click="abortMigration(row)">中断</el-button>
<el-button link type="danger" :loading="abortingId === row.migration_record_id" :disabled="row.status === 'aborting'" @click="abortMigration(row)">中断</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!loading && !queueRecords.length" description="当前服务暂无迁移中或排队中的虚拟机" />
<el-pagination
v-if="total > pageSize"
v-model:current-page="page"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[20, 50, 100]"
layout="total, sizes, prev, pager, next"
class="queue-pagination"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</el-card>
<el-dialog v-model="concurrencyVisible" title="迁移队列并发设置" width="480px" destroy-on-close>
@@ -114,29 +125,30 @@
</template>
<script setup>
import { computed, inject, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { computed, inject, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh, Setting } from '@element-plus/icons-vue'
import {
abortDataMigrate,
getDataMigrateConcurrency,
getDataMigrateProgress,
getVmMigrationRecordList,
abortDataMigrateQueue,
getDataMigrateQueue,
updateDataMigrateConcurrency
} from '@/api/admin/kvmService'
import { extractApiError } from '@/utils/kvmErrorUtil'
const router = useRouter()
const injectedServiceId = inject('serviceId', null)
const activeDetailTab = inject('activeKvmDetailTab', null)
const serviceId = computed(() => Number(injectedServiceId?.value) || 0)
const loading = ref(false)
const loadError = ref('')
const queueRecords = ref([])
const page = ref(1)
const pageSize = ref(20)
const total = ref(0)
const revision = ref(0)
const abortingId = ref(null)
const concurrency = reactive({ concurrency: 1, active_count: 0, queued_count: 0, available_slots: 0, updated_at: '' })
const activeStatuses = new Set(['pending', 'starting', 'exporting', 'importing', 'running', 'aborting', 'recovering'])
const listQueryStatuses = ['pending', 'exporting', 'importing', 'running']
let refreshTimer = null
let requestInFlight = false
@@ -160,53 +172,22 @@ const formatTime = value => {
return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString('zh-CN', { hour12: false })
}
const fetchDirectionRecords = async (direction, status) => {
const result = []
let page = 1
let total = 0
do {
const params = { page, count: 100, migration_type: 'data_migrate', status, [`${direction}_service_id`]: serviceId.value }
const res = await getVmMigrationRecordList(params)
if (res?.data?.code !== 200) throw new Error(extractApiError(res?.data, '加载迁移队列失败'))
const data = res.data.data || {}
const rows = Array.isArray(data.data) ? data.data : []
result.push(...rows)
total = Number(data.all_count) || rows.length
page += 1
} while (result.length < total && page <= 20)
return result
}
const loadData = async ({ silent = false } = {}) => {
if (!serviceId.value || requestInFlight) return
requestInFlight = true
if (!silent) loading.value = true
loadError.value = ''
try {
const concurrencyRes = await getDataMigrateConcurrency()
if (concurrencyRes?.data?.code !== 200) throw new Error(extractApiError(concurrencyRes?.data, '加载并发状态失败'))
Object.assign(concurrency, concurrencyRes.data.data || {})
const recordResults = await Promise.allSettled(
['source', 'target'].flatMap(direction => listQueryStatuses.map(status => fetchDirectionRecords(direction, status)))
)
const successfulGroups = recordResults.filter(result => result.status === 'fulfilled').map(result => result.value)
if (!successfulGroups.length) throw new Error('加载迁移任务失败')
const allRows = successfulGroups.flat()
const uniqueRows = allRows.filter((row, index, rows) => rows.findIndex(item => item.id === row.id) === index)
const candidates = uniqueRows.filter(row => activeStatuses.has(row.status))
const progressResults = await Promise.allSettled(candidates.map(row => getDataMigrateProgress({ migration_record_id: row.id })))
queueRecords.value = candidates.map((row, index) => {
const result = progressResults[index]
const progress = result.status === 'fulfilled' && result.value?.data?.code === 200 ? result.value.data.data : null
return progress ? { ...row, ...progress } : row
}).filter(row => activeStatuses.has(row.status)).sort((a, b) => {
const aQueued = Number(a.queue_position) > 0
const bQueued = Number(b.queue_position) > 0
if (aQueued !== bQueued) return aQueued ? 1 : -1
if (aQueued) return Number(a.queue_position) - Number(b.queue_position)
return new Date(a.started_at || a.CreatedAt || 0) - new Date(b.started_at || b.CreatedAt || 0)
})
const params = { service_id: serviceId.value, relation: 'any', page: page.value, count: pageSize.value }
if (silent && revision.value) params.after_revision = revision.value
const res = await getDataMigrateQueue(params)
if (res?.data?.code !== 200) throw new Error(extractApiError(res?.data, '加载迁移队列失败'))
const data = res.data.data || {}
revision.value = Number(data.revision) || revision.value
if (data.changed === false) return
queueRecords.value = Array.isArray(data.data) ? data.data : []
total.value = Number(data.all_count) || 0
Object.assign(concurrency, data.summary || {})
} catch (error) {
loadError.value = error.message || extractApiError(error?.response?.data, '加载迁移队列失败')
if (!silent) ElMessage.error(loadError.value)
@@ -233,6 +214,8 @@ const submitConcurrency = async () => {
const result = res.data.data || {}
ElMessage.success(`并发数已从 ${result.old_concurrency ?? concurrency.concurrency} 调整为 ${result.new_concurrency ?? concurrencyForm.value},放行 ${result.released_count || 0} 项任务`)
concurrencyVisible.value = false
if (result.summary) Object.assign(concurrency, result.summary)
revision.value = 0
await loadData()
} catch (error) {
ElMessage.error(error.message || extractApiError(error?.response?.data, '更新迁移并发失败'))
@@ -247,14 +230,15 @@ const abortMigration = async row => {
confirmButtonText: '确定中断', cancelButtonText: '取消', type: 'warning'
})
} catch { return }
abortingId.value = row.id
abortingId.value = row.migration_record_id
try {
const formData = new FormData()
formData.append('service_id', row.source_service_id)
formData.append('vm_id', row.source_vm_id)
const res = await abortDataMigrate(formData)
formData.append('migration_record_id', row.migration_record_id)
const res = await abortDataMigrateQueue(formData)
if (res?.data?.code !== 200) throw new Error(extractApiError(res?.data, '中断迁移失败'))
ElMessage.success('迁移任务已中断')
const result = res.data.data || {}
ElMessage.success(`迁移任务已中断,释放 ${result.released_count || 0} 个队列任务`)
revision.value = 0
await loadData()
} catch (error) {
ElMessage.error(error.message || extractApiError(error?.response?.data, '中断迁移失败'))
@@ -269,12 +253,31 @@ const goVm = (targetServiceId, vmId) => router.push({ path: '/virtualization/vm-
const goUser = userId => router.push({ path: '/user/detail', query: { user_id: userId } })
const goUserGoods = id => router.push(`/user-goods/detail/${id}`)
const startPolling = () => {
if (!refreshTimer) refreshTimer = window.setInterval(() => loadData({ silent: true }), 5000)
}
const stopPolling = () => {
if (refreshTimer) window.clearInterval(refreshTimer)
refreshTimer = null
}
const handlePageChange = () => { revision.value = 0; loadData() }
const handleSizeChange = () => { page.value = 1; revision.value = 0; loadData() }
onMounted(async () => {
await loadData()
refreshTimer = window.setInterval(() => loadData({ silent: true }), 5000)
if (!activeDetailTab || activeDetailTab.value === 'migration_queue') {
await loadData()
startPolling()
}
})
watch(() => activeDetailTab?.value, async tab => {
if (tab === 'migration_queue') {
revision.value = 0
await loadData()
startPolling()
} else stopPolling()
})
onBeforeUnmount(() => {
if (refreshTimer) window.clearInterval(refreshTimer)
stopPolling()
})
</script>
@@ -299,6 +302,7 @@ onBeforeUnmount(() => {
.stack-cell { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }
.progress-meta { display: flex; justify-content: space-between; gap: 8px; margin-top: 3px; }
.dialog-alert { margin-bottom: 18px; }
.queue-pagination { justify-content: flex-end; margin-top: 18px; }
.form-hint { margin-left: 10px; color: #909399; font-size: 12px; }
@media (max-width: 900px) {
.queue-header { flex-direction: column; }