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:
@@ -122,6 +122,7 @@ const articlePublishStatusAggregateJoin = `
|
||||
FROM publish_records pr
|
||||
WHERE pr.tenant_id = a.tenant_id
|
||||
AND pr.article_id = a.id
|
||||
AND pr.deleted_at IS NULL
|
||||
ORDER BY
|
||||
COALESCE(pr.target_type, 'platform_account'),
|
||||
COALESCE(pr.platform_account_id, pr.target_connection_id),
|
||||
|
||||
@@ -278,19 +278,38 @@ func (s *BrandService) Delete(ctx context.Context, id int64) error {
|
||||
_ = tx.Rollback(ctx)
|
||||
}()
|
||||
|
||||
tag, err := tx.Exec(ctx, `UPDATE brands SET deleted_at = NOW(), updated_at = NOW() WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL`, id, actor.TenantID)
|
||||
if err != nil || tag.RowsAffected() == 0 {
|
||||
return response.ErrNotFound(40420, "brand_not_found", "brand not found")
|
||||
tag, err := tx.Exec(ctx, `
|
||||
UPDATE brands
|
||||
SET status = 'deleting',
|
||||
deleted_at = COALESCE(deleted_at, NOW()),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
`, id, actor.TenantID)
|
||||
if err != nil {
|
||||
return response.ErrInternal(50010, "delete_failed", "failed to mark brand for deletion")
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
var alreadyDeleted bool
|
||||
if lookupErr := tx.QueryRow(ctx, `
|
||||
SELECT deleted_at IS NOT NULL
|
||||
FROM brands
|
||||
WHERE id = $1 AND tenant_id = $2
|
||||
`, id, actor.TenantID).Scan(&alreadyDeleted); lookupErr != nil {
|
||||
if errors.Is(lookupErr, pgx.ErrNoRows) {
|
||||
return response.ErrNotFound(40420, "brand_not_found", "brand not found")
|
||||
}
|
||||
return response.ErrInternal(50010, "delete_failed", "failed to inspect brand deletion state")
|
||||
}
|
||||
if !alreadyDeleted {
|
||||
return response.ErrNotFound(40420, "brand_not_found", "brand not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
_, _ = tx.Exec(ctx, `UPDATE brand_keywords SET deleted_at = NOW(), updated_at = NOW() WHERE brand_id = $1 AND tenant_id = $2 AND deleted_at IS NULL`, id, actor.TenantID)
|
||||
_, _ = tx.Exec(ctx, `UPDATE brand_questions SET deleted_at = NOW(), updated_at = NOW() WHERE brand_id = $1 AND tenant_id = $2 AND deleted_at IS NULL`, id, actor.TenantID)
|
||||
_, _ = tx.Exec(ctx, `UPDATE competitors SET deleted_at = NOW(), updated_at = NOW() WHERE brand_id = $1 AND tenant_id = $2 AND deleted_at IS NULL`, id, actor.TenantID)
|
||||
|
||||
if err := s.cleanupMonitoringAfterBrandDelete(ctx, actor.TenantID, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
afterJSON, _ := json.Marshal(map[string]interface{}{"id": id, "action": "cascade_soft_delete"})
|
||||
afterJSON, _ := json.Marshal(map[string]interface{}{"id": id, "status": "deleting", "cleanup": "background"})
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -158,6 +158,13 @@ func invalidateBrandCaches(ctx context.Context, c sharedcache.Cache, tenantID, b
|
||||
invalidateWorkspaceCaches(ctx, c, tenantID)
|
||||
}
|
||||
|
||||
func InvalidateBrandDeletionCaches(ctx context.Context, c sharedcache.Cache, tenantID, brandID int64) {
|
||||
invalidateBrandCaches(ctx, c, tenantID, brandID)
|
||||
invalidateArticleCaches(ctx, c, tenantID, nil)
|
||||
invalidatePromptRuleCaches(ctx, c, tenantID, brandID, nil)
|
||||
invalidateScheduleTaskCaches(ctx, c, tenantID, nil)
|
||||
}
|
||||
|
||||
func promptRuleGroupsCacheKey(tenantID, brandID int64) string {
|
||||
return fmt.Sprintf("prompt_rule:groups:%d:%d", tenantID, brandID)
|
||||
}
|
||||
|
||||
@@ -973,7 +973,10 @@ func (s *DesktopTaskService) Cancel(ctx context.Context, client *repository.Desk
|
||||
if req.LeaseToken != nil && strings.TrimSpace(*req.LeaseToken) != "" {
|
||||
task, err = taskRepo.CancelByLease(ctx, desktopID, HashDesktopClientToken(strings.TrimSpace(*req.LeaseToken)), errorJSON)
|
||||
} else {
|
||||
task, err = taskRepo.CancelByClient(ctx, desktopID, client.ID, errorJSON)
|
||||
task, err = taskRepo.CancelQueuedPublishByOwner(ctx, desktopID, client.TenantID, client.WorkspaceID, client.UserID, errorJSON)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
task, err = taskRepo.CancelByClient(ctx, desktopID, client.ID, errorJSON)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
@@ -1015,7 +1018,8 @@ func (s *DesktopTaskService) Cancel(ctx context.Context, client *repository.Desk
|
||||
}
|
||||
|
||||
func (s *DesktopTaskService) TenantCancel(ctx context.Context, actor auth.Actor, desktopID uuid.UUID, req CancelDesktopTaskRequest) (*DesktopTaskView, error) {
|
||||
if actor.PrimaryWorkspaceID == 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")
|
||||
}
|
||||
|
||||
@@ -1027,7 +1031,14 @@ func (s *DesktopTaskService) TenantCancel(ctx context.Context, actor auth.Actor,
|
||||
return nil, response.ErrBadRequest(40088, "invalid_desktop_task_cancel", "cancel payload must be serializable")
|
||||
}
|
||||
|
||||
task, err := s.repo.TenantCancel(ctx, desktopID, actor.PrimaryWorkspaceID, errorJSON)
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50107, "desktop_task_cancel_begin_failed", "failed to start desktop task cancel")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
taskRepo := repository.NewDesktopTaskRepository(tx)
|
||||
task, err := taskRepo.TenantCancel(ctx, desktopID, actor.TenantID, workspaceID, actor.UserID, errorJSON)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, response.ErrConflict(40984, "desktop_task_cancel_conflict", "desktop task cannot be canceled in its current state")
|
||||
@@ -1035,7 +1046,17 @@ func (s *DesktopTaskService) TenantCancel(ctx context.Context, actor auth.Actor,
|
||||
return nil, response.ErrInternal(50093, "desktop_task_tenant_cancel_failed", "failed to cancel desktop task")
|
||||
}
|
||||
|
||||
publishOutcome, err := syncDesktopPublishTaskState(ctx, tx, task)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, response.ErrInternal(50108, "desktop_task_cancel_commit_failed", "failed to commit desktop task cancel")
|
||||
}
|
||||
|
||||
s.publishTaskEvent(ctx, task, "task_canceled")
|
||||
s.afterRecoveredPublishOutcomes(ctx, []*desktopPublishSyncOutcome{publishOutcome})
|
||||
|
||||
view := buildDesktopTaskView(task)
|
||||
return &view, nil
|
||||
|
||||
@@ -269,6 +269,7 @@ func (s *EnterpriseSiteService) List(ctx context.Context) ([]EnterpriseSiteConne
|
||||
WHERE pr.tenant_id = $1
|
||||
AND pr.target_type = 'enterprise_site'
|
||||
AND pr.target_connection_id IS NOT NULL
|
||||
AND pr.deleted_at IS NULL
|
||||
ORDER BY pr.target_connection_id, pr.created_at DESC, pr.id DESC
|
||||
)
|
||||
SELECT
|
||||
@@ -1022,7 +1023,7 @@ func (s *EnterpriseSiteService) finishEnterprisePublishRecord(
|
||||
published_at = $5,
|
||||
error_message = $6,
|
||||
updated_at = NOW()
|
||||
WHERE id = $7 AND tenant_id = $8
|
||||
WHERE id = $7 AND tenant_id = $8 AND deleted_at IS NULL
|
||||
`, status, externalID, externalURL, nullableJSON(responsePayload), publishedAt, errorMessage, publishRecordID, tenantID); err != nil {
|
||||
return response.ErrInternal(50228, "enterprise_site_publish_record_update_failed", "企业站点发布结果更新失败")
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/auth"
|
||||
@@ -38,6 +40,9 @@ type PublishRecordResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
PublishBatchID int64 `json:"publish_batch_id"`
|
||||
ArticleID int64 `json:"article_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
BrandName *string `json:"brand_name"`
|
||||
BrandDeleted bool `json:"brand_deleted"`
|
||||
ArticleTitle *string `json:"article_title,omitempty"`
|
||||
PlatformAccountID *int64 `json:"platform_account_id"`
|
||||
DesktopAccountID *string `json:"desktop_account_id"`
|
||||
@@ -72,6 +77,10 @@ type PublishRecordListResponse struct {
|
||||
HistoryTotal int `json:"history_total"`
|
||||
}
|
||||
|
||||
type DeletePublishRecordResponse struct {
|
||||
Deleted bool `json:"deleted"`
|
||||
}
|
||||
|
||||
func (s *MediaService) ListPlatforms(ctx context.Context) ([]MediaPlatformResponse, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, platform_id, name, category, short_name, accent_color, login_url, logo_url, status, sort_order
|
||||
@@ -108,9 +117,8 @@ func (s *MediaService) ListPlatforms(ctx context.Context) ([]MediaPlatformRespon
|
||||
|
||||
func (s *MediaService) ListPublishRecords(ctx context.Context, req ListPublishRecordsRequest) (*PublishRecordListResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if actor.TenantID == 0 || actor.UserID == 0 {
|
||||
return nil, response.ErrUnauthorized(40132, "workspace_scope_missing", "workspace context is required")
|
||||
}
|
||||
|
||||
page := req.Page
|
||||
@@ -125,11 +133,11 @@ func (s *MediaService) ListPublishRecords(ctx context.Context, req ListPublishRe
|
||||
pendingStatuses := []string{"queued", "publishing"}
|
||||
historyStatuses := []string{"success", "failed", "cancelled", "canceled"}
|
||||
|
||||
pendingItems, err := s.listPublishRecordsByStatuses(ctx, actor.TenantID, brandID, pendingStatuses, title, 0, 0, "pr.created_at ASC, pr.id ASC")
|
||||
pendingItems, err := s.listPublishRecordsByStatuses(ctx, actor.TenantID, actor.UserID, pendingStatuses, title, 0, 0, "pr.created_at ASC, pr.id ASC")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
historyTotal, err := s.countPublishRecordsByStatuses(ctx, actor.TenantID, brandID, historyStatuses, title)
|
||||
historyTotal, err := s.countPublishRecordsByStatuses(ctx, actor.TenantID, actor.UserID, historyStatuses, title)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -152,7 +160,7 @@ func (s *MediaService) ListPublishRecords(ctx context.Context, req ListPublishRe
|
||||
historyOffset = 0
|
||||
}
|
||||
if remaining > 0 && historyOffset < historyTotal {
|
||||
historyItems, listErr := s.listPublishRecordsByStatuses(ctx, actor.TenantID, brandID, historyStatuses, title, remaining, historyOffset, "pr.updated_at DESC, pr.id DESC")
|
||||
historyItems, listErr := s.listPublishRecordsByStatuses(ctx, actor.TenantID, actor.UserID, historyStatuses, title, remaining, historyOffset, "pr.updated_at DESC, pr.id DESC")
|
||||
if listErr != nil {
|
||||
return nil, listErr
|
||||
}
|
||||
@@ -172,7 +180,7 @@ func (s *MediaService) ListPublishRecords(ctx context.Context, req ListPublishRe
|
||||
func (s *MediaService) listPublishRecordsByStatuses(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
userID int64,
|
||||
statuses []string,
|
||||
title string,
|
||||
limit int,
|
||||
@@ -181,6 +189,7 @@ func (s *MediaService) listPublishRecordsByStatuses(
|
||||
) ([]PublishRecordResponse, error) {
|
||||
query := `
|
||||
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,
|
||||
COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', '')) AS article_title,
|
||||
pr.platform_account_id, pa.desktop_id::text, dt.desktop_id::text,
|
||||
COALESCE(pr.target_type, 'platform_account'), pr.target_connection_id, pr.platform_id,
|
||||
@@ -194,6 +203,7 @@ func (s *MediaService) listPublishRecordsByStatuses(
|
||||
pr.external_manage_url, pr.published_at, pr.error_message, pr.created_at, pr.updated_at
|
||||
FROM publish_records pr
|
||||
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 article_versions av ON av.id = a.current_version_id AND av.article_id = a.id
|
||||
LEFT JOIN media_platforms mp ON mp.platform_id = pr.platform_id
|
||||
LEFT JOIN platform_accounts pa ON pa.id = pr.platform_account_id
|
||||
@@ -210,15 +220,21 @@ func (s *MediaService) listPublishRecordsByStatuses(
|
||||
LIMIT 1
|
||||
) dt ON TRUE
|
||||
WHERE pr.tenant_id = $1
|
||||
AND a.brand_id = $2
|
||||
AND a.deleted_at IS NULL
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM publish_batches pb
|
||||
WHERE pb.id = pr.publish_batch_id
|
||||
AND pb.tenant_id = pr.tenant_id
|
||||
AND pb.initiator_user_id = $2
|
||||
)
|
||||
AND pr.deleted_at IS NULL
|
||||
AND pr.status = ANY($3)
|
||||
AND (
|
||||
$4 = ''
|
||||
OR COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', ''), '') ILIKE '%' || $4 || '%'
|
||||
)
|
||||
`
|
||||
args := []any{tenantID, brandID, statuses, title}
|
||||
args := []any{tenantID, userID, statuses, title}
|
||||
|
||||
if strings.TrimSpace(orderBy) != "" {
|
||||
query += "\nORDER BY " + orderBy
|
||||
@@ -243,6 +259,9 @@ func (s *MediaService) listPublishRecordsByStatuses(
|
||||
&item.ID,
|
||||
&item.PublishBatchID,
|
||||
&item.ArticleID,
|
||||
&item.BrandID,
|
||||
&item.BrandName,
|
||||
&item.BrandDeleted,
|
||||
&item.ArticleTitle,
|
||||
&item.PlatformAccountID,
|
||||
&desktopAccountID,
|
||||
@@ -281,7 +300,7 @@ func (s *MediaService) listPublishRecordsByStatuses(
|
||||
func (s *MediaService) countPublishRecordsByStatuses(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
userID int64,
|
||||
statuses []string,
|
||||
title string,
|
||||
) (int, error) {
|
||||
@@ -292,14 +311,20 @@ func (s *MediaService) countPublishRecordsByStatuses(
|
||||
JOIN articles a ON a.id = pr.article_id AND a.tenant_id = pr.tenant_id
|
||||
LEFT JOIN article_versions av ON av.id = a.current_version_id AND av.article_id = a.id
|
||||
WHERE pr.tenant_id = $1
|
||||
AND a.brand_id = $2
|
||||
AND a.deleted_at IS NULL
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM publish_batches pb
|
||||
WHERE pb.id = pr.publish_batch_id
|
||||
AND pb.tenant_id = pr.tenant_id
|
||||
AND pb.initiator_user_id = $2
|
||||
)
|
||||
AND pr.deleted_at IS NULL
|
||||
AND pr.status = ANY($3)
|
||||
AND (
|
||||
$4 = ''
|
||||
OR COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', ''), '') ILIKE '%' || $4 || '%'
|
||||
)
|
||||
`, tenantID, brandID, statuses, title).Scan(&count)
|
||||
`, tenantID, userID, statuses, title).Scan(&count)
|
||||
if err != nil {
|
||||
return 0, response.ErrInternal(50043, "publish_record_query_failed", "failed to list publish records")
|
||||
}
|
||||
@@ -317,7 +342,9 @@ func (s *MediaService) ListArticlePublishRecords(ctx context.Context, articleID
|
||||
|
||||
func (s *MediaService) listArticlePublishRecords(ctx context.Context, tenantID, brandID, articleID int64) ([]PublishRecordResponse, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT pr.id, pr.publish_batch_id, pr.article_id, pr.platform_account_id, pa.desktop_id::text,
|
||||
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,
|
||||
pr.platform_account_id, pa.desktop_id::text,
|
||||
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,
|
||||
CASE
|
||||
@@ -334,7 +361,8 @@ func (s *MediaService) listArticlePublishRecords(ctx context.Context, tenantID,
|
||||
ON esc.id = pr.target_connection_id
|
||||
AND esc.tenant_id = pr.tenant_id
|
||||
JOIN articles a ON a.id = pr.article_id AND a.tenant_id = pr.tenant_id
|
||||
WHERE pr.tenant_id = $1 AND pr.article_id = $2 AND a.brand_id = $3 AND a.deleted_at IS NULL
|
||||
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
|
||||
ORDER BY pr.created_at DESC, pr.id DESC
|
||||
`, tenantID, articleID, brandID)
|
||||
if err != nil {
|
||||
@@ -350,6 +378,9 @@ func (s *MediaService) listArticlePublishRecords(ctx context.Context, tenantID,
|
||||
&item.ID,
|
||||
&item.PublishBatchID,
|
||||
&item.ArticleID,
|
||||
&item.BrandID,
|
||||
&item.BrandName,
|
||||
&item.BrandDeleted,
|
||||
&item.PlatformAccountID,
|
||||
&desktopAccountID,
|
||||
&item.TargetType,
|
||||
@@ -377,6 +408,112 @@ func (s *MediaService) listArticlePublishRecords(ctx context.Context, tenantID,
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *MediaService) DeletePublishRecord(ctx context.Context, recordID int64) (*DeletePublishRecordResponse, error) {
|
||||
if recordID <= 0 {
|
||||
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 {
|
||||
return nil, response.ErrUnauthorized(40132, "workspace_scope_missing", "workspace context is required")
|
||||
}
|
||||
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50044, "publish_record_delete_begin_failed", "failed to delete publish record")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
var publishBatchID int64
|
||||
var articleID int64
|
||||
var cancelErrorJSON []byte
|
||||
cancelErrorJSON, err = marshalOptionalJSON(map[string]any{
|
||||
"reason": "publish_record_deleted",
|
||||
"source": "tenant_web",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50044, "publish_record_delete_payload_failed", "failed to delete publish record")
|
||||
}
|
||||
err = tx.QueryRow(ctx, `
|
||||
SELECT pr.publish_batch_id, pr.article_id
|
||||
FROM publish_records pr
|
||||
JOIN articles a
|
||||
ON a.id = pr.article_id
|
||||
AND a.tenant_id = pr.tenant_id
|
||||
JOIN publish_batches pb
|
||||
ON pb.id = pr.publish_batch_id
|
||||
AND pb.tenant_id = pr.tenant_id
|
||||
WHERE pr.id = $1
|
||||
AND pr.tenant_id = $2
|
||||
AND pb.initiator_user_id = $3
|
||||
AND pr.deleted_at IS NULL
|
||||
FOR UPDATE OF pr
|
||||
`, recordID, actor.TenantID, actor.UserID).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")
|
||||
}
|
||||
return nil, response.ErrInternal(50044, "publish_record_delete_lookup_failed", "failed to delete publish record")
|
||||
}
|
||||
|
||||
if tag, err := tx.Exec(ctx, `
|
||||
UPDATE publish_records
|
||||
SET deleted_at = NOW(),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
AND tenant_id = $2
|
||||
AND deleted_at IS NULL
|
||||
`, recordID, actor.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 _, err := tx.Exec(ctx, `
|
||||
UPDATE desktop_tasks
|
||||
SET status = 'aborted',
|
||||
error = $3,
|
||||
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
|
||||
`, 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
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE publish_batches
|
||||
SET status = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND tenant_id = $3
|
||||
`, batchStatus, publishBatchID, actor.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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
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 {
|
||||
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")
|
||||
}
|
||||
return &DeletePublishRecordResponse{Deleted: true}, nil
|
||||
}
|
||||
|
||||
func publishBatchRequiresCover(accounts []platformAccountSeed) bool {
|
||||
for _, account := range accounts {
|
||||
if publishPlatformRequiresCover(account.PlatformID) {
|
||||
|
||||
@@ -566,6 +566,7 @@ func loadExistingPublishRecordTargets(
|
||||
AND pa.workspace_id = $2
|
||||
AND pr.article_id = $3
|
||||
AND pr.platform_account_id = ANY($4::bigint[])
|
||||
AND pr.deleted_at IS NULL
|
||||
AND pr.status IN ('queued', 'publishing', 'success')
|
||||
AND dt.status IN ('queued', 'in_progress', 'succeeded', 'unknown')
|
||||
ORDER BY pr.platform_account_id,
|
||||
@@ -731,13 +732,18 @@ func (s *PublishJobService) RetryTenantDesktopTask(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
brandID, err := s.resolveRetryArticleBrandID(ctx, actor.TenantID, articleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
title := strings.TrimSpace(stringPointerValue(extractStringPointer(payload, "title")))
|
||||
if title == "" {
|
||||
title = "文章发布"
|
||||
}
|
||||
|
||||
return s.Create(ctx, auth.Actor{
|
||||
publishCtx := auth.WithCurrentBrandID(ctx, brandID)
|
||||
return s.Create(publishCtx, auth.Actor{
|
||||
TenantID: actor.TenantID,
|
||||
UserID: actor.UserID,
|
||||
PrimaryWorkspaceID: workspaceID,
|
||||
@@ -895,7 +901,7 @@ func (s *PublishJobService) resolveRetryArticleID(ctx context.Context, tenantID
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT article_id
|
||||
FROM publish_records
|
||||
WHERE id = $1 AND tenant_id = $2
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
`, publishRecordID, tenantID).Scan(&articleID)
|
||||
if err == nil && articleID > 0 {
|
||||
return articleID, nil
|
||||
|
||||
@@ -138,6 +138,7 @@ func cancelQueuedPublishTasksForUnavailableArticle(ctx context.Context, tx pgx.T
|
||||
AND pr.tenant_id = $1
|
||||
AND pr.article_id = $2
|
||||
AND pr.status IN ('queued', 'publishing', 'pending', 'running')
|
||||
AND pr.deleted_at IS NULL
|
||||
AND j.tenant_id = $1
|
||||
AND j.article_id = $2
|
||||
AND j.status = 'queued'
|
||||
@@ -189,23 +190,27 @@ func cancelQueuedPublishTasksForUnavailableArticle(ctx context.Context, tx pgx.T
|
||||
SELECT 1
|
||||
FROM publish_records pr
|
||||
WHERE pr.publish_batch_id = pb.id
|
||||
AND pr.deleted_at IS NULL
|
||||
AND pr.status IN ('queued', 'publishing', 'pending', 'running')
|
||||
) THEN 'publishing'
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM publish_records pr
|
||||
WHERE pr.publish_batch_id = pb.id
|
||||
AND pr.deleted_at IS NULL
|
||||
AND pr.status IN ('success', 'published', 'publish_success')
|
||||
) AND EXISTS (
|
||||
SELECT 1
|
||||
FROM publish_records pr
|
||||
WHERE pr.publish_batch_id = pb.id
|
||||
AND pr.deleted_at IS NULL
|
||||
AND pr.status NOT IN ('success', 'published', 'publish_success')
|
||||
) THEN 'partial_success'
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM publish_records pr
|
||||
WHERE pr.publish_batch_id = pb.id
|
||||
AND pr.deleted_at IS NULL
|
||||
AND pr.status IN ('success', 'published', 'publish_success')
|
||||
) THEN 'success'
|
||||
ELSE 'failed'
|
||||
@@ -223,6 +228,7 @@ func cancelQueuedPublishTasksForUnavailableArticle(ctx context.Context, tx pgx.T
|
||||
FROM publish_records pr
|
||||
WHERE pr.article_id = a.id
|
||||
AND pr.tenant_id = a.tenant_id
|
||||
AND pr.deleted_at IS NULL
|
||||
AND pr.status IN ('queued', 'publishing', 'pending', 'running')
|
||||
) THEN 'publishing'
|
||||
WHEN EXISTS (
|
||||
@@ -230,12 +236,14 @@ func cancelQueuedPublishTasksForUnavailableArticle(ctx context.Context, tx pgx.T
|
||||
FROM publish_records pr
|
||||
WHERE pr.article_id = a.id
|
||||
AND pr.tenant_id = a.tenant_id
|
||||
AND pr.deleted_at IS NULL
|
||||
AND pr.status IN ('success', 'published', 'publish_success')
|
||||
) AND EXISTS (
|
||||
SELECT 1
|
||||
FROM publish_records pr
|
||||
WHERE pr.article_id = a.id
|
||||
AND pr.tenant_id = a.tenant_id
|
||||
AND pr.deleted_at IS NULL
|
||||
AND pr.status NOT IN ('success', 'published', 'publish_success')
|
||||
) THEN 'partial_success'
|
||||
WHEN EXISTS (
|
||||
@@ -243,6 +251,7 @@ func cancelQueuedPublishTasksForUnavailableArticle(ctx context.Context, tx pgx.T
|
||||
FROM publish_records pr
|
||||
WHERE pr.article_id = a.id
|
||||
AND pr.tenant_id = a.tenant_id
|
||||
AND pr.deleted_at IS NULL
|
||||
AND pr.status IN ('success', 'published', 'publish_success')
|
||||
) THEN 'success'
|
||||
ELSE 'failed'
|
||||
@@ -296,6 +305,7 @@ func cancelQueuedPublishTasksForDesktopAccountUnbind(
|
||||
AND dt.kind = 'publish'
|
||||
AND dt.status = 'queued'
|
||||
AND pr.status IN ('queued', 'publishing', 'pending', 'running')
|
||||
AND pr.deleted_at IS NULL
|
||||
`, tenantID, workspaceID, accountID)
|
||||
if err != nil {
|
||||
return 0, response.ErrInternal(50121, "publish_task_cancel_account_unbind_failed", "failed to inspect queued publish tasks for unbound account")
|
||||
@@ -335,6 +345,7 @@ func cancelQueuedPublishTasksForDesktopAccountUnbind(
|
||||
WHERE tenant_id = $1
|
||||
AND id = ANY($2::bigint[])
|
||||
AND status IN ('queued', 'publishing', 'pending', 'running')
|
||||
AND deleted_at IS NULL
|
||||
`, tenantID, recordIDs); err != nil {
|
||||
return 0, response.ErrInternal(50121, "publish_task_cancel_account_unbind_failed", "failed to cancel publish records for unbound account")
|
||||
}
|
||||
@@ -412,7 +423,7 @@ func syncDesktopPublishTaskState(
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT publish_batch_id, article_id
|
||||
FROM publish_records
|
||||
WHERE id = $1 AND tenant_id = $2
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
`, publishRecordID, task.TenantID).Scan(&publishBatchID, &articleID); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, response.ErrNotFound(40434, "publish_record_not_found", "publish record not found")
|
||||
@@ -451,7 +462,7 @@ func syncDesktopPublishTaskState(
|
||||
response_payload_json = $7,
|
||||
error_message = $8,
|
||||
updated_at = NOW()
|
||||
WHERE id = $9 AND tenant_id = $10
|
||||
WHERE id = $9 AND tenant_id = $10 AND deleted_at IS NULL
|
||||
`, publishStatus,
|
||||
externalArticleID,
|
||||
externalArticleURL,
|
||||
@@ -567,7 +578,9 @@ func desktopTaskToPublishStatus(taskStatus string) string {
|
||||
return "failed"
|
||||
case "succeeded":
|
||||
return "success"
|
||||
case "failed", "aborted":
|
||||
case "aborted":
|
||||
return "cancelled"
|
||||
case "failed":
|
||||
return "failed"
|
||||
default:
|
||||
return "failed"
|
||||
@@ -602,6 +615,7 @@ func recalculatePublishBatchStatus(ctx context.Context, tx pgx.Tx, publishBatchI
|
||||
SELECT status
|
||||
FROM publish_records
|
||||
WHERE publish_batch_id = $1
|
||||
AND deleted_at IS NULL
|
||||
`, publishBatchID)
|
||||
if err != nil {
|
||||
return "", response.ErrInternal(50054, "publish_batch_status_query_failed", "failed to inspect publish batch status")
|
||||
@@ -621,7 +635,7 @@ func recalculatePublishBatchStatus(ctx context.Context, tx pgx.Tx, publishBatchI
|
||||
}
|
||||
|
||||
if len(statuses) == 0 {
|
||||
return "publishing", nil
|
||||
return "failed", nil
|
||||
}
|
||||
|
||||
counts := map[string]int{
|
||||
@@ -650,6 +664,7 @@ func recalculateArticlePublishStatus(ctx context.Context, tx pgx.Tx, tenantID, a
|
||||
SELECT status
|
||||
FROM publish_records
|
||||
WHERE tenant_id = $1 AND article_id = $2
|
||||
AND deleted_at IS NULL
|
||||
`, tenantID, articleID)
|
||||
if err != nil {
|
||||
return "", response.ErrInternal(50055, "article_publish_status_query_failed", "failed to inspect article publish status")
|
||||
|
||||
@@ -175,6 +175,24 @@ func TestPublishBatchStatusToArticleStatusKeepsPartialSuccess(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecalculatePublishBatchStatusDefaultsToFailed(t *testing.T) {
|
||||
tx := &capturePublishDedupLookupTx{rows: emptyPublishDedupRows{}}
|
||||
|
||||
got, err := recalculatePublishBatchStatus(context.Background(), tx, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("recalculatePublishBatchStatus() error = %v", err)
|
||||
}
|
||||
if got != "failed" {
|
||||
t.Fatalf("recalculatePublishBatchStatus() = %q, want failed", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDesktopTaskToPublishStatusMapsAbortToCancelled(t *testing.T) {
|
||||
if got := desktopTaskToPublishStatus("aborted"); got != "cancelled" {
|
||||
t.Fatalf("desktopTaskToPublishStatus(aborted) = %q, want cancelled", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecalculateArticlePublishStatusDefaultsToUnpublished(t *testing.T) {
|
||||
tx := &capturePublishDedupLookupTx{rows: emptyPublishDedupRows{}}
|
||||
|
||||
|
||||
@@ -124,7 +124,8 @@ type DesktopTaskRepository interface {
|
||||
Complete(ctx context.Context, desktopID uuid.UUID, leaseTokenHash []byte, status string, resultJSON, errorJSON []byte) (*DesktopTask, error)
|
||||
CancelByLease(ctx context.Context, desktopID uuid.UUID, leaseTokenHash []byte, errorJSON []byte) (*DesktopTask, error)
|
||||
CancelByClient(ctx context.Context, desktopID, clientID uuid.UUID, errorJSON []byte) (*DesktopTask, error)
|
||||
TenantCancel(ctx context.Context, desktopID uuid.UUID, workspaceID int64, errorJSON []byte) (*DesktopTask, error)
|
||||
CancelQueuedPublishByOwner(ctx context.Context, desktopID uuid.UUID, tenantID, workspaceID, userID int64, errorJSON []byte) (*DesktopTask, error)
|
||||
TenantCancel(ctx context.Context, desktopID uuid.UUID, tenantID, workspaceID, userID int64, errorJSON []byte) (*DesktopTask, error)
|
||||
Reconcile(ctx context.Context, desktopID uuid.UUID, workspaceID int64, status string, resultJSON, errorJSON []byte) (*DesktopTask, error)
|
||||
MarkUnknownByClient(ctx context.Context, clientID uuid.UUID, workspaceID int64, errorJSON []byte) (int64, error)
|
||||
CreateAttempt(ctx context.Context, params CreateDesktopTaskAttemptParams) (*DesktopTaskAttempt, error)
|
||||
@@ -263,11 +264,27 @@ func (r *desktopTaskRepository) CancelByClient(ctx context.Context, desktopID, c
|
||||
return desktopTaskFromGenerated(row), nil
|
||||
}
|
||||
|
||||
func (r *desktopTaskRepository) TenantCancel(ctx context.Context, desktopID uuid.UUID, workspaceID int64, errorJSON []byte) (*DesktopTask, error) {
|
||||
func (r *desktopTaskRepository) CancelQueuedPublishByOwner(ctx context.Context, desktopID uuid.UUID, tenantID, workspaceID, userID int64, errorJSON []byte) (*DesktopTask, error) {
|
||||
row, err := r.q.CancelQueuedPublishDesktopTaskByOwner(ctx, generated.CancelQueuedPublishDesktopTaskByOwnerParams{
|
||||
Error: errorJSON,
|
||||
DesktopID: pgUUID(desktopID),
|
||||
TenantID: tenantID,
|
||||
WorkspaceID: workspaceID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return desktopTaskFromGenerated(row), nil
|
||||
}
|
||||
|
||||
func (r *desktopTaskRepository) TenantCancel(ctx context.Context, desktopID uuid.UUID, tenantID, workspaceID, userID int64, errorJSON []byte) (*DesktopTask, error) {
|
||||
row, err := r.q.TenantCancelDesktopTask(ctx, generated.TenantCancelDesktopTaskParams{
|
||||
Error: errorJSON,
|
||||
DesktopID: pgUUID(desktopID),
|
||||
TenantID: tenantID,
|
||||
WorkspaceID: workspaceID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -169,6 +169,26 @@ WHERE desktop_id = sqlc.arg(desktop_id)
|
||||
AND status = 'queued'
|
||||
RETURNING *;
|
||||
|
||||
-- name: CancelQueuedPublishDesktopTaskByOwner :one
|
||||
UPDATE desktop_tasks AS t
|
||||
SET status = 'aborted',
|
||||
error = sqlc.narg(error),
|
||||
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 = sqlc.arg(desktop_id)
|
||||
AND t.tenant_id = sqlc.arg(tenant_id)
|
||||
AND t.workspace_id = sqlc.arg(workspace_id)
|
||||
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 = sqlc.arg(user_id)
|
||||
RETURNING t.*;
|
||||
|
||||
-- name: TenantCancelDesktopTask :one
|
||||
UPDATE desktop_tasks
|
||||
SET status = 'aborted',
|
||||
@@ -178,8 +198,17 @@ SET status = 'aborted',
|
||||
lease_expires_at = NULL,
|
||||
updated_at = now()
|
||||
WHERE desktop_id = sqlc.arg(desktop_id)
|
||||
AND tenant_id = sqlc.arg(tenant_id)
|
||||
AND workspace_id = sqlc.arg(workspace_id)
|
||||
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 = sqlc.arg(user_id)
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: ReconcileDesktopTask :one
|
||||
|
||||
@@ -80,3 +80,17 @@ func (h *MediaHandler) ListPublishRecords(c *gin.Context) {
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
func (h *MediaHandler) DeletePublishRecord(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.DeletePublishRecord(c.Request.Context(), recordID)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
@@ -93,8 +93,9 @@ func RegisterRoutes(app *bootstrap.App) {
|
||||
tenantProtected.POST("/tasks/:id/reconcile", desktopTaskHandler.Reconcile)
|
||||
tenantProtected.POST("/tasks/:id/cancel", desktopTaskHandler.TenantCancel)
|
||||
tenantProtected.GET("/publish-tasks", desktopTaskHandler.TenantListPublish)
|
||||
tenantProtected.POST("/publish-tasks/:id/retry", middleware.RequireCurrentBrand(), desktopTaskHandler.TenantRetryPublish)
|
||||
tenantProtected.GET("/publish-records", middleware.RequireCurrentBrand(), mediaHandler.ListAllPublishRecords)
|
||||
tenantProtected.POST("/publish-tasks/:id/retry", desktopTaskHandler.TenantRetryPublish)
|
||||
tenantProtected.GET("/publish-records", mediaHandler.ListAllPublishRecords)
|
||||
tenantProtected.DELETE("/publish-records/:id", mediaHandler.DeletePublishRecord)
|
||||
tenantProtected.POST("/publish-jobs", middleware.RequireCurrentBrand(), publishJobHandler.Create)
|
||||
|
||||
complianceHandler := NewComplianceHandler(app)
|
||||
|
||||
@@ -35,6 +35,7 @@ func TestProtectedRoutes_RequireAuth(t *testing.T) {
|
||||
{http.MethodGet, "/api/tenant/publish-tasks"},
|
||||
{http.MethodPost, "/api/tenant/publish-tasks/:id/retry"},
|
||||
{http.MethodGet, "/api/tenant/publish-records"},
|
||||
{http.MethodDelete, "/api/tenant/publish-records/:id"},
|
||||
{http.MethodPost, "/api/tenant/publish-jobs"},
|
||||
{http.MethodGet, "/api/tenant/media/platforms"},
|
||||
{http.MethodGet, "/api/tenant/workspace/overview"},
|
||||
|
||||
Reference in New Issue
Block a user