From 998871e3c4e6a2a20d5a5d3e146ebcbe43baf0c8 Mon Sep 17 00:00:00 2001 From: Rogee Date: Tue, 29 Sep 2026 15:11:23 +0800 Subject: [PATCH] =?UTF-8?q?fix(creator):=20=E5=BC=BA=E5=88=B6=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E6=8A=A2=E9=94=81=E5=A4=B1=E8=B4=A5=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E2=80=94=E2=80=94=E7=A7=9F=E7=BA=A6=E8=A2=AB?= =?UTF-8?q?=E5=8D=A0/=E7=AB=9E=E5=93=81=E5=81=9C=E7=94=A8=E5=88=86?= =?UTF-8?q?=E5=88=AB=E8=BF=94=E5=9B=9E=E5=8F=AF=E8=AF=BB=20409?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ClaimCompetitorSync 失败后经 SyncClaimFailure 复查竞品状态, 区分 ErrSyncInProgress(该竞品正在同步中)与 ErrSyncDisabled(已停用) - creatorError 新增两分类映射 409;通用冲突文案保持不变 - 前端竞品强制同步去掉固定'强制同步失败'后缀,直接展示分类提示 - 回归:TestCompetitorSyncClaimFailureClassification(PG 集成) --- internal/controlplane/api/creator.go | 7 +++- internal/creator/content.go | 13 +++++++ internal/creator/integration_test.go | 38 +++++++++++++++++++ internal/creator/logic.go | 12 +++--- .../accounts/MonitoringAccountList.tsx | 2 +- .../pages/accounts/monitoring/$id/index.tsx | 2 +- 6 files changed, 66 insertions(+), 8 deletions(-) diff --git a/internal/controlplane/api/creator.go b/internal/controlplane/api/creator.go index 06146d4..d53431f 100644 --- a/internal/controlplane/api/creator.go +++ b/internal/controlplane/api/creator.go @@ -621,6 +621,10 @@ func creatorError(c fiber.Ctx, err error) error { case errors.Is(err, accountdomain.ErrNotFound): // 跨域引用(如账号不存在):account 包哨兵也映射 404,避免 500 掩盖真实语义。 status, message = fiber.StatusNotFound, "resource not found" + case errors.Is(err, creator.ErrSyncInProgress): + status, message = fiber.StatusConflict, creator.ErrSyncInProgress.Error() + case errors.Is(err, creator.ErrSyncDisabled): + status, message = fiber.StatusConflict, creator.ErrSyncDisabled.Error() case errors.Is(err, creator.ErrConflict): status, message = fiber.StatusConflict, creator.ErrConflict.Error() case errors.Is(err, creator.ErrNotFound): @@ -1291,7 +1295,8 @@ func syncCreatorCompetitorWithClaim(ctx context.Context, store *creator.Store, h return creator.CollectionReport{}, err } if !claimed { - return creator.CollectionReport{}, creator.ErrConflict + // 抢锁失败说明竞品正被其它同步占用或已停用,分类后返回可读的 409 提示。 + return creator.CollectionReport{}, store.SyncClaimFailure(ctx, competitorID) } // blocked(网关不可用等环境暂不可用)也必须排下一次重试:否则 next_sync_at 置空后定时获取对该账号永久失效。 nextRetry := func() *time.Time { diff --git a/internal/creator/content.go b/internal/creator/content.go index 35b48e7..972aaf5 100644 --- a/internal/creator/content.go +++ b/internal/creator/content.go @@ -282,6 +282,19 @@ func (s *Store) ClaimCompetitorSync(ctx context.Context, id string, force bool, return token, claimed != "", nil } +// SyncClaimFailure 在 ClaimCompetitorSync 抢锁失败后调用:区分租约被占与竞品停用, +// 返回面向用户的分类错误;读不到竞品时退回通用 ErrConflict。 +func (s *Store) SyncClaimFailure(ctx context.Context, id string) error { + competitor, err := s.GetCompetitor(ctx, id) + if err != nil { + return ErrConflict + } + if !competitor.Enabled { + return ErrSyncDisabled + } + return ErrSyncInProgress +} + func (s *Store) ListDueCompetitors(ctx context.Context, now time.Time) ([]Competitor, error) { if now.IsZero() { return nil, ErrInvalid diff --git a/internal/creator/integration_test.go b/internal/creator/integration_test.go index 557d234..3f03524 100644 --- a/internal/creator/integration_test.go +++ b/internal/creator/integration_test.go @@ -170,6 +170,44 @@ func TestCompetitorShareJobLifecycle(t *testing.T) { } } +// 强制同步抢锁失败的分类:租约被占 → ErrSyncInProgress;竞品停用 → ErrSyncDisabled。 +func TestCompetitorSyncClaimFailureClassification(t *testing.T) { + store, _, ctx := openCreatorIntegrationStore(t) + stamp := fmt.Sprintf("%d", time.Now().UnixNano()) + competitor, err := store.CreateCompetitor(ctx, CompetitorInput{ + Platform: PlatformDouyin, PlatformAccountKey: "sec_uid_claim_" + stamp, UniqueID: "claim_" + stamp, + Nickname: "Claim", HomepageURL: "https://www.douyin.com/user/sec_uid_claim_" + stamp, + }) + if err != nil { + t.Fatal(err) + } + now := time.Now().UTC() + // 首次 force 抢锁成功;租约未释放前第二次 force 抢锁失败 → 正在同步中。 + token, claimed, err := store.ClaimCompetitorSync(ctx, competitor.ID, true, now) + if err != nil || !claimed { + t.Fatalf("first force claim must succeed: err=%v claimed=%v", err, claimed) + } + if _, claimed, err := store.ClaimCompetitorSync(ctx, competitor.ID, true, now.Add(time.Second)); err != nil || claimed { + t.Fatalf("second force claim must fail while lease held: err=%v claimed=%v", err, claimed) + } + if err := store.SyncClaimFailure(ctx, competitor.ID); !errors.Is(err, ErrSyncInProgress) { + t.Fatalf("held lease must classify as sync in progress: err=%v", err) + } + // 释放后停用 → 已停用。 + if err := store.MarkCompetitorSync(ctx, competitor.ID, token, "idle", "", "", nil); err != nil { + t.Fatal(err) + } + if _, err := store.SetCompetitorEnabled(ctx, competitor.ID, false); err != nil { + t.Fatal(err) + } + if _, claimed, err := store.ClaimCompetitorSync(ctx, competitor.ID, true, now.Add(2*time.Second)); err != nil || claimed { + t.Fatalf("disabled competitor must not be claimable: err=%v claimed=%v", err, claimed) + } + if err := store.SyncClaimFailure(ctx, competitor.ID); !errors.Is(err, ErrSyncDisabled) { + t.Fatalf("disabled competitor must classify as disabled: err=%v", err) + } +} + func TestCompetitorUpsertIsIdempotent(t *testing.T) { store, _, ctx := openCreatorIntegrationStore(t) stamp := fmt.Sprintf("%d", time.Now().UnixNano()) diff --git a/internal/creator/logic.go b/internal/creator/logic.go index 6e59b27..12f9bdc 100644 --- a/internal/creator/logic.go +++ b/internal/creator/logic.go @@ -8,11 +8,13 @@ import ( ) var ( - ErrInvalid = errors.New("invalid creator input") - ErrConflict = errors.New("creator resource conflicts with existing state") - ErrNotFound = errors.New("creator resource not found") - ErrUnavailable = errors.New("creator capability is unavailable") - ErrUncertain = errors.New("creator operation result is uncertain") + ErrInvalid = errors.New("invalid creator input") + ErrConflict = errors.New("creator resource conflicts with existing state") + ErrNotFound = errors.New("creator resource not found") + ErrUnavailable = errors.New("creator capability is unavailable") + ErrUncertain = errors.New("creator operation result is uncertain") + ErrSyncInProgress = errors.New("该竞品正在同步中,请稍后再试") + ErrSyncDisabled = errors.New("该竞品已停用,无法同步;启用后再试") ) func ValidatePlatform(platform string) bool { diff --git a/web/src/components/accounts/MonitoringAccountList.tsx b/web/src/components/accounts/MonitoringAccountList.tsx index 976f9c5..b01a0c0 100644 --- a/web/src/components/accounts/MonitoringAccountList.tsx +++ b/web/src/components/accounts/MonitoringAccountList.tsx @@ -72,7 +72,7 @@ export default function MonitoringAccountList() { messageApi.success('强制同步已执行。'); await load(); } catch (actionError) { - messageApi.error(conflictMessage(actionError, '强制同步失败')); + messageApi.error(conflictMessage(actionError)); } finally { setActionBusy(''); } diff --git a/web/src/pages/accounts/monitoring/$id/index.tsx b/web/src/pages/accounts/monitoring/$id/index.tsx index b8c3638..d53269e 100644 --- a/web/src/pages/accounts/monitoring/$id/index.tsx +++ b/web/src/pages/accounts/monitoring/$id/index.tsx @@ -81,7 +81,7 @@ export default function Page() { messageApi.success('强制同步已执行。'); await loadCompetitor(); } catch (actionError) { - messageApi.error(conflictMessage(actionError, '强制同步失败')); + messageApi.error(conflictMessage(actionError)); } finally { setActionBusy(false); }