validate([ 'mobile' => ['required', 'regex:/^1[3-9]\d{9}$/'], ]); $mobile = $data['mobile']; Log::info('sms.send hit', [ 'mobile_mask' => $this->maskMobile($mobile), 'ip' => $request->ip(), 'user_agent' => substr((string) $request->userAgent(), 0, 200), ]); $result = $service->sendLoginCode($mobile, $sender); return response()->json($result->toHttpPayload(), $result->success ? 200 : 503); } 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'], ]); $verification = SmsVerification::query() ->where('scene', SmsVerification::SCENE_PARTICIPANT_LOGIN) ->where('mobile', $data['mobile']) ->where('status', SmsVerification::STATUS_SENT) ->where('expires_at', '>', now()) ->latest('id') ->first(); if ($verification === null || $verification->code !== $data['code']) { throw ValidationException::withMessages([ 'code' => ['验证码无效或已过期'], ]); } $user = DB::transaction(function () use ($data, $verification) { $verification = SmsVerification::query() ->whereKey($verification->id) ->where('scene', SmsVerification::SCENE_PARTICIPANT_LOGIN) ->where('mobile', $data['mobile']) ->where('status', SmsVerification::STATUS_SENT) ->where('expires_at', '>', now()) ->lockForUpdate() ->first(); if ($verification === null || $verification->code !== $data['code']) { throw ValidationException::withMessages([ 'code' => ['验证码无效或已过期'], ]); } $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'] ); $verification->forceFill([ 'status' => SmsVerification::STATUS_USED, 'used_at' => now(), ])->save(); $this->expirePreviousVerifications($data['mobile'], $verification->id); 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, ], ]); } private function expirePreviousVerifications(string $mobile, int $currentId): void { SmsVerification::query() ->where('scene', SmsVerification::SCENE_PARTICIPANT_LOGIN) ->where('mobile', $mobile) ->where('id', '<>', $currentId) ->whereIn('status', [ SmsVerification::STATUS_PENDING, SmsVerification::STATUS_SENT, ]) ->update(['status' => SmsVerification::STATUS_EXPIRED]); } }