feat: 实现清理失效账号安全方案(包含预览、SSE测试、安全删除以及本地标记清空)
This commit is contained in:
@@ -124,6 +124,15 @@
|
||||
>
|
||||
一键整理分组
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
:disabled="!selectedWebsite"
|
||||
@click="openCleanupDialog"
|
||||
title="清理当前网站已导入但失效/孤立的账号"
|
||||
>
|
||||
清理失效账号
|
||||
</el-button>
|
||||
<el-button size="small" text :disabled="websites.length === 0" @click="openBindingCreate(selectedWebsite || websites[0])">新增绑定</el-button>
|
||||
</div>
|
||||
<div class="binding-list" v-loading="bindingLoading">
|
||||
@@ -741,6 +750,97 @@
|
||||
<el-button @click="organizeDialog = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 清理失效账号弹窗 -->
|
||||
<el-dialog
|
||||
v-model="cleanupDialog"
|
||||
title="清理失效账号候选预览"
|
||||
width="1000px"
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-loading="cleanupLoading">
|
||||
<el-alert
|
||||
title="清理规则说明"
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-bottom: 15px;"
|
||||
>
|
||||
<div style="font-size: 13px; line-height: 1.5;">
|
||||
系统已自动分析出可能已经失效、上游孤立或属于脏数据的账号候选。<br />
|
||||
<strong>是否将删除</strong> 字段是由后端调用目标站账号测试接口的结果判定。只有测试明确失败(非正常响应)或 404 的账号,才会被允许清理。<br />
|
||||
“确认清理”时,后端会重新测试 and 验证所有账号状态,只清理最终判定为失效的项。无 SmartUp 导入备注的手工账号不会被处理。
|
||||
</div>
|
||||
</el-alert>
|
||||
|
||||
<el-table :data="cleanupItems" border stripe style="width: 100%; max-height: 500px; overflow-y: auto;">
|
||||
<el-table-column prop="account_id" label="账号 ID" width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="account_name" label="账号名" width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="upstream_name" label="来源上游" width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.upstream_name" size="small">{{ row.upstream_name }}</el-tag>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="source_group_name" label="来源分组" width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.source_group_name" type="info" size="small">{{ row.source_group_name }}</el-tag>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="upstream_key_id" label="Key ID" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.upstream_key_id" style="font-family: monospace;">#{{ row.upstream_key_id }}</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reason" label="原因" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="test_status" label="测试结果" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.test_status === 'test_passed'" type="success" size="small">测试通过</el-tag>
|
||||
<el-tag v-else-if="row.test_status === 'failed'" type="danger" size="small">测试失败</el-tag>
|
||||
<el-tag v-else-if="row.test_status === '404'" type="warning" size="small">404 不存在</el-tag>
|
||||
<el-tag v-else type="info" size="small">未知 / 异常</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="cleanup_allowed" label="是否将删除" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.cleanup_allowed" type="danger" effect="dark" size="small">将删除</el-tag>
|
||||
<el-tag v-else type="info" size="small">跳过</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="cleanupHasExecuted" prop="action_status" label="执行结果" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.action_status === 'deleted'" type="success" effect="dark" size="small">已删除</el-tag>
|
||||
<el-tag v-else-if="row.action_status === 'already_deleted'" type="warning" effect="dark" size="small">已不存在</el-tag>
|
||||
<el-tag v-else-if="row.action_status === 'skipped'" type="info" size="small">已跳过</el-tag>
|
||||
<el-tag v-else-if="row.action_status === 'failed'" type="danger" effect="dark" size="small">删除失败</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div v-if="cleanupItems.length === 0" style="padding: 30px; text-align: center; color: var(--el-text-color-secondary);">
|
||||
没有找到可以清理的失效或孤立账号。
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cleanupDialog = false" :disabled="cleanupExecuting">
|
||||
{{ cleanupHasExecuted ? '关闭' : '取消' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!cleanupHasExecuted"
|
||||
type="danger"
|
||||
:loading="cleanupExecuting"
|
||||
:disabled="cleanupLoading || cleanupItems.filter(i => i.cleanup_allowed).length === 0"
|
||||
@click="executeCleanup"
|
||||
>
|
||||
确认清理 ({{ cleanupItems.filter(i => i.cleanup_allowed).length }} 个)
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -765,6 +865,7 @@ import {
|
||||
type WebsiteGroup,
|
||||
type WebsiteSyncLog,
|
||||
type OrganizeGroupsItem,
|
||||
type CleanupInvalidAccountsItem,
|
||||
} from '@/api'
|
||||
|
||||
const websites = ref<(WebsiteData & { _testing?: boolean })[]>([])
|
||||
@@ -914,6 +1015,12 @@ const organizeLoading = ref(false)
|
||||
const organizeResults = ref<OrganizeGroupsItem[]>([])
|
||||
const organizeMessage = ref('')
|
||||
|
||||
const cleanupDialog = ref(false)
|
||||
const cleanupLoading = ref(false)
|
||||
const cleanupExecuting = ref(false)
|
||||
const cleanupHasExecuted = ref(false)
|
||||
const cleanupItems = ref<CleanupInvalidAccountsItem[]>([])
|
||||
|
||||
const upstreamGroupOptions = computed(() => {
|
||||
const rows: Array<{ key: string; label: string; rate: string | number; source: BindingSourceGroup }> = []
|
||||
for (const upstream of upstreams.value) {
|
||||
@@ -1568,6 +1675,58 @@ async function organizeWebsiteGroups() {
|
||||
}
|
||||
}
|
||||
|
||||
async function openCleanupDialog() {
|
||||
if (!selectedWebsite.value) return
|
||||
cleanupItems.value = []
|
||||
cleanupHasExecuted.value = false
|
||||
cleanupDialog.value = true
|
||||
cleanupLoading.value = true
|
||||
try {
|
||||
const res = await websitesApi.cleanupPreview(selectedWebsite.value.id)
|
||||
if (res.data.success) {
|
||||
cleanupItems.value = res.data.items
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '加载失效账号候选预览失败')
|
||||
cleanupDialog.value = false
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.response?.data?.detail || '加载失效账号候选预览失败')
|
||||
cleanupDialog.value = false
|
||||
} finally {
|
||||
cleanupLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function executeCleanup() {
|
||||
if (!selectedWebsite.value) return
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确认清理失效账号?此操作将永久从目标站删除测试失败或不存在的账号,本地 orphaned 键将清空导入关联。',
|
||||
'确认清理',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
cleanupExecuting.value = true
|
||||
try {
|
||||
const res = await websitesApi.cleanupExecute(selectedWebsite.value.id)
|
||||
if (res.data.success) {
|
||||
cleanupItems.value = res.data.items
|
||||
cleanupHasExecuted.value = true
|
||||
ElMessage.success(res.data.message || '清理完成')
|
||||
await Promise.all([loadLogs(), loadWebsiteGroups(), loadBindings()])
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '执行清理失败')
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.response?.data?.detail || '执行清理失败')
|
||||
} finally {
|
||||
cleanupExecuting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleBinding(row: GroupBindingData) {
|
||||
try {
|
||||
await websitesApi.updateBinding(row.id, { enabled: row.enabled })
|
||||
|
||||
Reference in New Issue
Block a user