{"record":{"id":"8551ccbd4dc1c85c","repo":"yikart/AiToEarn","slug":"error-8551cc","errorCode":null,"errorMessage":"刷新令牌后未能获取访问令牌","messagePattern":"刷新令牌后未能获取访问令牌","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"project/aitoearn-electron/server/src/modules/plat/tiktok/tiktok.auth.service.ts","lineNumber":520,"sourceCode":"      accountId: accountId,\n      platform: TokenPlatform.TIKTOK\n    });\n\n    if (!accountTokenInfo || !accountTokenInfo.refreshToken) {\n      throw new BadRequestException('无效的账号或刷新令牌丢失');\n    }\n\n    // 刷新并获取新令牌\n    const refreshResult = await this.refreshAccessToken(\n      accountTokenInfo.userId,\n      accountTokenInfo.accountId,\n      accountTokenInfo.refreshToken\n    );\n\n    // 刷新后再次从Redis获取\n    const newToken = await this.redisService.get(`tiktok:accessToken:${accountId}`);\n    if (!newToken || !newToken.access_token) {\n      throw new BadRequestException('刷新令牌后未能获取访问令牌');\n    }\n\n    return newToken.access_token;\n  }\n\n  /**\n   * 检查用户是否已授权TikTok\n   * @param accountId 账号ID\n   * @returns 是否已授权\n   */\n  async isAuthorized(accountId: string): Promise<boolean> {\n    try {\n      const accessToken = await this.getUserAccessToken(accountId);\n      return !!accessToken;\n    } catch (error) {\n      return false;\n    }\n  }","sourceCodeStart":502,"sourceCodeEnd":538,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-electron/server/src/modules/plat/tiktok/tiktok.auth.service.ts#L502-L538","documentation":"A BadRequestException thrown by getUserAccessToken after refreshAccessToken succeeded but the re-read of tiktok:accessToken:{accountId} from Redis returned nothing or a record without access_token. Indicates the refresh flow did not persist the new access token to Redis as expected.","triggerScenarios":"refreshAccessToken completed but its Redis setKey call failed or was skipped, a Redis eviction/TTL removed the key between refresh and read, or a race where another flow overwrote/deleted the key, leaving no access_token field.","commonSituations":"Redis TTL set shorter than the access token lifetime, shared Redis instance with an eviction policy (allkeys-lru) under memory pressure, multi-instance deployment where refresh wrote to a different Redis DB than the read, key name typo/namespace drift.","solutions":["Check refreshAccessToken: ensure the setKey(`tiktok:accessToken:${accountId}`) call actually runs and stores the response's access_token.","Confirm both refresh and read hit the same Redis instance/DB and the key TTL exceeds TikTok's access token lifetime (~24h).","Instead of re-reading Redis, have refreshAccessToken return the new access_token directly and use it.","Inspect Redis memory/eviction config if keys disappear under load."],"exampleFix":"// before\nawait this.refreshAccessToken(accountTokenInfo.userId, accountTokenInfo.accountId, accountTokenInfo.refreshToken);\nconst newToken = await this.redisService.get(`tiktok:accessToken:${accountId}`);\n// after\nconst refreshed = await this.refreshAccessToken(accountTokenInfo.userId, accountTokenInfo.accountId, accountTokenInfo.refreshToken);\nif (!refreshed?.access_token) {\n  throw new BadRequestException('刷新令牌后未能获取访问令牌');\n}\nreturn refreshed.access_token;","handlingStrategy":"fallback","validationCode":"// after refresh, verify the cache entry before trusting it\nconst cached = await redis.get(`tiktok:accessToken:${accountId}`);\nif (!cached?.access_token) {\n  throw new Error('refresh did not persist access token to redis');\n}","typeGuard":"function isStoredAccessToken(v: unknown): v is { access_token: string; expires_in: number } {\n  return typeof v === 'object' && v !== null && typeof (v as any).access_token === 'string';\n}","tryCatchPattern":"try {\n  return await api.getTikTokAccessToken(accountId);\n} catch (e) {\n  if (/刷新令牌后未能获取/.test((e as Error).message)) {\n    // fallback: force a direct refresh and read the returned token\n    const fresh = await api.forceRefreshTikTokToken(accountId);\n    return fresh.access_token;\n  }\n  throw e;\n}","preventionTips":["Have refreshAccessToken return the token directly instead of round-tripping through Redis.","Set Redis TTL to match TikTok's access token lifetime (~24h).","Use one Redis instance/DB consistently for token storage.","Avoid allkeys-lru eviction for token keys; use volatile-ttl or dedicated storage."],"tags":["tiktok","redis","cache-miss","access-token"],"backgroundTag":"redis-cache-miss","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}