feat(core): harden sessions settlement and credit activity
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
lotterLaravel E2E / e2e-api (push) Has been cancelled

This commit is contained in:
wchino
2026-07-22 20:53:43 +08:00
parent 35f6e46958
commit 15bd997c4e
48 changed files with 1920 additions and 93 deletions

View File

@@ -6,7 +6,9 @@ use Firebase\JWT\JWT;
use App\Models\Player;
use App\Lottery\ErrorCode;
use App\Support\PlayerAuthSource;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use App\Events\PlayerSessionReplacedBroadcast;
use App\Exceptions\PlayerAuthenticationException;
final class PlayerNativeAuthService
@@ -63,17 +65,62 @@ final class PlayerNativeAuthService
);
}
$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);
/** @var array{token: string, player: Player, session_version: int} $login */
$login = DB::transaction(function () use ($player, $password, $ttl): array {
$locked = Player::query()->lockForUpdate()->find($player->id);
if ($locked === null
|| ! $locked->isLotteryNative()
|| ! is_string($locked->password_hash)
|| ! Hash::check($password, $locked->password_hash)) {
throw new PlayerAuthenticationException(
'账号或密码错误',
ErrorCode::PlayerCredentialsInvalid->value,
);
}
if ($locked->login_locked_until !== null && $locked->login_locked_until->isFuture()) {
throw new PlayerAuthenticationException(
'登录已锁定',
ErrorCode::PlayerLoginLocked->value,
403,
);
}
if ((int) $locked->status !== 0) {
throw new PlayerAuthenticationException(
'账号已冻结',
ErrorCode::PlayerAccountSuspended->value,
403,
);
}
$sessionVersion = (int) ($locked->native_session_version ?? 0) + 1;
$locked->forceFill([
'login_failed_count' => 0,
'login_locked_until' => null,
'last_login_at' => now(),
'native_session_version' => $sessionVersion,
])->save();
return [
'token' => $this->issueToken($locked, $ttl),
'player' => $locked->refresh(),
'session_version' => $sessionVersion,
];
});
event(new PlayerSessionReplacedBroadcast(
(int) $login['player']->id,
$login['session_version'],
(int) floor(microtime(true) * 1000),
));
$player = $login['player'];
return [
'access_token' => $token,
'access_token' => $login['token'],
'expires_in' => $ttl,
'token_type' => 'Bearer',
'player' => [
@@ -100,6 +147,7 @@ final class PlayerNativeAuthService
$playerIdKey => (int) $player->id,
$authKey => PlayerAuthSource::LOTTERY_NATIVE,
'token_version' => (int) ($player->native_token_version ?? 0),
'session_version' => (int) ($player->native_session_version ?? 0),
'site_code' => (string) $player->site_code,
'iat' => $now,
'exp' => $now + $ttl,