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
@@ -135,6 +135,83 @@ func (q *Queries) CancelDesktopTaskByLease(ctx context.Context, arg CancelDeskto
return i, err
}
const cancelQueuedPublishDesktopTaskByOwner = `-- name: CancelQueuedPublishDesktopTaskByOwner :one
UPDATE desktop_tasks AS t
SET status = 'aborted',
error = $1,
active_attempt_id = NULL,
lease_token_hash = NULL,
lease_expires_at = NULL,
updated_at = now()
FROM desktop_publish_jobs AS j
WHERE t.desktop_id = $2
AND t.tenant_id = $3
AND t.workspace_id = $4
AND t.kind = 'publish'
AND t.status = 'queued'
AND j.desktop_id = t.job_id
AND j.tenant_id = t.tenant_id
AND j.workspace_id = t.workspace_id
AND j.created_by_user_id = $5
RETURNING t.id, t.desktop_id, t.job_id, t.tenant_id, t.workspace_id, t.target_account_id, t.target_client_id, t.platform_id, t.kind, t.payload, t.status, t.dedup_key, t.active_attempt_id, t.lease_token_hash, t.lease_expires_at, t.attempts, t.result, t.error, t.created_at, t.updated_at, t.priority, t.lane, t.lane_weight, t.source, t.scheduler_group_key, t.monitor_task_id, t.supersedes_task_id, t.control_flags, t.interrupt_generation, t.started_at, t.interrupted_at, t.interrupt_reason, t.enqueued_at, t.publish_submit_started_at
`
type CancelQueuedPublishDesktopTaskByOwnerParams struct {
Error []byte `json:"error"`
DesktopID pgtype.UUID `json:"desktop_id"`
TenantID int64 `json:"tenant_id"`
WorkspaceID int64 `json:"workspace_id"`
UserID int64 `json:"user_id"`
}
func (q *Queries) CancelQueuedPublishDesktopTaskByOwner(ctx context.Context, arg CancelQueuedPublishDesktopTaskByOwnerParams) (DesktopTask, error) {
row := q.db.QueryRow(ctx, cancelQueuedPublishDesktopTaskByOwner,
arg.Error,
arg.DesktopID,
arg.TenantID,
arg.WorkspaceID,
arg.UserID,
)
var i DesktopTask
err := row.Scan(
&i.ID,
&i.DesktopID,
&i.JobID,
&i.TenantID,
&i.WorkspaceID,
&i.TargetAccountID,
&i.TargetClientID,
&i.PlatformID,
&i.Kind,
&i.Payload,
&i.Status,
&i.DedupKey,
&i.ActiveAttemptID,
&i.LeaseTokenHash,
&i.LeaseExpiresAt,
&i.Attempts,
&i.Result,
&i.Error,
&i.CreatedAt,
&i.UpdatedAt,
&i.Priority,
&i.Lane,
&i.LaneWeight,
&i.Source,
&i.SchedulerGroupKey,
&i.MonitorTaskID,
&i.SupersedesTaskID,
&i.ControlFlags,
&i.InterruptGeneration,
&i.StartedAt,
&i.InterruptedAt,
&i.InterruptReason,
&i.EnqueuedAt,
&i.PublishSubmitStartedAt,
)
return i, err
}
const completeDesktopTask = `-- name: CompleteDesktopTask :one
UPDATE desktop_tasks
SET status = $1,
@@ -845,19 +922,30 @@ SET status = 'aborted',
lease_expires_at = NULL,
updated_at = now()
WHERE desktop_id = $2
AND workspace_id = $3
AND tenant_id = $3
AND workspace_id = $4
AND status = 'queued'
AND EXISTS (
SELECT 1
FROM desktop_publish_jobs AS j
WHERE j.desktop_id = desktop_tasks.job_id
AND j.tenant_id = desktop_tasks.tenant_id
AND j.workspace_id = desktop_tasks.workspace_id
AND j.created_by_user_id = $5
)
RETURNING id, desktop_id, job_id, tenant_id, workspace_id, target_account_id, target_client_id, platform_id, kind, payload, status, dedup_key, active_attempt_id, lease_token_hash, lease_expires_at, attempts, result, error, created_at, updated_at, priority, lane, lane_weight, source, scheduler_group_key, monitor_task_id, supersedes_task_id, control_flags, interrupt_generation, started_at, interrupted_at, interrupt_reason, enqueued_at, publish_submit_started_at
`
type TenantCancelDesktopTaskParams struct {
Error []byte `json:"error"`
DesktopID pgtype.UUID `json:"desktop_id"`
TenantID int64 `json:"tenant_id"`
WorkspaceID int64 `json:"workspace_id"`
UserID int64 `json:"user_id"`
}
func (q *Queries) TenantCancelDesktopTask(ctx context.Context, arg TenantCancelDesktopTaskParams) (DesktopTask, error) {
row := q.db.QueryRow(ctx, tenantCancelDesktopTask, arg.Error, arg.DesktopID, arg.WorkspaceID)
row := q.db.QueryRow(ctx, tenantCancelDesktopTask, arg.Error, arg.DesktopID, arg.TenantID, arg.WorkspaceID, arg.UserID)
var i DesktopTask
err := row.Scan(
&i.ID,
@@ -43,3 +43,24 @@ func TestReconcileDesktopTaskRetryResetsAttempts(t *testing.T) {
t.Fatalf("retry reconcile must not increment attempts; query:\n%s", query)
}
}
func TestCancelQueuedPublishDesktopTaskByOwnerIsScopedToCreator(t *testing.T) {
query := cancelQueuedPublishDesktopTaskByOwner
for _, fragment := range []string{
"FROM desktop_publish_jobs AS j",
"t.tenant_id = $3",
"t.workspace_id = $4",
"t.kind = 'publish'",
"t.status = 'queued'",
"j.desktop_id = t.job_id",
"j.created_by_user_id = $5",
} {
if !strings.Contains(query, fragment) {
t.Fatalf("owner cancel query missing %q; query:\n%s", fragment, query)
}
}
if strings.Contains(query, "target_client_id =") {
t.Fatalf("owner cancel query must not require the task target client; query:\n%s", query)
}
}
@@ -14,6 +14,7 @@ type Querier interface {
ApproveKolSubscription(ctx context.Context, arg ApproveKolSubscriptionParams) error
CancelDesktopTaskByClient(ctx context.Context, arg CancelDesktopTaskByClientParams) (DesktopTask, error)
CancelDesktopTaskByLease(ctx context.Context, arg CancelDesktopTaskByLeaseParams) (DesktopTask, error)
CancelQueuedPublishDesktopTaskByOwner(ctx context.Context, arg CancelQueuedPublishDesktopTaskByOwnerParams) (DesktopTask, error)
ClearDesktopAccountDeleteRequested(ctx context.Context, arg ClearDesktopAccountDeleteRequestedParams) (ClearDesktopAccountDeleteRequestedRow, error)
CompleteDesktopTask(ctx context.Context, arg CompleteDesktopTaskParams) (DesktopTask, error)
ConfirmReservation(ctx context.Context, arg ConfirmReservationParams) error