fix(creator): 修复导入页列表 SQL —— join 缺别名导致 /accounts/import 一进来就 500

- 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 逃逸)
This commit is contained in:
2026-09-28 09:19:02 +08:00
parent 2baa2e8406
commit 03f6145a61
2 changed files with 83 additions and 4 deletions
+4 -4
View File
@@ -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 `)
@@ -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)
}
}