diff --git a/frontend/src/views/service/Catalog.vue b/frontend/src/views/service/Catalog.vue index 94e1c71..65c0184 100644 --- a/frontend/src/views/service/Catalog.vue +++ b/frontend/src/views/service/Catalog.vue @@ -818,7 +818,7 @@ const canManageRollback = computed(() => authStore.isAdmin && lastDeliveryStatus const canRetryCloudDM = computed(() => deliveryRegisterFailed.value && Boolean(deploymentId.value)) const deliveryForm = reactive({ - instanceName: `mysql-${currentName.value}-billing-02`, + instanceName: generateInstanceName(activeServiceKey.value, currentName.value), version: 'MySQL 8.0', mode: 'single', spec: '1C / 2G', @@ -853,6 +853,13 @@ const collationMap: Record = { } const steps = ref([]) +const deliveryStageStepIndex: Record = { + precheck: 0, + install: 1, + configure: 2, + healthcheck: 3, + register: 4, +} const activeService = computed(() => basicServices.value.find((service) => service.key === activeServiceKey.value && !service.disabled)) const selectedTarget = computed(() => deliveryTargets.value.find((target) => target.id === selectedTargetId.value)) @@ -968,7 +975,7 @@ const deliveryStateClass = computed(() => { }) watch(currentName, (name) => { - deliveryForm.instanceName = `${activeServiceKey.value === 'mysql' ? 'mysql' : 'nginx'}-${name}-billing-02` + deliveryForm.instanceName = generateInstanceName(activeServiceKey.value, name) }) watch(selectedTargetId, () => { @@ -1120,8 +1127,11 @@ async function restoreWorkbench() { restoringWorkbench = true Object.assign(deliveryForm, snapshot.form) + if (!snapshot.deploymentId) { + deliveryForm.instanceName = generateInstanceName(activeServiceKey.value, currentName.value) + } deliveryForm.rootPassword = generateRootPassword() - precheckPassed.value = snapshot.precheckPassed + precheckPassed.value = snapshot.deploymentId ? snapshot.precheckPassed : false running.value = snapshot.running deliveryDone.value = snapshot.deliveryDone deliveryFailed.value = snapshot.deliveryFailed @@ -1145,7 +1155,8 @@ async function restoreWorkbench() { lastDeliveryStatus.value = data.task.status credentialAvailable.value = data.task.credential_available deliveryError.value = data.task.error_message || deliveryError.value - seenEventIds.value = new Set(data.events.map((event) => event.id)) + seenEventIds.value = new Set() + applyTaskEvents(data.events, false) applyDeliveryStatus(data.task.status, data.task.error_message || '') if (isTerminalDeliveryStatus(data.task.status)) { activeView.value = 'result' @@ -1173,8 +1184,7 @@ async function restoreWorkbench() { function hydrateServiceDefaults() { const service = activeService.value if (!service) return - const prefix = service.key === 'mysql' ? 'mysql' : 'nginx' - deliveryForm.instanceName = `${prefix}-${currentName.value}-billing-02` + deliveryForm.instanceName = generateInstanceName(service.key, currentName.value) deliveryForm.version = service.versions[0] || '' deliveryForm.mode = service.modes[1]?.value || service.modes[0]?.value || 'single' deliveryForm.spec = service.key === 'mysql' @@ -1235,6 +1245,7 @@ function resetExecutionState() { deliveredHost.value = '' deliveredPort.value = undefined deliveryLog.value = '[ready] 等待创建交付任务...' + seenEventIds.value = new Set() steps.value = defaultSteps().map((step) => ({ ...step, state: 'pending' })) } @@ -1357,7 +1368,7 @@ async function createTask() { upsertDeliveryHistory(task.id, task.status, task.error_message || '', task.created_at) deliveryLog.value += `\n[task] ${task.id} created by ${currentName.value}` applyDeliveryStatus(task.status, '') - startTaskPolling(task.id) + startTaskPolling(task.id, true) persistWorkbench() ElMessage.success('交付任务已创建') } catch (error) { @@ -1430,9 +1441,9 @@ function hostLabel(host: TargetHostOption) { return host.ip ? `${host.name} · ${host.ip}` : host.name } -function startTaskPolling(id: string) { +function startTaskPolling(id: string, resetEvents = false) { stopPolling() - seenEventIds.value = new Set() + if (resetEvents) seenEventIds.value = new Set() pollTask(id) pollTimer.value = window.setInterval(() => pollTask(id), 3000) } @@ -1461,14 +1472,30 @@ async function pollTask(id: string) { } } -function applyTaskEvents(events: TaskEvent[]) { +function applyTaskEvents(events: TaskEvent[], writeLogs = true) { events.forEach((event) => { if (seenEventIds.value.has(event.id)) return seenEventIds.value.add(event.id) - appendLog(`[${event.to_state}] ${event.message}`) + applyStageEvent(event) + if (writeLogs) appendLog(`[${event.to_state}] ${event.message}`) }) } +function applyStageEvent(event: TaskEvent) { + const stage = String(event.stage || '').toLowerCase() + const eventStatus = String(event.event_status || '').toLowerCase() + const stepIndex = deliveryStageStepIndex[stage] + if (stepIndex === undefined) return + const stateMap: Record = { + running: 'running', + success: 'done', + failed: 'failed', + } + const state = stateMap[eventStatus] + if (!state) return + setDeliveryStep(stepIndex, state) +} + function appendLog(message: unknown) { if (!message) return deliveryLog.value += `\n${String(message)}` @@ -1487,23 +1514,41 @@ async function copyText(value: string, message: string) { function applyDeliveryStatus(status: string, message: string) { lastDeliveryStatus.value = status if (message) appendLog(message) - if (['pending', 'validating', 'dispatching', 'running', 'registering', 'canceling', 'rollback_pending', 'rolling_back'].includes(status)) { + if (['pending', 'validating', 'dispatching', 'running'].includes(status)) { running.value = true - markStepRunning() + if (!steps.value.slice(0, 5).some((step) => step.state !== 'pending')) { + setDeliveryStep(0, 'running') + } + return + } + if (status === 'registering') { + running.value = true + setDeliveryStep(4, 'running') + return + } + if (status === 'canceling') { + running.value = true + return + } + if (['rollback_pending', 'rolling_back'].includes(status)) { + running.value = true + setDeliveryStep(5, 'running') return } if (status === 'finished') { running.value = false deliveryDone.value = true deliveryFailed.value = false - steps.value = steps.value.map((step) => ({ ...step, state: 'done' })) + steps.value = steps.value.map((step, index) => ( + index < 5 ? { ...step, state: 'done' } : step + )) return } if (status === 'rolled_back') { running.value = false deliveryDone.value = false deliveryFailed.value = true - steps.value = steps.value.map((step) => ({ ...step, state: 'done' })) + setDeliveryStep(5, 'done') return } if (['execution_failed', 'validation_failed', 'register_failed', 'rollback_failed', 'rollback_acknowledged', 'canceled'].includes(status)) { @@ -1511,7 +1556,11 @@ function applyDeliveryStatus(status: string, message: string) { deliveryDone.value = false deliveryFailed.value = true deliveryError.value = message || deliveryError.value - markCurrentStepFailed() + if (status === 'validation_failed') setDeliveryStep(0, 'failed') + else if (status === 'register_failed') setDeliveryStep(4, 'failed') + else if (status === 'rollback_failed') setDeliveryStep(5, 'failed') + else if (status === 'rollback_acknowledged') setDeliveryStep(5, 'done') + else markCurrentStepFailed() } } @@ -1519,7 +1568,8 @@ async function loadDeliveryTargets() { targetsLoading.value = true try { deliveryTargets.value = await deliveryApi.listTargets() - const preferred = deliveryTargets.value.find((target) => !/callback|rollback/i.test(target.name)) + const preferred = deliveryTargets.value.find((target) => /callback/i.test(target.name)) + || deliveryTargets.value.find((target) => !/rollback/i.test(target.name)) selectedTargetId.value = preferred?.id || deliveryTargets.value[0]?.id } catch (error) { ElMessage.error(error instanceof Error ? error.message : '获取部署目标失败') @@ -1623,6 +1673,17 @@ function normalizeDNSLabel(value: string) { return normalized || 'default' } +function generateInstanceName(serviceKey: string, businessLineName: string) { + const prefix = serviceKey === 'mysql' ? 'mysql' : 'nginx' + // 限长业务线段,确保 63 字符截断不会吃掉随机后缀(prefix 5 + 连字符 2 + 后缀 6 = 13) + const businessLine = normalizeDNSLabel(businessLineName).slice(0, 50).replace(/-+$/g, '') + const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789' + const values = new Uint32Array(6) + crypto.getRandomValues(values) + const suffix = Array.from(values, (value) => alphabet[value % alphabet.length]).join('') + return normalizeDNSLabel(`${prefix}-${businessLine}-${suffix}`) +} + function generateRootPassword() { const groups = [ 'ABCDEFGHJKLMNPQRSTUVWXYZ', @@ -1666,13 +1727,7 @@ function regenerateRootPassword() { } function regenerateInstanceName() { - const prefix = activeServiceKey.value === 'mysql' ? 'mysql' : 'service' - const businessLine = normalizeDNSLabel(currentName.value) - const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789' - const values = new Uint32Array(6) - crypto.getRandomValues(values) - const suffix = Array.from(values, (value) => alphabet[value % alphabet.length]).join('') - deliveryForm.instanceName = normalizeDNSLabel(`${prefix}-${businessLine}-${suffix}`) + deliveryForm.instanceName = generateInstanceName(activeServiceKey.value, currentName.value) precheckPassed.value = false ElMessage.success('已生成新的实例名称,请执行预检查确认可用') } @@ -1934,12 +1989,11 @@ async function releaseRollback() { } } -function markStepRunning() { - const index = steps.value.findIndex((step) => step.state === 'pending' || step.state === 'running') - if (index < 0) return +function setDeliveryStep(index: number, state: StepState) { + if (index < 0 || index >= steps.value.length) return steps.value = steps.value.map((step, stepIndex) => { - if (stepIndex < index) return { ...step, state: 'done' } - if (stepIndex === index) return { ...step, state: 'running' } + if (stepIndex < index && step.state !== 'failed') return { ...step, state: 'done' } + if (stepIndex === index) return { ...step, state } return step }) }