Preserve publish record history
This commit is contained in:
@@ -112,6 +112,12 @@ const publishRecordColumns = computed<TableColumnsType<PublishRecord>>(() => [
|
|||||||
key: 'published_at',
|
key: 'published_at',
|
||||||
width: 168,
|
width: 168,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '错误信息',
|
||||||
|
dataIndex: 'error_message',
|
||||||
|
key: 'error_message',
|
||||||
|
width: 220,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t('media.records.link'),
|
title: t('media.records.link'),
|
||||||
dataIndex: 'external_article_url',
|
dataIndex: 'external_article_url',
|
||||||
@@ -249,6 +255,18 @@ function shouldShowClientManageHint(record: PublishRecord): boolean {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function publishRecordError(record: PublishRecord): string {
|
||||||
|
return record.error_message?.trim() || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function publishRecordErrorPreview(record: PublishRecord): string {
|
||||||
|
const error = publishRecordError(record).replace(/\s+/g, ' ')
|
||||||
|
if (error.length <= 52) {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
return `${error.slice(0, 44)}...${error.slice(-5)}`
|
||||||
|
}
|
||||||
|
|
||||||
async function copyPublishLink(record: PublishRecord): Promise<void> {
|
async function copyPublishLink(record: PublishRecord): Promise<void> {
|
||||||
const target = resolvePublishLink(record)
|
const target = resolvePublishLink(record)
|
||||||
if (!target) {
|
if (!target) {
|
||||||
@@ -399,6 +417,14 @@ async function copyPublishLink(record: PublishRecord): Promise<void> {
|
|||||||
<template v-else-if="column.key === 'published_at'">
|
<template v-else-if="column.key === 'published_at'">
|
||||||
{{ formatDateTime(record.published_at) }}
|
{{ formatDateTime(record.published_at) }}
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="column.key === 'error_message'">
|
||||||
|
<a-tooltip v-if="publishRecordError(record)" :title="publishRecordError(record)">
|
||||||
|
<span class="article-drawer__record-error">
|
||||||
|
{{ publishRecordErrorPreview(record) }}
|
||||||
|
</span>
|
||||||
|
</a-tooltip>
|
||||||
|
<span v-else class="article-drawer__record-empty">--</span>
|
||||||
|
</template>
|
||||||
<template v-else-if="column.key === 'external_article_url'">
|
<template v-else-if="column.key === 'external_article_url'">
|
||||||
<div v-if="resolvePublishLink(record)" class="article-drawer__record-link-row">
|
<div v-if="resolvePublishLink(record)" class="article-drawer__record-link-row">
|
||||||
<a
|
<a
|
||||||
@@ -567,6 +593,18 @@ async function copyPublishLink(record: PublishRecord): Promise<void> {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.article-drawer__record-error {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 190px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #cf1322;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
vertical-align: middle;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.article-drawer__record-link {
|
.article-drawer__record-link {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { QuestionCircleOutlined, UserOutlined } from '@ant-design/icons-vue'
|
import { QuestionCircleOutlined, UserOutlined } from '@ant-design/icons-vue'
|
||||||
import type { MediaPlatform } from '@geo/shared-types'
|
import type { MediaPlatform, PublishRecord } from '@geo/shared-types'
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
@@ -80,15 +80,21 @@ const platformMap = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const platformEntries = computed<PublishPlatformEntry[]>(() => {
|
const platformEntries = computed<PublishPlatformEntry[]>(() => {
|
||||||
const entries: PublishPlatformEntry[] = []
|
const entriesByTarget = new Map<string, PublishPlatformEntry>()
|
||||||
|
const latestRecords = [...(publishRecordsQuery.data.value ?? [])].sort(comparePublishRecordsDesc)
|
||||||
|
|
||||||
for (const record of publishRecordsQuery.data.value ?? []) {
|
for (const record of latestRecords) {
|
||||||
const targetType = String(record.target_type ?? 'platform_account')
|
const targetType = String(record.target_type ?? 'platform_account')
|
||||||
const normalizedId = normalizePublishPlatformId(record.platform_id)
|
const normalizedId = normalizePublishPlatformId(record.platform_id)
|
||||||
const platform = platformMap.value.get(normalizedId)
|
const platform = platformMap.value.get(normalizedId)
|
||||||
const fallback = enterprisePlatformFallback(normalizedId)
|
const fallback = enterprisePlatformFallback(normalizedId)
|
||||||
|
const targetKey = publishRecordTargetKey(record, targetType, normalizedId)
|
||||||
|
|
||||||
entries.push({
|
if (entriesByTarget.has(targetKey)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
entriesByTarget.set(targetKey, {
|
||||||
key: `${targetType}:${record.id}`,
|
key: `${targetType}:${record.id}`,
|
||||||
name: record.platform_name || platform?.name || fallback.name,
|
name: record.platform_name || platform?.name || fallback.name,
|
||||||
shortName: platform?.short_name || fallback.shortName,
|
shortName: platform?.short_name || fallback.shortName,
|
||||||
@@ -100,7 +106,7 @@ const platformEntries = computed<PublishPlatformEntry[]>(() => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries
|
return [...entriesByTarget.values()]
|
||||||
})
|
})
|
||||||
|
|
||||||
const successEntries = computed(() =>
|
const successEntries = computed(() =>
|
||||||
@@ -224,6 +230,45 @@ function normalizePublishRecordStatus(status?: string | null): PublishRecordStat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function publishRecordTargetKey(
|
||||||
|
record: PublishRecord,
|
||||||
|
targetType: string,
|
||||||
|
normalizedPlatformId: string,
|
||||||
|
): string {
|
||||||
|
if (targetType === 'enterprise_site') {
|
||||||
|
return `${targetType}:${record.target_connection_id ?? normalizedPlatformId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountKey =
|
||||||
|
normalizeTargetKeyPart(record.platform_uid) ||
|
||||||
|
normalizeTargetKeyPart(record.desktop_account_id) ||
|
||||||
|
normalizeTargetKeyPart(record.platform_account_id) ||
|
||||||
|
normalizeTargetKeyPart(record.platform_nickname) ||
|
||||||
|
'unknown'
|
||||||
|
|
||||||
|
return `${targetType}:${normalizedPlatformId}:${accountKey}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTargetKeyPart(value: unknown): string {
|
||||||
|
return String(value ?? '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparePublishRecordsDesc(
|
||||||
|
a: PublishRecord,
|
||||||
|
b: PublishRecord,
|
||||||
|
): number {
|
||||||
|
const aTime = Date.parse(a.created_at || a.updated_at || '')
|
||||||
|
const bTime = Date.parse(b.created_at || b.updated_at || '')
|
||||||
|
const normalizedATime = Number.isFinite(aTime) ? aTime : 0
|
||||||
|
const normalizedBTime = Number.isFinite(bTime) ? bTime : 0
|
||||||
|
|
||||||
|
if (normalizedATime !== normalizedBTime) {
|
||||||
|
return normalizedBTime - normalizedATime
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(b.id ?? 0) - Number(a.id ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
function getAccountInitial(accountName: string): string {
|
function getAccountInitial(accountName: string): string {
|
||||||
const trimmed = String(accountName ?? '').trim()
|
const trimmed = String(accountName ?? '').trim()
|
||||||
if (!trimmed || trimmed === '--') {
|
if (!trimmed || trimmed === '--') {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import {
|
import {
|
||||||
ClockCircleOutlined,
|
ClockCircleOutlined,
|
||||||
CopyOutlined,
|
CopyOutlined,
|
||||||
DeleteOutlined,
|
|
||||||
DesktopOutlined,
|
DesktopOutlined,
|
||||||
ExportOutlined,
|
ExportOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
@@ -194,7 +193,6 @@ const appliedTitle = ref('')
|
|||||||
const copiedErrorTaskId = ref<string | null>(null)
|
const copiedErrorTaskId = ref<string | null>(null)
|
||||||
const retryingTaskId = ref<string | null>(null)
|
const retryingTaskId = ref<string | null>(null)
|
||||||
const cancelingTaskId = ref<string | null>(null)
|
const cancelingTaskId = ref<string | null>(null)
|
||||||
const deletingRecordId = ref<string | null>(null)
|
|
||||||
const manualRefreshing = ref(false)
|
const manualRefreshing = ref(false)
|
||||||
|
|
||||||
const recordsQuery = useQuery({
|
const recordsQuery = useQuery({
|
||||||
@@ -372,22 +370,6 @@ async function cancelTask(record: PublishRecordItem): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
async function copyErrorMessage(record: PublishRecordItem): Promise<void> {
|
||||||
if (!record.errorMessage) {
|
if (!record.errorMessage) {
|
||||||
return
|
return
|
||||||
@@ -640,25 +622,6 @@ async function copyErrorMessage(record: PublishRecordItem): Promise<void> {
|
|||||||
<ClockCircleOutlined />
|
<ClockCircleOutlined />
|
||||||
</span>
|
</span>
|
||||||
</a-tooltip>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1269,6 +1269,7 @@ export interface PublishRecord {
|
|||||||
brand_deleted?: boolean
|
brand_deleted?: boolean
|
||||||
article_title?: string | null
|
article_title?: string | null
|
||||||
platform_account_id?: number | null
|
platform_account_id?: number | null
|
||||||
|
platform_uid?: string | null
|
||||||
desktop_account_id: string | null
|
desktop_account_id: string | null
|
||||||
desktop_task_id?: string | null
|
desktop_task_id?: string | null
|
||||||
target_type?: 'platform_account' | 'enterprise_site' | string
|
target_type?: 'platform_account' | 'enterprise_site' | string
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ type PublishRecordResponse struct {
|
|||||||
BrandDeleted bool `json:"brand_deleted"`
|
BrandDeleted bool `json:"brand_deleted"`
|
||||||
ArticleTitle *string `json:"article_title,omitempty"`
|
ArticleTitle *string `json:"article_title,omitempty"`
|
||||||
PlatformAccountID *int64 `json:"platform_account_id"`
|
PlatformAccountID *int64 `json:"platform_account_id"`
|
||||||
|
PlatformUID *string `json:"platform_uid,omitempty"`
|
||||||
DesktopAccountID *string `json:"desktop_account_id"`
|
DesktopAccountID *string `json:"desktop_account_id"`
|
||||||
DesktopTaskID *string `json:"desktop_task_id,omitempty"`
|
DesktopTaskID *string `json:"desktop_task_id,omitempty"`
|
||||||
TargetType string `json:"target_type"`
|
TargetType string `json:"target_type"`
|
||||||
@@ -81,6 +82,13 @@ type DeletePublishRecordResponse struct {
|
|||||||
Deleted bool `json:"deleted"`
|
Deleted bool `json:"deleted"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const visiblePublishRecordWhereSQL = `
|
||||||
|
AND (
|
||||||
|
pr.deleted_at IS NULL
|
||||||
|
OR pr.status IN ('success', 'published', 'publish_success', 'failed', 'publish_failed', 'cancelled', 'canceled')
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
func (s *MediaService) ListPlatforms(ctx context.Context) ([]MediaPlatformResponse, error) {
|
func (s *MediaService) ListPlatforms(ctx context.Context) ([]MediaPlatformResponse, error) {
|
||||||
rows, err := s.pool.Query(ctx, `
|
rows, err := s.pool.Query(ctx, `
|
||||||
SELECT id, platform_id, name, category, short_name, accent_color, login_url, logo_url, status, sort_order
|
SELECT id, platform_id, name, category, short_name, accent_color, login_url, logo_url, status, sort_order
|
||||||
@@ -191,7 +199,7 @@ func (s *MediaService) listPublishRecordsByStatuses(
|
|||||||
SELECT pr.id, pr.publish_batch_id, pr.article_id,
|
SELECT pr.id, pr.publish_batch_id, pr.article_id,
|
||||||
a.brand_id, b.name AS brand_name, (b.id IS NULL OR b.deleted_at IS NOT NULL OR b.status IN ('deleting', 'deleted')) AS brand_deleted,
|
a.brand_id, b.name AS brand_name, (b.id IS NULL OR b.deleted_at IS NOT NULL OR b.status IN ('deleting', 'deleted')) AS brand_deleted,
|
||||||
COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', '')) AS article_title,
|
COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', '')) AS article_title,
|
||||||
pr.platform_account_id, pa.desktop_id::text, dt.desktop_id::text,
|
pr.platform_account_id, pa.platform_uid, pa.desktop_id::text, dt.desktop_id::text,
|
||||||
COALESCE(pr.target_type, 'platform_account'), pr.target_connection_id, pr.platform_id,
|
COALESCE(pr.target_type, 'platform_account'), pr.target_connection_id, pr.platform_id,
|
||||||
COALESCE(mp.name, esc.site_name, esc.name, pr.platform_id) AS platform_name,
|
COALESCE(mp.name, esc.site_name, esc.name, pr.platform_id) AS platform_name,
|
||||||
CASE
|
CASE
|
||||||
@@ -227,7 +235,7 @@ func (s *MediaService) listPublishRecordsByStatuses(
|
|||||||
AND pb.tenant_id = pr.tenant_id
|
AND pb.tenant_id = pr.tenant_id
|
||||||
AND pb.initiator_user_id = $2
|
AND pb.initiator_user_id = $2
|
||||||
)
|
)
|
||||||
AND pr.deleted_at IS NULL
|
` + visiblePublishRecordWhereSQL + `
|
||||||
AND pr.status = ANY($3)
|
AND pr.status = ANY($3)
|
||||||
AND (
|
AND (
|
||||||
$4 = ''
|
$4 = ''
|
||||||
@@ -264,6 +272,7 @@ func (s *MediaService) listPublishRecordsByStatuses(
|
|||||||
&item.BrandDeleted,
|
&item.BrandDeleted,
|
||||||
&item.ArticleTitle,
|
&item.ArticleTitle,
|
||||||
&item.PlatformAccountID,
|
&item.PlatformAccountID,
|
||||||
|
&item.PlatformUID,
|
||||||
&desktopAccountID,
|
&desktopAccountID,
|
||||||
&desktopTaskID,
|
&desktopTaskID,
|
||||||
&item.TargetType,
|
&item.TargetType,
|
||||||
@@ -318,7 +327,10 @@ func (s *MediaService) countPublishRecordsByStatuses(
|
|||||||
AND pb.tenant_id = pr.tenant_id
|
AND pb.tenant_id = pr.tenant_id
|
||||||
AND pb.initiator_user_id = $2
|
AND pb.initiator_user_id = $2
|
||||||
)
|
)
|
||||||
AND pr.deleted_at IS NULL
|
AND (
|
||||||
|
pr.deleted_at IS NULL
|
||||||
|
OR pr.status IN ('success', 'published', 'publish_success', 'failed', 'publish_failed', 'cancelled', 'canceled')
|
||||||
|
)
|
||||||
AND pr.status = ANY($3)
|
AND pr.status = ANY($3)
|
||||||
AND (
|
AND (
|
||||||
$4 = ''
|
$4 = ''
|
||||||
@@ -344,7 +356,7 @@ func (s *MediaService) listArticlePublishRecords(ctx context.Context, tenantID,
|
|||||||
rows, err := s.pool.Query(ctx, `
|
rows, err := s.pool.Query(ctx, `
|
||||||
SELECT pr.id, pr.publish_batch_id, pr.article_id,
|
SELECT pr.id, pr.publish_batch_id, pr.article_id,
|
||||||
a.brand_id, b.name AS brand_name, (b.id IS NULL OR b.deleted_at IS NOT NULL OR b.status IN ('deleting', 'deleted')) AS brand_deleted,
|
a.brand_id, b.name AS brand_name, (b.id IS NULL OR b.deleted_at IS NOT NULL OR b.status IN ('deleting', 'deleted')) AS brand_deleted,
|
||||||
pr.platform_account_id, pa.desktop_id::text,
|
pr.platform_account_id, pa.platform_uid, pa.desktop_id::text,
|
||||||
COALESCE(pr.target_type, 'platform_account'), pr.target_connection_id, pr.platform_id,
|
COALESCE(pr.target_type, 'platform_account'), pr.target_connection_id, pr.platform_id,
|
||||||
COALESCE(mp.name, esc.site_name, esc.name, pr.platform_id) AS platform_name,
|
COALESCE(mp.name, esc.site_name, esc.name, pr.platform_id) AS platform_name,
|
||||||
CASE
|
CASE
|
||||||
@@ -362,7 +374,14 @@ func (s *MediaService) listArticlePublishRecords(ctx context.Context, tenantID,
|
|||||||
AND esc.tenant_id = pr.tenant_id
|
AND esc.tenant_id = pr.tenant_id
|
||||||
JOIN articles a ON a.id = pr.article_id AND a.tenant_id = pr.tenant_id
|
JOIN articles a ON a.id = pr.article_id AND a.tenant_id = pr.tenant_id
|
||||||
LEFT JOIN brands b ON b.id = a.brand_id AND b.tenant_id = a.tenant_id
|
LEFT JOIN brands b ON b.id = a.brand_id AND b.tenant_id = a.tenant_id
|
||||||
WHERE pr.tenant_id = $1 AND pr.article_id = $2 AND a.brand_id = $3 AND a.deleted_at IS NULL AND pr.deleted_at IS NULL
|
WHERE pr.tenant_id = $1
|
||||||
|
AND pr.article_id = $2
|
||||||
|
AND a.brand_id = $3
|
||||||
|
AND a.deleted_at IS NULL
|
||||||
|
AND (
|
||||||
|
pr.deleted_at IS NULL
|
||||||
|
OR pr.status IN ('success', 'published', 'publish_success', 'failed', 'publish_failed', 'cancelled', 'canceled')
|
||||||
|
)
|
||||||
ORDER BY pr.created_at DESC, pr.id DESC
|
ORDER BY pr.created_at DESC, pr.id DESC
|
||||||
`, tenantID, articleID, brandID)
|
`, tenantID, articleID, brandID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -382,6 +401,7 @@ func (s *MediaService) listArticlePublishRecords(ctx context.Context, tenantID,
|
|||||||
&item.BrandName,
|
&item.BrandName,
|
||||||
&item.BrandDeleted,
|
&item.BrandDeleted,
|
||||||
&item.PlatformAccountID,
|
&item.PlatformAccountID,
|
||||||
|
&item.PlatformUID,
|
||||||
&desktopAccountID,
|
&desktopAccountID,
|
||||||
&item.TargetType,
|
&item.TargetType,
|
||||||
&item.TargetConnectionID,
|
&item.TargetConnectionID,
|
||||||
@@ -456,6 +476,10 @@ func (s *MediaService) DeletePublishRecord(ctx context.Context, recordID int64)
|
|||||||
return nil, response.ErrInternal(50044, "publish_record_delete_lookup_failed", "failed to delete publish record")
|
return nil, response.ErrInternal(50044, "publish_record_delete_lookup_failed", "failed to delete publish record")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !publishRecordMayBeDiscarded(publishStatus) {
|
||||||
|
return nil, response.ErrConflict(40996, "publish_record_history_locked", "completed publish records are kept for retry and audit")
|
||||||
|
}
|
||||||
|
|
||||||
if tag, err := tx.Exec(ctx, `
|
if tag, err := tx.Exec(ctx, `
|
||||||
UPDATE publish_records
|
UPDATE publish_records
|
||||||
SET deleted_at = NOW(),
|
SET deleted_at = NOW(),
|
||||||
@@ -469,7 +493,7 @@ func (s *MediaService) DeletePublishRecord(ctx context.Context, recordID int64)
|
|||||||
return nil, response.ErrNotFound(40435, "publish_record_not_found", "publish record not found")
|
return nil, response.ErrNotFound(40435, "publish_record_not_found", "publish record not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
if publishRecordMayHaveQueuedTask(publishStatus) {
|
if publishRecordMayBeDiscarded(publishStatus) {
|
||||||
if _, err := tx.Exec(ctx, `
|
if _, err := tx.Exec(ctx, `
|
||||||
UPDATE desktop_tasks
|
UPDATE desktop_tasks
|
||||||
SET status = 'aborted',
|
SET status = 'aborted',
|
||||||
@@ -535,9 +559,9 @@ func publishPlatformRequiresCover(platformID string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func publishRecordMayHaveQueuedTask(status string) bool {
|
func publishRecordMayBeDiscarded(status string) bool {
|
||||||
switch strings.TrimSpace(status) {
|
switch strings.TrimSpace(status) {
|
||||||
case "queued", "publishing", "pending", "running":
|
case "queued", "pending":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -923,7 +923,10 @@ func (s *PublishJobService) RetryTenantDesktopTask(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func shouldRequeuePublishTaskInsteadOfCreatingNew(task *repository.DesktopTask) bool {
|
func shouldRequeuePublishTaskInsteadOfCreatingNew(task *repository.DesktopTask) bool {
|
||||||
return task != nil && task.Kind == "publish" && task.Status == "unknown"
|
return task != nil &&
|
||||||
|
task.Kind == "publish" &&
|
||||||
|
task.Status == "unknown" &&
|
||||||
|
!publishUnknownTaskHasDefinitiveFailure(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PublishJobService) requeueUnknownPublishTask(
|
func (s *PublishJobService) requeueUnknownPublishTask(
|
||||||
|
|||||||
@@ -161,8 +161,16 @@ func TestShouldRequeuePublishTaskInsteadOfCreatingNewOnlyForUnknownPublish(t *te
|
|||||||
if !shouldRequeuePublishTaskInsteadOfCreatingNew(&repository.DesktopTask{
|
if !shouldRequeuePublishTaskInsteadOfCreatingNew(&repository.DesktopTask{
|
||||||
Kind: "publish",
|
Kind: "publish",
|
||||||
Status: "unknown",
|
Status: "unknown",
|
||||||
|
Error: []byte(`{"publish_submit_uncertain":true,"message":"network interrupted after submit"}`),
|
||||||
}) {
|
}) {
|
||||||
t.Fatal("unknown publish tasks should be retried by requeueing the existing task")
|
t.Fatal("submit-uncertain unknown publish tasks should be retried by requeueing the existing task")
|
||||||
|
}
|
||||||
|
if shouldRequeuePublishTaskInsteadOfCreatingNew(&repository.DesktopTask{
|
||||||
|
Kind: "publish",
|
||||||
|
Status: "unknown",
|
||||||
|
Error: []byte(`{"error_code":403,"error_msg":"CSRF token invalid"}`),
|
||||||
|
}) {
|
||||||
|
t.Fatal("definitive failed unknown publish tasks should create a new retry record")
|
||||||
}
|
}
|
||||||
if shouldRequeuePublishTaskInsteadOfCreatingNew(&repository.DesktopTask{
|
if shouldRequeuePublishTaskInsteadOfCreatingNew(&repository.DesktopTask{
|
||||||
Kind: "publish",
|
Kind: "publish",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -193,6 +194,33 @@ func TestDesktopTaskToPublishStatusMapsAbortToCancelled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPublishRecordMayBeDiscardedOnlyForQueuedRecords(t *testing.T) {
|
||||||
|
for _, status := range []string{"queued", "pending"} {
|
||||||
|
if !publishRecordMayBeDiscarded(status) {
|
||||||
|
t.Fatalf("publishRecordMayBeDiscarded(%q) = false, want true", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, status := range []string{"publishing", "running", "failed", "success", "cancelled"} {
|
||||||
|
if publishRecordMayBeDiscarded(status) {
|
||||||
|
t.Fatalf("publishRecordMayBeDiscarded(%q) = true, want false", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVisiblePublishRecordWhereIncludesDeletedTerminalFailures(t *testing.T) {
|
||||||
|
normalized := normalizeSQLWhitespace(visiblePublishRecordWhereSQL)
|
||||||
|
if !strings.Contains(normalized, "pr.deleted_at IS NULL") {
|
||||||
|
t.Fatalf("visible publish record filter must include active records: %s", normalized)
|
||||||
|
}
|
||||||
|
if !strings.Contains(normalized, "'failed'") || !strings.Contains(normalized, "'publish_failed'") {
|
||||||
|
t.Fatalf("visible publish record filter must keep terminal failed history: %s", normalized)
|
||||||
|
}
|
||||||
|
if strings.Contains(normalized, "'queued'") || strings.Contains(normalized, "'publishing'") {
|
||||||
|
t.Fatalf("visible publish record filter must not resurrect deleted active records: %s", normalized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRecalculateArticlePublishStatusDefaultsToUnpublished(t *testing.T) {
|
func TestRecalculateArticlePublishStatusDefaultsToUnpublished(t *testing.T) {
|
||||||
tx := &capturePublishDedupLookupTx{rows: emptyPublishDedupRows{}}
|
tx := &capturePublishDedupLookupTx{rows: emptyPublishDedupRows{}}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user