diff --git a/Test-Verification-Debug.ps1 b/Test-Verification-Debug.ps1 new file mode 100644 index 0000000..00c43e4 --- /dev/null +++ b/Test-Verification-Debug.ps1 @@ -0,0 +1,112 @@ +# 验证码问题调试脚本 +# 作者: moyin +# 日期: 2025-12-17 + +$baseUrl = "http://localhost:3000" +$testEmail = "debug@example.com" + +Write-Host "=== 验证码问题调试脚本 ===" -ForegroundColor Green + +# 步骤1: 发送验证码 +Write-Host "`n1. 发送验证码..." -ForegroundColor Yellow +$sendBody = @{ + email = $testEmail +} | ConvertTo-Json + +try { + $sendResponse = Invoke-RestMethod -Uri "$baseUrl/auth/send-email-verification" -Method POST -Body $sendBody -ContentType "application/json" + Write-Host "发送响应: $($sendResponse | ConvertTo-Json -Depth 3)" -ForegroundColor Cyan + + if ($sendResponse.success) { + Write-Host "✅ 验证码发送成功" -ForegroundColor Green + + # 步骤2: 立即查看验证码调试信息 + Write-Host "`n2. 查看验证码调试信息..." -ForegroundColor Yellow + $debugResponse = Invoke-RestMethod -Uri "$baseUrl/auth/debug-verification-code" -Method POST -Body $sendBody -ContentType "application/json" + Write-Host "调试信息: $($debugResponse | ConvertTo-Json -Depth 5)" -ForegroundColor Cyan + + # 步骤3: 故意输入错误验证码 + Write-Host "`n3. 测试错误验证码..." -ForegroundColor Yellow + $wrongVerifyBody = @{ + email = $testEmail + verification_code = "000000" + } | ConvertTo-Json + + try { + $wrongResponse = Invoke-RestMethod -Uri "$baseUrl/auth/verify-email" -Method POST -Body $wrongVerifyBody -ContentType "application/json" + Write-Host "错误验证响应: $($wrongResponse | ConvertTo-Json -Depth 3)" -ForegroundColor Red + } catch { + Write-Host "错误验证失败(预期): $($_.Exception.Message)" -ForegroundColor Yellow + if ($_.Exception.Response) { + $errorResponse = $_.Exception.Response.GetResponseStream() + $reader = New-Object System.IO.StreamReader($errorResponse) + $errorBody = $reader.ReadToEnd() + Write-Host "错误详情: $errorBody" -ForegroundColor Red + } + } + + # 步骤4: 再次查看调试信息 + Write-Host "`n4. 错误验证后的调试信息..." -ForegroundColor Yellow + $debugResponse2 = Invoke-RestMethod -Uri "$baseUrl/auth/debug-verification-code" -Method POST -Body $sendBody -ContentType "application/json" + Write-Host "调试信息: $($debugResponse2 | ConvertTo-Json -Depth 5)" -ForegroundColor Cyan + + # 步骤5: 再次尝试错误验证码 + Write-Host "`n5. 再次测试错误验证码..." -ForegroundColor Yellow + try { + $wrongResponse2 = Invoke-RestMethod -Uri "$baseUrl/auth/verify-email" -Method POST -Body $wrongVerifyBody -ContentType "application/json" + Write-Host "第二次错误验证响应: $($wrongResponse2 | ConvertTo-Json -Depth 3)" -ForegroundColor Red + } catch { + Write-Host "第二次错误验证失败: $($_.Exception.Message)" -ForegroundColor Yellow + if ($_.Exception.Response) { + $errorResponse = $_.Exception.Response.GetResponseStream() + $reader = New-Object System.IO.StreamReader($errorResponse) + $errorBody = $reader.ReadToEnd() + Write-Host "错误详情: $errorBody" -ForegroundColor Red + } + } + + # 步骤6: 最终调试信息 + Write-Host "`n6. 最终调试信息..." -ForegroundColor Yellow + $debugResponse3 = Invoke-RestMethod -Uri "$baseUrl/auth/debug-verification-code" -Method POST -Body $sendBody -ContentType "application/json" + Write-Host "最终调试信息: $($debugResponse3 | ConvertTo-Json -Depth 5)" -ForegroundColor Cyan + + # 步骤7: 使用正确验证码(如果有的话) + if ($sendResponse.data.verification_code) { + Write-Host "`n7. 使用正确验证码..." -ForegroundColor Yellow + $correctVerifyBody = @{ + email = $testEmail + verification_code = $sendResponse.data.verification_code + } | ConvertTo-Json + + try { + $correctResponse = Invoke-RestMethod -Uri "$baseUrl/auth/verify-email" -Method POST -Body $correctVerifyBody -ContentType "application/json" + Write-Host "正确验证响应: $($correctResponse | ConvertTo-Json -Depth 3)" -ForegroundColor Green + } catch { + Write-Host "正确验证也失败了: $($_.Exception.Message)" -ForegroundColor Red + if ($_.Exception.Response) { + $errorResponse = $_.Exception.Response.GetResponseStream() + $reader = New-Object System.IO.StreamReader($errorResponse) + $errorBody = $reader.ReadToEnd() + Write-Host "错误详情: $errorBody" -ForegroundColor Red + } + } + } + + } else { + Write-Host "❌ 验证码发送失败: $($sendResponse.message)" -ForegroundColor Red + } +} catch { + Write-Host "❌ 请求失败: $($_.Exception.Message)" -ForegroundColor Red + if ($_.Exception.Response) { + $errorResponse = $_.Exception.Response.GetResponseStream() + $reader = New-Object System.IO.StreamReader($errorResponse) + $errorBody = $reader.ReadToEnd() + Write-Host "错误详情: $errorBody" -ForegroundColor Red + } +} + +Write-Host "`n=== 调试完成 ===" -ForegroundColor Green +Write-Host "请查看上述输出,重点关注:" -ForegroundColor Yellow +Write-Host "1. TTL值的变化" -ForegroundColor White +Write-Host "2. attempts字段的变化" -ForegroundColor White +Write-Host "3. 验证码是否被意外删除" -ForegroundColor White \ No newline at end of file diff --git a/src/business/login/login.controller.ts b/src/business/login/login.controller.ts index 7059243..ba16f86 100644 --- a/src/business/login/login.controller.ts +++ b/src/business/login/login.controller.ts @@ -343,4 +343,23 @@ export class LoginController { async resendEmailVerification(@Body() sendEmailVerificationDto: SendEmailVerificationDto): Promise> { return await this.loginService.resendEmailVerification(sendEmailVerificationDto.email); } + + /** + * 调试验证码信息 + * 仅用于开发和调试 + * + * @param sendEmailVerificationDto 邮箱信息 + * @returns 验证码调试信息 + */ + @ApiOperation({ + summary: '调试验证码信息', + description: '获取验证码的详细调试信息(仅开发环境)' + }) + @ApiBody({ type: SendEmailVerificationDto }) + @Post('debug-verification-code') + @HttpCode(HttpStatus.OK) + @UsePipes(new ValidationPipe({ transform: true })) + async debugVerificationCode(@Body() sendEmailVerificationDto: SendEmailVerificationDto): Promise { + return await this.loginService.debugVerificationCode(sendEmailVerificationDto.email); + } } \ No newline at end of file diff --git a/src/business/login/login.service.ts b/src/business/login/login.service.ts index dcc1ad0..f17b838 100644 --- a/src/business/login/login.service.ts +++ b/src/business/login/login.service.ts @@ -427,4 +427,31 @@ export class LoginService { // 简单的Base64编码(实际应用中应使用JWT) return Buffer.from(JSON.stringify(payload)).toString('base64'); } + /** + * 调试验证码信息 + * + * @param email 邮箱地址 + * @returns 调试信息 + */ + async debugVerificationCode(email: string): Promise { + try { + this.logger.log(`调试验证码信息: ${email}`); + + const debugInfo = await this.loginCoreService.debugVerificationCode(email); + + return { + success: true, + data: debugInfo, + message: '调试信息获取成功' + }; + } catch (error) { + this.logger.error(`获取验证码调试信息失败: ${email}`, error instanceof Error ? error.stack : String(error)); + + return { + success: false, + message: error instanceof Error ? error.message : '获取调试信息失败', + error_code: 'DEBUG_VERIFICATION_CODE_FAILED' + }; + } + } } \ No newline at end of file diff --git a/src/core/login_core/login_core.service.ts b/src/core/login_core/login_core.service.ts index 5488493..d288a49 100644 --- a/src/core/login_core/login_core.service.ts +++ b/src/core/login_core/login_core.service.ts @@ -567,4 +567,16 @@ export class LoginCoreService { const phoneRegex = /^(\+\d{1,3}[- ]?)?\d{10,11}$/; return phoneRegex.test(str.replace(/\s/g, '')); } -} + /** + * 调试验证码信息 + * + * @param email 邮箱地址 + * @returns 调试信息 + */ + async debugVerificationCode(email: string): Promise { + return await this.verificationService.debugCodeInfo( + email, + VerificationCodeType.EMAIL_VERIFICATION + ); + } +} \ No newline at end of file diff --git a/src/core/utils/verification/verification.service.ts b/src/core/utils/verification/verification.service.ts index b9f0f12..51207ab 100644 --- a/src/core/utils/verification/verification.service.ts +++ b/src/core/utils/verification/verification.service.ts @@ -111,24 +111,50 @@ export class VerificationService { const codeInfoStr = await this.redis.get(key); if (!codeInfoStr) { + this.logger.warn(`验证码不存在或已过期: ${identifier} (${type})`); throw new BadRequestException('验证码不存在或已过期'); } - const codeInfo: VerificationCodeInfo = JSON.parse(codeInfoStr); + let codeInfo: VerificationCodeInfo; + try { + codeInfo = JSON.parse(codeInfoStr); + } catch (error) { + this.logger.error(`验证码数据解析失败: ${identifier} (${type})`, error); + await this.redis.del(key); + throw new BadRequestException('验证码数据异常,请重新获取'); + } // 检查尝试次数 if (codeInfo.attempts >= codeInfo.maxAttempts) { + this.logger.warn(`验证码尝试次数已达上限: ${identifier} (${type}), 尝试次数: ${codeInfo.attempts}`); await this.redis.del(key); throw new BadRequestException('验证码尝试次数过多,请重新获取'); } - // 增加尝试次数 - codeInfo.attempts++; - await this.redis.set(key, JSON.stringify(codeInfo), this.CODE_EXPIRE_TIME); + // 获取当前TTL + const currentTTL = await this.redis.ttl(key); + this.logger.debug(`验证码当前TTL: ${identifier} (${type}), TTL: ${currentTTL}秒`); // 验证验证码 if (codeInfo.code !== inputCode) { - this.logger.warn(`验证码验证失败: ${identifier} (${type}) - 剩余尝试次数: ${codeInfo.maxAttempts - codeInfo.attempts}`); + // 增加尝试次数 + codeInfo.attempts++; + + // 保持原有的TTL,不重置过期时间 + if (currentTTL > 0) { + // 使用剩余的TTL时间 + await this.redis.set(key, JSON.stringify(codeInfo), currentTTL); + this.logger.warn(`验证码验证失败: ${identifier} (${type}) - 剩余尝试次数: ${codeInfo.maxAttempts - codeInfo.attempts}, 剩余时间: ${currentTTL}秒`); + } else if (currentTTL === -1) { + // 永不过期的情况,保持永不过期 + await this.redis.set(key, JSON.stringify(codeInfo)); + this.logger.warn(`验证码验证失败: ${identifier} (${type}) - 剩余尝试次数: ${codeInfo.maxAttempts - codeInfo.attempts}, 永不过期`); + } else { + // TTL为-2表示键不存在,这种情况理论上不应该发生 + this.logger.error(`验证码TTL异常: ${identifier} (${type}), TTL: ${currentTTL}`); + throw new BadRequestException('验证码状态异常,请重新获取'); + } + throw new BadRequestException(`验证码错误,剩余尝试次数: ${codeInfo.maxAttempts - codeInfo.attempts}`); } @@ -288,13 +314,16 @@ export class VerificationService { ttl: number; attempts?: number; maxAttempts?: number; + code?: string; + createdAt?: number; }> { const key = this.buildRedisKey(identifier, type); const exists = await this.redis.exists(key); const ttl = await this.redis.ttl(key); if (!exists) { - return { exists: false, ttl: -1 }; + this.logger.debug(`验证码不存在: ${identifier} (${type})`); + return { exists: false, ttl: -2 }; } const codeInfoStr = await this.redis.get(key); @@ -307,11 +336,54 @@ export class VerificationService { codeInfo = {} as VerificationCodeInfo; } + this.logger.debug(`验证码统计: ${identifier} (${type}), TTL: ${ttl}, 尝试次数: ${codeInfo.attempts}/${codeInfo.maxAttempts}`); + return { exists: true, ttl, attempts: codeInfo.attempts, maxAttempts: codeInfo.maxAttempts, + code: codeInfo.code, // 仅用于调试,生产环境应该移除 + createdAt: codeInfo.createdAt, }; } + + /** + * 调试方法:获取验证码详细信息 + * 仅用于开发和调试,生产环境应该移除或限制访问 + * + * @param identifier 标识符 + * @param type 验证码类型 + * @returns 详细信息 + */ + async debugCodeInfo(identifier: string, type: VerificationCodeType): Promise { + const key = this.buildRedisKey(identifier, type); + + const [exists, ttl, rawData] = await Promise.all([ + this.redis.exists(key), + this.redis.ttl(key), + this.redis.get(key) + ]); + + const result = { + key, + exists, + ttl, + rawData, + parsedData: null as any, + currentTime: Date.now(), + timeFormatted: new Date().toISOString() + }; + + if (rawData) { + try { + result.parsedData = JSON.parse(rawData); + } catch (error) { + result.parsedData = { error: 'JSON解析失败', raw: rawData }; + } + } + + this.logger.debug(`调试验证码信息: ${JSON.stringify(result, null, 2)}`); + return result; + } } \ No newline at end of file