From 03f6145a61d54533e326fb0e14f5da4cad7e28b5 Mon Sep 17 00:00:00 2001 From: Rogee Date: Mon, 28 Sep 2026 09:19:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(creator):=20=E4=BF=AE=E5=A4=8D=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E9=A1=B5=E5=88=97=E8=A1=A8=20SQL=20=E2=80=94=E2=80=94?= =?UTF-8?q?=20join=20=E7=BC=BA=E5=88=AB=E5=90=8D=E5=AF=BC=E8=87=B4=20/acco?= =?UTF-8?q?unts/import=20=E4=B8=80=E8=BF=9B=E6=9D=A5=E5=B0=B1=20500?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ListCompetitorShareJobsWithAuthor 的 LEFT JOIN creator_competitor 缺 AS competitor 别名,ON 条件引用 competitor.id 直接报 missing FROM-clause entry(42P01) - competitorShareJobFilter 过滤条件误用原表名 creator_competitor_share_job, 与查询别名 share_job 不符,带 platform/status 参数时会再报缺列 - 新增集成测试 TestListCompetitorShareJobsWithAuthorSQL 覆盖 join 作者回填 与 platform/status 过滤(此前该 SQL 无任何测试,bug 逃逸) --- internal/creator/competitor_share_jobs.go | 8 +- .../creator/competitor_share_jobs_test.go | 79 +++++++++++++++++++ 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/internal/creator/competitor_share_jobs.go b/internal/creator/competitor_share_jobs.go index df56085..e499d0a 100644 --- a/internal/creator/competitor_share_jobs.go +++ b/internal/creator/competitor_share_jobs.go @@ -95,8 +95,8 @@ func (s *Store) ListCompetitorShareJobsWithAuthor(ctx context.Context, platform, query, args := competitorShareJobFilter(`SELECT share_job.id, share_job.platform, share_job.share_url, share_job.tags, share_job.status, share_job.attempts, share_job.competitor_id, share_job.failure_reason, share_job.lease_until, share_job.last_attempt_at, share_job.completed_at, share_job.created_at, share_job.updated_at, competitor.nickname, competitor.avatar_url - FROM creator_competitor_share_job share_job - LEFT JOIN creator_competitor ON competitor.id = share_job.competitor_id`, platform, status) + FROM creator_competitor_share_job AS share_job + LEFT JOIN creator_competitor AS competitor ON competitor.id = share_job.competitor_id`, platform, status) rows, err := s.db.QueryContext(ctx, query+` ORDER BY share_job.created_at DESC, share_job.id`, args...) if err != nil { return nil, databaseError(err) @@ -140,11 +140,11 @@ func competitorShareJobFilter(query, platform, status string) (string, []any) { args := make([]any, 0, 2) if platform != "" { args = append(args, platform) - conditions = append(conditions, "creator_competitor_share_job.platform = $"+strconv.Itoa(len(args))) + conditions = append(conditions, "share_job.platform = $"+strconv.Itoa(len(args))) } if status != "" { args = append(args, status) - conditions = append(conditions, "creator_competitor_share_job.status = $"+strconv.Itoa(len(args))) + conditions = append(conditions, "share_job.status = $"+strconv.Itoa(len(args))) } if len(conditions) > 0 { query += ` WHERE ` + strings.Join(conditions, ` AND `) diff --git a/internal/creator/competitor_share_jobs_test.go b/internal/creator/competitor_share_jobs_test.go index 4c606b1..416ac0c 100644 --- a/internal/creator/competitor_share_jobs_test.go +++ b/internal/creator/competitor_share_jobs_test.go @@ -3,6 +3,7 @@ package creator import ( "errors" "testing" + "time" ) func TestNormalizeCompetitorShareJobInput(t *testing.T) { @@ -27,3 +28,81 @@ func TestNormalizeCompetitorShareJobInput(t *testing.T) { } } } + +// 集成测试:ListCompetitorShareJobsWithAuthor 的 SQL(join 别名 + 过滤条件别名)必须真实可执行。 +// 回归背景:LEFT JOIN creator_competitor ON competitor.id=... 缺 AS competitor 别名, +// 过滤条件误用原表名而非 share_job 别名,导入页一进来就 500 "creator operation failed"。 +func TestListCompetitorShareJobsWithAuthorSQL(t *testing.T) { + store, _, ctx := openCreatorIntegrationStore(t) + if err := store.Ping(ctx); err != nil { + t.Fatal(err) + } + if err := store.EnsureSchema(ctx); err != nil { + t.Fatal(err) + } + + job, err := store.CreateCompetitorShareJob(ctx, CompetitorShareJobInput{ + Platform: PlatformDouyin, ShareURL: "https://v.douyin.com/import-author-sql/", + }) + if err != nil { + t.Fatal(err) + } + competitor, err := store.UpsertCompetitor(ctx, CompetitorInput{ + Platform: PlatformDouyin, PlatformAccountKey: "sec_uid_import-author-sql", + Nickname: "解析作者", AvatarURL: "https://example.com/a.jpg", + HomepageURL: "https://www.douyin.com/user/import-author-sql", + }) + if err != nil { + t.Fatal(err) + } + _, token, ok, err := store.ClaimCompetitorShareJob(ctx, job.ID, time.Now().UTC()) + if err != nil || !ok { + t.Fatalf("claim job: ok=%v err=%v", ok, err) + } + if err := store.MarkCompetitorShareJob(ctx, job.ID, token, CompetitorShareJobSucceeded, competitor.ID, ""); err != nil { + t.Fatal(err) + } + if _, err := store.CreateCompetitorShareJob(ctx, CompetitorShareJobInput{ + Platform: PlatformDouyin, ShareURL: "https://v.douyin.com/import-author-sql-other/", + }); err != nil { + t.Fatal(err) + } + + // 无过滤:两条任务都能列出,成功任务带作者信息。 + items, err := store.ListCompetitorShareJobsWithAuthor(ctx, "", "") + if err != nil { + t.Fatalf("list with author: %v", err) + } + if len(items) != 2 { + t.Fatalf("items = %d, want 2", len(items)) + } + var matched bool + for _, item := range items { + if item.ID != job.ID { + continue + } + matched = true + if item.AuthorName != "解析作者" || item.AuthorAvatarURL != "https://example.com/a.jpg" || item.CompetitorID != competitor.ID { + t.Fatalf("author view = %+v", item) + } + } + if !matched { + t.Fatalf("job %s missing in list", job.ID) + } + + // platform+status 过滤:条件引用 join 别名,不能报缺列。 + filtered, err := store.ListCompetitorShareJobsWithAuthor(ctx, PlatformDouyin, CompetitorShareJobSucceeded) + if err != nil { + t.Fatalf("list filtered: %v", err) + } + if len(filtered) != 1 || filtered[0].ID != job.ID { + t.Fatalf("filtered = %+v", filtered) + } + none, err := store.ListCompetitorShareJobsWithAuthor(ctx, PlatformDouyin, CompetitorShareJobFailed) + if err != nil { + t.Fatalf("list filtered empty: %v", err) + } + if len(none) != 0 { + t.Fatalf("expected empty, got %+v", none) + } +}