Files
lotteryLaravel/app/Services/AgentSettlement/GameSettlementReversalService.php
kang 6b2ea39ea1
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
fix: 修复并发竞态与幂等性缺陷并优化子树查询
- 扩大玩家ID随机数位宽降低碰撞概率
- 授信额度变更加 lockForUpdate 防并发下调
- 结算冲正与入账利用唯一索引实现幂等闸门
- 失败登录计数加锁防丢失
- 代理子树查询改用子查询替代全表 pluck
- 修复返点默认值百分比单位转换
- 测试中全量 fake 广播事件防 CI 500
2026-06-18 09:38:24 +08:00

115 lines
4.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services\AgentSettlement;
use App\Models\Player;
use App\Models\TicketItem;
use App\Services\Player\PlayerCreditService;
use App\Support\PlayerFundingMode;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
final class GameSettlementReversalService
{
public function __construct(
private readonly PlayerCreditService $playerCreditService,
) {}
public function reverseTicketItem(TicketItem $item): void
{
$ledger = DB::table('share_ledger')->where('ticket_item_id', $item->id)->whereNull('reversal_of_id')->first();
if ($ledger === null) {
return;
}
$settledAt = now();
DB::transaction(function () use ($item, $ledger, $settledAt): 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);
}
}
});
}
/**
* 识别数据库唯一约束冲突PostgreSQL SQLSTATE 23505SQLite 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;
}
}