feat: 切换 schema dump 基线并增强返点结算与管理校验

This commit is contained in:
2026-06-08 17:41:41 +08:00
parent 2d32f006c5
commit 8d5d7f5b17
130 changed files with 5746 additions and 6723 deletions

View File

@@ -43,10 +43,17 @@ final class AgentGameSettlementRecorder
$gameType = trim((string) ($item->play_code ?? '')) ?: '*';
$snapshot = $this->snapshotBuilder->buildForPlayer($player, $gameType);
$baseRebateRate = $this->baseRebateRateForTicketItem($item);
$basicRebateRate = $this->normalizeRate($baseRebateRate + (float) $snapshot['rebate_rate']);
$shareSnapshot = [
...$snapshot,
'base_rebate_rate' => $baseRebateRate,
'basic_rebate_rate' => $basicRebateRate,
];
$gameWinLoss = $this->platformWinLoss($item, $netWin, $terminalStatus);
$validBet = (int) $item->total_bet_amount;
$basicRebate = (int) round($validBet * $snapshot['rebate_rate']);
$basicRebate = (int) round($validBet * $basicRebateRate);
$extraRebate = (int) round($validBet * $snapshot['extra_rebate_rate']);
$extraByCode = [];
@@ -66,15 +73,11 @@ final class AgentGameSettlementRecorder
$settledAt = now();
DB::transaction(function () use ($item, $player, $snapshot, $gameWinLoss, $basicRebate, $result, $settledAt, $validBet, $extraRebate, $gameType): void {
DB::transaction(function () use ($item, $player, $snapshot, $shareSnapshot, $basicRebateRate, $gameWinLoss, $basicRebate, $result, $settledAt, $validBet, $extraRebate, $gameType): void {
$item->forceFill([
'agent_node_id' => $snapshot['agent_node_id'],
'share_snapshot' => [
'total_shares' => $snapshot['total_shares'],
'actual_shares' => $snapshot['actual_shares'],
'chain_codes' => $snapshot['chain_codes'],
],
'agent_rebate_rate_snapshot' => $snapshot['rebate_rate'],
'share_snapshot' => $shareSnapshot,
'agent_rebate_rate_snapshot' => $basicRebateRate,
'agent_settled_at' => $settledAt,
])->save();
@@ -83,7 +86,7 @@ final class AgentGameSettlementRecorder
'player_id' => $player->id,
'agent_node_id' => $snapshot['agent_node_id'],
'agent_path' => json_encode($snapshot['agent_path']),
'share_snapshot' => json_encode($snapshot),
'share_snapshot' => json_encode($shareSnapshot),
'game_win_loss' => (int) round($gameWinLoss),
'basic_rebate' => $basicRebate,
'shared_net_win_loss' => (int) round($result->sharedNetWinLoss),
@@ -99,7 +102,7 @@ final class AgentGameSettlementRecorder
'ticket_item_id' => $item->id,
'game_type' => $gameType,
'valid_bet_amount' => $validBet,
'rebate_rate' => $snapshot['rebate_rate'],
'rebate_rate' => $basicRebateRate,
'rebate_amount' => $basicRebate,
'rebate_type' => 'basic',
'owner_agent_id' => $snapshot['agent_node_id'],
@@ -138,6 +141,38 @@ final class AgentGameSettlementRecorder
});
}
private function baseRebateRateForTicketItem(TicketItem $item): float
{
$ruleSnapshot = is_array($item->rule_snapshot_json) ? $item->rule_snapshot_json : [];
$baseFromRule = isset($ruleSnapshot['base_rebate_rate'])
? (float) $ruleSnapshot['base_rebate_rate']
: null;
if ($baseFromRule !== null && $baseFromRule > 0) {
return $this->normalizeRate($baseFromRule);
}
$oddsSnapshot = is_array($item->odds_snapshot_json) ? $item->odds_snapshot_json : [];
foreach ($oddsSnapshot as $row) {
if (! is_array($row)) {
continue;
}
if (! array_key_exists('rebate_rate', $row)) {
continue;
}
return $this->normalizeRate((float) $row['rebate_rate']);
}
return 0.0;
}
private function normalizeRate(float $rate): float
{
return max(0.0, min(1.0, $rate));
}
private function platformWinLoss(TicketItem $item, int $netWin, string $terminalStatus): float
{
if ($terminalStatus === 'settled_lose') {

View File

@@ -2,6 +2,8 @@
namespace App\Services\AgentSettlement;
use App\Models\Player;
use App\Services\Player\PlayerCreditService;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
@@ -10,6 +12,7 @@ final class AgentSettlementBadDebtService
{
public function __construct(
private readonly AgentSettlementPeriodCompletionService $periodCompletion,
private readonly PlayerCreditService $playerCreditService,
) {}
public function writeOff(int $originalBillId, ?string $reason, int $adminUserId): int
@@ -99,6 +102,13 @@ final class AgentSettlementBadDebtService
'updated_at' => $now,
]);
if ((string) $original->owner_type === 'player' && (int) $original->owner_id > 0 && (int) $original->net_amount > 0) {
$player = Player::query()->find((int) $original->owner_id);
if ($player !== null) {
$this->playerCreditService->releaseFromSettlement($player, $unpaid, $originalBillId);
}
}
$this->periodCompletion->syncIfReady($periodId);
return $archiveBillId;

View File

@@ -56,6 +56,20 @@ final class AgentSettlementBillGuard
public function markConfirmed(int $billId): void
{
$this->assertPeriodMutable($billId);
$bill = DB::table('settlement_bills')->where('id', $billId)->first();
if ($bill === null) {
throw new \InvalidArgumentException('bill_not_found');
}
if ((string) $bill->status === 'confirmed') {
return;
}
if ((string) $bill->status !== 'pending_confirm') {
throw ValidationException::withMessages([
'bill' => ['not_confirmable'],
]);
}
DB::table('settlement_bills')->where('id', $billId)->update([
'status' => 'confirmed',

View File

@@ -18,7 +18,9 @@ final class SettlementCenterLedgerService
'bet_hold',
'bet_hold_release',
'game_settlement_loss',
'game_settlement_win',
'settlement_confirm',
'settlement_payout',
];
private const ADJUSTMENT_BIZ_TYPES = [

View File

@@ -5,6 +5,7 @@ namespace App\Services\AgentSettlement;
use App\Models\Player;
use App\Services\Player\PlayerCreditService;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
final class SettlementPaymentService
{
@@ -25,66 +26,72 @@ final class SettlementPaymentService
*/
public function recordPayment(int $billId, int $amount, int $adminUserId, array $meta = []): void
{
$bill = DB::table('settlement_bills')->where('id', $billId)->first();
if ($bill === null) {
throw new \InvalidArgumentException('bill_not_found');
}
DB::transaction(function () use ($billId, $amount, $adminUserId, $meta): void {
$bill = DB::table('settlement_bills')->where('id', $billId)->lockForUpdate()->first();
if ($bill === null) {
throw new \InvalidArgumentException('bill_not_found');
}
$this->billGuard->assertPeriodMutable($billId);
$this->billGuard->assertPayable($billId);
$this->billGuard->assertPeriodMutable($billId);
if (! in_array((string) $bill->status, ['confirmed', 'partial_paid', 'overdue'], true)) {
throw ValidationException::withMessages([
'bill' => ['not_payable'],
]);
}
$amount = min($amount, abs((int) $bill->unpaid_amount));
if ($amount <= 0) {
return;
}
$payAmount = min($amount, abs((int) $bill->unpaid_amount));
if ($payAmount <= 0) {
return;
}
[$payerType, $payerId, $payeeType, $payeeId] = $this->resolvePayerPayee($bill);
[$payerType, $payerId, $payeeType, $payeeId] = $this->resolvePayerPayee($bill);
DB::table('payment_records')->insert([
'settlement_bill_id' => $billId,
'payer_type' => $payerType,
'payer_id' => $payerId,
'payee_type' => $payeeType,
'payee_id' => $payeeId,
'amount' => $amount,
'method' => $meta['method'] ?? null,
'proof' => $meta['proof'] ?? null,
'remark' => $meta['remark'] ?? null,
'status' => 'confirmed',
'created_by' => $adminUserId,
'confirmed_by' => $adminUserId,
'confirmed_at' => now(),
'created_at' => now(),
'updated_at' => now(),
]);
DB::table('payment_records')->insert([
'settlement_bill_id' => $billId,
'payer_type' => $payerType,
'payer_id' => $payerId,
'payee_type' => $payeeType,
'payee_id' => $payeeId,
'amount' => $payAmount,
'method' => $meta['method'] ?? null,
'proof' => $meta['proof'] ?? null,
'remark' => $meta['remark'] ?? null,
'status' => 'confirmed',
'created_by' => $adminUserId,
'confirmed_by' => $adminUserId,
'confirmed_at' => now(),
'created_at' => now(),
'updated_at' => now(),
]);
$newPaid = (int) $bill->paid_amount + $amount;
$newUnpaid = max(0, (int) $bill->unpaid_amount - $amount);
$status = $newUnpaid === 0 ? 'settled' : 'partial_paid';
$newPaid = (int) $bill->paid_amount + $payAmount;
$newUnpaid = max(0, (int) $bill->unpaid_amount - $payAmount);
$status = $newUnpaid === 0 ? 'settled' : 'partial_paid';
DB::table('settlement_bills')->where('id', $billId)->update([
'paid_amount' => $newPaid,
'unpaid_amount' => $newUnpaid,
'status' => $status,
'updated_at' => now(),
]);
DB::table('settlement_bills')->where('id', $billId)->update([
'paid_amount' => $newPaid,
'unpaid_amount' => $newUnpaid,
'status' => $status,
'updated_at' => now(),
]);
if ($bill->owner_type === 'player' && (int) $bill->owner_id > 0) {
$player = Player::query()->find((int) $bill->owner_id);
if ($player !== null) {
if ((int) $bill->net_amount > 0) {
$this->playerCreditService->releaseFromSettlement($player, $amount, $billId);
} elseif ((int) $bill->net_amount < 0) {
$this->playerCreditService->applySettlementPayout($player, $amount, $billId);
}
if ($bill->owner_type === 'player' && (int) $bill->owner_id > 0) {
$player = Player::query()->find((int) $bill->owner_id);
if ($player !== null) {
if ((int) $bill->net_amount > 0) {
$this->playerCreditService->releaseFromSettlement($player, $payAmount, $billId);
} elseif ((int) $bill->net_amount < 0) {
$this->playerCreditService->applySettlementPayout($player, $payAmount, $billId);
}
if ($status === 'settled') {
$this->periodCloseRebate->markRebatesSettledForBill($billId);
if ($status === 'settled') {
$this->periodCloseRebate->markRebatesSettledForBill($billId);
}
}
}
}
$this->periodCompletion->syncIfReady((int) $bill->settlement_period_id);
$this->periodCompletion->syncIfReady((int) $bill->settlement_period_id);
});
}
/**