RewardBaseSyncService.php 6.2 KB

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