From a46ed4b9f6413d4bd96ef4653dae61b416d4a5f3 Mon Sep 17 00:00:00 2001 From: liangxu Date: Mon, 20 Apr 2026 11:07:48 +0800 Subject: [PATCH] fix(tenant): resolve desktop retry article id via publish record/batch Fall back to looking up article_id from publish_records or publish_batches when the desktop task payload lacks article_id at the top level. Covers payloads that only carry content_ref.{article_id,id}, publish_record_id, or publish_batch_id, preventing spurious 40096 errors when retrying older desktop tasks. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../tenant/app/publish_job_service.go | 61 +++++++++++++++++- .../tenant/app/publish_job_service_test.go | 63 +++++++++++++++++++ 2 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 server/internal/tenant/app/publish_job_service_test.go diff --git a/server/internal/tenant/app/publish_job_service.go b/server/internal/tenant/app/publish_job_service.go index 20a4a6f..7e0b4a9 100644 --- a/server/internal/tenant/app/publish_job_service.go +++ b/server/internal/tenant/app/publish_job_service.go @@ -264,9 +264,9 @@ func (s *PublishJobService) RetryByDesktopTask( } payload := unmarshalJSONObject(task.Payload) - articleID, ok := extractInt64FromMap(payload, "article_id") - if !ok || articleID <= 0 { - return nil, response.ErrBadRequest(40096, "desktop_task_article_missing", "desktop publish task is missing article_id") + articleID, err := s.resolveRetryArticleID(ctx, client.TenantID, payload) + if err != nil { + return nil, err } title := strings.TrimSpace(stringPointerValue(extractStringPointer(payload, "title"))) @@ -289,6 +289,61 @@ func (s *PublishJobService) RetryByDesktopTask( }) } +func (s *PublishJobService) resolveRetryArticleID(ctx context.Context, tenantID int64, payload map[string]any) (int64, error) { + if articleID, ok := resolveRetryArticleIDFromPayload(payload); ok { + return articleID, nil + } + + if publishRecordID, ok := extractInt64FromMap(payload, "publish_record_id"); ok && publishRecordID > 0 { + var articleID int64 + err := s.pool.QueryRow(ctx, ` + SELECT article_id + FROM publish_records + WHERE id = $1 AND tenant_id = $2 + `, publishRecordID, tenantID).Scan(&articleID) + if err == nil && articleID > 0 { + return articleID, nil + } + if err != nil && !errors.Is(err, pgx.ErrNoRows) { + return 0, response.ErrInternal(50113, "desktop_publish_record_lookup_failed", "failed to lookup desktop publish record") + } + } + + if publishBatchID, ok := extractInt64FromMap(payload, "publish_batch_id"); ok && publishBatchID > 0 { + var articleID int64 + err := s.pool.QueryRow(ctx, ` + SELECT article_id + FROM publish_batches + WHERE id = $1 AND tenant_id = $2 + `, publishBatchID, tenantID).Scan(&articleID) + if err == nil && articleID > 0 { + return articleID, nil + } + if err != nil && !errors.Is(err, pgx.ErrNoRows) { + return 0, response.ErrInternal(50114, "desktop_publish_batch_lookup_failed", "failed to lookup desktop publish batch") + } + } + + return 0, response.ErrBadRequest(40096, "desktop_task_article_missing", "desktop publish task is missing article_id") +} + +func resolveRetryArticleIDFromPayload(payload map[string]any) (int64, bool) { + if articleID, ok := extractInt64FromMap(payload, "article_id"); ok && articleID > 0 { + return articleID, true + } + if len(payload) == 0 { + return 0, false + } + nestedContentRef, ok := payload["content_ref"].(map[string]any) + if !ok { + return 0, false + } + if articleID, ok := extractInt64FromMap(nestedContentRef, "article_id", "id"); ok && articleID > 0 { + return articleID, true + } + return 0, false +} + func (s *PublishJobService) logWarn(message string, err error, fields ...zap.Field) { if s.logger == nil || err == nil { return diff --git a/server/internal/tenant/app/publish_job_service_test.go b/server/internal/tenant/app/publish_job_service_test.go new file mode 100644 index 0000000..4c43fc7 --- /dev/null +++ b/server/internal/tenant/app/publish_job_service_test.go @@ -0,0 +1,63 @@ +package app + +import "testing" + +func TestResolveRetryArticleIDFromPayload(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + payload map[string]any + wantID int64 + wantOK bool + }{ + { + name: "top level article id", + payload: map[string]any{ + "article_id": 78, + }, + wantID: 78, + wantOK: true, + }, + { + name: "nested content ref article id", + payload: map[string]any{ + "content_ref": map[string]any{ + "article_id": 79, + }, + }, + wantID: 79, + wantOK: true, + }, + { + name: "nested content ref generic id", + payload: map[string]any{ + "content_ref": map[string]any{ + "id": 80, + }, + }, + wantID: 80, + wantOK: true, + }, + { + name: "missing article id", + payload: map[string]any{ + "publish_record_id": 999, + }, + wantID: 0, + wantOK: false, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + gotID, gotOK := resolveRetryArticleIDFromPayload(tc.payload) + if gotID != tc.wantID || gotOK != tc.wantOK { + t.Fatalf("resolveRetryArticleIDFromPayload() = (%d, %v), want (%d, %v)", gotID, gotOK, tc.wantID, tc.wantOK) + } + }) + } +}