|
|
@@ -4,6 +4,7 @@ namespace App\Services;
|
|
|
|
|
|
use App\Models\User;
|
|
|
use Carbon\Carbon;
|
|
|
+use Illuminate\Contracts\Cache\LockTimeoutException;
|
|
|
use Illuminate\Http\Client\Pool;
|
|
|
use Illuminate\Http\Client\Response;
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
@@ -15,6 +16,8 @@ class ThirdGameBalanceService
|
|
|
{
|
|
|
private const CODE_SUCCESS = 10000;
|
|
|
|
|
|
+ private const QUOTA_REFRESH_SECONDS = 60;
|
|
|
+
|
|
|
private const HISTORY_QUERY_INTERVAL_SECONDS = 60;
|
|
|
|
|
|
private const HISTORY_PAGE_INTERVAL_SECONDS = 10;
|
|
|
@@ -46,17 +49,17 @@ class ThirdGameBalanceService
|
|
|
*/
|
|
|
public function quotaRatios(): array
|
|
|
{
|
|
|
- $response = $this->request('/api/server/quota', [
|
|
|
- 'currency' => (string) config('third_game.currency', 'CNY'),
|
|
|
- ]);
|
|
|
- if (!$response instanceof Response || !$response->successful()
|
|
|
- || (int) $response->json('code') !== self::CODE_SUCCESS
|
|
|
- || !is_array($response->json('data.ratios'))) {
|
|
|
+ $quota = $this->quotaData();
|
|
|
+ if ($quota !== null && $quota['date'] !== now('Asia/Shanghai')->toDateString()) {
|
|
|
+ return ['ok' => false, 'ratios' => [], 'retry_seconds' => self::QUOTA_REFRESH_SECONDS];
|
|
|
+ }
|
|
|
+ $data = $quota['data'] ?? null;
|
|
|
+ if (!is_array($data['ratios'] ?? null)) {
|
|
|
return ['ok' => false, 'ratios' => []];
|
|
|
}
|
|
|
|
|
|
$ratios = []; $invalid = [];
|
|
|
- foreach ($response->json('data.ratios') as $row) {
|
|
|
+ foreach ($data['ratios'] as $row) {
|
|
|
if (!is_array($row)) continue;
|
|
|
$name = $row['platfrom'] ?? $row['platform'] ?? $row['platType'] ?? '';
|
|
|
if (!is_string($name)) continue;
|
|
|
@@ -72,6 +75,69 @@ class ThirdGameBalanceService
|
|
|
return ['ok' => true, 'ratios' => array_diff_key($ratios, $invalid)];
|
|
|
}
|
|
|
|
|
|
+ /** 首页和 WebSocket 只读取快照,不请求供应商。 */
|
|
|
+ public function merchantBalance(): array
|
|
|
+ {
|
|
|
+ $currency = (string) config('third_game.currency', 'CNY');
|
|
|
+ $snapshot = Cache::get($this->quotaCacheKey($currency) . ':balance');
|
|
|
+ return [
|
|
|
+ 'balance' => $snapshot['balance'] ?? null,
|
|
|
+ 'currency' => $currency,
|
|
|
+ 'stale' => $snapshot !== null && ($snapshot['failed']
|
|
|
+ || now()->getTimestamp() - $snapshot['fetched_at'] >= self::QUOTA_REFRESH_SECONDS * 2),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ public function refreshMerchantBalance(): void
|
|
|
+ {
|
|
|
+ $this->quotaData();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 商户余额和分成比例共用一次 quota 请求。失败也进入冷却,最后一次有效余额单独保留。 */
|
|
|
+ private function quotaData(): ?array
|
|
|
+ {
|
|
|
+ if (!$this->configured()) return null;
|
|
|
+
|
|
|
+ $currency = (string) config('third_game.currency', 'CNY');
|
|
|
+ $key = $this->quotaCacheKey($currency);
|
|
|
+ $recent = Cache::get($key);
|
|
|
+ if ($recent !== null) return $recent;
|
|
|
+
|
|
|
+ $timeout = max(1, (int) config('third_game.timeout', 10));
|
|
|
+ try {
|
|
|
+ return Cache::lock($key . ':lock', $timeout + 10)->block($timeout + 1, function () use ($key, $currency): ?array {
|
|
|
+ $recent = Cache::get($key);
|
|
|
+ if ($recent !== null) return $recent;
|
|
|
+
|
|
|
+ $startedAt = now('Asia/Shanghai');
|
|
|
+ $response = $this->request('/api/server/quota', ['currency' => $currency]);
|
|
|
+ $data = $response instanceof Response && $response->successful()
|
|
|
+ && (int) $response->json('code') === self::CODE_SUCCESS ? $response->json('data') : null;
|
|
|
+ if (!is_array($data)) $data = null;
|
|
|
+
|
|
|
+ $balance = $this->decimalAmount($data[$currency] ?? null, signed: true);
|
|
|
+ $snapshotKey = $key . ':balance';
|
|
|
+ if ($balance !== null) {
|
|
|
+ Cache::forever($snapshotKey, ['balance' => $balance, 'fetched_at' => now()->getTimestamp(), 'failed' => false]);
|
|
|
+ } elseif ($snapshot = Cache::get($snapshotKey)) {
|
|
|
+ Cache::forever($snapshotKey, array_replace($snapshot, ['failed' => true]));
|
|
|
+ }
|
|
|
+ $recent = ['date' => $startedAt->toDateString(), 'data' => $data];
|
|
|
+ Cache::put($key, $recent, $startedAt->addSeconds(self::QUOTA_REFRESH_SECONDS));
|
|
|
+ return $recent;
|
|
|
+ });
|
|
|
+ } catch (LockTimeoutException $e) {
|
|
|
+ return Cache::get($key);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private function quotaCacheKey(string $currency): string
|
|
|
+ {
|
|
|
+ return 'third_game:merchant_quota:' . hash('sha256', json_encode([
|
|
|
+ config('third_game.api_url'), config('third_game.sn'), config('third_game.key'), $currency,
|
|
|
+ ], JSON_THROW_ON_ERROR));
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 批量查询用户的三方游戏总余额,三方与主钱包金额按 1:1 处理。
|
|
|
*
|
|
|
@@ -555,14 +621,14 @@ class ThirdGameBalanceService
|
|
|
return $total;
|
|
|
}
|
|
|
|
|
|
- private function decimalAmount($value): ?string
|
|
|
+ private function decimalAmount($value, bool $signed = false): ?string
|
|
|
{
|
|
|
if (!is_int($value) && !is_float($value) && !is_string($value)) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
$value = trim((string) $value);
|
|
|
- if (!preg_match('/^\d+(?:\.\d+)?$/D', $value)) {
|
|
|
+ if (!preg_match($signed ? '/^-?\d+(?:\.\d+)?$/D' : '/^\d+(?:\.\d+)?$/D', $value)) {
|
|
|
return null;
|
|
|
}
|
|
|
return bcadd($value, '0', 10);
|