feat: add brand asset cleanup worker and related functionality
- 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:
@@ -592,13 +592,13 @@ function handlePromptSaved(ruleId: number): void {
|
||||
}
|
||||
|
||||
function toggleSchedulePublishAccount(account: PublishAccountCard): void {
|
||||
if (!account.selectable) {
|
||||
return
|
||||
}
|
||||
if (form.publishAccountIds.includes(account.id)) {
|
||||
form.publishAccountIds = form.publishAccountIds.filter((id) => id !== account.id)
|
||||
return
|
||||
}
|
||||
if (!account.selectable) {
|
||||
return
|
||||
}
|
||||
form.publishAccountIds = [...form.publishAccountIds, account.id]
|
||||
}
|
||||
|
||||
@@ -606,6 +606,10 @@ function isSchedulePublishAccountSelected(accountId: string): boolean {
|
||||
return form.publishAccountIds.includes(accountId)
|
||||
}
|
||||
|
||||
function canToggleSchedulePublishAccount(account: PublishAccountCard): boolean {
|
||||
return account.selectable || isSchedulePublishAccountSelected(account.id)
|
||||
}
|
||||
|
||||
function buildScheduleEnterpriseSiteTargets(): ScheduleEnterpriseSiteTarget[] {
|
||||
return form.publishEnterpriseSiteIds
|
||||
.map((siteId) => ({
|
||||
@@ -1325,8 +1329,10 @@ function normalizeJsonRecord(value: Record<string, unknown>): Record<string, Jso
|
||||
'generate-task-drawer__account--active': isSchedulePublishAccountSelected(
|
||||
account.id,
|
||||
),
|
||||
'generate-task-drawer__account--disabled': !account.selectable,
|
||||
'generate-task-drawer__account--disabled':
|
||||
!canToggleSchedulePublishAccount(account),
|
||||
}"
|
||||
:aria-disabled="!canToggleSchedulePublishAccount(account)"
|
||||
@click="toggleSchedulePublishAccount(account)"
|
||||
>
|
||||
<span class="generate-task-drawer__account-check">
|
||||
|
||||
@@ -35,6 +35,7 @@ import type {
|
||||
CreatePublishBatchResponse,
|
||||
CreatePublishJobRequest,
|
||||
CreatePublishJobResponse,
|
||||
DeletePublishRecordResponse,
|
||||
DesktopAccountInfo,
|
||||
DesktopClientDownloadableRelease,
|
||||
DesktopClientDownloadableReleaseListResponse,
|
||||
@@ -351,8 +352,6 @@ const currentBrandScopedPathPatterns = [
|
||||
/^\/api\/tenant\/schedules(?:\/|$)/,
|
||||
/^\/api\/tenant\/instant-tasks(?:\/|$)/,
|
||||
/^\/api\/tenant\/publish-jobs(?:\/|$)/,
|
||||
/^\/api\/tenant\/publish-records(?:\/|$)/,
|
||||
/^\/api\/tenant\/publish-tasks\/[^/]+\/retry(?:\/|$)/,
|
||||
/^\/api\/tenant\/enterprise-sites\/[^/]+\/publish(?:\/|$)/,
|
||||
/^\/api\/tenant\/media-supply\/orders(?:\/|$)/,
|
||||
/^\/api\/tenant\/prompt-rules(?:\/|$)/,
|
||||
@@ -1396,6 +1395,12 @@ export const publishTasksApi = {
|
||||
params,
|
||||
})
|
||||
},
|
||||
cancel(taskId: string) {
|
||||
return apiClient.post<unknown, { reason: string }>(
|
||||
`/api/tenant/tasks/${encodeURIComponent(taskId)}/cancel`,
|
||||
{ reason: 'tenant_cancelled_publish' },
|
||||
)
|
||||
},
|
||||
retry(taskId: string) {
|
||||
return apiClient.post<CreatePublishJobResponse>(
|
||||
`/api/tenant/publish-tasks/${encodeURIComponent(taskId)}/retry`,
|
||||
@@ -1409,6 +1414,11 @@ export const publishRecordsApi = {
|
||||
params,
|
||||
})
|
||||
},
|
||||
remove(recordId: number) {
|
||||
return apiClient.remove<DeletePublishRecordResponse>(
|
||||
`/api/tenant/publish-records/${encodeURIComponent(String(recordId))}`,
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const enterpriseSitesApi = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user