where('period_id', $recordId) ->where('status', 1) ->order('id', 'asc') ->select() ->toArray(); /** @var array}> */ $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; } }