feat: add brand asset cleanup worker and related functionality
Frontend CI / Frontend (push) Successful in 3m23s
Backend CI / Backend (push) Failing after 6m46s

- Implemented a new BrandAssetCleanupWorker to handle the cleanup of brand-related assets after a brand is deleted.
- Added SQL queries for cleaning up articles, keywords, questions, competitors, and monitoring data associated with a brand.
- Introduced a new API endpoint to delete publish records.
- Updated the router to include the new delete publish record endpoint.
- Added tests for the BrandAssetCleanupWorker to ensure proper functionality.
- Created migration scripts to support soft deletion of publish records and to add the brand asset cleanup scheduler job.
This commit is contained in:
2026-06-19 11:45:13 +08:00
parent 97ae601cfd
commit 6e0519a232
29 changed files with 1621 additions and 54 deletions
@@ -2,11 +2,13 @@
import {
ClockCircleOutlined,
CopyOutlined,
DeleteOutlined,
DesktopOutlined,
ExportOutlined,
ReloadOutlined,
RetweetOutlined,
SearchOutlined,
StopOutlined,
} from '@ant-design/icons-vue'
import type { PublishRecord, PublishRecordListResponse } from '@geo/shared-types'
import { useQuery } from '@tanstack/vue-query'
@@ -21,7 +23,6 @@ import { formatDateTime } from '@/lib/display'
import { formatError, formatStoredErrorMessage } from '@/lib/errors'
import { resolvePlatformLogoUrl } from '@/lib/publish-account-cards'
import { getPublishPlatformMeta, normalizePublishPlatformId } from '@/lib/publish-platforms'
import { useCompanyStore } from '@/stores/company'
const PAGE_SIZE = 10
const INLINE_ERROR_HEAD_CHARS = 52
@@ -35,6 +36,9 @@ interface PublishRecordItem {
recordId: number
title: string
articleId: number
brandId: number
brandName: string
brandDeleted: boolean
targetType: string
platform: string
platformName: string
@@ -184,22 +188,21 @@ function buildDesktopWorkbenchUrl(record: {
}
const { t } = useI18n()
const companyStore = useCompanyStore()
const page = ref(1)
const searchTitle = ref('')
const appliedTitle = ref('')
const copiedErrorTaskId = ref<string | null>(null)
const retryingTaskId = ref<string | null>(null)
const cancelingTaskId = ref<string | null>(null)
const deletingRecordId = ref<string | null>(null)
const manualRefreshing = ref(false)
const recordsQuery = useQuery({
queryKey: computed(() => [
'tenant',
'publish-records',
companyStore.currentBrandId,
{ page: page.value, page_size: PAGE_SIZE, title: appliedTitle.value },
]),
enabled: computed(() => Boolean(companyStore.currentBrandId)),
queryFn: () =>
publishRecordsApi.list({
page: page.value,
@@ -246,6 +249,9 @@ const publishRecords = computed<PublishRecordItem[]>(() =>
recordId: record.id,
title: extractString(record.article_title) ?? `文章 #${record.article_id}`,
articleId: record.article_id,
brandId: record.brand_id,
brandName: extractString(record.brand_name) ?? `公司 #${record.brand_id}`,
brandDeleted: Boolean(record.brand_deleted),
targetType: record.target_type ?? 'platform_account',
platform: platformId,
platformName: isEnterpriseSite ? '企业自建站点' : record.platform_name || platformMeta.name,
@@ -312,7 +318,11 @@ function rowClassName(record: PublishRecordItem): string {
}
function canRetryTask(record: PublishRecordItem): boolean {
return Boolean(record.desktopTaskId && record.status === 'failed')
return Boolean(record.desktopTaskId && record.status === 'failed' && !record.brandDeleted)
}
function canCancelTask(record: PublishRecordItem): boolean {
return Boolean(record.desktopTaskId && record.status === 'queued')
}
async function retryTask(record: PublishRecordItem): Promise<void> {
@@ -346,6 +356,38 @@ async function retryTask(record: PublishRecordItem): Promise<void> {
}
}
async function cancelTask(record: PublishRecordItem): Promise<void> {
if (!record.desktopTaskId) {
return
}
cancelingTaskId.value = record.id
try {
await publishTasksApi.cancel(record.desktopTaskId)
message.success('已取消等待发布任务')
await recordsQuery.refetch()
} catch (error) {
message.error(formatError(error))
} finally {
cancelingTaskId.value = null
}
}
async function deleteRecord(record: PublishRecordItem): Promise<void> {
deletingRecordId.value = record.id
try {
await publishRecordsApi.remove(record.recordId)
message.success('发布记录已删除')
if (publishRecords.value.length === 1 && page.value > 1) {
page.value -= 1
}
await recordsQuery.refetch()
} catch (error) {
message.error(formatError(error))
} finally {
deletingRecordId.value = null
}
}
async function copyErrorMessage(record: PublishRecordItem): Promise<void> {
if (!record.errorMessage) {
return
@@ -443,6 +485,12 @@ async function copyErrorMessage(record: PublishRecordItem): Promise<void> {
<template v-if="record.articleId">文章 #{{ record.articleId }}</template>
<template v-else>未关联文章</template>
</span>
<span class="publish-brand-line">
<span class="publish-brand-name">{{ record.brandName }}</span>
<a-tag v-if="record.brandDeleted" color="default" class="publish-brand-tag">
公司已删除
</a-tag>
</span>
</div>
</template>
@@ -567,11 +615,50 @@ async function copyErrorMessage(record: PublishRecordItem): Promise<void> {
<template #icon><RetweetOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip v-if="isPendingStatus(record.status)" title="发布进行中">
<a-popconfirm
v-if="canCancelTask(record)"
title="确认取消这个等待发布任务?"
ok-text="取消发布"
cancel-text="保留"
placement="topRight"
@confirm="cancelTask(record)"
>
<a-tooltip title="取消发布任务" placement="top">
<a-button
type="text"
class="publish-action-btn publish-action-btn--danger"
:loading="cancelingTaskId === record.id"
:aria-label="`取消发布任务:${record.title}`"
@click.stop
>
<template #icon><StopOutlined /></template>
</a-button>
</a-tooltip>
</a-popconfirm>
<a-tooltip v-else-if="isPendingStatus(record.status)" title="发布进行中">
<span class="publish-action-placeholder">
<ClockCircleOutlined />
</span>
</a-tooltip>
<a-popconfirm
title="确认删除这条发布记录?"
ok-text="删除"
cancel-text="取消"
placement="topRight"
@confirm="deleteRecord(record)"
>
<a-tooltip title="删除发布记录" placement="top">
<a-button
type="text"
class="publish-action-btn publish-action-btn--danger"
:loading="deletingRecordId === record.id"
:aria-label="`删除发布记录:${record.title}`"
@click.stop
>
<template #icon><DeleteOutlined /></template>
</a-button>
</a-tooltip>
</a-popconfirm>
</div>
</template>
</template>
@@ -746,6 +833,30 @@ async function copyErrorMessage(record: PublishRecordItem): Promise<void> {
font-weight: 500;
}
.publish-brand-line {
display: inline-flex;
min-width: 0;
max-width: 100%;
align-items: center;
gap: 6px;
margin-top: 3px;
color: #64748b;
font-size: 12px;
line-height: 1.5;
}
.publish-brand-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.publish-brand-tag {
flex: 0 0 auto;
margin-inline-end: 0;
font-size: 11px;
}
.publish-account-cell {
display: flex;
min-width: 0;
@@ -843,6 +954,11 @@ async function copyErrorMessage(record: PublishRecordItem): Promise<void> {
color: #1677ff;
}
.publish-action-btn--danger:hover:not(:disabled) {
background: #fff1f0;
color: #cf1322;
}
.publish-action-tooltip-wrap {
display: inline-flex;
}