$memberIds * @return array */ public function totals(array $memberIds): array { $memberIds = array_values(array_unique(array_map('strval', $memberIds))); $totals = []; $pending = []; foreach ($memberIds as $memberId) { $cached = Cache::get($this->cacheKey($memberId)); if ($cached !== null) { $totals[$memberId] = (float) $cached; } else { $pending[] = $memberId; } } if ($pending === []) { return $totals; } if (!$this->configured()) { Log::warning('third_game_balance_config_missing'); foreach ($pending as $memberId) { $totals[$memberId] = null; } return $totals; } try { $responses = Http::pool(function (Pool $pool) use ($pending) { $requests = []; foreach ($pending as $memberId) { $random = $this->random(); $requests[] = $pool->as($memberId) ->asJson() ->withHeaders([ 'sign' => md5($random . config('third_game.sn') . config('third_game.key')), 'random' => $random, 'sn' => config('third_game.sn'), ]) ->timeout(max(1, (int) config('third_game.timeout', 10))) ->connectTimeout(5) ->withOptions($this->httpOptions()) ->post(config('third_game.api_url') . '/api/server/balanceAll', [ 'playerId' => $this->playerId($memberId), 'currency' => config('third_game.currency', 'CNY'), ]); } return $requests; }); } catch (\Throwable $e) { Log::warning('third_game_balance_pool_failed', ['message' => $e->getMessage()]); foreach ($pending as $memberId) { $totals[$memberId] = null; } return $totals; } foreach ($pending as $memberId) { $response = $responses[$memberId] ?? null; $balances = $this->balancesFromResponse($response); $total = $balances === null ? null : $this->totalFromBalances($balances); $totals[$memberId] = $total; if ($total !== null) { Cache::put( $this->cacheKey($memberId), $total, max(1, (int) config('third_game.cache_seconds', 30)) ); Cache::put( $this->detailCacheKey($memberId), $balances, max(1, (int) config('third_game.cache_seconds', 30)) ); } } return $totals; } public function playerId(string $memberId): string { return 'p' . substr(md5(config('third_game.sn') . '_' . $memberId), 0, 10); } /** * 查询单个用户在 ag、pg 等各游戏平台的余额。 */ public function detail(string $memberId, bool $forceRefresh = false): array { if (!$forceRefresh) { $cached = Cache::get($this->detailCacheKey($memberId)); if (is_array($cached)) { return $this->detailResult($memberId, $cached); } } $response = $this->request('/api/server/balanceAll', [ 'playerId' => $this->playerId($memberId), 'currency' => config('third_game.currency', 'CNY'), ]); $balances = $this->balancesFromResponse($response); if ($balances === null) { return ['ok' => false, 'msg' => $this->responseMessage($response)]; } $ttl = max(1, (int) config('third_game.cache_seconds', 30)); Cache::put($this->detailCacheKey($memberId), $balances, $ttl); Cache::put($this->cacheKey($memberId), $this->totalFromBalances($balances), $ttl); return $this->detailResult($memberId, $balances); } /** * 一键把用户在所有三方游戏平台的余额转出。 */ public function recycle(string $memberId): array { // 必须在调用 transferAll 前校验,避免三方已转出后才因本地配置错误中断。 $this->recycleRate(); $response = $this->request('/api/server/transferAll', [ 'playerId' => $this->playerId($memberId), 'currency' => config('third_game.currency', 'CNY'), ], true); $body = $response instanceof Response ? $response->json() : null; if (!is_array($body)) { return [ 'ok' => false, 'uncertain' => true, 'msg' => $response instanceof Response ? '三方游戏接口返回异常,回收结果待核对' : '三方游戏接口请求失败,回收结果待核对', ]; } if ((int) ($body['code'] ?? 0) === self::CODE_SUCCESS) { $data = $body['data'] ?? null; if (!is_array($data) || !array_key_exists('balanceAll', $data) || !is_numeric($data['balanceAll']) || (float) $data['balanceAll'] < 0) { return [ 'ok' => false, 'uncertain' => true, 'msg' => '三方游戏回收成功响应缺少有效 balanceAll,回收结果待核对', ]; } $gameBalance = $this->decimalAmount($data['balanceAll']); if ($gameBalance === null) { return [ 'ok' => false, 'uncertain' => true, 'msg' => '三方游戏回收金额格式异常,回收结果待核对', ]; } $this->clearCache($memberId); return [ 'ok' => true, 'game_balance' => $gameBalance, 'wallet_balance' => $this->toWalletBalance($gameBalance), ]; } if ($this->isPlayerNotFound((string) ($body['msg'] ?? ''))) { $this->clearCache($memberId); return ['ok' => true, 'game_balance' => '0.0000000000', 'wallet_balance' => '0.0000000000']; } return [ 'ok' => false, 'uncertain' => false, 'msg' => $this->responseMessage($response), ]; } public function toWalletBalance($gameBalance): string { $balance = $this->decimalAmount($gameBalance); if ($balance === null) { throw new \InvalidArgumentException('三方游戏余额格式异常'); } return bcdiv($balance, $this->recycleRate(), 10); } private function recycleRate(): string { $rate = trim((string) config('third_game.recycle_rate', '1')); if (!preg_match('/^\d+(?:\.\d+)?$/D', $rate) || bccomp($rate, '0', 10) <= 0) { throw new \RuntimeException('GAME_RECYCLE_RATE 必须是大于 0 的数字'); } return $rate; } private function balancesFromResponse($response): ?array { if (!$response instanceof Response) { return null; } $body = $response->json(); if (!is_array($body)) { return null; } if ((int) ($body['code'] ?? 0) === self::CODE_SUCCESS) { return is_array($body['data'] ?? null) ? $body['data'] : []; } if ($this->isPlayerNotFound((string) ($body['msg'] ?? ''))) { return []; } Log::warning('third_game_balance_query_failed', [ 'code' => $body['code'] ?? null, 'msg' => $body['msg'] ?? null, ]); return null; } private function detailResult(string $memberId, array $balances): array { $gameBalance = $this->sumBalances($balances); $walletBalances = []; $gameBalances = []; foreach ($balances as $platform => $balance) { if (is_numeric($balance)) { $walletBalances[] = [ 'platform' => (string) $platform, 'balance' => (float) $balance, ]; $gameBalances[] = [ 'platform' => (string) $platform, 'balance' => (float) $balance, ]; } } return [ 'ok' => true, 'member_id' => $memberId, 'player_id' => $this->playerId($memberId), 'currency' => (string) config('third_game.currency', 'CNY'), 'rate' => 1, 'list' => $walletBalances, 'game_list' => $gameBalances, 'total_game_balance' => (float) $gameBalance, 'total_balance' => $this->totalFromBalances($balances), ]; } private function totalFromBalances(array $balances): float { return (float) $this->sumBalances($balances); } private function sumBalances(array $balances): string { $total = '0.0000000000'; foreach ($balances as $balance) { $amount = $this->decimalAmount($balance); if ($amount !== null) { $total = bcadd($total, $amount, 10); } } return $total; } private function decimalAmount($value): ?string { if (!is_int($value) && !is_float($value) && !is_string($value)) { return null; } $value = trim((string) $value); if (!preg_match('/^\d+(?:\.\d+)?$/D', $value)) { return null; } return bcadd($value, '0', 10); } private function request(string $path, array $data, bool $transfer = false) { if (!$this->configured()) { return null; } $random = $this->random(); try { return Http::asJson() ->withHeaders([ 'sign' => md5($random . config('third_game.sn') . config('third_game.key')), 'random' => $random, 'sn' => config('third_game.sn'), ]) ->timeout(max(1, (int) config( $transfer ? 'third_game.transfer_timeout' : 'third_game.timeout', $transfer ? 65 : 10 ))) ->connectTimeout(5) ->withOptions($this->httpOptions()) ->post(config('third_game.api_url') . $path, $data); } catch (\Throwable $e) { Log::warning('third_game_request_failed', [ 'path' => $path, 'message' => $e->getMessage(), ]); return null; } } private function responseMessage($response): string { if (!$response instanceof Response) { return $this->configured() ? '三方游戏接口请求失败' : '三方游戏配置缺失'; } return (string) ($response->json('msg') ?: '三方游戏接口异常'); } private function configured(): bool { return config('third_game.api_url') !== '' && config('third_game.sn') !== '' && config('third_game.key') !== ''; } private function isPlayerNotFound(string $message): bool { $message = strtolower($message); return str_contains($message, 'playerid') && (str_contains($message, '不存在') || str_contains($message, 'not exist') || str_contains($message, 'not found')); } private function httpOptions(): array { $options = []; $proxy = (string) config('third_game.proxy', ''); if ($proxy !== '') { $options['proxy'] = $proxy; } $caBundle = (string) config('third_game.ca_bundle', ''); if ($caBundle !== '') { $options['verify'] = $caBundle; } return $options; } private function cacheKey(string $memberId): string { return 'third_game_total_balance:' . $memberId; } private function detailCacheKey(string $memberId): string { return 'third_game_balance_detail:' . $memberId; } private function clearCache(string $memberId): void { Cache::forget($this->cacheKey($memberId)); Cache::forget($this->detailCacheKey($memberId)); } private function random(): string { $characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; $value = ''; for ($i = 0; $i < 32; $i++) { $value .= $characters[random_int(0, strlen($characters) - 1)]; } return $value; } }