{"record":{"id":"8be7bd19113ed848","repo":"yikart/AiToEarn","slug":"errhttpback-err-user-code-had","errorCode":"ErrHttpBack.err_user_code_had","errorMessage":"err_user_code_had","messagePattern":"err_user_code_had","errorType":"error_code","errorClass":"AppHttpException","httpStatus":400,"severity":"warning","filePath":"project/aitoearn-electron/server/src/user/login.service.ts","lineNumber":34,"sourceCode":"  Password = 'phone_register_code',\n  Code = 'phone_login_code',\n  PhoneAuth = 'phone_login_auth_token', // 手机号一键登录\n}\n@Injectable()\nexport class LoginService {\n  constructor(\n    private readonly redisService: RedisService,\n    private readonly alicloudSmsService: AlicloudSmsService,\n  ) {}\n\n  /**\n   * 发送手机号注册的验证码\n   * @param phone\n   */\n  async postPhoneRegisterCode(phone: string) {\n    const cacheKey = `${LoginTypeCacheKey.Password}:${phone}`;\n    let code = await this.redisService.get(cacheKey);\n    if (code) throw new AppHttpException(ErrHttpBack.err_user_code_had);\n\n    code = getRandomString(6, true);\n    const res = await this.alicloudSmsService.sendLoginSms(phone, code);\n\n    if (process.env.NODE_ENV === 'production') {\n      if (!res) throw new AppHttpException(ErrHttpBack.err_user_code_send_fail);\n    }\n\n    this.redisService.setKey(cacheKey, code, 60 * 5);\n    console.log('发送短信成功', code);\n\n    return process.env.NODE_ENV === 'production' ? res : code;\n  }\n\n  /**\n   * 发送手机号登录的验证码\n   * @param phone\n   */","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-electron/server/src/user/login.service.ts#L16-L52","documentation":"AppHttpException with ErrHttpBack.err_user_code_had (errCode 40019, message '验证码未过期'). Thrown by LoginService.postPhoneRegisterCode (login.service.ts:34) when a verification code for this phone is still cached in Redis under key `Password:<phone>`, i.e. the previous code has not expired (5-minute TTL). Rate-limits SMS code requests.","triggerScenarios":"POST phone register code for a phone number whose Redis key `${LoginTypeCacheKey.Password}:${phone}` still holds an unexpired code — requesting a second code within the 5-minute window.","commonSituations":"User clicks 'send code' twice; automated test reuses the same phone number without clearing Redis; Redis TTL not respected by a client that retries on UI error; shared test phone across environments with a common Redis.","solutions":["Wait until the existing code expires (60*5s TTL from issue time) before requesting a new one","Use the still-valid cached code instead of requesting a new one","In tests, delete the Redis key `Password:<phone>` (or flush the test Redis) between runs","Disable the resend button client-side for the 5-minute cooldown window"],"exampleFix":"// before\nawait api.postPhoneRegisterCode(phone) // throws 40019 if code cached\n// after\ntry {\n  await api.postPhoneRegisterCode(phone)\n} catch (e) {\n  if (e.errCode === '40019') { /* reuse existing code or wait for TTL */ }\n}","handlingStrategy":"retry","validationCode":"const cooldownKey = `sms-cooldown:${phone}`\nif (await cache.get(cooldownKey)) throw new Error('Please wait for the previous code to expire')\nawait api.postPhoneRegisterCode(phone)\nawait cache.set(cooldownKey, '1', 300)","typeGuard":"null","tryCatchPattern":"try {\n  await api.postPhoneRegisterCode(phone)\n} catch (e) {\n  if (e?.errCode === '40019') {\n    // code still valid: prompt user to check SMS or wait out the 5-minute TTL\n  } else throw e\n}","preventionTips":["Enforce a client-side 5-minute resend cooldown matching the server TTL","Clear the Redis key `Password:<phone>` between test runs","Reuse the still-valid code instead of requesting a new one"],"tags":["rate-limit","sms","redis","verification-code"],"backgroundTag":"verification-code-already-sent","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}