mirror of
https://github.com/lkddi/Xboard.git
synced 2026-04-14 11:20:53 +08:00
fix: resolve traffic reset time generation and refactor reset logic
- Add fix-null mode to ResetTraffic command - Refactor reset logic for better separation of concerns - Update migration to reuse fix functionality
This commit is contained in:
@@ -3,70 +3,36 @@
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\TrafficResetLog;
|
||||
use App\Services\TrafficResetService;
|
||||
use App\Utils\Helper;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ResetTraffic extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*/
|
||||
protected $signature = 'reset:traffic {--batch-size=100 : 分批处理的批次大小} {--dry-run : 预演模式,不实际执行重置} {--max-time=300 : 最大执行时间(秒)}';
|
||||
protected $signature = 'reset:traffic {--fix-null : 修正模式,重新计算next_reset_at为null的用户}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*/
|
||||
protected $description = '流量重置 - 分批处理所有需要重置的用户';
|
||||
protected $description = '流量重置 - 处理所有需要重置的用户';
|
||||
|
||||
/**
|
||||
* 流量重置服务
|
||||
*/
|
||||
private TrafficResetService $trafficResetService;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*/
|
||||
public function __construct(TrafficResetService $trafficResetService)
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TrafficResetService $trafficResetService
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->trafficResetService = $trafficResetService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$batchSize = (int) $this->option('batch-size');
|
||||
$dryRun = $this->option('dry-run');
|
||||
$maxTime = (int) $this->option('max-time');
|
||||
$fixNull = $this->option('fix-null');
|
||||
|
||||
$this->info('🚀 开始执行流量重置任务...');
|
||||
$this->info("批次大小: {$batchSize} 用户/批");
|
||||
$this->info("最大执行时间: {$maxTime} 秒");
|
||||
|
||||
if ($dryRun) {
|
||||
$this->warn('⚠️ 预演模式 - 不会实际执行重置操作');
|
||||
if ($fixNull) {
|
||||
$this->warn('🔧 修正模式 - 将重新计算next_reset_at为null的用户');
|
||||
}
|
||||
|
||||
// 设置最大执行时间
|
||||
set_time_limit($maxTime);
|
||||
|
||||
try {
|
||||
if ($dryRun) {
|
||||
$result = $this->performDryRun($batchSize);
|
||||
} else {
|
||||
// 使用游标分页和进度回调
|
||||
$result = $this->trafficResetService->batchCheckReset($batchSize, function ($progress) {
|
||||
$this->info("📦 处理第 {$progress['batch_number']} 批 ({$progress['batch_size']} 用户) - 已处理: {$progress['total_processed']}");
|
||||
});
|
||||
}
|
||||
|
||||
$this->displayResults($result, $dryRun);
|
||||
|
||||
$result = $fixNull ? $this->performFix() : $this->performReset();
|
||||
$this->displayResults($result, $fixNull);
|
||||
return self::SUCCESS;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
@@ -75,131 +41,185 @@ class ResetTraffic extends Command
|
||||
Log::error('流量重置命令执行失败', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'options' => [
|
||||
'batch_size' => $batchSize,
|
||||
'dry_run' => $dryRun,
|
||||
'max_time' => $maxTime,
|
||||
],
|
||||
]);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示执行结果
|
||||
*/
|
||||
private function displayResults(array $result, bool $dryRun): void
|
||||
private function displayResults(array $result, bool $fixNull): void
|
||||
{
|
||||
$this->info("✅ 任务完成!");
|
||||
$this->line('');
|
||||
$this->info("✅ 任务完成!\n");
|
||||
|
||||
if ($dryRun) {
|
||||
$this->info("📊 预演结果统计:");
|
||||
$this->info("📋 待处理用户数: {$result['total_found']}");
|
||||
$this->info("⏱️ 预计处理时间: ~{$result['estimated_duration']} 秒");
|
||||
$this->info("🗂️ 预计批次数: {$result['estimated_batches']}");
|
||||
if ($fixNull) {
|
||||
$this->displayFixResults($result);
|
||||
} else {
|
||||
$this->info("📊 执行结果统计:");
|
||||
$this->info("👥 处理用户总数: {$result['total_processed']}");
|
||||
$this->info("🔄 重置用户数量: {$result['total_reset']}");
|
||||
$this->info("📦 处理批次数量: {$result['total_batches']}");
|
||||
$this->info("⏱️ 总执行时间: {$result['duration']} 秒");
|
||||
|
||||
if ($result['error_count'] > 0) {
|
||||
$this->warn("⚠️ 错误数量: {$result['error_count']}");
|
||||
$this->warn("详细错误信息请查看日志");
|
||||
} else {
|
||||
$this->info("✨ 无错误发生");
|
||||
}
|
||||
|
||||
// 显示性能指标
|
||||
if ($result['total_processed'] > 0) {
|
||||
$avgTime = round($result['duration'] / $result['total_processed'], 4);
|
||||
$this->info("⚡ 平均处理速度: {$avgTime} 秒/用户");
|
||||
}
|
||||
$this->displayExecutionResults($result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行预演模式
|
||||
*/
|
||||
private function performDryRun(int $batchSize): array
|
||||
private function displayFixResults(array $result): void
|
||||
{
|
||||
$this->info("🔍 扫描需要重置的用户...");
|
||||
$this->info("📊 修正结果统计:");
|
||||
$this->info("🔍 发现用户总数: {$result['total_found']}");
|
||||
$this->info("✅ 成功修正数量: {$result['total_fixed']}");
|
||||
$this->info("⏱️ 总执行时间: {$result['duration']} 秒");
|
||||
|
||||
$totalUsers = User::where('next_reset_at', '<=', time())
|
||||
if ($result['error_count'] > 0) {
|
||||
$this->warn("⚠️ 错误数量: {$result['error_count']}");
|
||||
$this->warn("详细错误信息请查看日志");
|
||||
} else {
|
||||
$this->info("✨ 无错误发生");
|
||||
}
|
||||
|
||||
if ($result['total_found'] > 0) {
|
||||
$avgTime = round($result['duration'] / $result['total_found'], 4);
|
||||
$this->info("⚡ 平均处理速度: {$avgTime} 秒/用户");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function displayExecutionResults(array $result): void
|
||||
{
|
||||
$this->info("📊 执行结果统计:");
|
||||
$this->info("👥 处理用户总数: {$result['total_processed']}");
|
||||
$this->info("🔄 重置用户数量: {$result['total_reset']}");
|
||||
$this->info("⏱️ 总执行时间: {$result['duration']} 秒");
|
||||
|
||||
if ($result['error_count'] > 0) {
|
||||
$this->warn("⚠️ 错误数量: {$result['error_count']}");
|
||||
$this->warn("详细错误信息请查看日志");
|
||||
} else {
|
||||
$this->info("✨ 无错误发生");
|
||||
}
|
||||
|
||||
if ($result['total_processed'] > 0) {
|
||||
$avgTime = round($result['duration'] / $result['total_processed'], 4);
|
||||
$this->info("⚡ 平均处理速度: {$avgTime} 秒/用户");
|
||||
}
|
||||
}
|
||||
|
||||
private function performReset(): array
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$totalResetCount = 0;
|
||||
$errors = [];
|
||||
|
||||
$users = $this->getResetQuery()->get();
|
||||
|
||||
if ($users->isEmpty()) {
|
||||
$this->info("😴 当前没有需要重置的用户");
|
||||
return [
|
||||
'total_processed' => 0,
|
||||
'total_reset' => 0,
|
||||
'error_count' => 0,
|
||||
'duration' => round(microtime(true) - $startTime, 2),
|
||||
];
|
||||
}
|
||||
|
||||
$this->info("找到 {$users->count()} 个需要重置的用户");
|
||||
|
||||
foreach ($users as $user) {
|
||||
try {
|
||||
$totalResetCount += (int) $this->trafficResetService->checkAndReset($user, TrafficResetLog::SOURCE_CRON);
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = [
|
||||
'user_id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
Log::error('用户流量重置失败', [
|
||||
'user_id' => $user->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'total_processed' => $users->count(),
|
||||
'total_reset' => $totalResetCount,
|
||||
'error_count' => count($errors),
|
||||
'duration' => round(microtime(true) - $startTime, 2),
|
||||
];
|
||||
}
|
||||
|
||||
private function performFix(): array
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$nullUsers = $this->getNullResetTimeUsers();
|
||||
|
||||
if ($nullUsers->isEmpty()) {
|
||||
$this->info("✅ 没有发现next_reset_at为null的用户");
|
||||
return [
|
||||
'total_found' => 0,
|
||||
'total_fixed' => 0,
|
||||
'error_count' => 0,
|
||||
'duration' => round(microtime(true) - $startTime, 2),
|
||||
];
|
||||
}
|
||||
|
||||
$this->info("🔧 发现 {$nullUsers->count()} 个next_reset_at为null的用户,开始修正...");
|
||||
|
||||
$fixedCount = 0;
|
||||
$errors = [];
|
||||
|
||||
foreach ($nullUsers as $user) {
|
||||
try {
|
||||
$nextResetTime = $this->trafficResetService->calculateNextResetTime($user);
|
||||
if ($nextResetTime) {
|
||||
$user->next_reset_at = $nextResetTime->timestamp;
|
||||
$user->save();
|
||||
$fixedCount++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = [
|
||||
'user_id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
Log::error('修正用户next_reset_at失败', [
|
||||
'user_id' => $user->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'total_found' => $nullUsers->count(),
|
||||
'total_fixed' => $fixedCount,
|
||||
'error_count' => count($errors),
|
||||
'duration' => round(microtime(true) - $startTime, 2),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function getResetQuery()
|
||||
{
|
||||
return User::where('next_reset_at', '<=', time())
|
||||
->whereNotNull('next_reset_at')
|
||||
->where(function ($query) {
|
||||
$query->where('expired_at', '>', time())
|
||||
->orWhereNull('expired_at');
|
||||
})
|
||||
->where('banned', 0)
|
||||
->whereNotNull('plan_id')
|
||||
->count();
|
||||
|
||||
if ($totalUsers === 0) {
|
||||
$this->info("😴 当前没有需要重置的用户");
|
||||
return [
|
||||
'total_found' => 0,
|
||||
'estimated_duration' => 0,
|
||||
'estimated_batches' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$this->info("找到 {$totalUsers} 个需要重置的用户");
|
||||
|
||||
// 预计批次数
|
||||
$estimatedBatches = ceil($totalUsers / $batchSize);
|
||||
|
||||
// 预计执行时间(基于经验值:每个用户平均0.1秒)
|
||||
$estimatedDuration = round($totalUsers * 0.1, 1);
|
||||
|
||||
$this->info("将分 {$estimatedBatches} 个批次处理(每批 {$batchSize} 用户)");
|
||||
|
||||
// 显示前几个用户的详情作为示例
|
||||
if ($this->option('verbose') || $totalUsers <= 20) {
|
||||
$sampleUsers = User::where('next_reset_at', '<=', time())
|
||||
->whereNotNull('next_reset_at')
|
||||
->where(function ($query) {
|
||||
$query->where('expired_at', '>', time())
|
||||
->orWhereNull('expired_at');
|
||||
})
|
||||
->where('banned', 0)
|
||||
->whereNotNull('plan_id')
|
||||
->with('plan')
|
||||
->limit(min(20, $totalUsers))
|
||||
->get();
|
||||
|
||||
$table = [];
|
||||
foreach ($sampleUsers as $user) {
|
||||
$table[] = [
|
||||
'ID' => $user->id,
|
||||
'邮箱' => substr($user->email, 0, 20) . (strlen($user->email) > 20 ? '...' : ''),
|
||||
'套餐' => $user->plan->name ?? 'N/A',
|
||||
'下次重置' => Carbon::createFromTimestamp($user->next_reset_at)->format('Y-m-d H:i:s'),
|
||||
'当前流量' => Helper::trafficConvert(($user->u ?? 0) + ($user->d ?? 0)),
|
||||
'重置次数' => $user->reset_count,
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($table)) {
|
||||
$this->info("📋 示例用户列表" . ($totalUsers > 20 ? "(显示前20个):" : ":"));
|
||||
$this->table([
|
||||
'ID',
|
||||
'邮箱',
|
||||
'套餐',
|
||||
'下次重置',
|
||||
'当前流量',
|
||||
'重置次数'
|
||||
], $table);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'total_found' => $totalUsers,
|
||||
'estimated_duration' => $estimatedDuration,
|
||||
'estimated_batches' => $estimatedBatches,
|
||||
];
|
||||
->whereNotNull('plan_id');
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function getNullResetTimeUsers()
|
||||
{
|
||||
return User::whereNull('next_reset_at')
|
||||
->whereNotNull('plan_id')
|
||||
->where(function ($query) {
|
||||
$query->where('expired_at', '>', time())
|
||||
->orWhereNull('expired_at');
|
||||
})
|
||||
->where('banned', 0)
|
||||
->with('plan:id,name,reset_traffic_method')
|
||||
->get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -45,6 +45,7 @@ class XboardUpdate extends Command
|
||||
Artisan::call("migrate");
|
||||
$this->info(Artisan::output());
|
||||
Artisan::call('horizon:terminate');
|
||||
Artisan::call('reset:traffic', ['--fix-null' => true]);
|
||||
$updateService = new UpdateService();
|
||||
$updateService->updateVersionCache();
|
||||
$themeService = app(ThemeService::class);
|
||||
|
||||
@@ -138,7 +138,10 @@ class UserController extends Controller
|
||||
'd',
|
||||
'transfer_enable',
|
||||
'email',
|
||||
'uuid'
|
||||
'uuid',
|
||||
'device_limit',
|
||||
'speed_limit',
|
||||
'next_reset_at'
|
||||
])
|
||||
->first();
|
||||
if (!$user) {
|
||||
|
||||
@@ -4,90 +4,23 @@ namespace App\Observers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\TrafficResetService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 用户模型观察者
|
||||
* 主要用于监听用户到期时间变化,自动更新流量重置时间
|
||||
*/
|
||||
class UserObserver
|
||||
{
|
||||
/**
|
||||
* 流量重置服务
|
||||
*/
|
||||
private TrafficResetService $trafficResetService;
|
||||
|
||||
public function __construct(TrafficResetService $trafficResetService)
|
||||
{
|
||||
$this->trafficResetService = $trafficResetService;
|
||||
public function __construct(
|
||||
private readonly TrafficResetService $trafficResetService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听用户更新事件
|
||||
* 当 expired_at 或 plan_id 发生变化时,重新计算下次重置时间
|
||||
*/
|
||||
public function updating(User $user): void
|
||||
public function updated(User $user): void
|
||||
{
|
||||
// 检查是否有相关字段发生变化
|
||||
$relevantFields = ['expired_at', 'plan_id'];
|
||||
$hasRelevantChanges = false;
|
||||
|
||||
foreach ($relevantFields as $field) {
|
||||
if ($user->isDirty($field)) {
|
||||
$hasRelevantChanges = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$hasRelevantChanges) {
|
||||
return; // 没有相关字段变化,直接返回
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$user->plan_id) {
|
||||
$user->next_reset_at = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 重新计算下次重置时间
|
||||
$nextResetTime = $this->trafficResetService->calculateNextResetTime($user);
|
||||
|
||||
if ($nextResetTime) {
|
||||
$user->setAttribute('next_reset_at', $nextResetTime->timestamp);
|
||||
} else {
|
||||
// 如果计算结果为空,清除重置时间
|
||||
$user->setAttribute('next_reset_at', null);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('更新用户流量重置时间失败', [
|
||||
'user_id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
// 不阻止用户更新操作,只记录错误
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听用户创建事件
|
||||
* 为新用户设置初始的重置时间
|
||||
*/
|
||||
public function created(User $user): void
|
||||
{
|
||||
// 如果用户有套餐和到期时间,设置初始重置时间
|
||||
if ($user->plan_id && $user->expired_at) {
|
||||
try {
|
||||
$this->trafficResetService->setInitialResetTime($user);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('设置新用户流量重置时间失败', [
|
||||
'user_id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
if ($user->isDirty(['plan_id', 'expired_at'])) {
|
||||
$user->refresh();
|
||||
User::withoutEvents(function () use ($user) {
|
||||
$nextResetTime = $this->trafficResetService->calculateNextResetTime($user);
|
||||
$user->next_reset_at = $nextResetTime?->timestamp;
|
||||
$user->save();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ namespace App\Providers;
|
||||
use App\Models\User;
|
||||
use App\Observers\UserObserver;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -24,7 +23,6 @@ class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
// 注册用户模型观察者
|
||||
User::observe(UserObserver::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use App\Models\User;
|
||||
use App\Services\TrafficResetService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class AddTrafficResetFieldsToUsers extends Migration
|
||||
{
|
||||
@@ -25,66 +22,8 @@ class AddTrafficResetFieldsToUsers extends Migration
|
||||
});
|
||||
}
|
||||
|
||||
// 为现有用户设置初始重置时间
|
||||
$this->migrateExistingUsers();
|
||||
}
|
||||
|
||||
/**
|
||||
* 为现有用户迁移流量重置数据
|
||||
*/
|
||||
private function migrateExistingUsers(): void
|
||||
{
|
||||
try {
|
||||
// 获取所有需要迁移的用户ID,避免查询条件变化
|
||||
$userIds = User::whereNotNull('plan_id')
|
||||
->where('banned', 0)
|
||||
->whereNull('next_reset_at')
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
|
||||
$totalUsers = count($userIds);
|
||||
if ($totalUsers === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo "开始迁移 {$totalUsers} 个用户的流量重置数据...\n";
|
||||
$trafficResetService = app(TrafficResetService::class);
|
||||
$processedCount = 0;
|
||||
$failedCount = 0;
|
||||
|
||||
// 分批处理用户ID
|
||||
$chunks = array_chunk($userIds, 200);
|
||||
|
||||
foreach ($chunks as $chunkIds) {
|
||||
$users = User::whereIn('id', $chunkIds)
|
||||
->with('plan:id,reset_traffic_method')
|
||||
->get();
|
||||
|
||||
foreach ($users as $user) {
|
||||
try {
|
||||
$trafficResetService->setInitialResetTime($user);
|
||||
$processedCount++;
|
||||
} catch (\Exception $e) {
|
||||
$failedCount++;
|
||||
Log::error('迁移用户流量重置时间失败', [
|
||||
'user_id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
// 每 100 个用户显示一次进度
|
||||
if (($processedCount + $failedCount) % 100 === 0 || ($processedCount + $failedCount) === $totalUsers) {
|
||||
$currentTotal = $processedCount + $failedCount;
|
||||
$percentage = round(($currentTotal / $totalUsers) * 100, 1);
|
||||
echo "进度: {$currentTotal}/{$totalUsers} ({$percentage}%) [成功: {$processedCount}, 失败: {$failedCount}]\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo "迁移完成!总计 {$totalUsers} 个用户,成功: {$processedCount},失败: {$failedCount}\n";
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
// Set initial reset time for existing users
|
||||
Artisan::call('reset:traffic', ['--fix-null' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user