first commit
This commit is contained in:
415
Xboard/app/Services/TrafficResetService.php
Normal file
415
Xboard/app/Services/TrafficResetService.php
Normal file
@@ -0,0 +1,415 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Plan;
|
||||
use App\Models\TrafficResetLog;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Services\Plugin\HookManager;
|
||||
|
||||
/**
|
||||
* Service for handling traffic reset.
|
||||
*/
|
||||
class TrafficResetService
|
||||
{
|
||||
/**
|
||||
* Check if a user's traffic should be reset and perform the reset.
|
||||
*/
|
||||
public function checkAndReset(User $user, string $triggerSource = TrafficResetLog::SOURCE_AUTO): bool
|
||||
{
|
||||
if (!$user->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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user