Browse Source

Merge pull request #848 from v2board/dev

1.7.4
tokumeikoi 2 years ago
parent
commit
0ca47622a5
68 changed files with 1345 additions and 398 deletions
  1. 1 1
      .env.example
  2. 1 1
      app/Console/Commands/CheckCommission.php
  3. 2 0
      app/Console/Commands/ResetLog.php
  4. 1 0
      app/Console/Commands/SendRemindMail.php
  5. 76 22
      app/Console/Commands/V2boardStatistics.php
  6. 1 3
      app/Exceptions/Handler.php
  7. 1 0
      app/Http/Controllers/Admin/CouponController.php
  8. 113 0
      app/Http/Controllers/Admin/Server/HysteriaController.php
  9. 13 23
      app/Http/Controllers/Admin/Server/ManageController.php
  10. 96 25
      app/Http/Controllers/Admin/StatController.php
  11. 2 12
      app/Http/Controllers/Admin/SystemController.php
  12. 8 9
      app/Http/Controllers/Client/ClientController.php
  13. 10 8
      app/Http/Controllers/Client/Protocols/Clash.php
  14. 12 3
      app/Http/Controllers/Client/Protocols/ClashMeta.php
  15. 113 0
      app/Http/Controllers/Client/Protocols/General.php
  16. 137 0
      app/Http/Controllers/Client/Protocols/Loon.php
  17. 5 0
      app/Http/Controllers/Client/Protocols/Passwall.php
  18. 5 0
      app/Http/Controllers/Client/Protocols/SagerNet.php
  19. 11 2
      app/Http/Controllers/Client/Protocols/Shadowrocket.php
  20. 9 0
      app/Http/Controllers/Client/Protocols/Stash.php
  21. 7 2
      app/Http/Controllers/Client/Protocols/V2rayN.php
  22. 5 0
      app/Http/Controllers/Client/Protocols/V2rayNG.php
  23. 2 2
      app/Http/Controllers/Passport/AuthController.php
  24. 5 3
      app/Http/Controllers/Server/DeepbworkController.php
  25. 5 3
      app/Http/Controllers/Server/ShadowsocksTidalabController.php
  26. 4 3
      app/Http/Controllers/Server/TrojanTidalabController.php
  27. 15 8
      app/Http/Controllers/Server/UniProxyController.php
  28. 1 0
      app/Http/Controllers/User/OrderController.php
  29. 1 0
      app/Http/Kernel.php
  30. 24 0
      app/Http/Middleware/RequestLog.php
  31. 16 2
      app/Http/Routes/AdminRoute.php
  32. 2 0
      app/Jobs/OrderHandleJob.php
  33. 2 0
      app/Jobs/SendEmailJob.php
  34. 1 1
      app/Jobs/SendTelegramJob.php
  35. 0 78
      app/Jobs/StatServerJob.php
  36. 0 80
      app/Jobs/StatUserJob.php
  37. 4 4
      app/Jobs/TrafficFetchJob.php
  38. 11 0
      app/Logging/MysqlLogger.php
  39. 44 0
      app/Logging/MysqlLoggerHandler.php
  40. 2 2
      app/Models/Log.php
  41. 19 0
      app/Models/ServerHysteria.php
  42. 16 0
      app/Models/Stat.php
  43. 16 1
      app/Payments/StripeCheckout.php
  44. 5 1
      app/Services/OrderService.php
  45. 41 2
      app/Services/ServerService.php
  46. 283 0
      app/Services/StatisticalService.php
  47. 12 4
      app/Services/UserService.php
  48. 3 0
      app/Utils/CacheKey.php
  49. 1 1
      app/Utils/Helper.php
  50. 1 1
      config/app.php
  51. 1 2
      config/horizon.php
  52. 6 1
      config/logging.php
  53. 64 18
      database/install.sql
  54. 55 28
      database/update.sql
  55. 0 14
      library/V2ray.php
  56. 0 0
      public/assets/admin/components.async.js
  57. 0 0
      public/assets/admin/components.chunk.css
  58. 0 0
      public/assets/admin/umi.js
  59. 0 0
      public/assets/admin/vendors.async.js
  60. 0 0
      public/theme/v2board/assets/components.async.js
  61. 26 26
      public/theme/v2board/assets/i18n/ja-JP.js
  62. 0 0
      public/theme/v2board/assets/umi.js
  63. 0 0
      public/theme/v2board/assets/vendors.async.js
  64. 1 1
      public/theme/v2board/dashboard.blade.php
  65. 5 0
      resources/views/errors/500.blade.php
  66. 0 1
      routes/web.php
  67. 11 0
      update.sh
  68. 11 0
      update_dev.sh

+ 1 - 1
.env.example

@@ -14,7 +14,7 @@ DB_USERNAME=root
 DB_PASSWORD=123456
 
 BROADCAST_DRIVER=log
-CACHE_DRIVER=redis
+CACHE_DRIVER=file
 QUEUE_CONNECTION=redis
 SESSION_DRIVER=redis
 SESSION_LIFETIME=120

+ 1 - 1
app/Console/Commands/CheckCommission.php

@@ -50,7 +50,7 @@ class CheckCommission extends Command
         if ((int)config('v2board.commission_auto_check_enable', 1)) {
             Order::where('commission_status', 0)
                 ->where('invite_user_id', '!=', NULL)
-                ->whereNotIn('status', [0, 2])
+                ->where('status', 3)
                 ->where('updated_at', '<=', strtotime('-3 day', time()))
                 ->update([
                     'commission_status' => 1

+ 2 - 0
app/Console/Commands/ResetLog.php

@@ -2,6 +2,7 @@
 
 namespace App\Console\Commands;
 
+use App\Models\Log;
 use App\Models\Plan;
 use App\Models\StatServer;
 use App\Models\StatUser;
@@ -46,5 +47,6 @@ class ResetLog extends Command
     {
         StatUser::where('record_at', '<', strtotime('-2 month', time()))->delete();
         StatServer::where('record_at', '<', strtotime('-2 month', time()))->delete();
+        Log::where('created_at', '<', strtotime('-1 month', time()))->delete();
     }
 }

+ 1 - 0
app/Console/Commands/SendRemindMail.php

@@ -45,6 +45,7 @@ class SendRemindMail extends Command
         $mailService = new MailService();
         foreach ($users as $user) {
             if ($user->remind_expire) $mailService->remindExpire($user);
+            if ($user->remind_traffic) $mailService->remindTraffic($user);
         }
     }
 }

+ 76 - 22
app/Console/Commands/V2boardStatistics.php

@@ -2,10 +2,15 @@
 
 namespace App\Console\Commands;
 
+use App\Models\StatServer;
+use App\Models\StatUser;
+use App\Models\User;
+use App\Services\StatisticalService;
 use Illuminate\Console\Command;
 use App\Models\Order;
-use App\Models\StatOrder;
+use App\Models\Stat;
 use App\Models\CommissionLog;
+use Illuminate\Support\Facades\DB;
 
 class V2boardStatistics extends Command
 {
@@ -40,38 +45,87 @@ class V2boardStatistics extends Command
      */
     public function handle()
     {
+        $startAt = microtime(true);
         ini_set('memory_limit', -1);
-        $this->statOrder();
+        $this->statUser();
+        $this->statServer();
+        $this->stat();
+        $this->info('耗时' . (microtime(true) - $startAt));
     }
 
-    private function statOrder()
+    private function statServer()
+    {
+        $createdAt = time();
+        $recordAt = strtotime('-1 day', strtotime(date('Y-m-d')));
+        $statService = new StatisticalService();
+        $statService->setStartAt($recordAt);
+        $statService->setServerStats();
+        $stats = $statService->getStatServer();
+        DB::beginTransaction();
+        foreach ($stats as $stat) {
+            if (!StatServer::insert([
+                'server_id' => $stat['server_id'],
+                'server_type' => $stat['server_type'],
+                'u' => $stat['u'],
+                'd' => $stat['d'],
+                'created_at' => $createdAt,
+                'updated_at' => $createdAt,
+                'record_type' => 'd',
+                'record_at' => $recordAt
+            ])) {
+                DB::rollback();
+                throw new \Exception('stat server fail');
+            }
+        }
+        DB::commit();
+        $statService->clearStatServer();
+    }
+
+    private function statUser()
+    {
+        $createdAt = time();
+        $recordAt = strtotime('-1 day', strtotime(date('Y-m-d')));
+        $statService = new StatisticalService();
+        $statService->setStartAt($recordAt);
+        $statService->setUserStats();
+        $stats = $statService->getStatUser();
+        DB::beginTransaction();
+        foreach ($stats as $stat) {
+            if (!StatUser::insert([
+                'user_id' => $stat['user_id'],
+                'u' => $stat['u'],
+                'd' => $stat['d'],
+                'server_rate' => $stat['server_rate'],
+                'created_at' => $createdAt,
+                'updated_at' => $createdAt,
+                'record_type' => 'd',
+                'record_at' => $recordAt
+            ])) {
+                DB::rollback();
+                throw new \Exception('stat user fail');
+            }
+        }
+        DB::commit();
+        $statService->clearStatUser();
+    }
+
+    private function stat()
     {
         $endAt = strtotime(date('Y-m-d'));
         $startAt = strtotime('-1 day', $endAt);
-        $orderBuilder = Order::where('paid_at', '>=', $startAt)
-            ->where('paid_at', '<', $endAt)
-            ->whereNotIn('status', [0, 2]);
-        $orderCount = $orderBuilder->count();
-        $orderAmount = $orderBuilder->sum('total_amount');
-        $commissionLogBuilder = CommissionLog::where('created_at', '>=', $startAt)
-            ->where('created_at', '<', $endAt);
-        $commissionCount = $commissionLogBuilder->count();
-        $commissionAmount = $commissionLogBuilder->sum('get_amount');
-        $data = [
-            'order_count' => $orderCount,
-            'order_amount' => $orderAmount,
-            'commission_count' => $commissionCount,
-            'commission_amount' => $commissionAmount,
-            'record_type' => 'd',
-            'record_at' => $startAt
-        ];
-        $statistic = StatOrder::where('record_at', $startAt)
+        $statisticalService = new StatisticalService();
+        $statisticalService->setStartAt($startAt);
+        $statisticalService->setEndAt($endAt);
+        $data = $statisticalService->generateStatData();
+        $data['record_at'] = $startAt;
+        $data['record_type'] = 'd';
+        $statistic = Stat::where('record_at', $startAt)
             ->where('record_type', 'd')
             ->first();
         if ($statistic) {
             $statistic->update($data);
             return;
         }
-        StatOrder::create($data);
+        Stat::create($data);
     }
 }

+ 1 - 3
app/Exceptions/Handler.php

@@ -54,9 +54,7 @@ class Handler extends ExceptionHandler
     public function render($request, Throwable $exception)
     {
         if ($exception instanceof ViewException) {
-            return response([
-                'message' => "主题初始化发生错误,请在后台对主题检查或配置后重试。"
-            ]);
+            abort(500, "主题渲染失败。如更新主题,参数可能发生变化请重新配置主题后再试。");
         }
         return parent::render($request, $exception);
     }

+ 1 - 0
app/Http/Controllers/Admin/CouponController.php

@@ -82,6 +82,7 @@ class CouponController extends Controller
         $coupons = [];
         $coupon = $request->validated();
         $coupon['created_at'] = $coupon['updated_at'] = time();
+        $coupon['show'] = 1;
         unset($coupon['generate_count']);
         for ($i = 0;$i < $request->input('generate_count');$i++) {
             $coupon['code'] = Helper::randomChar(8);

+ 113 - 0
app/Http/Controllers/Admin/Server/HysteriaController.php

@@ -0,0 +1,113 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Server;
+
+use App\Http\Requests\Admin\ServerVmessSave;
+use App\Http\Requests\Admin\ServerVmessUpdate;
+use App\Models\ServerHysteria;
+use App\Services\ServerService;
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\ServerVmess;
+
+class HysteriaController extends Controller
+{
+    public function save(Request $request)
+    {
+        $params = $request->validate([
+            'show' => '',
+            'name' => 'required',
+            'group_id' => 'required|array',
+            'route_id' => 'nullable|array',
+            'parent_id' => 'nullable|integer',
+            'host' => 'required',
+            'port' => 'required',
+            'server_port' => 'required',
+            'tags' => 'nullable|array',
+            'rate' => 'required|numeric',
+            'up_mbps' => 'required|numeric|min:1',
+            'down_mbps' => 'required|numeric|min:1',
+            'server_name' => 'nullable',
+            'insecure' => 'required|in:0,1'
+        ]);
+
+        if ($request->input('id')) {
+            $server = ServerHysteria::find($request->input('id'));
+            if (!$server) {
+                abort(500, '服务器不存在');
+            }
+            try {
+                $server->update($params);
+            } catch (\Exception $e) {
+                abort(500, '保存失败');
+            }
+            return response([
+                'data' => true
+            ]);
+        }
+
+        if (!ServerHysteria::create($params)) {
+            abort(500, '创建失败');
+        }
+
+        return response([
+            'data' => true
+        ]);
+    }
+
+    public function drop(Request $request)
+    {
+        if ($request->input('id')) {
+            $server = ServerHysteria::find($request->input('id'));
+            if (!$server) {
+                abort(500, '节点ID不存在');
+            }
+        }
+        return response([
+            'data' => $server->delete()
+        ]);
+    }
+
+    public function update(Request $request)
+    {
+        $request->validate([
+            'show' => 'in:0,1'
+        ], [
+            'show.in' => '显示状态格式不正确'
+        ]);
+        $params = $request->only([
+            'show',
+        ]);
+
+        $server = ServerHysteria::find($request->input('id'));
+
+        if (!$server) {
+            abort(500, '该服务器不存在');
+        }
+        try {
+            $server->update($params);
+        } catch (\Exception $e) {
+            abort(500, '保存失败');
+        }
+
+        return response([
+            'data' => true
+        ]);
+    }
+
+    public function copy(Request $request)
+    {
+        $server = ServerHysteria::find($request->input('id'));
+        $server->show = 0;
+        if (!$server) {
+            abort(500, '服务器不存在');
+        }
+        if (!ServerHysteria::create($server->toArray())) {
+            abort(500, '复制失败');
+        }
+
+        return response([
+            'data' => true
+        ]);
+    }
+}

+ 13 - 23
app/Http/Controllers/Admin/Server/ManageController.php

@@ -2,9 +2,6 @@
 
 namespace App\Http\Controllers\Admin\Server;
 
-use App\Models\ServerVmess;
-use App\Models\ServerShadowsocks;
-use App\Models\ServerTrojan;
 use App\Services\ServerService;
 use Illuminate\Http\Request;
 use App\Http\Controllers\Controller;
@@ -23,27 +20,20 @@ class ManageController extends Controller
     public function sort(Request $request)
     {
         ini_set('post_max_size', '1m');
+        $params = $request->only(
+                'shadowsocks',
+                'vmess',
+                'trojan',
+                'hysteria'
+            ) ?? [];
         DB::beginTransaction();
-        foreach ($request->input('sorts') ?? [] as $k => $v) {
-            switch ($v['key']) {
-                case 'shadowsocks':
-                    if (!ServerShadowsocks::find($v['value'])->update(['sort' => $v['sort']])) {
-                        DB::rollBack();
-                        abort(500, '保存失败');
-                    }
-                    break;
-                case 'vmess':
-                    if (!ServerVmess::find($v['value'])->update(['sort' => $v['sort']])) {
-                        DB::rollBack();
-                        abort(500, '保存失败');
-                    }
-                    break;
-                case 'trojan':
-                    if (!ServerTrojan::find($v['value'])->update(['sort' => $v['sort']])) {
-                        DB::rollBack();
-                        abort(500, '保存失败');
-                    }
-                    break;
+        foreach ($params as $k => $v) {
+            $model = 'App\\Models\\Server' . ucfirst($k);
+            foreach($v as $id => $sort) {
+                if (!$model::find($id)->update(['sort' => $sort])) {
+                    DB::rollBack();
+                    abort(500, '保存失败');
+                }
             }
         }
         DB::commit();

+ 96 - 25
app/Http/Controllers/Admin/StatController.php

@@ -7,6 +7,7 @@ use App\Models\ServerShadowsocks;
 use App\Models\ServerTrojan;
 use App\Models\StatUser;
 use App\Services\ServerService;
+use App\Services\StatisticalService;
 use Illuminate\Http\Request;
 use App\Http\Controllers\Controller;
 use App\Models\ServerGroup;
@@ -15,16 +16,85 @@ use App\Models\Plan;
 use App\Models\User;
 use App\Models\Ticket;
 use App\Models\Order;
-use App\Models\StatOrder;
+use App\Models\Stat;
 use App\Models\StatServer;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\DB;
 
 class StatController extends Controller
 {
+    public function getStat(Request $request)
+    {
+        $params = $request->validate([
+            'start_at' => '',
+            'end_at' => ''
+        ]);
+
+        if (isset($params['start_at']) && isset($params['end_at'])) {
+            $stats = Stat::where('record_at', '>=', $params['start_at'])
+                ->where('record_at', '<', $params['end_at'])
+                ->get()
+                ->makeHidden(['record_at', 'created_at', 'updated_at', 'id', 'record_type'])
+                ->toArray();
+        } else {
+            $statisticalService = new StatisticalService();
+            return [
+                'data' => $statisticalService->generateStatData()
+            ];
+        }
+
+        $stats = array_reduce($stats, function($carry, $item) {
+            foreach($item as $key => $value) {
+                if(isset($carry[$key]) && $carry[$key]) {
+                    $carry[$key] += $value;
+                } else {
+                    $carry[$key] = $value;
+                }
+            }
+            return $carry;
+        }, []);
+
+        return [
+            'data' => $stats
+        ];
+    }
+
+    public function getStatRecord(Request $request)
+    {
+        $request->validate([
+            'type' => 'required|in:paid_total,commission_total,register_count',
+            'start_at' => '',
+            'end_at' => ''
+        ]);
+
+        $statisticalService = new StatisticalService();
+        $statisticalService->setStartAt($request->input('start_at'));
+        $statisticalService->setEndAt($request->input('end_at'));
+        return [
+            'data' => $statisticalService->getStatRecord($request->input('type'))
+        ];
+    }
+
+    public function getRanking(Request $request)
+    {
+        $request->validate([
+            'type' => 'required|in:server_traffic_rank,user_consumption_rank,invite_rank',
+            'start_at' => '',
+            'end_at' => '',
+            'limit' => 'nullable|integer'
+        ]);
+
+        $statisticalService = new StatisticalService();
+        $statisticalService->setStartAt($request->input('start_at'));
+        $statisticalService->setEndAt($request->input('end_at'));
+        return [
+            'data' => $statisticalService->getRanking($request->input('type'), $request->input('limit') ?? 20)
+        ];
+    }
+
     public function getOverride(Request $request)
     {
-        return response([
+        return [
             'data' => [
                 'month_income' => Order::where('created_at', '>=', strtotime(date('Y-m-1')))
                     ->where('created_at', '<', time())
@@ -55,12 +125,12 @@ class StatController extends Controller
                     ->where('created_at', '<', strtotime(date('Y-m-1')))
                     ->sum('get_amount'),
             ]
-        ]);
+        ];
     }
 
     public function getOrder(Request $request)
     {
-        $statistics = StatOrder::where('record_type', 'd')
+        $statistics = Stat::where('record_type', 'd')
             ->limit(31)
             ->orderBy('record_at', 'DESC')
             ->get()
@@ -68,31 +138,31 @@ class StatController extends Controller
         $result = [];
         foreach ($statistics as $statistic) {
             $date = date('m-d', $statistic['record_at']);
-            array_push($result, [
+            $result[] = [
                 'type' => '收款金额',
                 'date' => $date,
-                'value' => $statistic['order_amount'] / 100
-            ]);
-            array_push($result, [
+                'value' => $statistic['paid_total'] / 100
+            ];
+            $result[] = [
                 'type' => '收款笔数',
                 'date' => $date,
-                'value' => $statistic['order_count']
-            ]);
-            array_push($result, [
+                'value' => $statistic['paid_count']
+            ];
+            $result[] = [
                 'type' => '佣金金额(已发放)',
                 'date' => $date,
-                'value' => $statistic['commission_amount'] / 100
-            ]);
-            array_push($result, [
+                'value' => $statistic['commission_total'] / 100
+            ];
+            $result[] = [
                 'type' => '佣金笔数(已发放)',
                 'date' => $date,
                 'value' => $statistic['commission_count']
-            ]);
+            ];
         }
         $result = array_reverse($result);
-        return response([
+        return [
             'data' => $result
-        ]);
+        ];
     }
 
     public function getServerLastRank()
@@ -106,12 +176,12 @@ class StatController extends Controller
         $startAt = strtotime('-1 day', strtotime(date('Y-m-d')));
         $endAt = strtotime(date('Y-m-d'));
         $statistics = StatServer::select([
-                'server_id',
-                'server_type',
-                'u',
-                'd',
-                DB::raw('(u+d) as total')
-            ])
+            'server_id',
+            'server_type',
+            'u',
+            'd',
+            DB::raw('(u+d) as total')
+        ])
             ->where('record_at', '>=', $startAt)
             ->where('record_at', '<', $endAt)
             ->where('record_type', 'd')
@@ -128,9 +198,9 @@ class StatController extends Controller
             $statistics[$k]['total'] = $statistics[$k]['total'] / 1073741824;
         }
         array_multisort(array_column($statistics, 'total'), SORT_DESC, $statistics);
-        return response([
+        return [
             'data' => $statistics
-        ]);
+        ];
     }
 
     public function getStatUser(Request $request)
@@ -150,5 +220,6 @@ class StatController extends Controller
             'total' => $total
         ];
     }
+
 }
 

+ 2 - 12
app/Http/Controllers/Admin/SystemController.php

@@ -2,20 +2,9 @@
 
 namespace App\Http\Controllers\Admin;
 
-use App\Models\ServerShadowsocks;
-use App\Models\ServerTrojan;
-use App\Services\ServerService;
 use App\Utils\CacheKey;
 use Illuminate\Http\Request;
 use App\Http\Controllers\Controller;
-use App\Models\ServerGroup;
-use App\Models\ServerVmess;
-use App\Models\Plan;
-use App\Models\User;
-use App\Models\Ticket;
-use App\Models\Order;
-use App\Models\StatOrder;
-use App\Models\StatServer;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Http;
@@ -33,7 +22,8 @@ class SystemController extends Controller
         return response([
             'data' => [
                 'schedule' => $this->getScheduleStatus(),
-                'horizon' => $this->getHorizonStatus()
+                'horizon' => $this->getHorizonStatus(),
+                'schedule_last_runtime' => Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null))
             ]
         ]);
     }

+ 8 - 9
app/Http/Controllers/Client/ClientController.php

@@ -2,9 +2,10 @@
 
 namespace App\Http\Controllers\Client;
 
-use App\Http\Controllers\Client\Protocols\V2rayN;
+use App\Http\Controllers\Client\Protocols\General;
 use App\Http\Controllers\Controller;
 use App\Services\ServerService;
+use App\Utils\Helper;
 use Illuminate\Http\Request;
 use App\Services\UserService;
 
@@ -23,7 +24,7 @@ class ClientController extends Controller
             $servers = $serverService->getAvailableServers($user);
             $this->setSubscribeInfoToServers($servers, $user);
             if ($flag) {
-                foreach (glob(app_path('Http//Controllers//Client//Protocols') . '/*.php') as $file) {
+                foreach (array_reverse(glob(app_path('Http//Controllers//Client//Protocols') . '/*.php')) as $file) {
                     $file = 'App\\Http\\Controllers\\Client\\Protocols\\' . basename($file, '.php');
                     $class = new $file($user, $servers);
                     if (strpos($flag, $class->flag) !== false) {
@@ -31,10 +32,8 @@ class ClientController extends Controller
                     }
                 }
             }
-            // todo 1.5.3 remove
-            $class = new V2rayN($user, $servers);
+            $class = new General($user, $servers);
             die($class->handle());
-            die('该客户端暂不支持进行订阅');
         }
     }
 
@@ -42,9 +41,9 @@ class ClientController extends Controller
     {
         if (!isset($servers[0])) return;
         if (!(int)config('v2board.show_info_to_server_enable', 0)) return;
-        $useTraffic = round($user['u'] / (1024*1024*1024), 2) + round($user['d'] / (1024*1024*1024), 2);
-        $totalTraffic = round($user['transfer_enable'] / (1024*1024*1024), 2);
-        $remainingTraffic = $totalTraffic - $useTraffic;
+        $useTraffic = $user['u'] + $user['d'];
+        $totalTraffic = $user['transfer_enable'];
+        $remainingTraffic = Helper::trafficConvert($totalTraffic - $useTraffic);
         $expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : '长期有效';
         $userService = new UserService();
         $resetDay = $userService->getResetDay($user);
@@ -57,7 +56,7 @@ class ClientController extends Controller
             ]));
         }
         array_unshift($servers, array_merge($servers[0], [
-            'name' => "剩余流量:{$remainingTraffic} GB",
+            'name' => "剩余流量:{$remainingTraffic}",
         ]));
     }
 }

+ 10 - 8
app/Http/Controllers/Client/Protocols/Clash.php

@@ -34,7 +34,6 @@ class Clash
         } else {
             $config = Yaml::parseFile($defaultConfig);
         }
-        $this->patch($config);
         $proxy = [];
         $proxies = [];
 
@@ -78,6 +77,11 @@ class Clash
             if ($isFilter) continue;
             $config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies);
         }
+
+        $config['proxy-groups'] = array_filter($config['proxy-groups'], function($group) {
+            return $group['proxies'];
+        });
+        $config['proxy-groups'] = array_values($config['proxy-groups']);
         // Force the current subscription domain to be a direct rule
         $subsDomain = $_SERVER['HTTP_HOST'];
         if ($subsDomain) {
@@ -124,6 +128,11 @@ class Clash
                     $array['servername'] = $tlsSettings['serverName'];
             }
         }
+        if ($server['network'] === 'tcp') {
+            $tcpSettings = $server['networkSettings'];
+            if (isset($tcpSettings['header']['type'])) $array['network'] = $tcpSettings['header']['type'];
+            if (isset($tcpSettings['header']['request']['path'][0])) $array['http-opts']['path'] = $tcpSettings['header']['request']['path'][0];
+        }
         if ($server['network'] === 'ws') {
             $array['network'] = 'ws';
             if ($server['networkSettings']) {
@@ -174,11 +183,4 @@ class Clash
     {
         return @preg_match($exp, null) !== false;
     }
-
-    private function patch(&$config)
-    {
-        // fix clash x dns mode
-        preg_match('#(ClashX)[/ ]([0-9.]*)#', $_SERVER['HTTP_USER_AGENT'], $matches);
-        if (isset($matches[2]) && $matches[2] < '1.96.2') $config['dns']['enhanced-mode'] = 'redir-host';
-    }
 }

+ 12 - 3
app/Http/Controllers/Client/Protocols/ClashMeta.php

@@ -7,7 +7,7 @@ use Symfony\Component\Yaml\Yaml;
 
 class ClashMeta
 {
-    public $flag = 'clashmeta';
+    public $flag = 'meta';
     private $servers;
     private $user;
 
@@ -68,6 +68,10 @@ class ClashMeta
             if ($isFilter) continue;
             $config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies);
         }
+        $config['proxy-groups'] = array_filter($config['proxy-groups'], function($group) {
+            return $group['proxies'];
+        });
+        $config['proxy-groups'] = array_values($config['proxy-groups']);
         // Force the current subscription domain to be a direct rule
         $subsDomain = $_SERVER['HTTP_HOST'];
         if ($subsDomain) {
@@ -82,12 +86,12 @@ class ClashMeta
     public static function buildShadowsocks($password, $server)
     {
         if ($server['cipher'] === '2022-blake3-aes-128-gcm') {
-            $serverKey = Helper::getShadowsocksServerKey($server['created_at'], 16);
+            $serverKey = Helper::getServerKey($server['created_at'], 16);
             $userKey = Helper::uuidToBase64($password, 16);
             $password = "{$serverKey}:{$userKey}";
         }
         if ($server['cipher'] === '2022-blake3-aes-256-gcm') {
-            $serverKey = Helper::getShadowsocksServerKey($server['created_at'], 32);
+            $serverKey = Helper::getServerKey($server['created_at'], 32);
             $userKey = Helper::uuidToBase64($password, 32);
             $password = "{$serverKey}:{$userKey}";
         }
@@ -124,6 +128,11 @@ class ClashMeta
                     $array['servername'] = $tlsSettings['serverName'];
             }
         }
+        if ($server['network'] === 'tcp') {
+            $tcpSettings = $server['networkSettings'];
+            if (isset($tcpSettings['header']['type'])) $array['network'] = $tcpSettings['header']['type'];
+            if (isset($tcpSettings['header']['request']['path'][0])) $array['http-opts']['path'] = $tcpSettings['header']['request']['path'][0];
+        }
         if ($server['network'] === 'ws') {
             $array['network'] = 'ws';
             if ($server['networkSettings']) {

+ 113 - 0
app/Http/Controllers/Client/Protocols/General.php

@@ -0,0 +1,113 @@
+<?php
+
+namespace App\Http\Controllers\Client\Protocols;
+
+
+use App\Utils\Helper;
+
+class General
+{
+    public $flag = 'general';
+    private $servers;
+    private $user;
+
+    public function __construct($user, $servers)
+    {
+        $this->user = $user;
+        $this->servers = $servers;
+    }
+
+    public function handle()
+    {
+        $servers = $this->servers;
+        $user = $this->user;
+        $uri = '';
+
+        foreach ($servers as $item) {
+            if ($item['type'] === 'vmess') {
+                $uri .= self::buildVmess($user['uuid'], $item);
+            }
+            if ($item['type'] === 'shadowsocks') {
+                $uri .= self::buildShadowsocks($user['uuid'], $item);
+            }
+            if ($item['type'] === 'trojan') {
+                $uri .= self::buildTrojan($user['uuid'], $item);
+            }
+        }
+        return base64_encode($uri);
+    }
+
+    public static function buildShadowsocks($password, $server)
+    {
+        if ($server['cipher'] === '2022-blake3-aes-128-gcm') {
+            $serverKey = Helper::getServerKey($server['created_at'], 16);
+            $userKey = Helper::uuidToBase64($password, 16);
+            $password = "{$serverKey}:{$userKey}";
+        }
+        if ($server['cipher'] === '2022-blake3-aes-256-gcm') {
+            $serverKey = Helper::getServerKey($server['created_at'], 32);
+            $userKey = Helper::uuidToBase64($password, 32);
+            $password = "{$serverKey}:{$userKey}";
+        }
+        $name = rawurlencode($server['name']);
+        $str = str_replace(
+            ['+', '/', '='],
+            ['-', '_', ''],
+            base64_encode("{$server['cipher']}:{$password}")
+        );
+        return "ss://{$str}@{$server['host']}:{$server['port']}#{$name}\r\n";
+    }
+
+    public static function buildVmess($uuid, $server)
+    {
+        $config = [
+            "v" => "2",
+            "ps" => $server['name'],
+            "add" => $server['host'],
+            "port" => (string)$server['port'],
+            "id" => $uuid,
+            "aid" => '0',
+            "net" => $server['network'],
+            "type" => "none",
+            "host" => "",
+            "path" => "",
+            "tls" => $server['tls'] ? "tls" : "",
+        ];
+        if ($server['tls']) {
+            if ($server['tlsSettings']) {
+                $tlsSettings = $server['tlsSettings'];
+                if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
+                    $config['sni'] = $tlsSettings['serverName'];
+            }
+        }
+        if ((string)$server['network'] === 'tcp') {
+            $tcpSettings = $server['networkSettings'];
+            if (isset($tcpSettings['header']['type'])) $config['type'] = $tcpSettings['header']['type'];
+            if (isset($tcpSettings['header']['request']['path'][0])) $config['path'] = $tcpSettings['header']['request']['path'][0];
+        }
+        if ((string)$server['network'] === 'ws') {
+            $wsSettings = $server['networkSettings'];
+            if (isset($wsSettings['path'])) $config['path'] = $wsSettings['path'];
+            if (isset($wsSettings['headers']['Host'])) $config['host'] = $wsSettings['headers']['Host'];
+        }
+        if ((string)$server['network'] === 'grpc') {
+            $grpcSettings = $server['networkSettings'];
+            if (isset($grpcSettings['serviceName'])) $config['path'] = $grpcSettings['serviceName'];
+        }
+        return "vmess://" . base64_encode(json_encode($config)) . "\r\n";
+    }
+
+    public static function buildTrojan($password, $server)
+    {
+        $name = rawurlencode($server['name']);
+        $query = http_build_query([
+            'allowInsecure' => $server['allow_insecure'],
+            'peer' => $server['server_name'],
+            'sni' => $server['server_name']
+        ]);
+        $uri = "trojan://{$password}@{$server['host']}:{$server['port']}?{$query}#{$name}";
+        $uri .= "\r\n";
+        return $uri;
+    }
+
+}

+ 137 - 0
app/Http/Controllers/Client/Protocols/Loon.php

@@ -0,0 +1,137 @@
+<?php
+
+namespace App\Http\Controllers\Client\Protocols;
+
+use App\Utils\Helper;
+
+class Loon
+{
+    public $flag = 'loon';
+    private $servers;
+    private $user;
+
+    public function __construct($user, $servers)
+    {
+        $this->user = $user;
+        $this->servers = $servers;
+    }
+
+    public function handle()
+    {
+        $servers = $this->servers;
+        $user = $this->user;
+
+        $uri = '';
+        header("Subscription-Userinfo: upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}");
+
+        foreach ($servers as $item) {
+            if ($item['type'] === 'shadowsocks'
+                && in_array($item['cipher'], [
+                    'aes-128-gcm',
+                    'aes-192-gcm',
+                    'aes-256-gcm',
+                    'chacha20-ietf-poly1305'
+                ])
+            ) {
+                $uri .= self::buildShadowsocks($user['uuid'], $item);
+            }
+            if ($item['type'] === 'vmess') {
+                $uri .= self::buildVmess($user['uuid'], $item);
+            }
+            if ($item['type'] === 'trojan') {
+                $uri .= self::buildTrojan($user['uuid'], $item);
+            }
+        }
+        return $uri;
+    }
+
+
+    public static function buildShadowsocks($password, $server)
+    {
+        $config = [
+            "{$server['name']}=Shadowsocks",
+            "{$server['host']}",
+            "{$server['port']}",
+            "{$server['cipher']}",
+            "{$password}",
+            'fast-open=false',
+            'udp=true'
+        ];
+        $config = array_filter($config);
+        $uri = implode(',', $config);
+        $uri .= "\r\n";
+        return $uri;
+    }
+
+    public static function buildVmess($uuid, $server)
+    {
+        $config = [
+            "{$server['name']}=vmess",
+            "{$server['host']}",
+            "{$server['port']}",
+            'auto',
+            "{$uuid}",
+            'fast-open=false',
+            'udp=true',
+            "alterId=0"
+        ];
+
+        if ($server['network'] === 'tcp') {
+            array_push($config, 'transport=tcp');
+            if ($server['networkSettings']) {
+                $tcpSettings = $server['networkSettings'];
+                if (isset($tcpSettings['header']['type']) && !empty($tcpSettings['header']['type']))
+                    $config = str_replace('transport=tcp', "transport={$tcpSettings['header']['type']}", $config);
+                if (isset($tcpSettings['header']['request']['path'][0]) && !empty($tcpSettings['header']['request']['path'][0]))
+                    array_push($config, "path={$tcpSettings['header']['request']['path'][0]}");
+                if (isset($tcpSettings['header']['Host']) && !empty($tcpSettings['header']['Host']))
+                    array_push($config, "host={$tcpSettings['header']['Host']}");
+            }
+        }
+        if ($server['tls']) {
+            if ($server['network'] === 'tcp')
+                array_push($config, 'over-tls=true');
+            if ($server['tlsSettings']) {
+                $tlsSettings = $server['tlsSettings'];
+                if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure']))
+                    array_push($config, 'skip-cert-verify=' . ($tlsSettings['allowInsecure'] ? 'true' : 'false'));
+                if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
+                    array_push($config, "tls-name={$tlsSettings['serverName']}");
+            }
+        }
+        if ($server['network'] === 'ws') {
+            array_push($config, 'transport=ws');
+            if ($server['networkSettings']) {
+                $wsSettings = $server['networkSettings'];
+                if (isset($wsSettings['path']) && !empty($wsSettings['path']))
+                    array_push($config, "path={$wsSettings['path']}");
+                if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
+                    array_push($config, "host={$wsSettings['headers']['Host']}");
+            }
+        }
+
+        $uri = implode(',', $config);
+        $uri .= "\r\n";
+        return $uri;
+    }
+
+    public static function buildTrojan($password, $server)
+    {
+        $config = [
+            "{$server['name']}=trojan",
+            "{$server['host']}",
+            "{$server['port']}",
+            "{$password}",
+            $server['server_name'] ? "tls-name={$server['server_name']}" : "",
+            'fast-open=false',
+            'udp=true'
+        ];
+        if (!empty($server['allow_insecure'])) {
+            array_push($config, $server['allow_insecure'] ? 'skip-cert-verify=true' : 'skip-cert-verify=false');
+        }
+        $config = array_filter($config);
+        $uri = implode(',', $config);
+        $uri .= "\r\n";
+        return $uri;
+    }
+}

+ 5 - 0
app/Http/Controllers/Client/Protocols/Passwall.php

@@ -68,6 +68,11 @@ class Passwall
                     $config['sni'] = $tlsSettings['serverName'];
             }
         }
+        if ((string)$server['network'] === 'tcp') {
+            $tcpSettings = $server['networkSettings'];
+            if (isset($tcpSettings['header']['type'])) $config['type'] = $tcpSettings['header']['type'];
+            if (isset($tcpSettings['header']['request']['path'][0])) $config['path'] = $tcpSettings['header']['request']['path'][0];
+        }
         if ((string)$server['network'] === 'ws') {
             $wsSettings = $server['networkSettings'];
             if (isset($wsSettings['path'])) $config['path'] = $wsSettings['path'];

+ 5 - 0
app/Http/Controllers/Client/Protocols/SagerNet.php

@@ -72,6 +72,11 @@ class SagerNet
                     $config['sni'] = urlencode($tlsSettings['serverName']);
             }
         }
+        if ((string)$server['network'] === 'tcp') {
+            $tcpSettings = $server['networkSettings'];
+            if (isset($tcpSettings['header']['type'])) $config['type'] = $tcpSettings['header']['type'];
+            if (isset($tcpSettings['header']['request']['path'][0])) $config['path'] = $tcpSettings['header']['request']['path'][0];
+        }
         if ((string)$server['network'] === 'ws') {
             $wsSettings = $server['networkSettings'];
             if (isset($wsSettings['path'])) $config['path'] = $wsSettings['path'];

+ 11 - 2
app/Http/Controllers/Client/Protocols/Shadowrocket.php

@@ -46,12 +46,12 @@ class Shadowrocket
     public static function buildShadowsocks($password, $server)
     {
         if ($server['cipher'] === '2022-blake3-aes-128-gcm') {
-            $serverKey = Helper::getShadowsocksServerKey($server['created_at'], 16);
+            $serverKey = Helper::getServerKey($server['created_at'], 16);
             $userKey = Helper::uuidToBase64($password, 16);
             $password = "{$serverKey}:{$userKey}";
         }
         if ($server['cipher'] === '2022-blake3-aes-256-gcm') {
-            $serverKey = Helper::getShadowsocksServerKey($server['created_at'], 32);
+            $serverKey = Helper::getServerKey($server['created_at'], 32);
             $userKey = Helper::uuidToBase64($password, 32);
             $password = "{$serverKey}:{$userKey}";
         }
@@ -82,6 +82,15 @@ class Shadowrocket
                     $config['peer'] = $tlsSettings['serverName'];
             }
         }
+        if ($server['network'] === 'tcp') {
+            if ($server['networkSettings']) {
+                $tcpSettings = $server['networkSettings'];
+                if (isset($tcpSettings['header']['type']) && !empty($tcpSettings['header']['type']))
+                    $config['obfs'] = $tcpSettings['header']['type'];
+                if (isset($tcpSettings['header']['request']['path'][0]) && !empty($tcpSettings['header']['request']['path'][0]))
+                    $config['path'] = $tcpSettings['header']['request']['path'][0];
+            }
+        }
         if ($server['network'] === 'ws') {
             $config['obfs'] = "websocket";
             if ($server['networkSettings']) {

+ 9 - 0
app/Http/Controllers/Client/Protocols/Stash.php

@@ -75,6 +75,10 @@ class Stash
             if ($isFilter) continue;
             $config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies);
         }
+        $config['proxy-groups'] = array_filter($config['proxy-groups'], function($group) {
+            return $group['proxies'];
+        });
+        $config['proxy-groups'] = array_values($config['proxy-groups']);
         // Force the current subscription domain to be a direct rule
         $subsDomain = $_SERVER['HTTP_HOST'];
         if ($subsDomain) {
@@ -121,6 +125,11 @@ class Stash
                     $array['servername'] = $tlsSettings['serverName'];
             }
         }
+        if ($server['network'] === 'tcp') {
+            $tcpSettings = $server['networkSettings'];
+            if (isset($tcpSettings['header']['type'])) $array['network'] = $tcpSettings['header']['type'];
+            if (isset($tcpSettings['header']['request']['path'][0])) $array['http-opts']['path'] = $tcpSettings['header']['request']['path'][0];
+        }
         if ($server['network'] === 'ws') {
             $array['network'] = 'ws';
             if ($server['networkSettings']) {

+ 7 - 2
app/Http/Controllers/Client/Protocols/V2rayN.php

@@ -40,12 +40,12 @@ class V2rayN
     public static function buildShadowsocks($password, $server)
     {
         if ($server['cipher'] === '2022-blake3-aes-128-gcm') {
-            $serverKey = Helper::getShadowsocksServerKey($server['created_at'], 16);
+            $serverKey = Helper::getServerKey($server['created_at'], 16);
             $userKey = Helper::uuidToBase64($password, 16);
             $password = "{$serverKey}:{$userKey}";
         }
         if ($server['cipher'] === '2022-blake3-aes-256-gcm') {
-            $serverKey = Helper::getShadowsocksServerKey($server['created_at'], 32);
+            $serverKey = Helper::getServerKey($server['created_at'], 32);
             $userKey = Helper::uuidToBase64($password, 32);
             $password = "{$serverKey}:{$userKey}";
         }
@@ -80,6 +80,11 @@ class V2rayN
                     $config['sni'] = $tlsSettings['serverName'];
             }
         }
+        if ((string)$server['network'] === 'tcp') {
+            $tcpSettings = $server['networkSettings'];
+            if (isset($tcpSettings['header']['type'])) $config['type'] = $tcpSettings['header']['type'];
+            if (isset($tcpSettings['header']['request']['path'][0])) $config['path'] = $tcpSettings['header']['request']['path'][0];
+        }
         if ((string)$server['network'] === 'ws') {
             $wsSettings = $server['networkSettings'];
             if (isset($wsSettings['path'])) $config['path'] = $wsSettings['path'];

+ 5 - 0
app/Http/Controllers/Client/Protocols/V2rayNG.php

@@ -68,6 +68,11 @@ class V2rayNG
                     $config['sni'] = $tlsSettings['serverName'];
             }
         }
+        if ((string)$server['network'] === 'tcp') {
+            $tcpSettings = $server['networkSettings'];
+            if (isset($tcpSettings['header']['type'])) $config['type'] = $tcpSettings['header']['type'];
+            if (isset($tcpSettings['header']['request']['path'][0])) $config['path'] = $tcpSettings['header']['request']['path'][0];
+        }
         if ((string)$server['network'] === 'ws') {
             $wsSettings = $server['networkSettings'];
             if (isset($wsSettings['path'])) $config['path'] = $wsSettings['path'];

+ 2 - 2
app/Http/Controllers/Passport/AuthController.php

@@ -116,7 +116,7 @@ class AuthController extends Controller
             if (empty($request->input('email_code'))) {
                 abort(500, __('Email verification code cannot be empty'));
             }
-            if (Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== $request->input('email_code')) {
+            if ((string)Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== (string)$request->input('email_code')) {
                 abort(500, __('Incorrect email verification code'));
             }
         }
@@ -286,7 +286,7 @@ class AuthController extends Controller
 
     public function forget(AuthForget $request)
     {
-        if (Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== $request->input('email_code')) {
+        if ((string)Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== (string)$request->input('email_code')) {
             abort(500, __('Incorrect email verification code'));
         }
         $user = User::where('email', $request->input('email'))->first();

+ 5 - 3
app/Http/Controllers/Server/DeepbworkController.php

@@ -3,6 +3,7 @@
 namespace App\Http\Controllers\Server;
 
 use App\Services\ServerService;
+use App\Services\StatisticalService;
 use App\Services\UserService;
 use App\Utils\CacheKey;
 use Illuminate\Http\Request;
@@ -81,11 +82,12 @@ class DeepbworkController extends Controller
         Cache::put(CacheKey::get('SERVER_VMESS_ONLINE_USER', $server->id), count($data), 3600);
         Cache::put(CacheKey::get('SERVER_VMESS_LAST_PUSH_AT', $server->id), time(), 3600);
         $userService = new UserService();
+        $formatData = [];
+
         foreach ($data as $item) {
-            $u = $item['u'];
-            $d = $item['d'];
-            $userService->trafficFetch($u, $d, $item['user_id'], $server->toArray(), 'vmess');
+            $formatData[$item['user_id']] = [$item['u'], $item['d']];
         }
+        $userService->trafficFetch($server->toArray(), 'vmess', $formatData);
 
         return response([
             'ret' => 1,

+ 5 - 3
app/Http/Controllers/Server/ShadowsocksTidalabController.php

@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Server;
 
 use App\Models\ServerShadowsocks;
 use App\Services\ServerService;
+use App\Services\StatisticalService;
 use App\Services\UserService;
 use App\Utils\CacheKey;
 use Illuminate\Http\Request;
@@ -73,11 +74,12 @@ class ShadowsocksTidalabController extends Controller
         Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_ONLINE_USER', $server->id), count($data), 3600);
         Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_PUSH_AT', $server->id), time(), 3600);
         $userService = new UserService();
+        $formatData = [];
+
         foreach ($data as $item) {
-            $u = $item['u'];
-            $d = $item['d'];
-            $userService->trafficFetch($u, $d, $item['user_id'], $server->toArray(), 'shadowsocks');
+            $formatData[$item['user_id']] = [$item['u'], $item['d']];
         }
+        $userService->trafficFetch($server->toArray(), 'shadowsocks', $formatData);
 
         return response([
             'ret' => 1,

+ 4 - 3
app/Http/Controllers/Server/TrojanTidalabController.php

@@ -3,6 +3,7 @@
 namespace App\Http\Controllers\Server;
 
 use App\Services\ServerService;
+use App\Services\StatisticalService;
 use App\Services\UserService;
 use App\Utils\CacheKey;
 use Illuminate\Http\Request;
@@ -78,11 +79,11 @@ class TrojanTidalabController extends Controller
         Cache::put(CacheKey::get('SERVER_TROJAN_ONLINE_USER', $server->id), count($data), 3600);
         Cache::put(CacheKey::get('SERVER_TROJAN_LAST_PUSH_AT', $server->id), time(), 3600);
         $userService = new UserService();
+        $formatData = [];
         foreach ($data as $item) {
-            $u = $item['u'];
-            $d = $item['d'];
-            $userService->trafficFetch($u, $d, $item['user_id'], $server->toArray(), 'trojan');
+            $formatData[$item['user_id']] = [$item['u'], $item['d']];
         }
+        $userService->trafficFetch($server->toArray(), 'trojan', $formatData);
 
         return response([
             'ret' => 1,

+ 15 - 8
app/Http/Controllers/Server/UniProxyController.php

@@ -3,6 +3,7 @@
 namespace App\Http\Controllers\Server;
 
 use App\Services\ServerService;
+use App\Services\StatisticalService;
 use App\Services\UserService;
 use App\Utils\CacheKey;
 use App\Utils\Helper;
@@ -63,11 +64,7 @@ class UniProxyController extends Controller
         Cache::put(CacheKey::get('SERVER_' . strtoupper($this->nodeType) . '_ONLINE_USER', $this->nodeInfo->id), count($data), 3600);
         Cache::put(CacheKey::get('SERVER_' . strtoupper($this->nodeType) . '_LAST_PUSH_AT', $this->nodeInfo->id), time(), 3600);
         $userService = new UserService();
-        foreach (array_keys($data) as $k) {
-            $u = $data[$k][0];
-            $d = $data[$k][1];
-            $userService->trafficFetch($u, $d, $k, $this->nodeInfo->toArray(), $this->nodeType);
-        }
+        $userService->trafficFetch($this->nodeInfo->toArray(), $this->nodeType, $data);
 
         return response([
             'data' => true
@@ -87,10 +84,10 @@ class UniProxyController extends Controller
                 ];
 
                 if ($this->nodeInfo->cipher === '2022-blake3-aes-128-gcm') {
-                    $response['server_key'] = Helper::getShadowsocksServerKey($this->nodeInfo->created_at, 16);
+                    $response['server_key'] = Helper::getServerKey($this->nodeInfo->created_at, 16);
                 }
                 if ($this->nodeInfo->cipher === '2022-blake3-aes-256-gcm') {
-                    $response['server_key'] = Helper::getShadowsocksServerKey($this->nodeInfo->created_at, 32);
+                    $response['server_key'] = Helper::getServerKey($this->nodeInfo->created_at, 32);
                 }
                 break;
             case 'vmess':
@@ -105,7 +102,17 @@ class UniProxyController extends Controller
                 $response = [
                     'host' => $this->nodeInfo->host,
                     'server_port' => $this->nodeInfo->server_port,
-                    'server_name' => $this->nodeInfo->server_name
+                    'server_name' => $this->nodeInfo->server_name,
+                ];
+                break;
+            case 'hysteria':
+                $response = [
+                    'host' => $this->nodeInfo->host,
+                    'server_port' => $this->nodeInfo->server_port,
+                    'server_name' => $this->nodeInfo->server_name,
+                    'up_mbps' => $this->nodeInfo->up_mbps,
+                    'down_mbps' => $this->nodeInfo->down_mbps,
+                    'obfs' => Helper::getServerKey($this->nodeInfo->created_at, 16)
                 ];
                 break;
         }

+ 1 - 0
app/Http/Controllers/User/OrderController.php

@@ -191,6 +191,7 @@ class OrderController extends Controller
         $payment = Payment::find($method);
         if (!$payment || $payment->enable !== 1) abort(500, __('Payment method is not available'));
         $paymentService = new PaymentService($payment->payment, $payment->id);
+        $order->handling_amount = NULL;
         if ($payment->handling_fee_fixed || $payment->handling_fee_percent) {
             $order->handling_amount = round(($order->total_amount * ($payment->handling_fee_percent / 100)) + $payment->handling_fee_fixed);
         }

+ 1 - 0
app/Http/Kernel.php

@@ -70,6 +70,7 @@ class Kernel extends HttpKernel
         'admin' => \App\Http\Middleware\Admin::class,
         'client' => \App\Http\Middleware\Client::class,
         'staff' => \App\Http\Middleware\Staff::class,
+        'log' => \App\Http\Middleware\RequestLog::class
     ];
 
     /**

+ 24 - 0
app/Http/Middleware/RequestLog.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Closure;
+
+class RequestLog
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param \Illuminate\Http\Request $request
+     * @param \Closure $next
+     * @return mixed
+     */
+    public function handle($request, Closure $next)
+    {
+        if ($request->method() === 'POST') {
+            $path = $request->path();
+            info("POST {$path}");
+        };
+        return $next($request);
+    }
+}

+ 16 - 2
app/Http/Routes/AdminRoute.php

@@ -9,7 +9,7 @@ class AdminRoute
     {
         $router->group([
             'prefix' => config('v2board.secure_path', config('v2board.frontend_admin_path', hash('crc32b', config('app.key')))),
-            'middleware' => 'admin'
+            'middleware' => ['admin', 'log']
         ], function ($router) {
             // Config
             $router->get ('/config/fetch', 'Admin\\ConfigController@fetch');
@@ -64,6 +64,16 @@ class AdminRoute
                 $router->post('copy', 'Admin\\Server\\ShadowsocksController@copy');
                 $router->post('sort', 'Admin\\Server\\ShadowsocksController@sort');
             });
+            $router->group([
+                'prefix' => 'server/hysteria'
+            ], function ($router) {
+                $router->get ('fetch', 'Admin\\Server\\HysteriaController@fetch');
+                $router->post('save', 'Admin\\Server\\HysteriaController@save');
+                $router->post('drop', 'Admin\\Server\\HysteriaController@drop');
+                $router->post('update', 'Admin\\Server\\HysteriaController@update');
+                $router->post('copy', 'Admin\\Server\\HysteriaController@copy');
+                $router->post('sort', 'Admin\\Server\\HysteriaController@sort');
+            });
             // Order
             $router->get ('/order/fetch', 'Admin\\OrderController@fetch');
             $router->post('/order/update', 'Admin\\OrderController@update');
@@ -81,11 +91,14 @@ class AdminRoute
             $router->post('/user/ban', 'Admin\\UserController@ban');
             $router->post('/user/resetSecret', 'Admin\\UserController@resetSecret');
             $router->post('/user/setInviteUser', 'Admin\\UserController@setInviteUser');
-            // StatOrder
+            // Stat
+            $router->get ('/stat/getStat', 'Admin\\StatController@getStat');
             $router->get ('/stat/getOverride', 'Admin\\StatController@getOverride');
             $router->get ('/stat/getServerLastRank', 'Admin\\StatController@getServerLastRank');
             $router->get ('/stat/getOrder', 'Admin\\StatController@getOrder');
             $router->get ('/stat/getStatUser', 'Admin\\StatController@getStatUser');
+            $router->get ('/stat/getRanking', 'Admin\\StatController@getRanking');
+            $router->get ('/stat/getStatRecord', 'Admin\\StatController@getStatRecord');
             // Notice
             $router->get ('/notice/fetch', 'Admin\\NoticeController@fetch');
             $router->post('/notice/save', 'Admin\\NoticeController@save');
@@ -121,6 +134,7 @@ class AdminRoute
             $router->get ('/system/getQueueStats', 'Admin\\SystemController@getQueueStats');
             $router->get ('/system/getQueueWorkload', 'Admin\\SystemController@getQueueWorkload');
             $router->get ('/system/getQueueMasters', '\\Laravel\\Horizon\\Http\\Controllers\\MasterSupervisorController@index');
+            $router->get ('/system/getSystemLog', 'Admin\\SystemController@getSystemLog');
             // Theme
             $router->get ('/theme/getThemes', 'Admin\\ThemeController@getThemes');
             $router->post('/theme/saveThemeConfig', 'Admin\\ThemeController@saveThemeConfig');

+ 2 - 0
app/Jobs/OrderHandleJob.php

@@ -15,6 +15,8 @@ class OrderHandleJob implements ShouldQueue
     use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
     protected $order;
 
+    public $tries = 3;
+    public $timeout = 5;
     /**
      * Create a new job instance.
      *

+ 2 - 0
app/Jobs/SendEmailJob.php

@@ -16,6 +16,8 @@ class SendEmailJob implements ShouldQueue
     use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
     protected $params;
 
+    public $tries = 3;
+    public $timeout = 10;
     /**
      * Create a new job instance.
      *

+ 1 - 1
app/Jobs/SendTelegramJob.php

@@ -16,7 +16,7 @@ class SendTelegramJob implements ShouldQueue
     protected $text;
 
     public $tries = 3;
-    public $timeout = 5;
+    public $timeout = 10;
 
     /**
      * Create a new job instance.

+ 0 - 78
app/Jobs/StatServerJob.php

@@ -1,78 +0,0 @@
-<?php
-
-namespace App\Jobs;
-
-use App\Models\StatServer;
-use Illuminate\Bus\Queueable;
-use Illuminate\Contracts\Queue\ShouldQueue;
-use Illuminate\Foundation\Bus\Dispatchable;
-use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\SerializesModels;
-
-class StatServerJob implements ShouldQueue
-{
-    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
-    protected $u;
-    protected $d;
-    protected $server;
-    protected $protocol;
-    protected $recordType;
-
-    public $tries = 3;
-    public $timeout = 60;
-
-    /**
-     * Create a new job instance.
-     *
-     * @return void
-     */
-    public function __construct($u, $d, $server, $protocol, $recordType = 'd')
-    {
-        $this->onQueue('stat');
-        $this->u = $u;
-        $this->d = $d;
-        $this->server = $server;
-        $this->protocol = $protocol;
-        $this->recordType = $recordType;
-    }
-
-    /**
-     * Execute the job.
-     *
-     * @return void
-     */
-    public function handle()
-    {
-        $recordAt = strtotime(date('Y-m-d'));
-        if ($this->recordType === 'm') {
-            //
-        }
-
-        $data = StatServer::lockForUpdate()
-            ->where('record_at', $recordAt)
-            ->where('server_id', $this->server['id'])
-            ->where('server_type', $this->protocol)
-            ->first();
-        if ($data) {
-            try {
-                $data->update([
-                    'u' => $data['u'] + $this->u,
-                    'd' => $data['d'] + $this->d
-                ]);
-            } catch (\Exception $e) {
-                abort(500, '节点统计数据更新失败');
-            }
-        } else {
-            if (!StatServer::create([
-                'server_id' => $this->server['id'],
-                'server_type' => $this->protocol,
-                'u' => $this->u,
-                'd' => $this->d,
-                'record_type' => $this->recordType,
-                'record_at' => $recordAt
-            ])) {
-                abort(500, '节点统计数据创建失败');
-            }
-        }
-    }
-}

+ 0 - 80
app/Jobs/StatUserJob.php

@@ -1,80 +0,0 @@
-<?php
-
-namespace App\Jobs;
-
-use App\Models\StatServer;
-use App\Models\StatUser;
-use Illuminate\Bus\Queueable;
-use Illuminate\Contracts\Queue\ShouldQueue;
-use Illuminate\Foundation\Bus\Dispatchable;
-use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\SerializesModels;
-
-class StatUserJob implements ShouldQueue
-{
-    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
-    protected $u;
-    protected $d;
-    protected $userId;
-    protected $server;
-    protected $protocol;
-    protected $recordType;
-
-    public $tries = 3;
-    public $timeout = 60;
-
-    /**
-     * Create a new job instance.
-     *
-     * @return void
-     */
-    public function __construct($u, $d, $userId, array $server, $protocol, $recordType = 'd')
-    {
-        $this->onQueue('stat');
-        $this->u = $u;
-        $this->d = $d;
-        $this->userId = $userId;
-        $this->server = $server;
-        $this->protocol = $protocol;
-        $this->recordType = $recordType;
-    }
-
-    /**
-     * Execute the job.
-     *
-     * @return void
-     */
-    public function handle()
-    {
-        $recordAt = strtotime(date('Y-m-d'));
-        if ($this->recordType === 'm') {
-            //
-        }
-
-        $data = StatUser::where('record_at', $recordAt)
-            ->where('server_rate', $this->server['rate'])
-            ->where('user_id', $this->userId)
-            ->first();
-        if ($data) {
-            try {
-                $data->update([
-                    'u' => $data['u'] + ($this->u * $this->server['rate']),
-                    'd' => $data['d'] + ($this->d * $this->server['rate'])
-                ]);
-            } catch (\Exception $e) {
-                abort(500, '用户统计数据更新失败');
-            }
-        } else {
-            if (!StatUser::create([
-                'user_id' => $this->userId,
-                'server_rate' => $this->server['rate'],
-                'u' => $this->u,
-                'd' => $this->d,
-                'record_type' => $this->recordType,
-                'record_at' => $recordAt
-            ])) {
-                abort(500, '用户统计数据创建失败');
-            }
-        }
-    }
-}

+ 4 - 4
app/Jobs/TrafficFetchJob.php

@@ -20,7 +20,7 @@ class TrafficFetchJob implements ShouldQueue
     protected $protocol;
 
     public $tries = 3;
-    public $timeout = 3;
+    public $timeout = 10;
 
     /**
      * Create a new job instance.
@@ -50,8 +50,8 @@ class TrafficFetchJob implements ShouldQueue
         $user->t = time();
         $user->u = $user->u + ($this->u * $this->server['rate']);
         $user->d = $user->d + ($this->d * $this->server['rate']);
-        if (!$user->save()) throw new \Exception('流量更新失败');
-        $mailService = new MailService();
-        $mailService->remindTraffic($user);
+        if (!$user->save()) {
+            info("流量更新失败\n未记录用户ID:{$this->userId}\n未记录上行:{$user->u}\n未记录下行:{$user->d}");
+        }
     }
 }

+ 11 - 0
app/Logging/MysqlLogger.php

@@ -0,0 +1,11 @@
+<?php
+namespace App\Logging;
+
+class MysqlLogger
+{
+    public function __invoke(array $config){
+        return tap(new \Monolog\Logger('mysql'), function ($logger) {
+            $logger->pushHandler(new MysqlLoggerHandler());
+        });
+    }
+}

+ 44 - 0
app/Logging/MysqlLoggerHandler.php

@@ -0,0 +1,44 @@
+<?php
+namespace App\Logging;
+
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+use Monolog\Handler\AbstractProcessingHandler;
+use Monolog\Logger;
+use App\Models\Log as LogModel;
+
+class MysqlLoggerHandler extends AbstractProcessingHandler
+{
+    public function __construct($level = Logger::DEBUG, bool $bubble = true)
+    {
+        parent::__construct($level, $bubble);
+    }
+
+    protected function write(array $record): void
+    {
+        try{
+            if(isset($record['context']['exception']) && is_object($record['context']['exception'])){
+                $record['context']['exception'] = (array)$record['context']['exception'];
+            }
+            $record['request_data'] = request()->all() ??[];
+            $log = [
+                'title' => $record['message'],
+                'level' => $record['level_name'],
+                'host' => $record['request_host'] ?? request()->getSchemeAndHttpHost(),
+                'uri' => $record['request_uri'] ?? request()->getRequestUri(),
+                'method' => $record['request_method'] ?? request()->getMethod(),
+                'ip' => request()->getClientIp(),
+                'data' => json_encode($record['request_data']) ,
+                'context' => isset($record['context']) ? json_encode($record['context']) : '',
+                'created_at' => strtotime($record['datetime']),
+                'updated_at' => strtotime($record['datetime']),
+            ];
+
+            LogModel::insert(
+                $log
+            );
+        }catch (\Exception $e){
+            Log::channel('daily')->error($e->getMessage().$e->getFile().$e->getTraceAsString());
+        }
+    }
+}

+ 2 - 2
app/Models/StatOrder.php → app/Models/Log.php

@@ -4,9 +4,9 @@ namespace App\Models;
 
 use Illuminate\Database\Eloquent\Model;
 
-class StatOrder extends Model
+class Log extends Model
 {
-    protected $table = 'v2_stat_order';
+    protected $table = 'v2_log';
     protected $dateFormat = 'U';
     protected $guarded = ['id'];
     protected $casts = [

+ 19 - 0
app/Models/ServerHysteria.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class ServerHysteria extends Model
+{
+    protected $table = 'v2_server_hysteria';
+    protected $dateFormat = 'U';
+    protected $guarded = ['id'];
+    protected $casts = [
+        'created_at' => 'timestamp',
+        'updated_at' => 'timestamp',
+        'group_id' => 'array',
+        'route_id' => 'array',
+        'tags' => 'array'
+    ];
+}

+ 16 - 0
app/Models/Stat.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class Stat extends Model
+{
+    protected $table = 'v2_stat';
+    protected $dateFormat = 'U';
+    protected $guarded = ['id'];
+    protected $casts = [
+        'created_at' => 'timestamp',
+        'updated_at' => 'timestamp'
+    ];
+}

+ 16 - 1
app/Payments/StripeCheckout.php

@@ -33,6 +33,11 @@ class StripeCheckout {
                 'label' => 'WebHook 密钥签名',
                 'description' => '',
                 'type' => 'input',
+            ],
+            'stripe_custom_field_name' => [
+                'label' => '自定义字段名称',
+                'description' => '例如可设置为“联系方式”,以便及时与客户取得联系',
+                'type' => 'input',
             ]
         ];
     }
@@ -44,6 +49,7 @@ class StripeCheckout {
         if (!$exchange) {
             abort(500, __('Currency conversion has timed out, please try again later'));
         }
+        $customFieldName = isset($this->config['stripe_custom_field_name']) ? $this->config['stripe_custom_field_name'] : 'Contact Infomation';
 
         $params = [
             'success_url' => $order['return_url'],
@@ -61,7 +67,16 @@ class StripeCheckout {
                     'quantity' => 1
                 ]
             ],
-            'mode' => 'payment'
+            'mode' => 'payment',
+            'invoice_creation' => ['enabled' => true],
+            'phone_number_collection' => ['enabled' => true],
+            'custom_fields' => [
+                [
+                    'key' => 'contactinfo',
+                    'label' => ['type' => 'custom', 'custom' => $customFieldName],
+                    'type' => 'text',
+                ],
+            ],
             // 'customer_email' => $user['email'] not support
 
         ];

+ 5 - 1
app/Services/OrderService.php

@@ -223,7 +223,11 @@ class OrderService
         $order->paid_at = time();
         $order->callback_no = $callbackNo;
         if (!$order->save()) return false;
-        OrderHandleJob::dispatch($order->trade_no);
+        try {
+            OrderHandleJob::dispatchNow($order->trade_no);
+        } catch (\Exception $e) {
+            return false;
+        }
         return true;
     }
 

+ 41 - 2
app/Services/ServerService.php

@@ -2,6 +2,7 @@
 
 namespace App\Services;
 
+use App\Models\ServerHysteria;
 use App\Models\ServerLog;
 use App\Models\ServerRoute;
 use App\Models\ServerShadowsocks;
@@ -61,6 +62,29 @@ class ServerService
         return $servers;
     }
 
+    public function getAvailableHysteria(User $user)
+    {
+        $availableServers = [];
+        $model = ServerHysteria::orderBy('sort', 'ASC');
+        $servers = $model->get()->keyBy('id');
+        foreach ($servers as $key => $v) {
+            if (!$v['show']) continue;
+            $servers[$key]['type'] = 'hysteria';
+            $servers[$key]['last_check_at'] = Cache::get(CacheKey::get('SERVER_HYSTERIA_LAST_CHECK_AT', $v['id']));
+            if (!in_array($user->group_id, $v['group_id'])) continue;
+            if (strpos($v['port'], '-') !== false) {
+                $servers[$key]['port'] = Helper::randomPort($v['port']);
+            }
+            if (isset($servers[$v['parent_id']])) {
+                $servers[$key]['last_check_at'] = Cache::get(CacheKey::get('SERVER_HYSTERIA_LAST_CHECK_AT', $v['parent_id']));
+                $servers[$key]['created_at'] = $servers[$v['parent_id']]['created_at'];
+            }
+            $servers[$key]['server_key'] = Helper::getServerKey($servers[$key]['created_at'], 16);
+            $availableServers[] = $servers[$key]->toArray();
+        }
+        return $availableServers;
+    }
+
     public function getAvailableShadowsocks(User $user)
     {
         $servers = [];
@@ -88,7 +112,8 @@ class ServerService
         $servers = array_merge(
             $this->getAvailableShadowsocks($user),
             $this->getAvailableVmess($user),
-            $this->getAvailableTrojan($user)
+            $this->getAvailableTrojan($user),
+            $this->getAvailableHysteria($user)
         );
         $tmp = array_column($servers, 'sort');
         array_multisort($tmp, SORT_ASC, $servers);
@@ -182,6 +207,17 @@ class ServerService
         return $servers;
     }
 
+    public function getAllHysteria()
+    {
+        $servers = ServerHysteria::orderBy('sort', 'ASC')
+            ->get()
+            ->toArray();
+        foreach ($servers as $k => $v) {
+            $servers[$k]['type'] = 'hysteria';
+        }
+        return $servers;
+    }
+
     private function mergeData(&$servers)
     {
         foreach ($servers as $k => $v) {
@@ -204,7 +240,8 @@ class ServerService
         $servers = array_merge(
             $this->getAllShadowsocks(),
             $this->getAllVMess(),
-            $this->getAllTrojan()
+            $this->getAllTrojan(),
+            $this->getAllHysteria()
         );
         $this->mergeData($servers);
         $tmp = array_column($servers, 'sort');
@@ -233,6 +270,8 @@ class ServerService
                 return ServerShadowsocks::find($serverId);
             case 'trojan':
                 return ServerTrojan::find($serverId);
+            case 'hysteria':
+                return ServerHysteria::find($serverId);
             default:
                 return false;
         }

+ 283 - 0
app/Services/StatisticalService.php

@@ -0,0 +1,283 @@
+<?php
+namespace App\Services;
+
+use App\Models\CommissionLog;
+use App\Models\Order;
+use App\Models\Stat;
+use App\Models\StatServer;
+use App\Models\StatUser;
+use App\Models\User;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\DB;
+
+class StatisticalService {
+    protected $userStats;
+    protected $startAt;
+    protected $endAt;
+    protected $serverStats;
+
+    public function __construct()
+    {
+        ini_set('memory_limit', -1);
+    }
+
+    public function setStartAt($timestamp) {
+        $this->startAt = $timestamp;
+    }
+
+    public function setEndAt($timestamp) {
+        $this->endAt = $timestamp;
+    }
+
+    public function setServerStats() {
+        $this->serverStats = Cache::get("stat_server_{$this->startAt}");
+        $this->serverStats = json_decode($this->serverStats, true) ?? [];
+        if (!is_array($this->serverStats)) {
+            $this->serverStats = [];
+        }
+    }
+
+    public function setUserStats() {
+        $this->userStats = Cache::get("stat_user_{$this->startAt}");
+        $this->userStats = json_decode($this->userStats, true) ?? [];
+        if (!is_array($this->userStats)) {
+            $this->userStats = [];
+        }
+    }
+
+    public function generateStatData(): array
+    {
+        $startAt = $this->startAt;
+        $endAt = $this->endAt;
+        if (!$startAt || !$endAt) {
+            $startAt = strtotime(date('Y-m-d'));
+            $endAt = strtotime('+1 day', $startAt);
+        }
+        $data = [];
+        $data['order_count'] = Order::where('created_at', '>=', $startAt)
+            ->where('created_at', '<', $endAt)
+            ->count();
+        $data['order_total'] = Order::where('created_at', '>=', $startAt)
+            ->where('created_at', '<', $endAt)
+            ->sum('total_amount');
+        $data['paid_count'] = Order::where('paid_at', '>=', $startAt)
+            ->where('paid_at', '<', $endAt)
+            ->whereNotIn('status', [0, 2])
+            ->count();
+        $data['paid_total'] = Order::where('paid_at', '>=', $startAt)
+            ->where('paid_at', '<', $endAt)
+            ->whereNotIn('status', [0, 2])
+            ->sum('total_amount');
+        $commissionLogBuilder = CommissionLog::where('created_at', '>=', $startAt)
+            ->where('created_at', '<', $endAt);
+        $data['commission_count'] = $commissionLogBuilder->count();
+        $data['commission_total'] = $commissionLogBuilder->sum('get_amount');
+        $data['register_count'] = User::where('created_at', '>=', $startAt)
+            ->where('created_at', '<', $endAt)
+            ->count();
+        $data['invite_count'] = User::where('created_at', '>=', $startAt)
+            ->where('created_at', '<', $endAt)
+            ->whereNotNull('invite_user_id')
+            ->count();
+        $data['transfer_used_total'] = StatServer::where('created_at', '>=', $startAt)
+                ->where('created_at', '<', $endAt)
+                ->select(DB::raw('SUM(u) + SUM(d) as total'))
+                ->value('total') ?? 0;
+        return $data;
+    }
+
+    public function statServer($serverId, $serverType, $u, $d)
+    {
+        $this->serverStats[$serverType] = $this->serverStats[$serverType] ?? [];
+        if (isset($this->serverStats[$serverType][$serverId])) {
+            $this->serverStats[$serverType][$serverId][0] += $u;
+            $this->serverStats[$serverType][$serverId][1] += $d;
+        } else {
+            $this->serverStats[$serverType][$serverId] = [$u, $d];
+        }
+        Cache::set("stat_server_{$this->startAt}", json_encode($this->serverStats));
+    }
+
+    public function statUser($rate, $userId, $u, $d)
+    {
+        $this->userStats[$rate] = $this->userStats[$rate] ?? [];
+        if (isset($this->userStats[$rate][$userId])) {
+            $this->userStats[$rate][$userId][0] += $u;
+            $this->userStats[$rate][$userId][1] += $d;
+        } else {
+            $this->userStats[$rate][$userId] = [$u, $d];
+        }
+        Cache::set("stat_user_{$this->startAt}", json_encode($this->userStats));
+    }
+
+    public function getStatUserByUserID($userId): array
+    {
+        $stats = [];
+        foreach (array_keys($this->userStats) as $rate) {
+            if (!isset($this->userStats[$rate][$userId])) continue;
+            $stats[] = [
+                'record_at' => $this->startAt,
+                'server_rate' => $rate,
+                'u' => $this->userStats[$rate][$userId][0],
+                'd' => $this->userStats[$rate][$userId][1],
+                'user_id' => $userId
+            ];
+        }
+        return $stats;
+    }
+
+    public function getStatUser()
+    {
+        $stats = [];
+        foreach ($this->userStats as $k => $v) {
+            foreach (array_keys($v) as $userId) {
+                if (isset($v[$userId])) {
+                    $stats[] = [
+                        'server_rate' => $k,
+                        'u' => $v[$userId][0],
+                        'd' => $v[$userId][1],
+                        'user_id' => $userId
+                    ];
+                }
+            }
+        }
+        return $stats;
+    }
+
+
+    public function getStatServer()
+    {
+        $stats = [];
+        foreach ($this->serverStats as $serverType => $v) {
+            foreach (array_keys($v) as $serverId) {
+                if (isset($v[$serverId])) {
+                    $stats[] = [
+                        'server_id' => $serverId,
+                        'server_type' => $serverType,
+                        'u' => $v[$serverId][0],
+                        'd' => $v[$serverId][1],
+                    ];
+                }
+            }
+        }
+        return $stats;
+    }
+
+    public function clearStatUser()
+    {
+        Cache::forget("stat_user_{$this->startAt}");
+    }
+
+    public function clearStatServer()
+    {
+        Cache::forget("stat_server_{$this->startAt}");
+    }
+
+    public function getStatRecord($type)
+    {
+        switch ($type) {
+            case "paid_total": {
+                return Stat::select([
+                    '*',
+                    DB::raw('paid_total / 100 as paid_total')
+                ])
+                    ->where('record_at', '>=', $this->startAt)
+                    ->where('record_at', '<', $this->endAt)
+                    ->orderBy('record_at', 'ASC')
+                    ->get();
+            }
+            case "commission_total": {
+                return Stat::select([
+                    '*',
+                    DB::raw('commission_total / 100 as commission_total')
+                ])
+                    ->where('record_at', '>=', $this->startAt)
+                    ->where('record_at', '<', $this->endAt)
+                    ->orderBy('record_at', 'ASC')
+                    ->get();
+            }
+            case "register_count": {
+                return Stat::where('record_at', '>=', $this->startAt)
+                    ->where('record_at', '<', $this->endAt)
+                    ->orderBy('record_at', 'ASC')
+                    ->get();
+            }
+        }
+    }
+
+    public function getRanking($type, $limit = 20)
+    {
+        switch ($type) {
+            case 'server_traffic_rank': {
+                return $this->buildServerTrafficRank($limit);
+            }
+            case 'user_consumption_rank': {
+                return $this->buildUserConsumptionRank($limit);
+            }
+            case 'invite_rank': {
+                return $this->buildInviteRank($limit);
+            }
+        }
+    }
+
+    private function buildInviteRank($limit)
+    {
+        $stats = User::select([
+            'invite_user_id',
+            DB::raw('count(*) as count')
+        ])
+            ->where('created_at', '>=', $this->startAt)
+            ->where('created_at', '<', $this->endAt)
+            ->whereNotNull('invite_user_id')
+            ->groupBy('invite_user_id')
+            ->orderBy('count', 'DESC')
+            ->limit($limit)
+            ->get();
+
+        $users = User::whereIn('id', $stats->pluck('invite_user_id')->toArray())->get()->keyBy('id');
+        foreach ($stats as $k => $v) {
+            if (!isset($users[$v['invite_user_id']])) continue;
+            $stats[$k]['email'] = $users[$v['invite_user_id']]['email'];
+        }
+        return $stats;
+    }
+
+    private function buildUserConsumptionRank($limit)
+    {
+        $stats = StatUser::select([
+            'user_id',
+            DB::raw('sum(u) as u'),
+            DB::raw('sum(d) as d'),
+            DB::raw('sum(u) + sum(d) as total')
+        ])
+            ->where('record_at', '>=', $this->startAt)
+            ->where('record_at', '<', $this->endAt)
+            ->groupBy('user_id')
+            ->orderBy('total', 'DESC')
+            ->limit($limit)
+            ->get();
+        $users = User::whereIn('id', $stats->pluck('user_id')->toArray())->get()->keyBy('id');
+        foreach ($stats as $k => $v) {
+            if (!isset($users[$v['user_id']])) continue;
+            $stats[$k]['email'] = $users[$v['user_id']]['email'];
+        }
+        return $stats;
+    }
+
+    private function buildServerTrafficRank($limit)
+    {
+        return StatServer::select([
+            'server_id',
+            'server_type',
+            DB::raw('sum(u) as u'),
+            DB::raw('sum(d) as d'),
+            DB::raw('sum(u) + sum(d) as total')
+        ])
+            ->where('record_at', '>=', $this->startAt)
+            ->where('record_at', '<', $this->endAt)
+            ->groupBy('server_id', 'server_type')
+            ->orderBy('total', 'DESC')
+            ->limit($limit)
+            ->get();
+    }
+}

+ 12 - 4
app/Services/UserService.php

@@ -168,10 +168,18 @@ class UserService
         return true;
     }
 
-    public function trafficFetch(int $u, int $d, int $userId, array $server, string $protocol)
+    public function trafficFetch(array $server, string $protocol, array $data)
     {
-        TrafficFetchJob::dispatch($u, $d, $userId, $server, $protocol);
-        StatServerJob::dispatch($u, $d, $server, $protocol, 'd');
-        StatUserJob::dispatch($u, $d, $userId, $server, $protocol, 'd');
+        $statService = new StatisticalService();
+        $statService->setStartAt(strtotime(date('Y-m-d')));
+        $statService->setUserStats();
+        $statService->setServerStats();
+        foreach (array_keys($data) as $userId) {
+            $u = $data[$userId][0];
+            $d = $data[$userId][1];
+            TrafficFetchJob::dispatch($u, $d, $userId, $server, $protocol);
+            $statService->statServer($server['id'], $protocol, $u, $d);
+            $statService->statUser($server['rate'], $userId, $u, $d);
+        }
     }
 }

+ 3 - 0
app/Utils/CacheKey.php

@@ -16,6 +16,9 @@ class CacheKey
         'SERVER_SHADOWSOCKS_ONLINE_USER' => 'ss节点在线用户',
         'SERVER_SHADOWSOCKS_LAST_CHECK_AT' => 'ss节点最后检查时间',
         'SERVER_SHADOWSOCKS_LAST_PUSH_AT' => 'ss节点最后推送时间',
+        'SERVER_HYSTERIA_ONLINE_USER' => 'hysteria节点在线用户',
+        'SERVER_HYSTERIA_LAST_CHECK_AT' => 'hysteria节点最后检查时间',
+        'SERVER_HYSTERIA_LAST_PUSH_AT' => 'hysteria节点最后推送时间',
         'TEMP_TOKEN' => '临时令牌',
         'LAST_SEND_EMAIL_REMIND_TRAFFIC' => '最后发送流量邮件提醒',
         'SCHEDULE_LAST_CHECK_AT' => '计划任务最后检查时间',

+ 1 - 1
app/Utils/Helper.php

@@ -9,7 +9,7 @@ class Helper
         return base64_encode(substr($uuid, 0, $length));
     }
 
-    public static function getShadowsocksServerKey($timestamp, $length)
+    public static function getServerKey($timestamp, $length)
     {
         return base64_encode(substr(md5($timestamp), 0, $length));
     }

+ 1 - 1
config/app.php

@@ -237,5 +237,5 @@ return [
     | The only modification by laravel config
     |
     */
-    'version' => '1.7.3.1672843907081'
+    'version' => '1.7.4.1681103823832'
 ];

+ 1 - 2
config/horizon.php

@@ -175,7 +175,6 @@ return [
                 'queue' => [
                     'order_handle',
                     'traffic_fetch',
-                    'stat',
                     'send_email',
                     'send_email_mass',
                     'send_telegram',
@@ -184,7 +183,7 @@ return [
                 'minProcesses' => 1,
                 'maxProcesses' => (int)ceil($parser->getRam()['total'] / 1024 / 1024 / 1024 * 6),
                 'tries' => 1,
-                'nice' => 0,
+                'balanceCooldown' => 3,
             ],
         ],
     ],

+ 6 - 1
config/logging.php

@@ -16,7 +16,7 @@ return [
     |
     */
 
-    'default' => env('LOG_CHANNEL', 'stack'),
+    'default' => 'mysql',
 
     /*
     |--------------------------------------------------------------------------
@@ -34,6 +34,11 @@ return [
     */
 
     'channels' => [
+        'mysql' => [
+            'driver' => 'custom',
+            'via' => App\Logging\MysqlLogger::class,
+        ],
+
         'stack' => [
             'driver' => 'stack',
             'channels' => ['daily'],

+ 64 - 18
database/install.sql

@@ -1,4 +1,4 @@
--- Adminer 4.8.1 MySQL 5.7.29 dump
+-- Adminer 4.7.7 MySQL dump
 
 SET NAMES utf8;
 SET time_zone = '+00:00';
@@ -81,6 +81,23 @@ CREATE TABLE `v2_knowledge` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知識庫';
 
 
+DROP TABLE IF EXISTS `v2_log`;
+CREATE TABLE `v2_log` (
+                          `id` int(11) NOT NULL AUTO_INCREMENT,
+                          `title` varchar(255) NOT NULL,
+                          `level` varchar(11) DEFAULT NULL,
+                          `host` varchar(255) DEFAULT NULL,
+                          `uri` varchar(255) NOT NULL,
+                          `method` varchar(11) NOT NULL,
+                          `data` text,
+                          `ip` varchar(128) DEFAULT NULL,
+                          `context` text,
+                          `created_at` int(11) NOT NULL,
+                          `updated_at` int(11) NOT NULL,
+                          PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+
 DROP TABLE IF EXISTS `v2_mail_log`;
 CREATE TABLE `v2_mail_log` (
                                `id` int(11) NOT NULL AUTO_INCREMENT,
@@ -163,8 +180,8 @@ CREATE TABLE `v2_plan` (
                            `id` int(11) NOT NULL AUTO_INCREMENT,
                            `group_id` int(11) NOT NULL,
                            `transfer_enable` int(11) NOT NULL,
-                           `speed_limit` int(11) DEFAULT NULL,
                            `name` varchar(255) NOT NULL,
+                           `speed_limit` int(11) DEFAULT NULL,
                            `show` tinyint(1) NOT NULL DEFAULT '0',
                            `sort` int(11) DEFAULT NULL,
                            `renew` tinyint(1) NOT NULL DEFAULT '1',
@@ -195,6 +212,30 @@ CREATE TABLE `v2_server_group` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 
+DROP TABLE IF EXISTS `v2_server_hysteria`;
+CREATE TABLE `v2_server_hysteria` (
+                                      `id` int(11) NOT NULL AUTO_INCREMENT,
+                                      `group_id` varchar(255) NOT NULL,
+                                      `route_id` varchar(255) DEFAULT NULL,
+                                      `name` varchar(255) NOT NULL,
+                                      `parent_id` int(11) DEFAULT NULL,
+                                      `host` varchar(255) NOT NULL,
+                                      `port` varchar(11) NOT NULL,
+                                      `server_port` int(11) NOT NULL,
+                                      `tags` varchar(255) DEFAULT NULL,
+                                      `rate` varchar(11) NOT NULL,
+                                      `show` tinyint(1) NOT NULL DEFAULT '0',
+                                      `sort` int(11) DEFAULT NULL,
+                                      `up_mbps` int(11) NOT NULL,
+                                      `down_mbps` int(11) NOT NULL,
+                                      `server_name` varchar(64) DEFAULT NULL,
+                                      `insecure` tinyint(1) NOT NULL DEFAULT '0',
+                                      `created_at` int(11) NOT NULL,
+                                      `updated_at` int(11) NOT NULL,
+                                      PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+
 DROP TABLE IF EXISTS `v2_server_route`;
 CREATE TABLE `v2_server_route` (
                                    `id` int(11) NOT NULL AUTO_INCREMENT,
@@ -280,19 +321,24 @@ CREATE TABLE `v2_server_vmess` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
 
-DROP TABLE IF EXISTS `v2_stat_order`;
-CREATE TABLE `v2_stat_order` (
-                                 `id` int(11) NOT NULL AUTO_INCREMENT,
-                                 `order_count` int(11) NOT NULL COMMENT '订单数量',
-                                 `order_amount` int(11) NOT NULL COMMENT '订单合计',
-                                 `commission_count` int(11) NOT NULL,
-                                 `commission_amount` int(11) NOT NULL COMMENT '佣金合计',
-                                 `record_type` char(1) NOT NULL,
-                                 `record_at` int(11) NOT NULL,
-                                 `created_at` int(11) NOT NULL,
-                                 `updated_at` int(11) NOT NULL,
-                                 PRIMARY KEY (`id`),
-                                 UNIQUE KEY `record_at` (`record_at`)
+DROP TABLE IF EXISTS `v2_stat`;
+CREATE TABLE `v2_stat` (
+                           `id` int(11) NOT NULL AUTO_INCREMENT,
+                           `record_at` int(11) NOT NULL,
+                           `record_type` char(1) NOT NULL,
+                           `order_count` int(11) NOT NULL COMMENT '订单数量',
+                           `order_total` int(11) NOT NULL COMMENT '订单合计',
+                           `commission_count` int(11) NOT NULL,
+                           `commission_total` int(11) NOT NULL COMMENT '佣金合计',
+                           `paid_count` int(11) NOT NULL,
+                           `paid_total` int(11) NOT NULL,
+                           `register_count` int(11) NOT NULL,
+                           `invite_count` int(11) NOT NULL,
+                           `transfer_used_total` varchar(32) NOT NULL,
+                           `created_at` int(11) NOT NULL,
+                           `updated_at` int(11) NOT NULL,
+                           PRIMARY KEY (`id`),
+                           UNIQUE KEY `record_at` (`record_at`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='订单统计';
 
 
@@ -379,8 +425,8 @@ CREATE TABLE `v2_user` (
                            `transfer_enable` bigint(20) NOT NULL DEFAULT '0',
                            `banned` tinyint(1) NOT NULL DEFAULT '0',
                            `is_admin` tinyint(1) NOT NULL DEFAULT '0',
-                           `is_staff` tinyint(1) NOT NULL DEFAULT '0',
                            `last_login_at` int(11) DEFAULT NULL,
+                           `is_staff` tinyint(1) NOT NULL DEFAULT '0',
                            `last_login_ip` int(11) DEFAULT NULL,
                            `uuid` varchar(36) NOT NULL,
                            `group_id` int(11) DEFAULT NULL,
@@ -389,8 +435,8 @@ CREATE TABLE `v2_user` (
                            `remind_expire` tinyint(4) DEFAULT '1',
                            `remind_traffic` tinyint(4) DEFAULT '1',
                            `token` char(32) NOT NULL,
-                           `remarks` text,
                            `expired_at` bigint(20) DEFAULT '0',
+                           `remarks` text,
                            `created_at` int(11) NOT NULL,
                            `updated_at` int(11) NOT NULL,
                            PRIMARY KEY (`id`),
@@ -398,4 +444,4 @@ CREATE TABLE `v2_user` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 
--- 2023-03-07 13:10:15
+-- 2023-05-23 17:01:12

+ 55 - 28
database/update.sql

@@ -107,17 +107,6 @@ CREATE TABLE `v2_coupon` (
 ALTER TABLE `v2_order`
 ADD `discount_amount` int(11) NULL AFTER `total_amount`;
 
-CREATE TABLE `v2_tutorial` (
-  `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,
-  `title` varchar(255) COLLATE 'utf8mb4_general_ci' NOT NULL,
-  `description` varchar(255) COLLATE 'utf8mb4_general_ci' NOT NULL,
-  `icon` varchar(255) COLLATE 'utf8mb4_general_ci' NOT NULL,
-  `steps` text NULL,
-  `show` tinyint(1) NOT NULL DEFAULT '0',
-  `created_at` int(11) NOT NULL,
-  `updated_at` int(11) NOT NULL
-);
-
 ALTER TABLE `v2_server_log`
 CHANGE `rate` `rate` decimal(10,2) NOT NULL AFTER `d`;
 
@@ -375,20 +364,6 @@ ALTER TABLE `v2_stat_server`
 ADD INDEX `record_at` (`record_at`),
 ADD INDEX `server_id` (`server_id`);
 
-CREATE TABLE `v2_stat_order` (
-  `id` int(11) NOT NULL AUTO_INCREMENT,
-  `order_count` int(11) NOT NULL COMMENT '订单数量',
-  `order_amount` int(11) NOT NULL COMMENT '订单合计',
-  `commission_count` int(11) NOT NULL,
-  `commission_amount` int(11) NOT NULL COMMENT '佣金合计',
-  `record_type` char(1) NOT NULL,
-  `record_at` int(11) NOT NULL,
-  `created_at` int(11) NOT NULL,
-  `updated_at` int(11) NOT NULL,
-  PRIMARY KEY (`id`),
-  UNIQUE KEY `record_at` (`record_at`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='订单统计';
-
 ALTER TABLE `v2_user`
 DROP `enable`;
 
@@ -599,9 +574,6 @@ ALTER TABLE `v2_server_shadowsocks`
 ALTER TABLE `v2_server_trojan`
     CHANGE `port` `port` varchar(11) NOT NULL COMMENT '连接端口' AFTER `host`;
 
-DELETE FROM `v2_stat_server`
-WHERE `server_type` = 'vmess';
-
 ALTER TABLE `v2_server_shadowsocks`
     ADD `route_id` varchar(255) COLLATE 'utf8mb4_general_ci' NULL AFTER `group_id`;
 
@@ -657,3 +629,58 @@ ALTER TABLE `v2_server_v2ray`
 
 ALTER TABLE `v2_server_vmess`
     CHANGE `network` `network` varchar(11) COLLATE 'utf8mb4_general_ci' NOT NULL AFTER `rate`;
+
+DROP TABLE IF EXISTS `v2_server_hysteria`;
+CREATE TABLE `v2_server_hysteria` (
+                                      `id` int(11) NOT NULL AUTO_INCREMENT,
+                                      `group_id` varchar(255) NOT NULL,
+                                      `route_id` varchar(255) DEFAULT NULL,
+                                      `name` varchar(255) NOT NULL,
+                                      `parent_id` int(11) DEFAULT NULL,
+                                      `host` varchar(255) NOT NULL,
+                                      `port` varchar(11) NOT NULL,
+                                      `server_port` int(11) NOT NULL,
+                                      `tags` varchar(255) DEFAULT NULL,
+                                      `rate` varchar(11) NOT NULL,
+                                      `show` tinyint(1) NOT NULL DEFAULT '0',
+                                      `sort` int(11) DEFAULT NULL,
+                                      `up_mbps` int(11) NOT NULL,
+                                      `down_mbps` int(11) NOT NULL,
+                                      `server_name` varchar(64) DEFAULT NULL,
+                                      `insecure` tinyint(1) NOT NULL DEFAULT '0',
+                                      `created_at` int(11) NOT NULL,
+                                      `updated_at` int(11) NOT NULL,
+                                      PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+ALTER TABLE `v2_plan`
+    ADD `capacity_limit` int(11) NULL AFTER `reset_traffic_method`;
+
+ALTER TABLE `v2_stat_order`
+    CHANGE `record_at` `record_at` int(11) NOT NULL AFTER `id`,
+    CHANGE `record_type` `record_type` char(1) COLLATE 'utf8_general_ci' NOT NULL AFTER `record_at`,
+    CHANGE `order_count` `paid_count` int(11) NOT NULL COMMENT '订单数量' AFTER `record_type`,
+    CHANGE `order_amount` `paid_total` int(11) NOT NULL COMMENT '订单合计' AFTER `paid_count`,
+    CHANGE `commission_count` `commission_count` int(11) NOT NULL AFTER `paid_total`,
+    CHANGE `commission_amount` `commission_total` int(11) NOT NULL COMMENT '佣金合计' AFTER `commission_count`,
+    ADD `order_count` int(11) NOT NULL AFTER `record_type`,
+    ADD `order_total` int(11) NOT NULL AFTER `order_count`,
+    ADD `register_count` int(11) NOT NULL AFTER `order_total`,
+    ADD `invite_count` int(11) NOT NULL AFTER `register_count`,
+    ADD `transfer_used_total` varchar(32) NOT NULL AFTER `invite_count`,
+    RENAME TO `v2_stat`;
+
+CREATE TABLE `v2_log` (
+                          `id` int(11) NOT NULL AUTO_INCREMENT,
+                          `title` varchar(255) NOT NULL,
+                          `level` varchar(11) DEFAULT NULL,
+                          `host` varchar(255) DEFAULT NULL,
+                          `uri` varchar(255) NOT NULL,
+                          `method` varchar(11) NOT NULL,
+                          `data` text,
+                          `ip` varchar(128) DEFAULT NULL,
+                          `context` text,
+                          `created_at` int(11) NOT NULL,
+                          `updated_at` int(11) NOT NULL,
+                          PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

+ 0 - 14
library/V2ray.php

@@ -1,14 +0,0 @@
-<?php
-
-namespace Library;
-
-
-class V2ray
-{
-    protected $config;
-
-    public function __construct()
-    {
-        $this->config = new \StdClass();
-    }
-}

File diff suppressed because it is too large
+ 0 - 0
public/assets/admin/components.async.js


File diff suppressed because it is too large
+ 0 - 0
public/assets/admin/components.chunk.css


File diff suppressed because it is too large
+ 0 - 0
public/assets/admin/umi.js


File diff suppressed because it is too large
+ 0 - 0
public/assets/admin/vendors.async.js


File diff suppressed because it is too large
+ 0 - 0
public/theme/v2board/assets/components.async.js


+ 26 - 26
public/theme/v2board/assets/i18n/ja-JP.js

@@ -9,7 +9,7 @@ window.settings.i18n['ja-JP'] = {
   '一次性': '一括払い',
   '重置流量包': '使用済みデータをリセット',
   '待支付': 'お支払い待ち',
-  '开通中': '処理中',
+  '开通中': '開通中',
   '已取消': 'キャンセル済み',
   '已完成': '済み',
   '已折抵': '控除済み',
@@ -80,7 +80,7 @@ window.settings.i18n['ja-JP'] = {
   '总计': '合計金額',
   '结账': 'チェックアウト',
   '等待支付中': 'お支払い待ち',
-  '开通中': '処理中',
+  '开通中': '開通中',
   '订单系统正在进行处理,请稍等1-3分钟。': 'システム処理中です、しばらくお待ちください',
   '已取消': 'キャンセル済み',
   '订单由于超时支付已被取消。': 'ご注文はキャンセルされました',
@@ -118,8 +118,8 @@ window.settings.i18n['ja-JP'] = {
   '立即开始': '今すぐ連携開始',
   '重置订阅信息': 'サブスクリプションURLの変更',
   '重置': '変更',
-  '确定要重置订阅信息?': 'サブスクリプションURLやUUIDをご変更なされますか?',
-  '如果你的订阅地址或信息泄露可以进行此操作。重置后你的UUID及订阅将会变更,需要重新进行订阅。': 'サブスクリプションのURL及び情報が外部に漏れた場合にご操作ください。操作後はUUIDやURLが変更され、再度サブスクリプションのインポートが必要になります',
+  '确定要重置订阅信息?': 'サブスクリプションURLをご変更なされますか?',
+  '如果你的订阅地址或信息泄露可以进行此操作。重置后你的UUID及订阅将会变更,需要重新进行订阅。': 'サブスクリプションのURL及び情報が外部に漏れた場合にご操作ください。操作後はUUIDやURLが変更され、再度サブスクリプションのインポートが必要になります',
   '重置成功': '変更完了',
   '两次新密码输入不同': 'ご入力されました新しいパスワードが一致しません',
   '两次密码输入不同': 'ご入力されましたパスワードが一致しません',
@@ -150,8 +150,8 @@ window.settings.i18n['ja-JP'] = {
   '确定重置当前已用流量?': '利用済みデータ量をリセットしますか?',
   '点击「确定」将会跳转到收银台,支付订单后系统将会清空您当月已使用流量。': '「確定」をクリックし次のページへ移動,お支払い後に当月分のデータ通信量は即時リセットされます',
   '确定': '確定',
-  '确定要重置订阅信息?': 'サブスクリプションURLやUUIDをご変更なされますか?',
-  '如果你的订阅地址或信息泄露可以进行此操作。重置后你的UUID及订阅将会变更,需要重新进行订阅。': 'サブスクリプションのURL及び情報が外部に漏れた場合にご操作ください。操作後はUUIDやURLが変更され、再度サブスクリプションのインポートが必要になります',
+  '确定要重置订阅信息?': 'サブスクリプションURLをご変更なされますか?',
+  '如果你的订阅地址或信息泄露可以进行此操作。重置后你的UUID及订阅将会变更,需要重新进行订阅。': 'サブスクリプションのURL及び情報が外部に漏れた場合にご操作ください。操作後はUUIDやURLが変更され、再度サブスクリプションのインポートが必要になります',
   '重置成功': '変更完了',
   '低': '低',
   '中': '中',
@@ -167,7 +167,7 @@ window.settings.i18n['ja-JP'] = {
   '关闭': '終了',
   '新的工单': '新規お問い合わせ',
   '新的工单': '新規お問い合わせ',
-  '确认': '送信',
+  '确认': 'OK',
   '主题': 'タイトル',
   '请输入工单主题': 'お問い合わせタイトルをご入力ください',
   '工单等级': 'ご希望のプライオリティ',
@@ -185,7 +185,7 @@ window.settings.i18n['ja-JP'] = {
   '一键订阅': 'ワンクリックインポート',
   '复制订阅': 'サブスクリプションのURLをコピー',
   '推广佣金划转至余额': 'コミッションを残高へ振替',
-  '确认': '送信',
+  '确认': 'OK',
   '划转后的余额仅用于{title}消费使用': '振替済みの残高は{title}でのみご利用可能です',
   '当前推广佣金余额': '現在のコミッション金額',
   '划转金额': '振替金額',
@@ -234,7 +234,7 @@ window.settings.i18n['ja-JP'] = {
   '已用流量将在 {reset_day} 日后重置': '利用済みデータ量は {reset_day} 日後にリセットします',
   '已用流量已在今日重置': '利用済みデータ量は本日リセットされました',
   '重置已用流量': '利用済みデータ量をリセット',
-  '查看节点状态': '查看节点状态',
+  '查看节点状态': '接続先サーバのステータス',
   '当前已使用流量达{rate}%': 'データ使用量が{rate}%になりました',
   '节点名称': 'サーバー名',
   '于 {date} 到期,距离到期还有 {day} 天。': 'ご利用期限は {date} まで,期限まであと {day} 日',
@@ -253,25 +253,25 @@ window.settings.i18n['ja-JP'] = {
   '捷径': 'ショートカット',
   '不会使用,查看使用教程': 'ご利用方法がわからない方はナレッジベースをご閲覧ください',
   '使用支持扫码的客户端进行订阅': '使用支持扫码的客户端进行订阅',
-  '扫描二维码订阅': '扫描二维码订阅',
+  '扫描二维码订阅': 'QRコードをスキャンしてサブスクを設定',
   '续费': '更新',
   '购买': '購入',
   '查看教程': 'チュートリアルを表示',
   '注意': '注意',
-  '你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?': '你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?',
-  '确定取消': '确定取消',
-  '返回我的订单': '返回我的订单',
-  '如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?': '如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?',
-  '选择最适合你的计划': '选择最适合你的计划',
-  '全部': '全',
-  '按周期': '周期別',
-  '遇到问题': '遇到问题',
-  '遇到问题可以通过工单与我们沟通': '遇到问题可以通过工单与我们沟通',
-  '按流量': '按流量',
-  '搜索文档': '搜索文档',
-  '技术支持': '技术支持',
-  '当前剩余佣金': '当前剩余佣金',
-  '三级分销比例': '三级分销比例',
-  '累计获得佣金': '累计获得佣金',
-  '您邀请的用户再次邀请用户将按照订单金额乘以分销等级的比例进行分成。': '您邀请的用户再次邀请用户将按照订单金额乘以分销等级的比例进行分成。'
+  '你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?': 'まだ購入が完了していないオーダーがあります。購入前にそちらをキャンセルする必要がありますが、キャンセルしてよろしいですか?',
+  '确定取消': 'キャンセル',
+  '返回我的订单': '注文履歴に戻る',
+  '如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?': 'もし既にお支払いが完了していると、注文をキャンセルすると支払いが失敗となる可能性があります。キャンセルしてもよろしいですか?',
+  '选择最适合你的计划': 'あなたにピッタリのプランをお選びください',
+  '全部': '全',
+  '按周期': '期間順',
+  '遇到问题': '何かお困りですか?',
+  '遇到问题可以通过工单与我们沟通': '何かお困りでしたら、お問い合わせからご連絡ください。',
+  '按流量': 'データ通信量順',
+  '搜索文档': 'ドキュメント内を検索',
+  '技术支持': 'テクニカルサポート',
+  '当前剩余佣金': 'コミッション残高',
+  '三级分销比例': '3ティア比率',
+  '累计获得佣金': '累計獲得コミッション金額',
+  '您邀请的用户再次邀请用户将按照订单金额乘以分销等级的比例进行分成。': 'お客様に招待された方が更に別の方を招待された場合、お客様は支払われるオーダーからティア分配分の比率分を受け取ることができます。'
 };

File diff suppressed because it is too large
+ 0 - 0
public/theme/v2board/assets/umi.js


File diff suppressed because it is too large
+ 0 - 0
public/theme/v2board/assets/vendors.async.js


+ 1 - 1
public/theme/v2board/dashboard.blade.php

@@ -23,7 +23,7 @@
     <script>
         window.settings = {
             title: '{{$title}}',
-            theme_path: '{{$theme_path}}',
+            assets_path: '/theme/{{$theme}}/assets',
             theme: {
                 sidebar: '{{$theme_config['theme_sidebar']}}',
                 header: '{{$theme_config['theme_header']}}',

+ 5 - 0
resources/views/errors/500.blade.php

@@ -0,0 +1,5 @@
+@extends('errors::minimal')
+
+@section('title', __('Server Error'))
+@section('code', '500')
+@section('message',  __($exception->getMessage() ?: 'Server Error'))

+ 0 - 1
routes/web.php

@@ -23,7 +23,6 @@ Route::get('/', function (Request $request) {
     $renderParams = [
         'title' => config('v2board.app_name', 'V2Board'),
         'theme' => config('v2board.frontend_theme', 'v2board'),
-        'theme_path' => '/theme/' . config('v2board.frontend_theme', 'v2board') . '/assets/',
         'version' => config('app.version'),
         'description' => config('v2board.app_description', 'V2Board is best'),
         'logo' => config('v2board.logo')

+ 11 - 0
update.sh

@@ -1,5 +1,16 @@
 #!/bin/bash
 
+if [ ! -d ".git" ]; then
+  echo "Please deploy using Git."
+  exit 1
+fi
+
+if ! command -v git &> /dev/null; then
+    echo "Git is not installed! Please install git and try again."
+    exit 1
+fi
+
+git config --global --add safe.directory $(pwd)
 git fetch --all && git reset --hard origin/master && git pull origin master
 rm -rf composer.lock composer.phar
 wget https://github.com/composer/composer/releases/latest/download/composer.phar -O composer.phar

+ 11 - 0
update_dev.sh

@@ -1,5 +1,16 @@
 #!/bin/bash
 
+if [ ! -d ".git" ]; then
+  echo "Please deploy using Git."
+  exit 1
+fi
+
+if ! command -v git &> /dev/null; then
+    echo "Git is not installed! Please install git and try again."
+    exit 1
+fi
+
+git config --global --add safe.directory $(pwd)
 git fetch --all && git reset --hard origin/dev && git pull origin dev
 git checkout dev
 rm -rf composer.lock composer.phar

Some files were not shown because too many files changed in this diff