{"record":{"id":"d5f2f43167045377","repo":"yikart/AiToEarn","slug":"error-response-data-error-error-mes-d5f2f4","errorCode":null,"errorMessage":"点赞推文失败: ${error.response?.data?.error || error.message}","messagePattern":"点赞推文失败: (.+?)","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"project/aitoearn-electron/server/src/modules/plat/twitter/twitter.service.ts","lineNumber":341,"sourceCode":"      const url = `${TWITTER_API_V2_URL}/users/${accountId}/likes`;\n      const data = {\n        tweet_id: tweetId\n      };\n\n      const response = await lastValueFrom(\n        this.httpService.post(url, data, {\n          headers: {\n            'Authorization': `Bearer ${accessToken}`,\n            'Content-Type': 'application/json'\n          }\n        })\n      );\n\n      this.logger.log(`成功点赞推文: userId=${userId}, accountId=${accountId}, tweetId=${tweetId}`);\n      return response.data;\n    } catch (error) {\n      this.logger.error(`点赞推文失败: ${error.message}`, error.stack);\n      throw new BadRequestException(`点赞推文失败: ${error.response?.data?.error || error.message}`);\n    }\n  }\n\n  /**\n   * 取消对推文的点赞\n   * @param accessToken 访问令牌\n   * @param userId 用户ID\n   * @param accountId Twitter账号ID\n   * @param tweetId 推文ID\n   * @returns 取消点赞结果\n   */\n  async unlikeTweet(accessToken: string, userId: string, accountId: string, tweetId: string) {\n    try {\n      // Twitter API V2 取消点赞端点\n      const url = `${TWITTER_API_V2_URL}/users/${accountId}/likes/${tweetId}`;\n\n      const response = await lastValueFrom(\n        this.httpService.delete(url, {","sourceCodeStart":323,"sourceCodeEnd":359,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-electron/server/src/modules/plat/twitter/twitter.service.ts#L323-L359","documentation":"likeTweet calls POST /2/users/:userId/likes with {tweet_id} in the body and a Bearer token. Twitter API v2 errors from Axios are wrapped in BadRequestException, preferring error.response.data.error from Twitter's payload.","triggerScenarios":"Tweet already liked (409 Conflict / 'user has already liked tweet'), tweet deleted (404), expired token (401), or the authenticated userId not matching the token's user (403).","commonSituations":"Re-running like jobs on already-liked tweets; double-click causing duplicate requests; tokens refreshed without the tweet.write scope; liking tweets of suspended accounts.","solutions":["Treat 'already liked' responses as success and continue (idempotent handling).","Refresh the user token on 401 and ensure tweet.write scope was granted.","Verify the userId path param equals the token owner's user id.","Check tweet existence before liking to skip 404s.","Inspect error.response.data.errors for Twitter's exact title/detail and implement dedupe keys in the caller."],"exampleFix":"// before\nawait twitterService.likeTweet(accessToken, userId, userId, tweetId)\n// after\ntry {\n  await twitterService.likeTweet(accessToken, userId, userId, String(tweetId))\n} catch (e) {\n  if (/already liked/i.test(e.message)) return // idempotent\n  throw e\n}","handlingStrategy":"try-catch","validationCode":"if (!accessToken || !userId || !/^[0-9]+$/.test(String(tweetId))) throw new Error('invalid likeTweet arguments')","typeGuard":"const isAlreadyLiked = (msg: string): boolean => /already liked/i.test(msg)","tryCatchPattern":"try {\n  await twitterService.likeTweet(accessToken, userId, userId, String(tweetId))\n} catch (e) {\n  const msg = e?.response?.data?.error || e.message\n  if (isAlreadyLiked(msg)) return // idempotent success\n  if (/401|unauthorized/i.test(msg)) return retryWithFreshToken()\n  throw e\n}","preventionTips":["Debounce double-clicks on the like button in the UI","Track liked state locally to skip redundant API calls","Ensure tweet.write scope is requested during authorization","Use the token owner's user id in the URL path"],"tags":["twitter-api","http-4xx","oauth","nestjs"],"backgroundTag":"twitter-api-request-failed","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}