131 lines
5.2 KiB
PHP
131 lines
5.2 KiB
PHP
<?php
|
||
|
||
namespace App\Services\AgentSettlement;
|
||
|
||
use App\Models\Player;
|
||
use App\Models\TicketItem;
|
||
use App\Support\PlayerFundingMode;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Database\QueryException;
|
||
use App\Services\Player\PlayerCreditService;
|
||
|
||
final class GameSettlementReversalService
|
||
{
|
||
public function __construct(
|
||
private readonly PlayerCreditService $playerCreditService,
|
||
) {}
|
||
|
||
public function reverseTicketItem(TicketItem $item, int $settlementVersion = 0): void
|
||
{
|
||
$ledger = DB::table('share_ledger as sl')
|
||
->where('sl.ticket_item_id', $item->id)
|
||
->whereNull('sl.reversal_of_id')
|
||
->whereNotExists(function ($query): void {
|
||
$query->selectRaw('1')
|
||
->from('share_ledger as reversal')
|
||
->whereColumn('reversal.reversal_of_id', 'sl.id');
|
||
})
|
||
->orderByDesc('sl.id')
|
||
->select('sl.*')
|
||
->first();
|
||
if ($ledger === null) {
|
||
return;
|
||
}
|
||
|
||
$settledAt = now();
|
||
|
||
DB::transaction(function () use ($item, $ledger, $settledAt, $settlementVersion): void {
|
||
// 幂等闸门:share_ledger.reversal_of_id 上有 partial unique 索引,
|
||
// 同一原账不允许插入多条反转记录。重复调用时唯一约束冲突即视为已反转、跳过。
|
||
// 一次只让 reversal_of_id 出现一次写入,确保所有后续副作用(rebate、credit 冲正)
|
||
// 只在唯一反转成功路径上执行。
|
||
try {
|
||
DB::table('share_ledger')->insert([
|
||
'ticket_item_id' => $item->id,
|
||
'player_id' => $ledger->player_id,
|
||
'agent_node_id' => $ledger->agent_node_id,
|
||
'agent_path' => $ledger->agent_path,
|
||
'share_snapshot' => $ledger->share_snapshot,
|
||
'game_win_loss' => -1 * (int) $ledger->game_win_loss,
|
||
'basic_rebate' => -1 * (int) $ledger->basic_rebate,
|
||
'shared_net_win_loss' => -1 * (int) $ledger->shared_net_win_loss,
|
||
'allocations_json' => $ledger->allocations_json,
|
||
'reversal_of_id' => $ledger->id,
|
||
'settled_at' => $settledAt,
|
||
'created_at' => $settledAt,
|
||
'updated_at' => $settledAt,
|
||
]);
|
||
} catch (QueryException $e) {
|
||
if ($this->isUniqueViolation($e)) {
|
||
return;
|
||
}
|
||
throw $e;
|
||
}
|
||
|
||
$rebates = DB::table('rebate_records')
|
||
->where('ticket_item_id', $item->id)
|
||
->where('status', 'accrued')
|
||
->get();
|
||
|
||
foreach ($rebates as $rebate) {
|
||
DB::table('rebate_records')->insert([
|
||
'player_id' => $rebate->player_id,
|
||
'ticket_item_id' => $item->id,
|
||
'game_type' => $rebate->game_type,
|
||
'valid_bet_amount' => $rebate->valid_bet_amount,
|
||
'rebate_rate' => $rebate->rebate_rate,
|
||
'rebate_amount' => -1 * (int) $rebate->rebate_amount,
|
||
'rebate_type' => $rebate->rebate_type,
|
||
'owner_agent_id' => $rebate->owner_agent_id,
|
||
'status' => 'reversed',
|
||
'reversal_of_id' => $rebate->id,
|
||
'created_at' => $settledAt,
|
||
'updated_at' => $settledAt,
|
||
]);
|
||
DB::table('rebate_records')->where('id', $rebate->id)->update(['status' => 'reversed']);
|
||
}
|
||
|
||
$player = Player::query()->find((int) $ledger->player_id);
|
||
if ($player !== null && PlayerFundingMode::usesCredit($player)) {
|
||
$gameWinLoss = (int) $ledger->game_win_loss;
|
||
if ($gameWinLoss !== 0) {
|
||
$this->playerCreditService->reverseGameSettlement($player, $gameWinLoss, $item->id, $settlementVersion);
|
||
}
|
||
$this->playerCreditService->restoreBetHoldAfterSettlementReversal(
|
||
$player,
|
||
(int) $item->actual_deduct_amount,
|
||
(int) $item->id,
|
||
$settlementVersion,
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 识别数据库唯一约束冲突(PostgreSQL SQLSTATE 23505;SQLite SQLSTATE 23000/error code 19)。
|
||
* 兜底扫描消息中 unique 字样,兼容驱动返回的 SQLSTATE 缺失场景。
|
||
*/
|
||
private function isUniqueViolation(QueryException $e): bool
|
||
{
|
||
$sqlState = (string) $e->getCode();
|
||
$errorInfo = $e->errorInfo ?? null;
|
||
$driverCode = is_array($errorInfo) ? (int) ($errorInfo[1] ?? 0) : 0;
|
||
|
||
if ($sqlState === '23505') {
|
||
return true;
|
||
}
|
||
if ($sqlState === '23000' || $sqlState === '19') {
|
||
return true;
|
||
}
|
||
if ($driverCode === 19 || $driverCode === 1062) {
|
||
return true;
|
||
}
|
||
$message = $e->getMessage();
|
||
if (stripos($message, 'unique') !== false || stripos($message, 'UNIQUE constraint') !== false) {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
}
|