Files
webman-buildadmin/app/common/service/GameBetSettleService.php
zhenhui c184fa8a46 1.优化后台测试推送功能页面
2.优化开奖和实时对局页面
2026-04-18 17:16:13 +08:00

267 lines
9.0 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
declare(strict_types=1);
namespace app\common\service;
use support\think\Db;
use Throwable;
/**
* 开奖后结算注单:写入 win_amount、status=已结算;中奖时入账并记 user_wallet_recordbiz_type=payout
*/
final class GameBetSettleService
{
private const BASE_ODDS = 33;
/**
* 对指定期次按开奖号码结算所有「待开奖」注单;同一注单幂等(仅 status=1 会更新)。
*
* @throws Throwable
*/
public static function settleBetsForDraw(int $recordId, int $resultNumber): void
{
if ($recordId <= 0 || $resultNumber < 1) {
return;
}
$now = time();
$bets = Db::name('bet_order')
->where('period_id', $recordId)
->where('status', 1)
->order('id', 'asc')
->select()
->toArray();
/** @var array<int, array{period_no: string, total_win: string, balance_after: string, orders: list<array{order_no: string, win_amount: string, hit: bool}>}> */
$aggregateByUser = [];
foreach ($bets as $bet) {
$betId = (int) ($bet['id'] ?? 0);
if ($betId <= 0) {
continue;
}
$win = self::computeWinAmount($bet, $resultNumber);
$jackpot = '0.0000';
$affected = Db::name('bet_order')
->where('id', $betId)
->where('status', 1)
->update([
'win_amount' => $win,
'jackpot_extra_amount' => $jackpot,
'status' => 2,
'update_time' => $now,
]);
if ($affected === 0) {
continue;
}
// 结算刚刚成功status 1 → 2把本单下注总额 1:1 累加到用户打码量
self::creditUserBetFlow($bet, $now);
$userId = (int) ($bet['user_id'] ?? 0);
if ($userId <= 0) {
continue;
}
$balanceAfter = (string) (Db::name('user')->where('id', $userId)->value('coin') ?? '0');
if (bccomp($win, '0', 4) > 0) {
$paid = self::creditUserPayout($bet, $betId, $win, $now);
if ($paid !== null) {
$balanceAfter = $paid;
}
}
$periodNo = (string) ($bet['period_no'] ?? '');
if (!isset($aggregateByUser[$userId])) {
$aggregateByUser[$userId] = [
'period_no' => $periodNo,
'total_win' => '0.0000',
'balance_after' => $balanceAfter,
'orders' => [],
];
}
$aggregateByUser[$userId]['total_win'] = bcadd($aggregateByUser[$userId]['total_win'], $win, 4);
$aggregateByUser[$userId]['balance_after'] = $balanceAfter;
$aggregateByUser[$userId]['orders'][] = [
'order_no' => (string) $betId,
'win_amount' => $win,
'hit' => bccomp($win, '0', 4) > 0,
];
}
foreach ($aggregateByUser as $userId => $agg) {
$hitOrderCount = 0;
foreach ($agg['orders'] as $o) {
if (($o['hit'] ?? false) === true) {
$hitOrderCount++;
}
}
UserPushService::publish((int) $userId, UserPushService::EVT_BET_SETTLED, [
'period_no' => $agg['period_no'],
'result_number' => $resultNumber,
'total_win_amount' => $agg['total_win'],
'order_count' => count($agg['orders']),
'hit_order_count' => $hitOrderCount,
'balance_after' => $agg['balance_after'],
]);
if (bccomp($agg['total_win'], '0', 4) > 0) {
UserPushService::publish((int) $userId, UserPushService::EVT_WALLET_CHANGED, [
'reason' => 'payout',
'ref_type' => 'game_period',
'ref_id' => (string) $recordId,
'delta' => $agg['total_win'],
'balance_after' => $agg['balance_after'],
]);
}
}
}
/**
* 补偿:库中已结束局次但注单仍为待开奖的,可重复调用(幂等)。
*/
public static function settlePendingForEndedRecords(): int
{
$rows = Db::name('game_record')
->where('status', 4)
->whereNotNull('result_number')
->field(['id', 'result_number'])
->order('id', 'asc')
->select()
->toArray();
$count = 0;
foreach ($rows as $row) {
$rid = (int) ($row['id'] ?? 0);
$rn = (int) ($row['result_number'] ?? 0);
if ($rid <= 0 || $rn < 1) {
continue;
}
$pending = Db::name('bet_order')
->where('period_id', $rid)
->where('status', 1)
->count();
if ($pending === 0) {
continue;
}
Db::startTrans();
try {
self::settleBetsForDraw($rid, $rn);
Db::commit();
$count++;
} catch (Throwable $e) {
Db::rollback();
throw $e;
}
}
return $count;
}
/**
* 应付派彩:开奖号码 ∈ pick_numbers 即中奖;整笔 total_amount × (连胜+1) × 33与 GameLiveService 一致)。
*/
public static function computeWinAmount(array $bet, int $resultNumber): string
{
$pickNumbers = $bet['pick_numbers'] ?? null;
if (is_string($pickNumbers)) {
$decoded = json_decode($pickNumbers, true);
$pickNumbers = is_array($decoded) ? $decoded : [];
}
if (!is_array($pickNumbers)) {
$pickNumbers = [];
}
if (!in_array($resultNumber, array_map('intval', $pickNumbers), true)) {
return '0.0000';
}
$total = (string) ($bet['total_amount'] ?? '0');
$streak = (int) ($bet['streak_at_bet'] ?? 0);
$odds = (string) (($streak + 1) * self::BASE_ODDS);
return bcmul($total, $odds, 4);
}
/**
* 累加玩家打码量(流水):按本注单 total_amount 1:1 加到 user.bet_flow_coin。
*
* 幂等性由调用点保证:只有 bet_order 首次从 status=1 变更为 status=2返回 $affected=1
* 时才会调用本方法,重复结算不会触发。
*/
private static function creditUserBetFlow(array $bet, int $now): void
{
$userId = isset($bet['user_id']) && is_numeric($bet['user_id']) ? intval($bet['user_id']) : 0;
if ($userId <= 0) {
return;
}
$totalRaw = $bet['total_amount'] ?? '0';
$total = is_string($totalRaw) ? trim($totalRaw) : (is_numeric($totalRaw) ? strval($totalRaw) : '0');
if ($total === '' || !is_numeric($total)) {
return;
}
$flow = bcadd($total, '0', 4);
if (bccomp($flow, '0', 4) <= 0) {
return;
}
// 原子加法:避免读-改-写导致的并发覆盖;$flow 已由 bcadd 归一化为纯数字字符串,不存在 SQL 注入
Db::name('user')
->where('id', $userId)
->update([
'bet_flow_coin' => Db::raw('bet_flow_coin + ' . $flow),
'update_time' => $now,
]);
}
/**
* @return string|null 派彩后余额;已幂等入账过时返回当前余额;失败或未执行派彩返回 null
*/
private static function creditUserPayout(array $bet, int $betId, string $winAmount, int $now): ?string
{
$userId = (int) ($bet['user_id'] ?? 0);
if ($userId <= 0) {
return null;
}
$idem = 'payout_bet_' . $betId;
if (Db::name('user_wallet_record')->where('idempotency_key', $idem)->value('id')) {
$coin = Db::name('user')->where('id', $userId)->value('coin');
return (string) ($coin ?? '0');
}
$user = Db::name('user')->where('id', $userId)->find();
if (!$user) {
return null;
}
$before = (string) ($user['coin'] ?? '0');
$after = bcadd($before, $winAmount, 4);
Db::name('user_wallet_record')->insert([
'user_id' => $userId,
'channel_id' => $bet['channel_id'] ?? null,
'biz_type' => 'payout',
'direction' => 1,
'amount' => $winAmount,
'balance_before' => $before,
'balance_after' => $after,
'ref_type' => 'bet_order',
'ref_id' => $betId,
'idempotency_key' => $idem,
'operator_admin_id' => null,
'remark' => '压注派彩',
'create_time' => $now,
]);
Db::name('user')->where('id', $userId)->update([
'coin' => $after,
'update_time' => $now,
]);
return $after;
}
}