feat(upstreams): add batch test-all / check-now-all endpoints
- POST /api/upstreams/test-all: batch connection test for all enabled
upstreams (no snapshot, no webhook); updates last_status, balance
- POST /api/upstreams/check-now-all: full batch sync (snapshot, diff,
webhook, key sync, priority sync); mirrors single check-now behavior
- Both routes are registered before /{uid} to avoid path capture
- Skips disabled upstreams (status=skipped); single failure does not
abort subsequent upstreams (serial execution)
- Returns UpstreamBatchActionResponse with per-item detail and summary
Refactor: extract _test_upstream_core(db, u) and _check_now_core(db, u)
- All four routes (single + batch × 2) now share the same core helpers
- Eliminates duplicate logic and future divergence risk
Frontend:
- Add UpstreamBatchActionResponse / Item / Summary TS types
- Add upstreamsApi.testAll() and upstreamsApi.checkNowAll()
- Add '一键测试' and '一键同步' buttons in Upstreams.vue toolbar
(order: 一键测试 → 一键同步 → 刷新 → 新增上游)
- Buttons disabled when list is empty or another batch op is running
- On completion: refresh list + ElMessageBox with per-item failure detail
This commit is contained in:
@@ -139,6 +139,28 @@ export interface GenerateKeysByGroupsForm {
|
||||
endpoint: string
|
||||
}
|
||||
|
||||
export interface UpstreamBatchActionItem {
|
||||
upstream_id: number
|
||||
upstream_name: string
|
||||
status: 'success' | 'failed' | 'skipped'
|
||||
message: string
|
||||
detail?: string | null
|
||||
}
|
||||
|
||||
export interface UpstreamBatchActionSummary {
|
||||
total: number
|
||||
success: number
|
||||
failed: number
|
||||
skipped: number
|
||||
}
|
||||
|
||||
export interface UpstreamBatchActionResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
summary: UpstreamBatchActionSummary
|
||||
items: UpstreamBatchActionItem[]
|
||||
}
|
||||
|
||||
export const upstreamsApi = {
|
||||
list: () => api.get<UpstreamData[]>('/api/upstreams'),
|
||||
create: (data: UpstreamForm) => api.post<UpstreamData>('/api/upstreams', data),
|
||||
@@ -152,6 +174,8 @@ export const upstreamsApi = {
|
||||
latestSnapshot: (id: number) => api.get(`/api/upstreams/${id}/snapshots/latest`),
|
||||
listSnapshots: (id: number, limit = 20, offset = 0) =>
|
||||
api.get<any[]>(`/api/upstreams/${id}/snapshots`, { params: { limit, offset } }),
|
||||
testAll: () => api.post<UpstreamBatchActionResponse>('/api/upstreams/test-all'),
|
||||
checkNowAll: () => api.post<UpstreamBatchActionResponse>('/api/upstreams/check-now-all'),
|
||||
}
|
||||
|
||||
// ——— Websites ———
|
||||
|
||||
@@ -7,6 +7,25 @@
|
||||
<h3 class="data-stage-title brand-type">检测与变更控制台</h3>
|
||||
</div>
|
||||
<div class="toolbar-cluster">
|
||||
<el-button
|
||||
id="btn-test-all"
|
||||
size="small"
|
||||
text
|
||||
:loading="testingAll"
|
||||
:disabled="list.length === 0 || testingAll || checkingAll"
|
||||
@click="testAll"
|
||||
title="批量测试所有启用上游的连接(不写快照)"
|
||||
>一键测试</el-button>
|
||||
<el-button
|
||||
id="btn-check-now-all"
|
||||
size="small"
|
||||
type="warning"
|
||||
plain
|
||||
:loading="checkingAll"
|
||||
:disabled="list.length === 0 || testingAll || checkingAll"
|
||||
@click="checkNowAll"
|
||||
title="批量完整同步所有启用上游(拉取倍率、写快照、触发 Webhook)"
|
||||
>一键同步</el-button>
|
||||
<el-button size="small" text @click="loadList" :loading="tableLoading">刷新</el-button>
|
||||
<el-button size="small" type="primary" @click="openCreate">新增上游</el-button>
|
||||
</div>
|
||||
@@ -397,11 +416,13 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import dayjs from 'dayjs'
|
||||
import { Refresh, Plus, Edit, List, Delete, Warning, Clock, ArrowRight, Pointer, Key } from '@element-plus/icons-vue'
|
||||
import { upstreamsApi, type GeneratedUpstreamKey, type UpstreamData } from '@/api'
|
||||
import { upstreamsApi, type GeneratedUpstreamKey, type UpstreamData, type UpstreamBatchActionResponse } from '@/api'
|
||||
import AuthCaptureDialog from '@/components/AuthCaptureDialog.vue'
|
||||
|
||||
const list = ref<(UpstreamData & { _testing?: boolean; _checking?: boolean })[]>([])
|
||||
const tableLoading = ref(false)
|
||||
const testingAll = ref(false)
|
||||
const checkingAll = ref(false)
|
||||
|
||||
const drawerVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
@@ -812,6 +833,54 @@ async function confirmDelete(row: UpstreamData) {
|
||||
}
|
||||
}
|
||||
|
||||
function _showBatchResult(res: UpstreamBatchActionResponse, actionLabel: string) {
|
||||
const { summary, items, message } = res
|
||||
const failed = items.filter(i => i.status === 'failed')
|
||||
|
||||
if (failed.length > 0) {
|
||||
const failedLines = failed
|
||||
.map(i => `• ${i.upstream_name}:${i.detail || i.message}`)
|
||||
.join('\n')
|
||||
ElMessageBox.alert(
|
||||
`${message}\n\n失败明细:\n${failedLines}`,
|
||||
`${actionLabel}完成`,
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: '知道了',
|
||||
customStyle: { whiteSpace: 'pre-wrap', maxHeight: '60vh', overflowY: 'auto' },
|
||||
}
|
||||
)
|
||||
} else {
|
||||
ElMessage[res.success ? 'success' : 'warning'](message)
|
||||
}
|
||||
}
|
||||
|
||||
async function testAll() {
|
||||
testingAll.value = true
|
||||
try {
|
||||
const res = await upstreamsApi.testAll()
|
||||
await loadList()
|
||||
_showBatchResult(res.data, '一键测试')
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.response?.data?.detail || '批量测试失败')
|
||||
} finally {
|
||||
testingAll.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function checkNowAll() {
|
||||
checkingAll.value = true
|
||||
try {
|
||||
const res = await upstreamsApi.checkNowAll()
|
||||
await loadList()
|
||||
_showBatchResult(res.data, '一键同步')
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.response?.data?.detail || '批量同步失败')
|
||||
} finally {
|
||||
checkingAll.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadList)
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user