toDateString() !== $date)) { throw new InvalidArgumentException('Dates must use Y-m-d'); } } if (($from === null) !== ($to === null) || ($from !== null && $from > $to)) throw new InvalidArgumentException('Invalid date range'); $result = ['rebates' => 0, 'backflow' => 0, 'failed' => 0]; $startedAt = now()->setTimezone('Asia/Shanghai')->format('Y-m-d H:i:s'); $query = DB::table('share_bet_facts')->select(['id', 'user_id', 'bet_at', 'bet_date']); if ($from !== null) { // A New York day spans two Shanghai dates. $query->whereBetween('bet_date', [$from, Carbon::parse($to)->addDay()->toDateString()]); } else { $cursor = DB::table('share_system_state')->where('key', self::CURSOR)->value('value'); $since = $cursor ? EffectiveBet::time(json_decode($cursor, true, 512, JSON_THROW_ON_ERROR)['updated_at'])->subMinutes(5) : now()->subDays(2); $query->where('updated_at', '>=', $since->setTimezone('Asia/Shanghai')->format('Y-m-d H:i:s')); } $days = $months = []; $query->orderBy('id')->chunkById(500, function ($facts) use (&$days, &$months, $from, $to) { foreach ($facts as $fact) { $day = Carbon::parse($fact->bet_at, 'Asia/Shanghai')->setTimezone('America/New_York')->toDateString(); if ($from === null || ($day >= $from && $day <= $to)) $days[$fact->user_id . '|' . $day] = [$fact->user_id, $day]; $months[$fact->user_id . '|' . substr((string) $fact->bet_date, 0, 7)] = [$fact->user_id, substr((string) $fact->bet_date, 0, 7)]; } }); $users = []; foreach (['rebate' => $days, 'backflow' => $months] as $type => $keys) { foreach ($keys as [$userId, $period]) { try { $user = $users[$userId] ??= DB::table('users')->where('id', $userId)->first(['id', 'member_id', 'level', 'username', 'first_name']); if (!$user) continue; if ($type === 'rebate' ? $this->rebate($user, $period) : $this->backflow($user, $period)) { ++$result[$type === 'rebate' ? 'rebates' : 'backflow']; } } catch (Throwable $e) { ++$result['failed']; Log::error('reward_base_sync_failed', ['type' => $type, 'user_id' => $userId, 'period' => $period, 'exception' => $e]); } } } if ($from === null && $result['failed'] === 0) { DB::table('share_system_state')->updateOrInsert(['key' => self::CURSOR], ['value' => json_encode(['updated_at' => $startedAt]), 'updated_at' => $startedAt, 'created_at' => $startedAt]); } return $result; } private function rebate(object $user, string $date): bool { return DB::transaction(function () use ($user, $date) { $row = DB::table('rebates')->where('member_id', $user->member_id)->where('date', $date)->lockForUpdate()->first(); if ($row && (int) $row->status !== 0) return false; $reward = RewardBasisService::rebateReward(RewardBasisService::rebateBasis($user, $date), (int) ($user->level ?? 0)); if (!$reward['has_facts'] && !$row) return false; $values = ['betting_amount' => $reward['betting_amount'], 'effective_betting_amount' => $reward['effective_amount'], 'profit' => $reward['profit'], 'amount' => $reward['amount'], 'need_flow' => $reward['need_flow'], 'details' => json_encode($reward['details'], JSON_UNESCAPED_UNICODE), 'username' => (string) ($user->username ?? ''), 'first_name' => (string) ($user->first_name ?? ''), 'updated_at' => now()]; if ($row) { // Rows without facts are legacy PC28 days; their stored basis stays the payout basis. if (!$reward['has_facts']) return false; return DB::table('rebates')->where('id', $row->id)->where('status', 0)->update($values) > 0; } DB::table('rebates')->insert($values + ['member_id' => $user->member_id, 'date' => $date, 'status' => 0, 'created_at' => now()]); return true; }, 3); } private function backflow(object $user, string $month): bool { return DB::transaction(function () use ($user, $month) { $row = DB::table('backflow')->where('member_id', $user->member_id)->where('date', $month)->lockForUpdate()->first(); if ($row && (int) $row->status !== 0) return false; $reward = RewardBasisService::backflowReward(RewardBasisService::backflowBasis($user, $month)); if (!$reward['has_facts'] && !$row) return false; $values = ['loss_amount' => $reward['loss_amount'], 'amount' => $reward['amount'], 'need_flow' => $reward['need_flow'], 'details' => json_encode($reward['details'], JSON_UNESCAPED_UNICODE), 'updated_at' => now()]; if ($row) return DB::table('backflow')->where('id', $row->id)->where('status', 0)->update($values) > 0; DB::table('backflow')->insert($values + ['member_id' => $user->member_id, 'date' => $month, 'status' => 0, 'created_at' => now()]); return true; }, 3); } }