Przeglądaj źródła

Client Api Alpha Release & Code Splitting

- 代码拆分,为客户端API,前后端分离做准备
- Client Api 对接 Alpha Release
- 规范WebApi格式

Update client.php
兔姬桑 2 lat temu
rodzic
commit
bc44fd9770
44 zmienionych plików z 2075 dodań i 1058 usunięć
  1. 9 2
      app/Exceptions/Handler.php
  2. 55 0
      app/Helpers/ClientApiResponse.php
  3. 54 0
      app/Helpers/DataChart.php
  4. 82 0
      app/Helpers/ResponseEnum.php
  5. 49 0
      app/Helpers/WebApiResponse.php
  6. 4 2
      app/Http/Controllers/Admin/LogsController.php
  7. 4 2
      app/Http/Controllers/Admin/NodeController.php
  8. 5 2
      app/Http/Controllers/Admin/UserController.php
  9. 105 0
      app/Http/Controllers/Api/Client/AuthController.php
  10. 304 0
      app/Http/Controllers/Api/Client/ClientController.php
  11. 0 328
      app/Http/Controllers/Api/Client/V1Controller.php
  12. 19 46
      app/Http/Controllers/Api/WebApi/CoreController.php
  13. 9 7
      app/Http/Controllers/Api/WebApi/SSController.php
  14. 7 25
      app/Http/Controllers/Api/WebApi/SSRController.php
  15. 18 16
      app/Http/Controllers/Api/WebApi/TrojanController.php
  16. 13 15
      app/Http/Controllers/Api/WebApi/V2RayController.php
  17. 0 275
      app/Http/Controllers/ClientController.php
  18. 0 98
      app/Http/Controllers/Controller.php
  19. 2 8
      app/Http/Controllers/User/AffiliateController.php
  20. 16 67
      app/Http/Controllers/User/SubscribeController.php
  21. 9 6
      app/Http/Controllers/UserController.php
  22. 25 15
      app/Http/Kernel.php
  23. 15 0
      app/Http/Middleware/AcceptHeader.php
  24. 30 0
      app/Http/Middleware/ClientAuthenticate.php
  25. 5 3
      app/Http/Middleware/SetLocale.php
  26. 10 14
      app/Http/Middleware/WebApi.php
  27. 28 0
      app/Http/Middleware/tmpCORS.php
  28. 1 2
      app/Jobs/VNet/reloadNode.php
  29. 27 76
      app/Models/Node.php
  30. 2 28
      app/Models/User.php
  31. 26 0
      app/Services/BaseService.php
  32. 214 0
      app/Services/ProxyServer.php
  33. 62 0
      app/Services/UserService.php
  34. 210 0
      config/client.php
  35. 67 0
      config/sanctum.php
  36. 1 1
      config/session.php
  37. 1 1
      config/version.php
  38. 36 0
      database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php
  39. 1 0
      resources/lang/en/errors.php
  40. 3 2
      resources/lang/en/user.php
  41. 1 0
      resources/lang/zh_CN/errors.php
  42. 3 2
      resources/lang/zh_CN/user.php
  43. 522 0
      resources/rules/bob.clash.yaml
  44. 21 15
      routes/api.php

+ 9 - 2
app/Exceptions/Handler.php

@@ -13,7 +13,7 @@ use Illuminate\Validation\ValidationException;
 use Log;
 use Log;
 use ReflectionException;
 use ReflectionException;
 use Response;
 use Response;
-use Symfony\Component\HttpKernel\Exception\HttpException;
+use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Throwable;
 use Throwable;
 
 
@@ -25,7 +25,7 @@ class Handler extends ExceptionHandler
      * @var array
      * @var array
      */
      */
     protected $dontReport = [
     protected $dontReport = [
-        HttpException::class,
+        //        HttpException::class,
         ValidationException::class,
         ValidationException::class,
     ];
     ];
 
 
@@ -35,6 +35,7 @@ class Handler extends ExceptionHandler
      * @var array
      * @var array
      */
      */
     protected $dontFlash = [
     protected $dontFlash = [
+        'current_password',
         'password',
         'password',
         'password_confirmation',
         'password_confirmation',
     ];
     ];
@@ -105,6 +106,12 @@ class Handler extends ExceptionHandler
                     }
                     }
 
 
                     return Response::view('auth.error', ['message' => trans('errors.system')], 500);
                     return Response::view('auth.error', ['message' => trans('errors.system')], 500);
+                case $exception instanceof MethodNotAllowedHttpException:
+                    if ($request->ajax() || $request->wantsJson()) {
+                        return Response::json(['status' => 'fail', 'message' => trans('errors.http')], 405);
+                    }
+
+                    return Response::view('auth.error', ['message' => trans('errors.http')], 405);
                 case $exception instanceof ErrorException: // 捕获系统错误异常
                 case $exception instanceof ErrorException: // 捕获系统错误异常
                     if ($request->ajax() || $request->wantsJson()) {
                     if ($request->ajax() || $request->wantsJson()) {
                         return Response::json([
                         return Response::json([

+ 55 - 0
app/Helpers/ClientApiResponse.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Helpers;
+
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use Log;
+
+trait ClientApiResponse
+{
+    private static $client;
+
+    public function __construct(Request $request)
+    {
+        if (str_contains($request->userAgent(), 'bob_vpn')) {
+            self::$client = 'bob';
+        }
+    }
+
+    public function setClient($client)
+    {
+        self::$client = $client;
+    }
+
+    public function succeed($data = null, $addition = null, $codeResponse = ResponseEnum::HTTP_OK): JsonResponse
+    {
+        return $this->jsonResponse(1, $codeResponse, $data, $addition);
+    }
+
+    private function jsonResponse($status, $codeResponse, $data = null, $addition = null): JsonResponse
+    {
+        [$code, $message] = $codeResponse;
+        $code = $code > 1000 ? (int) ($code / 1000) : $code;
+        if (self::$client === 'bob') { // bob 客户端 返回格式
+            $result = ['ret' => $status, 'msg' => $message, 'data' => $data];
+
+            if (isset($addition)) {
+                $result = array_merge($result, $addition);
+            }
+        } else { // ProxyPanel client api 规范格式
+            if (isset($data, $addition) && is_array($data)) {
+                $data = array_merge($data, $addition);
+            }
+
+            $result = ['status' => $status ? 'success' : 'fail', 'code' => $code, 'message' => $message, 'data' => $data ?? $addition];
+        }
+
+        return response()->json($result, $code, ['content-type' => 'application/json']);
+    }
+
+    public function failed($codeResponse = ResponseEnum::HTTP_ERROR, $data = null, $addition = null): JsonResponse
+    {
+        return $this->jsonResponse(0, $codeResponse, is_array($data) ? $data[0] : $data, $addition);
+    }
+}

+ 54 - 0
app/Helpers/DataChart.php

@@ -0,0 +1,54 @@
+<?php
+
+namespace App\Helpers;
+
+use App\Models\NodeDailyDataFlow;
+use App\Models\NodeHourlyDataFlow;
+use App\Models\UserDailyDataFlow;
+use App\Models\UserDataFlowLog;
+use App\Models\UserHourlyDataFlow;
+use DB;
+
+trait DataChart
+{
+    public function dataFlowChart($id, $is_node = false): array // 流量使用图表
+    {
+        if ($is_node) {
+            $currentFlow = UserDataFlowLog::whereNodeId($id);
+            $hourlyFlow = NodeHourlyDataFlow::whereNodeId($id)->whereDate('created_at',
+                date('Y-m-d'))->selectRaw('(DATE_FORMAT(node_hourly_data_flow.created_at, "%k")) as date, total')->pluck('total', 'date');
+            $dailyFlow = NodeDailyDataFlow::whereNodeId($id)->whereMonth('created_at',
+                date('n'))->selectRaw('(DATE_FORMAT(node_daily_data_flow.created_at, "%e")) as date, total')->pluck('total', 'date');
+        } else {
+            $currentFlow = UserDataFlowLog::whereUserId($id);
+            $hourlyFlow = UserHourlyDataFlow::userHourly($id)->whereDate('created_at',
+                date('Y-m-d'))->selectRaw('(DATE_FORMAT(user_hourly_data_flow.created_at, "%k")) as date, total')->pluck('total', 'date');
+            $dailyFlow = UserDailyDataFlow::userDaily($id)->whereMonth('created_at',
+                date('n'))->selectRaw('(DATE_FORMAT(user_daily_data_flow.created_at, "%e")) as date, total')->pluck('total', 'date');
+        }
+        $currentFlow = $currentFlow->where('log_time', '>=', strtotime(date('Y-m-d H:0')))->sum(DB::raw('u + d'));
+
+        // 节点一天内的流量
+        $hourlyData = array_fill(0, date('G') + 1, 0);
+        foreach ($hourlyFlow as $date => $dataFlow) {
+            $hourlyData[$date] = round($dataFlow / GB, 3);
+        }
+        $hourlyData[date('G') + 1] = round($currentFlow / GB, 3);
+
+        // 节点一个月内的流量
+        $dailyData = array_fill(0, date('j') - 1, 0);
+
+        foreach ($dailyFlow as $date => $dataFlow) {
+            $dailyData[$date - 1] = round($dataFlow / GB, 3);
+        }
+
+        $dailyData[date('j', strtotime(now())) - 1] = round(array_sum($hourlyData) + $currentFlow / GB, 3);
+
+        return [
+            'trafficDaily'  => $dailyData,
+            'trafficHourly' => $hourlyData,
+            'monthDays'     => range(1, date('j')), // 本月天数
+            'dayHours'      => range(0, date('G') + 1), // 本日小时
+        ];
+    }
+}

+ 82 - 0
app/Helpers/ResponseEnum.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace App\Helpers;
+
+class ResponseEnum
+{
+    // 001 ~ 099 表示系统状态;100 ~ 199 表示授权业务;200 ~ 299 表示用户业务
+
+    /*-------------------------------------------------------------------------------------------*/
+    // 100开头的表示 信息提示,这类状态表示临时的响应
+    // 100 - 继续
+    // 101 - 切换协议
+
+    /*-------------------------------------------------------------------------------------------*/
+    // 200表示服务器成功地接受了客户端请求
+    const HTTP_OK = [200001, '操作成功'];
+    const HTTP_ERROR = [200002, '操作失败'];
+    const HTTP_ACTION_COUNT_ERROR = [200302, '操作频繁'];
+    const USER_SERVICE_LOGIN_SUCCESS = [200200, '登录成功'];
+    const USER_SERVICE_LOGIN_ERROR = [200201, '登录失败'];
+    const USER_SERVICE_LOGOUT_SUCCESS = [200202, '退出登录成功'];
+    const USER_SERVICE_LOGOUT_ERROR = [200203, '退出登录失败'];
+    const USER_SERVICE_REGISTER_SUCCESS = [200104, '注册成功'];
+    const USER_SERVICE_REGISTER_ERROR = [200105, '注册失败'];
+    const USER_ACCOUNT_REGISTERED = [23001, '账号已注册'];
+
+    /*-------------------------------------------------------------------------------------------*/
+    // 300开头的表示服务器重定向,指向的别的地方,客户端浏览器必须采取更多操作来实现请求
+    // 302 - 对象已移动。
+    // 304 - 未修改。
+    // 307 - 临时重定向。
+
+    /*-------------------------------------------------------------------------------------------*/
+    // 400开头的表示客户端错误请求错误,请求不到数据,或者找不到等等
+    // 400 - 错误的请求
+    const CLIENT_NOT_FOUND_HTTP_ERROR = [400001, '请求失败'];
+    const CLIENT_PARAMETER_ERROR = [400200, '参数错误'];
+    const CLIENT_CREATED_ERROR = [400201, '数据已存在'];
+    const CLIENT_DELETED_ERROR = [400202, '数据不存在'];
+    // 401 - 访问被拒绝
+    const CLIENT_HTTP_UNSYNCHRONIZE_TIMER = [401001, '通信双方时钟不同步,或通信超时'];
+    const CLIENT_HTTP_UNAUTHORIZED = [401200, '授权失败,请先登录'];
+    const CLIENT_HTTP_UNAUTHORIZED_EXPIRED = [401201, '账号信息已过期,请重新登录'];
+    const CLIENT_HTTP_UNAUTHORIZED_BLACKLISTED = [401202, '账号已被禁止登录'];
+    // 403 - 禁止访问
+    // 404 - 没有找到文件或目录
+    const CLIENT_NOT_FOUND_ERROR = [404001, '没有找到该页面'];
+    // 405 - 用来访问本页面的 HTTP 谓词不被允许(方法不被允许)
+    const CLIENT_METHOD_HTTP_TYPE_ERROR = [405001, 'HTTP请求类型错误'];
+    // 406 - 客户端浏览器不接受所请求页面的 MIME 类型
+    // 407 - 要求进行代理身份验证
+    // 412 - 前提条件失败
+    // 413 – 请求实体太大
+    // 414 - 请求 URI 太长
+    // 415 – 不支持的媒体类型
+    // 416 – 所请求的范围无法满足
+    // 417 – 执行失败
+    // 423 – 锁定的错误
+
+    /*-------------------------------------------------------------------------------------------*/
+    // 500开头的表示服务器错误,服务器因为代码,或者什么原因终止运行
+    // 服务端操作错误码:500 ~ 599 开头,后拼接 3 位
+    // 500 - 内部服务器错误
+    const SYSTEM_ERROR = [500001, '服务器错误'];
+    const SYSTEM_UNAVAILABLE = [500002, '服务器正在维护,暂不可用'];
+    const SYSTEM_CACHE_CONFIG_ERROR = [500003, '缓存配置错误'];
+    const SYSTEM_CACHE_MISSED_ERROR = [500004, '缓存未命中'];
+    const SYSTEM_CONFIG_ERROR = [500005, '系统配置错误'];
+
+    // 业务操作错误码(外部服务或内部服务调用)
+    const SERVICE_REGISTER_ERROR = [500101, '注册失败'];
+    const SERVICE_LOGIN_ERROR = [500102, '登录失败'];
+    const SERVICE_LOGIN_ACCOUNT_ERROR = [500103, '账号或密码错误'];
+    const SERVICE_USER_INTEGRAL_ERROR = [500200, '积分不足'];
+
+    //501 - 页眉值指定了未实现的配置
+    //502 - Web 服务器用作网关或代理服务器时收到了无效响应
+    //503 - 服务不可用。这个错误代码为 IIS 6.0 所专用
+    //504 - 网关超时
+    //505 - HTTP 版本不受支持
+    /*-------------------------------------------------------------------------------------------*/
+}

+ 49 - 0
app/Helpers/WebApiResponse.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace App\Helpers;
+
+use Illuminate\Http\JsonResponse;
+
+trait WebApiResponse
+{
+    public function succeed($data = null, $addition = null, $codeResponse = ResponseEnum::HTTP_OK): JsonResponse // 成功
+    {
+        return $this->jsonResponse('success', $codeResponse, $data, $addition);
+    }
+
+    private function jsonResponse($status, $codeResponse, $data, $addition): JsonResponse // 返回数据
+    {
+        [$code, $message] = $codeResponse;
+        if ($status === 'success') {
+            $etag = self::abortIfNotModified($data);
+        }
+        $code = $code < 1000 ? $code : (int) ($code / 1000);
+        $data = compact('status', 'code', 'data', 'message');
+        if (isset($addition)) {
+            $data = array_merge($data, $addition);
+        }
+
+        return response()->json($data, $code, ['ETAG' => $etag ?? '']);
+    }
+
+    private static function abortIfNotModified($data): string // 检查数据是否有变动
+    {
+        $req = request();
+
+        if (! $req->isMethod('GET')) { // Only for "GET" method
+            return '';
+        }
+
+        $etag = sha1(json_encode($data));
+        if (! empty($req->header('IF-NONE-MATCH')) && hash_equals($etag, $req->header('IF-NONE-MATCH'))) {
+            abort(304);
+        }
+
+        return $etag;
+    }
+
+    public function failed($codeResponse = ResponseEnum::HTTP_ERROR, $data = null, $addition = null): JsonResponse // 失败
+    {
+        return $this->jsonResponse('fail', $codeResponse, $data, $addition);
+    }
+}

+ 4 - 2
app/Http/Controllers/Admin/LogsController.php

@@ -3,6 +3,7 @@
 namespace App\Http\Controllers\Admin;
 namespace App\Http\Controllers\Admin;
 
 
 use App\Components\IP;
 use App\Components\IP;
+use App\Helpers\DataChart;
 use App\Http\Controllers\Controller;
 use App\Http\Controllers\Controller;
 use App\Models\Node;
 use App\Models\Node;
 use App\Models\NodeOnlineIp;
 use App\Models\NodeOnlineIp;
@@ -19,8 +20,9 @@ use Response;
 
 
 class LogsController extends Controller
 class LogsController extends Controller
 {
 {
-    // 订单列表
-    public function orderList(Request $request)
+    use DataChart;
+
+    public function orderList(Request $request) // 订单列表
     {
     {
         $query = Order::with(['user:id,username', 'goods:id,name', 'coupon:id,name,sn']);
         $query = Order::with(['user:id,username', 'goods:id,name', 'coupon:id,name,sn']);
 
 

+ 4 - 2
app/Http/Controllers/Admin/NodeController.php

@@ -3,6 +3,7 @@
 namespace App\Http\Controllers\Admin;
 namespace App\Http\Controllers\Admin;
 
 
 use App\Components\NetworkDetection;
 use App\Components\NetworkDetection;
+use App\Helpers\DataChart;
 use App\Http\Controllers\Controller;
 use App\Http\Controllers\Controller;
 use App\Http\Requests\Admin\NodeRequest;
 use App\Http\Requests\Admin\NodeRequest;
 use App\Jobs\VNet\reloadNode;
 use App\Jobs\VNet\reloadNode;
@@ -20,8 +21,9 @@ use Response;
 
 
 class NodeController extends Controller
 class NodeController extends Controller
 {
 {
-    // 节点列表
-    public function index(Request $request)
+    use DataChart;
+
+    public function index(Request $request) // 节点列表
     {
     {
         $status = $request->input('status');
         $status = $request->input('status');
 
 

+ 5 - 2
app/Http/Controllers/Admin/UserController.php

@@ -15,6 +15,7 @@ use App\Models\User;
 use App\Models\UserGroup;
 use App\Models\UserGroup;
 use App\Models\UserHourlyDataFlow;
 use App\Models\UserHourlyDataFlow;
 use App\Models\UserOauth;
 use App\Models\UserOauth;
+use App\Services\ProxyServer;
 use Arr;
 use Arr;
 use Auth;
 use Auth;
 use Exception;
 use Exception;
@@ -291,9 +292,11 @@ class UserController extends Controller
 
 
     public function exportProxyConfig(Request $request, User $user): JsonResponse
     public function exportProxyConfig(Request $request, User $user): JsonResponse
     {
     {
-        $server = Node::findOrFail($request->input('id'))->getConfig($user); // 提取节点信息
+        $proxyServer = ProxyServer::getInstance();
+        $proxyServer->setUser($user);
+        $server = $proxyServer->getProxyConfig(Node::findOrFail($request->input('id')));
 
 
-        return Response::json(['status' => 'success', 'data' => $this->getUserNodeInfo($server, $request->input('type') !== 'text'), 'title' => $server['type']]);
+        return Response::json(['status' => 'success', 'data' => $proxyServer->getUserProxyConfig($server, $request->input('type') !== 'text'), 'title' => $server['type']]);
     }
     }
 
 
     public function oauth()
     public function oauth()

+ 105 - 0
app/Http/Controllers/Api/Client/AuthController.php

@@ -0,0 +1,105 @@
+<?php
+
+namespace App\Http\Controllers\Api\Client;
+
+use App\Components\Helpers;
+use App\Helpers\ClientApiResponse;
+use App\Helpers\ResponseEnum;
+use App\Services\UserService;
+use function auth;
+use function config;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use Illuminate\Routing\Controller;
+use Validator;
+
+class AuthController extends Controller
+{
+    use ClientApiResponse;
+
+    private static $method;
+
+    public function __construct(Request $request)
+    {
+        if (str_contains($request->userAgent(), 'bob_vpn')) {
+            $this->setClient('bob');
+        }
+    }
+
+    public function register(Request $request, UserService $userService): JsonResponse
+    {
+        $validator = Validator::make($request->all(), [
+            'nickname' => 'required|string|between:2,100',
+            'username' => 'required|'.(sysConfig('username_type') ?? 'email').'|max:100|unique:user,username',
+            'password' => 'required|string|confirmed|min:6',
+        ]);
+
+        if ($validator->fails()) {
+            return $this->failed(ResponseEnum::CLIENT_PARAMETER_ERROR, $validator->errors()->all());
+        }
+        $data = $validator->validated();
+
+        // 创建新用户
+        if ($user = Helpers::addUser($data['username'], $data['password'], (int) sysConfig('default_traffic'), sysConfig('default_days'), null, $data['nickname'])) {
+            auth()->login($user, true);
+
+            return $this->succeed([
+                'token'     => $user->createToken('client')->plainTextToken,
+                'expire_in' => time() + config('session.lifetime') * Minute,
+                'user'      => $userService->getProfile(),
+            ], null, ResponseEnum::USER_SERVICE_REGISTER_SUCCESS);
+        }
+
+        return $this->failed(ResponseEnum::USER_SERVICE_REGISTER_ERROR);
+    }
+
+    public function login(Request $request): JsonResponse
+    {
+        if (self::$client === 'bob') {
+            $rules = [
+                'email'  => 'required|'.(sysConfig('username_type') ?? 'email'),
+                'passwd' => 'required|string|min:6',
+            ];
+        } else {
+            $rules = [
+                'username' => 'required|'.(sysConfig('username_type') ?? 'email'),
+                'password' => 'required|string|min:6',
+            ];
+        }
+        $validator = Validator::make($request->all(), $rules);
+
+        if ($validator->fails()) {
+            return $this->failed(ResponseEnum::CLIENT_PARAMETER_ERROR, $validator->errors()->all());
+        }
+
+        if (auth()->attempt(['username' => $request->input('username') ?: $request->input('email'), 'password' => $request->input('password') ?: $request->input('passwd')],
+            true)) {
+            $user = auth()->user();
+            if ($user && $user->status === -1) {
+                return $this->failed(ResponseEnum::CLIENT_HTTP_UNAUTHORIZED_BLACKLISTED);
+            }
+            if (self::$client === 'bob') {
+                $request->session()->put('uid', $user->id);
+            }
+
+            $userService = UserService::getInstance();
+
+            return $this->succeed([
+                'token'     => $user->createToken('client')->plainTextToken,
+                'expire_in' => time() + config('session.lifetime') * Minute,
+                'user'      => $userService->getProfile(),
+            ], null, ResponseEnum::USER_SERVICE_LOGIN_SUCCESS);
+        }
+
+        return $this->failed(ResponseEnum::SERVICE_LOGIN_ACCOUNT_ERROR);
+    }
+
+    public function logout(Request $request): JsonResponse
+    {
+        auth()->logout();
+        $request->session()->invalidate();
+        $request->session()->regenerateToken();
+
+        return $this->failed(ResponseEnum::USER_SERVICE_LOGOUT_SUCCESS);
+    }
+}

+ 304 - 0
app/Http/Controllers/Api/Client/ClientController.php

@@ -0,0 +1,304 @@
+<?php
+
+namespace App\Http\Controllers\Api\Client;
+
+use App\Components\Helpers;
+use App\Helpers\ClientApiResponse;
+use App\Helpers\ResponseEnum;
+use App\Models\Article;
+use App\Models\GoodsCategory;
+use App\Models\Level;
+use App\Models\ReferralLog;
+use App\Models\Ticket;
+use App\Services\ProxyServer;
+use App\Services\UserService;
+use Arr;
+use Artisan;
+use function config;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use Illuminate\Routing\Controller;
+use Illuminate\Support\Facades\Cache;
+
+class ClientController extends Controller
+{
+    use ClientApiResponse;
+
+    private static $method;
+
+    public function __construct(Request $request)
+    {
+        if (str_contains($request->userAgent(), 'bob_vpn')) {
+            $this->setClient('bob');
+        }
+    }
+
+    public function getUserInfo()
+    {
+        $user = auth()->user();
+
+        if (! $user) {
+            return false;
+        }
+
+        $userInfo = UserService::getInstance()->getProfile();
+        $userInfo['user_name'] = $user->nickname;
+        $userInfo['email'] = $user->username;
+        $userInfo['class_expire'] = $user->expiration_date;
+        $userInfo['money'] = $user->credit;
+        $userInfo['plan']['name'] = $user->orders()->activePlan()->latest()->first()->goods->name ?? '无';
+        $ann = Article::type(2)->latest()->first();
+        $user_expire = now()->diffInDays($user->expired_at, false) < 0;
+        $total = $user->u + $user->d;
+        $transfer_enable = $user->transfer_enable;
+        $expired_days = now()->diffInDays($user->expired_at, false);
+        $userInfo['class_expire_notice'] = '';
+        if ($expired_days < 0) {
+            $userInfo['class_expire_notice'] = '账号会员已过期,请先续费再使用~';
+        } elseif ($expired_days > 0 && $expired_days <= config('client.class_expire_notice.days')) {
+            $userInfo['class_expire_notice'] = sprintf(config('client.class_expire_notice.msg'), $expired_days);
+        }
+
+        $data['info'] = [
+            'user'                 => $userInfo,
+            'ssrSubToken'          => $user->subscribe->code,
+            'user_expire'          => $user_expire,
+            'subUrl'               => route('sub', $user->subscribe->code),
+            'baseUrl'              => sysConfig('subscribe_domain') ?? sysConfig('website_url'),
+            'ann'                  => $ann,
+            'avatar'               => $user->avatar,
+            'usedTraffic'          => flowAutoShow($total),
+            'enableTraffic'        => flowAutoShow($transfer_enable),
+            'unUsedTraffic'        => flowAutoShow($transfer_enable - $total),
+            'reset_time'           => now()->diffInDays($user->reset_time, false),
+            'android_index_button' => config('client.android_index_button'),
+        ];
+
+        return $this->succeed(null, $data);
+    }
+
+    public function getOrders(Request $request)
+    {
+        $user = $request->user();
+        $orders = $user->orders()->orderByDesc('id')->limit(8)->get();
+        $data = [];
+        foreach ($orders as $order) {
+            $data[] = [
+                'id'           => $order->id,
+                'total_amount' => $order->amount * 100,
+                'plan'         => ['name' => $order->goods()->value('name') ?? '余额充值'],
+                'status'       => [-1 => 2, 0 => 0, 1 => 1, 2 => 3, 3 => 4][$order->status],
+                'created_at'   => strtotime($order->created_at),
+            ];
+        }
+
+        return $this->succeed($data);
+    }
+
+    public function getUserTransfer()
+    {
+        $user = auth()->user();
+
+        return $this->succeed(null, [
+            'arr' => [
+                'todayUsedTraffic' => flowAutoShow($user->d),
+                'lastUsedTraffic'  => flowAutoShow($user->u),
+                'unUsedTraffic'    => flowAutoShow($user->transfer_enable - $user->d - $user->u),
+            ],
+        ]);
+    }
+
+    public function shop()
+    {
+        $shops = [
+            'keys' => [],
+            'data' => [],
+        ];
+        foreach (GoodsCategory::query()->whereStatus(1)->whereHas('goods')->get() as $category) {
+            $shops['keys'][] = $category['name'];
+            $shops['data'][$category['name']] = $category->goods()->get(['name', 'price', 'traffic'])->append('traffic_label')->toArray();
+        }
+
+        return $this->succeed($shops);
+    }
+
+    public function getInvite()
+    {
+        $user = auth()->user();
+
+        $referral_traffic = flowAutoShow(sysConfig('referral_traffic') * MB);
+        $referral_percent = sysConfig('referral_percent');
+        // 邀请码
+        $code = $user->invites()->whereStatus(0)->value('code');
+
+        $data['invite_gift'] = trans('user.invite.promotion', [
+            'traffic'          => $referral_traffic,
+            'referral_percent' => $referral_percent * 100,
+        ]);
+
+        $data['invite_code'] = $code ?? UserService::getInstance()->inviteURI(true);
+        $data['invite_url'] = UserService::getInstance()->inviteURI();
+        $data['invite_text'] = $data['invite_url'].'&(复制整段文字到浏览器打开即可访问),找梯子最重要的就是稳定,这个已经上线三年了,一直稳定没有被封过,赶紧下载备用吧!'.($code ? '安装后打开填写我的邀请码【'.$code.'】,你还能多得3天会员.' : '');
+        // 累计数据
+        $data['back_sum'] = ReferralLog::uid()->sum('commission') / 100;
+        $data['user_num'] = $user->invitees()->count();
+        $data['list'] = $user->invitees()->whereHas('orders', function (Builder $query) {
+            $query->where('status', '>=', 2)->where('amount', '>', 0);
+        })->selectRaw('username as user_name, UNIX_TIMESTAMP(created_at) as datetime, id')->orderByDesc('created_at')->limit(10)->get()->toArray();
+        foreach ($data['list'] as &$item) {
+            $item['ref_get'] = ReferralLog::uid()->where('invitee_id', $item['id'])->sum('commission') / 100;
+        }
+
+        return $this->succeed(null, $data);
+    }
+
+    public function checkIn(Request $request): JsonResponse
+    {
+        $user = $request->user();
+        // 系统开启登录加积分功能才可以签到
+        if (! sysConfig('is_checkin')) {
+            return response()->json(['ret' => 0, 'title' => trans('common.failed'), 'msg' => trans('user.home.attendance.disable')]);
+        }
+
+        // 已签到过,验证是否有效
+        if (Cache::has('userCheckIn_'.$user->id)) {
+            return response()->json(['ret' => 0, 'title' => trans('common.success'), 'msg' => trans('user.home.attendance.done')]);
+        }
+
+        $traffic = random_int((int) sysConfig('min_rand_traffic'), (int) sysConfig('max_rand_traffic')) * MB;
+
+        if (! $user->incrementData($traffic)) {
+            return response()->json(['ret' => 0, 'title' => trans('common.failed'), 'msg' => trans('user.home.attendance.failed')]);
+        }
+
+        // 写入用户流量变动记录
+        Helpers::addUserTrafficModifyLog($user->id, null, $user->transfer_enable, $user->transfer_enable + $traffic, trans('user.home.attendance.attribute'));
+
+        // 多久后可以再签到
+        $ttl = sysConfig('traffic_limit_time') ? sysConfig('traffic_limit_time') * Minute : Day;
+        Cache::put('userCheckIn_'.$user->id, '1', $ttl);
+
+        return $this->succeed(null, null, [200, trans('user.home.attendance.success', ['data' => flowAutoShow($traffic)])]);
+    }
+
+    public function proxyCheck(Request $request)
+    {
+        $md5 = $request->get('md5', '');
+
+        $proxy = ProxyServer::getInstance()->getProxyCode('clash');
+        if (strtolower(md5(json_encode($proxy))) === strtolower($md5)) {
+            return $this->succeed(false);
+        }
+
+        return $this->succeed(true, ['md5' => strtolower(md5(json_encode($proxy)))]);
+    }
+
+    public function downloadProxies(Request $request)
+    {
+        $flag = strtolower($request->input('flag') ?? ($request->userAgent() ?? ''));
+
+        return ProxyServer::getInstance()->getProxyText($flag === 'v2rayng' ? 'v2rayng' : 'clash', $request->input('type'));
+    }
+
+    public function getProxyList()
+    {
+        $proxyServer = ProxyServer::getInstance();
+
+        $servers = [];
+        foreach ($proxyServer->getNodeList(null, false) as $node) {
+            $server = $proxyServer->getProxyConfig($node);
+            if ($server['type'] === '`shadowsocks`' || $server['type'] === 'shadowsocksr') {
+                $server['type'] = 1;
+            }
+
+            $online_log = $node->onlineLogs->where('log_time', '>=', strtotime('-5 minutes'))->sortBy('log_time')->first(); // 在线人数
+            $server['node_ip'] = filter_var($server['host'], FILTER_VALIDATE_IP) ? $server['host'] : gethostbyname($server['host']);
+            $server['online'] = $online_log->online_user ?? 0;
+            $this->getOnlineCount($server, $server['online']);
+            $servers[] = $server;
+        }
+
+        return $this->succeed($servers);
+    }
+
+    private function getOnlineCount(&$node, int $online)
+    {
+        $node['flag'] = $node['area'];
+
+        if ($online < 15) {
+            $node['text'] = '⭐ 畅 通';
+            $node['color'] = '#28a745';
+        } elseif ($online < 30) {
+            $node['text'] = '💫 拥 挤';
+            $node['color'] = '#ffc107';
+        } else {
+            $node['text'] = '🔥 爆 满';
+            $node['color'] = '#dc3545';
+        }
+    }
+
+    public function getconfig()
+    {
+        $config = $this->clientConfig();
+        Arr::forget($config, ['read', 'configured']);
+
+        return $this->succeed(null, ['config' => $config]);
+    }
+
+    private function clientConfig($key = '')
+    {
+        if (! config('client')) {
+            Artisan::call('config:cache');
+        }
+
+        if (config('client.configured') !== true && config('client.read')) {
+            $this->setClientConfig();
+        }
+
+        return $key ? config('client.'.$key) : config('client');
+    }
+
+    private function setClientConfig() //
+    {
+        $ann = Article::type(2)->latest()->first();
+
+        if ($ann) {
+            config(['client.notice.title' => $ann->title, 'client.notice.content' => $ann->content]);
+        }
+        config([
+            'client.configured'      => true,
+            'client.name'            => sysConfig('website_name'),
+            'client.node_class_name' => Level::all()->pluck('name', 'level')->toArray(),
+            'client.baseUrl'         => sysConfig('website_url'),
+            'client.subscribe_url'   => sysConfig('web_api_url') ?? sysConfig('website_url'),
+            'client.checkinMin'      => sysConfig('min_rand_traffic'),
+            'client.checkinMax'      => sysConfig('max_rand_traffic'),
+            'client.invite_gift'     => sysConfig('default_traffic') / 1024,
+        ]);
+    }
+
+    public function checkClientVersion(Request $request)
+    {
+        $version = $request->input('version');
+        $type = $request->input('type');
+        $config = $this->clientConfig('vpn_update');
+        if (! isset($version, $type)) {
+            return $this->failed(ResponseEnum::CLIENT_PARAMETER_ERROR);
+        }
+        $vpn = $config[$type];
+
+        if (! $config['enable'] || $vpn['version'] === $version) {
+            return $this->succeed(null, ['enable' => false]);
+        }
+
+        return $this->succeed(null, ['enable' => true, 'download_url' => $vpn['download_url'], 'must' => $vpn['must'], 'message' => $vpn['message']], [200, $vpn['message']]);
+    }
+
+    public function ticketList()
+    {
+        $ticket = Ticket::where('user_id', auth()->user()->id)->selectRaw('id, title, UNIX_TIMESTAMP(created_at) as datetime, status')->orderBy('created_at', 'DESC')->get();
+
+        return $this->succeed($ticket);
+    }
+}

+ 0 - 328
app/Http/Controllers/Api/Client/V1Controller.php

@@ -1,328 +0,0 @@
-<?php
-
-namespace App\Http\Controllers\Api\Client;
-
-use App\Components\Helpers;
-use App\Http\Controllers\Controller;
-use App\Http\Controllers\PaymentController;
-use App\Models\Coupon;
-use App\Models\Goods;
-use App\Models\GoodsCategory;
-use App\Models\Order;
-use App\Models\ReferralLog;
-use Exception;
-use Hashids\Hashids;
-use Illuminate\Http\JsonResponse;
-use Illuminate\Http\Request;
-use Illuminate\Support\Facades\Cache;
-use Log;
-use Validator;
-
-class V1Controller extends Controller
-{
-    private static $method;
-
-    public function __construct()
-    {
-        $this->middleware('auth:api')->except('login', 'register', 'shop', 'getConfig');
-        auth()->shouldUse('api');
-    }
-
-    /**
-     * @param  Request  $request
-     * @return JsonResponse
-     */
-    public static function getStatus(Request $request): JsonResponse
-    {
-        $order_id = $request->input('order_id');
-        $payment = Order::query()->find($order_id)->payment;
-        if ($payment) {
-            if ($payment->status === 1) {
-                return response()->json(['ret' => 1, 'msg' => '支付成功']);
-            }
-
-            if ($payment->status === -1) {
-                return response()->json(['ret' => 0, 'msg' => '订单超时未支付,已自动关闭']);
-            }
-
-            return response()->json(['ret' => 0, 'msg' => '等待支付']);
-        }
-
-        return response()->json(['ret' => 0, 'msg' => '未知订单']);
-    }
-
-    public function login(Request $request)
-    {
-        $validator = Validator::make($request->all(), [
-            'username' => 'required|'.(sysConfig('username_type') ?? 'email'),
-            'password' => 'required|string|min:6',
-        ]);
-
-        if ($validator->fails()) {
-            return response()->json(['ret' => 0, 'msg' => $validator->errors()->all()], 422);
-        }
-
-        if ($token = auth()->attempt($validator->validated())) {
-            return $this->createNewToken($token);
-        }
-
-        return response()->json(['ret' => 0, 'msg' => '登录信息错误'], 401);
-    }
-
-    protected function createNewToken($token)
-    {
-        return response()->json([
-            'ret'  => 1,
-            'data' => [
-                'access_token' => $token,
-                'token_type'   => 'bearer',
-                'expires_in'   => auth()->factory()->getTTL() * 60,
-                'user'         => auth()->user()->profile(),
-            ],
-        ]);
-    }
-
-    public function register(Request $request)
-    {
-        $validator = Validator::make($request->all(), [
-            'nickname' => 'required|string|between:2,100',
-            'username' => 'required|'.(sysConfig('username_type') ?? 'email').'|max:100|unique:user,username',
-            'password' => 'required|string|confirmed|min:6',
-        ]);
-
-        if ($validator->fails()) {
-            return response()->json($validator->errors()->all(), 400);
-        }
-        $data = $validator->validated();
-
-        // 创建新用户
-        $user = Helpers::addUser($data['username'], $data['password'], (int) sysConfig('default_traffic'), sysConfig('default_days'), null, $data['nickname']);
-
-        return response()->json(['ret' => 1, 'user' => $user], 201);
-    }
-
-    public function logout()
-    {
-        auth()->logout();
-
-        return response()->json(['ret' => 1]);
-    }
-
-    public function refresh()
-    {
-        return $this->createNewToken(auth()->refresh());
-    }
-
-    public function userProfile()
-    {
-        $user = auth()->user();
-        $userInfo = $user->profile();
-        $userInfo['subUrl'] = $user->subUrl();
-        $totalTransfer = $user->transfer_enable;
-        $usedTransfer = $user->used_traffic;
-        $unusedTraffic = max($totalTransfer - $usedTransfer, 0);
-        $userInfo['unusedTraffic'] = flowAutoShow($unusedTraffic);
-
-        return response()->json(['ret' => 1, 'data' => $userInfo]);
-    }
-
-    public function nodeList()
-    {
-        return response()->json(['ret' => 1, 'data' => auth()->user()->nodes()->get()]);
-    }
-
-    public function shop()
-    {
-        $shops = [
-            'keys' => [],
-            'data' => [],
-        ];
-        foreach (GoodsCategory::query()->whereStatus(1)->get() as $item) {
-            $shops['keys'][] = $item['name'];
-            $shops['data'][$item['name']] = $item->goods()->get()->append('traffic_label')->toArray();
-        }
-
-        return response()->json(['ret' => 1, 'data' => $shops]);
-    }
-
-    public function getConfig()
-    {
-        $config = config('bobclient');
-        $config['website_name'] = sysConfig('website_name');
-        $config['website_url'] = sysConfig('website_url');
-        $config['payment'] = [
-            'alipay' => sysConfig('is_AliPay'),
-            'wechat' => sysConfig('is_WeChatPay'),
-        ];
-
-        return response()->json(['ret' => 1, 'data' => $config]);
-    }
-
-    public function purchase(Request $request)
-    {
-        $goods_id = $request->input('goods_id');
-        $coupon_sn = $request->input('coupon_sn');
-        self::$method = $request->input('method');
-        $credit = $request->input('amount');
-        $pay_type = $request->input('pay_type');
-        $amount = 0;
-
-        if ($credit) { // 充值余额
-            if (! is_numeric($credit) || $credit <= 0) {
-                return response()->json(['ret' => 0, 'msg' => trans('user.payment.error')]);
-            }
-            $amount = $credit;
-        } elseif ($goods_id && self::$method) { // 购买服务
-            $goods = Goods::find($goods_id);
-            if (! $goods || ! $goods->status) {
-                return response()->json(['ret' => 0, 'msg' => '订单创建失败:商品已下架']);
-            }
-            $amount = $goods->price;
-
-            // 是否有生效的套餐
-            $activePlan = Order::userActivePlan()->doesntExist();
-
-            // 无生效套餐,禁止购买加油包
-            if ($goods->type === 1 && $activePlan) {
-                return response()->json(['ret' => 0, 'msg' => '购买加油包前,请先购买套餐']);
-            }
-
-            // 单个商品限购
-            if ($goods->limit_num) {
-                $count = Order::uid()->where('status', '>=', 0)->whereGoodsId($goods_id)->count();
-                if ($count >= $goods->limit_num) {
-                    return response()->json(['ret' => 0, 'msg' => '此商品限购'.$goods->limit_num.'次,您已购买'.$count.'次']);
-                }
-            }
-
-            // 使用优惠券
-            if ($coupon_sn) {
-                $coupon = Coupon::whereStatus(0)->whereIn('type', [1, 2])->whereSn($coupon_sn)->first();
-                if (! $coupon) {
-                    return response()->json(['ret' => 0, 'msg' => '订单创建失败:优惠券不存在']);
-                }
-
-                // 计算实际应支付总价
-                $amount = $coupon->type === 2 ? $goods->price * $coupon->value / 100 : $goods->price - $coupon->value;
-                $amount = $amount > 0 ? round($amount, 2) : 0; // 四舍五入保留2位小数,避免无法正常创建订单
-            }
-
-            //非余额付款下,检查在线支付是否开启
-            if (self::$method !== 'credit') {
-                // 判断是否开启在线支付
-                if (! sysConfig('is_onlinePay')) {
-                    return response()->json(['ret' => 0, 'msg' => '订单创建失败:系统并未开启在线支付功能']);
-                }
-
-                // 判断是否存在同个商品的未支付订单
-                if (Order::uid()->whereStatus(0)->exists()) {
-                    return response()->json(['ret' => 0, 'msg' => '订单创建失败:尚有未支付的订单,请先去支付']);
-                }
-            } elseif (Auth::getUser()->credit < $amount) { // 验证账号余额是否充足
-                return response()->json(['ret' => 0, 'msg' => '您的余额不足,请先充值']);
-            }
-
-            // 价格异常判断
-            if ($amount < 0) {
-                return response()->json(['ret' => 0, 'msg' => '订单创建失败:订单总价异常']);
-            }
-
-            if ($amount === 0 && self::$method !== 'credit') {
-                return response()->json(['ret' => 0, 'msg' => '订单创建失败:订单总价为0,无需使用在线支付']);
-            }
-        }
-
-        // 生成订单
-        try {
-            $newOrder = Order::create([
-                'sn'            => date('ymdHis').random_int(100000, 999999),
-                'user_id'       => auth()->id(),
-                'goods_id'      => $credit ? null : $goods_id,
-                'coupon_id'     => $coupon->id ?? null,
-                'origin_amount' => $credit ?: ($goods->price ?? 0),
-                'amount'        => $amount,
-                'pay_type'      => $pay_type,
-                'pay_way'       => self::$method,
-            ]);
-
-            // 使用优惠券,减少可使用次数
-            if (! empty($coupon)) {
-                if ($coupon->usable_times > 0) {
-                    $coupon->decrement('usable_times');
-                }
-
-                Helpers::addCouponLog('订单支付使用', $coupon->id, $goods_id, $newOrder->id);
-            }
-
-            $request->merge(['id' => $newOrder->id, 'type' => $pay_type, 'amount' => $amount]);
-            PaymentController::$method = self::$method;
-            // 生成支付单
-            $data = PaymentController::getClient()->purchase($request);
-            $data = $data->getData(true);
-            $data['order_id'] = $newOrder->id;
-
-            return response()->json($data);
-        } catch (Exception $e) {
-            Log::error('订单生成错误:'.$e->getMessage());
-        }
-
-        return response()->json(['ret' => 0, 'msg' => '订单创建失败']);
-    }
-
-    public function gift(Request $request)
-    {
-        $user = $request->user('api');
-        $referral_traffic = flowAutoShow(sysConfig('referral_traffic') * MB);
-        $referral_percent = sysConfig('referral_percent');
-        // 邀请码
-        $code = $user->invites()->whereStatus(1)->value('code');
-
-        $data['invite_gift'] = trans('user.invite.promotion', [
-            'traffic'          => $referral_traffic,
-            'referral_percent' => $referral_percent * 100,
-        ]);
-        $affSalt = sysConfig('aff_salt');
-        if (isset($affSalt)) {
-            $aff_link = route('register', ['aff' => (new Hashids($affSalt, 8))->encode($user->id)]);
-        } else {
-            $aff_link = route('register', ['aff' => $user->id]);
-        }
-        $data['invite_url'] = $aff_link;
-        $data['invite_text'] = $aff_link.'&(复制整段文字到浏览器打开即可访问),找梯子最重要的就是稳定,这个已经上线三年了,一直稳定没有被封过,赶紧下载备用吧!安装后打开填写我的邀请码【'.$code.'】,你还能多得3天会员.';
-        // 累计数据
-        $data['back_sum'] = ReferralLog::query()->where('inviter_id', $user->id)->sum('commission') / 100;
-        $data['user_sum'] = $user->invitees()->count();
-        $data['list'] = $user->invitees()->selectRaw('username, UNIX_TIMESTAMP(created_at) as created_at')->limit(10)->get();
-
-        return response()->json(['ret' => 1, 'data' => $data]);
-    }
-
-    public function checkIn(Request $request): JsonResponse
-    {
-        $user = $request->user();
-        // 系统开启登录加积分功能才可以签到
-        if (! sysConfig('is_checkin')) {
-            return response()->json(['ret' => 0, 'title' => trans('common.failed'), 'msg' => trans('user.home.attendance.disable')]);
-        }
-
-        // 已签到过,验证是否有效
-        if (Cache::has('userCheckIn_'.$user->id)) {
-            return response()->json(['ret' => 0, 'title' => trans('common.success'), 'msg' => trans('user.home.attendance.done')]);
-        }
-
-        $traffic = random_int((int) sysConfig('min_rand_traffic'), (int) sysConfig('max_rand_traffic')) * MB;
-
-        if (! $user->incrementData($traffic)) {
-            return response()->json(['ret' => 0, 'title' => trans('common.failed'), 'msg' => trans('user.home.attendance.failed')]);
-        }
-
-        // 写入用户流量变动记录
-        Helpers::addUserTrafficModifyLog($user->id, null, $user->transfer_enable, $user->transfer_enable + $traffic, trans('user.home.attendance.attribute'));
-
-        // 多久后可以再签到
-        $ttl = sysConfig('traffic_limit_time') ? sysConfig('traffic_limit_time') * Minute : Day;
-        Cache::put('userCheckIn_'.$user->id, '1', $ttl);
-
-        return response()->json(['ret' => 1, 'msg' => trans('user.home.attendance.success', ['data' => flowAutoShow($traffic)])]);
-    }
-}

+ 19 - 46
app/Http/Controllers/Api/WebApi/CoreController.php

@@ -2,23 +2,26 @@
 
 
 namespace App\Http\Controllers\Api\WebApi;
 namespace App\Http\Controllers\Api\WebApi;
 
 
+use App\Helpers\ResponseEnum;
+use App\Helpers\WebApiResponse;
 use App\Models\Node;
 use App\Models\Node;
 use App\Models\User;
 use App\Models\User;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
 use Illuminate\Routing\Controller;
 use Illuminate\Routing\Controller;
-use Response;
 use Validator;
 use Validator;
 
 
 class CoreController extends Controller
 class CoreController extends Controller
 {
 {
+    use WebApiResponse;
+
     // 上报节点心跳信息
     // 上报节点心跳信息
     public function setNodeStatus(Request $request, Node $node): JsonResponse
     public function setNodeStatus(Request $request, Node $node): JsonResponse
     {
     {
         $validator = Validator::make($request->all(), ['cpu' => 'required', 'mem' => 'required', 'disk' => 'required', 'uptime' => 'required|numeric']);
         $validator = Validator::make($request->all(), ['cpu' => 'required', 'mem' => 'required', 'disk' => 'required', 'uptime' => 'required|numeric']);
 
 
         if ($validator->fails()) {
         if ($validator->fails()) {
-            return $this->returnData('上报节点心跳信息失败,请检查字段');
+            return $this->failed(ResponseEnum::CLIENT_PARAMETER_ERROR, $validator->errors()->all());
         }
         }
 
 
         $data = array_map('intval', $validator->validated());
         $data = array_map('intval', $validator->validated());
@@ -28,40 +31,10 @@ class CoreController extends Controller
             'load'     => implode(' ', [$data['cpu'] / 100, $data['mem'] / 100, $data['disk'] / 100]),
             'load'     => implode(' ', [$data['cpu'] / 100, $data['mem'] / 100, $data['disk'] / 100]),
             'log_time' => time(),
             'log_time' => time(),
         ])) {
         ])) {
-            return $this->returnData('上报节点心跳信息成功', 200, 'success');
-        }
-
-        return $this->returnData('生成节点心跳信息失败', 400);
-    }
-
-    // 返回数据
-    public function returnData(string $message, int $code = 422, string $status = 'fail', array $data = [], array $addition = null): JsonResponse
-    {
-        $etag = self::abortIfNotModified($data);
-        $data = compact('status', 'code', 'data', 'message');
-
-        if (isset($addition)) {
-            $data = array_merge($data, $addition);
-        }
-
-        return Response::json($data)->header('ETAG', $etag)->setStatusCode($code);
-    }
-
-    // 检查数据是否有变动
-    private static function abortIfNotModified($data): string
-    {
-        $req = request();
-        // Only for "GET" method
-        if (! $req->isMethod('GET')) {
-            return '';
-        }
-
-        $etag = sha1(json_encode($data));
-        if (! empty($req->header('IF-NONE-MATCH')) && hash_equals($etag, $req->header('IF-NONE-MATCH'))) {
-            abort(304);
+            return $this->succeed();
         }
         }
 
 
-        return $etag;
+        return $this->failed([400201, '生成节点心跳信息失败']);
     }
     }
 
 
     // 上报节点在线IP
     // 上报节点在线IP
@@ -70,7 +43,7 @@ class CoreController extends Controller
         $validator = Validator::make($request->all(), ['*.uid' => 'required|numeric|exists:user,id', '*.ip' => 'required|string']);
         $validator = Validator::make($request->all(), ['*.uid' => 'required|numeric|exists:user,id', '*.ip' => 'required|string']);
 
 
         if ($validator->fails()) {
         if ($validator->fails()) {
-            return $this->returnData('上报节点在线用户IP信息失败,请检查字段');
+            return $this->failed(ResponseEnum::CLIENT_PARAMETER_ERROR, $validator->errors()->all());
         }
         }
 
 
         $onlineCount = 0;
         $onlineCount = 0;
@@ -80,14 +53,14 @@ class CoreController extends Controller
         }
         }
 
 
         if (isset($formattedData) && ! $node->onlineIps()->createMany($formattedData)) {  // 生成节点在线IP数据
         if (isset($formattedData) && ! $node->onlineIps()->createMany($formattedData)) {  // 生成节点在线IP数据
-            return $this->returnData('生成节点在线用户IP信息失败', 400);
+            return $this->failed([400201, '生成节点在线用户IP信息失败']);
         }
         }
 
 
         if ($node->onlineLogs()->create(['online_user' => $onlineCount, 'log_time' => time()])) { // 生成节点在线人数数据
         if ($node->onlineLogs()->create(['online_user' => $onlineCount, 'log_time' => time()])) { // 生成节点在线人数数据
-            return $this->returnData('上报节点在线情况成功', 200, 'success');
+            return $this->succeed();
         }
         }
 
 
-        return $this->returnData('生成节点在线情况失败', 400);
+        return $this->failed([400201, '生成节点在线情况失败']);
     }
     }
 
 
     // 上报用户流量日志
     // 上报用户流量日志
@@ -96,7 +69,7 @@ class CoreController extends Controller
         $validator = Validator::make($request->all(), ['*.uid' => 'required|numeric|exists:user,id', '*.upload' => 'required|numeric', '*.download' => 'required|numeric']);
         $validator = Validator::make($request->all(), ['*.uid' => 'required|numeric|exists:user,id', '*.upload' => 'required|numeric', '*.download' => 'required|numeric']);
 
 
         if ($validator->fails()) {
         if ($validator->fails()) {
-            return $this->returnData('上报用户流量日志失败,请检查字段');
+            return $this->failed(ResponseEnum::CLIENT_PARAMETER_ERROR, $validator->errors()->all());
         }
         }
 
 
         foreach ($validator->validated() as $input) { // 处理用户流量数据
         foreach ($validator->validated() as $input) { // 处理用户流量数据
@@ -113,10 +86,10 @@ class CoreController extends Controller
                 $user->update(['u' => $user->u + $log->u, 'd' => $user->d + $log->d, 't' => time()]);
                 $user->update(['u' => $user->u + $log->u, 'd' => $user->d + $log->d, 't' => time()]);
             }
             }
 
 
-            return $this->returnData('上报用户流量日志成功', 200, 'success');
+            return $this->succeed();
         }
         }
 
 
-        return $this->returnData('生成用户流量日志失败', 400);
+        return $this->failed([400201, '生成用户流量日志失败']);
     }
     }
 
 
     // 获取节点的审计规则
     // 获取节点的审计规则
@@ -132,11 +105,11 @@ class CoreController extends Controller
                 ];
                 ];
             }
             }
 
 
-            return $this->returnData('获取节点审计规则成功', 200, 'success', ['mode' => $ruleGroup->type ? 'reject' : 'allow', 'rules' => $data ?? []]);
+            return $this->succeed(['mode' => $ruleGroup->type ? 'reject' : 'allow', 'rules' => $data ?? []]);
         }
         }
 
 
         // 放行
         // 放行
-        return $this->returnData('获取节点审计规则成功', 200, 'success', ['mode' => 'all', 'rules' => $data ?? []]);
+        return $this->succeed(['mode' => 'all', 'rules' => $data ?? []]);
     }
     }
 
 
     // 上报用户触发审计规则记录
     // 上报用户触发审计规则记录
@@ -145,13 +118,13 @@ class CoreController extends Controller
         $validator = Validator::make($request->all(), ['uid' => 'required|numeric|exists:user,id', 'rule_id' => 'required|numeric|exists:rule,id', 'reason' => 'required']);
         $validator = Validator::make($request->all(), ['uid' => 'required|numeric|exists:user,id', 'rule_id' => 'required|numeric|exists:rule,id', 'reason' => 'required']);
 
 
         if ($validator->fails()) {
         if ($validator->fails()) {
-            return $this->returnData('上报日志失败,请检查字段');
+            return $this->failed(ResponseEnum::CLIENT_PARAMETER_ERROR, $validator->errors()->all());
         }
         }
         $data = $validator->validated();
         $data = $validator->validated();
         if ($node->ruleLogs()->create(['user_id' => $data['uid'], 'rule_id' => $data['rule_id'], 'reason' => $data['reason']])) {
         if ($node->ruleLogs()->create(['user_id' => $data['uid'], 'rule_id' => $data['rule_id'], 'reason' => $data['reason']])) {
-            return $this->returnData('上报日志成功', 200, 'success');
+            return $this->succeed();
         }
         }
 
 
-        return $this->returnData('上报用户触发审计规则日志失败', 400);
+        return $this->failed([400201, '上报用户触发审计规则日志失败']);
     }
     }
 }
 }

+ 9 - 7
app/Http/Controllers/Api/WebApi/SSController.php

@@ -2,13 +2,16 @@
 
 
 namespace App\Http\Controllers\Api\WebApi;
 namespace App\Http\Controllers\Api\WebApi;
 
 
+use App\Helpers\WebApiResponse;
 use App\Models\Node;
 use App\Models\Node;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\JsonResponse;
+use Illuminate\Routing\Controller;
 
 
-class SSController extends CoreController
+class SSController extends Controller
 {
 {
-    // 获取节点信息
-    public function getNodeInfo(Node $node): JsonResponse
+    use WebApiResponse;
+
+    public function getNodeInfo(Node $node): JsonResponse // 获取节点信息
     {
     {
         $data = [
         $data = [
             'id'           => $node->id,
             'id'           => $node->id,
@@ -22,11 +25,10 @@ class SSController extends CoreController
             $data['port'] = $node->port;
             $data['port'] = $node->port;
         }
         }
 
 
-        return $this->returnData('获取节点信息成功', 200, 'success', $data);
+        return $this->succeed($data);
     }
     }
 
 
-    // 获取节点可用的用户列表
-    public function getUserList(Node $node): JsonResponse
+    public function getUserList(Node $node): JsonResponse // 获取节点可用的用户列表
     {
     {
         foreach ($node->users() as $user) {
         foreach ($node->users() as $user) {
             $data[] = [
             $data[] = [
@@ -38,6 +40,6 @@ class SSController extends CoreController
             ];
             ];
         }
         }
 
 
-        return $this->returnData('获取用户列表成功', 200, 'success', $data ?? [], ['updateTime' => time()]);
+        return $this->succeed($data ?? [], ['updateTime' => time()]);
     }
     }
 }
 }

+ 7 - 25
app/Http/Controllers/Api/WebApi/SSRController.php

@@ -2,36 +2,18 @@
 
 
 namespace App\Http\Controllers\Api\WebApi;
 namespace App\Http\Controllers\Api\WebApi;
 
 
+use App\Helpers\WebApiResponse;
 use App\Models\Node;
 use App\Models\Node;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\JsonResponse;
+use Illuminate\Routing\Controller;
 
 
-class SSRController extends CoreController
+class SSRController extends Controller
 {
 {
-    // 获取节点信息
-    public function getNodeInfo(Node $node): JsonResponse
-    {
-        return $this->returnData('获取节点信息成功', 200, 'success', $this->nodeData($node));
-    }
+    use WebApiResponse;
 
 
-    // 生成节点信息
-    public function nodeData(Node $node): array
+    public function getNodeInfo(Node $node): JsonResponse // 获取节点信息
     {
     {
-        return [
-            'id'           => $node->id,
-            'method'       => $node->profile['method'] ?? '',
-            'protocol'     => $node->profile['protocol'] ?? '',
-            'obfs'         => $node->profile['obfs'] ?? '',
-            'obfs_param'   => $node->profile['obfs_param'] ?? '',
-            'is_udp'       => $node->is_udp,
-            'speed_limit'  => $node->getRawOriginal('speed_limit'),
-            'client_limit' => $node->client_limit,
-            'single'       => isset($node->profile['passwd']) ? 1 : 0,
-            'port'         => (string) $node->port,
-            'passwd'       => $node->profile['passwd'] ?? '',
-            'push_port'    => $node->push_port,
-            'secret'       => $node->auth->secret,
-            'redirect_url' => sysConfig('redirect_url'),
-        ];
+        return $this->succeed($node->getSSRConfig());
     }
     }
 
 
     // 获取节点可用的用户列表
     // 获取节点可用的用户列表
@@ -51,6 +33,6 @@ class SSRController extends CoreController
             ];
             ];
         }
         }
 
 
-        return $this->returnData('获取用户列表成功', 200, 'success', $data ?? [], ['updateTime' => time()]);
+        return $this->succeed($data ?? [], ['updateTime' => time()]);
     }
     }
 }
 }

+ 18 - 16
app/Http/Controllers/Api/WebApi/TrojanController.php

@@ -2,38 +2,40 @@
 
 
 namespace App\Http\Controllers\Api\WebApi;
 namespace App\Http\Controllers\Api\WebApi;
 
 
+use App\Helpers\WebApiResponse;
 use App\Models\Node;
 use App\Models\Node;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\JsonResponse;
+use Illuminate\Routing\Controller;
 
 
-class TrojanController extends CoreController
+class TrojanController extends Controller
 {
 {
-    // 获取节点信息
-    public function getNodeInfo(Node $node): JsonResponse
+    use WebApiResponse;
+
+    public function getNodeInfo(Node $node): JsonResponse // 获取节点信息
     {
     {
-        return $this->returnData('获取节点信息成功', 200, 'success', [
-            'id' => $node->id,
-            'is_udp' => (bool) $node->is_udp,
-            'speed_limit' => $node->getRawOriginal('speed_limit'),
+        return $this->succeed([
+            'id'           => $node->id,
+            'is_udp'       => (bool) $node->is_udp,
+            'speed_limit'  => $node->getRawOriginal('speed_limit'),
             'client_limit' => $node->client_limit,
             'client_limit' => $node->client_limit,
-            'push_port' => $node->push_port,
+            'push_port'    => $node->push_port,
             'redirect_url' => sysConfig('redirect_url'),
             'redirect_url' => sysConfig('redirect_url'),
-            'trojan_port' => $node->port,
-            'secret' => $node->auth->secret,
-            'license' => sysConfig('trojan_license'),
+            'trojan_port'  => $node->port,
+            'secret'       => $node->auth->secret,
+            'license'      => sysConfig('trojan_license'),
         ]);
         ]);
     }
     }
 
 
-    // 获取节点可用的用户列表
-    public function getUserList(Node $node): JsonResponse
+    public function getUserList(Node $node): JsonResponse // 获取节点可用的用户列表
     {
     {
         foreach ($node->users() as $user) {
         foreach ($node->users() as $user) {
             $data[] = [
             $data[] = [
-                'uid' => $user->id,
-                'password' => $user->passwd,
+                'uid'         => $user->id,
+                'password'    => $user->passwd,
                 'speed_limit' => $user->getRawOriginal('speed_limit'),
                 'speed_limit' => $user->getRawOriginal('speed_limit'),
             ];
             ];
         }
         }
 
 
-        return $this->returnData('获取用户列表成功', 200, 'success', $data ?? [], ['updateTime' => time()]);
+        return $this->succeed($data ?? [], ['updateTime' => time()]);
     }
     }
 }
 }

+ 13 - 15
app/Http/Controllers/Api/WebApi/V2RayController.php

@@ -2,28 +2,28 @@
 
 
 namespace App\Http\Controllers\Api\WebApi;
 namespace App\Http\Controllers\Api\WebApi;
 
 
+use App\Helpers\WebApiResponse;
 use App\Models\Node;
 use App\Models\Node;
 use App\Models\NodeCertificate;
 use App\Models\NodeCertificate;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\JsonResponse;
+use Illuminate\Routing\Controller;
 
 
-class V2RayController extends CoreController
+class V2RayController extends Controller
 {
 {
-    // 获取节点信息
-    public function getNodeInfo(Node $node): JsonResponse
+    use WebApiResponse;
+
+    public function getNodeInfo(Node $node): JsonResponse // 获取节点信息
     {
     {
         $cert = NodeCertificate::whereDomain($node->profile['v2_host'])->first();
         $cert = NodeCertificate::whereDomain($node->profile['v2_host'])->first();
         $tlsProvider = ! empty($node->profile['tls_provider']) ? $node->profile['tls_provider'] : sysConfig('v2ray_tls_provider');
         $tlsProvider = ! empty($node->profile['tls_provider']) ? $node->profile['tls_provider'] : sysConfig('v2ray_tls_provider');
-        if (! $tlsProvider) {
-            $tlsProvider = null;
-        }
 
 
-        return $this->returnData('获取节点信息成功', 200, 'success', [
+        return $this->succeed([
             'id'              => $node->id,
             'id'              => $node->id,
             'is_udp'          => (bool) $node->is_udp,
             'is_udp'          => (bool) $node->is_udp,
             'speed_limit'     => $node->getRawOriginal('speed_limit'),
             'speed_limit'     => $node->getRawOriginal('speed_limit'),
             'client_limit'    => $node->client_limit,
             'client_limit'    => $node->client_limit,
             'push_port'       => $node->push_port,
             'push_port'       => $node->push_port,
-            'redirect_url'    => (string) sysConfig('redirect_url'),
+            'redirect_url'    => (string) sysConfig('redirect_url', ''),
             'secret'          => $node->auth->secret,
             'secret'          => $node->auth->secret,
             'key'             => $cert ? $cert->key : '',
             'key'             => $cert ? $cert->key : '',
             'pem'             => $cert ? $cert->pem : '',
             'pem'             => $cert ? $cert->pem : '',
@@ -40,8 +40,7 @@ class V2RayController extends CoreController
         ]);
         ]);
     }
     }
 
 
-    // 获取节点可用的用户列表
-    public function getUserList(Node $node): JsonResponse
+    public function getUserList(Node $node): JsonResponse // 获取节点可用的用户列表
     {
     {
         foreach ($node->users() as $user) {
         foreach ($node->users() as $user) {
             $data[] = [
             $data[] = [
@@ -51,19 +50,18 @@ class V2RayController extends CoreController
             ];
             ];
         }
         }
 
 
-        return $this->returnData('获取用户列表成功', 200, 'success', $data ?? [], ['updateTime' => time()]);
+        return $this->succeed($data ?? [], ['updateTime' => time()]);
     }
     }
 
 
-    // 上报节点伪装域名证书信息
-    public function addCertificate(Node $node): JsonResponse
+    public function addCertificate(Node $node): JsonResponse // 上报节点伪装域名证书信息
     {
     {
         if (request()->has(['key', 'pem'])) {
         if (request()->has(['key', 'pem'])) {
             $cert = NodeCertificate::whereDomain($node->v2_host)->firstOrCreate(['domain' => $node->server]);
             $cert = NodeCertificate::whereDomain($node->v2_host)->firstOrCreate(['domain' => $node->server]);
             if ($cert && $cert->update(['key' => request('key'), 'pem' => request('pem')])) {
             if ($cert && $cert->update(['key' => request('key'), 'pem' => request('pem')])) {
-                return $this->returnData('上报节点伪装域名证书成功', 200, 'success');
+                return $this->succeed();
             }
             }
         }
         }
 
 
-        return $this->returnData('上报节点伪装域名证书失败,请检查字段');
+        return $this->failed([400201, '上报节点伪装域名证书失败,请检查字段']);
     }
     }
 }
 }

+ 0 - 275
app/Http/Controllers/ClientController.php

@@ -1,275 +0,0 @@
-<?php
-
-namespace App\Http\Controllers;
-
-use App\Components\Client\Clash;
-use App\Components\Client\QuantumultX;
-use App\Components\Client\Surfboard;
-use App\Components\Client\Surge;
-use App\Components\Client\URLSchemes;
-use App\Components\Client\V2rayN;
-use App\Models\User;
-use File;
-use Symfony\Component\Yaml\Yaml;
-
-class ClientController extends Controller
-{
-    public function config(string $target, User $user, array $servers)
-    {
-        if (str_contains($target, 'quantumult x')) {
-            return $this->quantumultX($user, $servers);
-        }
-        if (str_contains($target, 'quantumult')) {
-            return $this->quantumult($user, $servers);
-        }
-        if (str_contains($target, 'clash')) {
-            return $this->clash($servers);
-        }
-        if (str_contains($target, 'surfboard')) {
-            return $this->surfboard($user, $servers);
-        }
-        if (str_contains($target, 'surge')) {
-            return $this->surge($target, $user, $servers);
-        }
-        if (str_contains($target, 'shadowrocket')) {
-            return $this->shadowrocket($user, $servers);
-        }
-        if (str_contains($target, 'v2rayn')) {
-            return $this->v2rayN($user, $servers);
-        }
-        if (str_contains($target, 'v2rayng')) {
-            return $this->v2rayN($user, $servers);
-        }
-        if (str_contains($target, 'v2rayu')) {
-            return $this->v2rayN($user, $servers);
-        }
-//            if (strpos($target, 'shadowsocks') !== false) {
-//                exit($this->shaodowsocksSIP008($servers));
-//            }
-        return $this->origin($servers);
-    }
-
-    private function quantumultX(User $user, array $servers = []): string
-    {
-        $uri = '';
-        if (sysConfig('is_custom_subscribe')) {
-            header("subscription-userinfo: upload={$user->u}; download={$user->d}; total={$user->transfer_enable}; expire={$user->expired_at}");
-        }
-        foreach ($servers as $server) {
-            if ($server['type'] === 'shadowsocks') {
-                $uri .= QuantumultX::buildShadowsocks($server);
-            }
-            if ($server['type'] === 'shadowsocksr') {
-                $uri .= QuantumultX::buildShadowsocksr($server);
-            }
-            if ($server['type'] === 'v2ray') {
-                $uri .= QuantumultX::buildVmess($server);
-            }
-            if ($server['type'] === 'trojan') {
-                $uri .= QuantumultX::buildTrojan($server);
-            }
-        }
-
-        return base64_encode($uri);
-    }
-
-    private function quantumult(User $user, array $servers = []): string
-    {
-        if (sysConfig('is_custom_subscribe')) {
-            header("subscription-userinfo: upload={$user->u}; download={$user->d}; total={$user->transfer_enable}; expire={$user->expired_at}");
-        }
-
-        return $this->origin($servers);
-    }
-
-    private function origin(array $servers = [], bool $encode = true): string
-    {
-        $uri = '';
-        foreach ($servers as $server) {
-            if ($server['type'] === 'shadowsocks') {
-                $uri .= URLSchemes::buildShadowsocks($server);
-            }
-            if ($server['type'] === 'shadowsocksr') {
-                $uri .= URLSchemes::buildShadowsocksr($server);
-            }
-            if ($server['type'] === 'v2ray') {
-                $uri .= URLSchemes::buildVmess($server);
-            }
-            if ($server['type'] === 'trojan') {
-                $uri .= URLSchemes::buildTrojan($server);
-            }
-        }
-
-        return $encode ? base64_encode($uri) : $uri;
-    }
-
-    private function clash($servers)
-    {
-        $defaultConfig = base_path().'/resources/rules/default.clash.yaml';
-        $customConfig = base_path().'/resources/rules/custom.clash.yaml';
-        if (File::exists($customConfig)) {
-            $config = Yaml::parseFile($customConfig);
-        } else {
-            $config = Yaml::parseFile($defaultConfig);
-        }
-
-        foreach ($servers as $server) {
-            if ($server['type'] === 'shadowsocks') {
-                $proxy[] = Clash::buildShadowsocks($server);
-                $proxies[] = $server['name'];
-            }
-            if ($server['type'] === 'shadowsocksr') {
-                $proxy[] = Clash::buildShadowsocksr($server);
-                $proxies[] = $server['name'];
-            }
-            if ($server['type'] === 'v2ray') {
-                $proxy[] = Clash::buildVmess($server);
-                $proxies[] = $server['name'];
-            }
-            if ($server['type'] === 'trojan') {
-                $proxy[] = Clash::buildTrojan($server);
-                $proxies[] = $server['name'];
-            }
-        }
-
-        $config['proxies'] = array_merge($config['proxies'] ?: [], $proxy ?? []);
-        foreach ($config['proxy-groups'] as $k => $v) {
-            if (! is_array($config['proxy-groups'][$k]['proxies'])) {
-                continue;
-            }
-            $config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies ?? []);
-        }
-
-        return str_replace('$app_name', sysConfig('website_name'), Yaml::dump($config));
-    }
-
-    private function surfboard(User $user, array $servers = [])
-    {
-        $proxies = '';
-        $proxyGroup = '';
-
-        foreach ($servers as $server) {
-            if ($server['type'] === 'shadowsocks') {
-                $proxies .= Surfboard::buildShadowsocks($server);
-                $proxyGroup .= $server['name'].', ';
-            }
-            if ($server['type'] === 'v2ray') {
-                $proxies .= Surfboard::buildVmess($server);
-                $proxyGroup .= $server['name'].', ';
-            }
-        }
-
-        $defaultConfig = base_path().'/resources/rules/default.surfboard.conf';
-        $customConfig = base_path().'/resources/rules/custom.surfboard.conf';
-        if (File::exists($customConfig)) {
-            $config = file_get_contents($customConfig);
-        } else {
-            $config = file_get_contents($defaultConfig);
-        }
-
-        // Subscription link
-        $subsURL = route('sub', $user->subscribe->code);
-
-        return str_replace(['$subs_link', '$proxies', '$proxy_group'], [$subsURL, $proxies, rtrim($proxyGroup, ', ')], $config);
-    }
-
-    private function surge(string $target, User $user, array $servers = [])
-    {
-        $proxies = '';
-        $proxyGroup = '';
-
-        foreach ($servers as $server) {
-            if ($server['type'] === 'shadowsocks') {
-                $proxies .= Surge::buildShadowsocks($server);
-                $proxyGroup .= $server['name'].', ';
-            }
-            if ($server['type'] === 'v2ray') {
-                $proxies .= Surge::buildVmess($server);
-                $proxyGroup .= $server['name'].', ';
-            }
-            if ($server['type'] === 'trojan') {
-                $proxies .= Surge::buildTrojan($server);
-                $proxyGroup .= $server['name'].', ';
-            }
-        }
-
-        if (str_contains($target, 'list')) {
-            return $proxies;
-        }
-
-        $defaultConfig = base_path().'/resources/rules/default.surge.conf';
-        $customConfig = base_path().'/resources/rules/custom.surge.conf';
-        if (File::exists($customConfig)) {
-            $config = file_get_contents($customConfig);
-        } else {
-            $config = file_get_contents($defaultConfig);
-        }
-
-        // Subscription link
-        $subsURL = route('sub', $user->subscribe->code);
-
-        return str_replace(['$subs_link', '$proxies', '$proxy_group'], [$subsURL, $proxies, rtrim($proxyGroup, ', ')], $config);
-    }
-
-    private function shadowrocket(User $user, array $servers = []): string
-    {
-        //display remaining traffic and expire date
-        $uri = '';
-        if (sysConfig('is_custom_subscribe')) {
-            $upload = flowAutoShow($user->u);
-            $download = flowAutoShow($user->d);
-            $totalTraffic = flowAutoShow($user->transfer_enable);
-            $uri = "STATUS=📤:{$upload}📥:{$download}⏳:{$totalTraffic}📅:{$user->expired_at}\r\n";
-        }
-        $uri .= $this->origin($servers, false);
-
-        return base64_encode($uri);
-    }
-
-    private function shaodowsocksSIP008(array $servers = []): string
-    {
-        foreach ($servers as $server) {
-            if ($server['type'] === 'shadowsocks') {
-                $configs[] = URLSchemes::buildShadowsocksSIP008($server);
-            }
-        }
-
-        return json_encode(['version' => 1, 'remark' => sysConfig('website_name'), 'servers' => $configs ?? []], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
-    }
-
-    private function v2rayN(User $user, $servers)
-    {
-        $uri = '';
-        if (sysConfig('is_custom_subscribe')) {
-            $text = '';
-            if (strtotime($user->expired_at) > time()) {
-                if ($user->transfer_enable == 0) {
-                    $text .= '剩余流量:0';
-                } else {
-                    $text .= '剩余流量:'.flowAutoShow($user->transfer_enable);
-                }
-                $text .= ', 过期时间:'.$user->expired_at;
-            } else {
-                $text .= '账户已过期,请续费后使用';
-            }
-            $uri .= 'vmess://'.base64url_encode(json_encode([
-                'v' => '2', 'ps' => $text, 'add' => sysConfig('website_url'), 'port' => 0, 'id' => $user->vmess_id, 'aid' => 0, 'net' => 'tcp',
-                'type' => 'none', 'host' => sysConfig('website_url'), 'path' => '/', 'tls' => 'tls',
-            ], JSON_PRETTY_PRINT)).PHP_EOL;
-        }
-
-        foreach ($servers as $server) {
-            if ($server['type'] === 'shadowsocksr') {
-                $uri .= V2rayN::buildShadowsocksr($server);
-            }
-            if ($server['type'] === 'v2ray') {
-                $uri .= V2rayN::buildVmess($server);
-            }
-            if ($server['type'] === 'trojan') {
-                $uri .= V2rayN::buildTrojan($server);
-            }
-        }
-
-        return base64_encode($uri);
-    }
-}

+ 0 - 98
app/Http/Controllers/Controller.php

@@ -2,14 +2,6 @@
 
 
 namespace App\Http\Controllers;
 namespace App\Http\Controllers;
 
 
-use App\Components\Client\Text;
-use App\Components\Client\URLSchemes;
-use App\Models\NodeDailyDataFlow;
-use App\Models\NodeHourlyDataFlow;
-use App\Models\UserDailyDataFlow;
-use App\Models\UserDataFlowLog;
-use App\Models\UserHourlyDataFlow;
-use DB;
 use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
 use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
 use Illuminate\Foundation\Bus\DispatchesJobs;
 use Illuminate\Foundation\Bus\DispatchesJobs;
 use Illuminate\Foundation\Validation\ValidatesRequests;
 use Illuminate\Foundation\Validation\ValidatesRequests;
@@ -20,94 +12,4 @@ class Controller extends BaseController
     use AuthorizesRequests;
     use AuthorizesRequests;
     use DispatchesJobs;
     use DispatchesJobs;
     use ValidatesRequests;
     use ValidatesRequests;
-
-    // 节点信息
-    public function getUserNodeInfo(array $server, bool $is_url): ?string
-    {
-        $type = $is_url ? new URLSchemes() : new Text();
-        switch ($server['type']) {
-            case'shadowsocks':
-                $data = $type->buildShadowsocks($server);
-                break;
-            case 'shadowsocksr':
-                $data = $type->buildShadowsocksr($server);
-                break;
-            case 'v2ray':
-                $data = $type->buildVmess($server);
-                break;
-            case 'trojan':
-                $data = $type->buildTrojan($server);
-                break;
-            default:
-        }
-
-        return $data ?? null;
-    }
-
-    // 流量使用图表
-    public function dataFlowChart($id, $is_node = false): array
-    {
-        if ($is_node) {
-            $currentFlow = UserDataFlowLog::whereNodeId($id);
-            $hourlyFlow = NodeHourlyDataFlow::whereNodeId($id)->whereDate('created_at', date('Y-m-d'))->selectRaw('(DATE_FORMAT(node_hourly_data_flow.created_at, "%k")) as date, total')->pluck('total', 'date');
-            $dailyFlow = NodeDailyDataFlow::whereNodeId($id)->whereMonth('created_at', date('n'))->selectRaw('(DATE_FORMAT(node_daily_data_flow.created_at, "%e")) as date, total')->pluck('total', 'date');
-        } else {
-            $currentFlow = UserDataFlowLog::whereUserId($id);
-            $hourlyFlow = UserHourlyDataFlow::userHourly($id)->whereDate('created_at', date('Y-m-d'))->selectRaw('(DATE_FORMAT(user_hourly_data_flow.created_at, "%k")) as date, total')->pluck('total', 'date');
-            $dailyFlow = UserDailyDataFlow::userDaily($id)->whereMonth('created_at', date('n'))->selectRaw('(DATE_FORMAT(user_daily_data_flow.created_at, "%e")) as date, total')->pluck('total', 'date');
-        }
-        $currentFlow = $currentFlow->where('log_time', '>=', strtotime(date('Y-m-d H:0')))->sum(DB::raw('u + d'));
-
-        // 节点一天内的流量
-        $hourlyData = array_fill(0, date('G') + 1, 0);
-        foreach ($hourlyFlow as $date => $dataFlow) {
-            $hourlyData[$date] = round($dataFlow / GB, 3);
-        }
-        $hourlyData[date('G') + 1] = round($currentFlow / GB, 3);
-
-        // 节点一个月内的流量
-        $dailyData = array_fill(0, date('j') - 1, 0);
-
-        foreach ($dailyFlow as $date => $dataFlow) {
-            $dailyData[$date - 1] = round($dataFlow / GB, 3);
-        }
-
-        $dailyData[date('j', strtotime(now())) - 1] = round(array_sum($hourlyData) + $currentFlow / GB, 3);
-
-        return [
-            'trafficDaily'  => $dailyData,
-            'trafficHourly' => $hourlyData,
-            'monthDays'     => range(1, date('j')), // 本月天数
-            'dayHours'      => range(0, date('G') + 1), // 本日小时
-        ];
-    }
-
-    /*
-        // 将Base64图片转换为本地图片并保存
-        public function base64ImageSaver($base64_image_content): ?string
-        {
-            // 匹配出图片的格式
-            if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $base64_image_content, $result)) {
-                $type = $result[2];
-
-                $directory = date('Ymd');
-                $path = '/assets/images/qrcode/'.$directory.'/';
-                // 检查是否有该文件夹,如果没有就创建,并给予最高权限
-                if (! file_exists(public_path($path))
-                    && ! mkdir($concurrentDirectory = public_path($path), 0755, true)
-                    && ! is_dir($concurrentDirectory)) {
-                    throw new RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
-                }
-
-                $fileName = Str::random(18).".{$type}";
-                if (file_put_contents(public_path($path.$fileName), base64_decode(str_replace($result[1], '', $base64_image_content)))) {
-                    chmod(public_path($path.$fileName), 0744);
-
-                    return $path.$fileName;
-                }
-            }
-
-            return '';
-        }
-     */
 }
 }

+ 2 - 8
app/Http/Controllers/User/AffiliateController.php

@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
 use App\Models\Order;
 use App\Models\Order;
 use App\Models\ReferralApply;
 use App\Models\ReferralApply;
 use App\Models\ReferralLog;
 use App\Models\ReferralLog;
+use App\Services\UserService;
 use Auth;
 use Auth;
 use Hashids\Hashids;
 use Hashids\Hashids;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\JsonResponse;
@@ -20,20 +21,13 @@ class AffiliateController extends Controller
             return Response::view('auth.error', ['message' => trans('user.purchase_required').'<a class="btn btn-sm btn-danger" href="/">'.trans('common.back').'</a>'], 402);
             return Response::view('auth.error', ['message' => trans('user.purchase_required').'<a class="btn btn-sm btn-danger" href="/">'.trans('common.back').'</a>'], 402);
         }
         }
 
 
-        $affSalt = sysConfig('aff_salt');
-        if (isset($affSalt)) {
-            $aff_link = route('register', ['aff' => (new Hashids($affSalt, 8))->encode(Auth::id())]);
-        } else {
-            $aff_link = route('register', ['aff' => Auth::id()]);
-        }
-
         return view('user.referral', [
         return view('user.referral', [
             'referral_traffic'  => flowAutoShow(sysConfig('referral_traffic') * MB),
             'referral_traffic'  => flowAutoShow(sysConfig('referral_traffic') * MB),
             'referral_percent'  => sysConfig('referral_percent'),
             'referral_percent'  => sysConfig('referral_percent'),
             'referral_money'    => sysConfig('referral_money'),
             'referral_money'    => sysConfig('referral_money'),
             'totalAmount'       => ReferralLog::uid()->sum('commission') / 100,
             'totalAmount'       => ReferralLog::uid()->sum('commission') / 100,
             'canAmount'         => ReferralLog::uid()->whereStatus(0)->sum('commission') / 100,
             'canAmount'         => ReferralLog::uid()->whereStatus(0)->sum('commission') / 100,
-            'aff_link'          => $aff_link,
+            'aff_link'          => UserService::getInstance()->inviteURI(),
             'referralLogList'   => ReferralLog::uid()->with('invitee:id,username')->latest()->paginate(10, ['*'], 'log_page'),
             'referralLogList'   => ReferralLog::uid()->with('invitee:id,username')->latest()->paginate(10, ['*'], 'log_page'),
             'referralApplyList' => ReferralApply::uid()->latest()->paginate(10, ['*'], 'apply_page'),
             'referralApplyList' => ReferralApply::uid()->latest()->paginate(10, ['*'], 'apply_page'),
             'referralUserList'  => Auth::getUser()->invitees()->select(['username', 'created_at'])->latest()->paginate(10, ['*'], 'user_page'),
             'referralUserList'  => Auth::getUser()->invitees()->select(['username', 'created_at'])->latest()->paginate(10, ['*'], 'user_page'),

+ 16 - 67
app/Http/Controllers/User/SubscribeController.php

@@ -3,27 +3,30 @@
 namespace App\Http\Controllers\User;
 namespace App\Http\Controllers\User;
 
 
 use App\Components\IP;
 use App\Components\IP;
-use App\Http\Controllers\ClientController;
 use App\Http\Controllers\Controller;
 use App\Http\Controllers\Controller;
 use App\Models\UserSubscribe;
 use App\Models\UserSubscribe;
 use App\Models\UserSubscribeLog;
 use App\Models\UserSubscribeLog;
-use Arr;
+use App\Services\ProxyServer;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
 use Redirect;
 use Redirect;
 use Response;
 use Response;
 
 
 class SubscribeController extends Controller
 class SubscribeController extends Controller
 {
 {
-    private $subType;
+    private static $subType;
+    private $proxyServer;
 
 
     // 通过订阅码获取订阅信息
     // 通过订阅码获取订阅信息
     public function getSubscribeByCode(Request $request, string $code)
     public function getSubscribeByCode(Request $request, string $code)
     {
     {
-        if (empty($code)) {
+        preg_match('/[0-9A-Za-z]+/', $code, $matches, PREG_UNMATCHED_AS_NULL);
+
+        if (empty($matches) || empty($code)) {
             return Redirect::route('login');
             return Redirect::route('login');
         }
         }
-        $this->subType = $request->input('type');
-        $target = strtolower($request->input('target') ?? ($request->userAgent() ?? ''));
+        $code = $matches[0];
+        $this->proxyServer = ProxyServer::getInstance();
+        self::$subType = is_numeric($request->input('type')) ? $request->input('type') : null;
 
 
         // 检查订阅码是否有效
         // 检查订阅码是否有效
         $subscribe = UserSubscribe::whereCode($code)->first();
         $subscribe = UserSubscribe::whereCode($code)->first();
@@ -61,70 +64,16 @@ class SubscribeController extends Controller
 
 
             return $this->failed(trans('errors.subscribe.question'));
             return $this->failed(trans('errors.subscribe.question'));
         }
         }
+        $this->proxyServer->setUser($user);
+        $subscribe->increment('times'); // 更新访问次数
+        $this->subscribeLog($subscribe->id, IP::getClientIp(), json_encode(['Host' => $request->getHost(), 'User-Agent' => $request->userAgent()])); // 记录每次请求
 
 
-        // 更新访问次数
-        $subscribe->increment('times');
-
-        // 记录每次请求
-        $this->subscribeLog($subscribe->id, IP::getClientIp(), json_encode(['Host' => $request->getHost(), 'User-Agent' => $request->userAgent()]));
-
-        // 获取这个账号可用节点
-        $query = $user->nodes()->whereIn('is_display', [2, 3]);
-
-        if ($this->subType === 1) {
-            $query = $query->whereIn('type', [1, 4]);
-        } elseif ($this->subType) {
-            $query = $query->whereType($this->subType);
-        }
-
-        $nodeList = $query->orderByDesc('sort')->orderBy('id')->get();
-        if (empty($nodeList)) {
-            return $this->failed(trans('errors.subscribe.none'));
-        }
-
-        $servers = [];
-        foreach ($nodeList as $node) {
-            $servers[] = $node->getConfig($user);
-        }
-
-        // 打乱数组
-        if (sysConfig('rand_subscribe')) {
-            $servers = Arr::shuffle($servers);
-        }
-
-        if (sysConfig('subscribe_max')) {
-            $servers = array_slice($servers, 0, (int) sysConfig('subscribe_max'));
-        }
-
-        return (new ClientController)->config($target, $user, $servers);
-    }
-
-    // 抛出错误的节点信息,用于兼容防止客户端订阅失败
-    private function failed($text)
-    {
-        return Response::make(base64url_encode($this->infoGenerator($text)));
+        return ProxyServer::getInstance()->getProxyText(strtolower($request->input('target') ?? ($request->userAgent() ?? '')), self::$subType);
     }
     }
 
 
-    private function infoGenerator($text): string
-    {
-        switch ($this->subType) {
-            case 2:
-                $result = 'vmess://'.base64url_encode(json_encode([
-                    'v' => '2', 'ps' => $text, 'add' => '0.0.0.0', 'port' => 0, 'id' => 0, 'aid' => 0, 'net' => 'tcp',
-                    'type' => 'none', 'host' => '', 'path' => '/', 'tls' => 'tls',
-                ], JSON_PRETTY_PRINT));
-                break;
-            case 3:
-                $result = 'trojan://[email protected]:0?peer=0.0.0.0#'.rawurlencode($text);
-                break;
-            case 1:
-            case 4:
-            default:
-                $result = 'ssr://'.base64url_encode('0.0.0.0:0:origin:none:plain:'.base64url_encode('0000').'/?obfsparam=&protoparam=&remarks='.base64url_encode($text).'&group='.base64url_encode(sysConfig('website_name')).'&udpport=0&uot=0');
-                break;
-        }
-
-        return $result.PHP_EOL;
+    private function failed(string $text)
+    { // 抛出错误的节点信息,用于兼容防止客户端订阅失败
+        return Response::make(base64url_encode($this->proxyServer->failedProxyReturn($text, self::$subType ?? 1)));
     }
     }
 
 
     private function subscribeLog($subscribeId, $ip, $headers): void
     private function subscribeLog($subscribeId, $ip, $headers): void

+ 9 - 6
app/Http/Controllers/UserController.php

@@ -3,6 +3,7 @@
 namespace App\Http\Controllers;
 namespace App\Http\Controllers;
 
 
 use App\Components\Helpers;
 use App\Components\Helpers;
+use App\Helpers\DataChart;
 use App\Models\Article;
 use App\Models\Article;
 use App\Models\Coupon;
 use App\Models\Coupon;
 use App\Models\Goods;
 use App\Models\Goods;
@@ -15,6 +16,7 @@ use App\Models\User;
 use App\Notifications\TicketCreated;
 use App\Notifications\TicketCreated;
 use App\Notifications\TicketReplied;
 use App\Notifications\TicketReplied;
 use App\Services\CouponService;
 use App\Services\CouponService;
+use App\Services\ProxyServer;
 use Cache;
 use Cache;
 use DB;
 use DB;
 use Exception;
 use Exception;
@@ -32,6 +34,8 @@ use Validator;
 
 
 class UserController extends Controller
 class UserController extends Controller
 {
 {
+    use DataChart;
+
     public function index()
     public function index()
     {
     {
         // 用户转换
         // 用户转换
@@ -108,9 +112,10 @@ class UserController extends Controller
     {
     {
         $user = auth()->user();
         $user = auth()->user();
         if ($request->isMethod('POST')) {
         if ($request->isMethod('POST')) {
-            $server = Node::findOrFail($request->input('id'))->getConfig($user); // 提取节点信息
+            $proxyServer = ProxyServer::getInstance();
+            $server = $proxyServer->getProxyConfig(Node::findOrFail($request->input('id')));
 
 
-            return Response::json(['status' => 'success', 'data' => $this->getUserNodeInfo($server, $request->input('type') !== 'text'), 'title' => $server['type']]);
+            return Response::json(['status' => 'success', 'data' => $proxyServer->getUserProxyConfig($server, $request->input('type') !== 'text'), 'title' => $server['type']]);
         }
         }
 
 
         // 获取当前用户可用节点
         // 获取当前用户可用节点
@@ -477,9 +482,8 @@ class UserController extends Controller
         ]);
         ]);
     }
     }
 
 
-    // 更换订阅地址
     public function exchangeSubscribe(): ?JsonResponse
     public function exchangeSubscribe(): ?JsonResponse
-    {
+    { // 更换订阅地址
         try {
         try {
             DB::beginTransaction();
             DB::beginTransaction();
 
 
@@ -501,9 +505,8 @@ class UserController extends Controller
         }
         }
     }
     }
 
 
-    // 转换成管理员的身份
     public function switchToAdmin(): JsonResponse
     public function switchToAdmin(): JsonResponse
-    {
+    { // 转换成管理员的身份
         if (! Session::has('admin')) {
         if (! Session::has('admin')) {
             return Response::json(['status' => 'fail', 'message' => trans('errors.unauthorized')]);
             return Response::json(['status' => 'fail', 'message' => trans('errors.unauthorized')]);
         }
         }

+ 25 - 15
app/Http/Kernel.php

@@ -2,9 +2,11 @@
 
 
 namespace App\Http;
 namespace App\Http;
 
 
+use App\Http\Middleware\AcceptHeader;
 use App\Http\Middleware\Affiliate;
 use App\Http\Middleware\Affiliate;
 use App\Http\Middleware\Authenticate;
 use App\Http\Middleware\Authenticate;
 use App\Http\Middleware\CheckForMaintenanceMode;
 use App\Http\Middleware\CheckForMaintenanceMode;
+use App\Http\Middleware\ClientAuthenticate;
 use App\Http\Middleware\EncryptCookies;
 use App\Http\Middleware\EncryptCookies;
 use App\Http\Middleware\isForbidden;
 use App\Http\Middleware\isForbidden;
 use App\Http\Middleware\isLogin;
 use App\Http\Middleware\isLogin;
@@ -14,6 +16,7 @@ use App\Http\Middleware\Permission;
 use App\Http\Middleware\RedirectIfAuthenticated;
 use App\Http\Middleware\RedirectIfAuthenticated;
 use App\Http\Middleware\SetLocale;
 use App\Http\Middleware\SetLocale;
 use App\Http\Middleware\Telegram;
 use App\Http\Middleware\Telegram;
+use App\Http\Middleware\tmpCORS;
 use App\Http\Middleware\TrimStrings;
 use App\Http\Middleware\TrimStrings;
 use App\Http\Middleware\TrustProxies;
 use App\Http\Middleware\TrustProxies;
 use App\Http\Middleware\VerifyCsrfToken;
 use App\Http\Middleware\VerifyCsrfToken;
@@ -83,6 +86,12 @@ class Kernel extends HttpKernel
         ],
         ],
 
 
         'api' => [
         'api' => [
+            AcceptHeader::class,
+            EncryptCookies::class,
+            AddQueuedCookiesToResponse::class,
+            StartSession::class,
+            tmpCORS::class,
+            SetLocale::class,
             'throttle:60,1',
             'throttle:60,1',
             SubstituteBindings::class,
             SubstituteBindings::class,
         ],
         ],
@@ -96,21 +105,22 @@ class Kernel extends HttpKernel
      * @var array
      * @var array
      */
      */
     protected $routeMiddleware = [
     protected $routeMiddleware = [
-        'auth' => Authenticate::class,
-        'auth.basic' => AuthenticateWithBasicAuth::class,
-        'bindings' => SubstituteBindings::class,
-        'cache.headers' => SetCacheHeaders::class,
-        'can' => Authorize::class,
-        'guest' => RedirectIfAuthenticated::class,
+        'auth'             => Authenticate::class,
+        'auth.basic'       => AuthenticateWithBasicAuth::class,
+        'auth.client'      => ClientAuthenticate::class,
+        'bindings'         => SubstituteBindings::class,
+        'cache.headers'    => SetCacheHeaders::class,
+        'can'              => Authorize::class,
+        'guest'            => RedirectIfAuthenticated::class,
         'password.confirm' => RequirePassword::class,
         'password.confirm' => RequirePassword::class,
-        'signed' => ValidateSignature::class,
-        'telegram'=>Telegram::class,
-        'throttle' => ThrottleRequests::class,
-        'verified' => EnsureEmailIsVerified::class,
-        'webApi' => WebApi::class,
-        'isMaintenance' => isMaintenance::class,
-        'isSecurity' => isSecurity::class,
-        'isForbidden' => isForbidden::class,
-        'affiliate' => Affiliate::class,
+        'signed'           => ValidateSignature::class,
+        'telegram'         => Telegram::class,
+        'throttle'         => ThrottleRequests::class,
+        'verified'         => EnsureEmailIsVerified::class,
+        'webApi'           => WebApi::class,
+        'isMaintenance'    => isMaintenance::class,
+        'isSecurity'       => isSecurity::class,
+        'isForbidden'      => isForbidden::class,
+        'affiliate'        => Affiliate::class,
     ];
     ];
 }
 }

+ 15 - 0
app/Http/Middleware/AcceptHeader.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Closure;
+
+class AcceptHeader
+{
+    public function handle($request, Closure $next)
+    {
+        $request->headers->set('Accept', 'application/json');
+
+        return $next($request);
+    }
+}

+ 30 - 0
app/Http/Middleware/ClientAuthenticate.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use App\Helpers\ClientApiResponse;
+use App\Helpers\ResponseEnum;
+use Closure;
+use Illuminate\Http\Request;
+
+class ClientAuthenticate
+{
+    use ClientApiResponse;
+
+    /**
+     * Handle an incoming request.
+     *
+     * @param  Request  $request
+     * @param  Closure  $next
+     * @return mixed
+     */
+    public function handle($request, Closure $next)
+    {
+        $session = $request->session();
+        if (isset($session) && ! $session->get('uid')) {
+            return $this->jsonResponse(-1, ResponseEnum::USER_SERVICE_LOGIN_ERROR);
+        }
+
+        return $next($request);
+    }
+}

+ 5 - 3
app/Http/Middleware/SetLocale.php

@@ -4,6 +4,7 @@ namespace App\Http\Middleware;
 
 
 use Closure;
 use Closure;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
+use Illuminate\Support\Facades\App;
 use Session;
 use Session;
 
 
 class SetLocale
 class SetLocale
@@ -19,11 +20,12 @@ class SetLocale
     {
     {
         if (Session::has('locale')) {
         if (Session::has('locale')) {
             app()->setLocale(Session::get('locale'));
             app()->setLocale(Session::get('locale'));
-        }
-
-        if ($request->query('locale')) {
+        } elseif ($request->query('locale')) {
             Session::put('locale', $request->query('locale'));
             Session::put('locale', $request->query('locale'));
             app()->setLocale($request->query('locale'));
             app()->setLocale($request->query('locale'));
+        } elseif ($request->header('content-language')) {
+            Session::put('locale', $request->header('content-language'));
+            App::setLocale($request->header('content-language'));
         }
         }
 
 
         return $next($request);
         return $next($request);

+ 10 - 14
app/Http/Middleware/WebApi.php

@@ -2,9 +2,9 @@
 
 
 namespace App\Http\Middleware;
 namespace App\Http\Middleware;
 
 
+use App\Helpers\ResponseEnum;
+use App\Helpers\WebApiResponse;
 use Closure;
 use Closure;
-use Illuminate\Http\JsonResponse;
-use Response;
 
 
 class WebApi
 class WebApi
 {
 {
@@ -15,31 +15,27 @@ class WebApi
      * @param  Closure  $next
      * @param  Closure  $next
      * @return mixed
      * @return mixed
      */
      */
+    use WebApiResponse;
+
     public function handle($request, Closure $next)
     public function handle($request, Closure $next)
     {
     {
         $node = $request->node;
         $node = $request->node;
         $key = $request->header('key');
         $key = $request->header('key');
         $time = $request->header('timestamp');
         $time = $request->header('timestamp');
 
 
-        if (! isset($key)) {// 未提供 key
-            return $this->returnData('Your key is null!', 400);
+        if (! isset($key)) { // 未提供 key
+            return $this->failed([400200, 'Your key is null!']);
         }
         }
 
 
         $nodeAuth = $node->auth ?? null;
         $nodeAuth = $node->auth ?? null;
-        if (! isset($nodeAuth) || $key !== $nodeAuth->key) {// key不存在/不匹配
-            return $this->returnData('Token is invalid!');
+        if (! isset($nodeAuth) || $key !== $nodeAuth->key) { // key不存在/不匹配
+            return $this->failed(ResponseEnum::CLIENT_PARAMETER_ERROR);
         }
         }
 
 
-        if (abs($time - time()) >= 300) {// 时差超过5分钟
-            return $this->returnData('Please resynchronize the server time!');
+        if (abs($time - time()) >= 300) { // 时差超过5分钟
+            return $this->failed(ResponseEnum::CLIENT_HTTP_UNSYNCHRONIZE_TIMER);
         }
         }
 
 
         return $next($request);
         return $next($request);
     }
     }
-
-    // 返回数据
-    private function returnData(string $message, int $code = 401): JsonResponse
-    {
-        return Response::json(['status' => 'fail', 'code' => $code, 'message' => $message]);
-    }
 }
 }

+ 28 - 0
app/Http/Middleware/tmpCORS.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Closure;
+
+class tmpCORS
+{
+    public function handle($request, Closure $next)
+    {
+        config(['session.same_site' => null]);
+        $origin = $request->header('origin');
+        if (empty($origin)) {
+            $referer = $request->header('referer');
+            if (! empty($referer) && preg_match("/^((https|http):\/\/)?([^\/]+)/i", $referer, $matches)) {
+                $origin = $matches[0];
+            }
+        }
+        $response = $next($request);
+        $response->header('Access-Control-Allow-Origin', trim($origin, '/'));
+        $response->header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
+        $response->header('Access-Control-Allow-Headers', 'Content-Type,X-Requested-With');
+        $response->header('Access-Control-Allow-Credentials', 'true');
+        $response->header('Access-Control-Max-Age', 10080);
+
+        return $response;
+    }
+}

+ 1 - 2
app/Jobs/VNet/reloadNode.php

@@ -2,7 +2,6 @@
 
 
 namespace App\Jobs\VNet;
 namespace App\Jobs\VNet;
 
 
-use App\Http\Controllers\Api\WebApi\SSRController;
 use Arr;
 use Arr;
 use Exception;
 use Exception;
 use Http;
 use Http;
@@ -35,7 +34,7 @@ class reloadNode implements ShouldQueue
     public function handle(): bool
     public function handle(): bool
     {
     {
         foreach ($this->nodes as $node) {
         foreach ($this->nodes as $node) {
-            $data = (new SSRController())->nodeData($node);
+            $data = $node->getSSRConfig();
 
 
             if ($node->is_ddns) {
             if ($node->is_ddns) {
                 if (! $this->send($node->server.':'.$node->push_port, $node->auth->secret, $data)) {
                 if (! $this->send($node->server.':'.$node->push_port, $node->auth->secret, $data)) {

+ 27 - 76
app/Models/Node.php

@@ -3,7 +3,6 @@
 namespace App\Models;
 namespace App\Models;
 
 
 use App\Components\IP;
 use App\Components\IP;
-use Arr;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@@ -18,9 +17,7 @@ class Node extends Model
 {
 {
     protected $table = 'node';
     protected $table = 'node';
     protected $guarded = [];
     protected $guarded = [];
-    protected $casts = [
-        'profile' => 'array',
-    ];
+    protected $casts = ['profile' => 'array'];
 
 
     public function labels()
     public function labels()
     {
     {
@@ -62,6 +59,11 @@ class Node extends Model
         return $this->hasMany(NodeHourlyDataFlow::class);
         return $this->hasMany(NodeHourlyDataFlow::class);
     }
     }
 
 
+    public function country(): HasOne
+    {
+        return $this->HasOne(Country::class, 'code', 'country_code');
+    }
+
     public function ruleGroup(): BelongsTo
     public function ruleGroup(): BelongsTo
     {
     {
         return $this->belongsTo(RuleGroup::class);
         return $this->belongsTo(RuleGroup::class);
@@ -123,7 +125,7 @@ class Node extends Model
     public function ips(int $type = 4): array
     public function ips(int $type = 4): array
     {
     {
         // 使用DDNS的node先通过gethostbyname获取ip地址
         // 使用DDNS的node先通过gethostbyname获取ip地址
-        if ($this->attributes['is_ddns']) { // When ddns is enable, only domain can be used to check the ip
+        if ($this->attributes['is_ddns'] ?? 0) { // When ddns is enabled, only domain can be used to check the ip
             $ip = gethostbyname($this->attributes['server']);
             $ip = gethostbyname($this->attributes['server']);
             if (strcmp($ip, $this->attributes['server']) === 0) {
             if (strcmp($ip, $this->attributes['server']) === 0) {
                 Log::warning('获取 【'.$this->attributes['server'].'】 IP失败'.$ip);
                 Log::warning('获取 【'.$this->attributes['server'].'】 IP失败'.$ip);
@@ -136,77 +138,6 @@ class Node extends Model
         return array_map('trim', explode(',', $ip));
         return array_map('trim', explode(',', $ip));
     }
     }
 
 
-    public function getConfig(User $user)
-    {
-        $config = [
-            'id'    => $this->id,
-            'name'  => $this->name,
-            'host'  => $this->host,
-            'group' => sysConfig('website_name'),
-            'udp'   => $this->is_udp,
-        ];
-
-        if ($this->relay_node_id) {
-            $parentConfig = $this->relayNode->getConfig($user);
-            $config = array_merge($config, Arr::except($parentConfig, ['id', 'name', 'host', 'group', 'udp']));
-            if ($parentConfig['type'] === 'trojan') {
-                $config['sni'] = $parentConfig['host'];
-            }
-            $config['port'] = $this->port;
-        } else {
-            switch ($this->type) {
-                case 0:
-                    $config = array_merge($config, [
-                        'type'   => 'shadowsocks',
-                        'passwd' => $user->passwd,
-                    ], $this->profile);
-                    if ($this->port) {
-                        $config['port'] = $this->port;
-                    } else {
-                        $config['port'] = $user->port;
-                    }
-                    break;
-                case 2:
-                    $config = array_merge($config, [
-                        'type' => 'v2ray',
-                        'port' => $this->port,
-                        'uuid' => $user->vmess_id,
-                    ], $this->profile);
-                    break;
-                case 3:
-                    $config = array_merge($config, [
-                        'type'   => 'trojan',
-                        'port'   => $this->port,
-                        'passwd' => $user->passwd,
-                        'sni'    => '',
-                    ], $this->profile);
-                    break;
-                case 1:
-                case 4:
-                    $config = array_merge($config, [
-                        'type' => 'shadowsocksr',
-                    ], $this->profile);
-                    if ($this->profile['passwd'] && $this->port) {
-                        //单端口使用中转的端口
-                        $config['port'] = $this->port;
-                        $config['protocol_param'] = $user->port.':'.$user->passwd;
-                    } else {
-                        $config['port'] = $user->port;
-                        $config['passwd'] = $user->passwd;
-                        if ($this->type === 1) {
-                            $config['method'] = $user->method;
-                            $config['protocol'] = $user->protocol;
-                            $config['obfs'] = $user->obfs;
-                        }
-                    }
-
-                    break;
-            }
-        }
-
-        return $config;
-    }
-
     public function getSpeedLimitAttribute($value)
     public function getSpeedLimitAttribute($value)
     {
     {
         return $value / Mbps;
         return $value / Mbps;
@@ -232,4 +163,24 @@ class Node extends Model
     {
     {
         return $this->server ?? $this->ip ?? $this->ipv6;
         return $this->server ?? $this->ip ?? $this->ipv6;
     }
     }
+
+    public function getSSRConfig(): array
+    {
+        return [
+            'id'           => $this->id,
+            'method'       => $this->profile['method'] ?? '',
+            'protocol'     => $this->profile['protocol'] ?? '',
+            'obfs'         => $this->profile['obfs'] ?? '',
+            'obfs_param'   => $this->profile['obfs_param'] ?? '',
+            'is_udp'       => $this->is_udp,
+            'speed_limit'  => $this->getRawOriginal('speed_limit'),
+            'client_limit' => $this->client_limit,
+            'single'       => isset($this->profile['passwd']) ? 1 : 0,
+            'port'         => (string) $this->port,
+            'passwd'       => $this->profile['passwd'] ?? '',
+            'push_port'    => $this->push_port,
+            'secret'       => $this->auth->secret,
+            'redirect_url' => sysConfig('redirect_url', ''),
+        ];
+    }
 }
 }

+ 2 - 28
app/Models/User.php

@@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
 use Illuminate\Foundation\Auth\User as Authenticatable;
 use Illuminate\Foundation\Auth\User as Authenticatable;
 use Illuminate\Notifications\Notifiable;
 use Illuminate\Notifications\Notifiable;
 use Kyslik\ColumnSortable\Sortable;
 use Kyslik\ColumnSortable\Sortable;
+use Laravel\Sanctum\HasApiTokens;
 use Spatie\Permission\Traits\HasRoles;
 use Spatie\Permission\Traits\HasRoles;
 
 
 /**
 /**
@@ -17,7 +18,7 @@ use Spatie\Permission\Traits\HasRoles;
  */
  */
 class User extends Authenticatable
 class User extends Authenticatable
 {
 {
-    use Notifiable, HasRoles, Sortable;
+    use HasApiTokens, Notifiable, HasRoles, Sortable;
 
 
     public $sortable = ['id', 'credit', 'port', 't', 'expired_at'];
     public $sortable = ['id', 'credit', 'port', 't', 'expired_at'];
     protected $table = 'user';
     protected $table = 'user';
@@ -61,33 +62,6 @@ class User extends Authenticatable
         return $this->hasMany(UserOauth::class);
         return $this->hasMany(UserOauth::class);
     }
     }
 
 
-    public function profile()
-    {
-        return [
-            'id'              => $this->id,
-            'nickname'        => $this->nickname,
-            'account'         => $this->username,
-            'port'            => $this->port,
-            'passwd'          => $this->passwd,
-            'uuid'            => $this->vmess_id,
-            'transfer_enable' => $this->transfer_enable,
-            'u'               => $this->u,
-            'd'               => $this->d,
-            't'               => $this->t,
-            'enable'          => $this->enable,
-            'speed_limit'     => $this->speed_limit,
-            'credit'          => $this->credit,
-            'expired_at'      => $this->expired_at,
-            'ban_time'        => $this->ban_time,
-            'level'           => $this->level_name,
-            'group'           => $this->userGroup->name ?? null,
-            'last_login'      => $this->last_login,
-            'reset_time'      => $this->reset_time,
-            'invite_num'      => $this->invite_num,
-            'status'          => $this->status,
-        ];
-    }
-
     public function onlineIpLogs(): HasMany
     public function onlineIpLogs(): HasMany
     {
     {
         return $this->hasMany(NodeOnlineIp::class);
         return $this->hasMany(NodeOnlineIp::class);

+ 26 - 0
app/Services/BaseService.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace App\Services;
+
+class BaseService
+{
+    protected static $instance;
+
+    protected function __construct()
+    {
+    }
+
+    public static function getInstance()
+    {
+        if (static::$instance instanceof static) {
+            return self::$instance;
+        }
+        static::$instance = new static();
+
+        return self::$instance;
+    }
+
+    protected function __clone()
+    {
+    }
+}

+ 214 - 0
app/Services/ProxyServer.php

@@ -0,0 +1,214 @@
+<?php
+
+namespace App\Services;
+
+use App\Components\Client\Text;
+use App\Components\Client\URLSchemes;
+use App\Helpers\ClientConfig;
+use App\Models\Node;
+use App\Models\User;
+use Arr;
+
+class ProxyServer extends BaseService
+{
+    use ClientConfig;
+
+    private static $user;
+
+    public function __construct()
+    {
+        parent::__construct();
+        self::$user = auth()->user();
+    }
+
+    public function getUser()
+    {
+        return self::$user;
+    }
+
+    public function setUser(User $user)
+    {
+        self::$user = $user;
+    }
+
+    public function getProxyText($target, $type = null)
+    {
+        $servers = $this->getNodeList($type);
+        if (empty($servers)) {
+            return $this->failedProxyReturn(trans('errors.subscribe.none'), $type);
+        }
+
+        if (sysConfig('rand_subscribe')) {// 打乱数组
+            $servers = Arr::shuffle($servers);
+        }
+
+        $max = (int) sysConfig('subscribe_max');
+        if ($max) { // 订阅数量限制
+            $servers = Arr::random($servers, $max);
+        }
+
+        return $this->clientConfig($servers, $target);
+    }
+
+    public function getNodeList($type = null, $isConfig = true)
+    {
+        $query = self::$user->nodes()->whereIn('is_display', [2, 3]); // 获取这个账号可用节点
+
+        if (isset($type)) {
+            if ($type === 1) {
+                $query = $query->whereIn('type', [1, 4]);
+            } elseif ($type) {
+                $query = $query->whereType($type);
+            }
+        }
+
+        $nodes = $query->orderByDesc('sort')->orderBy('id')->get();
+
+        if ($isConfig) {
+            $servers = [];
+            foreach ($nodes as $node) {
+                $servers[] = $this->getProxyConfig($node);
+            }
+
+            return $servers;
+        }
+
+        return $nodes;
+    }
+
+    public function getProxyConfig(Node $node) // 提取节点信息
+    {
+        $user = self::$user;
+        $config = [
+            'id'    => $node->id,
+            'name'  => $node->name,
+            'area'  => $node->country->name,
+            'host'  => $node->host,
+            'group' => sysConfig('website_name'),
+            'udp'   => $node->is_udp,
+        ];
+
+        if ($node->relay_node_id) {
+            $parentConfig = $this->getProxyConfig($node->relayNode);
+            $config = array_merge($config, Arr::except($parentConfig, ['id', 'name', 'host', 'group', 'udp']));
+            if ($parentConfig['type'] === 'trojan') {
+                $config['sni'] = $parentConfig['host'];
+            }
+            $config['port'] = $node->port;
+        } else {
+            switch ($node->type) {
+                case 0:
+                    $config = array_merge($config, [
+                        'type'   => 'shadowsocks',
+                        'passwd' => $user->passwd,
+                    ], $node->profile);
+                    if ($node->port) {
+                        $config['port'] = $node->port;
+                    } else {
+                        $config['port'] = $user->port;
+                    }
+                    break;
+                case 2:
+                    $config = array_merge($config, [
+                        'type' => 'v2ray',
+                        'port' => $node->port,
+                        'uuid' => $user->vmess_id,
+                    ], $node->profile);
+                    break;
+                case 3:
+                    $config = array_merge($config, [
+                        'type'   => 'trojan',
+                        'port'   => $node->port,
+                        'passwd' => $user->passwd,
+                        'sni'    => '',
+                    ], $node->profile);
+                    break;
+                case 1:
+                case 4:
+                default:
+                    $config = array_merge($config, ['type' => 'shadowsocksr'], $node->profile);
+                    if ($node->profile['passwd'] && $node->port) {
+                        //单端口使用中转的端口
+                        $config['port'] = $node->port;
+                        $config['protocol_param'] = $user->port.':'.$user->passwd;
+                    } else {
+                        $config['port'] = $user->port;
+                        $config['passwd'] = $user->passwd;
+                        if ($node->type === 1) {
+                            $config['method'] = $user->method;
+                            $config['protocol'] = $user->protocol;
+                            $config['obfs'] = $user->obfs;
+                        }
+                    }
+                    break;
+            }
+        }
+
+        return $config;
+    }
+
+    public function failedProxyReturn(string $text, $type = 1): string
+    {
+        switch ($type) {
+            case 2:
+                $url = sysConfig('website_url');
+                $result = 'vmess://'.base64url_encode(json_encode([
+                    'v'    => '2',
+                    'ps'   => $text,
+                    'add'  => $url,
+                    'port' => 0,
+                    'id'   => 0,
+                    'aid'  => 0,
+                    'net'  => 'tcp',
+                    'type' => 'none',
+                    'host' => $url,
+                    'path' => '/',
+                    'tls'  => 'tls',
+                ], JSON_PRETTY_PRINT));
+                break;
+            case 3:
+                $result = 'trojan://[email protected]:0?peer=0.0.0.0#'.rawurlencode($text);
+                break;
+            case 1:
+            case 4:
+            default:
+                $result = 'ssr://'.base64url_encode('0.0.0.0:0:origin:none:plain:'.base64url_encode('0000').'/?obfsparam=&protoparam=&remarks='.base64url_encode($text).'&group='.base64url_encode(sysConfig('website_name')).'&udpport=0&uot=0');
+                break;
+        }
+
+        return $result.PHP_EOL;
+    }
+
+    public function getProxyCode($target, $type = null) // 客户端用代理信息
+    {
+        $servers = $this->getNodeList($type);
+        if (empty($servers)) {
+            return null;
+        }
+
+        return $this->clientConfig($servers, $target);
+    }
+
+    public function getUserProxyConfig(array $server, bool $is_url): ?string // 用户显示用代理信息
+    {
+        $type = $is_url ? new URLSchemes() : new Text();
+        switch ($server['type']) {
+            case'shadowsocks':
+                $data = $type->buildShadowsocks($server);
+                break;
+            case 'shadowsocksr':
+                $data = $type->buildShadowsocksr($server);
+                break;
+            case 'v2ray':
+                $data = $type->buildVmess($server);
+                break;
+            case 'trojan':
+                $data = $type->buildTrojan($server);
+                break;
+            default:
+                $data = null;
+        }
+
+        return $data;
+    }
+}

+ 62 - 0
app/Services/UserService.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace App\Services;
+
+use Hashids\Hashids;
+
+class UserService extends BaseService
+{
+    private static $user;
+
+    public function __construct($user = null)
+    {
+        parent::__construct();
+        if (isset($user)) {
+            self::$user = $user;
+        } else {
+            self::$user = auth()->user();
+        }
+    }
+
+    public function getProfile()
+    {
+        $user = self::$user;
+
+        return [
+            'id'              => $user->id,
+            'nickname'        => $user->nickname,
+            'account'         => $user->username,
+            'port'            => $user->port,
+            'passwd'          => $user->passwd,
+            'uuid'            => $user->vmess_id,
+            'transfer_enable' => $user->transfer_enable,
+            'u'               => $user->u,
+            'd'               => $user->d,
+            't'               => $user->t,
+            'enable'          => $user->enable,
+            'speed_limit'     => $user->speed_limit,
+            'credit'          => $user->credit,
+            'expired_at'      => strtotime($user->expired_at),
+            'ban_time'        => $user->ban_time,
+            'level'           => $user->level_name,
+            'group'           => $user->userGroup->name ?? null,
+            'last_login'      => $user->last_login,
+            'reset_time'      => $user->reset_date,
+            'invite_num'      => $user->invite_num,
+            'status'          => $user->status,
+            'invite_url'      => $this->inviteURI(),
+        ];
+    }
+
+    public function inviteURI($isCode = false): string
+    {
+        $affSalt = sysConfig('aff_salt');
+        if (isset($affSalt)) {
+            $aff = (new Hashids($affSalt, 8))->encode(self::$user->id());
+        } else {
+            $aff = self::$user->id;
+        }
+
+        return $isCode ? $aff : sysConfig('website_url').route('register', ['aff' => 1], false);
+    }
+}

+ 210 - 0
config/client.php

@@ -0,0 +1,210 @@
+<?php
+
+return [
+    'read' => true, // 由数据库接管基础参数, true 为 接管,false为全部使用本文件中的参数;
+    // 安卓左侧按钮和跳转地址配置
+    'left_button' => [
+        'button1' => [
+            'text' => '邀请返利',
+            'url' => '/user/setting/invite',
+            'status' => true,
+        ],
+        'button2' => [
+            'text' => '购买套餐',
+            'url' => '/user/shop',
+            'status' => true,
+        ],
+        'button3' => [
+            'text' => '官方网站',
+            'url' => '/',
+            'status' => true,
+        ],
+    ],
+
+    // 新版安卓端首页两个按钮的显示和跳转
+    'android_index_button' => [
+        [
+            'name' => '商店',
+            'url' => '/user/shop',
+        ],
+        [
+            'name' => '官网',
+            'url' => '/user',
+        ],
+    ],
+
+    // .env里面的key值
+    'key' => env('APP_KEY'),
+
+    // 站点名称
+    'name' => 'Bob`s加速器',
+
+    // 面板地址,最后不要带有 /
+    'baseUrl' => 'http://www.xxx.com',
+
+    // API地址
+    'subscribe_url' => 'http://api.xxx.com',
+
+    // 签到获得流量
+    'checkinMin' => 1, //用户签到最少流量 单位MB
+    'checkinMax' => 50, //用户签到最多流量
+
+    'code_payback' => 10, //充值返利百分比
+    'invite_gift' => 2, //邀请新用户获得流量奖励,单位G
+
+    // 软件版本和更新地址
+    'vpn_update' => [
+        'enable' => false,                           //是否开启更新
+        'android' => [
+            'version' => '2.4.3',                    // 版本号
+            'download_url' => env('APP_URL').'/clients/bob.apk',       //下载地址
+            'message' => '版本更新:<br/>1.添加点击签到提示框<br/>2.修复剩余流量显示问题',       //提示信息
+            'must' => false,                          //true:强制更新  false:不强制更新
+        ],
+        'windows' => [
+            'version' => '3.7.0',                    // 版本号
+            'download_url' => env('APP_URL').'/clients/bob.exe',       //下载地址
+            'message' => '版本更新:<br/>1.修复剩余流量显示问题<br/>2.优化节点测试显示<br/>3.修复弹出网页部分按钮无法使用问题',       //提示信息
+            'must' => false,                          //true:强制更新  false:不强制更新
+        ],
+        'mac' => [
+            'version' => '3.7.0',                    // 版本号
+            'download_url' => env('APP_URL').'/clients/bob.zip',       // 下载地址
+            'message' => '版本更新:<br/>1.修复剩余流量显示问题<br/>2.优化节点测试显示<br/>3.修复弹出网页部分按钮无法使用问题',       //提示信息
+            'must' => false,                          // true:强制更新  false:不强制更新
+        ],
+    ],
+
+    // Crisp在线客服
+    'crisp_enable' => false,  // 是否开启
+    'crisp_id' => '2c3c28c2-9265-45ea-8e85-0xxxxx',       // Crisp 的网站ID
+    'crisp_logo_url' => 'http://xxxx/vpn/kefu.png',       // Crisp 客服logo
+
+    // 个人中心头像
+    'user_avatar' => env('APP_URL').'/assets/images/avatar.svg',
+
+    'show_address' => false, // PC端展示用户IP和地址
+    'node_class_name' => [], // 节点的等级对应的名字 格式为 节点等级 => 节点等级名字
+    'hidden_node' => [],  // 需要隐藏的节点ID, 数组形式 1,2,3,4
+    'login' => [ // 登录页面配置
+        'telegram_url' => 'https://t.me/+nW8AwsPPUsliYzg1',  // 留空的话则不展示telegram群
+        'qq_url' => '',  // 留空的话则不展示QQ群
+        'background_img' => env('APP_URL').'/assets/images/logo_1.png', // 背景图片地址,图片宽高不超过 860px * 544px 就行 (留空为默认的背景图)
+        'text' => '<p>'.env('APP_NAME').'</p>',
+        'text_color' => 'rgba(255, 255, 255, 0.8);',    // 文字和按钮颜色   默认颜色 rgba(255, 255, 255, 0.8);
+        'button_color' => '#667afa',    // 文字和按钮颜色 默认颜色:#8077f1(v2版本配置)
+    ],
+
+    // PC端消息中心图片和跳转链接
+    'message' => [
+        'background_img' => 'https://malus.s3cdn.net/uploads/malus_user-guide.jpg', // 背景图片地址
+        'url' => 'https://www.google.com',    // 跳转链接
+    ],
+
+    // 客户端ping检测 1:中转机 2:落地机
+    'ping_test' => 1,
+
+    // 支付
+    'payment' => [
+        'alipay' => 'theadpay',
+        'wechat' => 'paybeaver',
+        'default' => 'paybeaver',
+        'telegram_admin' => 0,    // 额外的 Telegram 管理员 ID,接收支付提醒
+        'paybeaver' => [
+            'app_id' => '',
+            'app_secret' => '',
+            'pay_url' => 'https://api.paybeaver.com',
+        ],
+        'mgate' => [
+            'mgate_api_url' => 'https://api.umipay.net',
+            'mgate_app_id' => '',
+            'mgate_app_secret' => '',
+        ],
+        // stripe支付需要https://dashboard.stripe.com/webhooks去配置好webhook
+        // 客户端webhook: https://xxxx.com/api/v1/bob/payment/notify
+        // 然后复制最新的webhook密钥签名到下面的stripe_webhook
+        'stripe' => [
+            'stripe_key' => '',
+            'stripe_currency' => 'hkd',
+            'stripe_webhook' => '',
+        ],
+        // 平头哥支付
+        'theadpay' => [
+            'theadpay_url' => 'https://jk.theadpay.com/v1/jk/orders',
+            'theadpay_mchid' => '',
+            'theadpay_key' => '',
+        ],
+        // 易支付
+        'policepay' => [
+            'partner' => '', //商户号
+            'key' => '', //商户key
+            'sign_type' => strtoupper('MD5'),
+            'input_charset' => strtolower('utf-8'),
+            'name' => '手抓饼',                  //商品名称,目前无意义
+            'transport' => 'https',                   //访问模式,根据自己的服务器是否支持ssl访问,若支持请选择https;若不支持请选择http
+            'appname' => 'PolicePay',           //网站英文名
+            'apiurl' => 'https://policepay.cc/',      //支付网关 注意结尾的/符号
+            'min_price' => '1',                       //最小支付金额(请填正数)
+        ],
+        // 当面付
+        'facepay' => [
+            'alipay_app_id' => '',              //商户号
+            'merchant_private_key' => '',
+            'alipay_public_key' => '',
+        ],
+    ],
+
+    // 商城配置
+    'shop_plan' => [
+        '标准会员' => [1, 2, 3, 4],  //对应商店显示的名称 + [商品ID]
+        '高级会员' => [1, 2, 3, 4],  //对应商店显示的名称 + [商品ID]
+        '至尊会员' => [1, 2, 3, 4],  //对应商店显示的名称 + [商品ID]
+    ],
+
+    // 购买配置
+    'enable_bought_reset' => true,  // 购买时是否重置流量
+    'enable_bought_extend' => true,  // 购买时是否延长等级期限(同等级配套)
+
+    // 更改订阅方式
+    'clash_online_user' => 1,   // 1: 根据在线用户数排序来订阅节点 2: 原版sspanel订阅方式
+
+    // 检查用户计算机时间
+    'check_time' => [
+        'is_check' => true,  // 是否开启检查
+        'differ_time' => 90,    // 相差多少秒提示
+        'warning_text' => '请校准系统时间为北京时间,否则会导致无法上网!', // 提示内容
+    ],
+
+    // 弹窗公告
+    'notice' => [
+        'is_start' => true, // 是否开启弹窗公告
+        'title' => '最新公告', // 标题
+        'content' => '<strong>这是最新 <i>公告</i> 内容</strong>', // 公告内容,可以为html格式,也可以纯文本
+    ],
+
+    // 用户登录状态保存天数
+    'login_time' => 7,
+
+    // Telegram 机器人
+    'enable_telegram' => false,  // 是否开启TG机器人
+    'telegram_token' => '',  // Telegram bot,bot 的 token
+
+    // PC端菜单栏显示控制
+    'menu' => [
+        'shop' => true,          // 会员
+        'user' => true,          // 我的
+        'gift' => true,          // 邀请
+    ],
+
+    // 安卓端商店显示
+    'android_shop_show' => true,
+
+    // 注册页发送邮件显示
+    'enable_email_verify' => true,
+
+    // 会员即将过期提醒
+    'class_expire_notice' => [
+        'days' => 7,    // 多天内过期提醒
+        'msg' => '您好,系统发现您的账号还剩%s天就过期了,请记得及时续费哦~',  // 过期提醒文字 (%s不要删,这个是替换天数用的)
+    ],
+];

+ 67 - 0
config/sanctum.php

@@ -0,0 +1,67 @@
+<?php
+
+use Laravel\Sanctum\Sanctum;
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Stateful Domains
+    |--------------------------------------------------------------------------
+    |
+    | Requests from the following domains / hosts will receive stateful API
+    | authentication cookies. Typically, these should include your local
+    | and production domains which access your API via a frontend SPA.
+    |
+    */
+
+    'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
+        '%s%s',
+        'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
+        Sanctum::currentApplicationUrlWithPort()
+    ))),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Sanctum Guards
+    |--------------------------------------------------------------------------
+    |
+    | This array contains the authentication guards that will be checked when
+    | Sanctum is trying to authenticate a request. If none of these guards
+    | are able to authenticate the request, Sanctum will use the bearer
+    | token that's present on an incoming request for authentication.
+    |
+    */
+
+    'guard' => ['web'],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Expiration Minutes
+    |--------------------------------------------------------------------------
+    |
+    | This value controls the number of minutes until an issued token will be
+    | considered expired. If this value is null, personal access tokens do
+    | not expire. This won't tweak the lifetime of first-party sessions.
+    |
+    */
+
+    'expiration' => null,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Sanctum Middleware
+    |--------------------------------------------------------------------------
+    |
+    | When authenticating your first-party SPA with Sanctum you may need to
+    | customize some of the middleware Sanctum uses while processing the
+    | request. You may change the middleware listed below as required.
+    |
+    */
+
+    'middleware' => [
+        'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
+        'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
+    ],
+
+];

+ 1 - 1
config/session.php

@@ -168,7 +168,7 @@ return [
     |
     |
     */
     */
 
 
-    'secure' => env('SESSION_SECURE_COOKIE'),
+    'secure' => env('REDIRECT_HTTPS', true),
 
 
     /*
     /*
     |--------------------------------------------------------------------------
     |--------------------------------------------------------------------------

+ 1 - 1
config/version.php

@@ -2,5 +2,5 @@
 
 
 return [
 return [
     'name' => 'ProxyPanel',
     'name' => 'ProxyPanel',
-    'number' => '2.7.1',
+    'number' => '2.7.b',
 ];
 ];

+ 36 - 0
database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php

@@ -0,0 +1,36 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreatePersonalAccessTokensTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('personal_access_tokens', function (Blueprint $table) {
+            $table->bigIncrements('id');
+            $table->morphs('tokenable');
+            $table->string('name');
+            $table->string('token', 64)->unique();
+            $table->text('abilities')->nullable();
+            $table->timestamp('last_used_at')->nullable();
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('personal_access_tokens');
+    }
+}

+ 1 - 0
resources/lang/en/errors.php

@@ -7,6 +7,7 @@ return [
         'china'   => 'Chinese IP or Proxy Access Forbidden',
         'china'   => 'Chinese IP or Proxy Access Forbidden',
         'oversea' => 'Oversea IP or Proxy Access Forbidden',
         'oversea' => 'Oversea IP or Proxy Access Forbidden',
     ],
     ],
+    'http'         => 'HTTP Method Not Allowed',
     'log'          => 'Logs',
     'log'          => 'Logs',
     'missing_page' => 'Page Not Found',
     'missing_page' => 'Page Not Found',
     'refresh'      => 'Refresh',
     'refresh'      => 'Refresh',

+ 3 - 2
resources/lang/en/user.php

@@ -188,8 +188,9 @@ return [
         'active_prepaid_tips'     => 'After active:<br>Current order will be set to expired! <br> Expired dates will be recalculated!',
         'active_prepaid_tips'     => 'After active:<br>Current order will be set to expired! <br> Expired dates will be recalculated!',
     ],
     ],
     'service'             => [
     'service'             => [
-        'node_count' => 'Include <code>:num</code> Nodes',
-        'unlimited'  => 'Unlimited Speed',
+        'node_count'    => 'Include <code>:num</code> Nodes',
+        'country_count' => 'Covered <code>:num</code> Countries or Areas',
+        'unlimited'     => 'Unlimited Speed',
     ],
     ],
     'node'                => [
     'node'                => [
         'info'     => 'Configuration information',
         'info'     => 'Configuration information',

+ 1 - 0
resources/lang/zh_CN/errors.php

@@ -7,6 +7,7 @@ return [
         'china'   => '检测到中国IP或使用代理访问,禁止访问',
         'china'   => '检测到中国IP或使用代理访问,禁止访问',
         'oversea' => '检测到海外IP或代理访问,禁止访问',
         'oversea' => '检测到海外IP或代理访问,禁止访问',
     ],
     ],
+    'http'         => 'HTTP请求类型错误',
     'log'          => '日志',
     'log'          => '日志',
     'missing_page' => '找不到网页',
     'missing_page' => '找不到网页',
     'refresh'      => '刷 新',
     'refresh'      => '刷 新',

+ 3 - 2
resources/lang/zh_CN/user.php

@@ -172,8 +172,9 @@ return [
         'call4help'     => '请开工单通知客服',
         'call4help'     => '请开工单通知客服',
     ],
     ],
     'service'             => [
     'service'             => [
-        'node_count' => '<code>:num</code>条优质线路',
-        'unlimited'  => '不限速',
+        'node_count'    => '<code>:num</code> 条优质线路',
+        'country_count' => '覆盖 <code>:num</code> 个国家或地区',
+        'unlimited'     => '不限速',
     ],
     ],
     'payment'             => [
     'payment'             => [
         'error'           => '充值余额不合规',
         'error'           => '充值余额不合规',

+ 522 - 0
resources/rules/bob.clash.yaml

@@ -0,0 +1,522 @@
+port: 7890
+socks-port: 7891
+allow-lan: false
+mode: rule
+log-level: info
+external-controller: 127.0.0.1:9090
+experimental:
+  ignore-resolve-fail: true
+dns:
+  enable: true
+  ipv6: false
+  enhanced-mode: redir-host
+  nameserver:
+    - 1.2.4.8
+    - 223.5.5.5
+  fallback:
+    - tls://1.0.0.1:853
+    - tls://dns.google:853
+proxies:
+
+proxy-groups:
+  - { name: "Proxy", type: select, proxies: [] }
+
+rules:
+  # Apple
+  - DOMAIN,safebrowsing.urlsec.qq.com,DIRECT # 如果您并不信任此服务提供商或防止其下载消耗过多带宽资源,可以进入 Safari 设置,关闭 Fraudulent Website Warning 功能,并使用 REJECT 策略。
+  - DOMAIN,safebrowsing.googleapis.com,DIRECT # 如果您并不信任此服务提供商或防止其下载消耗过多带宽资源,可以进入 Safari 设置,关闭 Fraudulent Website Warning 功能,并使用 REJECT 策略。
+  - DOMAIN,ocsp.apple.com,Proxy
+  - DOMAIN-SUFFIX,digicert.com,Proxy
+  - DOMAIN-SUFFIX,entrust.net,Proxy
+  - DOMAIN,ocsp.verisign.net,Proxy
+  - DOMAIN-SUFFIX,apps.apple.com,Proxy
+  - DOMAIN,itunes.apple.com,Proxy
+  - DOMAIN-SUFFIX,blobstore.apple.com,Proxy
+  - DOMAIN-SUFFIX,music.apple.com,DIRECT
+  - DOMAIN-SUFFIX,mzstatic.com,DIRECT
+  - DOMAIN-SUFFIX,itunes.apple.com,DIRECT
+  - DOMAIN-SUFFIX,icloud.com,DIRECT
+  - DOMAIN-SUFFIX,icloud-content.com,DIRECT
+  - DOMAIN-SUFFIX,me.com,DIRECT
+  - DOMAIN-SUFFIX,mzstatic.com,DIRECT
+  - DOMAIN-SUFFIX,akadns.net,DIRECT
+  - DOMAIN-SUFFIX,aaplimg.com,DIRECT
+  - DOMAIN-SUFFIX,cdn-apple.com,DIRECT
+  - DOMAIN-SUFFIX,apple.com,DIRECT
+  - DOMAIN-SUFFIX,apple-cloudkit.com,DIRECT
+  # - DOMAIN,e.crashlytics.com,REJECT //注释此选项有助于大多数App开发者分析崩溃信息;如果您拒绝一切崩溃数据统计、搜集,请取消 # 注释。
+
+
+  # 自定义规则
+  ## 您可以在此处插入您补充的自定义规则(请注意保持缩进)
+
+  # 国内网站
+  - DOMAIN-SUFFIX,cn,DIRECT
+  - DOMAIN-KEYWORD,-cn,DIRECT
+
+  - DOMAIN-SUFFIX,126.com,DIRECT
+  - DOMAIN-SUFFIX,126.net,DIRECT
+  - DOMAIN-SUFFIX,127.net,DIRECT
+  - DOMAIN-SUFFIX,163.com,DIRECT
+  - DOMAIN-SUFFIX,360buyimg.com,DIRECT
+  - DOMAIN-SUFFIX,36kr.com,DIRECT
+  - DOMAIN-SUFFIX,acfun.tv,DIRECT
+  - DOMAIN-SUFFIX,air-matters.com,DIRECT
+  - DOMAIN-SUFFIX,aixifan.com,DIRECT
+  - DOMAIN-SUFFIX,akamaized.net,DIRECT
+  - DOMAIN-KEYWORD,alicdn,DIRECT
+  - DOMAIN-KEYWORD,alipay,DIRECT
+  - DOMAIN-KEYWORD,taobao,DIRECT
+  - DOMAIN-SUFFIX,amap.com,DIRECT
+  - DOMAIN-SUFFIX,autonavi.com,DIRECT
+  - DOMAIN-KEYWORD,baidu,DIRECT
+  - DOMAIN-SUFFIX,bdimg.com,DIRECT
+  - DOMAIN-SUFFIX,bdstatic.com,DIRECT
+  - DOMAIN-SUFFIX,bilibili.com,DIRECT
+  - DOMAIN-SUFFIX,bilivideo.com,DIRECT
+  - DOMAIN-SUFFIX,caiyunapp.com,DIRECT
+  - DOMAIN-SUFFIX,clouddn.com,DIRECT
+  - DOMAIN-SUFFIX,cnbeta.com,DIRECT
+  - DOMAIN-SUFFIX,cnbetacdn.com,DIRECT
+  - DOMAIN-SUFFIX,cootekservice.com,DIRECT
+  - DOMAIN-SUFFIX,csdn.net,DIRECT
+  - DOMAIN-SUFFIX,ctrip.com,DIRECT
+  - DOMAIN-SUFFIX,dgtle.com,DIRECT
+  - DOMAIN-SUFFIX,dianping.com,DIRECT
+  - DOMAIN-SUFFIX,douban.com,DIRECT
+  - DOMAIN-SUFFIX,doubanio.com,DIRECT
+  - DOMAIN-SUFFIX,duokan.com,DIRECT
+  - DOMAIN-SUFFIX,easou.com,DIRECT
+  - DOMAIN-SUFFIX,ele.me,DIRECT
+  - DOMAIN-SUFFIX,feng.com,DIRECT
+  - DOMAIN-SUFFIX,fir.im,DIRECT
+  - DOMAIN-SUFFIX,frdic.com,DIRECT
+  - DOMAIN-SUFFIX,g-cores.com,DIRECT
+  - DOMAIN-SUFFIX,godic.net,DIRECT
+  - DOMAIN-SUFFIX,gtimg.com,DIRECT
+  - DOMAIN,cdn.hockeyapp.net,DIRECT
+  - DOMAIN-SUFFIX,hdslb.com,DIRECT
+  - DOMAIN-SUFFIX,hongxiu.com,DIRECT
+  - DOMAIN-SUFFIX,hxcdn.net,DIRECT
+  - DOMAIN-SUFFIX,iciba.com,DIRECT
+  - DOMAIN-SUFFIX,ifeng.com,DIRECT
+  - DOMAIN-SUFFIX,ifengimg.com,DIRECT
+  - DOMAIN-SUFFIX,ipip.net,DIRECT
+  - DOMAIN-SUFFIX,iqiyi.com,DIRECT
+  - DOMAIN-SUFFIX,jd.com,DIRECT
+  - DOMAIN-SUFFIX,jianshu.com,DIRECT
+  - DOMAIN-SUFFIX,knewone.com,DIRECT
+  - DOMAIN-SUFFIX,le.com,DIRECT
+  - DOMAIN-SUFFIX,lecloud.com,DIRECT
+  - DOMAIN-SUFFIX,lemicp.com,DIRECT
+  - DOMAIN-SUFFIX,licdn.com,DIRECT
+  - DOMAIN-SUFFIX,linkedin.com,DIRECT
+  - DOMAIN-SUFFIX,luoo.net,DIRECT
+  - DOMAIN-SUFFIX,meituan.com,DIRECT
+  - DOMAIN-SUFFIX,meituan.net,DIRECT
+  - DOMAIN-SUFFIX,mi.com,DIRECT
+  - DOMAIN-SUFFIX,miaopai.com,DIRECT
+  - DOMAIN-SUFFIX,microsoft.com,DIRECT
+  - DOMAIN-SUFFIX,microsoftonline.com,DIRECT
+  - DOMAIN-SUFFIX,miui.com,DIRECT
+  - DOMAIN-SUFFIX,miwifi.com,DIRECT
+  - DOMAIN-SUFFIX,mob.com,DIRECT
+  - DOMAIN-SUFFIX,netease.com,DIRECT
+  - DOMAIN-SUFFIX,office.com,DIRECT
+  - DOMAIN-SUFFIX,office365.com,DIRECT
+  - DOMAIN-KEYWORD,officecdn,DIRECT
+  - DOMAIN-SUFFIX,oschina.net,DIRECT
+  - DOMAIN-SUFFIX,ppsimg.com,DIRECT
+  - DOMAIN-SUFFIX,pstatp.com,DIRECT
+  - DOMAIN-SUFFIX,qcloud.com,DIRECT
+  - DOMAIN-SUFFIX,qdaily.com,DIRECT
+  - DOMAIN-SUFFIX,qdmm.com,DIRECT
+  - DOMAIN-SUFFIX,qhimg.com,DIRECT
+  - DOMAIN-SUFFIX,qhres.com,DIRECT
+  - DOMAIN-SUFFIX,qidian.com,DIRECT
+  - DOMAIN-SUFFIX,qihucdn.com,DIRECT
+  - DOMAIN-SUFFIX,qiniu.com,DIRECT
+  - DOMAIN-SUFFIX,qiniucdn.com,DIRECT
+  - DOMAIN-SUFFIX,qiyipic.com,DIRECT
+  - DOMAIN-SUFFIX,qq.com,DIRECT
+  - DOMAIN-SUFFIX,qqurl.com,DIRECT
+  - DOMAIN-SUFFIX,rarbg.to,DIRECT
+  - DOMAIN-SUFFIX,ruguoapp.com,DIRECT
+  - DOMAIN-SUFFIX,segmentfault.com,DIRECT
+  - DOMAIN-SUFFIX,sinaapp.com,DIRECT
+  - DOMAIN-SUFFIX,smzdm.com,DIRECT
+  - DOMAIN-SUFFIX,snapdrop.net,DIRECT
+  - DOMAIN-SUFFIX,sogou.com,DIRECT
+  - DOMAIN-SUFFIX,sogoucdn.com,DIRECT
+  - DOMAIN-SUFFIX,sohu.com,DIRECT
+  - DOMAIN-SUFFIX,soku.com,DIRECT
+  - DOMAIN-SUFFIX,speedtest.net,DIRECT
+  - DOMAIN-SUFFIX,sspai.com,DIRECT
+  - DOMAIN-SUFFIX,suning.com,DIRECT
+  - DOMAIN-SUFFIX,taobao.com,DIRECT
+  - DOMAIN-SUFFIX,tencent.com,DIRECT
+  - DOMAIN-SUFFIX,tenpay.com,DIRECT
+  - DOMAIN-SUFFIX,tianyancha.com,DIRECT
+  - DOMAIN-SUFFIX,tmall.com,DIRECT
+  - DOMAIN-SUFFIX,tudou.com,DIRECT
+  - DOMAIN-SUFFIX,umetrip.com,DIRECT
+  - DOMAIN-SUFFIX,upaiyun.com,DIRECT
+  - DOMAIN-SUFFIX,upyun.com,DIRECT
+  - DOMAIN-SUFFIX,veryzhun.com,DIRECT
+  - DOMAIN-SUFFIX,weather.com,DIRECT
+  - DOMAIN-SUFFIX,weibo.com,DIRECT
+  - DOMAIN-SUFFIX,xiami.com,DIRECT
+  - DOMAIN-SUFFIX,xiami.net,DIRECT
+  - DOMAIN-SUFFIX,xiaomicp.com,DIRECT
+  - DOMAIN-SUFFIX,ximalaya.com,DIRECT
+  - DOMAIN-SUFFIX,xmcdn.com,DIRECT
+  - DOMAIN-SUFFIX,xunlei.com,DIRECT
+  - DOMAIN-SUFFIX,yhd.com,DIRECT
+  - DOMAIN-SUFFIX,yihaodianimg.com,DIRECT
+  - DOMAIN-SUFFIX,yinxiang.com,DIRECT
+  - DOMAIN-SUFFIX,ykimg.com,DIRECT
+  - DOMAIN-SUFFIX,youdao.com,DIRECT
+  - DOMAIN-SUFFIX,youku.com,DIRECT
+  - DOMAIN-SUFFIX,zealer.com,DIRECT
+  - DOMAIN-SUFFIX,zhihu.com,DIRECT
+  - DOMAIN-SUFFIX,zhimg.com,DIRECT
+  - DOMAIN-SUFFIX,zimuzu.tv,DIRECT
+  - DOMAIN-SUFFIX,zoho.com,DIRECT
+
+  # 抗 DNS 污染
+  - DOMAIN-KEYWORD,amazon,Proxy
+  - DOMAIN-KEYWORD,google,Proxy
+  - DOMAIN-KEYWORD,gmail,Proxy
+  - DOMAIN-KEYWORD,youtube,Proxy
+  - DOMAIN-KEYWORD,facebook,Proxy
+  - DOMAIN-SUFFIX,fb.me,Proxy
+  - DOMAIN-SUFFIX,fbcdn.net,Proxy
+  - DOMAIN-KEYWORD,twitter,Proxy
+  - DOMAIN-KEYWORD,instagram,Proxy
+  - DOMAIN-KEYWORD,dropbox,Proxy
+  - DOMAIN-SUFFIX,twimg.com,Proxy
+  - DOMAIN-KEYWORD,blogspot,Proxy
+  - DOMAIN-SUFFIX,youtu.be,Proxy
+  - DOMAIN-KEYWORD,whatsapp,Proxy
+
+  # 常见广告域名屏蔽
+  - DOMAIN-KEYWORD,admarvel,REJECT
+  - DOMAIN-KEYWORD,admaster,REJECT
+  - DOMAIN-KEYWORD,adsage,REJECT
+  - DOMAIN-KEYWORD,adsmogo,REJECT
+  - DOMAIN-KEYWORD,adsrvmedia,REJECT
+  - DOMAIN-KEYWORD,adwords,REJECT
+  - DOMAIN-KEYWORD,adservice,REJECT
+  - DOMAIN-KEYWORD,domob,REJECT
+  - DOMAIN-KEYWORD,duomeng,REJECT
+  - DOMAIN-KEYWORD,dwtrack,REJECT
+  - DOMAIN-KEYWORD,guanggao,REJECT
+  - DOMAIN-KEYWORD,lianmeng,REJECT
+  - DOMAIN-SUFFIX,mmstat.com,REJECT
+  - DOMAIN-KEYWORD,omgmta,REJECT
+  - DOMAIN-KEYWORD,openx,REJECT
+  - DOMAIN-KEYWORD,partnerad,REJECT
+  - DOMAIN-KEYWORD,pingfore,REJECT
+  - DOMAIN-KEYWORD,supersonicads,REJECT
+  - DOMAIN-KEYWORD,tracking,REJECT
+  - DOMAIN-KEYWORD,uedas,REJECT
+  - DOMAIN-KEYWORD,umeng,REJECT
+  - DOMAIN-KEYWORD,usage,REJECT
+  - DOMAIN-KEYWORD,wlmonitor,REJECT
+  - DOMAIN-KEYWORD,zjtoolbar,REJECT
+
+  # 国外网站
+  - DOMAIN-SUFFIX,9to5mac.com,Proxy
+  - DOMAIN-SUFFIX,abpchina.org,Proxy
+  - DOMAIN-SUFFIX,adblockplus.org,Proxy
+  - DOMAIN-SUFFIX,adobe.com,Proxy
+  - DOMAIN-SUFFIX,alfredapp.com,Proxy
+  - DOMAIN-SUFFIX,amplitude.com,Proxy
+  - DOMAIN-SUFFIX,ampproject.org,Proxy
+  - DOMAIN-SUFFIX,android.com,Proxy
+  - DOMAIN-SUFFIX,angularjs.org,Proxy
+  - DOMAIN-SUFFIX,aolcdn.com,Proxy
+  - DOMAIN-SUFFIX,apkpure.com,Proxy
+  - DOMAIN-SUFFIX,appledaily.com,Proxy
+  - DOMAIN-SUFFIX,appshopper.com,Proxy
+  - DOMAIN-SUFFIX,appspot.com,Proxy
+  - DOMAIN-SUFFIX,arcgis.com,Proxy
+  - DOMAIN-SUFFIX,archive.org,Proxy
+  - DOMAIN-SUFFIX,armorgames.com,Proxy
+  - DOMAIN-SUFFIX,aspnetcdn.com,Proxy
+  - DOMAIN-SUFFIX,att.com,Proxy
+  - DOMAIN-SUFFIX,awsstatic.com,Proxy
+  - DOMAIN-SUFFIX,azureedge.net,Proxy
+  - DOMAIN-SUFFIX,azurewebsites.net,Proxy
+  - DOMAIN-SUFFIX,bing.com,Proxy
+  - DOMAIN-SUFFIX,bintray.com,Proxy
+  - DOMAIN-SUFFIX,bit.com,Proxy
+  - DOMAIN-SUFFIX,bit.ly,Proxy
+  - DOMAIN-SUFFIX,bitbucket.org,Proxy
+  - DOMAIN-SUFFIX,bjango.com,Proxy
+  - DOMAIN-SUFFIX,bkrtx.com,Proxy
+  - DOMAIN-SUFFIX,blog.com,Proxy
+  - DOMAIN-SUFFIX,blogcdn.com,Proxy
+  - DOMAIN-SUFFIX,blogger.com,Proxy
+  - DOMAIN-SUFFIX,blogsmithmedia.com,Proxy
+  - DOMAIN-SUFFIX,blogspot.com,Proxy
+  - DOMAIN-SUFFIX,blogspot.hk,Proxy
+  - DOMAIN-SUFFIX,bloomberg.com,Proxy
+  - DOMAIN-SUFFIX,box.com,Proxy
+  - DOMAIN-SUFFIX,box.net,Proxy
+  - DOMAIN-SUFFIX,cachefly.net,Proxy
+  - DOMAIN-SUFFIX,chromium.org,Proxy
+  - DOMAIN-SUFFIX,cl.ly,Proxy
+  - DOMAIN-SUFFIX,cloudflare.com,Proxy
+  - DOMAIN-SUFFIX,cloudfront.net,Proxy
+  - DOMAIN-SUFFIX,cloudmagic.com,Proxy
+  - DOMAIN-SUFFIX,cmail19.com,Proxy
+  - DOMAIN-SUFFIX,cnet.com,Proxy
+  - DOMAIN-SUFFIX,cocoapods.org,Proxy
+  - DOMAIN-SUFFIX,comodoca.com,Proxy
+  - DOMAIN-SUFFIX,crashlytics.com,Proxy
+  - DOMAIN-SUFFIX,culturedcode.com,Proxy
+  - DOMAIN-SUFFIX,d.pr,Proxy
+  - DOMAIN-SUFFIX,danilo.to,Proxy
+  - DOMAIN-SUFFIX,dayone.me,Proxy
+  - DOMAIN-SUFFIX,db.tt,Proxy
+  - DOMAIN-SUFFIX,deskconnect.com,Proxy
+  - DOMAIN-SUFFIX,disq.us,Proxy
+  - DOMAIN-SUFFIX,disqus.com,Proxy
+  - DOMAIN-SUFFIX,disquscdn.com,Proxy
+  - DOMAIN-SUFFIX,dnsimple.com,Proxy
+  - DOMAIN-SUFFIX,docker.com,Proxy
+  - DOMAIN-SUFFIX,dribbble.com,Proxy
+  - DOMAIN-SUFFIX,droplr.com,Proxy
+  - DOMAIN-SUFFIX,duckduckgo.com,Proxy
+  - DOMAIN-SUFFIX,dueapp.com,Proxy
+  - DOMAIN-SUFFIX,dytt8.net,Proxy
+  - DOMAIN-SUFFIX,edgecastcdn.net,Proxy
+  - DOMAIN-SUFFIX,edgekey.net,Proxy
+  - DOMAIN-SUFFIX,edgesuite.net,Proxy
+  - DOMAIN-SUFFIX,engadget.com,Proxy
+  - DOMAIN-SUFFIX,entrust.net,Proxy
+  - DOMAIN-SUFFIX,eurekavpt.com,Proxy
+  - DOMAIN-SUFFIX,evernote.com,Proxy
+  - DOMAIN-SUFFIX,fabric.io,Proxy
+  - DOMAIN-SUFFIX,fast.com,Proxy
+  - DOMAIN-SUFFIX,fastly.net,Proxy
+  - DOMAIN-SUFFIX,fc2.com,Proxy
+  - DOMAIN-SUFFIX,feedburner.com,Proxy
+  - DOMAIN-SUFFIX,feedly.com,Proxy
+  - DOMAIN-SUFFIX,feedsportal.com,Proxy
+  - DOMAIN-SUFFIX,fiftythree.com,Proxy
+  - DOMAIN-SUFFIX,firebaseio.com,Proxy
+  - DOMAIN-SUFFIX,flexibits.com,Proxy
+  - DOMAIN-SUFFIX,flickr.com,Proxy
+  - DOMAIN-SUFFIX,flipboard.com,Proxy
+  - DOMAIN-SUFFIX,g.co,Proxy
+  - DOMAIN-SUFFIX,gabia.net,Proxy
+  - DOMAIN-SUFFIX,geni.us,Proxy
+  - DOMAIN-SUFFIX,gfx.ms,Proxy
+  - DOMAIN-SUFFIX,ggpht.com,Proxy
+  - DOMAIN-SUFFIX,ghostnoteapp.com,Proxy
+  - DOMAIN-SUFFIX,git.io,Proxy
+  - DOMAIN-KEYWORD,github,Proxy
+  - DOMAIN-SUFFIX,globalsign.com,Proxy
+  - DOMAIN-SUFFIX,gmodules.com,Proxy
+  - DOMAIN-SUFFIX,godaddy.com,Proxy
+  - DOMAIN-SUFFIX,golang.org,Proxy
+  - DOMAIN-SUFFIX,gongm.in,Proxy
+  - DOMAIN-SUFFIX,goo.gl,Proxy
+  - DOMAIN-SUFFIX,goodreaders.com,Proxy
+  - DOMAIN-SUFFIX,goodreads.com,Proxy
+  - DOMAIN-SUFFIX,gravatar.com,Proxy
+  - DOMAIN-SUFFIX,gstatic.com,Proxy
+  - DOMAIN-SUFFIX,gvt0.com,Proxy
+  - DOMAIN-SUFFIX,hockeyapp.net,Proxy
+  - DOMAIN-SUFFIX,hotmail.com,Proxy
+  - DOMAIN-SUFFIX,icons8.com,Proxy
+  - DOMAIN-SUFFIX,ifixit.com,Proxy
+  - DOMAIN-SUFFIX,ift.tt,Proxy
+  - DOMAIN-SUFFIX,ifttt.com,Proxy
+  - DOMAIN-SUFFIX,iherb.com,Proxy
+  - DOMAIN-SUFFIX,imageshack.us,Proxy
+  - DOMAIN-SUFFIX,img.ly,Proxy
+  - DOMAIN-SUFFIX,imgur.com,Proxy
+  - DOMAIN-SUFFIX,imore.com,Proxy
+  - DOMAIN-SUFFIX,instapaper.com,Proxy
+  - DOMAIN-SUFFIX,ipn.li,Proxy
+  - DOMAIN-SUFFIX,is.gd,Proxy
+  - DOMAIN-SUFFIX,issuu.com,Proxy
+  - DOMAIN-SUFFIX,itgonglun.com,Proxy
+  - DOMAIN-SUFFIX,itun.es,Proxy
+  - DOMAIN-SUFFIX,ixquick.com,Proxy
+  - DOMAIN-SUFFIX,j.mp,Proxy
+  - DOMAIN-SUFFIX,js.revsci.net,Proxy
+  - DOMAIN-SUFFIX,jshint.com,Proxy
+  - DOMAIN-SUFFIX,jtvnw.net,Proxy
+  - DOMAIN-SUFFIX,justgetflux.com,Proxy
+  - DOMAIN-SUFFIX,kat.cr,Proxy
+  - DOMAIN-SUFFIX,klip.me,Proxy
+  - DOMAIN-SUFFIX,libsyn.com,Proxy
+  - DOMAIN-SUFFIX,linode.com,Proxy
+  - DOMAIN-SUFFIX,lithium.com,Proxy
+  - DOMAIN-SUFFIX,littlehj.com,Proxy
+  - DOMAIN-SUFFIX,live.com,Proxy
+  - DOMAIN-SUFFIX,live.net,Proxy
+  - DOMAIN-SUFFIX,livefilestore.com,Proxy
+  - DOMAIN-SUFFIX,llnwd.net,Proxy
+  - DOMAIN-SUFFIX,macid.co,Proxy
+  - DOMAIN-SUFFIX,macromedia.com,Proxy
+  - DOMAIN-SUFFIX,macrumors.com,Proxy
+  - DOMAIN-SUFFIX,mashable.com,Proxy
+  - DOMAIN-SUFFIX,mathjax.org,Proxy
+  - DOMAIN-SUFFIX,medium.com,Proxy
+  - DOMAIN-SUFFIX,mega.co.nz,Proxy
+  - DOMAIN-SUFFIX,mega.nz,Proxy
+  - DOMAIN-SUFFIX,megaupload.com,Proxy
+  - DOMAIN-SUFFIX,microsofttranslator.com,Proxy
+  - DOMAIN-SUFFIX,mindnode.com,Proxy
+  - DOMAIN-SUFFIX,mobile01.com,Proxy
+  - DOMAIN-SUFFIX,modmyi.com,Proxy
+  - DOMAIN-SUFFIX,msedge.net,Proxy
+  - DOMAIN-SUFFIX,myfontastic.com,Proxy
+  - DOMAIN-SUFFIX,name.com,Proxy
+  - DOMAIN-SUFFIX,nextmedia.com,Proxy
+  - DOMAIN-SUFFIX,nsstatic.net,Proxy
+  - DOMAIN-SUFFIX,nssurge.com,Proxy
+  - DOMAIN-SUFFIX,nyt.com,Proxy
+  - DOMAIN-SUFFIX,nytimes.com,Proxy
+  - DOMAIN-SUFFIX,omnigroup.com,Proxy
+  - DOMAIN-SUFFIX,onedrive.com,Proxy
+  - DOMAIN-SUFFIX,onenote.com,Proxy
+  - DOMAIN-SUFFIX,ooyala.com,Proxy
+  - DOMAIN-SUFFIX,openvpn.net,Proxy
+  - DOMAIN-SUFFIX,openwrt.org,Proxy
+  - DOMAIN-SUFFIX,orkut.com,Proxy
+  - DOMAIN-SUFFIX,osxdaily.com,Proxy
+  - DOMAIN-SUFFIX,outlook.com,Proxy
+  - DOMAIN-SUFFIX,ow.ly,Proxy
+  - DOMAIN-SUFFIX,paddleapi.com,Proxy
+  - DOMAIN-SUFFIX,parallels.com,Proxy
+  - DOMAIN-SUFFIX,parse.com,Proxy
+  - DOMAIN-SUFFIX,pdfexpert.com,Proxy
+  - DOMAIN-SUFFIX,periscope.tv,Proxy
+  - DOMAIN-SUFFIX,pinboard.in,Proxy
+  - DOMAIN-SUFFIX,pinterest.com,Proxy
+  - DOMAIN-SUFFIX,pixelmator.com,Proxy
+  - DOMAIN-SUFFIX,pixiv.net,Proxy
+  - DOMAIN-SUFFIX,playpcesor.com,Proxy
+  - DOMAIN-SUFFIX,playstation.com,Proxy
+  - DOMAIN-SUFFIX,playstation.com.hk,Proxy
+  - DOMAIN-SUFFIX,playstation.net,Proxy
+  - DOMAIN-SUFFIX,playstationnetwork.com,Proxy
+  - DOMAIN-SUFFIX,pushwoosh.com,Proxy
+  - DOMAIN-SUFFIX,rime.im,Proxy
+  - DOMAIN-SUFFIX,servebom.com,Proxy
+  - DOMAIN-SUFFIX,sfx.ms,Proxy
+  - DOMAIN-SUFFIX,shadowsocks.org,Proxy
+  - DOMAIN-SUFFIX,sharethis.com,Proxy
+  - DOMAIN-SUFFIX,shazam.com,Proxy
+  - DOMAIN-SUFFIX,skype.com,Proxy
+  - DOMAIN-SUFFIX,smartdnsProxy.com,Proxy
+  - DOMAIN-SUFFIX,smartmailcloud.com,Proxy
+  - DOMAIN-SUFFIX,sndcdn.com,Proxy
+  - DOMAIN-SUFFIX,sony.com,Proxy
+  - DOMAIN-SUFFIX,soundcloud.com,Proxy
+  - DOMAIN-SUFFIX,sourceforge.net,Proxy
+  - DOMAIN-SUFFIX,spotify.com,Proxy
+  - DOMAIN-SUFFIX,squarespace.com,Proxy
+  - DOMAIN-SUFFIX,sstatic.net,Proxy
+  - DOMAIN-SUFFIX,st.luluku.pw,Proxy
+  - DOMAIN-SUFFIX,stackoverflow.com,Proxy
+  - DOMAIN-SUFFIX,startpage.com,Proxy
+  - DOMAIN-SUFFIX,staticflickr.com,Proxy
+  - DOMAIN-SUFFIX,steamcommunity.com,Proxy
+  - DOMAIN-SUFFIX,symauth.com,Proxy
+  - DOMAIN-SUFFIX,symcb.com,Proxy
+  - DOMAIN-SUFFIX,symcd.com,Proxy
+  - DOMAIN-SUFFIX,tapbots.com,Proxy
+  - DOMAIN-SUFFIX,tapbots.net,Proxy
+  - DOMAIN-SUFFIX,tdesktop.com,Proxy
+  - DOMAIN-SUFFIX,techcrunch.com,Proxy
+  - DOMAIN-SUFFIX,techsmith.com,Proxy
+  - DOMAIN-SUFFIX,thepiratebay.org,Proxy
+  - DOMAIN-SUFFIX,theverge.com,Proxy
+  - DOMAIN-SUFFIX,time.com,Proxy
+  - DOMAIN-SUFFIX,timeinc.net,Proxy
+  - DOMAIN-SUFFIX,tiny.cc,Proxy
+  - DOMAIN-SUFFIX,tinypic.com,Proxy
+  - DOMAIN-SUFFIX,tmblr.co,Proxy
+  - DOMAIN-SUFFIX,todoist.com,Proxy
+  - DOMAIN-SUFFIX,trello.com,Proxy
+  - DOMAIN-SUFFIX,trustasiassl.com,Proxy
+  - DOMAIN-SUFFIX,tumblr.co,Proxy
+  - DOMAIN-SUFFIX,tumblr.com,Proxy
+  - DOMAIN-SUFFIX,tweetdeck.com,Proxy
+  - DOMAIN-SUFFIX,tweetmarker.net,Proxy
+  - DOMAIN-SUFFIX,twitch.tv,Proxy
+  - DOMAIN-SUFFIX,txmblr.com,Proxy
+  - DOMAIN-SUFFIX,typekit.net,Proxy
+  - DOMAIN-SUFFIX,ubertags.com,Proxy
+  - DOMAIN-SUFFIX,ublock.org,Proxy
+  - DOMAIN-SUFFIX,ubnt.com,Proxy
+  - DOMAIN-SUFFIX,ulyssesapp.com,Proxy
+  - DOMAIN-SUFFIX,urchin.com,Proxy
+  - DOMAIN-SUFFIX,usertrust.com,Proxy
+  - DOMAIN-SUFFIX,v.gd,Proxy
+  - DOMAIN-SUFFIX,v2ex.com,Proxy
+  - DOMAIN-SUFFIX,vimeo.com,Proxy
+  - DOMAIN-SUFFIX,vimeocdn.com,Proxy
+  - DOMAIN-SUFFIX,vine.co,Proxy
+  - DOMAIN-SUFFIX,vivaldi.com,Proxy
+  - DOMAIN-SUFFIX,vox-cdn.com,Proxy
+  - DOMAIN-SUFFIX,vsco.co,Proxy
+  - DOMAIN-SUFFIX,vultr.com,Proxy
+  - DOMAIN-SUFFIX,w.org,Proxy
+  - DOMAIN-SUFFIX,w3schools.com,Proxy
+  - DOMAIN-SUFFIX,webtype.com,Proxy
+  - DOMAIN-SUFFIX,wikiwand.com,Proxy
+  - DOMAIN-SUFFIX,wikileaks.org,Proxy
+  - DOMAIN-SUFFIX,wikimedia.org,Proxy
+  - DOMAIN-SUFFIX,wikipedia.com,Proxy
+  - DOMAIN-SUFFIX,wikipedia.org,Proxy
+  - DOMAIN-SUFFIX,windows.com,Proxy
+  - DOMAIN-SUFFIX,windows.net,Proxy
+  - DOMAIN-SUFFIX,wire.com,Proxy
+  - DOMAIN-SUFFIX,wordpress.com,Proxy
+  - DOMAIN-SUFFIX,workflowy.com,Proxy
+  - DOMAIN-SUFFIX,wp.com,Proxy
+  - DOMAIN-SUFFIX,wsj.com,Proxy
+  - DOMAIN-SUFFIX,wsj.net,Proxy
+  - DOMAIN-SUFFIX,xda-developers.com,Proxy
+  - DOMAIN-SUFFIX,xeeno.com,Proxy
+  - DOMAIN-SUFFIX,xiti.com,Proxy
+  - DOMAIN-SUFFIX,yahoo.com,Proxy
+  - DOMAIN-SUFFIX,yimg.com,Proxy
+  - DOMAIN-SUFFIX,ying.com,Proxy
+  - DOMAIN-SUFFIX,yoyo.org,Proxy
+  - DOMAIN-SUFFIX,ytimg.com,Proxy
+
+  # Telegram
+  - DOMAIN-SUFFIX,telegra.ph,Proxy
+  - DOMAIN-SUFFIX,telegram.org,Proxy
+
+  - IP-CIDR,91.108.4.0/22,Proxy,no-resolve
+  - IP-CIDR,91.108.8.0/22,Proxy,no-resolve
+  - IP-CIDR,91.108.12.0/22,Proxy,no-resolve
+  - IP-CIDR,91.108.16.0/22,Proxy,no-resolve
+  - IP-CIDR,91.108.56.0/22,Proxy,no-resolve
+  - IP-CIDR,149.154.160.0/22,Proxy,no-resolve
+  - IP-CIDR,149.154.164.0/22,Proxy,no-resolve
+  - IP-CIDR,149.154.168.0/22,Proxy,no-resolve
+  - IP-CIDR,149.154.172.0/22,Proxy,no-resolve
+
+  # LAN
+  - DOMAIN-SUFFIX,local,DIRECT
+  - IP-CIDR,127.0.0.0/8,DIRECT
+  - IP-CIDR,172.16.0.0/12,DIRECT
+  - IP-CIDR,192.168.0.0/16,DIRECT
+  - IP-CIDR,10.0.0.0/8,DIRECT
+  - IP-CIDR,17.0.0.0/8,DIRECT
+  - IP-CIDR,100.64.0.0/10,DIRECT
+
+  # 最终规则
+  - GEOIP,CN,DIRECT
+  - MATCH,Proxy

+ 21 - 15
routes/api.php

@@ -1,7 +1,7 @@
 <?php
 <?php
 
 
 // 后端WEBAPI
 // 后端WEBAPI
-Route::group(['namespace' => 'Api\WebApi', 'middleware' => 'webApi'], function () {
+Route::group(['namespace' => 'Api\WebApi', 'middleware' => 'webApi', 'domain' => sysConfig('web_api_url') ?? sysConfig('website_url')], function () {
     // ss后端WEBAPI V1版
     // ss后端WEBAPI V1版
     Route::group(['prefix' => 'ss/v1'], function () {
     Route::group(['prefix' => 'ss/v1'], function () {
         Route::get('node/{node}', 'SSController@getNodeInfo'); // 获取节点信息
         Route::get('node/{node}', 'SSController@getNodeInfo'); // 获取节点信息
@@ -71,18 +71,24 @@ Route::group(['namespace' => 'Api\WebApi', 'middleware' => 'webApi'], function (
 });
 });
 
 
 // 客户端API
 // 客户端API
-Route::group(['namespace' => 'Api\Client', 'prefix' => 'client/v1'], function () {
-    Route::get('config', 'V1Controller@getConfig'); // 获取配置
-    Route::post('login', 'V1Controller@login'); // 登录
-    Route::post('register', 'V1Controller@register'); // 注册
-    Route::get('logout', 'V1Controller@logout'); // 退出
-    Route::get('refresh', 'V1Controller@refresh'); // 刷新令牌
-    Route::get('profile', 'V1Controller@userProfile'); // 获取账户信息
-    Route::get('nodes', 'V1Controller@nodeList'); // 获取账户全部节点
-    Route::get('node/{id}', 'V1Controller@nodeList'); // 获取账户个别节点
-    Route::get('shop', 'V1Controller@shop'); // 获取商品信息
-    Route::get('gift', 'V1Controller@gift'); // 获取邀请信息
-    Route::post('checkIn', 'V1Controller@checkIn'); // 签到
-    Route::post('payment/purchase', 'V1Controller@purchase'); // 获取商品信息
-    Route::get('payment/getStatus', 'V1Controller@getStatus'); // 获取商品信息
+
+Route::group(['namespace' => 'Api\Client', 'prefix' => 'v1'], function () {
+    Route::post('login', 'AuthController@login'); // 登录
+    Route::post('register', 'AuthController@register'); // 注册
+    Route::get('logout', 'AuthController@logout'); //登出
+    Route::get('getconfig', 'ClientController@getConfig'); // 获取配置文件
+    Route::get('version/update', 'ClientController@checkClientVersion'); // 检查更新
+    Route::get('shop', 'ClientController@shop'); // 获取商品列表
+    Route::group(['middleware' => 'auth.client'], function () { // 用户验证
+        Route::get('getclash', 'ClientController@downloadProxies'); // 下载节点配置
+        Route::get('getuserinfo', 'ClientController@getUserInfo'); // 获取用户信息
+        Route::post('doCheckIn', 'ClientController@checkIn'); // 签到
+        Route::get('checkClashUpdate', 'ClientController@proxyCheck'); // 判断是否更新订阅
+        Route::get('proxy', 'ClientController@getProxyList'); // 获取节点列表
+        Route::get('tickets', 'ClientController@ticketList'); // 获取工单列表
+        Route::post('tickets/add', 'ClientController@ticket_add'); // 提交工单
+        Route::get('order', 'ClientController@getOrders'); // 获取订单列表
+        Route::get('invite/gift', 'ClientController@getInvite'); // 获取邀请详情和列表
+        Route::get('gettransfer', 'ClientController@getUserTransfer'); // 获取剩余流量
+    });
 });
 });