validate([ 'mobile' => ['required', 'regex:/^1[3-9]\d{9}$/'], ]); $mobile = $data['mobile']; $key = $this->cacheKey($mobile); $check = Cache::get($key); Log::info('sms.send hit', [ 'mobile_mask' => $this->maskMobile($mobile), 'ip' => $request->ip(), 'user_agent' => substr((string) $request->userAgent(), 0, 200), ]); $resendSec = (int) config('sms.resend_interval_seconds', 60); if (is_array($check) && isset($check['time']) && (time() - (int) $check['time']) <= $resendSec) { Log::warning('sms.send rate_limited', [ 'mobile_mask' => $this->maskMobile($mobile), 'last_send_at' => $check['time'] ?? null, 'resend_interval_seconds' => $resendSec, ]); throw ValidationException::withMessages([ 'mobile' => ['请勿频繁发送'], ]); } $code = (string) random_int(100000, 999999); $smsSign = (string) config('sms.sign'); $content = "{$smsSign}您的验证码是:{$code},验证码五分钟内有效,如非本人操作,请忽略。"; $useMock = config('sms.mock') || config('sms.skip_gateway') || (config('app.debug') && empty(config('sms.app_id')) && empty(config('sms.secret'))); Log::info('sms.send branch', [ 'mobile_mask' => $this->maskMobile($mobile), 'use_mock' => $useMock, 'sms_mock_config' => (bool) config('sms.mock'), 'sms_skip_gateway' => (bool) config('sms.skip_gateway'), 'app_debug' => (bool) config('app.debug'), 'has_sms_credentials' => config('sms.app_id') !== '' && config('sms.secret') !== '', ]); if ($useMock) { Cache::put($key, ['code' => $code, 'time' => time()], 300); Log::info('sms.send mock_ok cache_set', [ 'mobile_mask' => $this->maskMobile($mobile), 'code' => $code, 'ttl_sec' => 300, ]); return response()->json([ 'message' => '发送成功', 'debug_code' => $code, ]); } Log::info('sms.send gateway_call', ['mobile_mask' => $this->maskMobile($mobile)]); $result = $sms->sendContent($mobile, $content); if (! $result) { Log::warning('sms.send gateway_failed', ['mobile_mask' => $this->maskMobile($mobile)]); return response()->json([ 'message' => '发送失败', ], 422); } Cache::put($key, ['code' => $code, 'time' => time()], 300); Log::info('sms.send gateway_ok cache_set', [ 'mobile_mask' => $this->maskMobile($mobile), 'debug_code_logged' => config('app.debug') ? $code : null, ]); return response()->json([ 'message' => '发送成功', 'debug_code' => config('app.debug') ? $code : null, ]); } public function login(Request $request): JsonResponse { $data = $request->validate([ 'mobile' => ['required', 'regex:/^1[3-9]\d{9}$/'], 'code' => ['required', 'string', 'max:64'], 'competition_slug' => ['required', 'string', 'max:64'], ]); $key = $this->cacheKey($data['mobile']); $cached = Cache::get($key); if (! is_array($cached) || ($cached['code'] ?? null) !== $data['code']) { throw ValidationException::withMessages([ 'code' => ['验证码无效或已过期'], ]); } $user = DB::transaction(function () use ($data, $key) { Cache::forget($key); $competition = Competition::query() ->where('slug', $data['competition_slug']) ->where('published', true) ->first(); if ($competition === null) { throw ValidationException::withMessages([ 'competition_slug' => ['赛事不存在或未发布'], ]); } $user = User::query()->firstOrCreate( ['mobile' => $data['mobile']], ['name' => null, 'email' => null, 'password' => null] ); Application::query()->firstOrCreate( [ 'user_id' => $user->id, 'competition_id' => $competition->id, ], ['status' => 'draft'] ); return $user; }); $user->tokens()->delete(); $token = $user->createToken('web')->plainTextToken; return response()->json([ 'token' => $token, 'token_type' => 'Bearer', 'user' => [ 'id' => $user->id, 'mobile' => $user->mobile, 'name' => $user->name, 'email' => $user->email, ], ]); } }