132 lines
4.2 KiB
PHP
132 lines
4.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Ticket;
|
|
|
|
use App\Models\Player;
|
|
use App\Models\WalletTxn;
|
|
use App\Lottery\ErrorCode;
|
|
use App\Models\TicketOrder;
|
|
use App\Models\PlayerWallet;
|
|
use App\Exceptions\TicketOperationException;
|
|
|
|
final class TicketWalletService
|
|
{
|
|
private const TXN_POSTED = 'posted';
|
|
|
|
private const TXN_DIR_OUT = 2;
|
|
|
|
private const TXN_DIR_IN = 1;
|
|
|
|
public function deduct(Player $player, string $currencyCode, int $amountMinor, TicketOrder $order): int
|
|
{
|
|
$wallet = PlayerWallet::query()
|
|
->where('player_id', $player->id)
|
|
->where('wallet_type', 'lottery')
|
|
->where('currency_code', strtoupper($currencyCode))
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
if ($wallet === null) {
|
|
$wallet = PlayerWallet::query()->create([
|
|
'player_id' => $player->id,
|
|
'wallet_type' => 'lottery',
|
|
'currency_code' => strtoupper($currencyCode),
|
|
'balance' => 0,
|
|
'frozen_balance' => 0,
|
|
'status' => 0,
|
|
'version' => 0,
|
|
]);
|
|
$wallet = PlayerWallet::query()->whereKey($wallet->id)->lockForUpdate()->firstOrFail();
|
|
}
|
|
|
|
$before = (int) $wallet->balance;
|
|
if ($before < $amountMinor) {
|
|
throw new TicketOperationException('bet_insufficient_balance', ErrorCode::BetInsufficientBalance->value);
|
|
}
|
|
|
|
$after = $before - $amountMinor;
|
|
$wallet->forceFill([
|
|
'balance' => $after,
|
|
'version' => (int) $wallet->version + 1,
|
|
])->save();
|
|
|
|
WalletTxn::query()->create([
|
|
'txn_no' => $this->newTxnNo(),
|
|
'player_id' => $player->id,
|
|
'wallet_id' => $wallet->id,
|
|
'biz_type' => 'bet_deduct',
|
|
'biz_no' => $order->order_no,
|
|
'direction' => self::TXN_DIR_OUT,
|
|
'amount' => $amountMinor,
|
|
'balance_before' => $before,
|
|
'balance_after' => $after,
|
|
'status' => self::TXN_POSTED,
|
|
'external_ref_no' => null,
|
|
'idempotent_key' => $order->client_trace_id,
|
|
'remark' => null,
|
|
]);
|
|
|
|
return $after;
|
|
}
|
|
|
|
/**
|
|
* 结算派彩入账(产品文档:派彩写入钱包流水;幂等键按结算批次 + 玩家)。
|
|
*/
|
|
public function creditSettlementPayout(Player $player, string $currencyCode, int $amountMinor, int $settlementBatchId): void
|
|
{
|
|
if ($amountMinor <= 0) {
|
|
return;
|
|
}
|
|
|
|
$currency = strtoupper($currencyCode);
|
|
|
|
$wallet = PlayerWallet::query()
|
|
->where('player_id', $player->id)
|
|
->where('wallet_type', 'lottery')
|
|
->where('currency_code', $currency)
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
if ($wallet === null) {
|
|
$wallet = PlayerWallet::query()->create([
|
|
'player_id' => $player->id,
|
|
'wallet_type' => 'lottery',
|
|
'currency_code' => $currency,
|
|
'balance' => 0,
|
|
'frozen_balance' => 0,
|
|
'status' => 0,
|
|
'version' => 0,
|
|
]);
|
|
$wallet = PlayerWallet::query()->whereKey($wallet->id)->lockForUpdate()->firstOrFail();
|
|
}
|
|
|
|
$before = (int) $wallet->balance;
|
|
$after = $before + $amountMinor;
|
|
$wallet->forceFill([
|
|
'balance' => $after,
|
|
'version' => (int) $wallet->version + 1,
|
|
])->save();
|
|
|
|
WalletTxn::query()->create([
|
|
'txn_no' => $this->newTxnNo(),
|
|
'player_id' => $player->id,
|
|
'wallet_id' => $wallet->id,
|
|
'biz_type' => 'settle_payout',
|
|
'biz_no' => 'SB'.$settlementBatchId,
|
|
'direction' => self::TXN_DIR_IN,
|
|
'amount' => $amountMinor,
|
|
'balance_before' => $before,
|
|
'balance_after' => $after,
|
|
'status' => self::TXN_POSTED,
|
|
'external_ref_no' => null,
|
|
'idempotent_key' => 'settle-payout:'.$settlementBatchId.':'.$player->id,
|
|
'remark' => null,
|
|
]);
|
|
}
|
|
|
|
private function newTxnNo(): string
|
|
{
|
|
return 'WL'.now()->format('YmdHis').str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
|
}
|
|
}
|