fix(creator): 强制同步抢锁失败分类提示——租约被占/竞品停用分别返回可读 409
douyin-release-gate / verify (push) Failing after 4m57s
douyin-release-gate / verify (push) Failing after 4m57s
- ClaimCompetitorSync 失败后经 SyncClaimFailure 复查竞品状态, 区分 ErrSyncInProgress(该竞品正在同步中)与 ErrSyncDisabled(已停用) - creatorError 新增两分类映射 409;通用冲突文案保持不变 - 前端竞品强制同步去掉固定'强制同步失败'后缀,直接展示分类提示 - 回归:TestCompetitorSyncClaimFailureClassification(PG 集成)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function MonitoringAccountList() {
|
||||
messageApi.success('强制同步已执行。');
|
||||
await load();
|
||||
} catch (actionError) {
|
||||
messageApi.error(conflictMessage(actionError, '强制同步失败'));
|
||||
messageApi.error(conflictMessage(actionError));
|
||||
} finally {
|
||||
setActionBusy('');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user