feat: enhance plan validation, traffic system and email verification

- feat: add plan price validation
- feat: make traffic packages stackable
- feat: add commission and invite info to admin order details
- feat: apply email whitelist to verification code API
- fix: subscription link copy compatibility for non-HTTPS
- fix: resolve route editing 500 error in certain cases
- refactor: restructure traffic reset logic
This commit is contained in:
xboard
2025-06-22 01:18:38 +08:00
parent 7bab761db6
commit 4fe2f35183
34 changed files with 2176 additions and 539 deletions
+178 -149
View File
@@ -2,175 +2,204 @@
namespace App\Console\Commands;
use App\Models\Plan;
use App\Services\TrafficResetService;
use App\Utils\Helper;
use Illuminate\Console\Command;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class ResetTraffic extends Command
{
/**
* @var Builder
* The name and signature of the console command.
*/
protected $builder;
protected $signature = 'reset:traffic {--batch-size=100 : 分批处理的批次大小} {--dry-run : 预演模式,不实际执行重置} {--max-time=300 : 最大执行时间(秒)}';
/**
* @var string
* The console command description.
*/
protected $signature = 'reset:traffic';
protected $description = '流量重置 - 分批处理所有需要重置的用户';
/**
* @var string
* 流量重置服务
*/
protected $description = '流量清空';
private TrafficResetService $trafficResetService;
public function __construct()
/**
* Create a new command instance.
*/
public function __construct(TrafficResetService $trafficResetService)
{
parent::__construct();
$this->builder = User::where('expired_at', '!=', NULL)
->where('expired_at', '>', time());
$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');
$this->info('🚀 开始执行流量重置任务...');
$this->info("批次大小: {$batchSize} 用户/批");
$this->info("最大执行时间: {$maxTime}");
if ($dryRun) {
$this->warn('⚠️ 预演模式 - 不会实际执行重置操作');
}
/**
* 执行流量重置命令
*/
public function handle()
{
ini_set('memory_limit', -1);
// 设置最大执行时间
set_time_limit($maxTime);
// 按重置方法分组查询所有套餐
$resetMethods = Plan::select(
DB::raw("GROUP_CONCAT(`id`) as plan_ids"),
DB::raw("reset_traffic_method as method")
)
->groupBy('reset_traffic_method')
->get()
->toArray();
$startTime = microtime(true);
// 使用闭包直接引用方法
$resetHandlers = [
Plan::RESET_TRAFFIC_FIRST_DAY_MONTH => fn($builder) => $this->resetByMonthFirstDay($builder),
Plan::RESET_TRAFFIC_MONTHLY => fn($builder) => $this->resetByExpireDay($builder),
Plan::RESET_TRAFFIC_NEVER => null,
Plan::RESET_TRAFFIC_FIRST_DAY_YEAR => fn($builder) => $this->resetByYearFirstDay($builder),
Plan::RESET_TRAFFIC_YEARLY => fn($builder) => $this->resetByExpireYear($builder),
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);
return self::SUCCESS;
} catch (\Exception $e) {
$this->error("❌ 任务执行失败: {$e->getMessage()}");
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
{
$this->info("✅ 任务完成!");
$this->line('');
if ($dryRun) {
$this->info("📊 预演结果统计:");
$this->info("📋 待处理用户数: {$result['total_found']}");
$this->info("⏱️ 预计处理时间: ~{$result['estimated_duration']}");
$this->info("🗂️ 预计批次数: {$result['estimated_batches']}");
} 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} 秒/用户");
}
}
}
/**
* 执行预演模式
*/
private function performDryRun(int $batchSize): array
{
$this->info("🔍 扫描需要重置的用户...");
$totalUsers = \App\Models\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 = \App\Models\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',
'下次重置' => $user->next_reset_at->format('m-d H:i'),
'当前流量' => Helper::trafficConvert(($user->u ?? 0) + ($user->d ?? 0)),
'重置次数' => $user->reset_count,
];
}
// 处理每种重置方法
foreach ($resetMethods as $resetMethod) {
$planIds = explode(',', $resetMethod['plan_ids']);
// 获取重置方法
$method = $resetMethod['method'];
if ($method === NULL) {
$method = (int) admin_setting('reset_traffic_method', 0);
}
// 跳过不重置的方法
if ($method === 2) {
continue;
}
// 获取该方法的处理器
$handler = $resetHandlers[$method] ?? null;
if (!$handler) {
continue;
}
// 创建查询构建器并执行重置
$userQuery = (clone $this->builder)->whereIn('plan_id', $planIds);
$handler($userQuery);
}
if (!empty($table)) {
$this->info("📋 示例用户列表" . ($totalUsers > 20 ? "(显示前20个):" : ""));
$this->table([
'ID',
'邮箱',
'套餐',
'下次重置',
'当前流量',
'重置次数'
], $table);
}
}
/**
* 按用户年度到期日重置流量
*/
private function resetByExpireYear(Builder $builder): void
{
$today = date('m-d');
$this->resetUsersByDateCondition($builder, function ($user) use ($today) {
return date('m-d', $user->expired_at) === $today;
});
}
/**
* 按新年第一天重置流量
*/
private function resetByYearFirstDay(Builder $builder): void
{
$isNewYear = date('md') === '0101';
if (!$isNewYear) {
return;
}
$this->resetAllUsers($builder);
}
/**
* 按月初第一天重置流量
*/
private function resetByMonthFirstDay(Builder $builder): void
{
$isFirstDayOfMonth = date('d') === '01';
if (!$isFirstDayOfMonth) {
return;
}
$this->resetAllUsers($builder);
}
/**
* 按用户到期日重置流量
*/
private function resetByExpireDay(Builder $builder): void
{
$today = date('d');
$lastDay = date('d', strtotime('last day of +0 months'));
$this->resetUsersByDateCondition($builder, function ($user) use ($today, $lastDay) {
$expireDay = date('d', $user->expired_at);
return $expireDay === $today || ($today === $lastDay && $expireDay >= $today);
});
}
/**
* 重置所有符合条件的用户流量
*/
private function resetAllUsers(Builder $builder): void
{
$this->resetUsersByDateCondition($builder, function () {
return true;
});
}
/**
* 根据日期条件重置用户流量
* @param Builder $builder 用户查询构建器
* @param callable $condition 日期条件回调
*/
private function resetUsersByDateCondition(Builder $builder, callable $condition): void
{
/** @var \App\Models\User[] $users */
$users = $builder->with('plan')->get();
$usersToUpdate = [];
foreach ($users as $user) {
if ($condition($user)) {
$usersToUpdate[] = [
'id' => $user->id,
'transfer_enable' => $user->plan->transfer_enable
];
}
}
foreach ($usersToUpdate as $userData) {
User::where('id', $userData['id'])->update([
'transfer_enable' => (intval($userData['transfer_enable']) * 1073741824),
'u' => 0,
'd' => 0
]);
}
}
}
return [
'total_found' => $totalUsers,
'estimated_duration' => $estimatedDuration,
'estimated_batches' => $estimatedBatches,
];
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ class Kernel extends ConsoleKernel
$schedule->command('check:commission')->everyMinute()->onOneServer();
$schedule->command('check:ticket')->everyMinute()->onOneServer();
// reset
$schedule->command('reset:traffic')->daily()->onOneServer();
$schedule->command('reset:traffic')->everyMinute()->onOneServer();
$schedule->command('reset:log')->daily()->onOneServer();
// send
$schedule->command('send:remindMail', ['--force'])->dailyAt('11:30')->onOneServer();