} */ public function login(string $siteCode, string $username, string $password): array { $username = trim($username); $siteCode = trim($siteCode); if ($username === '' || $password === '') { throw new PlayerAuthenticationException( '账号或密码错误', ErrorCode::PlayerCredentialsInvalid->value, ); } if ($siteCode === '') { $siteCode = trim((string) config('lottery.integration.default_site_code', '')); } $player = Player::query() ->where('site_code', $siteCode) ->where('username', $username) ->where('auth_source', PlayerAuthSource::LOTTERY_NATIVE) ->first(); if ($player === null || ! is_string($player->password_hash) || $player->password_hash === '') { throw new PlayerAuthenticationException( '账号或密码错误', ErrorCode::PlayerCredentialsInvalid->value, ); } if ($player->login_locked_until !== null && $player->login_locked_until->isFuture()) { throw new PlayerAuthenticationException( '登录已锁定', ErrorCode::PlayerLoginLocked->value, 403, ); } if ((int) $player->status !== 0) { throw new PlayerAuthenticationException( '账号已冻结', ErrorCode::PlayerAccountSuspended->value, 403, ); } if (! Hash::check($password, $player->password_hash)) { $this->recordFailedLogin($player); throw new PlayerAuthenticationException( '账号或密码错误', ErrorCode::PlayerCredentialsInvalid->value, ); } $player->forceFill([ 'login_failed_count' => 0, 'login_locked_until' => null, 'last_login_at' => now(), ])->save(); $ttl = (int) config('lottery.player_auth.native.ttl_seconds', 28800); $token = $this->issueToken($player, $ttl); return [ 'access_token' => $token, 'expires_in' => $ttl, 'token_type' => 'Bearer', 'player' => [ 'id' => (int) $player->id, 'site_code' => $player->site_code, 'username' => $player->username, 'nickname' => $player->nickname, 'funding_mode' => $player->funding_mode, 'auth_source' => $player->auth_source, ], ]; } public function issueToken(Player $player, ?int $ttlSeconds = null): string { $secret = (string) config('lottery.player_auth.native.secret', ''); if ($secret === '') { throw new PlayerAuthenticationException( '原生登录未配置', ErrorCode::PlayerSsoSecretNotConfigured->value, 503, ); } $ttl = $ttlSeconds ?? (int) config('lottery.player_auth.native.ttl_seconds', 28800); $now = time(); $playerIdKey = (string) config('lottery.player_auth.native.claim_player_id', 'player_id'); $authKey = (string) config('lottery.player_auth.native.claim_auth_source', 'auth_source'); $payload = [ $playerIdKey => (int) $player->id, $authKey => PlayerAuthSource::LOTTERY_NATIVE, 'site_code' => (string) $player->site_code, 'iat' => $now, 'exp' => $now + $ttl, ]; return JWT::encode($payload, $secret, (string) config('lottery.player_auth.jwt.algorithm', 'HS256')); } private function recordFailedLogin(Player $player): void { $max = (int) config('lottery.player_auth.native.max_login_attempts', 8); $lockMinutes = (int) config('lottery.player_auth.native.lock_minutes', 15); $count = (int) $player->login_failed_count + 1; $updates = ['login_failed_count' => $count]; if ($count >= $max) { $updates['login_locked_until'] = now()->addMinutes($lockMinutes); $updates['login_failed_count'] = 0; } $player->forceFill($updates)->save(); } }