shouldResetTraffic()) { return false; } return $this->performReset($user, $triggerSource); } /** * Perform the traffic reset for a user. */ public function performReset(User $user, string $triggerSource = TrafficResetLog::SOURCE_MANUAL): bool { try { return DB::transaction(function () use ($user, $triggerSource) { $oldUpload = $user->u ?? 0; $oldDownload = $user->d ?? 0; $oldTotal = $oldUpload + $oldDownload; $nextResetTime = $this->calculateNextResetTime($user); $user->update([ 'u' => 0, 'd' => 0, 'last_reset_at' => time(), 'reset_count' => $user->reset_count + 1, 'next_reset_at' => $nextResetTime ? $nextResetTime->timestamp : null, ]); $this->recordResetLog($user, [ 'reset_type' => $this->getResetTypeFromPlan($user->plan), 'trigger_source' => $triggerSource, 'old_upload' => $oldUpload, 'old_download' => $oldDownload, 'old_total' => $oldTotal, 'new_upload' => 0, 'new_download' => 0, 'new_total' => 0, ]); $this->clearUserCache($user); HookManager::call('traffic.reset.after', $user); return true; }); } catch (\Exception $e) { Log::error(__('traffic_reset.reset_failed'), [ 'user_id' => $user->id, 'email' => $user->email, 'error' => $e->getMessage(), 'trigger_source' => $triggerSource, ]); return false; } } /** * Calculate the next traffic reset time for a user. */ public function calculateNextResetTime(User $user): ?Carbon { if ( !$user->plan || $user->plan->reset_traffic_method === Plan::RESET_TRAFFIC_NEVER || ($user->plan->reset_traffic_method === Plan::RESET_TRAFFIC_FOLLOW_SYSTEM && (int) admin_setting('reset_traffic_method', Plan::RESET_TRAFFIC_MONTHLY) === Plan::RESET_TRAFFIC_NEVER) || $user->expired_at === NULL ) { return null; } $resetMethod = $user->plan->reset_traffic_method; if ($resetMethod === Plan::RESET_TRAFFIC_FOLLOW_SYSTEM) { $resetMethod = (int) admin_setting('reset_traffic_method', Plan::RESET_TRAFFIC_MONTHLY); } $now = Carbon::now(config('app.timezone')); return match ($resetMethod) { Plan::RESET_TRAFFIC_FIRST_DAY_MONTH => $this->getNextMonthFirstDay($now), Plan::RESET_TRAFFIC_MONTHLY => $this->getNextMonthlyReset($user, $now), Plan::RESET_TRAFFIC_FIRST_DAY_YEAR => $this->getNextYearFirstDay($now), Plan::RESET_TRAFFIC_YEARLY => $this->getNextYearlyReset($user, $now), default => null, }; } /** * Get the first day of the next month. */ private function getNextMonthFirstDay(Carbon $from): Carbon { return $from->copy()->addMonth()->startOfMonth(); } /** * Get the next monthly reset time based on the user's expiration date. * * Logic: * 1. If the user has no expiration date, reset on the 1st of each month. * 2. If the user has an expiration date, use the day of that date as the monthly reset day. * 3. Prioritize the reset day in the current month if it has not passed yet. * 4. Handle cases where the day does not exist in a month (e.g., 31st in February). */ private function getNextMonthlyReset(User $user, Carbon $from): Carbon { $expiredAt = Carbon::createFromTimestamp($user->expired_at, config('app.timezone')); $resetDay = $expiredAt->day; $resetTime = [$expiredAt->hour, $expiredAt->minute, $expiredAt->second]; $currentMonthTarget = $from->copy()->day($resetDay)->setTime(...$resetTime); if ($currentMonthTarget->timestamp > $from->timestamp) { return $currentMonthTarget; } $nextMonthTarget = $from->copy()->startOfMonth()->addMonths(1)->day($resetDay)->setTime(...$resetTime); if ($nextMonthTarget->month !== ($from->month % 12) + 1) { $nextMonth = ($from->month % 12) + 1; $nextYear = $from->year + ($from->month === 12 ? 1 : 0); $lastDayOfNextMonth = Carbon::create($nextYear, $nextMonth, 1)->endOfMonth()->day; $targetDay = min($resetDay, $lastDayOfNextMonth); $nextMonthTarget = Carbon::create($nextYear, $nextMonth, $targetDay)->setTime(...$resetTime); } return $nextMonthTarget; } /** * Get the first day of the next year. */ private function getNextYearFirstDay(Carbon $from): Carbon { return $from->copy()->addYear()->startOfYear(); } /** * Get the next yearly reset time based on the user's expiration date. * * Logic: * 1. If the user has no expiration date, reset on January 1st of each year. * 2. If the user has an expiration date, use the month and day of that date as the yearly reset date. * 3. Prioritize the reset date in the current year if it has not passed yet. * 4. Handle the case of February 29th in a leap year. */ private function getNextYearlyReset(User $user, Carbon $from): Carbon { $expiredAt = Carbon::createFromTimestamp($user->expired_at, config('app.timezone')); $resetMonth = $expiredAt->month; $resetDay = $expiredAt->day; $resetTime = [$expiredAt->hour, $expiredAt->minute, $expiredAt->second]; $currentYearTarget = $from->copy()->month($resetMonth)->day($resetDay)->setTime(...$resetTime); if ($currentYearTarget->timestamp > $from->timestamp) { return $currentYearTarget; } $nextYearTarget = $from->copy()->startOfYear()->addYears(1)->month($resetMonth)->day($resetDay)->setTime(...$resetTime); if ($nextYearTarget->month !== $resetMonth) { $nextYear = $from->year + 1; $lastDayOfMonth = Carbon::create($nextYear, $resetMonth, 1)->endOfMonth()->day; $targetDay = min($resetDay, $lastDayOfMonth); $nextYearTarget = Carbon::create($nextYear, $resetMonth, $targetDay)->setTime(...$resetTime); } return $nextYearTarget; } /** * Record the traffic reset log. */ private function recordResetLog(User $user, array $data): void { TrafficResetLog::create([ 'user_id' => $user->id, 'reset_type' => $data['reset_type'], 'reset_time' => now(), 'old_upload' => $data['old_upload'], 'old_download' => $data['old_download'], 'old_total' => $data['old_total'], 'new_upload' => $data['new_upload'], 'new_download' => $data['new_download'], 'new_total' => $data['new_total'], 'trigger_source' => $data['trigger_source'], 'metadata' => $data['metadata'] ?? null, ]); } /** * Get the reset type from the user's plan. */ private function getResetTypeFromPlan(?Plan $plan): string { if (!$plan) { return TrafficResetLog::TYPE_MANUAL; } $resetMethod = $plan->reset_traffic_method; if ($resetMethod === Plan::RESET_TRAFFIC_FOLLOW_SYSTEM) { $resetMethod = (int) admin_setting('reset_traffic_method', Plan::RESET_TRAFFIC_MONTHLY); } return match ($resetMethod) { Plan::RESET_TRAFFIC_FIRST_DAY_MONTH => TrafficResetLog::TYPE_FIRST_DAY_MONTH, Plan::RESET_TRAFFIC_MONTHLY => TrafficResetLog::TYPE_MONTHLY, Plan::RESET_TRAFFIC_FIRST_DAY_YEAR => TrafficResetLog::TYPE_FIRST_DAY_YEAR, Plan::RESET_TRAFFIC_YEARLY => TrafficResetLog::TYPE_YEARLY, Plan::RESET_TRAFFIC_NEVER => TrafficResetLog::TYPE_MANUAL, default => TrafficResetLog::TYPE_MANUAL, }; } /** * Clear user-related cache. */ private function clearUserCache(User $user): void { $cacheKeys = [ "user_traffic_{$user->id}", "user_reset_status_{$user->id}", "user_subscription_{$user->token}", ]; foreach ($cacheKeys as $key) { Cache::forget($key); } } /** * Batch check and reset users. Processes all eligible users in batches. */ public function batchCheckReset(int $batchSize = 100, ?callable $progressCallback = null): array { $startTime = microtime(true); $totalResetCount = 0; $totalProcessedCount = 0; $batchNumber = 1; $errors = []; $lastProcessedId = 0; try { do { $users = User::where('next_reset_at', '<=', time()) ->whereNotNull('next_reset_at') ->where('id', '>', $lastProcessedId) ->where(function ($query) { $query->where('expired_at', '>', time()) ->orWhereNull('expired_at'); }) ->where('banned', 0) ->whereNotNull('plan_id') ->orderBy('id') ->limit($batchSize) ->get(); if ($users->isEmpty()) { break; } $batchResetCount = 0; if ($progressCallback) { $progressCallback([ 'batch_number' => $batchNumber, 'batch_size' => $users->count(), 'total_processed' => $totalProcessedCount, ]); } foreach ($users as $user) { try { if ($this->checkAndReset($user, TrafficResetLog::SOURCE_CRON)) { $batchResetCount++; $totalResetCount++; } $totalProcessedCount++; $lastProcessedId = $user->id; } catch (\Exception $e) { $error = [ 'user_id' => $user->id, 'email' => $user->email, 'error' => $e->getMessage(), 'batch' => $batchNumber, 'timestamp' => now()->toDateTimeString(), ]; $batchErrors[] = $error; $errors[] = $error; Log::error('User traffic reset failed', $error); $totalProcessedCount++; $lastProcessedId = $user->id; } } $batchNumber++; if ($batchNumber % 10 === 0) { gc_collect_cycles(); } if ($batchNumber % 5 === 0) { usleep(100000); } } while (true); } catch (\Exception $e) { Log::error('Batch traffic reset task failed with an exception', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'total_processed' => $totalProcessedCount, 'total_reset' => $totalResetCount, 'last_processed_id' => $lastProcessedId, ]); $errors[] = [ 'type' => 'system_error', 'error' => $e->getMessage(), 'batch' => $batchNumber, 'last_processed_id' => $lastProcessedId, 'timestamp' => now()->toDateTimeString(), ]; } $totalDuration = round(microtime(true) - $startTime, 2); $result = [ 'total_processed' => $totalProcessedCount, 'total_reset' => $totalResetCount, 'total_batches' => $batchNumber - 1, 'error_count' => count($errors), 'errors' => $errors, 'duration' => $totalDuration, 'batch_size' => $batchSize, 'last_processed_id' => $lastProcessedId, 'completed_at' => now()->toDateTimeString(), ]; return $result; } /** * Set the initial reset time for a new user. */ public function setInitialResetTime(User $user): void { if ($user->next_reset_at !== null) { return; } $nextResetTime = $this->calculateNextResetTime($user); if ($nextResetTime) { $user->update(['next_reset_at' => $nextResetTime->timestamp]); } } /** * Get the user's traffic reset history. */ public function getUserResetHistory(User $user, int $limit = 10): \Illuminate\Database\Eloquent\Collection { return $user->trafficResetLogs() ->orderBy('reset_time', 'desc') ->limit($limit) ->get(); } /** * Check if the user is eligible for traffic reset. */ public function canReset(User $user): bool { return $user->isActive() && $user->plan !== null; } /** * Manually reset a user's traffic (Admin function). */ public function manualReset(User $user, array $metadata = []): bool { if (!$this->canReset($user)) { return false; } return $this->performReset($user, TrafficResetLog::SOURCE_MANUAL); } }