fix publish record deletion
Desktop Client Build / Resolve Build Metadata (push) Successful in 26s
Frontend CI / Frontend (push) Successful in 2m47s
Backend CI / Backend (push) Failing after 10m1s
Desktop Client Build / Build Desktop Client (push) Successful in 23m3s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 29s
Desktop Client Build / Resolve Build Metadata (push) Successful in 26s
Frontend CI / Frontend (push) Successful in 2m47s
Backend CI / Backend (push) Failing after 10m1s
Desktop Client Build / Build Desktop Client (push) Successful in 23m3s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 29s
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
DesktopOutlined,
|
||||
ExportOutlined,
|
||||
ReloadOutlined,
|
||||
@@ -193,6 +194,7 @@ 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({
|
||||
@@ -370,6 +372,19 @@ 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('发文记录已删除')
|
||||
await recordsQuery.refetch()
|
||||
} catch (error) {
|
||||
message.error(formatError(error))
|
||||
} finally {
|
||||
deletingRecordId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function copyErrorMessage(record: PublishRecordItem): Promise<void> {
|
||||
if (!record.errorMessage) {
|
||||
return
|
||||
@@ -622,6 +637,25 @@ async function copyErrorMessage(record: PublishRecordItem): Promise<void> {
|
||||
<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>
|
||||
|
||||
@@ -78,6 +78,7 @@ import {
|
||||
import {
|
||||
cancelDesktopTask,
|
||||
checkDesktopClientRelease,
|
||||
deleteDesktopPublishRecord,
|
||||
initTransport,
|
||||
listDesktopPublishTasks,
|
||||
resolveDesktopClientReleaseDownloadURL,
|
||||
@@ -1246,6 +1247,9 @@ function registerBridgeHandlers(): void {
|
||||
safeHandle('desktop:cancel-publish-task', async (_event, taskId: string) => {
|
||||
return cancelDesktopTask(taskId, { reason: 'user_cancelled_publish' })
|
||||
})
|
||||
safeHandle('desktop:delete-publish-record', async (_event, recordId: number) => {
|
||||
return deleteDesktopPublishRecord(recordId)
|
||||
})
|
||||
safeHandle('desktop:open-external-url', async (_event, url: string) => {
|
||||
if (!isSafeExternalUrl(url)) {
|
||||
throw new Error('unsupported_external_url')
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
CancelDesktopTaskRequest,
|
||||
CompleteDesktopTaskRequest,
|
||||
CreatePublishJobResponse,
|
||||
DeletePublishRecordResponse,
|
||||
DesktopAccountInfo,
|
||||
DesktopArticleContent,
|
||||
DesktopClientHeartbeatRequest,
|
||||
@@ -678,6 +679,14 @@ export async function retryDesktopPublishTask(taskId: string): Promise<CreatePub
|
||||
)
|
||||
}
|
||||
|
||||
export async function deleteDesktopPublishRecord(
|
||||
recordId: number,
|
||||
): Promise<DeletePublishRecordResponse> {
|
||||
return desktopDelete<DeletePublishRecordResponse>(
|
||||
`/api/desktop/publish-records/${encodeURIComponent(String(recordId))}`,
|
||||
)
|
||||
}
|
||||
|
||||
export async function upsertDesktopAccount(
|
||||
payload: UpsertDesktopAccountRequest,
|
||||
): Promise<DesktopAccountInfo> {
|
||||
|
||||
@@ -226,6 +226,10 @@ const desktopBridge = {
|
||||
ipcRenderer.invoke('desktop:retry-publish-task', taskId) as Promise<CreatePublishJobResponse>,
|
||||
cancelPublishTask: (taskId: string) =>
|
||||
ipcRenderer.invoke('desktop:cancel-publish-task', taskId) as Promise<DesktopTaskInfo>,
|
||||
deletePublishRecord: (recordId: number) =>
|
||||
ipcRenderer.invoke('desktop:delete-publish-record', recordId) as Promise<{
|
||||
deleted: boolean
|
||||
}>,
|
||||
openExternalUrl: (url: string) =>
|
||||
ipcRenderer.invoke('desktop:open-external-url', url) as Promise<null>,
|
||||
openSettingsWindow: () => ipcRenderer.invoke('desktop:open-settings-window') as Promise<null>,
|
||||
|
||||
+1
@@ -133,6 +133,7 @@ declare global {
|
||||
): Promise<DesktopPublishTaskListResponse>
|
||||
retryPublishTask(taskId: string): Promise<CreatePublishJobResponse>
|
||||
cancelPublishTask(taskId: string): Promise<DesktopTaskInfo>
|
||||
deletePublishRecord(recordId: number): Promise<{ deleted: boolean }>
|
||||
openExternalUrl(url: string): Promise<null>
|
||||
openSettingsWindow(): Promise<null>
|
||||
getAppSettings(): Promise<DesktopAppSettings>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
DesktopOutlined,
|
||||
ExportOutlined,
|
||||
ReloadOutlined,
|
||||
@@ -36,6 +37,7 @@ const publishPlatformIndex: Record<string, (typeof desktopPublishMediaCatalog)[n
|
||||
|
||||
interface PublishTaskItem {
|
||||
id: string
|
||||
publishRecordId: number | null
|
||||
title: string
|
||||
articleId: number | null
|
||||
platform: string
|
||||
@@ -397,6 +399,7 @@ const consolePendingTaskId = ref<string | null>(null)
|
||||
const copiedErrorTaskId = ref<string | null>(null)
|
||||
const retryingTaskId = ref<string | null>(null)
|
||||
const cancelingTaskId = ref<string | null>(null)
|
||||
const deletingTaskId = ref<string | null>(null)
|
||||
|
||||
function notifySuccess(message: string) {
|
||||
notification.success({
|
||||
@@ -629,6 +632,30 @@ async function cancelTask(record: PublishTaskItem) {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRecord(record: PublishTaskItem) {
|
||||
if (!record.publishRecordId) {
|
||||
return
|
||||
}
|
||||
|
||||
deletingTaskId.value = record.id
|
||||
try {
|
||||
await window.desktopBridge.app.deletePublishRecord(record.publishRecordId)
|
||||
notifySuccess('发文记录已删除')
|
||||
await refreshTasks(currentPage.value)
|
||||
} catch (err) {
|
||||
notifyActionError(
|
||||
normalizeTaskErrorMessage(unwrapClientErrorMessage(err, '删除发文记录失败')) ??
|
||||
'删除发文记录失败',
|
||||
)
|
||||
} finally {
|
||||
deletingTaskId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function canDeleteRecord(record: PublishTaskItem): boolean {
|
||||
return record.publishRecordId !== null
|
||||
}
|
||||
|
||||
async function copyTextToClipboard(text: string): Promise<void> {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
@@ -741,6 +768,7 @@ const publishTasks = computed<PublishTaskItem[]>(() =>
|
||||
const articleId =
|
||||
extractNumber(payload.article_id) ??
|
||||
extractNumber(nestedContentRef?.article_id ?? nestedContentRef?.id)
|
||||
const publishRecordId = extractNumber(payload.publish_record_id)
|
||||
const pendingTasks =
|
||||
taskPage.value?.items.filter((item) =>
|
||||
isPendingStatus(normalizePublishTaskStatus(item.status)),
|
||||
@@ -782,6 +810,7 @@ const publishTasks = computed<PublishTaskItem[]>(() =>
|
||||
|
||||
return {
|
||||
id: task.id,
|
||||
publishRecordId,
|
||||
title: extractString(payload.title) ?? `发布任务 · ${translatePlatform(task.platform)}`,
|
||||
articleId,
|
||||
platform: task.platform,
|
||||
@@ -1096,6 +1125,26 @@ const tableScroll = { x: 1080 } as const
|
||||
<ClockCircleOutlined />
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-popconfirm
|
||||
v-if="canDeleteRecord(record)"
|
||||
title="确认删除这条发文记录?删除后后台和客户端发文管理都不再显示。"
|
||||
ok-text="删除"
|
||||
cancel-text="保留"
|
||||
placement="topRight"
|
||||
@confirm="deleteRecord(record)"
|
||||
>
|
||||
<a-tooltip title="删除发文记录" placement="top">
|
||||
<a-button
|
||||
type="text"
|
||||
class="icon-action-btn icon-action-btn--danger"
|
||||
:loading="deletingTaskId === record.id"
|
||||
:aria-label="`删除发文记录:${record.title}`"
|
||||
@click.stop
|
||||
>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -759,6 +759,20 @@ func (s *DesktopTaskService) leaseNextQueuedPublishTask(
|
||||
AND dt.kind = 'publish'
|
||||
AND dt.status = 'queued'
|
||||
AND dt.attempts < $4
|
||||
AND (
|
||||
NOT (dt.payload ? 'publish_record_id')
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM publish_records pr
|
||||
WHERE pr.tenant_id = dt.tenant_id
|
||||
AND pr.id = CASE
|
||||
WHEN (dt.payload->>'publish_record_id') ~ '^[0-9]+$'
|
||||
THEN (dt.payload->>'publish_record_id')::bigint
|
||||
ELSE NULL
|
||||
END
|
||||
AND pr.deleted_at IS NULL
|
||||
)
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM platform_accounts AS pa
|
||||
@@ -866,6 +880,21 @@ func leaseQueuedDesktopTaskByIDSQL() string {
|
||||
AND dt.workspace_id = $2
|
||||
AND dt.status = 'queued'
|
||||
AND (dt.kind <> 'publish' OR dt.attempts < $7)
|
||||
AND (
|
||||
dt.kind <> 'publish'
|
||||
OR NOT (dt.payload ? 'publish_record_id')
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM publish_records pr
|
||||
WHERE pr.tenant_id = dt.tenant_id
|
||||
AND pr.id = CASE
|
||||
WHEN (dt.payload->>'publish_record_id') ~ '^[0-9]+$'
|
||||
THEN (dt.payload->>'publish_record_id')::bigint
|
||||
ELSE NULL
|
||||
END
|
||||
AND pr.deleted_at IS NULL
|
||||
)
|
||||
)
|
||||
AND (
|
||||
dt.kind <> 'publish'
|
||||
OR EXISTS (
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/auth"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/response"
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository"
|
||||
)
|
||||
|
||||
type MediaService struct {
|
||||
@@ -82,11 +83,15 @@ type DeletePublishRecordResponse struct {
|
||||
Deleted bool `json:"deleted"`
|
||||
}
|
||||
|
||||
type publishRecordDeleteOwner struct {
|
||||
TenantID int64
|
||||
WorkspaceID int64
|
||||
UserID int64
|
||||
Source string
|
||||
}
|
||||
|
||||
const visiblePublishRecordWhereSQL = `
|
||||
AND (
|
||||
pr.deleted_at IS NULL
|
||||
OR pr.status IN ('success', 'published', 'publish_success', 'failed', 'publish_failed', 'cancelled', 'canceled')
|
||||
)
|
||||
AND pr.deleted_at IS NULL
|
||||
`
|
||||
|
||||
func (s *MediaService) ListPlatforms(ctx context.Context) ([]MediaPlatformResponse, error) {
|
||||
@@ -327,10 +332,7 @@ func (s *MediaService) countPublishRecordsByStatuses(
|
||||
AND pb.tenant_id = pr.tenant_id
|
||||
AND pb.initiator_user_id = $2
|
||||
)
|
||||
AND (
|
||||
pr.deleted_at IS NULL
|
||||
OR pr.status IN ('success', 'published', 'publish_success', 'failed', 'publish_failed', 'cancelled', 'canceled')
|
||||
)
|
||||
`+visiblePublishRecordWhereSQL+`
|
||||
AND pr.status = ANY($3)
|
||||
AND (
|
||||
$4 = ''
|
||||
@@ -378,10 +380,7 @@ func (s *MediaService) listArticlePublishRecords(ctx context.Context, tenantID,
|
||||
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')
|
||||
)
|
||||
`+visiblePublishRecordWhereSQL+`
|
||||
ORDER BY pr.created_at DESC, pr.id DESC
|
||||
`, tenantID, articleID, brandID)
|
||||
if err != nil {
|
||||
@@ -433,7 +432,35 @@ func (s *MediaService) DeletePublishRecord(ctx context.Context, recordID int64)
|
||||
return nil, response.ErrBadRequest(40001, "invalid_publish_record_id", "publish record id must be a positive number")
|
||||
}
|
||||
actor := auth.MustActor(ctx)
|
||||
if actor.TenantID == 0 || actor.UserID == 0 {
|
||||
workspaceID := auth.CurrentWorkspaceID(ctx)
|
||||
if actor.TenantID == 0 || actor.UserID == 0 || workspaceID == 0 {
|
||||
return nil, response.ErrUnauthorized(40132, "workspace_scope_missing", "workspace context is required")
|
||||
}
|
||||
return s.deletePublishRecordForOwner(ctx, recordID, publishRecordDeleteOwner{
|
||||
TenantID: actor.TenantID,
|
||||
WorkspaceID: workspaceID,
|
||||
UserID: actor.UserID,
|
||||
Source: "tenant_web",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MediaService) DeleteDesktopPublishRecord(ctx context.Context, client *repository.DesktopClient, recordID int64) (*DeletePublishRecordResponse, error) {
|
||||
if recordID <= 0 {
|
||||
return nil, response.ErrBadRequest(40001, "invalid_publish_record_id", "publish record id must be a positive number")
|
||||
}
|
||||
if client == nil || client.TenantID == 0 || client.WorkspaceID == 0 || client.UserID == 0 {
|
||||
return nil, response.ErrUnauthorized(40108, "desktop_client_missing", "desktop client context is required")
|
||||
}
|
||||
return s.deletePublishRecordForOwner(ctx, recordID, publishRecordDeleteOwner{
|
||||
TenantID: client.TenantID,
|
||||
WorkspaceID: client.WorkspaceID,
|
||||
UserID: client.UserID,
|
||||
Source: "desktop_client",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MediaService) deletePublishRecordForOwner(ctx context.Context, recordID int64, owner publishRecordDeleteOwner) (*DeletePublishRecordResponse, error) {
|
||||
if owner.TenantID == 0 || owner.WorkspaceID == 0 || owner.UserID == 0 {
|
||||
return nil, response.ErrUnauthorized(40132, "workspace_scope_missing", "workspace context is required")
|
||||
}
|
||||
|
||||
@@ -445,17 +472,16 @@ func (s *MediaService) DeletePublishRecord(ctx context.Context, recordID int64)
|
||||
|
||||
var publishBatchID int64
|
||||
var articleID int64
|
||||
var publishStatus string
|
||||
cancelErrorJSONBytes, err := marshalOptionalJSON(map[string]any{
|
||||
"reason": "publish_record_deleted",
|
||||
"source": "tenant_web",
|
||||
"source": owner.Source,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50044, "publish_record_delete_payload_failed", "failed to delete publish record")
|
||||
}
|
||||
cancelErrorJSON := string(cancelErrorJSONBytes)
|
||||
err = tx.QueryRow(ctx, `
|
||||
SELECT pr.publish_batch_id, pr.article_id, pr.status
|
||||
SELECT pr.publish_batch_id, pr.article_id
|
||||
FROM publish_records pr
|
||||
JOIN articles a
|
||||
ON a.id = pr.article_id
|
||||
@@ -466,9 +492,44 @@ func (s *MediaService) DeletePublishRecord(ctx context.Context, recordID int64)
|
||||
WHERE pr.id = $1
|
||||
AND pr.tenant_id = $2
|
||||
AND pb.initiator_user_id = $3
|
||||
AND (
|
||||
(
|
||||
COALESCE(pr.target_type, 'platform_account') = 'platform_account'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM platform_accounts pa
|
||||
WHERE pa.id = pr.platform_account_id
|
||||
AND pa.tenant_id = pr.tenant_id
|
||||
AND pa.workspace_id = $4
|
||||
)
|
||||
)
|
||||
OR (
|
||||
COALESCE(pr.target_type, 'platform_account') = 'enterprise_site'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM enterprise_site_connections esc
|
||||
WHERE esc.id = pr.target_connection_id
|
||||
AND esc.tenant_id = pr.tenant_id
|
||||
AND esc.workspace_id = $4
|
||||
)
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM desktop_tasks dt
|
||||
JOIN desktop_publish_jobs j
|
||||
ON j.desktop_id = dt.job_id
|
||||
AND j.tenant_id = dt.tenant_id
|
||||
AND j.workspace_id = dt.workspace_id
|
||||
WHERE dt.tenant_id = pr.tenant_id
|
||||
AND dt.workspace_id = $4
|
||||
AND dt.kind = 'publish'
|
||||
AND dt.payload->>'publish_record_id' = pr.id::text
|
||||
AND j.created_by_user_id = $3
|
||||
)
|
||||
)
|
||||
AND pr.deleted_at IS NULL
|
||||
FOR UPDATE OF pr
|
||||
`, recordID, actor.TenantID, actor.UserID).Scan(&publishBatchID, &articleID, &publishStatus)
|
||||
`, recordID, owner.TenantID, owner.UserID, owner.WorkspaceID).Scan(&publishBatchID, &articleID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, response.ErrNotFound(40435, "publish_record_not_found", "publish record not found")
|
||||
@@ -476,10 +537,6 @@ func (s *MediaService) DeletePublishRecord(ctx context.Context, recordID int64)
|
||||
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, `
|
||||
UPDATE publish_records
|
||||
SET deleted_at = NOW(),
|
||||
@@ -487,30 +544,12 @@ func (s *MediaService) DeletePublishRecord(ctx context.Context, recordID int64)
|
||||
WHERE id = $1
|
||||
AND tenant_id = $2
|
||||
AND deleted_at IS NULL
|
||||
`, recordID, actor.TenantID); err != nil {
|
||||
`, recordID, owner.TenantID); err != nil {
|
||||
return nil, response.ErrInternal(50044, "publish_record_delete_failed", "failed to delete publish record")
|
||||
} else if tag.RowsAffected() == 0 {
|
||||
return nil, response.ErrNotFound(40435, "publish_record_not_found", "publish record not found")
|
||||
}
|
||||
|
||||
if publishRecordMayBeDiscarded(publishStatus) {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE desktop_tasks
|
||||
SET status = 'aborted',
|
||||
error = $3::jsonb,
|
||||
active_attempt_id = NULL,
|
||||
lease_token_hash = NULL,
|
||||
lease_expires_at = NULL,
|
||||
updated_at = NOW()
|
||||
WHERE tenant_id = $1
|
||||
AND kind = 'publish'
|
||||
AND status = 'queued'
|
||||
AND payload->>'publish_record_id' = $2::text
|
||||
`, actor.TenantID, recordID, cancelErrorJSON); err != nil {
|
||||
return nil, response.ErrInternal(50044, "publish_record_delete_cancel_task_failed", "failed to cancel queued publish task before deleting record")
|
||||
}
|
||||
}
|
||||
|
||||
batchStatus, err := recalculatePublishBatchStatus(ctx, tx, publishBatchID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -519,11 +558,11 @@ func (s *MediaService) DeletePublishRecord(ctx context.Context, recordID int64)
|
||||
UPDATE publish_batches
|
||||
SET status = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND tenant_id = $3
|
||||
`, batchStatus, publishBatchID, actor.TenantID); err != nil {
|
||||
`, batchStatus, publishBatchID, owner.TenantID); err != nil {
|
||||
return nil, response.ErrInternal(50054, "publish_batch_update_failed", "failed to update publish batch")
|
||||
}
|
||||
|
||||
articleStatus, err := recalculateArticlePublishStatus(ctx, tx, actor.TenantID, articleID)
|
||||
articleStatus, err := recalculateArticlePublishStatus(ctx, tx, owner.TenantID, articleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -531,16 +570,38 @@ func (s *MediaService) DeletePublishRecord(ctx context.Context, recordID int64)
|
||||
UPDATE articles
|
||||
SET publish_status = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND tenant_id = $3 AND deleted_at IS NULL
|
||||
`, articleStatus, articleID, actor.TenantID); err != nil {
|
||||
`, articleStatus, articleID, owner.TenantID); err != nil {
|
||||
return nil, response.ErrInternal(50055, "article_publish_update_failed", "failed to update article publish status")
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, response.ErrInternal(50044, "publish_record_delete_commit_failed", "failed to delete publish record")
|
||||
}
|
||||
|
||||
s.abortQueuedDesktopTasksForDeletedPublishRecord(ctx, owner, recordID, cancelErrorJSON)
|
||||
return &DeletePublishRecordResponse{Deleted: true}, nil
|
||||
}
|
||||
|
||||
func (s *MediaService) abortQueuedDesktopTasksForDeletedPublishRecord(ctx context.Context, owner publishRecordDeleteOwner, recordID int64, errorJSON string) {
|
||||
if s == nil || s.pool == nil {
|
||||
return
|
||||
}
|
||||
_, _ = s.pool.Exec(ctx, `
|
||||
UPDATE desktop_tasks
|
||||
SET status = 'aborted',
|
||||
error = $3::jsonb,
|
||||
active_attempt_id = NULL,
|
||||
lease_token_hash = NULL,
|
||||
lease_expires_at = NULL,
|
||||
updated_at = NOW()
|
||||
WHERE tenant_id = $1
|
||||
AND workspace_id = $4
|
||||
AND kind = 'publish'
|
||||
AND status = 'queued'
|
||||
AND payload->>'publish_record_id' = $2::text
|
||||
`, owner.TenantID, recordID, errorJSON, owner.WorkspaceID)
|
||||
}
|
||||
|
||||
func publishBatchRequiresCover(accounts []platformAccountSeed) bool {
|
||||
for _, account := range accounts {
|
||||
if publishPlatformRequiresCover(account.PlatformID) {
|
||||
@@ -561,7 +622,7 @@ func publishPlatformRequiresCover(platformID string) bool {
|
||||
|
||||
func publishRecordMayBeDiscarded(status string) bool {
|
||||
switch strings.TrimSpace(status) {
|
||||
case "queued", "pending":
|
||||
case "queued", "pending", "publishing", "running", "failed", "publish_failed", "success", "published", "publish_success", "cancelled", "canceled":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -194,30 +194,29 @@ func TestDesktopTaskToPublishStatusMapsAbortToCancelled(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishRecordMayBeDiscardedOnlyForQueuedRecords(t *testing.T) {
|
||||
for _, status := range []string{"queued", "pending"} {
|
||||
func TestPublishRecordMayBeDiscardedIncludesHistoryRecords(t *testing.T) {
|
||||
for _, status := range []string{"queued", "pending", "publishing", "running", "failed", "success", "cancelled"} {
|
||||
if !publishRecordMayBeDiscarded(status) {
|
||||
t.Fatalf("publishRecordMayBeDiscarded(%q) = false, want true", status)
|
||||
}
|
||||
}
|
||||
|
||||
for _, status := range []string{"publishing", "running", "failed", "success", "cancelled"} {
|
||||
for _, status := range []string{"", "archived"} {
|
||||
if publishRecordMayBeDiscarded(status) {
|
||||
t.Fatalf("publishRecordMayBeDiscarded(%q) = true, want false", status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVisiblePublishRecordWhereIncludesDeletedTerminalFailures(t *testing.T) {
|
||||
func TestVisiblePublishRecordWhereExcludesDeletedRecords(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)
|
||||
for _, status := range []string{"'queued'", "'publishing'", "'failed'", "'publish_failed'", "'success'", "'cancelled'"} {
|
||||
if strings.Contains(normalized, status) {
|
||||
t.Fatalf("visible publish record filter must not resurrect deleted records by status %s: %s", status, normalized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,3 +94,17 @@ func (h *MediaHandler) DeletePublishRecord(c *gin.Context) {
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
func (h *MediaHandler) DeleteDesktopPublishRecord(c *gin.Context) {
|
||||
recordID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil || recordID <= 0 {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_publish_record_id", "publish record id must be a positive number"))
|
||||
return
|
||||
}
|
||||
data, err := h.svc.DeleteDesktopPublishRecord(c.Request.Context(), MustDesktopClient(c.Request.Context()), recordID)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ func RegisterRoutes(app *bootstrap.App) {
|
||||
desktopAuth.GET("/content/articles/:id", desktopContentHandler.Article)
|
||||
desktopAuth.GET("/content/assets/:token", publicAssets.ServeDesktop)
|
||||
desktopAuth.GET("/publish-tasks", desktopTaskHandler.ListPublish)
|
||||
desktopAuth.DELETE("/publish-records/:id", mediaHandler.DeleteDesktopPublishRecord)
|
||||
desktopAuth.POST("/accounts/health-reports", desktopAccountHandler.ReportHealth)
|
||||
desktopAuth.POST("/accounts", desktopAccountHandler.Upsert)
|
||||
desktopAuth.PATCH("/accounts/:id", desktopAccountHandler.Patch)
|
||||
|
||||
@@ -89,6 +89,7 @@ func TestDesktopClientRoutes_ClientTokenAuth(t *testing.T) {
|
||||
{http.MethodGet, "/api/desktop/dispatch"},
|
||||
{http.MethodGet, "/api/desktop/accounts"},
|
||||
{http.MethodGet, "/api/desktop/publish-tasks"},
|
||||
{http.MethodDelete, "/api/desktop/publish-records/:id"},
|
||||
{http.MethodGet, "/api/desktop/content/assets/:token"},
|
||||
{http.MethodPost, "/api/desktop/clients/offline"},
|
||||
{http.MethodPost, "/api/desktop/tasks/lease"},
|
||||
|
||||
Reference in New Issue
Block a user