where('player_id', $player->id) ->exists(); if ($exists) { DB::table('player_credit_accounts') ->where('player_id', $player->id) ->update([ 'credit_limit' => $limit, 'updated_at' => $now, ]); return; } DB::table('player_credit_accounts')->insert([ 'player_id' => $player->id, 'credit_limit' => $limit, 'used_credit' => 0, 'frozen_credit' => 0, 'created_at' => $now, 'updated_at' => $now, ]); } /** 可用授信(主货币整数,与后台「授信额度」一致)。 */ public function availableCredit(Player $player): int { $row = DB::table('player_credit_accounts')->where('player_id', $player->id)->first(); if ($row === null) { return 0; } return max(0, (int) $row->credit_limit - (int) $row->used_credit - (int) $row->frozen_credit); } /** 可用授信(最小货币单位,供玩家端钱包/下注与钱包余额 API 对齐)。 */ public function availableCreditMinor(Player $player, ?string $currencyCode = null): int { $currency = $currencyCode ?? (string) $player->default_currency; return CreditAmountScale::majorToMinor($this->availableCredit($player), $currency); } public function holdForBet(Player $player, int $amountMinor): void { if ($amountMinor <= 0) { return; } if (! PlayerFundingMode::usesCredit($player)) { return; } $currency = (string) $player->default_currency; $availableMinor = $this->availableCreditMinor($player, $currency); if ($amountMinor > $availableMinor) { throw ValidationException::withMessages([ 'credit' => ['insufficient'], ]); } $majorDelta = CreditAmountScale::minorToMajor($amountMinor, $currency); DB::table('player_credit_accounts') ->where('player_id', $player->id) ->update([ 'used_credit' => DB::raw('used_credit + '.$majorDelta), 'updated_at' => now(), ]); DB::table('credit_ledger')->insert([ 'owner_type' => 'player', 'owner_id' => $player->id, 'amount' => -$amountMinor, 'reason' => 'bet_hold', 'ref_type' => 'bet', 'ref_id' => null, 'created_at' => now(), 'updated_at' => now(), ]); } public function applySettledLoss(Player $player, int $amountMinor, int $ticketItemId): void { if ($amountMinor <= 0) { return; } if (! PlayerFundingMode::usesCredit($player)) { return; } $currency = (string) $player->default_currency; $majorDelta = CreditAmountScale::minorToMajor($amountMinor, $currency); DB::table('player_credit_accounts') ->where('player_id', $player->id) ->update([ 'used_credit' => DB::raw('used_credit + '.$majorDelta), 'updated_at' => now(), ]); DB::table('credit_ledger')->insert([ 'owner_type' => 'player', 'owner_id' => $player->id, 'amount' => -$amountMinor, 'reason' => 'game_settlement_loss', 'ref_type' => 'ticket_item', 'ref_id' => $ticketItemId, 'created_at' => now(), 'updated_at' => now(), ]); } public function assertMayPlaceBet(Player $player, int $amountMinor): void { if (! PlayerFundingMode::usesCredit($player)) { return; } $overdue = DB::table('settlement_bills') ->where('owner_type', 'player') ->where('owner_id', $player->id) ->where('status', 'overdue') ->where('unpaid_amount', '>', 0) ->exists(); if ($overdue) { throw ValidationException::withMessages([ 'credit' => ['overdue'], ]); } $agentNodeId = (int) ($player->agent_node_id ?? 0); if ($agentNodeId > 0) { AgentOverdueGuard::assertAgentMayGrantCredit($agentNodeId); } $this->holdForBet($player, $amountMinor); } public function releaseBetHold(Player $player, int $amountMinor, int $ticketItemId): void { if ($amountMinor <= 0 || ! PlayerFundingMode::usesCredit($player)) { return; } $this->decreaseUsedCredit($player, $amountMinor); DB::table('credit_ledger')->insert([ 'owner_type' => 'player', 'owner_id' => $player->id, 'amount' => $amountMinor, 'reason' => 'bet_hold_release', 'ref_type' => 'ticket_item', 'ref_id' => $ticketItemId, 'created_at' => now(), 'updated_at' => now(), ]); } public function releaseFromSettlement(Player $player, int $amountMinor, int $billId): void { if ($amountMinor <= 0) { return; } $this->decreaseUsedCredit($player, $amountMinor); DB::table('credit_ledger')->insert([ 'owner_type' => 'player', 'owner_id' => $player->id, 'amount' => $amountMinor, 'reason' => 'settlement_confirm', 'ref_type' => 'settlement_bill', 'ref_id' => $billId, 'created_at' => now(), 'updated_at' => now(), ]); } private function decreaseUsedCredit(Player $player, int $amountMinor): void { if ($amountMinor <= 0) { return; } $playerId = (int) $player->id; $row = DB::table('player_credit_accounts')->where('player_id', $playerId)->first(); if ($row === null) { return; } $majorDelta = CreditAmountScale::minorToMajor($amountMinor, (string) $player->default_currency); $next = max(0, (int) $row->used_credit - $majorDelta); DB::table('player_credit_accounts') ->where('player_id', $playerId) ->update([ 'used_credit' => $next, 'updated_at' => now(), ]); } }