package creator import ( "context" "database/sql" "errors" "time" ) // 同步互斥租约:与作品/评论采集共用 creator_collection_checkpoint(统一租约模式), // collection_kind='sync',窗口列填 now() 占位(同步租约不跟踪采集窗口)。 func (s *Store) ClaimSourceSync(ctx context.Context, sourceType, sourceID string) (string, error) { if sourceType != SourceOwned && sourceType != SourceCompetitor || sourceID == "" { return "", ErrInvalid } token := newID("lease") now := time.Now().UTC() var claimed string err := s.db.QueryRowContext(ctx, ` INSERT INTO creator_collection_checkpoint (source_type, source_id, collection_kind, cursor, window_start, window_end, status, lease_token, lease_until, last_error) VALUES ($1, $2, 'sync', '', $3, $3, 'running', $4, $3::timestamptz + interval '10 minutes', '') ON CONFLICT (source_type, source_id, collection_kind) DO UPDATE SET status = 'running', lease_token = EXCLUDED.lease_token, lease_until = EXCLUDED.lease_until WHERE creator_collection_checkpoint.lease_until IS NULL OR creator_collection_checkpoint.lease_until <= now() RETURNING lease_token`, sourceType, sourceID, now, token).Scan(&claimed) if errors.Is(err, sql.ErrNoRows) { return "", ErrConflict } if err != nil { return "", databaseError(err) } return claimed, nil } func (s *Store) ReleaseSourceSync(ctx context.Context, sourceType, sourceID, token string) error { if sourceType != SourceOwned && sourceType != SourceCompetitor || sourceID == "" || token == "" { return ErrInvalid } result, err := s.db.ExecContext(ctx, ` DELETE FROM creator_collection_checkpoint WHERE source_type = $1 AND source_id = $2 AND collection_kind = 'sync' AND lease_token = $3`, sourceType, sourceID, token) if err != nil { return databaseError(err) } if rows, err := result.RowsAffected(); err != nil { return err } else if rows != 1 { return ErrConflict } return nil }