diff --git a/app/Console/Commands/CheckCommission.php b/app/Console/Commands/CheckCommission.php index 3c3589b..f9e5518 100644 --- a/app/Console/Commands/CheckCommission.php +++ b/app/Console/Commands/CheckCommission.php @@ -112,16 +112,13 @@ class CheckCommission extends Command DB::rollBack(); return false; } - if (!CommissionLog::create([ + CommissionLog::create([ 'invite_user_id' => $inviteUserId, 'user_id' => $order->user_id, 'trade_no' => $order->trade_no, 'order_amount' => $order->total_amount, 'get_amount' => $commissionBalance - ])) { - DB::rollBack(); - return false; - } + ]); $inviteUserId = $inviter->invite_user_id; // update order actual commission balance $order->actual_commission_balance = $order->actual_commission_balance + $commissionBalance; diff --git a/app/Console/Commands/ExportV2Log.php b/app/Console/Commands/ExportV2Log.php index ec2ccbe..1bc1fa3 100644 --- a/app/Console/Commands/ExportV2Log.php +++ b/app/Console/Commands/ExportV2Log.php @@ -19,11 +19,11 @@ class ExportV2Log extends Command public function handle() { $days = $this->argument('days'); - $date = Carbon::now()->subDays($days)->startOfDay(); + $date = Carbon::now()->subDays((float) $days)->startOfDay(); $logs = DB::table('v2_log') - ->where('created_at', '>=', $date->timestamp) - ->get(); + ->where('created_at', '>=', $date->timestamp) + ->get(); $fileName = "v2_logs_" . Carbon::now()->format('Y_m_d_His') . ".csv"; $handle = fopen(storage_path("logs/$fileName"), 'w'); @@ -35,19 +35,19 @@ class ExportV2Log extends Command fputcsv($handle, [ $log->level, $log->id, - $log->title, - $log->host, - $log->uri, - $log->method, - $log->data, - $log->ip, - $log->context, - Carbon::createFromTimestamp($log->created_at)->toDateTimeString(), + $log->title, + $log->host, + $log->uri, + $log->method, + $log->data, + $log->ip, + $log->context, + Carbon::createFromTimestamp($log->created_at)->toDateTimeString(), Carbon::createFromTimestamp($log->updated_at)->toDateTimeString() ]); } fclose($handle); - $this->info("日志成功导出到: ". storage_path("logs/$fileName")); + $this->info("日志成功导出到: " . storage_path("logs/$fileName")); } } diff --git a/app/Console/Commands/ResetTraffic.php b/app/Console/Commands/ResetTraffic.php index 4c90b1a..60ee4a1 100644 --- a/app/Console/Commands/ResetTraffic.php +++ b/app/Console/Commands/ResetTraffic.php @@ -5,30 +5,26 @@ namespace App\Console\Commands; use App\Models\Plan; use Illuminate\Console\Command; use App\Models\User; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\DB; class ResetTraffic extends Command { - protected $builder; /** - * The name and signature of the console command. - * + * @var Builder + */ + protected $builder; + + /** * @var string */ protected $signature = 'reset:traffic'; /** - * The console command description. - * * @var string */ protected $description = '流量清空'; - /** - * Create a new command instance. - * - * @return void - */ public function __construct() { parent::__construct(); @@ -37,13 +33,13 @@ class ResetTraffic extends Command } /** - * Execute the console command. - * - * @return mixed + * 执行流量重置命令 */ public function handle() { ini_set('memory_limit', -1); + + // 按重置方法分组查询所有套餐 $resetMethods = Plan::select( DB::raw("GROUP_CONCAT(`id`) as plan_ids"), DB::raw("reset_traffic_method as method") @@ -51,138 +47,117 @@ class ResetTraffic extends Command ->groupBy('reset_traffic_method') ->get() ->toArray(); + + // 使用闭包直接引用方法 + $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), + ]; + + // 处理每种重置方法 foreach ($resetMethods as $resetMethod) { $planIds = explode(',', $resetMethod['plan_ids']); - switch (true) { - case ($resetMethod['method'] === NULL): { - $resetTrafficMethod = admin_setting('reset_traffic_method', 0); - $builder = with(clone ($this->builder))->whereIn('plan_id', $planIds); - switch ((int) $resetTrafficMethod) { - // month first day - case 0: - $this->resetByMonthFirstDay($builder); - break; - // expire day - case 1: - $this->resetByExpireDay($builder); - break; - // no action - case 2: - break; - // year first day - case 3: - $this->resetByYearFirstDay($builder); - // year expire day - case 4: - $this->resetByExpireYear($builder); - } - break; - } - case ($resetMethod['method'] === 0): { - $builder = with(clone ($this->builder))->whereIn('plan_id', $planIds); - $this->resetByMonthFirstDay($builder); - break; - } - case ($resetMethod['method'] === 1): { - $builder = with(clone ($this->builder))->whereIn('plan_id', $planIds); - $this->resetByExpireDay($builder); - break; - } - case ($resetMethod['method'] === 2): { - break; - } - case ($resetMethod['method'] === 3): { - $builder = with(clone ($this->builder))->whereIn('plan_id', $planIds); - $this->resetByYearFirstDay($builder); - break; - } - case ($resetMethod['method'] === 4): { - $builder = with(clone ($this->builder))->whereIn('plan_id', $planIds); - $this->resetByExpireYear($builder); - break; - } + + // 获取重置方法 + $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); } } - private function resetByExpireYear($builder): void + /** + * 按用户年度到期日重置流量 + */ + private function resetByExpireYear(Builder $builder): void { - - $users = $builder->with('plan')->get(); - $usersToUpdate = []; - foreach ($users as $user) { - $expireDay = date('m-d', $user->expired_at); - $today = date('m-d'); - if ($expireDay === $today) { - $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 - ]); - } + $today = date('m-d'); + $this->resetUsersByDateCondition($builder, function ($user) use ($today) { + return date('m-d', $user->expired_at) === $today; + }); } - private function resetByYearFirstDay($builder): void + /** + * 按新年第一天重置流量 + */ + private function resetByYearFirstDay(Builder $builder): void { - $users = $builder->with('plan')->get(); - $usersToUpdate = []; - foreach ($users as $user) { - if ((string) date('md') === '0101') { - $usersToUpdate[] = [ - 'id' => $user->id, - 'transfer_enable' => $user->plan->transfer_enable - ]; - } + $isNewYear = date('md') === '0101'; + if (!$isNewYear) { + return; } - foreach ($usersToUpdate as $userData) { - User::where('id', $userData['id'])->update([ - 'transfer_enable' => (intval($userData['transfer_enable']) * 1073741824), - 'u' => 0, - 'd' => 0 - ]); - } + $this->resetAllUsers($builder); } - private function resetByMonthFirstDay($builder): void + /** + * 按月初第一天重置流量 + */ + private function resetByMonthFirstDay(Builder $builder): void { - $users = $builder->with('plan')->get(); - $usersToUpdate = []; - foreach ($users as $user) { - if ((string) date('d') === '01') { - $usersToUpdate[] = [ - 'id' => $user->id, - 'transfer_enable' => $user->plan->transfer_enable - ]; - } + $isFirstDayOfMonth = date('d') === '01'; + if (!$isFirstDayOfMonth) { + return; } - foreach ($usersToUpdate as $userData) { - User::where('id', $userData['id'])->update([ - 'transfer_enable' => (intval($userData['transfer_enable']) * 1073741824), - 'u' => 0, - 'd' => 0 - ]); - } + $this->resetAllUsers($builder); } - private function resetByExpireDay($builder): void + + /** + * 按用户到期日重置流量 + */ + private function resetByExpireDay(Builder $builder): void { - $lastDay = date('d', strtotime('last day of +0 months')); $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) { - $expireDay = date('d', $user->expired_at); - if ($expireDay === $today || ($today === $lastDay && $expireDay >= $today)) { + if ($condition($user)) { $usersToUpdate[] = [ 'id' => $user->id, 'transfer_enable' => $user->plan->transfer_enable diff --git a/app/Console/Commands/XboardInstall.php b/app/Console/Commands/XboardInstall.php index 658fd06..a33d2f2 100644 --- a/app/Console/Commands/XboardInstall.php +++ b/app/Console/Commands/XboardInstall.php @@ -50,9 +50,9 @@ class XboardInstall extends Command { try { $isDocker = file_exists('/.dockerenv'); - $enableSqlite = env('ENABLE_SQLITE', false); - $enableRedis = env('ENABLE_REDIS', false); - $adminAccount = env('ADMIN_ACCOUNT', ''); + $enableSqlite = getenv('ENABLE_SQLITE', false); + $enableRedis = getenv('ENABLE_REDIS', false); + $adminAccount = getenv('ADMIN_ACCOUNT', false); $this->info("__ __ ____ _ "); $this->info("\ \ / /| __ ) ___ __ _ _ __ __| | "); $this->info(" \ \/ / | __ \ / _ \ / _` | '__/ _` | "); @@ -60,7 +60,7 @@ class XboardInstall extends Command $this->info("/_/ \_\|____/ \___/ \__,_|_| \__,_| "); if ( (File::exists(base_path() . '/.env') && $this->getEnvValue('INSTALLED')) - || (env('INSTALLED', false) && $isDocker) + || (getenv('INSTALLED', false) && $isDocker) ) { $securePath = admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key')))); $this->info("访问 http(s)://你的站点/{$securePath} 进入管理面板,你可以在用户中心修改你的密码。"); diff --git a/app/Console/Commands/XboardStatistics.php b/app/Console/Commands/XboardStatistics.php index 01c275e..0bd4736 100644 --- a/app/Console/Commands/XboardStatistics.php +++ b/app/Console/Commands/XboardStatistics.php @@ -2,12 +2,10 @@ namespace App\Console\Commands; -use App\Models\StatServer; -use App\Models\StatUser; use App\Services\StatisticalService; use Illuminate\Console\Command; use App\Models\Stat; -use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; class XboardStatistics extends Command { @@ -50,67 +48,6 @@ class XboardStatistics extends Command info('统计任务执行完毕。耗时:' . (microtime(true) - $startAt) / 1000); } - private function statServer() - { - try { - DB::beginTransaction(); - $createdAt = time(); - $recordAt = strtotime('-1 day', strtotime(date('Y-m-d'))); - $statService = new StatisticalService(); - $statService->setStartAt($recordAt); - $stats = $statService->getStatServer(); - foreach ($stats as $stat) { - if (!StatServer::insert([ - 'server_id' => $stat['server_id'], - 'server_type' => $stat['server_type'], - 'u' => $stat['u'], - 'd' => $stat['d'], - 'created_at' => $createdAt, - 'updated_at' => $createdAt, - 'record_type' => 'd', - 'record_at' => $recordAt - ])) { - throw new \Exception('stat server fail'); - } - } - DB::commit(); - $statService->clearStatServer(); - } catch (\Exception $e) { - DB::rollback(); - \Log::error($e->getMessage(), ['exception' => $e]); - } - } - - private function statUser() - { - try { - DB::beginTransaction(); - $createdAt = time(); - $recordAt = strtotime('-1 day', strtotime(date('Y-m-d'))); - $statService = new StatisticalService(); - $statService->setStartAt($recordAt); - $stats = $statService->getStatUser(); - foreach ($stats as $stat) { - if (!StatUser::insert([ - 'user_id' => $stat['user_id'], - 'u' => $stat['u'], - 'd' => $stat['d'], - 'server_rate' => $stat['server_rate'], - 'created_at' => $createdAt, - 'updated_at' => $createdAt, - 'record_type' => 'd', - 'record_at' => $recordAt - ])) { - throw new \Exception('stat user fail'); - } - } - DB::commit(); - $statService->clearStatUser(); - } catch (\Exception $e) { - DB::rollback(); - \Log::error($e->getMessage(), ['exception' => $e]); - } - } private function stat() { @@ -132,7 +69,7 @@ class XboardStatistics extends Command } Stat::create($data); } catch (\Exception $e) { - \Log::error($e->getMessage(), ['exception' => $e]); + Log::error($e->getMessage(), ['exception' => $e]); } } } diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 62b2fc7..feb49c3 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -42,9 +42,9 @@ class Kernel extends ConsoleKernel // horizon metrics $schedule->command('horizon:snapshot')->everyFiveMinutes()->onOneServer(); // backup Timing - if (env('ENABLE_AUTO_BACKUP_AND_UPDATE', false)) { - $schedule->command('backup:database', ['true'])->daily()->onOneServer(); - } + // if (env('ENABLE_AUTO_BACKUP_AND_UPDATE', false)) { + // $schedule->command('backup:database', ['true'])->daily()->onOneServer(); + // } // 每分钟清理过期的在线状态 $schedule->call(function () { app(UserOnlineService::class)->cleanExpiredOnlineStatus(); diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 7b789aa..f76d9c0 100755 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -16,7 +16,7 @@ class Handler extends ExceptionHandler /** * A list of the exception types that are not reported. * - * @var array + * @var array> */ protected $dontReport = [ ApiException::class, @@ -26,7 +26,7 @@ class Handler extends ExceptionHandler /** * A list of the inputs that are never flashed for validation exceptions. * - * @var array + * @var array */ protected $dontFlash = [ 'password', diff --git a/app/Http/Controllers/V1/Passport/AuthController.php b/app/Http/Controllers/V1/Passport/AuthController.php index cd34e4f..4bea6a6 100644 --- a/app/Http/Controllers/V1/Passport/AuthController.php +++ b/app/Http/Controllers/V1/Passport/AuthController.php @@ -7,227 +7,89 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Passport\AuthForget; use App\Http\Requests\Passport\AuthLogin; use App\Http\Requests\Passport\AuthRegister; -use App\Jobs\SendEmailJob; -use App\Models\InviteCode; -use App\Models\Plan; -use App\Models\User; +use App\Services\Auth\LoginService; +use App\Services\Auth\MailLinkService; +use App\Services\Auth\RegisterService; use App\Services\AuthService; -use App\Utils\CacheKey; -use App\Utils\Dict; -use App\Utils\Helper; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Cache; -use ReCaptcha\ReCaptcha; class AuthController extends Controller { + protected MailLinkService $mailLinkService; + protected RegisterService $registerService; + protected LoginService $loginService; + + public function __construct( + MailLinkService $mailLinkService, + RegisterService $registerService, + LoginService $loginService + ) { + $this->mailLinkService = $mailLinkService; + $this->registerService = $registerService; + $this->loginService = $loginService; + } + + /** + * 通过邮件链接登录 + */ public function loginWithMailLink(Request $request) { - if (!(int)admin_setting('login_with_mail_link_enable')) { - return $this->fail([404,null]); - } $params = $request->validate([ 'email' => 'required|email:strict', 'redirect' => 'nullable' ]); - if (Cache::get(CacheKey::get('LAST_SEND_LOGIN_WITH_MAIL_LINK_TIMESTAMP', $params['email']))) { - return $this->fail([429 ,__('Sending frequently, please try again later')]); + [$success, $result] = $this->mailLinkService->handleMailLink( + $params['email'], + $request->input('redirect') + ); + + if (!$success) { + return $this->fail($result); } - $user = User::where('email', $params['email'])->first(); - if (!$user) { - return $this->success(true); - } - - $code = Helper::guid(); - $key = CacheKey::get('TEMP_TOKEN', $code); - Cache::put($key, $user->id, 300); - Cache::put(CacheKey::get('LAST_SEND_LOGIN_WITH_MAIL_LINK_TIMESTAMP', $params['email']), time(), 60); - - - $redirect = '/#/login?verify=' . $code . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard'); - if (admin_setting('app_url')) { - $link = admin_setting('app_url') . $redirect; - } else { - $link = url($redirect); - } - - SendEmailJob::dispatch([ - 'email' => $user->email, - 'subject' => __('Login to :name', [ - 'name' => admin_setting('app_name', 'XBoard') - ]), - 'template_name' => 'login', - 'template_value' => [ - 'name' => admin_setting('app_name', 'XBoard'), - 'link' => $link, - 'url' => admin_setting('app_url') - ] - ]); - - return $this->success($link); - + return $this->success($result); } + /** + * 用户注册 + */ public function register(AuthRegister $request) { - if ((int)admin_setting('register_limit_by_ip_enable', 0)) { - $registerCountByIP = Cache::get(CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip())) ?? 0; - if ((int)$registerCountByIP >= (int)admin_setting('register_limit_count', 3)) { - return $this->fail([429,__('Register frequently, please try again after :minute minute', [ - 'minute' => admin_setting('register_limit_expire', 60) - ])]); - } - } - if ((int)admin_setting('recaptcha_enable', 0)) { - $recaptcha = new ReCaptcha(admin_setting('recaptcha_key')); - $recaptchaResp = $recaptcha->verify($request->input('recaptcha_data')); - if (!$recaptchaResp->isSuccess()) { - return $this->fail([400,__('Invalid code is incorrect')]); - } - } - if ((int)admin_setting('email_whitelist_enable', 0)) { - if (!Helper::emailSuffixVerify( - $request->input('email'), - admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT)) - ) { - return $this->fail([400,__('Email suffix is not in the Whitelist')]); - } - } - if ((int)admin_setting('email_gmail_limit_enable', 0)) { - $prefix = explode('@', $request->input('email'))[0]; - if (strpos($prefix, '.') !== false || strpos($prefix, '+') !== false) { - return $this->fail([400,__('Gmail alias is not supported')]); - } - } - if ((int)admin_setting('stop_register', 0)) { - return $this->fail([400,__('Registration has closed')]); - } - if ((int)admin_setting('invite_force', 0)) { - if (empty($request->input('invite_code'))) { - return $this->fail([422,__('You must use the invitation code to register')]); - } - } - if ((int)admin_setting('email_verify', 0)) { - if (empty($request->input('email_code'))) { - return $this->fail([422,__('Email verification code cannot be empty')]); - } - if ((string)Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== (string)$request->input('email_code')) { - return $this->fail([400,__('Incorrect email verification code')]); - } - } - $email = $request->input('email'); - $password = $request->input('password'); - $exist = User::where('email', $email)->first(); - if ($exist) { - return $this->fail([400201,__('Email already exists')]); - } - $user = new User(); - $user->email = $email; - $user->password = password_hash($password, PASSWORD_DEFAULT); - $user->uuid = Helper::guid(true); - $user->token = Helper::guid(); - // TODO 增加过期默认值、流量告急提醒默认值 - $user->remind_expire = admin_setting('default_remind_expire',1); - $user->remind_traffic = admin_setting('default_remind_traffic',1); - if ($request->input('invite_code')) { - $inviteCode = InviteCode::where('code', $request->input('invite_code')) - ->where('status', 0) - ->first(); - if (!$inviteCode) { - if ((int)admin_setting('invite_force', 0)) { - return $this->fail([400,__('Invalid invitation code')]); - } - } else { - $user->invite_user_id = $inviteCode->user_id ? $inviteCode->user_id : null; - if (!(int)admin_setting('invite_never_expire', 0)) { - $inviteCode->status = 1; - $inviteCode->save(); - } - } + [$success, $result] = $this->registerService->register($request); + + if (!$success) { + return $this->fail($result); } - // try out - if ((int)admin_setting('try_out_plan_id', 0)) { - $plan = Plan::find(admin_setting('try_out_plan_id')); - if ($plan) { - $user->transfer_enable = $plan->transfer_enable * 1073741824; - $user->plan_id = $plan->id; - $user->group_id = $plan->group_id; - $user->expired_at = time() + (admin_setting('try_out_hour', 1) * 3600); - $user->speed_limit = $plan->speed_limit; - } - } - - if (!$user->save()) { - return $this->fail([500,__('Register failed')]); - } - if ((int)admin_setting('email_verify', 0)) { - Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))); - } - - $user->last_login_at = time(); - $user->save(); - - if ((int)admin_setting('register_limit_by_ip_enable', 0)) { - Cache::put( - CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip()), - (int)$registerCountByIP + 1, - (int)admin_setting('register_limit_expire', 60) * 60 - ); - } - - $authService = new AuthService($user); - - $data = $authService->generateAuthData(); - return $this->success($data); + $authService = new AuthService($result); + return $this->success($authService->generateAuthData()); } + /** + * 用户登录 + */ public function login(AuthLogin $request) { $email = $request->input('email'); $password = $request->input('password'); - if ((int)admin_setting('password_limit_enable', 1)) { - $passwordErrorCount = (int)Cache::get(CacheKey::get('PASSWORD_ERROR_LIMIT', $email), 0); - if ($passwordErrorCount >= (int)admin_setting('password_limit_count', 5)) { - return $this->fail([429,__('There are too many password errors, please try again after :minute minutes.', [ - 'minute' => admin_setting('password_limit_expire', 60) - ])]); - } + [$success, $result] = $this->loginService->login($email, $password); + + if (!$success) { + return $this->fail($result); } - $user = User::where('email', $email)->first(); - if (!$user) { - return $this->fail([400, __('Incorrect email or password')]); - } - if (!Helper::multiPasswordVerify( - $user->password_algo, - $user->password_salt, - $password, - $user->password) - ) { - if ((int)admin_setting('password_limit_enable')) { - Cache::put( - CacheKey::get('PASSWORD_ERROR_LIMIT', $email), - (int)$passwordErrorCount + 1, - 60 * (int)admin_setting('password_limit_expire', 60) - ); - } - return $this->fail([400, __('Incorrect email or password')]); - } - - if ($user->banned) { - return $this->fail([400, __('Your account has been suspended')]); - } - - $authService = new AuthService($user); + $authService = new AuthService($result); return $this->success($authService->generateAuthData()); } + /** + * 通过token登录 + */ public function token2Login(Request $request) { + // 处理直接通过token重定向 if ($token = $request->input('token')) { $redirect = '/#/login?verify=' . $token . '&redirect=' . ($request->input('redirect', 'dashboard')); @@ -238,9 +100,9 @@ class AuthController extends Controller ); } + // 处理通过验证码登录 if ($verify = $request->input('verify')) { - $key = CacheKey::get('TEMP_TOKEN', $verify); - $userId = Cache::get($key); + $userId = $this->mailLinkService->handleTokenLogin($verify); if (!$userId) { return response()->json([ @@ -248,15 +110,14 @@ class AuthController extends Controller ], 400); } - $user = User::findOrFail($userId); + $user = \App\Models\User::find($userId); - if ($user->banned) { + if (!$user) { return response()->json([ - 'message' => __('Your account has been suspended') + 'message' => __('User not found') ], 400); } - Cache::forget($key); $authService = new AuthService($user); return response()->json([ @@ -269,6 +130,9 @@ class AuthController extends Controller ], 400); } + /** + * 获取快速登录URL + */ public function getQuickLoginUrl(Request $request) { $authorization = $request->input('auth_data') ?? $request->header('authorization'); @@ -287,38 +151,25 @@ class AuthController extends Controller ], 401); } - $code = Helper::guid(); - $key = CacheKey::get('TEMP_TOKEN', $code); - Cache::put($key, $user['id'], 60); - $redirect = '/#/login?verify=' . $code . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard'); - if (admin_setting('app_url')) { - $url = admin_setting('app_url') . $redirect; - } else { - $url = url($redirect); - } + $url = $this->mailLinkService->getQuickLoginUrl($user, $request->input('redirect')); return $this->success($url); } + /** + * 忘记密码处理 + */ public function forget(AuthForget $request) { - $forgetRequestLimitKey = CacheKey::get('FORGET_REQUEST_LIMIT', $request->input('email')); - $forgetRequestLimit = (int)Cache::get($forgetRequestLimitKey); - if ($forgetRequestLimit >= 3) return $this->fail([429, __('Reset failed, Please try again later')]); - if ((string)Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== (string)$request->input('email_code')) { - Cache::put($forgetRequestLimitKey, $forgetRequestLimit ? $forgetRequestLimit + 1 : 1, 300); - return $this->fail([400,__('Incorrect email verification code')]); + [$success, $result] = $this->loginService->resetPassword( + $request->input('email'), + $request->input('email_code'), + $request->input('password') + ); + + if (!$success) { + return $this->fail($result); } - $user = User::where('email', $request->input('email'))->first(); - if (!$user) { - return $this->fail([400,__('This email is not registered in the system')]); - } - $user->password = password_hash($request->input('password'), PASSWORD_DEFAULT); - $user->password_algo = NULL; - $user->password_salt = NULL; - if (!$user->save()) { - return $this->fail([500,__('Reset failed')]); - } - Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))); + return $this->success(true); } } diff --git a/app/Http/Controllers/V1/Passport/CommController.php b/app/Http/Controllers/V1/Passport/CommController.php index ce135fa..60b2cd7 100644 --- a/app/Http/Controllers/V1/Passport/CommController.php +++ b/app/Http/Controllers/V1/Passport/CommController.php @@ -15,14 +15,10 @@ use ReCaptcha\ReCaptcha; class CommController extends Controller { - private function isEmailVerify() - { - return $this->success((int)admin_setting('email_verify', 0) ? 1 : 0); - } public function sendEmailVerify(CommSendEmailVerify $request) { - if ((int)admin_setting('recaptcha_enable', 0)) { + if ((int) admin_setting('recaptcha_enable', 0)) { $recaptcha = new ReCaptcha(admin_setting('recaptcha_key')); $recaptchaResp = $recaptcha->verify($request->input('recaptcha_data')); if (!$recaptchaResp->isSuccess()) { @@ -63,12 +59,4 @@ class CommController extends Controller return $this->success(true); } - private function getEmailSuffix() - { - $suffix = admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT); - if (!is_array($suffix)) { - return preg_split('/,/', $suffix); - } - return $suffix; - } } diff --git a/app/Http/Controllers/V1/Server/UniProxyController.php b/app/Http/Controllers/V1/Server/UniProxyController.php index 36c358c..860199f 100644 --- a/app/Http/Controllers/V1/Server/UniProxyController.php +++ b/app/Http/Controllers/V1/Server/UniProxyController.php @@ -150,6 +150,20 @@ class UniProxyController extends Controller 'socks' => [ 'server_port' => (int) $serverPort, ], + 'naive' => [ + 'server_port' => (int) $serverPort, + 'tls' => (int) $protocolSettings['tls'], + 'tls_settings' => $protocolSettings['tls_settings'] + ], + 'http' => [ + 'server_port' => (int) $serverPort, + 'tls' => (int) $protocolSettings['tls'], + 'tls_settings' => $protocolSettings['tls_settings'] + ], + 'mieru' => [ + 'server_port' => (string) $serverPort, + 'protocol' => (int) $protocolSettings['protocol'], + ], default => [] }; @@ -163,7 +177,7 @@ class UniProxyController extends Controller } $eTag = sha1(json_encode($response)); - if (strpos($request->header('If-None-Match', '') ?? '', $eTag) !== false) { + if (strpos($request->header('If-None-Match', ''), $eTag) !== false) { return response(null, 304); } return response($response)->header('ETag', "\"{$eTag}\""); diff --git a/app/Http/Controllers/V1/Staff/NoticeController.php b/app/Http/Controllers/V1/Staff/NoticeController.php deleted file mode 100644 index cf30cb3..0000000 --- a/app/Http/Controllers/V1/Staff/NoticeController.php +++ /dev/null @@ -1,58 +0,0 @@ -get(); - return $this->success($data); - } - - public function save(NoticeSave $request) - { - $data = $request->only([ - 'title', - 'content', - 'img_url' - ]); - if (!$request->input('id')) { - if (!Notice::create($data)) { - return $this->fail([500, '创建失败']); - } - } else { - try { - Notice::find($request->input('id'))->update($data); - } catch (\Exception $e) { - \Log::error($e); - return $this->fail([500, '保存失败']); - } - } - return $this->success(true); - } - - public function drop(Request $request) - { - $request->validate([ - 'id' => 'required' - ],[ - 'id.required' => '公告ID不能为空' - ]); - - $notice = Notice::find($request->input('id')); - if (!$notice) { - return $this->fail([400202,'公告不存在']); - } - if (!$notice->delete()) { - return $this->fail([500,'公告删除失败']); - } - return $this->success(true); - } -} diff --git a/app/Http/Controllers/V1/Staff/PlanController.php b/app/Http/Controllers/V1/Staff/PlanController.php deleted file mode 100755 index 96a507e..0000000 --- a/app/Http/Controllers/V1/Staff/PlanController.php +++ /dev/null @@ -1,35 +0,0 @@ -where('plan_id', '!=', NULL) - ->where(function ($query) { - $query->where('expired_at', '>=', time()) - ->orWhere('expired_at', NULL); - }) - ->groupBy("plan_id") - ->get(); - $plans = Plan::orderBy('sort', 'ASC')->get(); - foreach ($plans as $k => $v) { - $plans[$k]->count = 0; - foreach ($counts as $kk => $vv) { - if ($plans[$k]->id === $counts[$kk]->plan_id) $plans[$k]->count = $counts[$kk]->count; - } - } - return $this->success($plans); - } -} diff --git a/app/Http/Controllers/V1/Staff/TicketController.php b/app/Http/Controllers/V1/Staff/TicketController.php deleted file mode 100644 index ae32373..0000000 --- a/app/Http/Controllers/V1/Staff/TicketController.php +++ /dev/null @@ -1,82 +0,0 @@ -input('id')) { - $ticket = Ticket::where('id', $request->input('id')) - ->first(); - if (!$ticket) { - return $this->fail([400,'工单不存在']); - } - $ticket['message'] = TicketMessage::where('ticket_id', $ticket->id)->get(); - for ($i = 0; $i < count($ticket['message']); $i++) { - if ($ticket['message'][$i]['user_id'] !== $ticket->user_id) { - $ticket['message'][$i]['is_me'] = true; - } else { - $ticket['message'][$i]['is_me'] = false; - } - } - return $this->success($ticket); - } - $current = $request->input('current') ? $request->input('current') : 1; - $pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10; - $model = Ticket::orderBy('created_at', 'DESC'); - if ($request->input('status') !== NULL) { - $model->where('status', $request->input('status')); - } - $total = $model->count(); - $res = $model->forPage($current, $pageSize) - ->get(); - return response([ - 'data' => $res, - 'total' => $total - ]); - } - - public function reply(Request $request) - { - $request->validate([ - 'id' => 'required', - 'message' => 'required|string' - ],[ - 'id.required' => '工单ID不能为空', - 'message.required' => '消息不能为空' - ]); - $ticketService = new TicketService(); - $ticketService->replyByAdmin( - $request->input('id'), - $request->input('message'), - $request->user()->id - ); - return $this->success(true); - } - - public function close(Request $request) - { - - if (empty($request->input('id'))) { - return $this->fail([422,'工单ID不能为空']); - } - $ticket = Ticket::where('id', $request->input('id')) - ->first(); - if (!$ticket) { - return $this->fail([400202,'工单不存在']); - } - $ticket->status = Ticket::STATUS_CLOSED; - if (!$ticket->save()) { - return $this->fail([500, '工单关闭失败']); - } - return $this->success(true); - } -} diff --git a/app/Http/Controllers/V1/Staff/UserController.php b/app/Http/Controllers/V1/Staff/UserController.php deleted file mode 100644 index 97bd9df..0000000 --- a/app/Http/Controllers/V1/Staff/UserController.php +++ /dev/null @@ -1,102 +0,0 @@ -input('id'))) { - return $this->fail([422,'用户ID不能为空']); - } - $user = User::where('is_admin', 0) - ->where('id', $request->input('id')) - ->where('is_staff', 0) - ->first(); - if (!$user) return $this->fail([400202,'用户不存在']); - return $this->success($user); - } - - public function update(UserUpdate $request) - { - $params = $request->validated(); - $user = User::find($request->input('id')); - if (!$user) { - return $this->fail([400202,'用户不存在']); - } - if (User::where('email', $params['email'])->first() && $user->email !== $params['email']) { - return $this->fail([400201,'邮箱已被使用']); - } - if (isset($params['password'])) { - $params['password'] = password_hash($params['password'], PASSWORD_DEFAULT); - $params['password_algo'] = NULL; - } else { - unset($params['password']); - } - if (isset($params['plan_id'])) { - $plan = Plan::find($params['plan_id']); - if (!$plan) { - return $this->fail([400202,'订阅不存在']); - } - $params['group_id'] = $plan->group_id; - } - - try { - $user->update($params); - } catch (\Exception $e) { - \Log::error($e); - return $this->fail([500,'更新失败']); - } - return $this->success(true); - } - - public function sendMail(UserSendMail $request) - { - $sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC'; - $sort = $request->input('sort') ? $request->input('sort') : 'created_at'; - $builder = User::orderBy($sort, $sortType); - $this->filter($request, $builder); - $users = $builder->get(); - foreach ($users as $user) { - SendEmailJob::dispatch([ - 'email' => $user->email, - 'subject' => $request->input('subject'), - 'template_name' => 'notify', - 'template_value' => [ - 'name' => admin_setting('app_name', 'XBoard'), - 'url' => admin_setting('app_url'), - 'content' => $request->input('content') - ] - ]); - } - - return $this->success(true); - } - - public function ban(Request $request) - { - $sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC'; - $sort = $request->input('sort') ? $request->input('sort') : 'created_at'; - $builder = User::orderBy($sort, $sortType); - $this->filter($request, $builder); - try { - $builder->update([ - 'banned' => 1 - ]); - } catch (\Exception $e) { - \Log::error($e); - return $this->fail([500,'处理上失败']); - } - - return $this->success(true); - } -} diff --git a/app/Http/Controllers/V1/User/OrderController.php b/app/Http/Controllers/V1/User/OrderController.php index 6ad6742..08a925b 100755 --- a/app/Http/Controllers/V1/User/OrderController.php +++ b/app/Http/Controllers/V1/User/OrderController.php @@ -43,16 +43,15 @@ class OrderController extends Controller $request->validate([ 'trade_no' => 'required|string', ]); - $order = Order::with('payment') + $order = Order::with(['payment','plan']) ->where('user_id', $request->user()->id) ->where('trade_no', $request->input('trade_no')) ->first(); if (!$order) { return $this->fail([400, __('Order does not exist or has been paid')]); } - $order['plan'] = Plan::find($order->plan_id); $order['try_out_plan_id'] = (int) admin_setting('try_out_plan_id'); - if (!$order['plan']) { + if (!$order->plan) { return $this->fail([400, __('Subscription plan does not exist')]); } if ($order->surplus_order_ids) { @@ -81,7 +80,7 @@ class OrderController extends Controller // Validate plan purchase $planService->validatePurchase($user, $request->input('period')); - return DB::transaction(function () use ($request, $plan, $user, $userService, $planService) { + return DB::transaction(function () use ($request, $plan, $user, $userService) { $period = $request->input('period'); $newPeriod = PlanService::getPeriodKey($period); @@ -169,12 +168,13 @@ class OrderController extends Controller ]); } $payment = Payment::find($method); - if (!$payment || $payment->enable !== 1) + if (!$payment || !$payment->enable) { return $this->fail([400, __('Payment method is not available')]); + } $paymentService = new PaymentService($payment->payment, $payment->id); $order->handling_amount = NULL; if ($payment->handling_fee_fixed || $payment->handling_fee_percent) { - $order->handling_amount = round(($order->total_amount * ($payment->handling_fee_percent / 100)) + $payment->handling_fee_fixed); + $order->handling_amount = (int) round(($order->total_amount * ($payment->handling_fee_percent / 100)) + $payment->handling_fee_fixed); } $order->payment_id = $method; if (!$order->save()) diff --git a/app/Http/Controllers/V1/User/UserController.php b/app/Http/Controllers/V1/User/UserController.php index c33f00c..5fbc733 100755 --- a/app/Http/Controllers/V1/User/UserController.php +++ b/app/Http/Controllers/V1/User/UserController.php @@ -58,11 +58,13 @@ class UserController extends Controller if (!$user) { return $this->fail([400, __('The user does not exist')]); } - if (!Helper::multiPasswordVerify( - $user->password_algo, - $user->password_salt, - $request->input('old_password'), - $user->password) + if ( + !Helper::multiPasswordVerify( + $user->password_algo, + $user->password_salt, + $request->input('old_password'), + $user->password + ) ) { return $this->fail([400, __('The old password is wrong')]); } diff --git a/app/Http/Controllers/V2/Admin/ConfigController.php b/app/Http/Controllers/V2/Admin/ConfigController.php index 48a9483..e41498d 100644 --- a/app/Http/Controllers/V2/Admin/ConfigController.php +++ b/app/Http/Controllers/V2/Admin/ConfigController.php @@ -64,12 +64,6 @@ class ConfigController extends Controller return $this->success(true); } - private function getTemplateContent(string $filename): string - { - $path = resource_path("rules/{$filename}"); - return File::exists($path) ? File::get($path) : ''; - } - public function fetch(Request $request) { $key = $request->input('key'); diff --git a/app/Http/Controllers/V2/Admin/OrderController.php b/app/Http/Controllers/V2/Admin/OrderController.php index ddc4f50..05ebdb6 100644 --- a/app/Http/Controllers/V2/Admin/OrderController.php +++ b/app/Http/Controllers/V2/Admin/OrderController.php @@ -15,6 +15,7 @@ use App\Utils\Helper; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Facades\Log; class OrderController extends Controller { @@ -27,7 +28,7 @@ class OrderController extends Controller if ($order->surplus_order_ids) { $order['surplus_orders'] = Order::whereIn('id', $order->surplus_order_ids)->get(); } - $order['period'] = PlanService::getLegacyPeriod($order->period); + $order['period'] = PlanService::getLegacyPeriod((string) $order->period); return $this->success($order); } @@ -45,17 +46,21 @@ class OrderController extends Controller $this->applyFiltersAndSorts($request, $orderModel); - return response()->json( - $orderModel - ->latest('created_at') - ->paginate( - perPage: $pageSize, - page: $current - )->through(fn($order) => [ - ...$order->toArray(), - 'period' => PlanService::getLegacyPeriod($order->period) - ]), - ); + /** @var \Illuminate\Pagination\LengthAwarePaginator $paginatedResults */ + $paginatedResults = $orderModel + ->latest('created_at') + ->paginate( + perPage: $pageSize, + page: $current + ); + + $paginatedResults->getCollection()->transform(function($order) { + $orderArray = $order->toArray(); + $orderArray['period'] = PlanService::getLegacyPeriod((string) $order->period); + return $orderArray; + }); + + return response()->json($paginatedResults); } private function applyFiltersAndSorts(Request $request, Builder $builder): void @@ -112,8 +117,8 @@ class OrderController extends Controller 'lte' => '<=', 'like' => 'like', 'notlike' => 'not like', - 'null' => static fn($q) => $q->whereNull($queryField), - 'notnull' => static fn($q) => $q->whereNotNull($queryField), + 'null' => static fn($q) => $q->whereNull($field), + 'notnull' => static fn($q) => $q->whereNotNull($field), default => 'like' }, match (strtolower($operator)) { 'like', 'notlike' => "%{$filterValue}%", @@ -184,7 +189,7 @@ class OrderController extends Controller try { $order->update($params); } catch (\Exception $e) { - \Log::error($e); + Log::error($e); return $this->fail([500, '更新失败']); } @@ -215,11 +220,12 @@ class OrderController extends Controller $orderService = new OrderService($order); $order->user_id = $user->id; $order->plan_id = $plan->id; - $order->period = PlanService::getPeriodKey($request->input('period')); + $period = $request->input('period'); + $order->period = (int) PlanService::getPeriodKey((string) $period); $order->trade_no = Helper::guid(); $order->total_amount = $request->input('total_amount'); - if (PlanService::getPeriodKey($order->period) === Plan::PERIOD_RESET_TRAFFIC) { + if (PlanService::getPeriodKey((string) $order->period) === Plan::PERIOD_RESET_TRAFFIC) { $order->type = Order::TYPE_RESET_TRAFFIC; } else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id) { $order->type = Order::TYPE_UPGRADE; diff --git a/app/Http/Controllers/V2/Admin/PlanController.php b/app/Http/Controllers/V2/Admin/PlanController.php index 9d7063a..63c817f 100644 --- a/app/Http/Controllers/V2/Admin/PlanController.php +++ b/app/Http/Controllers/V2/Admin/PlanController.php @@ -8,6 +8,7 @@ use App\Models\Plan; use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; class PlanController extends Controller { @@ -58,7 +59,7 @@ class PlanController extends Controller return $this->success(true); } catch (\Exception $e) { DB::rollBack(); - \Log::error($e); + Log::error($e); return $this->fail([500, '保存失败']); } } @@ -76,12 +77,12 @@ class PlanController extends Controller if (User::where('plan_id', $request->input('id'))->first()) { return $this->fail([400201, '该订阅下存在用户无法删除']); } - if ($request->input('id')) { - $plan = Plan::find($request->input('id')); - if (!$plan) { - return $this->fail([400202, '该订阅不存在']); - } + + $plan = Plan::find($request->input('id')); + if (!$plan) { + return $this->fail([400202, '该订阅不存在']); } + return $this->success($plan->delete()); } @@ -101,7 +102,7 @@ class PlanController extends Controller try { $plan->update($updateData); } catch (\Exception $e) { - \Log::error($e); + Log::error($e); return $this->fail([500, '保存失败']); } @@ -124,7 +125,7 @@ class PlanController extends Controller DB::commit(); } catch (\Exception $e) { DB::rollBack(); - \Log::error($e); + Log::error($e); return $this->fail([500, '保存失败']); } return $this->success(true); diff --git a/app/Http/Controllers/V2/Admin/Server/GroupController.php b/app/Http/Controllers/V2/Admin/Server/GroupController.php index a8c96fc..83a53ac 100644 --- a/app/Http/Controllers/V2/Admin/Server/GroupController.php +++ b/app/Http/Controllers/V2/Admin/Server/GroupController.php @@ -14,15 +14,15 @@ class GroupController extends Controller { public function fetch(Request $request): JsonResponse { - $serverGroups = ServerGroup::query() ->orderByDesc('id') ->withCount('users') - ->get() - ->transform(function ($group) { - $group->server_count = $group->servers()->count(); - return $group; - }); + ->get(); + + // 只在需要时手动加载server_count + $serverGroups->each(function ($group) { + $group->setAttribute('server_count', $group->server_count); + }); return $this->success($serverGroups); } diff --git a/app/Http/Controllers/V2/Admin/Server/ManageController.php b/app/Http/Controllers/V2/Admin/Server/ManageController.php index 2def3af..762ecd9 100644 --- a/app/Http/Controllers/V2/Admin/Server/ManageController.php +++ b/app/Http/Controllers/V2/Admin/Server/ManageController.php @@ -10,12 +10,13 @@ use App\Models\ServerGroup; use App\Services\ServerService; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; class ManageController extends Controller { public function getNodes(Request $request) { - $servers = collect(ServerService::getAllServers())->map(function ($item) { + $servers = ServerService::getAllServers()->map(function ($item) { $item['groups'] = ServerGroup::whereIn('id', $item['group_ids'])->get(['name', 'id']); $item['parent'] = $item->parent; return $item; @@ -41,7 +42,7 @@ class ManageController extends Controller DB::commit(); } catch (\Exception $e) { DB::rollBack(); - \Log::error($e); + Log::error($e); return $this->fail([500, '保存失败']); } @@ -60,7 +61,7 @@ class ManageController extends Controller $server->update($params); return $this->success(true); } catch (\Exception $e) { - \Log::error($e); + Log::error($e); return $this->fail([500, '保存失败']); } } @@ -69,7 +70,7 @@ class ManageController extends Controller Server::create($params); return $this->success(true); } catch (\Exception $e) { - \Log::error($e); + Log::error($e); return $this->fail([500, '创建失败']); } @@ -83,7 +84,7 @@ class ManageController extends Controller 'show' => 'integer', ]); - if (Server::where('id', $request->id)->update(['show' => $request->show]) === false) { + if (!Server::where('id', $request->id)->update(['show' => $request->show])) { return $this->fail([500, '保存失败']); } return $this->success(true); diff --git a/app/Http/Controllers/V2/Admin/StatController.php b/app/Http/Controllers/V2/Admin/StatController.php index 5eac168..e7d615c 100644 --- a/app/Http/Controllers/V2/Admin/StatController.php +++ b/app/Http/Controllers/V2/Admin/StatController.php @@ -25,8 +25,7 @@ class StatController extends Controller { // 获取在线节点数 $onlineNodes = Server::all()->filter(function ($server) { - $server->loadServerStatus(); - return $server->is_online; + return !!$server->is_online; })->count(); // 获取在线设备数和在线用户数 $onlineDevices = User::where('t', '>=', time() - 600) @@ -268,8 +267,7 @@ class StatController extends Controller // 获取在线节点数 $onlineNodes = Server::all()->filter(function ($server) { - $server->loadServerStatus(); - return $server->is_online; + return !!$server->is_online; })->count(); // 获取在线设备数和在线用户数 diff --git a/app/Http/Controllers/V2/Admin/TicketController.php b/app/Http/Controllers/V2/Admin/TicketController.php index 039570f..ca6d8c4 100644 --- a/app/Http/Controllers/V2/Admin/TicketController.php +++ b/app/Http/Controllers/V2/Admin/TicketController.php @@ -55,13 +55,10 @@ class TicketController extends Controller if (!$ticket) { return $this->fail([400202, '工单不存在']); } - $ticket->user = UserController::transformUserData($ticket->user); - $ticket->messages->each(function ($message) use ($ticket) { - $message->is_me = $message->user_id !== $ticket->user_id; + $result = $ticket->toArray(); + $result['user'] = UserController::transformUserData($ticket->user); - }); - - return $this->success($ticket); + return $this->success($result); } /** @@ -91,12 +88,16 @@ class TicketController extends Controller perPage: $request->integer('pageSize', 10), page: $request->integer('current', 1) ); - $tickets->getCollection()->transform(function ($ticket) { - $ticket->user = UserController::transformUserData($ticket->user); - return $ticket; - }); + + // 获取items然后映射转换 + $items = collect($tickets->items())->map(function ($ticket) { + $ticketData = $ticket->toArray(); + $ticketData['user'] = UserController::transformUserData($ticket->user); + return $ticketData; + })->all(); + return response([ - 'data' => $tickets->items(), + 'data' => $items, 'total' => $tickets->total() ]); } @@ -137,4 +138,19 @@ class TicketController extends Controller return $this->fail([500101, '关闭失败']); } } + + public function show($ticketId) + { + $ticket = Ticket::with([ + 'user', + 'messages' => function ($query) { + $query->with(['user']); // 如果需要用户信息 + } + ])->findOrFail($ticketId); + + // 自动包含 is_me 属性 + return response()->json([ + 'data' => $ticket + ]); + } } diff --git a/app/Http/Controllers/V2/Admin/UserController.php b/app/Http/Controllers/V2/Admin/UserController.php index 6b28c4c..f144b81 100644 --- a/app/Http/Controllers/V2/Admin/UserController.php +++ b/app/Http/Controllers/V2/Admin/UserController.php @@ -81,7 +81,7 @@ class UserController extends Controller // 处理关联查询 if (str_contains($field, '.')) { [$relation, $relationField] = explode('.', $field); - $query->whereHas($relation, function($q) use ($relationField, $value) { + $query->whereHas($relation, function ($q) use ($relationField, $value) { if (is_array($value)) { $q->whereIn($relationField, $value); } else if (is_string($value) && str_contains($value, ':')) { @@ -163,7 +163,7 @@ class UserController extends Controller $users = $userModel->orderBy('id', 'desc') ->paginate($pageSize, ['*'], 'page', $current); - $users->getCollection()->transform(function ($user) { + $users->getCollection()->map(function ($user) { return self::transformUserData($user); }); @@ -177,13 +177,14 @@ class UserController extends Controller * Transform user data for response * * @param User $user - * @return User + * @return array */ - public static function transformUserData(User $user): User + public static function transformUserData(User $user): array { - $user->subscribe_url = Helper::getSubscribeUrl($user->token); - $user->balance = $user->balance / 100; - $user->commission_balance = $user->commission_balance / 100; + $user = $user->toArray(); + $user['balance'] = $user['balance'] / 100; + $user['commission_balance'] = $user['commission_balance'] / 100; + $user['subscribe_url'] = Helper::getSubscribeUrl($user['token']); return $user; } @@ -235,7 +236,7 @@ class UserController extends Controller if (isset($params['banned']) && (int) $params['banned'] === 1) { $authService = new AuthService($user); - $authService->removeSession(); + $authService->removeAllSessions(); } if (isset($params['balance'])) { $params['balance'] = $params['balance'] * 100; @@ -263,7 +264,7 @@ class UserController extends Controller { ini_set('memory_limit', '-1'); gc_enable(); // 启用垃圾回收 - + // 优化查询:使用with预加载plan关系,避免N+1问题 $query = User::with('plan:id,name') ->orderBy('id', 'asc') @@ -278,18 +279,18 @@ class UserController extends Controller 'token', 'plan_id' ]); - + $this->applyFiltersAndSorts($request, $query); - + $filename = 'users_' . date('Y-m-d_His') . '.csv'; - - return response()->streamDownload(function() use ($query) { + + return response()->streamDownload(function () use ($query) { // 打开输出流 $output = fopen('php://output', 'w'); - + // 添加BOM标记,确保Excel正确显示中文 - fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF)); - + fprintf($output, chr(0xEF) . chr(0xBB) . chr(0xBF)); + // 写入CSV头部 fputcsv($output, [ '邮箱', @@ -301,9 +302,9 @@ class UserController extends Controller '订阅计划', '订阅地址' ]); - + // 分批处理数据以减少内存使用 - $query->chunk(500, function($users) use ($output) { + $query->chunk(500, function ($users) use ($output) { foreach ($users as $user) { try { $row = [ @@ -325,11 +326,11 @@ class UserController extends Controller continue; // 继续处理下一条记录 } } - + // 清理内存 gc_collect_cycles(); }); - + fclose($output); }, $filename, [ 'Content-Type' => 'text/csv; charset=UTF-8', diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index d8db93e..4801da4 100755 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -11,7 +11,7 @@ class Kernel extends HttpKernel * * These middleware are run during every request to your application. * - * @var array + * @var array */ protected $middleware = [ \Illuminate\Http\Middleware\HandleCors::class, @@ -26,7 +26,7 @@ class Kernel extends HttpKernel /** * The application's route middleware groups. * - * @var array + * @var array> */ protected $middlewareGroups = [ 'web' => [ @@ -57,7 +57,7 @@ class Kernel extends HttpKernel * * These middleware may be assigned to groups or used individually. * - * @var array + * @var array */ protected $middlewareAliases = [ 'auth' => \App\Http\Middleware\Authenticate::class, @@ -84,7 +84,7 @@ class Kernel extends HttpKernel * * This forces non-global middleware to always be in the given order. * - * @var array + * @var array */ protected $middlewarePriority = [ \Illuminate\Session\Middleware\StartSession::class, diff --git a/app/Http/Middleware/Admin.php b/app/Http/Middleware/Admin.php index b39f49d..1a63b4f 100755 --- a/app/Http/Middleware/Admin.php +++ b/app/Http/Middleware/Admin.php @@ -5,6 +5,7 @@ namespace App\Http\Middleware; use App\Exceptions\ApiException; use Illuminate\Support\Facades\Auth; use Closure; +use App\Models\User; class Admin { @@ -17,15 +18,13 @@ class Admin */ public function handle($request, Closure $next) { - if (!Auth::guard('sanctum')->check()) { - throw new ApiException('未登录或登陆已过期', 403); - } - + /** @var User|null $user */ $user = Auth::guard('sanctum')->user(); - if (!$user->is_admin) { - throw new ApiException('无管理员权限', 403); + + if (!$user || !$user->is_admin) { + return response()->json(['message' => 'Unauthorized'], 403); } - + return $next($request); } } diff --git a/app/Http/Middleware/CheckForMaintenanceMode.php b/app/Http/Middleware/CheckForMaintenanceMode.php index 35b9824..53fcdd5 100755 --- a/app/Http/Middleware/CheckForMaintenanceMode.php +++ b/app/Http/Middleware/CheckForMaintenanceMode.php @@ -2,16 +2,17 @@ namespace App\Http\Middleware; -use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode as Middleware; +use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance; -class CheckForMaintenanceMode extends Middleware +class CheckForMaintenanceMode extends PreventRequestsDuringMaintenance { /** - * The URIs that should be reachable while maintenance mode is enabled. - * - * @var array + * 维护模式白名单URI + * @var array */ protected $except = [ - // + // 示例: + // '/api/health-check', + // '/status' ]; } diff --git a/app/Http/Middleware/EncryptCookies.php b/app/Http/Middleware/EncryptCookies.php index 033136a..31e9d1a 100755 --- a/app/Http/Middleware/EncryptCookies.php +++ b/app/Http/Middleware/EncryptCookies.php @@ -7,9 +7,8 @@ use Illuminate\Cookie\Middleware\EncryptCookies as Middleware; class EncryptCookies extends Middleware { /** - * The names of the cookies that should not be encrypted. - * - * @var array + * 不需要加密的Cookie名称列表 + * @var array */ protected $except = [ // diff --git a/app/Http/Middleware/TrimStrings.php b/app/Http/Middleware/TrimStrings.php index 5a50e7b..fb507a4 100755 --- a/app/Http/Middleware/TrimStrings.php +++ b/app/Http/Middleware/TrimStrings.php @@ -7,12 +7,13 @@ use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware; class TrimStrings extends Middleware { /** - * The names of the attributes that should not be trimmed. - * - * @var array + * 不需要去除前后空格的字段名 + * @var array */ protected $except = [ 'password', 'password_confirmation', + 'encrypted_data', + 'signature' ]; } diff --git a/app/Http/Middleware/TrustProxies.php b/app/Http/Middleware/TrustProxies.php index 392efca..83c5400 100755 --- a/app/Http/Middleware/TrustProxies.php +++ b/app/Http/Middleware/TrustProxies.php @@ -8,9 +8,8 @@ use Illuminate\Http\Request; class TrustProxies extends Middleware { /** - * The trusted proxies for this application. - * - * @var array|string + * 可信代理列表 + * @var array|string|null */ protected $proxies = [ "173.245.48.0/20", @@ -36,8 +35,7 @@ class TrustProxies extends Middleware ]; /** - * The headers that should be used to detect proxies. - * + * 代理头映射 * @var int */ protected $headers = diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index 324a166..9e7c0bd 100755 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -7,16 +7,14 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware; class VerifyCsrfToken extends Middleware { /** - * Indicates whether the XSRF-TOKEN cookie should be set on the response. - * + * 是否在响应中设置XSRF-TOKEN cookie * @var bool */ protected $addHttpCookie = true; /** - * The URIs that should be excluded from CSRF verification. - * - * @var array + * 不需要CSRF验证的URI列表 + * @var array */ protected $except = [ // diff --git a/app/Http/Requests/Admin/ServerSave.php b/app/Http/Requests/Admin/ServerSave.php index 54fc40e..b4d40b7 100644 --- a/app/Http/Requests/Admin/ServerSave.php +++ b/app/Http/Requests/Admin/ServerSave.php @@ -54,7 +54,19 @@ class ServerSave extends FormRequest 'reality_settings.short_id' => 'nullable|string', ], 'socks' => [ - ] + ], + 'naive' => [ + 'tls' => 'required|integer', + 'tls_settings' => 'nullable|array', + ], + 'http' => [ + 'tls' => 'required|integer', + 'tls_settings' => 'nullable|array', + ], + 'mieru' => [ + 'transport' => 'required|string', + 'multiplexing' => 'required|string', + ], ]; private function getBaseRules(): array diff --git a/app/Http/Resources/OrderResource.php b/app/Http/Resources/OrderResource.php index d2d5ff8..ae3e6e4 100644 --- a/app/Http/Resources/OrderResource.php +++ b/app/Http/Resources/OrderResource.php @@ -2,10 +2,14 @@ namespace App\Http\Resources; +use App\Models\Order; use App\Services\PlanService; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; +/** + * @mixin Order + */ class OrderResource extends JsonResource { /** @@ -17,8 +21,8 @@ class OrderResource extends JsonResource { return [ ...parent::toArray($request), - 'period' => PlanService::getLegacyPeriod($this->period), - 'plan' => PlanResource::make($this->plan), + 'period' => PlanService::getLegacyPeriod((string)$this->period), + 'plan' => $this->whenLoaded('plan', fn() => PlanResource::make($this->plan)), ]; } } diff --git a/app/Http/Routes/V1/ClientRoute.php b/app/Http/Routes/V1/ClientRoute.php index 8c38cfb..ad13989 100644 --- a/app/Http/Routes/V1/ClientRoute.php +++ b/app/Http/Routes/V1/ClientRoute.php @@ -1,6 +1,8 @@ 'client' ], function ($router) { // Client - $router->get('/subscribe', 'V1\\Client\\ClientController@subscribe')->name('client.subscribe.legacy'); + $router->get('/subscribe', [ClientController::class, 'subscribe'])->name('client.subscribe.legacy'); // App - $router->get('/app/getConfig', 'V1\\Client\\AppController@getConfig'); - $router->get('/app/getVersion', 'V1\\Client\\AppController@getVersion'); + $router->get('/app/getConfig', [AppController::class, 'getConfig']); + $router->get('/app/getVersion', [AppController::class, 'getVersion']); }); } } diff --git a/app/Http/Routes/V1/PassportRoute.php b/app/Http/Routes/V1/PassportRoute.php index dc405e8..3134b96 100644 --- a/app/Http/Routes/V1/PassportRoute.php +++ b/app/Http/Routes/V1/PassportRoute.php @@ -1,6 +1,8 @@ 'passport' ], function ($router) { // Auth - $router->post('/auth/register', 'V1\\Passport\\AuthController@register'); - $router->post('/auth/login', 'V1\\Passport\\AuthController@login'); - $router->get ('/auth/token2Login', 'V1\\Passport\\AuthController@token2Login'); - $router->post('/auth/forget', 'V1\\Passport\\AuthController@forget'); - $router->post('/auth/getQuickLoginUrl', 'V1\\Passport\\AuthController@getQuickLoginUrl'); - $router->post('/auth/loginWithMailLink', 'V1\\Passport\\AuthController@loginWithMailLink'); + $router->post('/auth/register', [AuthController::class, 'register']); + $router->post('/auth/login', [AuthController::class, 'login']); + $router->get('/auth/token2Login', [AuthController::class, 'token2Login']); + $router->post('/auth/forget', [AuthController::class, 'forget']); + $router->post('/auth/getQuickLoginUrl', [AuthController::class, 'getQuickLoginUrl']); + $router->post('/auth/loginWithMailLink', [AuthController::class, 'loginWithMailLink']); // Comm - $router->post('/comm/sendEmailVerify', 'V1\\Passport\\CommController@sendEmailVerify'); - $router->post('/comm/pv', 'V1\\Passport\\CommController@pv'); + $router->post('/comm/sendEmailVerify', [CommController::class, 'sendEmailVerify']); + $router->post('/comm/pv', [CommController::class, 'pv']); }); } } diff --git a/app/Http/Routes/V1/StaffRoute.php b/app/Http/Routes/V1/StaffRoute.php deleted file mode 100644 index 3889bee..0000000 --- a/app/Http/Routes/V1/StaffRoute.php +++ /dev/null @@ -1,32 +0,0 @@ -group([ - 'prefix' => 'staff', - 'middleware' => 'staff' - ], function ($router) { - // Ticket - // $router->get ('/ticket/fetch', 'V1\\Staff\\TicketController@fetch'); - // $router->post('/ticket/reply', 'V1\\Staff\\TicketController@reply'); - // $router->post('/ticket/close', 'V1\\Staff\\TicketController@close'); - // // User - // $router->post('/user/update', 'V1\\Staff\\UserController@update'); - // $router->get ('/user/getUserInfoById', 'V1\\Staff\\UserController@getUserInfoById'); - // $router->post('/user/sendMail', 'V1\\Staff\\UserController@sendMail'); - // $router->post('/user/ban', 'V1\\Staff\\UserController@ban'); - // // Plan - // $router->get ('/plan/fetch', 'V1\\Staff\\PlanController@fetch'); - // // Notice - // $router->get ('/notice/fetch', 'V1\\Admin\\NoticeController@fetch'); - // $router->post('/notice/save', 'V1\\Admin\\NoticeController@save'); - // $router->post('/notice/update', 'V1\\Admin\\NoticeController@update'); - // $router->post('/notice/drop', 'V1\\Admin\\NoticeController@drop'); - }); - } -} diff --git a/app/Http/Routes/V1/UserRoute.php b/app/Http/Routes/V1/UserRoute.php index 839a552..578f64d 100644 --- a/app/Http/Routes/V1/UserRoute.php +++ b/app/Http/Routes/V1/UserRoute.php @@ -1,6 +1,18 @@ 'user' ], function ($router) { // User - $router->get ('/resetSecurity', 'V1\\User\\UserController@resetSecurity'); - $router->get ('/info', 'V1\\User\\UserController@info'); - $router->post('/changePassword', 'V1\\User\\UserController@changePassword'); - $router->post('/update', 'V1\\User\\UserController@update'); - $router->get ('/getSubscribe', 'V1\\User\\UserController@getSubscribe'); - $router->get ('/getStat', 'V1\\User\\UserController@getStat'); - $router->get ('/checkLogin', 'V1\\User\\UserController@checkLogin'); - $router->post('/transfer', 'V1\\User\\UserController@transfer'); - $router->post('/getQuickLoginUrl', 'V1\\User\\UserController@getQuickLoginUrl'); - $router->get ('/getActiveSession', 'V1\\User\\UserController@getActiveSession'); - $router->post('/removeActiveSession', 'V1\\User\\UserController@removeActiveSession'); + $router->get('/resetSecurity', [UserController::class, 'resetSecurity']); + $router->get('/info', [UserController::class, 'info']); + $router->post('/changePassword', [UserController::class, 'changePassword']); + $router->post('/update', [UserController::class, 'update']); + $router->get('/getSubscribe', [UserController::class, 'getSubscribe']); + $router->get('/getStat', [UserController::class, 'getStat']); + $router->get('/checkLogin', [UserController::class, 'checkLogin']); + $router->post('/transfer', [UserController::class, 'transfer']); + $router->post('/getQuickLoginUrl', [UserController::class, 'getQuickLoginUrl']); + $router->get('/getActiveSession', [UserController::class, 'getActiveSession']); + $router->post('/removeActiveSession', [UserController::class, 'removeActiveSession']); // Order - $router->post('/order/save', 'V1\\User\\OrderController@save'); - $router->post('/order/checkout', 'V1\\User\\OrderController@checkout'); - $router->get ('/order/check', 'V1\\User\\OrderController@check'); - $router->get ('/order/detail', 'V1\\User\\OrderController@detail'); - $router->get ('/order/fetch', 'V1\\User\\OrderController@fetch'); - $router->get ('/order/getPaymentMethod', 'V1\\User\\OrderController@getPaymentMethod'); - $router->post('/order/cancel', 'V1\\User\\OrderController@cancel'); + $router->post('/order/save', [OrderController::class, 'save']); + $router->post('/order/checkout', [OrderController::class, 'checkout']); + $router->get('/order/check', [OrderController::class, 'check']); + $router->get('/order/detail', [OrderController::class, 'detail']); + $router->get('/order/fetch', [OrderController::class, 'fetch']); + $router->get('/order/getPaymentMethod', [OrderController::class, 'getPaymentMethod']); + $router->post('/order/cancel', [OrderController::class, 'cancel']); // Plan - $router->get ('/plan/fetch', 'V1\\User\\PlanController@fetch'); + $router->get('/plan/fetch', [PlanController::class, 'fetch']); // Invite - $router->get ('/invite/save', 'V1\\User\\InviteController@save'); - $router->get ('/invite/fetch', 'V1\\User\\InviteController@fetch'); - $router->get ('/invite/details', 'V1\\User\\InviteController@details'); + $router->get('/invite/save', [InviteController::class, 'save']); + $router->get('/invite/fetch', [InviteController::class, 'fetch']); + $router->get('/invite/details', [InviteController::class, 'details']); // Notice - $router->get ('/notice/fetch', 'V1\\User\\NoticeController@fetch'); + $router->get('/notice/fetch', [NoticeController::class, 'fetch']); // Ticket - $router->post('/ticket/reply', 'V1\\User\\TicketController@reply'); - $router->post('/ticket/close', 'V1\\User\\TicketController@close'); - $router->post('/ticket/save', 'V1\\User\\TicketController@save'); - $router->get ('/ticket/fetch', 'V1\\User\\TicketController@fetch'); - $router->post('/ticket/withdraw', 'V1\\User\\TicketController@withdraw'); + $router->post('/ticket/reply', [TicketController::class, 'reply']); + $router->post('/ticket/close', [TicketController::class, 'close']); + $router->post('/ticket/save', [TicketController::class, 'save']); + $router->get('/ticket/fetch', [TicketController::class, 'fetch']); + $router->post('/ticket/withdraw', [TicketController::class, 'withdraw']); // Server - $router->get ('/server/fetch', 'V1\\User\\ServerController@fetch'); + $router->get('/server/fetch', [ServerController::class, 'fetch']); // Coupon - $router->post('/coupon/check', 'V1\\User\\CouponController@check'); + $router->post('/coupon/check', [CouponController::class, 'check']); // Telegram - $router->get ('/telegram/getBotInfo', 'V1\\User\\TelegramController@getBotInfo'); + $router->get('/telegram/getBotInfo', [TelegramController::class, 'getBotInfo']); // Comm - $router->get ('/comm/config', 'V1\\User\\CommController@config'); - $router->Post('/comm/getStripePublicKey', 'V1\\User\\CommController@getStripePublicKey'); + $router->get('/comm/config', [CommController::class, 'config']); + $router->Post('/comm/getStripePublicKey', [CommController::class, 'getStripePublicKey']); // Knowledge - $router->get ('/knowledge/fetch', 'V1\\User\\KnowledgeController@fetch'); - $router->get ('/knowledge/getCategory', 'V1\\User\\KnowledgeController@getCategory'); + $router->get('/knowledge/fetch', [KnowledgeController::class, 'fetch']); + $router->get('/knowledge/getCategory', [KnowledgeController::class, 'getCategory']); // Stat - $router->get ('/stat/getTrafficLog', 'V1\\User\\StatController@getTrafficLog'); + $router->get('/stat/getTrafficLog', [StatController::class, 'getTrafficLog']); }); } } diff --git a/app/Http/Routes/V2/PassportRoute.php b/app/Http/Routes/V2/PassportRoute.php index d45fc65..ed91d81 100644 --- a/app/Http/Routes/V2/PassportRoute.php +++ b/app/Http/Routes/V2/PassportRoute.php @@ -1,6 +1,8 @@ 'passport' ], function ($router) { // Auth - $router->post('/auth/register', 'V1\\Passport\\AuthController@register'); - $router->post('/auth/login', 'V1\\Passport\\AuthController@login'); - $router->get ('/auth/token2Login', 'V1\\Passport\\AuthController@token2Login'); - $router->post('/auth/forget', 'V1\\Passport\\AuthController@forget'); - $router->post('/auth/getQuickLoginUrl', 'V1\\Passport\\AuthController@getQuickLoginUrl'); - $router->post('/auth/loginWithMailLink', 'V1\\Passport\\AuthController@loginWithMailLink'); + $router->post('/auth/register', [AuthController::class, 'register']); + $router->post('/auth/login', [AuthController::class, 'login']); + $router->get ('/auth/token2Login', [AuthController::class, 'token2Login']); + $router->post('/auth/forget', [AuthController::class, 'forget']); + $router->post('/auth/getQuickLoginUrl', [AuthController::class, 'getQuickLoginUrl']); + $router->post('/auth/loginWithMailLink', [AuthController::class, 'loginWithMailLink']); // Comm - $router->post('/comm/sendEmailVerify', 'V1\\Passport\\CommController@sendEmailVerify'); - $router->post('/comm/pv', 'V1\\Passport\\CommController@pv'); + $router->post('/comm/sendEmailVerify', [CommController::class, 'sendEmailVerify']); + $router->post('/comm/pv', [CommController::class, 'pv']); }); } } diff --git a/app/Http/Routes/V2/UserRoute.php b/app/Http/Routes/V2/UserRoute.php index ceed9bc..38bc12f 100644 --- a/app/Http/Routes/V2/UserRoute.php +++ b/app/Http/Routes/V2/UserRoute.php @@ -1,6 +1,7 @@ 'user' ], function ($router) { // User - $router->get('/resetSecurity', 'V1\\User\\UserController@resetSecurity'); - $router->get('/info', 'V1\\User\\UserController@info'); + $router->get('/resetSecurity', [UserController::class, 'resetSecurity']); + $router->get('/info', [UserController::class, 'info']); }); } } diff --git a/app/Logging/MysqlLoggerHandler.php b/app/Logging/MysqlLoggerHandler.php index dfe178a..eb0b09e 100644 --- a/app/Logging/MysqlLoggerHandler.php +++ b/app/Logging/MysqlLoggerHandler.php @@ -17,28 +17,28 @@ class MysqlLoggerHandler extends AbstractProcessingHandler protected function write(LogRecord $record): void { $record = $record->toArray(); - try{ - if(isset($record['context']['exception']) && is_object($record['context']['exception'])){ + try { + if (isset($record['context']['exception']) && is_object($record['context']['exception'])) { $record['context']['exception'] = (array)$record['context']['exception']; } - $record['request_data'] = request()->all() ??[]; + + $record['request_data'] = request()->all(); + $log = [ 'title' => $record['message'], 'level' => $record['level_name'], - 'host' => $record['request_host'] ?? request()->getSchemeAndHttpHost(), - 'uri' => $record['request_uri'] ?? request()->getRequestUri(), - 'method' => $record['request_method'] ?? request()->getMethod(), + 'host' => $record['extra']['request_host'] ?? request()->getSchemeAndHttpHost(), + 'uri' => $record['extra']['request_uri'] ?? request()->getRequestUri(), + 'method' => $record['extra']['request_method'] ?? request()->getMethod(), 'ip' => request()->getClientIp(), - 'data' => json_encode($record['request_data']) , - 'context' => isset($record['context']) ? json_encode($record['context']) : '', + 'data' => json_encode($record['request_data']), + 'context' => json_encode($record['context']), 'created_at' => $record['datetime']->getTimestamp(), 'updated_at' => $record['datetime']->getTimestamp(), ]; - LogModel::insert( - $log - ); - }catch (\Exception $e){ + LogModel::insert($log); + } catch (\Exception $e) { // Log::channel('daily')->error($e->getMessage().$e->getFile().$e->getTraceAsString()); } } diff --git a/app/Models/Order.php b/app/Models/Order.php index 593c9a1..4ee0b14 100755 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -3,7 +3,38 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; +/** + * App\Models\Order + * + * @property int $id + * @property int $user_id + * @property int $plan_id + * @property int|null $payment_id + * @property int $period + * @property string $trade_no + * @property int $total_amount + * @property int|null $handling_amount + * @property int|null $balance_amount + * @property int $type + * @property int $status + * @property array|null $surplus_order_ids + * @property int|null $coupon_id + * @property int $created_at + * @property int $updated_at + * @property int|null $commission_status + * @property int|null $invite_user_id + * @property int|null $actual_commission_balance + * @property int|null $commission_rate + * @property int|null $commission_auto_check + * + * @property-read Plan $plan + * @property-read Payment|null $payment + * @property-read User $user + * @property-read \Illuminate\Database\Eloquent\Collection $commission_log + */ class Order extends Model { protected $table = 'v2_order'; @@ -12,7 +43,8 @@ class Order extends Model protected $casts = [ 'created_at' => 'timestamp', 'updated_at' => 'timestamp', - 'surplus_order_ids' => 'array' + 'surplus_order_ids' => 'array', + 'handling_amount' => 'integer' ]; const STATUS_PENDING = 0; // 待支付 @@ -40,21 +72,34 @@ class Order extends Model self::TYPE_RESET_TRAFFIC => '流量重置', ]; - public function payment() + /** + * 获取与订单关联的支付方式 + */ + public function payment(): BelongsTo { return $this->belongsTo(Payment::class, 'payment_id', 'id'); } - public function user() + /** + * 获取与订单关联的用户 + */ + public function user(): BelongsTo { return $this->belongsTo(User::class, 'user_id', 'id'); } - public function plan() + + /** + * 获取与订单关联的套餐 + */ + public function plan(): BelongsTo { - return $this->belongsTo(Plan::class); + return $this->belongsTo(Plan::class, 'plan_id', 'id'); } - public function commission_log() + /** + * 获取与订单关联的佣金记录 + */ + public function commission_log(): HasMany { return $this->hasMany(CommissionLog::class, 'trade_no', 'trade_no'); } diff --git a/app/Models/Payment.php b/app/Models/Payment.php index 5830367..fec8b00 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -12,6 +12,7 @@ class Payment extends Model protected $casts = [ 'created_at' => 'timestamp', 'updated_at' => 'timestamp', - 'config' => 'array' + 'config' => 'array', + 'enable' => 'boolean' ]; } diff --git a/app/Models/Plan.php b/app/Models/Plan.php index ba48ef1..84740af 100755 --- a/app/Models/Plan.php +++ b/app/Models/Plan.php @@ -7,7 +7,31 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use InvalidArgumentException; use Carbon\Carbon; +use Illuminate\Database\Eloquent\Relations\HasOne; +/** + * App\Models\Plan + * + * @property int $id + * @property string $name 套餐名称 + * @property int|null $group_id 权限组ID + * @property int $transfer_enable 流量(KB) + * @property int|null $speed_limit 速度限制Mbps + * @property bool $show 是否显示 + * @property bool $renew 是否允许续费 + * @property bool $sell 是否允许购买 + * @property array|null $prices 价格配置 + * @property int $sort 排序 + * @property string|null $content 套餐描述 + * @property int $reset_traffic_method 流量重置方式 + * @property int|null $capacity_limit 订阅人数限制 + * @property int|null $device_limit 设备数量限制 + * @property int $created_at + * @property int $updated_at + * + * @property-read ServerGroup|null $group 关联的权限组 + * @property-read \Illuminate\Database\Eloquent\Collection $order 关联的订单 + */ class Plan extends Model { use HasFactory; @@ -16,12 +40,12 @@ class Plan extends Model protected $dateFormat = 'U'; // 定义流量重置方式 - public const RESET_TRAFFIC_FOLLOW_SYSTEM = 0; // 跟随系统设置 - public const RESET_TRAFFIC_FIRST_DAY_MONTH = 1; // 每月1号 - public const RESET_TRAFFIC_MONTHLY = 2; // 按月重置 - public const RESET_TRAFFIC_NEVER = 3; // 不重置 - public const RESET_TRAFFIC_FIRST_DAY_YEAR = 4; // 每年1月1日 - public const RESET_TRAFFIC_YEARLY = 5; // 按年重置 + public const RESET_TRAFFIC_FOLLOW_SYSTEM = null; // 跟随系统设置 + public const RESET_TRAFFIC_FIRST_DAY_MONTH = 0; // 每月1号 + public const RESET_TRAFFIC_MONTHLY = 1; // 按月重置 + public const RESET_TRAFFIC_NEVER = 2; // 不重置 + public const RESET_TRAFFIC_FIRST_DAY_YEAR = 3; // 每年1月1日 + public const RESET_TRAFFIC_YEARLY = 4; // 按年重置 // 定义价格类型 public const PRICE_TYPE_RESET_TRAFFIC = 'reset_traffic'; // 重置流量价格 @@ -346,7 +370,7 @@ class Plan extends Model return $this->hasMany(User::class); } - public function group() + public function group(): HasOne { return $this->hasOne(ServerGroup::class, 'id', 'group_id'); } @@ -384,4 +408,9 @@ class Plan extends Model $prices[self::PRICE_TYPE_RESET_TRAFFIC] = max(0, $price); $this->prices = $prices; } + + public function order(): HasMany + { + return $this->hasMany(Order::class); + } } \ No newline at end of file diff --git a/app/Models/Server.php b/app/Models/Server.php index 02b51da..ce0047d 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -9,7 +9,45 @@ use Illuminate\Support\Facades\Cache; use App\Utils\CacheKey; use App\Utils\Helper; use App\Models\User; +use Illuminate\Database\Eloquent\Casts\Attribute; +/** + * App\Models\Server + * + * @property int $id + * @property string $name 节点名称 + * @property string $type 服务类型 + * @property string $host 主机地址 + * @property string $port 端口 + * @property string|null $server_port 服务器端口 + * @property array|null $group_ids 分组IDs + * @property array|null $route_ids 路由IDs + * @property array|null $tags 标签 + * @property string|null $show 是否显示 + * @property string|null $allow_insecure 是否允许不安全 + * @property string|null $network 网络类型 + * @property int|null $parent_id 父节点ID + * @property float|null $rate 倍率 + * @property int|null $sort 排序 + * @property array|null $protocol_settings 协议设置 + * @property int $created_at + * @property int $updated_at + * + * @property-read Server|null $parent 父节点 + * @property-read \Illuminate\Database\Eloquent\Collection $stats 节点统计 + * + * @property-read int|null $last_check_at 最后检查时间(Unix时间戳) + * @property-read int|null $last_push_at 最后推送时间(Unix时间戳) + * @property-read int $online 在线用户数 + * @property-read int $is_online 是否在线(1在线 0离线) + * @property-read string $available_status 可用状态描述 + * @property-read string $cache_key 缓存键 + * @property string|null $ports 端口范围 + * @property string|null $password 密码 + * @property int|null $u 上行流量 + * @property int|null $d 下行流量 + * @property int|null $total 总流量 + */ class Server extends Model { public const TYPE_HYSTERIA = 'hysteria'; @@ -19,6 +57,9 @@ class Server extends Model public const TYPE_TUIC = 'tuic'; public const TYPE_SHADOWSOCKS = 'shadowsocks'; public const TYPE_SOCKS = 'socks'; + public const TYPE_NAIVE = 'naive'; + public const TYPE_HTTP = 'http'; + public const TYPE_MIERU = 'mieru'; public const STATUS_OFFLINE = 0; public const STATUS_ONLINE_NO_PUSH = 1; public const STATUS_ONLINE = 2; @@ -53,6 +94,9 @@ class Server extends Model self::TYPE_TUIC, self::TYPE_SHADOWSOCKS, self::TYPE_SOCKS, + self::TYPE_NAIVE, + self::TYPE_HTTP, + self::TYPE_MIERU, ]; protected $table = 'v2_server'; @@ -143,6 +187,32 @@ class Server extends Model 'allow_insecure' => ['type' => 'boolean', 'default' => false] ] ] + ], + self::TYPE_SOCKS => [ + 'tls' => ['type' => 'integer', 'default' => 0], + 'tls_settings' => [ + 'type' => 'object', + 'fields' => [ + 'allow_insecure' => ['type' => 'boolean', 'default' => false] + ] + ] + ], + self::TYPE_NAIVE => [ + 'tls' => ['type' => 'integer', 'default' => 0], + 'tls_settings' => ['type' => 'array', 'default' => null] + ], + self::TYPE_HTTP => [ + 'tls' => ['type' => 'integer', 'default' => 0], + 'tls_settings' => [ + 'type' => 'object', + 'fields' => [ + 'allow_insecure' => ['type' => 'boolean', 'default' => false] + ] + ] + ], + self::TYPE_MIERU => [ + 'transport' => ['type' => 'string', 'default' => 'tcp'], + 'multiplexing' => ['type' => 'string', 'default' => 'MULTIPLEXING_LOW'] ] ]; @@ -174,19 +244,6 @@ class Server extends Model return $result; } - private function getDefaultSettings(array $configs): array - { - $defaults = []; - foreach ($configs as $key => $config) { - if ($config['type'] === 'object') { - $defaults[$key] = $this->getDefaultSettings($config['fields']); - } else { - $defaults[$key] = $config['default']; - } - } - return $defaults; - } - public function getProtocolSettingsAttribute($value) { $settings = json_decode($value, true) ?? []; @@ -206,53 +263,22 @@ class Server extends Model $this->attributes['protocol_settings'] = json_encode($castedSettings); } - public function loadParentCreatedAt(): void - { - if ($this->parent_id) { - $this->created_at = $this->parent()->value('created_at'); - } - } - - public function loadServerStatus(): void - { - $type = strtoupper($this->type); - $serverId = $this->parent_id ?: $this->id; - - $this->last_check_at = Cache::get(CacheKey::get("SERVER_{$type}_LAST_CHECK_AT", $serverId)); - $this->last_push_at = Cache::get(CacheKey::get("SERVER_{$type}_LAST_PUSH_AT", $serverId)); - $this->online = Cache::get(CacheKey::get("SERVER_{$type}_ONLINE_USER", $serverId)) ?? 0; - $this->is_online = (time() - 300 > $this->last_check_at) ? 0 : 1; - $this->available_status = $this->getAvailableStatus(); - $this->cache_key = "{$this->type}-{$this->id}-{$this->updated_at}-{$this->is_online}"; - } - - public function handlePortAllocation(): void - { - if (strpos($this->port, '-') !== false) { - $this->ports = $this->port; - $this->port = Helper::randomPort($this->port); - } else { - $this->port = (int) $this->port; - } - } - - public function generateShadowsocksPassword(User $user): void + public function generateShadowsocksPassword(User $user): string { if ($this->type !== self::TYPE_SHADOWSOCKS) { - return; + return $user->uuid; } - $this->password = $user->uuid; $cipher = data_get($this, 'protocol_settings.cipher'); if (!$cipher || !isset(self::CIPHER_CONFIGURATIONS[$cipher])) { - return; + return $user->uuid; } $config = self::CIPHER_CONFIGURATIONS[$cipher]; $serverKey = Helper::getServerKey($this->created_at, $config['serverKeySize']); $userKey = Helper::uuidToBase64($user->uuid, $config['userKeySize']); - $this->password = "{$serverKey}:{$userKey}"; + return "{$serverKey}:{$userKey}"; } public static function normalizeType(string $type): string @@ -297,4 +323,99 @@ class Server extends Model return ServerRoute::whereIn('id', $this->route_ids)->get(); } + /** + * 最后检查时间访问器 + */ + protected function lastCheckAt(): Attribute + { + return Attribute::make( + get: function () { + $type = strtoupper($this->type); + $serverId = $this->parent_id ?: $this->id; + return Cache::get(CacheKey::get("SERVER_{$type}_LAST_CHECK_AT", $serverId)); + } + ); + } + + /** + * 最后推送时间访问器 + */ + protected function lastPushAt(): Attribute + { + return Attribute::make( + get: function () { + $type = strtoupper($this->type); + $serverId = $this->parent_id ?: $this->id; + return Cache::get(CacheKey::get("SERVER_{$type}_LAST_PUSH_AT", $serverId)); + } + ); + } + + /** + * 在线用户数访问器 + */ + protected function online(): Attribute + { + return Attribute::make( + get: function () { + $type = strtoupper($this->type); + $serverId = $this->parent_id ?: $this->id; + return Cache::get(CacheKey::get("SERVER_{$type}_ONLINE_USER", $serverId)) ?? 0; + } + ); + } + + /** + * 是否在线访问器 + */ + protected function isOnline(): Attribute + { + return Attribute::make( + get: function () { + return (time() - 300 > $this->last_check_at) ? 0 : 1; + } + ); + } + + /** + * 可用状态访问器 + */ + protected function availableStatus(): Attribute + { + return Attribute::make( + get: function () { + if ($this->is_online) { + return true; + } + return false; + } + ); + } + + /** + * 缓存键访问器 + */ + protected function cacheKey(): Attribute + { + return Attribute::make( + get: function () { + return "{$this->type}-{$this->id}-{$this->updated_at}-{$this->is_online}"; + } + ); + } + + /** + * 服务器密钥访问器 + */ + protected function serverKey(): Attribute + { + return Attribute::make( + get: function () { + if ($this->type === self::TYPE_SHADOWSOCKS) { + return Helper::getServerKey($this->created_at, 16); + } + return null; + } + ); + } } diff --git a/app/Models/ServerGroup.php b/app/Models/ServerGroup.php index 3a47144..57f3514 100755 --- a/app/Models/ServerGroup.php +++ b/app/Models/ServerGroup.php @@ -4,7 +4,17 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Casts\Attribute; +/** + * App\Models\ServerGroup + * + * @property int $id + * @property string $name 分组名 + * @property int $created_at + * @property int $updated_at + * @property-read int $server_count 服务器数量 + */ class ServerGroup extends Model { protected $table = 'v2_server_group'; @@ -23,4 +33,14 @@ class ServerGroup extends Model { return Server::whereJsonContains('group_ids', (string) $this->id)->get(); } + + /** + * 获取服务器数量 + */ + protected function serverCount(): Attribute + { + return Attribute::make( + get: fn () => Server::whereJsonContains('group_ids', (string) $this->id)->count(), + ); + } } diff --git a/app/Models/StatServer.php b/app/Models/StatServer.php index fc4cbd1..efa1fc6 100644 --- a/app/Models/StatServer.php +++ b/app/Models/StatServer.php @@ -4,6 +4,18 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +/** + * App\Models\StatServer + * + * @property int $id + * @property int $server_id 服务器ID + * @property int $u 上行流量 + * @property int $d 下行流量 + * @property int $record_at 记录时间 + * @property int $created_at + * @property int $updated_at + * @property-read int $value 通过SUM(u + d)计算的总流量值,仅在查询指定时可用 + */ class StatServer extends Model { protected $table = 'v2_stat_server'; diff --git a/app/Models/StatUser.php b/app/Models/StatUser.php index 07984d9..a956bd7 100644 --- a/app/Models/StatUser.php +++ b/app/Models/StatUser.php @@ -4,6 +4,18 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +/** + * App\Models\StatUser + * + * @property int $id + * @property int $user_id 用户ID + * @property int $u 上行流量 + * @property int $d 下行流量 + * @property int $record_at 记录时间 + * @property int $created_at + * @property int $updated_at + * @property-read int $value 通过SUM(u + d)计算的总流量值,仅在查询指定时可用 + */ class StatUser extends Model { protected $table = 'v2_stat_user'; diff --git a/app/Models/Ticket.php b/app/Models/Ticket.php index b92a5e9..86ba9b6 100644 --- a/app/Models/Ticket.php +++ b/app/Models/Ticket.php @@ -3,7 +3,25 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; +/** + * App\Models\Ticket + * + * @property int $id + * @property int $user_id 用户ID + * @property string $subject 工单主题 + * @property string|null $level 工单等级 + * @property int $status 工单状态 + * @property int|null $reply_status 回复状态 + * @property int|null $last_reply_user_id 最后回复人 + * @property int $created_at + * @property int $updated_at + * + * @property-read User $user 关联的用户 + * @property-read \Illuminate\Database\Eloquent\Collection $messages 关联的工单消息 + */ class Ticket extends Model { protected $table = 'v2_ticket'; @@ -21,16 +39,21 @@ class Ticket extends Model self::STATUS_CLOSED => '关闭' ]; - public function user() + public function user(): BelongsTo { return $this->belongsTo(User::class, 'user_id', 'id'); } - public function messages() + + /** + * 关联的工单消息 + */ + public function messages(): HasMany { return $this->hasMany(TicketMessage::class, 'ticket_id', 'id'); } + // 即将删除 - public function message() + public function message(): HasMany { return $this->hasMany(TicketMessage::class, 'ticket_id', 'id'); } diff --git a/app/Models/TicketMessage.php b/app/Models/TicketMessage.php index 4673b33..0cc3c95 100644 --- a/app/Models/TicketMessage.php +++ b/app/Models/TicketMessage.php @@ -3,7 +3,20 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +/** + * App\Models\TicketMessage + * + * @property int $id + * @property int $ticket_id + * @property int $user_id + * @property string $message + * @property \Illuminate\Support\Carbon $created_at + * @property \Illuminate\Support\Carbon $updated_at + * @property-read \App\Models\Ticket $ticket 关联的工单 + * @property-read bool $is_me 当前消息是否由工单发起人发送 + */ class TicketMessage extends Model { protected $table = 'v2_ticket_message'; @@ -13,4 +26,22 @@ class TicketMessage extends Model 'created_at' => 'timestamp', 'updated_at' => 'timestamp' ]; + + protected $appends = ['is_me']; + + /** + * 关联的工单 + */ + public function ticket(): BelongsTo + { + return $this->belongsTo(Ticket::class, 'ticket_id', 'id'); + } + + /** + * 判断消息是否由工单发起人发送 + */ + public function getIsMeAttribute(): bool + { + return $this->ticket->user_id === $this->user_id; + } } diff --git a/app/Models/User.php b/app/Models/User.php index e23baa0..238130a 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,10 +2,55 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Model; +use App\Utils\Helper; use Illuminate\Foundation\Auth\User as Authenticatable; use Laravel\Sanctum\HasApiTokens; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; +/** + * App\Models\User + * + * @property int $id 用户ID + * @property string $email 邮箱 + * @property string $password 密码 + * @property string|null $password_algo 加密方式 + * @property string|null $password_salt 加密盐 + * @property string $token 邀请码 + * @property string $uuid + * @property int|null $invite_user_id 邀请人 + * @property int|null $plan_id 订阅ID + * @property int|null $group_id 权限组ID + * @property int|null $transfer_enable 流量(KB) + * @property int|null $speed_limit 限速Mbps + * @property int|null $u 上行流量 + * @property int|null $d 下行流量 + * @property int|null $banned 是否封禁 + * @property int|null $remind_expire 到期提醒 + * @property int|null $remind_traffic 流量提醒 + * @property int|null $expired_at 过期时间 + * @property int|null $balance 余额 + * @property int|null $commission_balance 佣金余额 + * @property float $commission_rate 返佣比例 + * @property int|null $device_limit 设备限制数量 + * @property int|null $discount 折扣 + * @property int|null $last_login_at 最后登录时间 + * @property int|null $parent_id 父账户ID + * @property int|null $is_admin 是否管理员 + * @property int $created_at + * @property int $updated_at + * @property bool $commission_auto_check 是否自动计算佣金 + * + * @property-read User|null $invite_user 邀请人信息 + * @property-read \App\Models\Plan|null $plan 用户订阅计划 + * @property-read ServerGroup|null $group 权限组 + * @property-read \Illuminate\Database\Eloquent\Collection $codes 邀请码列表 + * @property-read \Illuminate\Database\Eloquent\Collection $orders 订单列表 + * @property-read \Illuminate\Database\Eloquent\Collection $stat 统计信息 + * @property-read \Illuminate\Database\Eloquent\Collection $tickets 工单列表 + * @property-read User|null $parent 父账户 + * @property-read string $subscribe_url 订阅链接(动态生成) + */ class User extends Authenticatable { use HasApiTokens; @@ -14,52 +59,72 @@ class User extends Authenticatable protected $guarded = ['id']; protected $casts = [ 'created_at' => 'timestamp', - 'updated_at' => 'timestamp' + 'updated_at' => 'timestamp', + 'banned' => 'boolean', + 'remind_expire' => 'boolean', + 'remind_traffic' => 'boolean', + 'commission_auto_check' => 'boolean', + 'commission_rate' => 'float' ]; protected $hidden = ['password']; + public const COMMISSION_TYPE_SYSTEM = 0; + public const COMMISSION_TYPE_PERIOD = 1; + public const COMMISSION_TYPE_ONETIME = 2; + // 获取邀请人信息 - public function invite_user() + public function invite_user(): BelongsTo { return $this->belongsTo(self::class, 'invite_user_id', 'id'); } - // 获取用户套餐 - public function plan() + /** + * 获取用户订阅计划 + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function plan(): BelongsTo { return $this->belongsTo(Plan::class, 'plan_id', 'id'); } - public function group() + public function group(): BelongsTo { return $this->belongsTo(ServerGroup::class, 'group_id', 'id'); } // 获取用户邀请码列表 - public function codes() + public function codes(): HasMany { return $this->hasMany(InviteCode::class, 'user_id', 'id'); } - public function orders() + public function orders(): HasMany { return $this->hasMany(Order::class, 'user_id', 'id'); } - public function stat() + public function stat(): HasMany { return $this->hasMany(StatUser::class, 'user_id', 'id'); } // 关联工单列表 - public function tickets() + public function tickets(): HasMany { return $this->hasMany(Ticket::class, 'user_id', 'id'); } - public function parent() + public function parent(): BelongsTo { return $this->belongsTo(self::class, 'parent_id', 'id'); } + + /** + * 获取订阅链接属性 + */ + public function getSubscribeUrlAttribute(): string + { + return Helper::getSubscribeUrl($this->token); + } } diff --git a/app/Payments/BTCPay.php b/app/Payments/BTCPay.php index 00a3cea..432fc17 100644 --- a/app/Payments/BTCPay.php +++ b/app/Payments/BTCPay.php @@ -83,7 +83,6 @@ class BTCPay implements PaymentInterface if (!self::hashEqual($signraturHeader, $computedSignature)) { throw new ApiException('HMAC signature does not match', 400); - return false; } //get order id store in metadata @@ -112,8 +111,8 @@ class BTCPay implements PaymentInterface $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_HEADER, 0); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_HEADER, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 300); curl_setopt($ch, CURLOPT_POSTFIELDS, $params); curl_setopt( diff --git a/app/Payments/BinancePay.php b/app/Payments/BinancePay.php new file mode 100644 index 0000000..7b727ba --- /dev/null +++ b/app/Payments/BinancePay.php @@ -0,0 +1,118 @@ +config = $config; + } + + public function form() + { + return [ + 'api_key' => [ + 'label' => 'API Key', + 'type' => 'input', + 'description' => '请输入您的 Binance API Key' + ], + 'secret_key' => [ + 'label' => 'Secret Key', + 'type' => 'input', + 'description' => '请输入您的 Binance Secret Key' + ] + ]; + } + + public function pay($order) + { + $timestamp = intval(microtime(true) * 1000); // Timestamp in milliseconds + $nonceStr = bin2hex(random_bytes(16)); // Generating a nonce + $request = [ + "env" => [ + "terminalType" => "APP" + ], + 'merchantTradeNo' => strval($order['trade_no']), + 'fiatCurrency' => 'CNY', + 'fiatAmount' => ($order["total_amount"] / 100), + 'supportPayCurrency' => "USDT,BNB", + 'description' => strval($order['trade_no']), + 'webhookUrl' => $order['notify_url'], + 'returnUrl' => $order['return_url'], + "goodsDetails" => [ + [ + "goodsType" => "01", + "goodsCategory" => "D000", + "referenceGoodsId" => "7876763A3B", + "goodsName" => "Ice Cream", + "goodsDetail" => "Greentea ice cream cone" + ] + ] + ]; + $body = json_encode($request, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, 'https://bpay.binanceapi.com/binancepay/openapi/v3/order'); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json; charset=utf-8', + 'BinancePay-Timestamp: ' . $timestamp, + 'BinancePay-Nonce: ' . $nonceStr, + 'BinancePay-Certificate-SN: ' . $this->config['api_key'], + 'BinancePay-Signature: ' . $this->generateSignature($body, $this->config['secret_key'], $timestamp, $nonceStr), + ]); + curl_setopt($ch, CURLOPT_PROXY, "socks5h://154.3.37.204:47714"); + curl_setopt($ch, CURLOPT_PROXYUSERPWD, "GGn28Io5fW:9VkWfoPGiG"); + $response = curl_exec($ch); + curl_close($ch); + if (!$response) { + abort(400, '支付失败,请稍后再试'); + } + $res = json_decode($response, true); + \Log::channel('daily')->info($res); + if (!is_array($res)) { + abort(400, '支付失败,请稍后再试'); + } + if (isset($res['code']) && $res['code'] == '400201') { + $res['data'] = \Cache::get('CheckoutInfo_' . strval($order['trade_no'])); + } + if (!isset($res['data'])) { + abort(400, '支付失败,请稍后再试'); + } + if (!is_array($res['data']) || !isset($res['data']['checkoutUrl'])) { + abort(400, '支付失败,请稍后再试'); + } + // 缓存支付信息 + \Cache::put('CheckoutInfo_' . strval($order['trade_no']), $res['data']); + return [ + 'type' => 1, // 0:qrcode 1:url + 'data' => $res['data']['checkoutUrl'] + ]; + } + + public function notify($params) + { + $bizStatus = $params['bizStatus']; + if ($bizStatus !== 'PAY_SUCCESS'){ + return false; + } + $data = json_decode($params['data'], true); + + return [ + 'trade_no' => $data['merchantTradeNo'], + 'callback_no' => $params['bizIdStr'], + 'custom_result' => '{"returnCode":"SUCCESS","returnMessage":null}' + ]; + } + private function generateSignature($body, $secret, $timestamp, $nonceStr) + { + $payload = $timestamp . chr(0x0A) . $nonceStr . chr(0x0A) . $body . chr(0x0A); + return strtoupper(hash_hmac('sha512', $payload, $secret)); + } +} diff --git a/app/Payments/Coinbase.php b/app/Payments/Coinbase.php index bb5c3bb..9c9cdc2 100644 --- a/app/Payments/Coinbase.php +++ b/app/Payments/Coinbase.php @@ -95,8 +95,8 @@ class Coinbase implements PaymentInterface $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_HEADER, 0); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_HEADER, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 300); curl_setopt($ch, CURLOPT_POSTFIELDS, $params); curl_setopt( diff --git a/app/Payments/EPayWxpay.php b/app/Payments/EPayWxpay.php new file mode 100644 index 0000000..db1deca --- /dev/null +++ b/app/Payments/EPayWxpay.php @@ -0,0 +1,71 @@ +config = $config; + } + + public function form() + { + return [ + 'url' => [ + 'label' => 'URL', + 'description' => '', + 'type' => 'input', + ], + 'pid' => [ + 'label' => 'PID', + 'description' => '', + 'type' => 'input', + ], + 'key' => [ + 'label' => 'KEY', + 'description' => '', + 'type' => 'input', + ] + ]; + } + + public function pay($order) + { + $params = [ + 'money' => $order['total_amount'] / 100, + 'type' => 'wxpay', + 'name' => $order['trade_no'], + 'notify_url' => $order['notify_url'], + 'return_url' => $order['return_url'], + 'out_trade_no' => $order['trade_no'], + 'pid' => $this->config['pid'] + ]; + ksort($params); + reset($params); + $str = stripslashes(urldecode(http_build_query($params))) . $this->config['key']; + $params['sign'] = md5($str); + $params['sign_type'] = 'MD5'; + return [ + 'type' => 1, // 0:qrcode 1:url + 'data' => $this->config['url'] . '/submit.php?' . http_build_query($params) + ]; + } + + public function notify($params) + { + $sign = $params['sign']; + unset($params['sign']); + unset($params['sign_type']); + ksort($params); + reset($params); + $str = stripslashes(urldecode(http_build_query($params))) . $this->config['key']; + if ($sign !== md5($str)) { + return false; + } + return [ + 'trade_no' => $params['out_trade_no'], + 'callback_no' => $params['trade_no'] + ]; + } +} diff --git a/app/Payments/HiiCashPayment.php b/app/Payments/HiiCashPayment.php new file mode 100644 index 0000000..8571653 --- /dev/null +++ b/app/Payments/HiiCashPayment.php @@ -0,0 +1,113 @@ +config = $config; + } + + public function form() + { + return [ + 'rate' => [ + 'label' => '汇率', + 'description' => 'HiiCash支付单位为美元,如果您站点金额单位不为美元则需要填写汇率', + 'type' => 'input', + 'default' => '2333' + ], + 'pid' => [ + 'label' => '商户号', + 'description' => '', + 'type' => 'input', + ], + 'appid' => [ + 'label' => '应用ID', + 'description' => '', + 'type' => 'input' + ], + 'key' => [ + 'label' => '私钥', + 'description' => '', + 'type' => 'input', + ] + ]; + } + + public function pay($order) + { + + $request = [ + "mchNo" => $this->config["pid"], + "appId" => $this->config["appid"], + "mchOrderNo" => $order["trade_no"], + "amount" => ceil(($order["total_amount"] * 100) / ($this->config['rate'] ?? "1")) / 100, + "payDataType" => "Cashier", + "currency" => "USD", + "subject" => $order["trade_no"], + "notifyUrl" => $order["notify_url"], + "returnUrl" => $order["return_url"], + ]; + $headers = [ + "HiicashPay-Timestamp" => (int)(string)floor(microtime(true) * 1000), + "HiicashPay-Nonce" => \Str::random(32), + "HiicashPay-AppId" => $this->config["appid"] + ]; + $body = json_encode($request, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $payload = $headers['HiicashPay-Timestamp'] . chr(0x0A) . $headers['HiicashPay-Nonce'] . chr(0x0A) . $body . chr(0x0A); + $signature = $this->generate_signature($payload, $this->config['key']); + $headers["HiicashPay-Signature"] = $signature; + $jsonStr = json_encode($request, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $httpHeaders = []; + foreach ($headers as $key => $header) { + $httpHeaders[] = $key . ': ' . $header; + } + $httpHeaders[] = 'Content-Type: application/json; charset=utf-8'; + $httpHeaders[] = 'Content-Length: ' . strlen($jsonStr); + $ch = curl_init(file_get_contents('https://hiicash.oss-ap-northeast-1.aliyuncs.com/gateway.txt') . 'pay/order/create'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonStr); + curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeaders); + $response = curl_exec($ch); + curl_close($ch); + if (!$response) { + abort(400, '支付失败,请稍后再试'); + } + $res = json_decode($response, true); + if (!is_array($res) || !isset($res['data'])) { + abort(400, '支付失败,请稍后再试'); + } + if (!is_array($res['data']) || !isset($res['data']['payData'])) { + abort(400, '支付失败,请稍后再试'); + } + return [ + 'type' => 1, // 0:qrcode 1:url + 'data' => $res['data']['payData'] + ]; + } + + public function notify($params) + { + if (!isset($params['mchOrderNo']) || !isset($params['mchOrderNo'])) { + return false; + } + return [ + 'trade_no' => $params['mchOrderNo'], + 'callback_no' => $params['payOrderId'], + 'custom_result' => '{"returnCode": "success","returnMsg": ""}' + ]; + } + + + // 使用 HMAC-SHA512 算法生成签名 + function generate_signature(string $payload, string $secret_key) + { + $hash = hash_hmac("sha512", $payload, $secret_key, true); + // 将签名转换为大写字符串 + return strtoupper(bin2hex($hash)); + } +} diff --git a/app/Payments/PayPal.php b/app/Payments/PayPal.php new file mode 100644 index 0000000..544f9b4 --- /dev/null +++ b/app/Payments/PayPal.php @@ -0,0 +1,132 @@ +config = $config; + $this->client = new Client(); + $this->apiHost = optional($this->config)['mode'] == 'sandbox' ? "https://api.sandbox.paypal.com" : "https://api.paypal.com"; + } + + public function form() + { + return [ + 'mode' => [ + 'label' => 'Mode', + 'description' => '沙箱/生产模式 sandbox/live', + 'type' => 'input', + ], + 'client_id' => [ + 'label' => 'Client ID', + 'description' => 'PayPal Client ID', + 'type' => 'input', + ], + 'client_secret' => [ + 'label' => 'Client Secret', + 'description' => 'PayPal Client Secret', + 'type' => 'input', + ], + 'rate' => [ + 'label' => '汇率', + 'description' => 'Paypal支付单位为USD,如果您站点金额单位不为USD则需要填写汇率', + 'type' => 'input', + 'default' => '2333' + ], + ]; + } + + public function pay($order) + { + $this->token = json_decode($this->client->post("{$this->apiHost}/v1/oauth2/token", [ + 'auth' => [$this->config['client_id'], $this->config['client_secret']], + 'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'], + 'form_params' => ['grant_type' => 'client_credentials'] + ])->getBody(), true)['access_token']; + // 创建订单 + $order = json_decode($this->client->request('POST', "{$this->apiHost}/v2/checkout/orders", [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'PayPal-Request-Id' => $order['trade_no'], + 'Authorization' => "Bearer {$this->token}" + ], + 'json' => [ + 'intent' => 'CAPTURE', + 'purchase_units' => [ + [ + "reference_id" => $order['trade_no'], + "amount" => [ + "currency_code" => "USD", + "value" => number_format(ceil(($order["total_amount"] * 100) / ($this->config['rate'] ?? "1")) / 10000, 2, '.', '') + ] + ] + ], + "payment_source" => [ + "paypal" => [ + "experience_context" => [ + "payment_method_preference" => "UNRESTRICTED", + "brand_name" => $order['trade_no'], + "locale" => "zh-CN", + "landing_page" => "NO_PREFERENCE", + "shipping_preference" => "NO_SHIPPING", + "user_action" => "PAY_NOW", + "return_url" => $order['return_url'], + "cancel_url" => $order['return_url'] + ] + ] + ] + ] + + ])->getBody(), true); + + $payerActionUrl = ''; + foreach ($order['links'] as $link) { + if ($link['rel'] === 'payer-action') { + $payerActionUrl = $link['href']; + break; + } + } + + return [ + 'type' => 1, // 0:qrcode 1:url + 'data' => $payerActionUrl + ]; + + } + + public function notify($params) + { + $this->token = json_decode($this->client->post("{$this->apiHost}/v1/oauth2/token", [ + 'auth' => [$this->config['client_id'], $this->config['client_secret']], + 'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'], + 'form_params' => ['grant_type' => 'client_credentials'] + ])->getBody(), true)['access_token']; + $resource = $params['resource']; + $purchase_units = $resource['purchase_units']; + if ($params['event_type'] == 'CHECKOUT.ORDER.APPROVED') { + $order = json_decode($this->client->request('POST', "{$this->apiHost}/v2/checkout/orders/{$resource['id']}/capture", [ + "headers" => [ + 'Content-Type' => 'application/json', + 'Authorization' => "Bearer {$this->token}" + ] + ])->getBody(), true); + if ($order['status'] == 'COMPLETED') { + return [ + 'trade_no' => $purchase_units[0]['reference_id'], + 'callback_no' => $order['id'] + ]; + } + } + return false; + + } +} diff --git a/app/Protocols/Clash.php b/app/Protocols/Clash.php index 63bca4b..2946e02 100644 --- a/app/Protocols/Clash.php +++ b/app/Protocols/Clash.php @@ -28,7 +28,7 @@ class Clash implements ProtocolInterface $servers = $this->servers; $user = $this->user; $appName = admin_setting('app_name', 'XBoard'); - + // 优先从 admin_setting 获取模板 $template = admin_setting('subscribe_template_clash'); if (empty($template)) { @@ -40,7 +40,7 @@ class Clash implements ProtocolInterface $template = file_get_contents($defaultConfig); } } - + $config = Yaml::parse($template); $proxy = []; $proxies = []; @@ -67,6 +67,14 @@ class Clash implements ProtocolInterface array_push($proxy, self::buildTrojan($user['uuid'], $item)); array_push($proxies, $item['name']); } + if ($item['type'] === 'socks') { + array_push($proxy, self::buildSocks5($user['uuid'], $item)); + array_push($proxies, $item['name']); + } + if ($item['type'] === 'http') { + array_push($proxy, self::buildHttp($user['uuid'], $item)); + array_push($proxies, $item['name']); + } } $config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy); @@ -171,7 +179,7 @@ class Clash implements ProtocolInterface if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') { $array['http-opts'] = [ 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'), - 'path' => \Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])) + 'path' => \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])) ]; } break; @@ -231,9 +239,56 @@ class Clash implements ProtocolInterface return $array; } + public static function buildSocks5($password, $server) + { + $protocol_settings = $server['protocol_settings']; + $array = []; + $array['name'] = $server['name']; + $array['type'] = 'socks5'; + $array['server'] = $server['host']; + $array['port'] = $server['port']; + $array['udp'] = true; + + $array['username'] = $password; + $array['password'] = $password; + + // TLS 配置 + if (data_get($protocol_settings, 'tls')) { + $array['tls'] = true; + $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false); + } + + return $array; + } + + public static function buildHttp($password, $server) + { + $protocol_settings = $server['protocol_settings']; + $array = []; + $array['name'] = $server['name']; + $array['type'] = 'http'; + $array['server'] = $server['host']; + $array['port'] = $server['port']; + + $array['username'] = $password; + $array['password'] = $password; + + // TLS 配置 + if (data_get($protocol_settings, 'tls')) { + $array['tls'] = true; + $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false); + } + + return $array; + } + private function isMatch($exp, $str) { - return @preg_match($exp, $str); + try { + return preg_match($exp, $str) === 1; + } catch (\Exception $e) { + return false; + } } private function isRegex($exp) @@ -241,6 +296,10 @@ class Clash implements ProtocolInterface if (empty($exp)) { return false; } - return @preg_match((string) $exp, '') !== false; + try { + return preg_match($exp, '') !== false; + } catch (\Exception $e) { + return false; + } } } diff --git a/app/Protocols/ClashMeta.php b/app/Protocols/ClashMeta.php index c14c7a2..bac28f2 100644 --- a/app/Protocols/ClashMeta.php +++ b/app/Protocols/ClashMeta.php @@ -13,7 +13,11 @@ class ClashMeta implements ProtocolInterface private $servers; private $user; - public function __construct($user, $servers, array $options = null) + /** + * @param mixed $user 用户实例 + * @param array $servers 服务器列表 + */ + public function __construct($user, $servers) { $this->user = $user; $this->servers = $servers; @@ -78,6 +82,18 @@ class ClashMeta implements ProtocolInterface array_push($proxy, self::buildTuic($user['uuid'], $item)); array_push($proxies, $item['name']); } + if ($item['type'] === 'socks') { + array_push($proxy, self::buildSocks5($user['uuid'], $item)); + array_push($proxies, $item['name']); + } + if ($item['type'] === 'http') { + array_push($proxy, self::buildHttp($user['uuid'], $item)); + array_push($proxies, $item['name']); + } + if ($item['type'] === 'mieru') { + array_push($proxy, self::buildMieru($user['uuid'], $item)); + array_push($proxies, $item['name']); + } } $config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy); @@ -176,7 +192,7 @@ class ClashMeta implements ProtocolInterface if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') { $array['http-opts'] = [ 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'), - 'path' => \Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])) + 'path' => \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])) ]; } break; @@ -369,9 +385,78 @@ class ClashMeta implements ProtocolInterface return $array; } + public static function buildMieru($password, $server) + { + $protocol_settings = data_get($server, 'protocol_settings', []); + $array = [ + 'name' => $server['name'], + 'type' => 'mieru', + 'server' => $server['host'], + 'port' => $server['port'], + 'username' => $password, + 'password' => $password, + 'transport' => strtoupper(data_get($protocol_settings, 'transport', 'TCP')), + 'multiplexing' => data_get($protocol_settings, 'multiplexing', 'MULTIPLEXING_LOW') + ]; + + // 如果配置了端口范围 + if (isset($server['ports'])) { + $array['port-range'] = $server['ports']; + } + + return $array; + } + + public static function buildSocks5($password, $server) + { + $protocol_settings = $server['protocol_settings']; + $array = []; + $array['name'] = $server['name']; + $array['type'] = 'socks5'; + $array['server'] = $server['host']; + $array['port'] = $server['port']; + $array['udp'] = true; + + $array['username'] = $password; + $array['password'] = $password; + + // TLS 配置 + if (data_get($protocol_settings, 'tls')) { + $array['tls'] = true; + $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false); + } + + return $array; + } + + public static function buildHttp($password, $server) + { + $protocol_settings = $server['protocol_settings']; + $array = []; + $array['name'] = $server['name']; + $array['type'] = 'http'; + $array['server'] = $server['host']; + $array['port'] = $server['port']; + + $array['username'] = $password; + $array['password'] = $password; + + // TLS 配置 + if (data_get($protocol_settings, 'tls')) { + $array['tls'] = true; + $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false); + } + + return $array; + } + private function isMatch($exp, $str) { - return @preg_match($exp, $str); + try { + return preg_match($exp, $str) === 1; + } catch (\Exception $e) { + return false; + } } private function isRegex($exp) @@ -379,6 +464,10 @@ class ClashMeta implements ProtocolInterface if (empty($exp)) { return false; } - return @preg_match($exp, '') !== false; + try { + return preg_match($exp, '') !== false; + } catch (\Exception $e) { + return false; + } } } diff --git a/app/Protocols/General.php b/app/Protocols/General.php index 5c18caa..6da11f3 100644 --- a/app/Protocols/General.php +++ b/app/Protocols/General.php @@ -5,7 +5,7 @@ namespace App\Protocols; use App\Contracts\ProtocolInterface; use App\Utils\Helper; - +use Illuminate\Support\Arr; class General implements ProtocolInterface { public $flags = ['general', 'v2rayn', 'v2rayng', 'passwall', 'ssrplus', 'sagernet']; @@ -45,6 +45,9 @@ class General implements ProtocolInterface if ($item['type'] === 'hysteria') { $uri .= self::buildHysteria($user['uuid'], $item); } + if ($item['type'] === 'socks') { + $uri .= self::buildSocks($user['uuid'], $item); + } } return base64_encode($uri); } @@ -87,8 +90,11 @@ class General implements ProtocolInterface case 'tcp': if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') { $config['type'] = data_get($protocol_settings, 'network_settings.header.type', 'http'); - $config['path'] = \Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])); - $config['host'] = data_get($protocol_settings, 'network_settings.headers.Host') ? \Arr::random(data_get($protocol_settings, 'network_settings.headers.Host'), ['/']) : null; + $config['path'] = Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])); + $config['host'] = + data_get($protocol_settings, 'network_settings.headers.Host') + ? Arr::random(data_get($protocol_settings, 'network_settings.headers.Host', ['/']), ) + : null; } break; case 'ws': @@ -249,4 +255,11 @@ class General implements ProtocolInterface return $uri; } + public static function buildSocks($password, $server) + { + $name = rawurlencode($server['name']); + $credentials = base64_encode("{$password}:{$password}"); + return "socks://{$credentials}@{$server['host']}:{$server['port']}#{$name}\r\n"; + } + } diff --git a/app/Protocols/QuantumultX.php b/app/Protocols/QuantumultX.php index 25cf6a3..b759a4a 100644 --- a/app/Protocols/QuantumultX.php +++ b/app/Protocols/QuantumultX.php @@ -53,7 +53,6 @@ class QuantumultX implements ProtocolInterface 'udp-relay=true', "tag={$server['name']}" ]; - $config = array_filter($config); $uri = implode(',', $config); $uri .= "\r\n"; return $uri; diff --git a/app/Protocols/Shadowrocket.php b/app/Protocols/Shadowrocket.php index 017b840..fe63e05 100644 --- a/app/Protocols/Shadowrocket.php +++ b/app/Protocols/Shadowrocket.php @@ -89,9 +89,9 @@ class Shadowrocket implements ProtocolInterface if ($protocol_settings['tls']) { $config['tls'] = 1; if (data_get($protocol_settings, 'tls_settings')) { - if (data_get($protocol_settings, 'tls_settings.allow_insecure') && !empty(data_get($protocol_settings, 'tls_settings.allow_insecure'))) + if (!!data_get($protocol_settings, 'tls_settings.allow_insecure')) $config['allowInsecure'] = (int) data_get($protocol_settings, 'tls_settings.allow_insecure'); - if (data_get($protocol_settings, 'tls_settings.server_name') && !empty(data_get($protocol_settings, 'tls_settings.server_name'))) + if (!!data_get($protocol_settings, 'tls_settings.server_name')) $config['peer'] = data_get($protocol_settings, 'tls_settings.server_name'); } } @@ -100,8 +100,8 @@ class Shadowrocket implements ProtocolInterface case 'tcp': if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') { $config['obfs'] = data_get($protocol_settings, 'network_settings.header.type'); - $config['path'] = \Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])); - $config['obfsParam'] = \Arr::random(data_get($protocol_settings, 'network_settings.header.request.headers.Host', ['www.example.com'])); + $config['path'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])); + $config['obfsParam'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.headers.Host', ['www.example.com'])); } break; case 'ws': @@ -168,8 +168,8 @@ class Shadowrocket implements ProtocolInterface case 'tcp': if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') { $config['obfs'] = data_get($protocol_settings, 'network_settings.header.type'); - $config['path'] = \Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])); - $config['obfsParam'] = \Arr::random(data_get($protocol_settings, 'network_settings.header.request.headers.Host', ['www.example.com'])); + $config['path'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])); + $config['obfsParam'] = \Illuminate\Support\Arr::random(data_get($protocol_settings, 'network_settings.header.request.headers.Host', ['www.example.com'])); } break; case 'ws': @@ -225,6 +225,8 @@ class Shadowrocket implements ProtocolInterface public static function buildHysteria($password, $server) { $protocol_settings = $server['protocol_settings']; + $uri = ''; // 初始化变量 + switch (data_get($protocol_settings, 'version')) { case 1: $params = [ diff --git a/app/Protocols/Shadowsocks.php b/app/Protocols/Shadowsocks.php index dd76370..3bebc48 100644 --- a/app/Protocols/Shadowsocks.php +++ b/app/Protocols/Shadowsocks.php @@ -47,7 +47,7 @@ class Shadowsocks implements ProtocolInterface $subs['version'] = 1; $subs['bytes_used'] = $bytesUsed; $subs['bytes_remaining'] = $bytesRemaining; - $subs['servers'] = array_merge($subs['servers'] ? $subs['servers'] : [], $configs); + $subs['servers'] = array_merge($subs['servers'], $configs); return json_encode($subs, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); } diff --git a/app/Protocols/SingBox.php b/app/Protocols/SingBox.php index 192ea36..877c568 100644 --- a/app/Protocols/SingBox.php +++ b/app/Protocols/SingBox.php @@ -11,7 +11,7 @@ class SingBox implements ProtocolInterface private $user; private $config; - public function __construct($user, $servers, array $options = null) + public function __construct($user, $servers) { $this->user = $user; $this->servers = $servers; @@ -84,6 +84,14 @@ class SingBox implements ProtocolInterface $tuicConfig = $this->buildTuic($this->user['uuid'], $item); $proxies[] = $tuicConfig; } + if ($item['type'] === 'socks') { + $socksConfig = $this->buildSocks($this->user['uuid'], $item); + $proxies[] = $socksConfig; + } + if ($item['type'] === 'http') { + $httpConfig = $this->buildHttp($this->user['uuid'], $item); + $proxies[] = $httpConfig; + } } foreach ($outbounds as &$outbound) { if (in_array($outbound['type'], ['urltest', 'selector'])) { @@ -361,4 +369,58 @@ class SingBox implements ProtocolInterface return $array; } + + protected function buildSocks($password, $server): array + { + $protocol_settings = data_get($server, 'protocol_settings', []); + $array = [ + 'type' => 'socks', + 'tag' => $server['name'], + 'server' => $server['host'], + 'server_port' => $server['port'], + 'version' => '5', // 默认使用 socks5 + 'username' => $password, + 'password' => $password, + ]; + + if (data_get($protocol_settings, 'udp_over_tcp')) { + $array['udp_over_tcp'] = true; + } + + return $array; + } + + protected function buildHttp($password, $server): array + { + $protocol_settings = data_get($server, 'protocol_settings', []); + $array = [ + 'type' => 'http', + 'tag' => $server['name'], + 'server' => $server['host'], + 'server_port' => $server['port'], + 'username' => $password, + 'password' => $password, + ]; + + if ($path = data_get($protocol_settings, 'path')) { + $array['path'] = $path; + } + + if ($headers = data_get($protocol_settings, 'headers')) { + $array['headers'] = $headers; + } + + if (data_get($protocol_settings, 'tls')) { + $array['tls'] = [ + 'enabled' => true, + 'insecure' => (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false), + ]; + + if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { + $array['tls']['server_name'] = $serverName; + } + } + + return $array; + } } diff --git a/app/Protocols/Stash.php b/app/Protocols/Stash.php index 927e95e..4d2522c 100644 --- a/app/Protocols/Stash.php +++ b/app/Protocols/Stash.php @@ -82,6 +82,18 @@ class Stash implements ProtocolInterface array_push($proxy, self::buildTrojan($user['uuid'], $item)); array_push($proxies, $item['name']); } + if ($item['type'] === 'tuic') { + array_push($proxy, self::buildTuic($user['uuid'], $item)); + array_push($proxies, $item['name']); + } + if ($item['type'] === 'socks') { + array_push($proxy, self::buildSocks5($user['uuid'], $item)); + array_push($proxies, $item['name']); + } + if ($item['type'] === 'http') { + array_push($proxy, self::buildHttp($user['uuid'], $item)); + array_push($proxies, $item['name']); + } } $config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy); @@ -289,12 +301,91 @@ class Stash implements ProtocolInterface } + public static function buildTuic($password, $server) + { + $protocol_settings = data_get($server, 'protocol_settings', []); + $array = [ + 'name' => $server['name'], + 'type' => 'tuic', + 'server' => $server['host'], + 'port' => $server['port'], + 'uuid' => $password, + 'password' => $password, + 'congestion-controller' => data_get($protocol_settings, 'congestion_control', 'cubic'), + 'udp-relay-mode' => data_get($protocol_settings, 'udp_relay_mode', 'native'), + 'alpn' => data_get($protocol_settings, 'alpn', ['h3']), + 'reduce-rtt' => true, + 'fast-open' => true, + 'heartbeat-interval' => 10000, + 'request-timeout' => 8000, + 'max-udp-relay-packet-size' => 1500, + ]; + + $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls.allow_insecure', false); + if ($serverName = data_get($protocol_settings, 'tls.server_name')) { + $array['sni'] = $serverName; + } + + return $array; + } + + public static function buildSocks5($password, $server) + { + $protocol_settings = $server['protocol_settings']; + $array = [ + 'name' => $server['name'], + 'type' => 'socks5', + 'server' => $server['host'], + 'port' => $server['port'], + 'username' => $password, + 'password' => $password, + 'udp' => true, + ]; + + if (data_get($protocol_settings, 'tls')) { + $array['tls'] = true; + $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false); + if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { + $array['sni'] = $serverName; + } + } + + return $array; + } + + public static function buildHttp($password, $server) + { + $protocol_settings = $server['protocol_settings']; + $array = [ + 'name' => $server['name'], + 'type' => 'http', + 'server' => $server['host'], + 'port' => $server['port'], + 'username' => $password, + 'password' => $password, + ]; + + if (data_get($protocol_settings, 'tls')) { + $array['tls'] = true; + $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false); + if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { + $array['sni'] = $serverName; + } + } + + return $array; + } + private function isRegex($exp) { if (empty($exp)) { return false; } - return @preg_match($exp, '') !== false; + try { + return preg_match($exp, '') !== false; + } catch (\Exception $e) { + return false; + } } private function isMatch($exp, $str) diff --git a/app/Protocols/Surfboard.php b/app/Protocols/Surfboard.php index 5fe05d7..525ce1c 100644 --- a/app/Protocols/Surfboard.php +++ b/app/Protocols/Surfboard.php @@ -4,6 +4,7 @@ namespace App\Protocols; use App\Utils\Helper; use App\Contracts\ProtocolInterface; +use Illuminate\Support\Facades\File; class Surfboard implements ProtocolInterface { @@ -63,7 +64,7 @@ class Surfboard implements ProtocolInterface $defaultConfig = base_path() . '/resources/rules/default.surfboard.conf'; $customConfig = base_path() . '/resources/rules/custom.surfboard.conf'; - if (\File::exists($customConfig)) { + if (File::exists($customConfig)) { $config = file_get_contents("$customConfig"); } else { $config = file_get_contents("$defaultConfig"); @@ -127,9 +128,9 @@ class Surfboard implements ProtocolInterface array_push($config, 'tls=true'); if (data_get($protocol_settings, 'tls_settings')) { $tlsSettings = data_get($protocol_settings, 'tls_settings'); - if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure'])) + if (!!data_get($tlsSettings, 'allowInsecure')) array_push($config, 'skip-cert-verify=' . ($tlsSettings['allowInsecure'] ? 'true' : 'false')); - if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName'])) + if (!!data_get($tlsSettings, 'serverName')) array_push($config, "sni={$tlsSettings['serverName']}"); } } @@ -161,8 +162,8 @@ class Surfboard implements ProtocolInterface 'tfo=true', 'udp-relay=true' ]; - if (!empty($protocol_settings['allow_insecure'])) { - array_push($config, $protocol_settings['allow_insecure'] ? 'skip-cert-verify=true' : 'skip-cert-verify=false'); + if (data_get($protocol_settings, 'allow_insecure')) { + array_push($config, !!data_get($protocol_settings, 'allow_insecure') ? 'skip-cert-verify=true' : 'skip-cert-verify=false'); } $config = array_filter($config); $uri = implode(',', $config); diff --git a/app/Protocols/Surge.php b/app/Protocols/Surge.php index 4b8931f..1657436 100644 --- a/app/Protocols/Surge.php +++ b/app/Protocols/Surge.php @@ -164,7 +164,7 @@ class Surge implements ProtocolInterface 'udp-relay=true' ]; if (!empty($protocol_settings['allow_insecure'])) { - array_push($config, $protocol_settings['allow_insecure'] ? 'skip-cert-verify=true' : 'skip-cert-verify=false'); + array_push($config, !!data_get($protocol_settings, 'allow_insecure') ? 'skip-cert-verify=true' : 'skip-cert-verify=false'); } $config = array_filter($config); $uri = implode(',', $config); @@ -189,7 +189,7 @@ class Surge implements ProtocolInterface 'udp-relay=true' ]; if (data_get($protocol_settings, 'tls.allow_insecure')) { - $config[] = data_get($protocol_settings, 'tls.allow_insecure') ? 'skip-cert-verify=true' : 'skip-cert-verify=false'; + $config[] = !!data_get($protocol_settings, 'tls.allow_insecure') ? 'skip-cert-verify=true' : 'skip-cert-verify=false'; } $config = array_filter($config); $uri = implode(',', $config); diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 3049068..b92a7dd 100755 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -8,17 +8,15 @@ use Illuminate\Support\Facades\Gate; class AuthServiceProvider extends ServiceProvider { /** - * The policy mappings for the application. - * - * @var array + * 策略映射 + * @var array */ protected $policies = [ // 'App\Model' => 'App\Policies\ModelPolicy', ]; /** - * Register any authentication / authorization services. - * + * 注册任何认证/授权服务 * @return void */ public function boot() diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index e2d1497..299c8be 100755 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -8,16 +8,14 @@ use Illuminate\Support\Facades\Event; class EventServiceProvider extends ServiceProvider { /** - * The event listener mappings for the application. - * - * @var array + * 事件监听器映射 + * @var array> */ protected $listen = [ ]; /** - * Register any events for your application. - * + * 注册任何事件 * @return void */ public function boot() diff --git a/app/Services/Auth/LoginService.php b/app/Services/Auth/LoginService.php new file mode 100644 index 0000000..8538d1e --- /dev/null +++ b/app/Services/Auth/LoginService.php @@ -0,0 +1,111 @@ += (int)admin_setting('password_limit_count', 5)) { + return [false, [429, __('There are too many password errors, please try again after :minute minutes.', [ + 'minute' => admin_setting('password_limit_expire', 60) + ])]]; + } + } + + // 查找用户 + $user = User::where('email', $email)->first(); + if (!$user) { + return [false, [400, __('Incorrect email or password')]]; + } + + // 验证密码 + if (!Helper::multiPasswordVerify( + $user->password_algo, + $user->password_salt, + $password, + $user->password) + ) { + // 增加密码错误计数 + if ((int)admin_setting('password_limit_enable', true)) { + $passwordErrorCount = (int)Cache::get(CacheKey::get('PASSWORD_ERROR_LIMIT', $email), 0); + Cache::put( + CacheKey::get('PASSWORD_ERROR_LIMIT', $email), + (int)$passwordErrorCount + 1, + 60 * (int)admin_setting('password_limit_expire', 60) + ); + } + return [false, [400, __('Incorrect email or password')]]; + } + + // 检查账户状态 + if ($user->banned) { + return [false, [400, __('Your account has been suspended')]]; + } + + // 更新最后登录时间 + $user->last_login_at = time(); + $user->save(); + + return [true, $user]; + } + + /** + * 处理密码重置 + * + * @param string $email 用户邮箱 + * @param string $emailCode 邮箱验证码 + * @param string $password 新密码 + * @return array [成功状态, 结果或错误信息] + */ + public function resetPassword(string $email, string $emailCode, string $password): array + { + // 检查重置请求限制 + $forgetRequestLimitKey = CacheKey::get('FORGET_REQUEST_LIMIT', $email); + $forgetRequestLimit = (int)Cache::get($forgetRequestLimitKey); + if ($forgetRequestLimit >= 3) { + return [false, [429, __('Reset failed, Please try again later')]]; + } + + // 验证邮箱验证码 + if ((string)Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $email)) !== (string)$emailCode) { + Cache::put($forgetRequestLimitKey, $forgetRequestLimit ? $forgetRequestLimit + 1 : 1, 300); + return [false, [400, __('Incorrect email verification code')]]; + } + + // 查找用户 + $user = User::where('email', $email)->first(); + if (!$user) { + return [false, [400, __('This email is not registered in the system')]]; + } + + // 更新密码 + $user->password = password_hash($password, PASSWORD_DEFAULT); + $user->password_algo = NULL; + $user->password_salt = NULL; + + if (!$user->save()) { + return [false, [500, __('Reset failed')]]; + } + + // 清除邮箱验证码 + Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $email)); + + return [true, true]; + } +} \ No newline at end of file diff --git a/app/Services/Auth/MailLinkService.php b/app/Services/Auth/MailLinkService.php new file mode 100644 index 0000000..653df7a --- /dev/null +++ b/app/Services/Auth/MailLinkService.php @@ -0,0 +1,122 @@ +first(); + if (!$user) { + return [true, true]; // 成功但用户不存在,保护用户隐私 + } + + $code = Helper::guid(); + $key = CacheKey::get('TEMP_TOKEN', $code); + Cache::put($key, $user->id, 300); + Cache::put(CacheKey::get('LAST_SEND_LOGIN_WITH_MAIL_LINK_TIMESTAMP', $email), time(), 60); + + $redirectUrl = '/#/login?verify=' . $code . '&redirect=' . ($redirect ? $redirect : 'dashboard'); + if (admin_setting('app_url')) { + $link = admin_setting('app_url') . $redirectUrl; + } else { + $link = url($redirectUrl); + } + + $this->sendMailLinkEmail($user, $link); + + return [true, $link]; + } + + /** + * 发送邮件链接登录邮件 + * + * @param User $user 用户对象 + * @param string $link 登录链接 + * @return void + */ + private function sendMailLinkEmail(User $user, string $link): void + { + SendEmailJob::dispatch([ + 'email' => $user->email, + 'subject' => __('Login to :name', [ + 'name' => admin_setting('app_name', 'XBoard') + ]), + 'template_name' => 'login', + 'template_value' => [ + 'name' => admin_setting('app_name', 'XBoard'), + 'link' => $link, + 'url' => admin_setting('app_url') + ] + ]); + } + + /** + * 获取快速登录URL + * + * @param User $user 用户对象 + * @param string|null $redirect 重定向地址 + * @return string 登录URL + */ + public function getQuickLoginUrl(User $user, ?string $redirect = null): string + { + $code = Helper::guid(); + $key = CacheKey::get('TEMP_TOKEN', $code); + Cache::put($key, $user->id, 60); + + $redirectUrl = '/#/login?verify=' . $code . '&redirect=' . ($redirect ? $redirect : 'dashboard'); + + if (admin_setting('app_url')) { + return admin_setting('app_url') . $redirectUrl; + } else { + return url($redirectUrl); + } + } + + /** + * 处理Token登录 + * + * @param string $token 登录令牌 + * @return int|null 用户ID或null + */ + public function handleTokenLogin(string $token): ?int + { + $key = CacheKey::get('TEMP_TOKEN', $token); + $userId = Cache::get($key); + + if (!$userId) { + return null; + } + + $user = User::find($userId); + + if (!$user || $user->banned) { + return null; + } + + Cache::forget($key); + + return $userId; + } +} \ No newline at end of file diff --git a/app/Services/Auth/RegisterService.php b/app/Services/Auth/RegisterService.php new file mode 100644 index 0000000..9956b49 --- /dev/null +++ b/app/Services/Auth/RegisterService.php @@ -0,0 +1,209 @@ +ip())) ?? 0; + if ((int)$registerCountByIP >= (int)admin_setting('register_limit_count', 3)) { + return [false, [429, __('Register frequently, please try again after :minute minute', [ + 'minute' => admin_setting('register_limit_expire', 60) + ])]]; + } + } + + // 检查验证码 + if ((int)admin_setting('recaptcha_enable', 0)) { + $recaptcha = new ReCaptcha(admin_setting('recaptcha_key')); + $recaptchaResp = $recaptcha->verify($request->input('recaptcha_data')); + if (!$recaptchaResp->isSuccess()) { + return [false, [400, __('Invalid code is incorrect')]]; + } + } + + // 检查邮箱白名单 + if ((int)admin_setting('email_whitelist_enable', 0)) { + if (!Helper::emailSuffixVerify( + $request->input('email'), + admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT)) + ) { + return [false, [400, __('Email suffix is not in the Whitelist')]]; + } + } + + // 检查Gmail限制 + if ((int)admin_setting('email_gmail_limit_enable', 0)) { + $prefix = explode('@', $request->input('email'))[0]; + if (strpos($prefix, '.') !== false || strpos($prefix, '+') !== false) { + return [false, [400, __('Gmail alias is not supported')]]; + } + } + + // 检查是否关闭注册 + if ((int)admin_setting('stop_register', 0)) { + return [false, [400, __('Registration has closed')]]; + } + + // 检查邀请码要求 + if ((int)admin_setting('invite_force', 0)) { + if (empty($request->input('invite_code'))) { + return [false, [422, __('You must use the invitation code to register')]]; + } + } + + // 检查邮箱验证 + if ((int)admin_setting('email_verify', 0)) { + if (empty($request->input('email_code'))) { + return [false, [422, __('Email verification code cannot be empty')]]; + } + if ((string)Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== (string)$request->input('email_code')) { + return [false, [400, __('Incorrect email verification code')]]; + } + } + + // 检查邮箱是否存在 + $email = $request->input('email'); + $exist = User::where('email', $email)->first(); + if ($exist) { + return [false, [400201, __('Email already exists')]]; + } + + return [true, null]; + } + + /** + * 处理邀请码 + * + * @param User $user 用户对象 + * @param string|null $inviteCode 邀请码 + * @return array [是否成功, 错误消息] + */ + public function handleInviteCode(User $user, ?string $inviteCode): array + { + if (!$inviteCode) { + return [true, null]; + } + + $inviteCodeModel = InviteCode::where('code', $inviteCode) + ->where('status', 0) + ->first(); + + if (!$inviteCodeModel) { + if ((int)admin_setting('invite_force', 0)) { + return [false, [400, __('Invalid invitation code')]]; + } + return [true, null]; + } + + $user->invite_user_id = $inviteCodeModel->user_id ? $inviteCodeModel->user_id : null; + + if (!(int)admin_setting('invite_never_expire', 0)) { + $inviteCodeModel->status = true; + $inviteCodeModel->save(); + } + + return [true, null]; + } + + /** + * 处理试用计划 + * + * @param User $user 用户对象 + * @return void + */ + public function handleTryOut(User $user): void + { + if ((int)admin_setting('try_out_plan_id', 0)) { + $plan = Plan::find(admin_setting('try_out_plan_id')); + if ($plan) { + $user->transfer_enable = $plan->transfer_enable * 1073741824; + $user->plan_id = $plan->id; + $user->group_id = $plan->group_id; + $user->expired_at = time() + (admin_setting('try_out_hour', 1) * 3600); + $user->speed_limit = $plan->speed_limit; + } + } + } + + /** + * 注册用户 + * + * @param Request $request 请求对象 + * @return array [成功状态, 用户对象或错误信息] + */ + public function register(Request $request): array + { + // 验证注册数据 + [$valid, $error] = $this->validateRegister($request); + if (!$valid) { + return [false, $error]; + } + + $email = $request->input('email'); + $password = $request->input('password'); + + // 创建用户 + $user = new User(); + $user->email = $email; + $user->password = password_hash($password, PASSWORD_DEFAULT); + $user->uuid = Helper::guid(true); + $user->token = Helper::guid(); + $user->remind_expire = admin_setting('default_remind_expire', 1); + $user->remind_traffic = admin_setting('default_remind_traffic', 1); + + // 处理邀请码 + [$inviteSuccess, $inviteError] = $this->handleInviteCode($user, $request->input('invite_code')); + if (!$inviteSuccess) { + return [false, $inviteError]; + } + + // 处理试用计划 + $this->handleTryOut($user); + + // 保存用户 + if (!$user->save()) { + return [false, [500, __('Register failed')]]; + } + + // 清除邮箱验证码 + if ((int)admin_setting('email_verify', 0)) { + Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $email)); + } + + // 更新最近登录时间 + $user->last_login_at = time(); + $user->save(); + + // 更新IP注册计数 + if ((int)admin_setting('register_limit_by_ip_enable', 0)) { + $registerCountByIP = Cache::get(CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip())) ?? 0; + Cache::put( + CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip()), + (int)$registerCountByIP + 1, + (int)admin_setting('register_limit_expire', 60) * 60 + ); + } + + return [true, $user]; + } +} \ No newline at end of file diff --git a/app/Services/AuthService.php b/app/Services/AuthService.php index 9b73072..f82fdf9 100644 --- a/app/Services/AuthService.php +++ b/app/Services/AuthService.php @@ -40,7 +40,13 @@ class AuthService return $this->user->tokens()->get()->toArray(); } - public function removeSession(): bool + public function removeSession(string $sessionId): bool + { + $this->user->tokens()->where('id', $sessionId)->delete(); + return true; + } + + public function removeAllSessions(): bool { $this->user->tokens()->delete(); return true; @@ -54,4 +60,26 @@ class AuthService return $accessToken?->tokenable; } + + /** + * 解密认证数据 + * + * @param string $authorization + * @return array|null 用户数据或null + */ + public static function decryptAuthData(string $authorization): ?array + { + $user = self::findUserByBearerToken($authorization); + + if (!$user) { + return null; + } + + return [ + 'id' => $user->id, + 'email' => $user->email, + 'is_admin' => (bool)$user->is_admin, + 'is_staff' => (bool)$user->is_staff + ]; + } } diff --git a/app/Services/OrderService.php b/app/Services/OrderService.php index 73848d1..329ae67 100644 --- a/app/Services/OrderService.php +++ b/app/Services/OrderService.php @@ -127,30 +127,30 @@ class OrderService $inviter = User::find($user->invite_user_id); if (!$inviter) return; + $commissionType = (int) $inviter->commission_type; + if ($commissionType === User::COMMISSION_TYPE_SYSTEM) { + $commissionType = (bool) admin_setting('commission_first_time_enable', true) ? User::COMMISSION_TYPE_ONETIME : User::COMMISSION_TYPE_PERIOD; + } $isCommission = false; - switch ((int) $inviter->commission_type) { - case 0: - $commissionFirstTime = (int) admin_setting('commission_first_time_enable', 1); - $isCommission = (!$commissionFirstTime || ($commissionFirstTime && !$this->haveValidOrder($user))); - break; - case 1: + switch ($commissionType) { + case User::COMMISSION_TYPE_PERIOD: $isCommission = true; break; - case 2: + case User::COMMISSION_TYPE_ONETIME: $isCommission = !$this->haveValidOrder($user); break; } if (!$isCommission) return; - if ($inviter && $inviter->commission_rate) { + if ($inviter->commission_rate) { $order->commission_balance = $order->total_amount * ($inviter->commission_rate / 100); } else { $order->commission_balance = $order->total_amount * (admin_setting('invite_commission', 10) / 100); } } - private function haveValidOrder(User $user) + private function haveValidOrder(User $user): Order|null { return Order::where('user_id', $user->id) ->whereNotIn('status', [0, 2]) diff --git a/app/Services/PlanService.php b/app/Services/PlanService.php index 52549cf..e67363d 100644 --- a/app/Services/PlanService.php +++ b/app/Services/PlanService.php @@ -74,10 +74,9 @@ class PlanService // 转换周期格式为新版格式 $periodKey = self::getPeriodKey($period); + $price = $this->plan->prices[$periodKey] ?? null; - - // 检查价格时使用新版格式 - if (!isset($this->plan->prices[$periodKey]) || $this->plan->prices[$periodKey] === NULL) { + if ($price === null) { throw new ApiException(__('This payment period cannot be purchased, please choose another period')); } diff --git a/app/Services/Plugin/HookManager.php b/app/Services/Plugin/HookManager.php index 4147962..6e3ef51 100644 --- a/app/Services/Plugin/HookManager.php +++ b/app/Services/Plugin/HookManager.php @@ -45,9 +45,16 @@ class HookManager * @param mixed ...$args 其他参数 * @return mixed */ - public static function filter(string $hook, mixed $value): mixed + public static function filter(string $hook, mixed $value, mixed ...$args): mixed { - return Eventy::filter($hook, $value); + if (!self::hasHook($hook)) { + return $value; + } + + /** @phpstan-ignore-next-line */ + $result = Eventy::filter($hook, $value, ...$args); + + return $result; } /** @@ -88,4 +95,10 @@ class HookManager Eventy::removeAction($hook, $callback); Eventy::removeFilter($hook, $callback); } + + private static function hasHook(string $hook): bool + { + // Implementation of hasHook method + return true; // Placeholder return, actual implementation needed + } } \ No newline at end of file diff --git a/app/Services/ServerService.php b/app/Services/ServerService.php index b2ff59d..40d01fa 100644 --- a/app/Services/ServerService.php +++ b/app/Services/ServerService.php @@ -15,14 +15,18 @@ class ServerService * 获取所有服务器列表 * @return Collection */ - public static function getAllServers() + public static function getAllServers(): Collection { - return Server::orderBy('sort', 'ASC') - ->get() - ->transform(function (Server $server) { - $server->loadServerStatus(); - return $server; - }); + $query = Server::orderBy('sort', 'ASC'); + + return $query->get()->append([ + 'last_check_at', + 'last_push_at', + 'online', + 'is_online', + 'available_status', + 'cache_key' + ]); } /** @@ -32,28 +36,25 @@ class ServerService */ public static function getAvailableServers(User $user): array { - return Server::whereJsonContains('group_ids', (string) $user->group_id) + $servers = Server::whereJsonContains('group_ids', (string) $user->group_id) ->where('show', true) ->orderBy('sort', 'ASC') ->get() - ->transform(function (Server $server) use ($user) { - $server->loadParentCreatedAt(); - $server->handlePortAllocation(); - $server->loadServerStatus(); - if ($server->type === 'shadowsocks') { - $server->server_key = Helper::getServerKey($server->created_at, 16); - } - $server->generateShadowsocksPassword($user); + ->append(['last_check_at', 'last_push_at', 'online', 'is_online', 'available_status', 'cache_key', 'server_key']); - return $server; - }) - ->toArray(); + $servers = collect($servers)->map(function ($server) use ($user) { + // 判断动态端口 + if (str_contains($server?->port, '-')) { + $server->port = (int) Helper::randomPort($server->port); + $server->ports = $server->port; + } + $server->password = $server->generateShadowsocksPassword($user); + return $server; + })->toArray(); + + return $servers; } - /** - * 加 - */ - /** * 根据权限组获取可用的用户列表 * @param array $groupIds diff --git a/app/Services/StatisticalService.php b/app/Services/StatisticalService.php index 8d38c63..e57d6ae 100644 --- a/app/Services/StatisticalService.php +++ b/app/Services/StatisticalService.php @@ -122,7 +122,7 @@ class StatisticalService $key = "{$rate}_{$uid}"; $stats[$key] = $stats[$key] ?? [ 'record_at' => $this->startAt, - 'server_rate' => number_format($rate, 2, '.', ''), + 'server_rate' => number_format((float) $rate, 2, '.', ''), 'u' => 0, 'd' => 0, 'user_id' => intval($userId), @@ -156,27 +156,40 @@ class StatisticalService /** - * 获取缓存中的服务器爆表 + * Retrieve server statistics from Redis cache. + * + * @return array */ - public function getStatServer() + public function getStatServer(): array { + /** @var array $stats */ $stats = []; $statsServer = $this->redis->zrange($this->statServerKey, 0, -1, true); + foreach ($statsServer as $member => $value) { - list($serverType, $serverId, $type) = explode('_', $member); + $parts = explode('_', $member); + if (count($parts) !== 3) { + continue; // Skip malformed members + } + [$serverType, $serverId, $type] = $parts; + + if (!in_array($type, ['u', 'd'], true)) { + continue; // Skip invalid types + } + $key = "{$serverType}_{$serverId}"; if (!isset($stats[$key])) { $stats[$key] = [ - 'server_id' => intval($serverId), + 'server_id' => (int) $serverId, 'server_type' => $serverType, - 'u' => 0, - 'd' => 0, + 'u' => 0.0, + 'd' => 0.0, ]; } - $stats[$key][$type] += $value; + $stats[$key][$type] += (float) $value; } - return array_values($stats); + return array_values($stats); } /** @@ -281,25 +294,22 @@ class StatisticalService ->where('record_type', 'd'); } ) + ->withSum('stats as u', 'u') // 预加载 u 的总和 + ->withSum('stats as d', 'd') // 预加载 d 的总和 ->get() - ->each(function ($item) { - $item->u = (int) $item->stats()->sum('u'); - $item->d = (int) $item->stats()->sum('d'); - $item->total = (int) $item->u + $item->d; - $item->server_name = optional($item->parent)->name ?? $item->name; - $item->server_id = $item->id; - $item->server_type = $item->type; + ->map(function ($item) { + return [ + 'server_name' => optional($item->parent)->name ?? $item->name, + 'server_id' => $item->id, + 'server_type' => $item->type, + 'u' => (int) $item->u, + 'd' => (int) $item->d, + 'total' => (int) $item->u + (int) $item->d, + ]; }) ->sortByDesc('total') - ->select([ - 'server_name', - 'server_id', - 'server_type', - 'u', - 'd', - 'total' - ]) - ->values()->toArray(); + ->values() + ->toArray(); return $statistics; } diff --git a/app/Services/UserOnlineService.php b/app/Services/UserOnlineService.php index 5445fa0..8ddc556 100644 --- a/app/Services/UserOnlineService.php +++ b/app/Services/UserOnlineService.php @@ -156,13 +156,17 @@ class UserOnlineService } /** - * 计算设备数量 + * Calculate the number of devices based on IPs array and device limit mode. + * + * @param array $ipsArray Array containing IP data + * @return int Number of devices */ private function calculateDeviceCount(array $ipsArray): int { - // 设备限制模式 - return match ((int) admin_setting('device_limit_mode', 0)) { - // 宽松模式 + $mode = (int) admin_setting('device_limit_mode', 0); + + return match ($mode) { + // Loose mode: Count unique IPs (ignoring suffixes after '_') 1 => collect($ipsArray) ->filter(fn(mixed $data): bool => is_array($data) && isset($data['aliveips'])) ->flatMap( @@ -173,9 +177,12 @@ class UserOnlineService ) ->unique() ->count(), + // Strict mode: Sum total number of alive IPs 0 => collect($ipsArray) ->filter(fn(mixed $data): bool => is_array($data) && isset($data['aliveips'])) - ->sum(fn(array $data): int => count($data['aliveips'])) + ->sum(fn(array $data): int => count($data['aliveips'])), + // Handle invalid modes + default => throw new \InvalidArgumentException("Invalid device limit mode: $mode"), }; } } \ No newline at end of file diff --git a/app/Services/UserService.php b/app/Services/UserService.php index a733bfc..a085630 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -12,7 +12,7 @@ use App\Services\Plugin\HookManager; class UserService { - private function calcResetDayByMonthFirstDay() + private function calcResetDayByMonthFirstDay(): int { $today = date('d'); $lastDay = date('d', strtotime('last day of +0 months')); @@ -51,55 +51,34 @@ class UserService return (int) (($nextYear - time()) / 86400); } - public function getResetDay(User $user) + public function getResetDay(User $user): ?int { - if (!isset($user->plan)) { - $user->plan = Plan::find($user->plan_id); - } - if ($user->expired_at <= time() || $user->expired_at === NULL) + // 前置条件检查 + if ($user->expired_at <= time() || $user->expired_at === null) { return null; - // if reset method is not reset - if ($user->plan->reset_traffic_method === 2) - return null; - switch (true) { - case ($user->plan->reset_traffic_method === NULL): { - $resetTrafficMethod = admin_setting('reset_traffic_method', 0); - switch ((int) $resetTrafficMethod) { - // month first day - case 0: - return $this->calcResetDayByMonthFirstDay(); - // expire day - case 1: - return $this->calcResetDayByExpireDay($user->expired_at); - // no action - case 2: - return null; - // year first day - case 3: - return $this->calcResetDayByYearFirstDay(); - // year expire day - case 4: - return $this->calcResetDayByYearExpiredAt($user->expired_at); - } - break; - } - case ($user->plan->reset_traffic_method === 0): { - return $this->calcResetDayByMonthFirstDay(); - } - case ($user->plan->reset_traffic_method === 1): { - return $this->calcResetDayByExpireDay($user->expired_at); - } - case ($user->plan->reset_traffic_method === 2): { - return null; - } - case ($user->plan->reset_traffic_method === 3): { - return $this->calcResetDayByYearFirstDay(); - } - case ($user->plan->reset_traffic_method === 4): { - return $this->calcResetDayByYearExpiredAt($user->expired_at); - } } - return null; + + // 获取重置方式逻辑统一 + $resetMethod = $user->plan->reset_traffic_method === Plan::RESET_TRAFFIC_FOLLOW_SYSTEM + ? (int)admin_setting('reset_traffic_method', 0) + : $user->plan->reset_traffic_method; + + // 验证重置方式有效性 + if (!in_array($resetMethod, array_keys(Plan::getResetTrafficMethods()), true)) { + return null; + } + + // 方法映射表 + $methodHandlers = [ + Plan::RESET_TRAFFIC_FIRST_DAY_MONTH => fn() => $this->calcResetDayByMonthFirstDay(), + Plan::RESET_TRAFFIC_MONTHLY => fn() => $this->calcResetDayByExpireDay($user->expired_at), + Plan::RESET_TRAFFIC_FIRST_DAY_YEAR => fn() => $this->calcResetDayByYearFirstDay(), + Plan::RESET_TRAFFIC_YEARLY => fn() => $this->calcResetDayByYearExpiredAt($user->expired_at), + ]; + + $handler = $methodHandlers[$resetMethod] ?? null; + + return $handler ? $handler() : null; } public function isAvailable(User $user) diff --git a/app/Support/Setting.php b/app/Support/Setting.php index d112ef9..6b7165b 100644 --- a/app/Support/Setting.php +++ b/app/Support/Setting.php @@ -32,10 +32,11 @@ class Setting /** * 设置配置信息. * - * @param array $data - * @return $this + * @param string $key + * @param mixed $value + * @return bool 设置是否成功 */ - public function set($key, $value = null): bool + public function set(string $key, $value = null): bool { if (is_array($value)) { $value = json_encode($value); @@ -50,12 +51,12 @@ class Setting /** * 保存配置到数据库. * - * @param array $data - * @return $this + * @param array $settings 要保存的设置数组 + * @return bool 保存是否成功 */ - public function save(array $data = []): bool + public function save(array $settings): bool { - foreach ($data as $key => $value) { + foreach ($settings as $key => $value) { $this->set($key, $value); } @@ -99,4 +100,22 @@ class Setting { return $this->fromDatabase(); } + + /** + * 更新单个设置项 + * + * @param string $key 设置键名 + * @param mixed $value 设置值 + * @return bool 更新是否成功 + */ + public function update(string $key, $value): bool + { + if (is_array($value)) { + $value = json_encode($value); + } + $key = strtolower($key); + SettingModel::updateOrCreate(['name' => $key], ['value' => $value]); + $this->cache->forget(self::CACHE_KEY); + return true; + } } diff --git a/app/Utils/CacheKey.php b/app/Utils/CacheKey.php index 132186e..384f012 100644 --- a/app/Utils/CacheKey.php +++ b/app/Utils/CacheKey.php @@ -31,6 +31,22 @@ class CacheKey 'MULTI_SERVER_TUIC_ONLINE_USER' => 'TUIC节点多服务器在线用户', 'SERVER_TUIC_LAST_CHECK_AT' => 'TUIC节点最后检查时间', 'SERVER_TUIC_LAST_PUSH_AT' => 'TUIC节点最后推送时间', + 'SERVER_SOCKS_ONLINE_USER' => 'socks节点在线用户', + 'MULTI_SERVER_SOCKS_ONLINE_USER' => 'socks节点多服务器在线用户', + 'SERVER_SOCKS_LAST_CHECK_AT' => 'socks节点最后检查时间', + 'SERVER_SOCKS_LAST_PUSH_AT' => 'socks节点最后推送时间', + 'SERVER_NAIVE_ONLINE_USER' => 'naive节点在线用户', + 'MULTI_SERVER_NAIVE_ONLINE_USER' => 'naive节点多服务器在线用户', + 'SERVER_NAIVE_LAST_CHECK_AT' => 'naive节点最后检查时间', + 'SERVER_NAIVE_LAST_PUSH_AT' => 'naive节点最后推送时间', + 'SERVER_HTTP_ONLINE_USER' => 'http节点在线用户', + 'MULTI_SERVER_HTTP_ONLINE_USER' => 'http节点多服务器在线用户', + 'SERVER_HTTP_LAST_CHECK_AT' => 'http节点最后检查时间', + 'SERVER_HTTP_LAST_PUSH_AT' => 'http节点最后推送时间', + 'SERVER_MIERU_ONLINE_USER' => 'mieru节点在线用户', + 'MULTI_SERVER_MIERU_ONLINE_USER' => 'mieru节点多服务器在线用户', + 'SERVER_MIERU_LAST_CHECK_AT' => 'mieru节点最后检查时间', + 'SERVER_MIERU_LAST_PUSH_AT' => 'mieru节点最后推送时间', 'TEMP_TOKEN' => '临时令牌', 'LAST_SEND_EMAIL_REMIND_TRAFFIC' => '最后发送流量邮件提醒', 'SCHEDULE_LAST_CHECK_AT' => '计划任务最后检查时间', diff --git a/app/Utils/Helper.php b/app/Utils/Helper.php index f974af8..d153875 100644 --- a/app/Utils/Helper.php +++ b/app/Utils/Helper.php @@ -134,7 +134,7 @@ class Helper public static function randomPort($range): int { $portRange = explode('-', $range); - return random_int($portRange[0], $portRange[1]); + return random_int((int)$portRange[0], (int)$portRange[1]); } public static function base64EncodeUrlSafe($data) diff --git a/composer.json b/composer.json index eca5f28..6b9f00c 100755 --- a/composer.json +++ b/composer.json @@ -37,8 +37,10 @@ "require-dev": { "barryvdh/laravel-debugbar": "^3.9", "fakerphp/faker": "^1.9.1", + "larastan/larastan": "^3.0", "mockery/mockery": "^1.6", "nunomaduro/collision": "^8.0", + "nunomaduro/larastan": "^3.1", "orangehill/iseed": "^3.0", "phpunit/phpunit": "^10.5", "spatie/laravel-ignition": "^2.4" diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..d14e701 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,17 @@ +includes: + - vendor/larastan/larastan/extension.neon + - vendor/nesbot/carbon/extension.neon + +parameters: + + paths: + - app/ + + # Level 10 is the highest level + level: 5 + + ignoreErrors: + - '#Negated boolean expression is always false\.#' + +# excludePaths: +# - ./*/*/FileToBeExcluded.php \ No newline at end of file