ThirdGameCommissionService.php 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. <?php
  2. namespace App\Services;
  3. use App\Services\Sharing\EffectiveBet as Money;
  4. use Illuminate\Contracts\Cache\LockTimeoutException;
  5. use Illuminate\Support\Facades\Cache;
  6. use Illuminate\Support\Facades\DB;
  7. use Illuminate\Support\Facades\Schema;
  8. class ThirdGameCommissionService
  9. {
  10. public const BASIS = 'game_gross_profit';
  11. public function amount(string $platform, string $grossProfit, array $rates): ?string
  12. {
  13. if (bccomp($grossProfit, '0', 10) === 0) return Money::zero();
  14. $ratio = $rates['ratios'][strtolower(trim($platform))] ?? null;
  15. return $ratio === null ? null : bcmul($grossProfit, $ratio, 10);
  16. }
  17. /** Finance reads original orders, independently of the reporting projection's sync schedule. */
  18. public function summary(?string $start, ?string $end): array
  19. {
  20. $known = Money::zero(); $people = []; $rates = null;
  21. $quality = ['missing_ratio' => 0, 'missing_time' => 0, 'missing_amount' => 0, 'foreign_currency' => 0];
  22. if (Schema::hasTable('third_game_orders')) {
  23. $query = DB::table('third_game_orders as o')->join('users as u', 'u.id', '=', 'o.user_id')
  24. ->where(function ($q) { $q->whereNull('u.from')->orWhere('u.from', '<>', 2); })
  25. ->where('o.status', 1)->select('o.*');
  26. $query->where('o.player_id', config('third_game.player_prefix', 'u') === 'tu' ? 'like' : 'not like', 'tu%');
  27. if ($start !== null && $end !== null) {
  28. $query->where(function ($q) use ($start, $end) {
  29. $q->whereBetween('o.last_update_time', [$start, $end])
  30. ->orWhere(function ($q) use ($end) { $q->whereNull('o.last_update_time')->where('o.bet_time', '<=', $end); });
  31. });
  32. }
  33. foreach ($query->orderBy('o.id')->cursor() as $row) {
  34. if ($row->last_update_time === null) { ++$quality['missing_time']; continue; }
  35. if (strtoupper(trim((string) $row->currency)) !== config('reporting.currency', 'CNY')) { ++$quality['foreign_currency']; continue; }
  36. if ($row->settled_amount === null) { ++$quality['missing_amount']; continue; }
  37. $gross = bcsub('0', (string) $row->settled_amount, 10);
  38. if (bccomp($gross, '0', 10) === 0) continue;
  39. $rates ??= $this->rates();
  40. $amount = $this->amount((string) $row->platform, $gross, $rates);
  41. if ($amount === null) { ++$quality['missing_ratio']; continue; }
  42. $known = bcadd($known, $amount, 10);
  43. if (bccomp($amount, '0', 10) !== 0) $people[$row->user_id] = true;
  44. }
  45. }
  46. $complete = array_sum($quality) === 0;
  47. return ['amount' => $complete ? $known : null, 'known_amount' => $known, 'people' => count($people),
  48. 'complete' => $complete, 'quality' => $quality, 'basis' => self::BASIS, 'estimated' => true,
  49. 'rate_date' => $rates['date'] ?? null, 'rates_fetched_at' => $rates['fetched_at'] ?? null];
  50. }
  51. /** One quota request per merchant/currency/Beijing day, including failed requests. */
  52. public function rates(): array
  53. {
  54. $now = now('Asia/Shanghai');
  55. $unavailable = ['date' => $now->toDateString(), 'fetched_at' => null, 'available' => false, 'ratios' => []];
  56. if (trim((string) config('third_game.sn')) === '' || trim((string) config('third_game.key')) === '') return $unavailable;
  57. $merchant = hash('sha256', json_encode([
  58. config('third_game.api_url'), config('third_game.sn'), config('third_game.key'), config('third_game.currency', 'CNY'),
  59. ]));
  60. $key = 'third_game:commission_rates:' . $merchant . ':' . $now->toDateString();
  61. $cached = Cache::get($key);
  62. if (is_array($cached)) return $cached;
  63. try {
  64. return Cache::lock($key . ':lock', max(30, (int) config('third_game.timeout', 10) + 10))->block(2, function () use ($key, $now, $unavailable) {
  65. $cached = Cache::get($key);
  66. if (is_array($cached)) return $cached;
  67. // Reserve the day before HTTP so concurrent requests and failures never retry in a loop.
  68. Cache::put($key, $unavailable, $now->copy()->endOfDay());
  69. $result = app(ThirdGameBalanceService::class)->quotaRatios();
  70. $snapshot = ['date' => $now->toDateString(), 'fetched_at' => now('Asia/Shanghai')->toDateTimeString(),
  71. 'available' => (bool) $result['ok'], 'ratios' => $result['ratios']];
  72. Cache::put($key, $snapshot, $now->copy()->endOfDay());
  73. return $snapshot;
  74. });
  75. } catch (LockTimeoutException $e) {
  76. return Cache::get($key, $unavailable);
  77. }
  78. }
  79. }