feat: 实现清理失效账号安全方案(包含预览、SSE测试、安全删除以及本地标记清空)

This commit is contained in:
liumangmang
2026-07-02 00:19:26 +08:00
parent 1ef6188c02
commit 536c0aa3d4
6 changed files with 798 additions and 3 deletions
+17
View File
@@ -330,6 +330,19 @@ export interface OrganizeGroupsItem {
message: string
}
export interface CleanupInvalidAccountsItem {
account_id: string
account_name: string | null
upstream_key_id: number | null
upstream_name: string | null
source_group_name: string | null
reason: string
test_status: string
cleanup_allowed: boolean
action_status: string | null
message: string
}
export const websitesApi = {
list: () => api.get<WebsiteData[]>('/api/websites'),
create: (data: WebsiteForm) => api.post<WebsiteData>('/api/websites', data),
@@ -357,6 +370,10 @@ export const websitesApi = {
}) => api.post<{ success: boolean; message: string; items: ImportAccountItem[] }>(`/api/websites/${id}/accounts/import-upstream-keys`, data),
organizeGroups: (id: number) =>
api.post<{ success: boolean; message: string; items: OrganizeGroupsItem[] }>(`/api/websites/${id}/groups/organize`),
cleanupPreview: (id: number) =>
api.post<{ success: boolean; message: string; items: CleanupInvalidAccountsItem[] }>(`/api/websites/${id}/accounts/cleanup-invalid/preview`),
cleanupExecute: (id: number) =>
api.post<{ success: boolean; message: string; items: CleanupInvalidAccountsItem[] }>(`/api/websites/${id}/accounts/cleanup-invalid/execute`),
listBindings: () => api.get<GroupBindingData[]>('/api/group-bindings'),
createBinding: (data: GroupBindingForm) => api.post<GroupBindingData>('/api/group-bindings', data),
updateBinding: (id: number, data: Partial<GroupBindingForm>) => api.put<GroupBindingData>(`/api/group-bindings/${id}`, data),
+159
View File
@@ -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 })