RewardBaseSyncService.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. <?php
  2. namespace App\Services;
  3. use App\Services\Sharing\EffectiveBet;
  4. use Carbon\Carbon;
  5. use Illuminate\Support\Facades\DB;
  6. use Illuminate\Support\Facades\Log;
  7. use InvalidArgumentException;
  8. use Throwable;
  9. /**
  10. * Keeps unpaid rebates (New York day) and backflow (Shanghai month) rows in step with share_bet_facts,
  11. * so admin lists show every game category. Paid rows are never touched; payout recomputes on its own.
  12. */
  13. class RewardBaseSyncService
  14. {
  15. private const CURSOR = 'reward_bases_cursor';
  16. public function run(?string $from = null, ?string $to = null): array
  17. {
  18. foreach ([$from, $to] as $date) {
  19. if ($date !== null && (!preg_match('/^\d{4}-\d{2}-\d{2}$/D', $date) || Carbon::parse($date)->toDateString() !== $date)) {
  20. throw new InvalidArgumentException('Dates must use Y-m-d');
  21. }
  22. }
  23. if (($from === null) !== ($to === null) || ($from !== null && $from > $to)) throw new InvalidArgumentException('Invalid date range');
  24. $result = ['rebates' => 0, 'backflow' => 0, 'failed' => 0];
  25. $startedAt = now()->setTimezone('Asia/Shanghai')->format('Y-m-d H:i:s');
  26. $query = DB::table('share_bet_facts')->select(['id', 'user_id', 'bet_at', 'bet_date']);
  27. if ($from !== null) {
  28. // A New York day spans two Shanghai dates.
  29. $query->whereBetween('bet_date', [$from, Carbon::parse($to)->addDay()->toDateString()]);
  30. } else {
  31. $cursor = DB::table('share_system_state')->where('key', self::CURSOR)->value('value');
  32. $since = $cursor ? EffectiveBet::time(json_decode($cursor, true, 512, JSON_THROW_ON_ERROR)['updated_at'])->subMinutes(5)
  33. : now()->subDays(2);
  34. $query->where('updated_at', '>=', $since->setTimezone('Asia/Shanghai')->format('Y-m-d H:i:s'));
  35. }
  36. $days = $months = [];
  37. $query->orderBy('id')->chunkById(500, function ($facts) use (&$days, &$months, $from, $to) {
  38. foreach ($facts as $fact) {
  39. $day = Carbon::parse($fact->bet_at, 'Asia/Shanghai')->setTimezone('America/New_York')->toDateString();
  40. if ($from === null || ($day >= $from && $day <= $to)) $days[$fact->user_id . '|' . $day] = [$fact->user_id, $day];
  41. $months[$fact->user_id . '|' . substr((string) $fact->bet_date, 0, 7)] = [$fact->user_id, substr((string) $fact->bet_date, 0, 7)];
  42. }
  43. });
  44. $users = [];
  45. foreach (['rebate' => $days, 'backflow' => $months] as $type => $keys) {
  46. foreach ($keys as [$userId, $period]) {
  47. try {
  48. $user = $users[$userId] ??= DB::table('users')->where('id', $userId)->first(['id', 'member_id', 'level', 'username', 'first_name']);
  49. if (!$user) continue;
  50. if ($type === 'rebate' ? $this->rebate($user, $period) : $this->backflow($user, $period)) {
  51. ++$result[$type === 'rebate' ? 'rebates' : 'backflow'];
  52. }
  53. } catch (Throwable $e) {
  54. ++$result['failed'];
  55. Log::error('reward_base_sync_failed', ['type' => $type, 'user_id' => $userId, 'period' => $period, 'exception' => $e]);
  56. }
  57. }
  58. }
  59. if ($from === null && $result['failed'] === 0) {
  60. DB::table('share_system_state')->updateOrInsert(['key' => self::CURSOR],
  61. ['value' => json_encode(['updated_at' => $startedAt]), 'updated_at' => $startedAt, 'created_at' => $startedAt]);
  62. }
  63. return $result;
  64. }
  65. private function rebate(object $user, string $date): bool
  66. {
  67. return DB::transaction(function () use ($user, $date) {
  68. $row = DB::table('rebates')->where('member_id', $user->member_id)->where('date', $date)->lockForUpdate()->first();
  69. if ($row && (int) $row->status !== 0) return false;
  70. $reward = RewardBasisService::rebateReward(RewardBasisService::rebateBasis($user, $date), (int) ($user->level ?? 0));
  71. if (!$reward['has_facts'] && !$row) return false;
  72. $values = ['betting_amount' => $reward['betting_amount'], 'effective_betting_amount' => $reward['effective_amount'],
  73. 'profit' => $reward['profit'], 'amount' => $reward['amount'], 'need_flow' => $reward['need_flow'],
  74. 'details' => json_encode($reward['details'], JSON_UNESCAPED_UNICODE),
  75. 'username' => (string) ($user->username ?? ''), 'first_name' => (string) ($user->first_name ?? ''),
  76. 'updated_at' => now()];
  77. if ($row) {
  78. // Rows without facts are legacy PC28 days; their stored basis stays the payout basis.
  79. if (!$reward['has_facts']) return false;
  80. return DB::table('rebates')->where('id', $row->id)->where('status', 0)->update($values) > 0;
  81. }
  82. DB::table('rebates')->insert($values + ['member_id' => $user->member_id, 'date' => $date, 'status' => 0, 'created_at' => now()]);
  83. return true;
  84. }, 3);
  85. }
  86. private function backflow(object $user, string $month): bool
  87. {
  88. return DB::transaction(function () use ($user, $month) {
  89. $row = DB::table('backflow')->where('member_id', $user->member_id)->where('date', $month)->lockForUpdate()->first();
  90. if ($row && (int) $row->status !== 0) return false;
  91. $reward = RewardBasisService::backflowReward(RewardBasisService::backflowBasis($user, $month));
  92. if (!$reward['has_facts'] && !$row) return false;
  93. $values = ['loss_amount' => $reward['loss_amount'], 'amount' => $reward['amount'], 'need_flow' => $reward['need_flow'],
  94. 'details' => json_encode($reward['details'], JSON_UNESCAPED_UNICODE), 'updated_at' => now()];
  95. if ($row) return DB::table('backflow')->where('id', $row->id)->where('status', 0)->update($values) > 0;
  96. DB::table('backflow')->insert($values + ['member_id' => $user->member_id, 'date' => $month, 'status' => 0, 'created_at' => now()]);
  97. return true;
  98. }, 3);
  99. }
  100. }