Browse Source

Merge branch 'dev'

tokumeikoi 2 years ago
parent
commit
ecef0315a0
100 changed files with 1668 additions and 868 deletions
  1. 1 0
      app/Console/Commands/CheckCommission.php
  2. 51 0
      app/Console/Commands/ClearUser.php
  3. 54 0
      app/Console/Commands/ResetPassword.php
  4. 42 0
      app/Console/Commands/ResetTraffic.php
  5. 2 0
      app/Console/Commands/Test.php
  6. 9 9
      app/Console/Commands/V2boardStatistics.php
  7. 24 0
      app/Exceptions/Handler.php
  8. 113 129
      app/Http/Controllers/Admin/ConfigController.php
  9. 10 1
      app/Http/Controllers/Admin/CouponController.php
  10. 2 1
      app/Http/Controllers/Admin/NoticeController.php
  11. 29 14
      app/Http/Controllers/Admin/PaymentController.php
  12. 0 1
      app/Http/Controllers/Admin/PlanController.php
  13. 6 4
      app/Http/Controllers/Admin/StatController.php
  14. 90 0
      app/Http/Controllers/Admin/ThemeController.php
  15. 8 8
      app/Http/Controllers/Admin/TicketController.php
  16. 3 5
      app/Http/Controllers/Admin/UserController.php
  17. 23 0
      app/Http/Controllers/Client/ClientController.php
  18. 2 2
      app/Http/Controllers/Client/Protocols/Clash.php
  19. 3 3
      app/Http/Controllers/Client/Protocols/SagerNet.php
  20. 2 2
      app/Http/Controllers/Client/Protocols/Stash.php
  21. 2 1
      app/Http/Controllers/Client/Protocols/Surfboard.php
  22. 3 0
      app/Http/Controllers/Client/Protocols/Surge.php
  23. 2 8
      app/Http/Controllers/Guest/CommController.php
  24. 1 1
      app/Http/Controllers/Guest/PaymentController.php
  25. 16 0
      app/Http/Controllers/Passport/AuthController.php
  26. 128 4
      app/Http/Controllers/Server/DeepbworkController.php
  27. 5 1
      app/Http/Controllers/Server/ShadowsocksTidalabController.php
  28. 23 4
      app/Http/Controllers/Server/TrojanTidalabController.php
  29. 128 0
      app/Http/Controllers/Server/VProxyController.php
  30. 0 7
      app/Http/Controllers/Staff/TicketController.php
  31. 1 0
      app/Http/Controllers/User/InviteController.php
  32. 8 10
      app/Http/Controllers/User/KnowledgeController.php
  33. 14 4
      app/Http/Controllers/User/OrderController.php
  34. 6 2
      app/Http/Controllers/User/PlanController.php
  35. 2 3
      app/Http/Controllers/User/StatController.php
  36. 15 27
      app/Http/Controllers/User/TicketController.php
  37. 5 33
      app/Http/Controllers/User/UserController.php
  38. 2 0
      app/Http/Middleware/Client.php
  39. 86 109
      app/Http/Requests/Admin/ConfigSave.php
  40. 4 2
      app/Http/Requests/Admin/NoticeSave.php
  41. 1 1
      app/Http/Requests/Admin/PlanSave.php
  42. 5 1
      app/Http/Requests/Admin/ServerShadowsocksSave.php
  43. 5 0
      app/Http/Routes/AdminRoute.php
  44. 0 1
      app/Http/Routes/GuestRoute.php
  45. 1 0
      app/Jobs/StatServerJob.php
  46. 1 3
      app/Jobs/StatUserJob.php
  47. 2 1
      app/Models/Notice.php
  48. 2 1
      app/Models/ServerShadowsocks.php
  49. 10 13
      app/Payments/CoinPayments.php
  50. 3 0
      app/Providers/RouteServiceProvider.php
  51. 9 8
      app/Services/OrderService.php
  52. 1 1
      app/Services/PaymentService.php
  53. 0 144
      app/Services/ServerService.php
  54. 48 0
      app/Services/ThemeService.php
  55. 26 1
      app/Services/TicketService.php
  56. 46 0
      app/Services/UserService.php
  57. 2 1
      app/Utils/CacheKey.php
  58. 4 6
      app/Utils/Helper.php
  59. 6 7
      composer.json
  60. 1 1
      config/app.php
  61. 2 0
      config/theme/.gitignore
  62. 22 18
      database/install.sql
  63. 77 4
      database/update.sql
  64. 6 0
      init.sh
  65. 2 1
      public/assets/admin/env.example.js
  66. 0 0
      public/assets/admin/umi.css
  67. 0 0
      public/assets/admin/umi.js
  68. 2 2
      public/theme/.gitignore
  69. 0 0
      public/theme/v2board/assets/components.async.js
  70. 0 0
      public/theme/v2board/assets/components.chunk.css
  71. 29 8
      public/theme/v2board/assets/i18n/en-US.js
  72. 27 6
      public/theme/v2board/assets/i18n/ja-JP.js
  73. 26 5
      public/theme/v2board/assets/i18n/ko-KR.js
  74. 26 5
      public/theme/v2board/assets/i18n/vi-VN.js
  75. 28 7
      public/theme/v2board/assets/i18n/zh-CN.js
  76. 198 177
      public/theme/v2board/assets/i18n/zh-TW.js
  77. BIN
      public/theme/v2board/assets/images/icon/Clash For Android.png
  78. BIN
      public/theme/v2board/assets/images/icon/Clash For Windows.png
  79. BIN
      public/theme/v2board/assets/images/icon/ClashX.png
  80. BIN
      public/theme/v2board/assets/images/icon/QuantumultX.png
  81. BIN
      public/theme/v2board/assets/images/icon/Shadowrocket.png
  82. BIN
      public/theme/v2board/assets/images/icon/Stash.png
  83. BIN
      public/theme/v2board/assets/images/icon/Surfboard.png
  84. BIN
      public/theme/v2board/assets/images/icon/Surge.png
  85. 1 0
      public/theme/v2board/assets/umi.css
  86. 0 0
      public/theme/v2board/assets/umi.js
  87. 53 0
      public/theme/v2board/config.php
  88. 9 7
      public/theme/v2board/dashboard.blade.php
  89. 1 5
      public/vendor/horizon/app-dark.css
  90. 1 5
      public/vendor/horizon/app.css
  91. 0 0
      public/vendor/horizon/app.js
  92. 4 3
      public/vendor/horizon/mix-manifest.json
  93. 5 1
      resources/lang/en-US.json
  94. 5 1
      resources/lang/zh-CN.json
  95. 5 3
      resources/rules/app.clash.yaml
  96. 9 6
      resources/rules/default.clash.yaml
  97. 5 2
      resources/rules/default.surfboard.conf
  98. 5 2
      resources/rules/default.surge.conf
  99. 9 8
      resources/views/admin.blade.php
  100. 13 7
      routes/web.php

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

@@ -98,6 +98,7 @@ class CheckCommission extends Command
             if (!$inviter) continue;
             if (!isset($commissionShareLevels[$l])) continue;
             $commissionBalance = $order->commission_balance * ($commissionShareLevels[$l] / 100);
+            if (!$commissionBalance) continue;
             if ((int)config('v2board.withdraw_close_enable', 0)) {
                 $inviter->balance = $inviter->balance + $commissionBalance;
             } else {

+ 51 - 0
app/Console/Commands/ClearUser.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Models\Ticket;
+use App\Models\User;
+use Illuminate\Console\Command;
+
+class ClearUser extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'clear:user';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '清理用户';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return mixed
+     */
+    public function handle()
+    {
+        $builder = User::where('plan_id', NULL)
+            ->where('transfer_enable', 0)
+            ->where('expired_at', 0)
+            ->where('last_login_at', NULL);
+        $count = $builder->count();
+        if ($builder->delete()) {
+            $this->info("已删除${count}位没有任何数据的用户");
+        }
+    }
+}

+ 54 - 0
app/Console/Commands/ResetPassword.php

@@ -0,0 +1,54 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Models\Plan;
+use App\Utils\Helper;
+use Illuminate\Console\Command;
+use App\Models\User;
+use Illuminate\Support\Facades\DB;
+
+class ResetPassword extends Command
+{
+    protected $builder;
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'reset:password {email}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '重置用户密码';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return mixed
+     */
+    public function handle()
+    {
+        $user = User::where('email', $this->argument('email'))->first();
+        if (!$user) abort(500, '邮箱不存在');
+        $password = Helper::guid(false);
+        $user->password = password_hash($password, PASSWORD_DEFAULT);
+        $user->password_algo = null;
+        if (!$user->save()) abort(500, '重置失败');
+        $this->info("!!!重置成功!!!");
+        $this->info("新密码为:{$password},请尽快修改密码。");
+    }
+}

+ 42 - 0
app/Console/Commands/ResetTraffic.php

@@ -69,6 +69,12 @@ class ResetTraffic extends Command
                         // no action
                         case 2:
                             break;
+                        // year first day
+                        case 3:
+                            $this->resetByYearFirstDay($builder);
+                        // year expire day
+                        case 4:
+                            $this->resetByExpireYear($builder);
                     }
                     break;
                 }
@@ -85,10 +91,46 @@ class ResetTraffic extends Command
                 case ($resetMethod['method'] === 2): {
                     break;
                 }
+                case ($resetMethod['method'] === 3): {
+                    $builder = with(clone($this->builder))->whereIn('plan_id', $planIds);
+                    $this->resetByYearFirstDay($builder);
+                    break;
+                }
+                case ($resetMethod['method'] === 4): {
+                    $builder = with(clone($this->builder))->whereIn('plan_id', $planIds);
+                    $this->resetByExpireYear($builder);
+                    break;
+                }
             }
         }
     }
 
+    private function resetByExpireYear($builder):void
+    {
+        $users = [];
+        foreach ($builder->get() as $item) {
+            $expireDay = date('m-d', $item->expired_at);
+            $today = date('m-d');
+            if ($expireDay === $today) {
+                array_push($users, $item->id);
+            }
+        }
+        User::whereIn('id', $users)->update([
+            'u' => 0,
+            'd' => 0
+        ]);
+    }
+
+    private function resetByYearFirstDay($builder):void
+    {
+        if ((string)date('md') === '0101') {
+            $builder->update([
+                'u' => 0,
+                'd' => 0
+            ]);
+        }
+    }
+
     private function resetByMonthFirstDay($builder):void
     {
         if ((string)date('d') === '01') {

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

@@ -7,6 +7,8 @@ use App\Models\User;
 use App\Utils\CacheKey;
 use App\Utils\Helper;
 use Illuminate\Console\Command;
+use Illuminate\Filesystem\Filesystem;
+use Illuminate\Foundation\Console\ConfigCacheCommand;
 use Illuminate\Support\Facades\Cache;
 use Matriphe\Larinfo;
 

+ 9 - 9
app/Console/Commands/V2boardStatistics.php

@@ -2,12 +2,10 @@
 
 namespace App\Console\Commands;
 
-use App\Jobs\StatServerJob;
 use Illuminate\Console\Command;
 use App\Models\Order;
 use App\Models\StatOrder;
-use App\Models\ServerLog;
-use Illuminate\Support\Facades\DB;
+use App\Models\CommissionLog;
 
 class V2boardStatistics extends Command
 {
@@ -50,14 +48,16 @@ class V2boardStatistics extends Command
     {
         $endAt = strtotime(date('Y-m-d'));
         $startAt = strtotime('-1 day', $endAt);
-        $builder = Order::where('paid_at', '>=', $startAt)
+        $orderBuilder = Order::where('paid_at', '>=', $startAt)
             ->where('paid_at', '<', $endAt)
             ->whereNotIn('status', [0, 2]);
-        $orderCount = $builder->count();
-        $orderAmount = $builder->sum('total_amount');
-        $builder = $builder->whereNotNull('actual_commission_balance');
-        $commissionCount = $builder->count();
-        $commissionAmount = $builder->sum('actual_commission_balance');
+        $orderCount = $orderBuilder->count();
+        $orderAmount = $orderBuilder->sum('total_amount');
+        $commissionBuilder = CommissionLog::where('created_at', '>=', $startAt)
+            ->where('created_at', '<', $endAt)
+            ->where('get_amount', '>', 0);
+        $commissionCount = $commissionBuilder->count();
+        $commissionAmount = $commissionBuilder->sum('get_amount');
         $data = [
             'order_count' => $orderCount,
             'order_amount' => $orderAmount,

+ 24 - 0
app/Exceptions/Handler.php

@@ -3,7 +3,10 @@
 namespace App\Exceptions;
 
 use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
+use Illuminate\Support\Arr;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Throwable;
+use Facade\Ignition\Exceptions\ViewException;
 
 class Handler extends ExceptionHandler
 {
@@ -50,6 +53,27 @@ class Handler extends ExceptionHandler
      */
     public function render($request, Throwable $exception)
     {
+        if ($exception instanceof ViewException) {
+            return response([
+                'message' => "主题初始化发生错误,请在后台对主题检查或配置后重试。"
+            ]);
+        }
         return parent::render($request, $exception);
     }
+
+
+    protected function convertExceptionToArray(Throwable $e)
+    {
+        return config('app.debug') ? [
+            'message' => $e->getMessage(),
+            'exception' => get_class($e),
+            'file' => $e->getFile(),
+            'line' => $e->getLine(),
+            'trace' => collect($e->getTrace())->map(function ($trace) {
+                return Arr::except($trace, ['args']);
+            })->all(),
+        ] : [
+            'message' => $this->isHttpException($e) ? $e->getMessage() : __("Uh-oh, we've had some problems, we're working on it."),
+        ];
+    }
 }

+ 113 - 129
app/Http/Controllers/Admin/ConfigController.php

@@ -8,6 +8,8 @@ use App\Services\TelegramService;
 use Illuminate\Http\Request;
 use App\Utils\Dict;
 use App\Http\Controllers\Controller;
+use Illuminate\Support\Facades\Artisan;
+use Illuminate\Support\Facades\File;
 use Illuminate\Support\Facades\Mail;
 
 class ConfigController extends Controller
@@ -54,152 +56,134 @@ class ConfigController extends Controller
 
     public function setTelegramWebhook(Request $request)
     {
+        $hookUrl = url('/api/v1/guest/telegram/webhook?access_token=' . md5(config('v2board.telegram_bot_token', $request->input('telegram_bot_token'))));
         $telegramService = new TelegramService($request->input('telegram_bot_token'));
         $telegramService->getMe();
-        $telegramService->setWebhook(
-            url(
-                '/api/v1/guest/telegram/webhook?access_token=' . md5(config('v2board.telegram_bot_token', $request->input('telegram_bot_token')))
-            )
-        );
+        $telegramService->setWebhook($hookUrl);
         return response([
             'data' => true
         ]);
     }
 
-    public function fetch()
+    public function fetch(Request $request)
     {
+        $key = $request->input('key');
+        $data = [
+            'invite' => [
+                'invite_force' => (int)config('v2board.invite_force', 0),
+                'invite_commission' => config('v2board.invite_commission', 10),
+                'invite_gen_limit' => config('v2board.invite_gen_limit', 5),
+                'invite_never_expire' => config('v2board.invite_never_expire', 0),
+                'commission_first_time_enable' => config('v2board.commission_first_time_enable', 1),
+                'commission_auto_check_enable' => config('v2board.commission_auto_check_enable', 1),
+                'commission_withdraw_limit' => config('v2board.commission_withdraw_limit', 100),
+                'commission_withdraw_method' => config('v2board.commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT),
+                'withdraw_close_enable' => config('v2board.withdraw_close_enable', 0),
+                'commission_distribution_enable' => config('v2board.commission_distribution_enable', 0),
+                'commission_distribution_l1' => config('v2board.commission_distribution_l1'),
+                'commission_distribution_l2' => config('v2board.commission_distribution_l2'),
+                'commission_distribution_l3' => config('v2board.commission_distribution_l3')
+            ],
+            'site' => [
+                'logo' => config('v2board.logo'),
+                'force_https' => (int)config('v2board.force_https', 0),
+                'safe_mode_enable' => (int)config('v2board.safe_mode_enable', 0),
+                'stop_register' => (int)config('v2board.stop_register', 0),
+                'email_verify' => (int)config('v2board.email_verify', 0),
+                'app_name' => config('v2board.app_name', 'V2Board'),
+                'app_description' => config('v2board.app_description', 'V2Board is best!'),
+                'app_url' => config('v2board.app_url'),
+                'subscribe_url' => config('v2board.subscribe_url'),
+                'try_out_plan_id' => (int)config('v2board.try_out_plan_id', 0),
+                'try_out_hour' => (int)config('v2board.try_out_hour', 1),
+                'email_whitelist_enable' => (int)config('v2board.email_whitelist_enable', 0),
+                'email_whitelist_suffix' => config('v2board.email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT),
+                'email_gmail_limit_enable' => config('v2board.email_gmail_limit_enable', 0),
+                'recaptcha_enable' => (int)config('v2board.recaptcha_enable', 0),
+                'recaptcha_key' => config('v2board.recaptcha_key'),
+                'recaptcha_site_key' => config('v2board.recaptcha_site_key'),
+                'tos_url' => config('v2board.tos_url'),
+                'currency' => config('v2board.currency', 'CNY'),
+                'currency_symbol' => config('v2board.currency_symbol', '¥'),
+                'register_limit_by_ip_enable' => (int)config('v2board.register_limit_by_ip_enable', 0),
+                'register_limit_count' => config('v2board.register_limit_count', 3),
+                'register_limit_expire' => config('v2board.register_limit_expire', 60)
+            ],
+            'subscribe' => [
+                'plan_change_enable' => (int)config('v2board.plan_change_enable', 1),
+                'reset_traffic_method' => (int)config('v2board.reset_traffic_method', 0),
+                'surplus_enable' => (int)config('v2board.surplus_enable', 1),
+                'new_order_event_id' => (int)config('v2board.new_order_event_id', 0),
+                'renew_order_event_id' => (int)config('v2board.renew_order_event_id', 0),
+                'change_order_event_id' => (int)config('v2board.change_order_event_id', 0),
+                'show_info_to_server_enable' => (int)config('v2board.show_info_to_server_enable', 0)
+            ],
+            'frontend' => [
+                'frontend_theme' => config('v2board.frontend_theme', 'v2board'),
+                'frontend_theme_sidebar' => config('v2board.frontend_theme_sidebar', 'light'),
+                'frontend_theme_header' => config('v2board.frontend_theme_header', 'dark'),
+                'frontend_theme_color' => config('v2board.frontend_theme_color', 'default'),
+                'frontend_background_url' => config('v2board.frontend_background_url'),
+                'frontend_admin_path' => config('v2board.frontend_admin_path', 'admin')
+            ],
+            'server' => [
+                'server_token' => config('v2board.server_token'),
+                'server_license' => config('v2board.server_license'),
+                'server_log_enable' => config('v2board.server_log_enable', 0),
+                'server_v2ray_domain' => config('v2board.server_v2ray_domain'),
+                'server_v2ray_protocol' => config('v2board.server_v2ray_protocol'),
+            ],
+            'email' => [
+                'email_template' => config('v2board.email_template', 'default'),
+                'email_host' => config('v2board.email_host'),
+                'email_port' => config('v2board.email_port'),
+                'email_username' => config('v2board.email_username'),
+                'email_password' => config('v2board.email_password'),
+                'email_encryption' => config('v2board.email_encryption'),
+                'email_from_address' => config('v2board.email_from_address')
+            ],
+            'telegram' => [
+                'telegram_bot_enable' => config('v2board.telegram_bot_enable', 0),
+                'telegram_bot_token' => config('v2board.telegram_bot_token'),
+                'telegram_discuss_link' => config('v2board.telegram_discuss_link')
+            ],
+            'app' => [
+                'windows_version' => config('v2board.windows_version'),
+                'windows_download_url' => config('v2board.windows_download_url'),
+                'macos_version' => config('v2board.macos_version'),
+                'macos_download_url' => config('v2board.macos_download_url'),
+                'android_version' => config('v2board.android_version'),
+                'android_download_url' => config('v2board.android_download_url')
+            ]
+        ];
+        if ($key && isset($data[$key])) {
+            return response([
+                'data' => [
+                    $key => $data[$key]
+                ]
+            ]);
+        };
         // TODO: default should be in Dict
         return response([
-            'data' => [
-                'invite' => [
-                    'invite_force' => (int)config('v2board.invite_force', 0),
-                    'invite_commission' => config('v2board.invite_commission', 10),
-                    'invite_gen_limit' => config('v2board.invite_gen_limit', 5),
-                    'invite_never_expire' => config('v2board.invite_never_expire', 0),
-                    'commission_first_time_enable' => config('v2board.commission_first_time_enable', 1),
-                    'commission_auto_check_enable' => config('v2board.commission_auto_check_enable', 1),
-                    'commission_withdraw_limit' => config('v2board.commission_withdraw_limit', 100),
-                    'commission_withdraw_method' => config('v2board.commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT),
-                    'withdraw_close_enable' => config('v2board.withdraw_close_enable', 0),
-                    'commission_distribution_enable' => config('v2board.commission_distribution_enable', 0),
-                    'commission_distribution_l1' => config('v2board.commission_distribution_l1'),
-                    'commission_distribution_l2' => config('v2board.commission_distribution_l2'),
-                    'commission_distribution_l3' => config('v2board.commission_distribution_l3')
-                ],
-                'site' => [
-                    'safe_mode_enable' => (int)config('v2board.safe_mode_enable', 0),
-                    'stop_register' => (int)config('v2board.stop_register', 0),
-                    'email_verify' => (int)config('v2board.email_verify', 0),
-                    'app_name' => config('v2board.app_name', 'V2Board'),
-                    'app_description' => config('v2board.app_description', 'V2Board is best!'),
-                    'app_url' => config('v2board.app_url'),
-                    'subscribe_url' => config('v2board.subscribe_url'),
-                    'try_out_plan_id' => (int)config('v2board.try_out_plan_id', 0),
-                    'try_out_hour' => (int)config('v2board.try_out_hour', 1),
-                    'email_whitelist_enable' => (int)config('v2board.email_whitelist_enable', 0),
-                    'email_whitelist_suffix' => config('v2board.email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT),
-                    'email_gmail_limit_enable' => config('v2board.email_gmail_limit_enable', 0),
-                    'recaptcha_enable' => (int)config('v2board.recaptcha_enable', 0),
-                    'recaptcha_key' => config('v2board.recaptcha_key'),
-                    'recaptcha_site_key' => config('v2board.recaptcha_site_key'),
-                    'tos_url' => config('v2board.tos_url'),
-                    'currency' => config('v2board.currency', 'CNY'),
-                    'currency_symbol' => config('v2board.currency_symbol', '¥')
-                ],
-                'subscribe' => [
-                    'plan_change_enable' => (int)config('v2board.plan_change_enable', 1),
-                    'reset_traffic_method' => (int)config('v2board.reset_traffic_method', 0),
-                    'surplus_enable' => (int)config('v2board.surplus_enable', 1),
-                    'new_order_event_id' => (int)config('v2board.new_order_event_id', 0),
-                    'renew_order_event_id' => (int)config('v2board.renew_order_event_id', 0),
-                    'change_order_event_id' => (int)config('v2board.change_order_event_id', 0),
-                ],
-                'pay' => [
-                    // alipay
-                    'alipay_enable' => (int)config('v2board.alipay_enable'),
-                    'alipay_appid' => config('v2board.alipay_appid'),
-                    'alipay_pubkey' => config('v2board.alipay_pubkey'),
-                    'alipay_privkey' => config('v2board.alipay_privkey'),
-                    // stripe
-                    'stripe_alipay_enable' => (int)config('v2board.stripe_alipay_enable', 0),
-                    'stripe_wepay_enable' => (int)config('v2board.stripe_wepay_enable', 0),
-                    'stripe_card_enable' => (int)config('v2board.stripe_card_enable', 0),
-                    'stripe_sk_live' => config('v2board.stripe_sk_live'),
-                    'stripe_pk_live' => config('v2board.stripe_pk_live'),
-                    'stripe_webhook_key' => config('v2board.stripe_webhook_key'),
-                    'stripe_currency' => config('v2board.stripe_currency', 'hkd'),
-                    // bitpayx
-                    'bitpayx_name' => config('v2board.bitpayx_name', '在线支付'),
-                    'bitpayx_enable' => (int)config('v2board.bitpayx_enable', 0),
-                    'bitpayx_appsecret' => config('v2board.bitpayx_appsecret'),
-                    // mGate
-                    'mgate_name' => config('v2board.mgate_name', '在线支付'),
-                    'mgate_enable' => (int)config('v2board.mgate_enable', 0),
-                    'mgate_url' => config('v2board.mgate_url'),
-                    'mgate_app_id' => config('v2board.mgate_app_id'),
-                    'mgate_app_secret' => config('v2board.mgate_app_secret'),
-                    // Epay
-                    'epay_name' => config('v2board.epay_name', '在线支付'),
-                    'epay_enable' => (int)config('v2board.epay_enable', 0),
-                    'epay_url' => config('v2board.epay_url'),
-                    'epay_pid' => config('v2board.epay_pid'),
-                    'epay_key' => config('v2board.epay_key'),
-                ],
-                'frontend' => [
-                    'frontend_theme' => config('v2board.frontend_theme', 'v2board'),
-                    'frontend_theme_sidebar' => config('v2board.frontend_theme_sidebar', 'light'),
-                    'frontend_theme_header' => config('v2board.frontend_theme_header', 'dark'),
-                    'frontend_theme_color' => config('v2board.frontend_theme_color', 'default'),
-                    'frontend_background_url' => config('v2board.frontend_background_url'),
-                    'frontend_admin_path' => config('v2board.frontend_admin_path', 'admin'),
-                    'frontend_customer_service_method' => config('v2board.frontend_customer_service_method', 0),
-                    'frontend_customer_service_id' => config('v2board.frontend_customer_service_id'),
-                ],
-                'server' => [
-                    'server_token' => config('v2board.server_token'),
-                    'server_license' => config('v2board.server_license'),
-                    'server_log_enable' => config('v2board.server_log_enable', 0),
-                    'server_v2ray_domain' => config('v2board.server_v2ray_domain'),
-                    'server_v2ray_protocol' => config('v2board.server_v2ray_protocol'),
-                ],
-                'email' => [
-                    'email_template' => config('v2board.email_template', 'default'),
-                    'email_host' => config('v2board.email_host'),
-                    'email_port' => config('v2board.email_port'),
-                    'email_username' => config('v2board.email_username'),
-                    'email_password' => config('v2board.email_password'),
-                    'email_encryption' => config('v2board.email_encryption'),
-                    'email_from_address' => config('v2board.email_from_address')
-                ],
-                'telegram' => [
-                    'telegram_bot_enable' => config('v2board.telegram_bot_enable', 0),
-                    'telegram_bot_token' => config('v2board.telegram_bot_token'),
-                    'telegram_discuss_link' => config('v2board.telegram_discuss_link')
-                ],
-                'app' => [
-                    'windows_version' => config('v2board.windows_version'),
-                    'windows_download_url' => config('v2board.windows_download_url'),
-                    'macos_version' => config('v2board.macos_version'),
-                    'macos_download_url' => config('v2board.macos_download_url'),
-                    'android_version' => config('v2board.android_version'),
-                    'android_download_url' => config('v2board.android_download_url')
-                ]
-            ]
+            'data' => $data
         ]);
     }
 
     public function save(ConfigSave $request)
     {
         $data = $request->validated();
-        $array = \Config::get('v2board');
-        foreach ($data as $k => $v) {
-            if (!in_array($k, array_keys($request->validated()))) {
-                abort(500, '参数' . $k . '不在规则内,禁止修改');
+        $config = config('v2board');
+        foreach (ConfigSave::RULES as $k => $v) {
+            if (!in_array($k, array_keys(ConfigSave::RULES))) {
+                unset($config[$k]);
+                continue;
+            }
+            if (array_key_exists($k, $data)) {
+                $config[$k] = $data[$k];
             }
-            $array[$k] = $v;
         }
-        $data = var_export($array, 1);
-        if (!\File::put(base_path() . '/config/v2board.php', "<?php\n return $data ;")) {
+        $data = var_export($config, 1);
+        if (!File::put(base_path() . '/config/v2board.php', "<?php\n return $data ;")) {
             abort(500, '修改失败');
         }
         if (function_exists('opcache_reset')) {
@@ -207,7 +191,7 @@ class ConfigController extends Controller
                 abort(500, '缓存清除失败,请卸载或检查opcache配置状态');
             }
         }
-        \Artisan::call('config:cache');
+        Artisan::call('config:cache');
         return response([
             'data' => true
         ]);

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

@@ -88,7 +88,16 @@ class CouponController extends Controller
             array_push($coupons, $coupon);
         }
         DB::beginTransaction();
-        if (!Coupon::insert($coupons)) {
+        if (!Coupon::insert(array_map(function ($item) use ($coupon) {
+            // format data
+            if (isset($item['limit_plan_ids']) && is_array($item['limit_plan_ids'])) {
+                $item['limit_plan_ids'] = json_encode($coupon['limit_plan_ids']);
+            }
+            if (isset($item['limit_period']) && is_array($item['limit_period'])) {
+                $item['limit_period'] = json_encode($coupon['limit_period']);
+            }
+            return $item;
+        }, $coupons))) {
             DB::rollBack();
             abort(500, '生成失败');
         }

+ 2 - 1
app/Http/Controllers/Admin/NoticeController.php

@@ -22,7 +22,8 @@ class NoticeController extends Controller
         $data = $request->only([
             'title',
             'content',
-            'img_url'
+            'img_url',
+            'tags'
         ]);
         if (!$request->input('id')) {
             if (!Notice::create($data)) {

+ 29 - 14
app/Http/Controllers/Admin/PaymentController.php

@@ -46,35 +46,50 @@ class PaymentController extends Controller
         ]);
     }
 
+    public function show(Request $request)
+    {
+        $payment = Payment::find($request->input('id'));
+        if (!$payment) abort(500, '支付方式不存在');
+        $payment->enable = !$payment->enable;
+        if (!$payment->save()) abort(500, '保存失败');
+        return response([
+            'data' => true
+        ]);
+    }
+
     public function save(Request $request)
     {
         if (!config('v2board.app_url')) {
             abort(500, '请在站点配置中配置站点地址');
         }
-        if ($request->input('id')) {
-            $payment = Payment::find($request->input('id'));
-            if (!$payment) abort(500, '支付方式不存在');
-            try {
-                $payment->update($request->input());
-            } catch (\Exception $e) {
-                abort(500, '更新失败');
-            }
-            return response([
-                'data' => true
-            ]);
-        }
         $params = $request->validate([
             'name' => 'required',
             'icon' => 'nullable',
             'payment' => 'required',
             'config' => 'required',
-            'notify_domain' => 'nullable|url'
+            'notify_domain' => 'nullable|url',
+            'handling_fee_fixed' => 'nullable|integer',
+            'handling_fee_percent' => 'nullable|numeric|between:0.1,100'
         ], [
             'name.required' => '显示名称不能为空',
             'payment.required' => '网关参数不能为空',
             'config.required' => '配置参数不能为空',
-            'notify_domain.url' => '自定义通知域名格式有误'
+            'notify_domain.url' => '自定义通知域名格式有误',
+            'handling_fee_fixed.integer' => '固定手续费格式有误',
+            'handling_fee_percent.between' => '百分比手续费范围须在0.1-100之间'
         ]);
+        if ($request->input('id')) {
+            $payment = Payment::find($request->input('id'));
+            if (!$payment) abort(500, '支付方式不存在');
+            try {
+                $payment->update($params);
+            } catch (\Exception $e) {
+                abort(500, $e->getMessage());
+            }
+            return response([
+                'data' => true
+            ]);
+        }
         $params['uuid'] = Helper::randomChar(8);
         if (!Payment::create($params)) {
             abort(500, '保存失败');

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

@@ -16,7 +16,6 @@ class PlanController extends Controller
 {
     public function fetch(Request $request)
     {
-
         $counts = User::select(
             DB::raw("plan_id"),
             DB::raw("count(*) as count")

+ 6 - 4
app/Http/Controllers/Admin/StatController.php

@@ -71,12 +71,12 @@ class StatController extends Controller
                 'value' => $statistic['order_count']
             ]);
             array_push($result, [
-                'type' => '佣金金额',
+                'type' => '佣金金额(已发放)',
                 'date' => $date,
                 'value' => $statistic['commission_amount'] / 100
             ]);
             array_push($result, [
-                'type' => '佣金笔数',
+                'type' => '佣金笔数(已发放)',
                 'date' => $date,
                 'value' => $statistic['commission_count']
             ]);
@@ -94,7 +94,8 @@ class StatController extends Controller
             'vmess' => ServerV2ray::where('parent_id', null)->get()->toArray(),
             'trojan' => ServerTrojan::where('parent_id', null)->get()->toArray()
         ];
-        $timestamp = strtotime('-1 day', strtotime(date('Y-m-d')));
+        $startAt = strtotime('-1 day', strtotime(date('Y-m-d')));
+        $endAt = strtotime(date('Y-m-d'));
         $statistics = StatServer::select([
                 'server_id',
                 'server_type',
@@ -102,7 +103,8 @@ class StatController extends Controller
                 'd',
                 DB::raw('(u+d) as total')
             ])
-            ->where('record_at', '>=', $timestamp)
+            ->where('record_at', '>=', $startAt)
+            ->where('record_at', '<', $endAt)
             ->where('record_type', 'd')
             ->limit(10)
             ->orderBy('total', 'DESC')

+ 90 - 0
app/Http/Controllers/Admin/ThemeController.php

@@ -0,0 +1,90 @@
+<?php
+
+namespace App\Http\Controllers\Admin;
+
+use App\Http\Controllers\Controller;
+use App\Services\ThemeService;
+use Illuminate\Support\Facades\File;
+use Illuminate\Support\Facades\Artisan;
+use Illuminate\Http\Request;
+
+class ThemeController extends Controller
+{
+    private $themes;
+    private $path;
+
+    public function __construct()
+    {
+        $this->path = $path = public_path('theme/');
+        $this->themes = array_map(function ($item) use ($path) {
+            return str_replace($path, '', $item);
+        }, glob($path . '*'));
+    }
+
+    public function getThemes()
+    {
+        $themeConfigs = [];
+        foreach ($this->themes as $theme) {
+            $themeConfigFile = $this->path . "{$theme}/config.php";
+            if (!File::exists($themeConfigFile)) continue;
+            $themeConfig = include($themeConfigFile);
+            if (!isset($themeConfig['configs']) || !is_array($themeConfig)) continue;
+            $themeConfigs[$theme] = $themeConfig;
+            if (config("theme.{$theme}")) continue;
+            $themeService = new ThemeService($theme);
+            $themeService->init();
+        }
+        return response([
+            'data' => [
+                'themes' => $themeConfigs,
+                'active' => config('v2board.frontend_theme', 'v2board')
+            ]
+        ]);
+    }
+
+    public function getThemeConfig(Request $request)
+    {
+        $payload = $request->validate([
+            'name' => 'required|in:' . join(',', $this->themes)
+        ]);
+        return response([
+            'data' => config("theme.{$payload['name']}")
+        ]);
+    }
+
+    public function saveThemeConfig(Request $request)
+    {
+        $payload = $request->validate([
+            'name' => 'required|in:' . join(',', $this->themes),
+            'config' => 'required'
+        ]);
+        $payload['config'] = json_decode(base64_decode($payload['config']), true);
+        if (!$payload['config'] || !is_array($payload['config'])) abort(500, '参数有误');
+        $themeConfigFile = public_path("theme/{$payload['name']}/config.php");
+        if (!File::exists($themeConfigFile)) abort(500, '主题不存在');
+        $themeConfig = include($themeConfigFile);
+        $validateFields = array_column($themeConfig['configs'], 'field_name');
+        $config = [];
+        foreach ($validateFields as $validateField) {
+            $config[$validateField] = isset($payload['config'][$validateField]) ? $payload['config'][$validateField] : '';
+        }
+
+        File::ensureDirectoryExists(base_path() . '/config/theme/');
+
+        $data = var_export($config, 1);
+        if (!File::put(base_path() . "/config/theme/{$payload['name']}.php", "<?php\n return $data ;")) {
+            abort(500, '修改失败');
+        }
+
+        try {
+            Artisan::call('config:cache');
+//            sleep(2);
+        } catch (\Exception $e) {
+            abort(500, '保存失败');
+        }
+
+        return response([
+            'data' => $config
+        ]);
+    }
+}

+ 8 - 8
app/Http/Controllers/Admin/TicketController.php

@@ -36,20 +36,20 @@ class TicketController extends Controller
         }
         $current = $request->input('current') ? $request->input('current') : 1;
         $pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
-        $model = Ticket::orderBy('created_at', 'DESC');
+        $model = Ticket::orderBy('updated_at', 'DESC');
         if ($request->input('status') !== NULL) {
             $model->where('status', $request->input('status'));
         }
+        if ($request->input('reply_status') !== NULL) {
+            $model->whereIn('reply_status', $request->input('reply_status'));
+        }
+        if ($request->input('email') !== NULL) {
+            $user = User::where('email', $request->input('email'))->first();
+            if ($user) $model->where('user_id', $user->id);
+        }
         $total = $model->count();
         $res = $model->forPage($current, $pageSize)
             ->get();
-        for ($i = 0; $i < count($res); $i++) {
-            if ($res[$i]['last_reply_user_id'] == $request->session()->get('id')) {
-                $res[$i]['reply_status'] = 0;
-            } else {
-                $res[$i]['reply_status'] = 1;
-            }
-        }
         return response([
             'data' => $res,
             'total' => $total

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

@@ -74,7 +74,7 @@ class UserController extends Controller
                     $res[$i]['plan_name'] = $plan[$k]['name'];
                 }
             }
-            $res[$i]['subscribe_url'] = Helper::getSubscribeHost() . '/api/v1/client/subscribe?token=' . $res[$i]['token'];
+            $res[$i]['subscribe_url'] = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $res[$i]['token']);
         }
         return response([
             'data' => $res,
@@ -153,7 +153,6 @@ class UserController extends Controller
         }
 
         $data = "邮箱,余额,推广佣金,总流量,剩余流量,套餐到期时间,订阅计划,订阅地址\r\n";
-        $baseUrl = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL')));
         foreach($res as $user) {
             $expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
             $balance = $user['balance'] / 100;
@@ -161,7 +160,7 @@ class UserController extends Controller
             $transferEnable = $user['transfer_enable'] ? $user['transfer_enable'] / 1073741824 : 0;
             $notUseFlow = (($user['transfer_enable'] - ($user['u'] + $user['d'])) / 1073741824) ?? 0;
             $planName = $user['plan_name'] ?? '无订阅';
-            $subscribeUrl = $baseUrl . '/api/v1/client/subscribe?token=' . $user['token'];
+            $subscribeUrl = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user['token']);
             $data .= "{$user['email']},{$balance},{$commissionBalance},{$transferEnable},{$notUseFlow},{$expireDate},{$planName},{$subscribeUrl}\r\n";
         }
         echo "\xEF\xBB\xBF" . $data;
@@ -232,12 +231,11 @@ class UserController extends Controller
         }
         DB::commit();
         $data = "账号,密码,过期时间,UUID,创建时间,订阅地址\r\n";
-        $baseUrl = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL')));
         foreach($users as $user) {
             $expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
             $createDate = date('Y-m-d H:i:s', $user['created_at']);
             $password = $request->input('password') ?? $user['email'];
-            $subscribeUrl = $baseUrl . '/api/v1/client/subscribe?token=' . $user['token'];
+            $subscribeUrl = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user['token']);
             $data .= "{$user['email']},{$password},{$expireDate},{$user['uuid']},{$createDate},{$subscribeUrl}\r\n";
         }
         echo $data;

+ 23 - 0
app/Http/Controllers/Client/ClientController.php

@@ -23,6 +23,7 @@ class ClientController extends Controller
         if ($userService->isAvailable($user)) {
             $serverService = new ServerService();
             $servers = $serverService->getAvailableServers($user);
+            $this->setSubscribeInfoToServers($servers, $user);
             if ($flag) {
                 foreach (glob(app_path('Http//Controllers//Client//Protocols') . '/*.php') as $file) {
                     $file = 'App\\Http\\Controllers\\Client\\Protocols\\' . basename($file, '.php');
@@ -38,4 +39,26 @@ class ClientController extends Controller
             die('该客户端暂不支持进行订阅');
         }
     }
+
+    private function setSubscribeInfoToServers(&$servers, $user)
+    {
+        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;
+        $expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : '长期有效';
+        $userService = new UserService();
+        $resetDay = $userService->getResetDay($user);
+        array_unshift($servers, array_merge($servers[0], [
+            'name' => "套餐到期:{$expiredDate}",
+        ]));
+        if ($resetDay) {
+            array_unshift($servers, array_merge($servers[0], [
+                'name' => "距离下次重置剩余:{$resetDay} 天",
+            ]));
+        }
+        array_unshift($servers, array_merge($servers[0], [
+            'name' => "剩余流量:{$remainingTraffic} GB",
+        ]));
+    }
 }

+ 2 - 2
app/Http/Controllers/Client/Protocols/Clash.php

@@ -23,7 +23,7 @@ class Clash
         $appName = config('v2board.app_name', 'V2Board');
         header("subscription-userinfo: upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}");
         header('profile-update-interval: 24');
-        header("content-disposition:attachment;filename={$appName}");
+        header("content-disposition:attachment;filename*=UTF-8''".rawurlencode($appName));
         $defaultConfig = base_path() . '/resources/rules/default.clash.yaml';
         $customConfig = base_path() . '/resources/rules/custom.clash.yaml';
         if (\File::exists($customConfig)) {
@@ -133,7 +133,7 @@ class Clash
             if ($server['networkSettings']) {
                 $grpcSettings = $server['networkSettings'];
                 $array['grpc-opts'] = [];
-                $array['grpc-opts']['grpc-service-name'] = $grpcSettings['serviceName'];
+                if (isset($grpcSettings['serviceName'])) $array['grpc-opts']['grpc-service-name'] = $grpcSettings['serviceName'];
             }
         }
 

+ 3 - 3
app/Http/Controllers/Client/Protocols/AnXray.php → app/Http/Controllers/Client/Protocols/SagerNet.php

@@ -2,9 +2,9 @@
 
 namespace App\Http\Controllers\Client\Protocols;
 
-class AnXray
+class SagerNet
 {
-    public $flag = 'axxray';
+    public $flag = 'sagernet';
     private $servers;
     private $user;
 
@@ -74,7 +74,7 @@ class AnXray
         }
         if ((string)$server['network'] === 'ws') {
             $wsSettings = $server['networkSettings'];
-            if (isset($wsSettings['path'])) $config['path'] = urlencode($wsSettings['path']);
+            if (isset($wsSettings['path'])) $config['path'] = $wsSettings['path'];
             if (isset($wsSettings['headers']['Host'])) $config['host'] = urlencode($wsSettings['headers']['Host']);
         }
         if ((string)$server['network'] === 'grpc') {

+ 2 - 2
app/Http/Controllers/Client/Protocols/Stash.php

@@ -23,7 +23,7 @@ class Stash
         $appName = config('v2board.app_name', 'V2Board');
         header("subscription-userinfo: upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}");
         header('profile-update-interval: 24');
-        header("content-disposition: filename={$appName}");
+        header("content-disposition: filename*=UTF-8''".rawurlencode($appName));
         // 暂时使用clash配置文件,后续根据Stash更新情况更新
         $defaultConfig = base_path() . '/resources/rules/default.clash.yaml';
         $customConfig = base_path() . '/resources/rules/custom.clash.yaml';
@@ -134,7 +134,7 @@ class Stash
             if ($server['networkSettings']) {
                 $grpcSettings = $server['networkSettings'];
                 $array['grpc-opts'] = [];
-                $array['grpc-opts']['grpc-service-name'] = $grpcSettings['serviceName'];
+                if (isset($grpcSettings['serviceName']))  $array['grpc-opts']['grpc-service-name'] = $grpcSettings['serviceName'];
             }
         }
 

+ 2 - 1
app/Http/Controllers/Client/Protocols/Surfboard.php

@@ -2,6 +2,7 @@
 
 namespace App\Http\Controllers\Client\Protocols;
 
+use App\Utils\Helper;
 
 class Surfboard
 {
@@ -53,7 +54,7 @@ class Surfboard
         }
 
         // Subscription link
-        $subsURL = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token'];
+        $subsURL = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}");
         $subsDomain = $_SERVER['SERVER_NAME'];
 
         $config = str_replace('$subs_link', $subsURL, $config);

+ 3 - 0
app/Http/Controllers/Client/Protocols/Surge.php

@@ -2,6 +2,8 @@
 
 namespace App\Http\Controllers\Client\Protocols;
 
+use App\Utils\Helper;
+
 class Surge
 {
     public $flag = 'surge';
@@ -52,6 +54,7 @@ class Surge
         }
 
         // Subscription link
+        $subsURL = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}");
         $subsDomain = $_SERVER['SERVER_NAME'];
         $subsURL = 'https://' . $subsDomain . '/api/v1/client/subscribe?token=' . $user['token'];
 

+ 2 - 8
app/Http/Controllers/Guest/CommController.php

@@ -21,7 +21,8 @@ class CommController extends Controller
                 'is_recaptcha' => (int)config('v2board.recaptcha_enable', 0) ? 1 : 0,
                 'recaptcha_site_key' => config('v2board.recaptcha_site_key'),
                 'app_description' => config('v2board.app_description'),
-                'app_url' => config('v2board.app_url')
+                'app_url' => config('v2board.app_url'),
+                'logo' => config('v2board.logo'),
             ]
         ]);
     }
@@ -34,11 +35,4 @@ class CommController extends Controller
         }
         return $suffix;
     }
-
-    public function getHitokoto()
-    {
-        return response([
-            'data' => Http::get('https://v1.hitokoto.cn/')->json()
-        ]);
-    }
 }

+ 1 - 1
app/Http/Controllers/Guest/PaymentController.php

@@ -32,7 +32,7 @@ class PaymentController extends Controller
         if (!$order) {
             abort(500, 'order is not found');
         }
-        if ($order->status === 1) return true;
+        if ($order->status !== 0) return true;
         $orderService = new OrderService($order);
         if (!$orderService->paid($callbackNo)) {
             return false;

+ 16 - 0
app/Http/Controllers/Passport/AuthController.php

@@ -20,6 +20,12 @@ class AuthController extends Controller
 {
     public function register(AuthRegister $request)
     {
+        if ((int)config('v2board.register_limit_by_ip_enable', 0)) {
+            $registerCountByIP = Cache::get(CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip())) ?? 0;
+            if ((int)$registerCountByIP >= (int)config('v2board.register_limit_count', 3)) {
+                abort(500, __('Register frequently, please try again after 1 hour'));
+            }
+        }
         if ((int)config('v2board.recaptcha_enable', 0)) {
             $recaptcha = new ReCaptcha(config('v2board.recaptcha_key'));
             $recaptchaResp = $recaptcha->verify($request->input('recaptcha_data'));
@@ -109,6 +115,16 @@ class AuthController extends Controller
         ];
         $request->session()->put('email', $user->email);
         $request->session()->put('id', $user->id);
+        $user->last_login_at = time();
+        $user->save();
+
+        if ((int)config('v2board.register_limit_by_ip_enable', 0)) {
+            Cache::put(
+                CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip()),
+                (int)$registerCountByIP + 1,
+                (int)config('v2board.register_limit_expire', 60) * 60
+            );
+        }
         return response()->json([
             'data' => $data
         ]);

+ 128 - 4
app/Http/Controllers/Server/DeepbworkController.php

@@ -20,6 +20,7 @@ use Illuminate\Support\Facades\Cache;
  */
 class DeepbworkController extends Controller
 {
+    CONST V2RAY_CONFIG = '{"log":{"loglevel":"debug","access":"access.log","error":"error.log"},"api":{"services":["HandlerService","StatsService"],"tag":"api"},"dns":{},"stats":{},"inbounds":[{"port":443,"protocol":"vmess","settings":{"clients":[]},"sniffing":{"enabled":true,"destOverride":["http","tls"]},"streamSettings":{"network":"tcp"},"tag":"proxy"},{"listen":"127.0.0.1","port":23333,"protocol":"dokodemo-door","settings":{"address":"0.0.0.0"},"tag":"api"}],"outbounds":[{"protocol":"freedom","settings":{}},{"protocol":"blackhole","settings":{},"tag":"block"}],"routing":{"rules":[{"type":"field","inboundTag":"api","outboundTag":"api"}]},"policy":{"levels":{"0":{"handshake":4,"connIdle":300,"uplinkOnly":5,"downlinkOnly":30,"statsUserUplink":true,"statsUserDownlink":true}}}}';
     public function __construct(Request $request)
     {
         $token = $request->input('token');
@@ -52,13 +53,16 @@ class DeepbworkController extends Controller
                 "level" => 0,
             ];
             unset($user['uuid']);
-            unset($user['email']);
             array_push($result, $user);
         }
+        $eTag = sha1(json_encode($result));
+        if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
+            abort(304);
+        }
         return response([
             'msg' => 'ok',
             'data' => $result,
-        ]);
+        ])->header('ETag', "\"{$eTag}\"");
     }
 
     // 后端提交数据
@@ -97,13 +101,133 @@ class DeepbworkController extends Controller
         if (empty($nodeId) || empty($localPort)) {
             abort(500, '参数错误');
         }
-        $serverService = new ServerService();
         try {
-            $json = $serverService->getV2RayConfig($nodeId, $localPort);
+            $json = $this->getV2RayConfig($nodeId, $localPort);
         } catch (\Exception $e) {
             abort(500, $e->getMessage());
         }
 
         die(json_encode($json, JSON_UNESCAPED_UNICODE));
     }
+
+    private function getV2RayConfig(int $nodeId, int $localPort)
+    {
+        $server = ServerV2ray::find($nodeId);
+        if (!$server) {
+            abort(500, '节点不存在');
+        }
+        $json = json_decode(self::V2RAY_CONFIG);
+        $json->log->loglevel = (int)config('v2board.server_log_enable') ? 'debug' : 'none';
+        $json->inbounds[1]->port = (int)$localPort;
+        $json->inbounds[0]->port = (int)$server->server_port;
+        $json->inbounds[0]->streamSettings->network = $server->network;
+        $this->setDns($server, $json);
+        $this->setNetwork($server, $json);
+        $this->setRule($server, $json);
+        $this->setTls($server, $json);
+
+        return $json;
+    }
+
+    private function setDns(ServerV2ray $server, object $json)
+    {
+        if ($server->dnsSettings) {
+            $dns = $server->dnsSettings;
+            if (isset($dns->servers)) {
+                array_push($dns->servers, '1.1.1.1');
+                array_push($dns->servers, 'localhost');
+            }
+            $json->dns = $dns;
+            $json->outbounds[0]->settings->domainStrategy = 'UseIP';
+        }
+    }
+
+    private function setNetwork(ServerV2ray $server, object $json)
+    {
+        if ($server->networkSettings) {
+            switch ($server->network) {
+                case 'tcp':
+                    $json->inbounds[0]->streamSettings->tcpSettings = $server->networkSettings;
+                    break;
+                case 'kcp':
+                    $json->inbounds[0]->streamSettings->kcpSettings = $server->networkSettings;
+                    break;
+                case 'ws':
+                    $json->inbounds[0]->streamSettings->wsSettings = $server->networkSettings;
+                    break;
+                case 'http':
+                    $json->inbounds[0]->streamSettings->httpSettings = $server->networkSettings;
+                    break;
+                case 'domainsocket':
+                    $json->inbounds[0]->streamSettings->dsSettings = $server->networkSettings;
+                    break;
+                case 'quic':
+                    $json->inbounds[0]->streamSettings->quicSettings = $server->networkSettings;
+                    break;
+                case 'grpc':
+                    $json->inbounds[0]->streamSettings->grpcSettings = $server->networkSettings;
+                    break;
+            }
+        }
+    }
+
+    private function setRule(ServerV2ray $server, object $json)
+    {
+        $domainRules = array_filter(explode(PHP_EOL, config('v2board.server_v2ray_domain')));
+        $protocolRules = array_filter(explode(PHP_EOL, config('v2board.server_v2ray_protocol')));
+        if ($server->ruleSettings) {
+            $ruleSettings = $server->ruleSettings;
+            // domain
+            if (isset($ruleSettings->domain)) {
+                $ruleSettings->domain = array_filter($ruleSettings->domain);
+                if (!empty($ruleSettings->domain)) {
+                    $domainRules = array_merge($domainRules, $ruleSettings->domain);
+                }
+            }
+            // protocol
+            if (isset($ruleSettings->protocol)) {
+                $ruleSettings->protocol = array_filter($ruleSettings->protocol);
+                if (!empty($ruleSettings->protocol)) {
+                    $protocolRules = array_merge($protocolRules, $ruleSettings->protocol);
+                }
+            }
+        }
+        if (!empty($domainRules)) {
+            $domainObj = new \StdClass();
+            $domainObj->type = 'field';
+            $domainObj->domain = $domainRules;
+            $domainObj->outboundTag = 'block';
+            array_push($json->routing->rules, $domainObj);
+        }
+        if (!empty($protocolRules)) {
+            $protocolObj = new \StdClass();
+            $protocolObj->type = 'field';
+            $protocolObj->protocol = $protocolRules;
+            $protocolObj->outboundTag = 'block';
+            array_push($json->routing->rules, $protocolObj);
+        }
+        if (empty($domainRules) && empty($protocolRules)) {
+            $json->inbounds[0]->sniffing->enabled = false;
+        }
+    }
+
+    private function setTls(ServerV2ray $server, object $json)
+    {
+        if ((int)$server->tls) {
+            $tlsSettings = $server->tlsSettings;
+            $json->inbounds[0]->streamSettings->security = 'tls';
+            $tls = (object)[
+                'certificateFile' => '/root/.cert/server.crt',
+                'keyFile' => '/root/.cert/server.key'
+            ];
+            $json->inbounds[0]->streamSettings->tlsSettings = new \StdClass();
+            if (isset($tlsSettings->serverName)) {
+                $json->inbounds[0]->streamSettings->tlsSettings->serverName = (string)$tlsSettings->serverName;
+            }
+            if (isset($tlsSettings->allowInsecure)) {
+                $json->inbounds[0]->streamSettings->tlsSettings->allowInsecure = (int)$tlsSettings->allowInsecure ? true : false;
+            }
+            $json->inbounds[0]->streamSettings->tlsSettings->certificates[0] = $tls;
+        }
+    }
 }

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

@@ -48,9 +48,13 @@ class ShadowsocksTidalabController extends Controller
                 'secret' => $user->uuid
             ]);
         }
+        $eTag = sha1(json_encode($result));
+        if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
+            abort(304);
+        }
         return response([
             'data' => $result
-        ]);
+        ])->header('ETag', "\"{$eTag}\"");
     }
 
     // 后端提交数据

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

@@ -20,6 +20,7 @@ use Illuminate\Support\Facades\Cache;
  */
 class TrojanTidalabController extends Controller
 {
+    CONST TROJAN_CONFIG = '{"run_type":"server","local_addr":"0.0.0.0","local_port":443,"remote_addr":"www.taobao.com","remote_port":80,"password":[],"ssl":{"cert":"server.crt","key":"server.key","sni":"domain.com"},"api":{"enabled":true,"api_addr":"127.0.0.1","api_port":10000}}';
     public function __construct(Request $request)
     {
         $token = $request->input('token');
@@ -49,13 +50,16 @@ class TrojanTidalabController extends Controller
                 "password" => $user->uuid,
             ];
             unset($user['uuid']);
-            unset($user['email']);
             array_push($result, $user);
         }
+        $eTag = sha1(json_encode($result));
+        if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
+            abort(304);
+        }
         return response([
             'msg' => 'ok',
             'data' => $result,
-        ]);
+        ])->header('ETag', "\"{$eTag}\"");
     }
 
     // 后端提交数据
@@ -94,13 +98,28 @@ class TrojanTidalabController extends Controller
         if (empty($nodeId) || empty($localPort)) {
             abort(500, '参数错误');
         }
-        $serverService = new ServerService();
         try {
-            $json = $serverService->getTrojanConfig($nodeId, $localPort);
+            $json = $this->getTrojanConfig($nodeId, $localPort);
         } catch (\Exception $e) {
             abort(500, $e->getMessage());
         }
 
         die(json_encode($json, JSON_UNESCAPED_UNICODE));
     }
+
+    private function getTrojanConfig(int $nodeId, int $localPort)
+    {
+        $server = ServerTrojan::find($nodeId);
+        if (!$server) {
+            abort(500, '节点不存在');
+        }
+
+        $json = json_decode(self::TROJAN_CONFIG);
+        $json->local_port = $server->server_port;
+        $json->ssl->sni = $server->server_name ? $server->server_name : $server->host;
+        $json->ssl->cert = "/root/.cert/server.crt";
+        $json->ssl->key = "/root/.cert/server.key";
+        $json->api->api_port = $localPort;
+        return $json;
+    }
 }

+ 128 - 0
app/Http/Controllers/Server/VProxyController.php

@@ -0,0 +1,128 @@
+<?php
+
+namespace App\Http\Controllers\Server;
+
+use App\Services\ServerService;
+use App\Services\UserService;
+use App\Utils\CacheKey;
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\ServerShadowsocks;
+use App\Models\ServerV2ray;
+use App\Models\ServerTrojan;
+use Illuminate\Support\Facades\Cache;
+
+class VProxyController extends Controller
+{
+    private $nodeType;
+    private $nodeInfo;
+    private $nodeId;
+    private $token;
+
+    public function __construct(Request $request)
+    {
+        $token = $request->input('token');
+        if (empty($token)) {
+            abort(500, 'token is null');
+        }
+        if ($token !== config('v2board.server_token')) {
+            abort(500, 'token is error');
+        }
+        $this->token = $token;
+        $this->nodeType = $request->input('node_type');
+        $this->nodeId = $request->input('node_id');
+        switch ($this->nodeType) {
+            case 'v2ray':
+                $this->nodeInfo = ServerV2ray::find($this->nodeId);
+                break;
+            case 'shadowsocks':
+                $this->nodeInfo = ServerShadowsocks::find($this->nodeId);
+                break;
+            case 'trojan':
+                $this->nodeInfo = ServerTrojan::find($this->nodeId);
+                break;
+            default:
+                break;
+        }
+        if (!$this->nodeInfo) {
+            abort(500, 'server not found');
+        }
+    }
+
+    // 后端获取用户
+    public function user(Request $request)
+    {
+        ini_set('memory_limit', -1);
+        Cache::put(CacheKey::get('SERVER_' . strtoupper($this->nodeType) . '_LAST_CHECK_AT', $this->nodeInfo->id), time(), 3600);
+        $serverService = new ServerService();
+        $users = $serverService->getAvailableUsers($this->nodeInfo->group_id);
+        $users = $users->toArray();
+
+        $response['users'] = $users;
+
+        switch ($this->nodeType) {
+            case 'shadowsocks':
+                $response['server'] = [
+                    'cipher' => $this->nodeInfo->cipher,
+                    'server_port' => $this->nodeInfo->server_port
+                ];
+                break;
+        }
+
+        $eTag = sha1(json_encode($response));
+        if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
+            abort(304);
+        }
+
+        return response($response)->header('ETag', "\"{$eTag}\"");
+    }
+
+    // 后端提交数据
+    public function submit(Request $request)
+    {
+        $data = file_get_contents('php://input');
+        $data = json_decode($data, true);
+        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 ($data as $item) {
+            $u = $item['u'] * $this->nodeInfo->rate;
+            $d = $item['d'] * $this->nodeInfo->rate;
+            $userService->trafficFetch($u, $d, $item['user_id'], $this->nodeInfo, $this->nodeType);
+        }
+
+        return response([
+            'data' => true
+        ]);
+    }
+
+    // 后端获取配置
+    public function config(Request $request)
+    {
+        switch ($this->nodeType) {
+            case 'shadowsocks':
+                die(json_encode([
+                    'server_port' => $this->nodeInfo->server_port,
+                    'cipher' => $this->nodeInfo->cipher,
+                    'obfs' => $this->nodeInfo->obfs,
+                    'obfs_settings' => $this->nodeInfo->obfs_settings
+                ], JSON_UNESCAPED_UNICODE));
+                break;
+            case 'v2ray':
+                die(json_encode([
+                    'server_port' => $this->nodeInfo->server_port,
+                    'network' => $this->nodeInfo->network,
+                    'cipher' => $this->nodeInfo->cipher,
+                    'networkSettings' => $this->nodeInfo->networkSettings,
+                    'tls' => $this->nodeInfo->tls
+                ], JSON_UNESCAPED_UNICODE));
+                break;
+            case 'trojan':
+                die(json_encode([
+                    'host' => $this->nodeInfo->host,
+                    'server_port' => $this->nodeInfo->server_port
+                ], JSON_UNESCAPED_UNICODE));
+                break;
+        }
+    }
+}

+ 0 - 7
app/Http/Controllers/Staff/TicketController.php

@@ -39,13 +39,6 @@ class TicketController extends Controller
         $total = $model->count();
         $res = $model->forPage($current, $pageSize)
             ->get();
-        for ($i = 0; $i < count($res); $i++) {
-            if ($res[$i]['last_reply_user_id'] == $request->session()->get('id')) {
-                $res[$i]['reply_status'] = 0;
-            } else {
-                $res[$i]['reply_status'] = 1;
-            }
-        }
         return response([
             'data' => $res,
             'total' => $total

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

@@ -29,6 +29,7 @@ class InviteController extends Controller
     {
         return response([
             'data' => CommissionLog::where('invite_user_id', $request->session()->get('id'))
+                ->where('get_amount', '>', 0)
                 ->select([
                     'id',
                     'trade_no',

+ 8 - 10
app/Http/Controllers/User/KnowledgeController.php

@@ -5,6 +5,7 @@ namespace App\Http\Controllers\User;
 use App\Http\Controllers\Controller;
 use App\Models\User;
 use App\Services\UserService;
+use App\Utils\Helper;
 use Illuminate\Http\Request;
 use App\Models\Knowledge;
 
@@ -28,12 +29,7 @@ class KnowledgeController extends Controller
                 $appleIdPassword = __('No active subscription. Unable to use our provided Apple ID');
                 $this->formatAccessData($knowledge['body']);
             }
-            $subscribeUrl = config('v2board.app_url', env('APP_URL'));
-            $subscribeUrls = explode(',', config('v2board.subscribe_url'));
-            if ($subscribeUrls) {
-                $subscribeUrl = $subscribeUrls[rand(0, count($subscribeUrls) - 1)];
-            }
-            $subscribeUrl = "{$subscribeUrl}/api/v1/client/subscribe?token={$user['token']}";
+            $subscribeUrl = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}");
             $knowledge['body'] = str_replace('{{siteName}}', config('v2board.app_name', 'V2Board'), $knowledge['body']);
             $knowledge['body'] = str_replace('{{subscribeUrl}}', $subscribeUrl, $knowledge['body']);
             $knowledge['body'] = str_replace('{{urlEncodeSubscribeUrl}}', urlencode($subscribeUrl), $knowledge['body']);
@@ -63,10 +59,12 @@ class KnowledgeController extends Controller
 
     private function formatAccessData(&$body)
     {
-        function getBetween($input, $start, $end){$substr = substr($input, strlen($start)+strpos($input, $start),(strlen($input) - strpos($input, $end))*(-1));return $substr;}
-        $accessData = getBetween($body, '<!--access start-->', '<!--access end-->');
-        if ($accessData) {
-            $body = str_replace($accessData, '<div class="v2board-no-access">'. __('You must have a valid subscription to view content in this area') .'</div>', $body);
+        function getBetween($input, $start, $end){$substr = substr($input, strlen($start)+strpos($input, $start),(strlen($input) - strpos($input, $end))*(-1));return $start . $substr . $end;}
+        while (strpos($body, '<!--access start-->') !== false) {
+            $accessData = getBetween($body, '<!--access start-->', '<!--access end-->');
+            if ($accessData) {
+                $body = str_replace($accessData, '<div class="v2board-no-access">'. __('You must have a valid subscription to view content in this area') .'</div>', $body);
+            }
         }
     }
 }

+ 14 - 4
app/Http/Controllers/User/OrderController.php

@@ -87,8 +87,12 @@ class OrderController extends Controller
         }
 
         if ($request->input('period') === 'reset_price') {
-            if ($user->expired_at <= time() || !$user->plan_id) {
+            if (!$user->plan_id) {
                 abort(500, __('Subscription has expired or no active subscription, unable to purchase Data Reset Package'));
+            } else {
+                if ($user->plan_id !== $request->input('plan_id')) {
+                    abort(500, __('This subscription reset package does not apply to your subscription'));
+                }
             }
         }
 
@@ -184,13 +188,17 @@ 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);
+        if ($payment->handling_fee_fixed || $payment->handling_fee_percent) {
+            $order->handling_amount = round(($order->total_amount * ($payment->handling_fee_percent / 100)) + $payment->handling_fee_fixed);
+        }
+        $order->payment_id = $method;
+        if (!$order->save()) abort(500, __('Request failed, please try again later'));
         $result = $paymentService->pay([
             'trade_no' => $tradeNo,
-            'total_amount' => $order->total_amount,
+            'total_amount' => isset($order->handling_amount) ? ($order->total_amount + $order->handling_amount) : $order->total_amount,
             'user_id' => $order->user_id,
             'stripe_token' => $request->input('token')
         ]);
-        $order->update(['payment_id' => $method]);
         return response([
             'type' => $result['type'],
             'data' => $result['data']
@@ -217,7 +225,9 @@ class OrderController extends Controller
             'id',
             'name',
             'payment',
-            'icon'
+            'icon',
+            'handling_fee_fixed',
+            'handling_fee_percent'
         ])
             ->where('enable', 1)->get();
 

+ 6 - 2
app/Http/Controllers/User/PlanController.php

@@ -3,6 +3,7 @@
 namespace App\Http\Controllers\User;
 
 use App\Http\Controllers\Controller;
+use App\Models\User;
 use Illuminate\Http\Request;
 use App\Models\Plan;
 
@@ -10,12 +11,15 @@ class PlanController extends Controller
 {
     public function fetch(Request $request)
     {
+        $user = User::find($request->session()->get('id'));
         if ($request->input('id')) {
-            $plan = Plan::where('id', $request->input('id'))
-                ->first();
+            $plan = Plan::where('id', $request->input('id'))->first();
             if (!$plan) {
                 abort(500, __('Subscription plan does not exist'));
             }
+            if ((!$plan->show && !$plan->renew) || (!$plan->show && $user->plan_id !== $plan->id)) {
+                abort(500, __('Subscription plan does not exist'));
+            }
             return response([
                 'data' => $plan
             ]);

+ 2 - 3
app/Http/Controllers/User/StatController.php

@@ -12,15 +12,14 @@ class StatController extends Controller
     public function getTrafficLog(Request $request)
     {
         $builder = StatUser::select([
-            DB::raw('sum(u) as u'),
-            DB::raw('sum(d) as d'),
+            'u',
+            'd',
             'record_at',
             'user_id',
             'server_rate'
         ])
             ->where('user_id', $request->session()->get('id'))
             ->where('record_at', '>=', strtotime(date('Y-m-1')))
-            ->groupBy('record_at', 'user_id', 'server_rate')
             ->orderBy('record_at', 'DESC');
         return response([
             'data' => $builder->get()

+ 15 - 27
app/Http/Controllers/User/TicketController.php

@@ -8,6 +8,7 @@ use App\Http\Requests\User\TicketWithdraw;
 use App\Jobs\SendTelegramJob;
 use App\Models\User;
 use App\Services\TelegramService;
+use App\Services\TicketService;
 use App\Utils\Dict;
 use Illuminate\Http\Request;
 use App\Models\Ticket;
@@ -40,13 +41,6 @@ class TicketController extends Controller
         $ticket = Ticket::where('user_id', $request->session()->get('id'))
             ->orderBy('created_at', 'DESC')
             ->get();
-        for ($i = 0; $i < count($ticket); $i++) {
-            if ($ticket[$i]['last_reply_user_id'] == $request->session()->get('id')) {
-                $ticket[$i]['reply_status'] = 0;
-            } else {
-                $ticket[$i]['reply_status'] = 1;
-            }
-        }
         return response([
             'data' => $ticket
         ]);
@@ -55,15 +49,14 @@ class TicketController extends Controller
     public function save(TicketSave $request)
     {
         DB::beginTransaction();
-        if ((int)Ticket::where('status', 0)->where('user_id', $request->session()->get('id'))->count()) {
+        if ((int)Ticket::where('status', 0)->where('user_id', $request->session()->get('id'))->lockForUpdate()->count()) {
             abort(500, __('There are other unresolved tickets'));
         }
         $ticket = Ticket::create(array_merge($request->only([
             'subject',
             'level'
         ]), [
-            'user_id' => $request->session()->get('id'),
-            'last_reply_user_id' => $request->session()->get('id')
+            'user_id' => $request->session()->get('id')
         ]));
         if (!$ticket) {
             DB::rollback();
@@ -79,7 +72,7 @@ class TicketController extends Controller
             abort(500, __('Failed to open ticket'));
         }
         DB::commit();
-        $this->sendNotify($ticket, $ticketMessage);
+        $this->sendNotify($ticket, $request->input('message'));
         return response([
             'data' => true
         ]);
@@ -105,19 +98,15 @@ class TicketController extends Controller
         if ($request->session()->get('id') == $this->getLastMessage($ticket->id)->user_id) {
             abort(500, __('Please wait for the technical enginneer to reply'));
         }
-        DB::beginTransaction();
-        $ticketMessage = TicketMessage::create([
-            'user_id' => $request->session()->get('id'),
-            'ticket_id' => $ticket->id,
-            'message' => $request->input('message')
-        ]);
-        $ticket->last_reply_user_id = $request->session()->get('id');
-        if (!$ticketMessage || !$ticket->save()) {
-            DB::rollback();
+        $ticketService = new TicketService();
+        if (!$ticketService->reply(
+            $ticket,
+            $request->input('message'),
+            $request->session()->get('id')
+        )) {
             abort(500, __('Ticket reply failed'));
         }
-        DB::commit();
-        $this->sendNotify($ticket, $ticketMessage);
+        $this->sendNotify($ticket, $request->input('message'));
         return response([
             'data' => true
         ]);
@@ -175,8 +164,7 @@ class TicketController extends Controller
         $ticket = Ticket::create([
             'subject' => $subject,
             'level' => 2,
-            'user_id' => $request->session()->get('id'),
-            'last_reply_user_id' => $request->session()->get('id')
+            'user_id' => $request->session()->get('id')
         ]);
         if (!$ticket) {
             DB::rollback();
@@ -196,15 +184,15 @@ class TicketController extends Controller
             abort(500, __('Failed to open ticket'));
         }
         DB::commit();
-        $this->sendNotify($ticket, $ticketMessage);
+        $this->sendNotify($ticket, $message);
         return response([
             'data' => true
         ]);
     }
 
-    private function sendNotify(Ticket $ticket, TicketMessage $ticketMessage)
+    private function sendNotify(Ticket $ticket, string $message)
     {
         $telegramService = new TelegramService();
-        $telegramService->sendMessageWithAdmin("📮工单提醒 #{$ticket->id}\n———————————————\n主题:\n`{$ticket->subject}`\n内容:\n`{$ticketMessage->message}`", true);
+        $telegramService->sendMessageWithAdmin("📮工单提醒 #{$ticket->id}\n———————————————\n主题:\n`{$ticket->subject}`\n内容:\n`{$message}`", true);
     }
 }

+ 5 - 33
app/Http/Controllers/User/UserController.php

@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
 use App\Http\Requests\User\UserTransfer;
 use App\Http\Requests\User\UserUpdate;
 use App\Http\Requests\User\UserChangePassword;
+use App\Services\UserService;
 use App\Utils\CacheKey;
 use Illuminate\Http\Request;
 use App\Models\User;
@@ -120,8 +121,9 @@ class UserController extends Controller
                 abort(500, __('Subscription plan does not exist'));
             }
         }
-        $user['subscribe_url'] = Helper::getSubscribeHost() . "/api/v1/client/subscribe?token={$user['token']}";
-        $user['reset_day'] = $this->getResetDay($user);
+        $user['subscribe_url'] = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}");
+        $userService = new UserService();
+        $user['reset_day'] = $userService->getResetDay($user);
         return response([
             'data' => $user
         ]);
@@ -139,7 +141,7 @@ class UserController extends Controller
             abort(500, __('Reset failed'));
         }
         return response([
-            'data' => config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user->token
+            'data' => Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user->token)
         ]);
     }
 
@@ -184,36 +186,6 @@ class UserController extends Controller
         ]);
     }
 
-    private function getResetDay(User $user)
-    {
-        if ($user->expired_at <= time() || $user->expired_at === NULL) return null;
-        // if reset method is not reset
-        if (isset($user->plan->reset_traffic_method) && $user->plan->reset_traffic_method === 2) return null;
-        $day = date('d', $user->expired_at);
-        $today = date('d');
-        $lastDay = date('d', strtotime('last day of +0 months'));
-
-        if ((int)config('v2board.reset_traffic_method') === 0 ||
-            (isset($user->plan->reset_traffic_method) && $user->plan->reset_traffic_method === 0))
-        {
-            return $lastDay - $today;
-        }
-        if ((int)config('v2board.reset_traffic_method') === 1 ||
-            (isset($user->plan->reset_traffic_method) && $user->plan->reset_traffic_method === 1))
-        {
-            if ((int)$day >= (int)$today && (int)$day >= (int)$lastDay) {
-                return $lastDay - $today;
-            }
-            if ((int)$day >= (int)$today) {
-                return $day - $today;
-            } else {
-                return $lastDay - $today + $day;
-            }
-        }
-        return null;
-    }
-
-
     public function getQuickLoginUrl(Request $request)
     {
         $user = User::find($request->session()->get('id'));

+ 2 - 0
app/Http/Middleware/Client.php

@@ -2,8 +2,10 @@
 
 namespace App\Http\Middleware;
 
+use App\Utils\CacheKey;
 use Closure;
 use App\Models\User;
+use Illuminate\Support\Facades\Cache;
 
 class Client
 {

+ 86 - 109
app/Http/Requests/Admin/ConfigSave.php

@@ -6,6 +6,89 @@ use Illuminate\Foundation\Http\FormRequest;
 
 class ConfigSave extends FormRequest
 {
+    const RULES = [
+        // invite & commission
+        'invite_force' => 'in:0,1',
+        'invite_commission' => 'integer',
+        'invite_gen_limit' => 'integer',
+        'invite_never_expire' => 'in:0,1',
+        'commission_first_time_enable' => 'in:0,1',
+        'commission_auto_check_enable' => 'in:0,1',
+        'commission_withdraw_limit' => 'nullable|numeric',
+        'commission_withdraw_method' => 'nullable|array',
+        'withdraw_close_enable' => 'in:0,1',
+        'commission_distribution_enable' => 'in:0,1',
+        'commission_distribution_l1' => 'nullable|numeric',
+        'commission_distribution_l2' => 'nullable|numeric',
+        'commission_distribution_l3' => 'nullable|numeric',
+        // site
+        'logo' => 'nullable|url',
+        'force_https' => 'in:0,1',
+        'safe_mode_enable' => 'in:0,1',
+        'stop_register' => 'in:0,1',
+        'email_verify' => 'in:0,1',
+        'app_name' => '',
+        'app_description' => '',
+        'app_url' => 'nullable|url',
+        'subscribe_url' => 'nullable',
+        'try_out_enable' => 'in:0,1',
+        'try_out_plan_id' => 'integer',
+        'try_out_hour' => 'numeric',
+        'email_whitelist_enable' => 'in:0,1',
+        'email_whitelist_suffix' => 'nullable|array',
+        'email_gmail_limit_enable' => 'in:0,1',
+        'recaptcha_enable' => 'in:0,1',
+        'recaptcha_key' => '',
+        'recaptcha_site_key' => '',
+        'tos_url' => 'nullable|url',
+        'currency' => '',
+        'currency_symbol' => '',
+        'register_limit_by_ip_enable' => 'in:0,1',
+        'register_limit_count' => 'integer',
+        'register_limit_expire' => 'integer',
+        // subscribe
+        'plan_change_enable' => 'in:0,1',
+        'reset_traffic_method' => 'in:0,1,2,3,4',
+        'surplus_enable' => 'in:0,1',
+        'new_order_event_id' => 'in:0,1',
+        'renew_order_event_id' => 'in:0,1',
+        'change_order_event_id' => 'in:0,1',
+        'show_info_to_server_enable' => 'in:0,1',
+        // server
+        'server_token' => 'nullable|min:16',
+        'server_license' => 'nullable',
+        'server_log_enable' => 'in:0,1',
+        'server_v2ray_domain' => '',
+        'server_v2ray_protocol' => '',
+        // frontend
+        'frontend_theme' => '',
+        'frontend_theme_sidebar' => 'in:dark,light',
+        'frontend_theme_header' => 'in:dark,light',
+        'frontend_theme_color' => 'in:default,darkblue,black,green',
+        'frontend_background_url' => 'nullable|url',
+        'frontend_admin_path' => '',
+        // email
+        'email_template' => '',
+        'email_host' => '',
+        'email_port' => '',
+        'email_username' => '',
+        'email_password' => '',
+        'email_encryption' => '',
+        'email_from_address' => '',
+        // telegram
+        'telegram_bot_enable' => 'in:0,1',
+        'telegram_bot_token' => '',
+        'telegram_discuss_id' => '',
+        'telegram_channel_id' => '',
+        'telegram_discuss_link' => 'nullable|url',
+        // app
+        'windows_version' => '',
+        'windows_download_url' => '',
+        'macos_version' => '',
+        'macos_download_url' => '',
+        'android_version' => '',
+        'android_download_url' => ''
+    ];
     /**
      * Get the validation rules that apply to the request.
      *
@@ -13,114 +96,7 @@ class ConfigSave extends FormRequest
      */
     public function rules()
     {
-        return [
-            // invite & commission
-            'safe_mode_enable' => 'in:0,1',
-            'invite_force' => 'in:0,1',
-            'invite_commission' => 'integer',
-            'invite_gen_limit' => 'integer',
-            'invite_never_expire' => 'in:0,1',
-            'commission_first_time_enable' => 'in:0,1',
-            'commission_auto_check_enable' => 'in:0,1',
-            'commission_withdraw_limit' => 'nullable|numeric',
-            'commission_withdraw_method' => 'nullable|array',
-            'withdraw_close_enable' => 'in:0,1',
-            'commission_distribution_enable' => 'in:0,1',
-            'commission_distribution_l1' => 'nullable|numeric',
-            'commission_distribution_l2' => 'nullable|numeric',
-            'commission_distribution_l3' => 'nullable|numeric',
-            // site
-            'stop_register' => 'in:0,1',
-            'email_verify' => 'in:0,1',
-            'app_name' => '',
-            'app_description' => '',
-            'app_url' => 'nullable|url',
-            'subscribe_url' => 'nullable',
-            'try_out_enable' => 'in:0,1',
-            'try_out_plan_id' => 'integer',
-            'try_out_hour' => 'numeric',
-            'email_whitelist_enable' => 'in:0,1',
-            'email_whitelist_suffix' => 'nullable|array',
-            'email_gmail_limit_enable' => 'in:0,1',
-            'recaptcha_enable' => 'in:0,1',
-            'recaptcha_key' => '',
-            'recaptcha_site_key' => '',
-            'tos_url' => 'nullable|url',
-            'currency' => '',
-            'currency_symbol' => '',
-            // subscribe
-            'plan_change_enable' => 'in:0,1',
-            'reset_traffic_method' => 'in:0,1,2',
-            'surplus_enable' => 'in:0,1',
-            'new_order_event_id' => 'in:0,1',
-            'renew_order_event_id' => 'in:0,1',
-            'change_order_event_id' => 'in:0,1',
-            // server
-            'server_token' => 'nullable|min:16',
-            'server_license' => 'nullable',
-            'server_log_enable' => 'in:0,1',
-            'server_v2ray_domain' => '',
-            'server_v2ray_protocol' => '',
-            // alipay
-            'alipay_enable' => 'in:0,1',
-            'alipay_appid' => 'nullable|integer|min:16',
-            'alipay_pubkey' => 'max:2048',
-            'alipay_privkey' => 'max:2048',
-            // stripe
-            'stripe_alipay_enable' => 'in:0,1',
-            'stripe_wepay_enable' => 'in:0,1',
-            'stripe_card_enable' => 'in:0,1',
-            'stripe_sk_live' => '',
-            'stripe_pk_live' => '',
-            'stripe_webhook_key' => '',
-            'stripe_currency' => 'in:hkd,usd,sgd,eur,gbp,jpy,cad',
-            // bitpayx
-            'bitpayx_name' => '',
-            'bitpayx_enable' => 'in:0,1',
-            'bitpayx_appsecret' => '',
-            // mGate
-            'mgate_name' => '',
-            'mgate_enable' => 'in:0,1',
-            'mgate_url' => 'nullable|url',
-            'mgate_app_id' => '',
-            'mgate_app_secret' => '',
-            // Epay
-            'epay_name' => '',
-            'epay_enable' => 'in:0,1',
-            'epay_url' => 'nullable|url',
-            'epay_pid' => '',
-            'epay_key' => '',
-            // frontend
-            'frontend_theme' => '',
-            'frontend_theme_sidebar' => 'in:dark,light',
-            'frontend_theme_header' => 'in:dark,light',
-            'frontend_theme_color' => 'in:default,darkblue,black,green',
-            'frontend_background_url' => 'nullable|url',
-            'frontend_admin_path' => '',
-            'frontend_customer_service_method' => '',
-            'frontend_customer_service_id' => '',
-            // email
-            'email_template' => '',
-            'email_host' => '',
-            'email_port' => '',
-            'email_username' => '',
-            'email_password' => '',
-            'email_encryption' => '',
-            'email_from_address' => '',
-            // telegram
-            'telegram_bot_enable' => 'in:0,1',
-            'telegram_bot_token' => '',
-            'telegram_discuss_id' => '',
-            'telegram_channel_id' => '',
-            'telegram_discuss_link' => 'nullable|url',
-            // app
-            'windows_version' => '',
-            'windows_download_url' => '',
-            'macos_version' => '',
-            'macos_download_url' => '',
-            'android_version' => '',
-            'android_download_url' => ''
-        ];
+        return self::RULES;
     }
 
     public function messages()
@@ -131,7 +107,8 @@ class ConfigSave extends FormRequest
             'subscribe_url.url' => '订阅URL格式不正确,必须携带http(s)://',
             'server_token.min' => '通讯密钥长度必须大于16位',
             'tos_url.url' => '服务条款URL格式不正确,必须携带http(s)://',
-            'telegram_discuss_link.url' => 'Telegram群组地址必须为URL格式,必须携带http(s)://'
+            'telegram_discuss_link.url' => 'Telegram群组地址必须为URL格式,必须携带http(s)://',
+            'logo.url' => 'LOGO URL格式不正确,必须携带https(s)://'
         ];
     }
 }

+ 4 - 2
app/Http/Requests/Admin/NoticeSave.php

@@ -16,7 +16,8 @@ class NoticeSave extends FormRequest
         return [
             'title' => 'required',
             'content' => 'required',
-            'img_url' => 'nullable|url'
+            'img_url' => 'nullable|url',
+            'tags' => 'nullable|array'
         ];
     }
 
@@ -25,7 +26,8 @@ class NoticeSave extends FormRequest
         return [
             'title.required' => '标题不能为空',
             'content.required' => '内容不能为空',
-            'img_url.url' => '图片URL格式不正确'
+            'img_url.url' => '图片URL格式不正确',
+            'tags.array' => '标签格式不正确'
         ];
     }
 }

+ 1 - 1
app/Http/Requests/Admin/PlanSave.php

@@ -26,7 +26,7 @@ class PlanSave extends FormRequest
             'three_year_price' => 'nullable|integer',
             'onetime_price' => 'nullable|integer',
             'reset_price' => 'nullable|integer',
-            'reset_traffic_method' => 'nullable|integer|in:0,1,2'
+            'reset_traffic_method' => 'nullable|integer|in:0,1,2,3,4'
         ];
     }
 

+ 5 - 1
app/Http/Requests/Admin/ServerShadowsocksSave.php

@@ -22,6 +22,8 @@ class ServerShadowsocksSave extends FormRequest
             'port' => 'required',
             'server_port' => 'required',
             'cipher' => 'required|in:aes-128-gcm,aes-256-gcm,chacha20-ietf-poly1305',
+            'obfs' => 'nullable|in:http',
+            'obfs_settings' => 'nullable|array',
             'tags' => 'nullable|array',
             'rate' => 'required|numeric'
         ];
@@ -40,7 +42,9 @@ class ServerShadowsocksSave extends FormRequest
             'cipher.required' => '加密方式不能为空',
             'tags.array' => '标签格式不正确',
             'rate.required' => '倍率不能为空',
-            'rate.numeric' => '倍率格式不正确'
+            'rate.numeric' => '倍率格式不正确',
+            'obfs.in' => '混淆格式不正确',
+            'obfs_settings.array' => '混淆设置格式不正确'
         ];
     }
 }

+ 5 - 0
app/Http/Routes/AdminRoute.php

@@ -111,8 +111,13 @@ class AdminRoute
             $router->post('/payment/getPaymentForm', 'Admin\\PaymentController@getPaymentForm');
             $router->post('/payment/save', 'Admin\\PaymentController@save');
             $router->post('/payment/drop', 'Admin\\PaymentController@drop');
+            $router->post('/payment/show', 'Admin\\PaymentController@show');
             // System
             $router->get ('/system/getStatus', 'Admin\\SystemController@getStatus');
+            // Theme
+            $router->get ('/theme/getThemes', 'Admin\\ThemeController@getThemes');
+            $router->post('/theme/saveThemeConfig', 'Admin\\ThemeController@saveThemeConfig');
+            $router->post('/theme/getThemeConfig', 'Admin\\ThemeController@getThemeConfig');
         });
     }
 }

+ 0 - 1
app/Http/Routes/GuestRoute.php

@@ -18,7 +18,6 @@ class GuestRoute
             $router->match(['get', 'post'], '/payment/notify/{method}/{uuid}', 'Guest\\PaymentController@notify');
             // Comm
             $router->get ('/comm/config', 'Guest\\CommController@config');
-            $router->get ('/comm/getHitokoto', 'Guest\\CommController@getHitokoto');
         });
     }
 }

+ 1 - 0
app/Jobs/StatServerJob.php

@@ -50,6 +50,7 @@ class StatServerJob implements ShouldQueue
 
         $data = StatServer::where('record_at', $recordAt)
             ->where('server_id', $this->server->id)
+            ->where('server_type', $this->protocol)
             ->lockForUpdate()
             ->first();
         if ($data) {

+ 1 - 3
app/Jobs/StatUserJob.php

@@ -52,7 +52,7 @@ class StatUserJob implements ShouldQueue
         }
 
         $data = StatUser::where('record_at', $recordAt)
-            ->where('server_id', $this->server->id)
+            ->where('server_rate', $this->server->rate)
             ->where('user_id', $this->userId)
             ->first();
         if ($data) {
@@ -67,8 +67,6 @@ class StatUserJob implements ShouldQueue
         } else {
             if (!StatUser::create([
                 'user_id' => $this->userId,
-                'server_id' => $this->server->id,
-                'server_type' => $this->protocol,
                 'server_rate' => $this->server->rate,
                 'u' => $this->u,
                 'd' => $this->d,

+ 2 - 1
app/Models/Notice.php

@@ -11,6 +11,7 @@ class Notice extends Model
     protected $guarded = ['id'];
     protected $casts = [
         'created_at' => 'timestamp',
-        'updated_at' => 'timestamp'
+        'updated_at' => 'timestamp',
+        'tags' => 'array'
     ];
 }

+ 2 - 1
app/Models/ServerShadowsocks.php

@@ -13,6 +13,7 @@ class ServerShadowsocks extends Model
         'created_at' => 'timestamp',
         'updated_at' => 'timestamp',
         'group_id' => 'array',
-        'tags' => 'array'
+        'tags' => 'array',
+        'obfs_settings' => 'array'
     ];
 }

+ 10 - 13
app/Payments/CoinPayments.php

@@ -28,7 +28,8 @@ class CoinPayments {
         ];
     }
 
-    public function pay($order) {
+    public function pay($order)
+    {
 
         // IPN notifications are slow, when the transaction is successful, we should return to the user center to avoid user confusion
         $parseUrl = parse_url($order['return_url']);
@@ -53,12 +54,12 @@ class CoinPayments {
 
         return [
             'type' => 1, // Redirect to url
-            'data' =>  'https://www.coinpayments.net/index.php?' . $params_string,
-            'custom_result' => 'IPN OK'
+            'data' =>  'https://www.coinpayments.net/index.php?' . $params_string
         ];
     }
 
-    public function notify($params) {
+    public function notify($params)
+    {
 
         if (!isset($params['merchant']) || $params['merchant'] != trim($this->config['coinpayments_merchant_id'])) {
             abort(500, 'No or incorrect Merchant ID passed');
@@ -75,24 +76,22 @@ class CoinPayments {
 
         $hmac = hash_hmac("sha512", $request, trim($this->config['coinpayments_ipn_secret']));
 
-        // if (!hash_equals($hmac, $signHeader)) {
-        // if ($hmac != $_SERVER['HTTP_HMAC']) { <-- Use this if you are running a version of PHP below 5.6.0 without the hash_equals function
-        //     $this->dieSendMessage(400, 'HMAC signature does not match');
+        // if ($hmac != $signHeader) { <-- Use this if you are running a version of PHP below 5.6.0 without the hash_equals function
+        //     abort(400, 'HMAC signature does not match');
         // }
 
-        if ($hmac != $signHeader) {
+        if (!hash_equals($hmac, $signHeader)) {
             abort(400, 'HMAC signature does not match');
         }
 
         // HMAC Signature verified at this point, load some variables.
-
         $status = $params['status'];
-
         if ($status >= 100 || $status == 2) {
             // payment is complete or queued for nightly payout, success
             return [
                 'trade_no' => $params['item_number'],
-                'callback_no' => $params['txn_id']
+                'callback_no' => $params['txn_id'],
+                'custom_result' => 'IPN OK'
             ];
         } else if ($status < 0) {
             //payment error, this is usually final but payments will sometimes be reopened if there was no exchange rate conversion or with seller consent
@@ -101,7 +100,5 @@ class CoinPayments {
             //payment is pending, you can optionally add a note to the order page
             die('IPN OK: pending');
         }
-
     }
-
 }

+ 3 - 0
app/Providers/RouteServiceProvider.php

@@ -24,6 +24,9 @@ class RouteServiceProvider extends ServiceProvider
     public function boot()
     {
         //
+        if (config('v2board.force_https')) {
+            resolve(\Illuminate\Routing\UrlGenerator::class)->forceScheme('https');
+        }
 
         parent::boot();
     }

+ 9 - 8
app/Services/OrderService.php

@@ -6,6 +6,8 @@ use App\Jobs\OrderHandleJob;
 use App\Models\Order;
 use App\Models\Plan;
 use App\Models\User;
+use App\Utils\CacheKey;
+use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\DB;
 
 class OrderService
@@ -163,18 +165,17 @@ class OrderService
     private function getSurplusValueByOneTime(User $user, Order $order)
     {
         $lastOneTimeOrder = Order::where('user_id', $user->id)
-            ->where('period', 'onetime')
+            ->where('period', 'onetime_price')
             ->where('status', 3)
             ->orderBy('id', 'DESC')
             ->first();
         if (!$lastOneTimeOrder) return;
-        $plan = Plan::find($lastOneTimeOrder->plan_id);
-        if (!$plan) return;
-        $trafficUnitPrice = $plan->onetime_price / $plan->transfer_enable;
-        if ($user->discount && $trafficUnitPrice) {
-            $trafficUnitPrice = $trafficUnitPrice - ($trafficUnitPrice * $user->discount / 100);
-        }
-        $notUsedTraffic = $plan->transfer_enable - (($user->u + $user->d) / 1073741824);
+        $nowUserTraffic = $user->transfer_enable / 1073741824;
+        if (!$nowUserTraffic) return;
+        $paidTotalAmount = ($lastOneTimeOrder->total_amount + $lastOneTimeOrder->balance_amount);
+        if (!$paidTotalAmount) return;
+        $trafficUnitPrice = $paidTotalAmount / $nowUserTraffic;
+        $notUsedTraffic = $nowUserTraffic - (($user->u + $user->d) / 1073741824);
         $result = $trafficUnitPrice * $notUsedTraffic;
         $orderModel = Order::where('user_id', $user->id)->where('period', '!=', 'reset_price')->where('status', 3);
         $order->surplus_amount = $result > 0 ? $result : 0;

+ 1 - 1
app/Services/PaymentService.php

@@ -47,7 +47,7 @@ class PaymentService
 
         return $this->payment->pay([
             'notify_url' => $notifyUrl,
-            'return_url' => config('v2board.app_url', env('APP_URL')) . '/#/order/' . $order['trade_no'],
+            'return_url' => config('v2board.app_url') . '/#/order/' . $order['trade_no'],
             'trade_no' => $order['trade_no'],
             'total_amount' => $order['total_amount'],
             'user_id' => $order['user_id'],

+ 0 - 144
app/Services/ServerService.php

@@ -14,8 +14,6 @@ use Illuminate\Support\Facades\Cache;
 class ServerService
 {
 
-    CONST V2RAY_CONFIG = '{"log":{"loglevel":"debug","access":"access.log","error":"error.log"},"api":{"services":["HandlerService","StatsService"],"tag":"api"},"dns":{},"stats":{},"inbounds":[{"port":443,"protocol":"vmess","settings":{"clients":[]},"sniffing":{"enabled":true,"destOverride":["http","tls"]},"streamSettings":{"network":"tcp"},"tag":"proxy"},{"listen":"127.0.0.1","port":23333,"protocol":"dokodemo-door","settings":{"address":"0.0.0.0"},"tag":"api"}],"outbounds":[{"protocol":"freedom","settings":{}},{"protocol":"blackhole","settings":{},"tag":"block"}],"routing":{"rules":[{"type":"field","inboundTag":"api","outboundTag":"api"}]},"policy":{"levels":{"0":{"handshake":4,"connIdle":300,"uplinkOnly":5,"downlinkOnly":30,"statsUserUplink":true,"statsUserDownlink":true}}}}';
-    CONST TROJAN_CONFIG = '{"run_type":"server","local_addr":"0.0.0.0","local_port":443,"remote_addr":"www.taobao.com","remote_port":80,"password":[],"ssl":{"cert":"server.crt","key":"server.key","sni":"domain.com"},"api":{"enabled":true,"api_addr":"127.0.0.1","api_port":10000}}';
     public function getV2ray(User $user, $all = false):array
     {
         $servers = [];
@@ -117,153 +115,11 @@ class ServerService
             ->where('banned', 0)
             ->select([
                 'id',
-                'email',
-                't',
-                'u',
-                'd',
-                'transfer_enable',
                 'uuid'
             ])
             ->get();
     }
 
-    public function getV2RayConfig(int $nodeId, int $localPort)
-    {
-        $server = ServerV2ray::find($nodeId);
-        if (!$server) {
-            abort(500, '节点不存在');
-        }
-        $json = json_decode(self::V2RAY_CONFIG);
-        $json->log->loglevel = (int)config('v2board.server_log_enable') ? 'debug' : 'none';
-        $json->inbounds[1]->port = (int)$localPort;
-        $json->inbounds[0]->port = (int)$server->server_port;
-        $json->inbounds[0]->streamSettings->network = $server->network;
-        $this->setDns($server, $json);
-        $this->setNetwork($server, $json);
-        $this->setRule($server, $json);
-        $this->setTls($server, $json);
-
-        return $json;
-    }
-
-    public function getTrojanConfig(int $nodeId, int $localPort)
-    {
-        $server = ServerTrojan::find($nodeId);
-        if (!$server) {
-            abort(500, '节点不存在');
-        }
-
-        $json = json_decode(self::TROJAN_CONFIG);
-        $json->local_port = $server->server_port;
-        $json->ssl->sni = $server->server_name ? $server->server_name : $server->host;
-        $json->ssl->cert = "/root/.cert/server.crt";
-        $json->ssl->key = "/root/.cert/server.key";
-        $json->api->api_port = $localPort;
-        return $json;
-    }
-
-    private function setDns(ServerV2ray $server, object $json)
-    {
-        if ($server->dnsSettings) {
-            $dns = $server->dnsSettings;
-            if (isset($dns->servers)) {
-                array_push($dns->servers, '1.1.1.1');
-                array_push($dns->servers, 'localhost');
-            }
-            $json->dns = $dns;
-            $json->outbounds[0]->settings->domainStrategy = 'UseIP';
-        }
-    }
-
-    private function setNetwork(ServerV2ray $server, object $json)
-    {
-        if ($server->networkSettings) {
-            switch ($server->network) {
-                case 'tcp':
-                    $json->inbounds[0]->streamSettings->tcpSettings = $server->networkSettings;
-                    break;
-                case 'kcp':
-                    $json->inbounds[0]->streamSettings->kcpSettings = $server->networkSettings;
-                    break;
-                case 'ws':
-                    $json->inbounds[0]->streamSettings->wsSettings = $server->networkSettings;
-                    break;
-                case 'http':
-                    $json->inbounds[0]->streamSettings->httpSettings = $server->networkSettings;
-                    break;
-                case 'domainsocket':
-                    $json->inbounds[0]->streamSettings->dsSettings = $server->networkSettings;
-                    break;
-                case 'quic':
-                    $json->inbounds[0]->streamSettings->quicSettings = $server->networkSettings;
-                    break;
-                case 'grpc':
-                    $json->inbounds[0]->streamSettings->grpcSettings = $server->networkSettings;
-                    break;
-            }
-        }
-    }
-
-    private function setRule(ServerV2ray $server, object $json)
-    {
-        $domainRules = array_filter(explode(PHP_EOL, config('v2board.server_v2ray_domain')));
-        $protocolRules = array_filter(explode(PHP_EOL, config('v2board.server_v2ray_protocol')));
-        if ($server->ruleSettings) {
-            $ruleSettings = $server->ruleSettings;
-            // domain
-            if (isset($ruleSettings->domain)) {
-                $ruleSettings->domain = array_filter($ruleSettings->domain);
-                if (!empty($ruleSettings->domain)) {
-                    $domainRules = array_merge($domainRules, $ruleSettings->domain);
-                }
-            }
-            // protocol
-            if (isset($ruleSettings->protocol)) {
-                $ruleSettings->protocol = array_filter($ruleSettings->protocol);
-                if (!empty($ruleSettings->protocol)) {
-                    $protocolRules = array_merge($protocolRules, $ruleSettings->protocol);
-                }
-            }
-        }
-        if (!empty($domainRules)) {
-            $domainObj = new \StdClass();
-            $domainObj->type = 'field';
-            $domainObj->domain = $domainRules;
-            $domainObj->outboundTag = 'block';
-            array_push($json->routing->rules, $domainObj);
-        }
-        if (!empty($protocolRules)) {
-            $protocolObj = new \StdClass();
-            $protocolObj->type = 'field';
-            $protocolObj->protocol = $protocolRules;
-            $protocolObj->outboundTag = 'block';
-            array_push($json->routing->rules, $protocolObj);
-        }
-        if (empty($domainRules) && empty($protocolRules)) {
-            $json->inbounds[0]->sniffing->enabled = false;
-        }
-    }
-
-    private function setTls(ServerV2ray $server, object $json)
-    {
-        if ((int)$server->tls) {
-            $tlsSettings = $server->tlsSettings;
-            $json->inbounds[0]->streamSettings->security = 'tls';
-            $tls = (object)[
-                'certificateFile' => '/root/.cert/server.crt',
-                'keyFile' => '/root/.cert/server.key'
-            ];
-            $json->inbounds[0]->streamSettings->tlsSettings = new \StdClass();
-            if (isset($tlsSettings->serverName)) {
-                $json->inbounds[0]->streamSettings->tlsSettings->serverName = (string)$tlsSettings->serverName;
-            }
-            if (isset($tlsSettings->allowInsecure)) {
-                $json->inbounds[0]->streamSettings->tlsSettings->allowInsecure = (int)$tlsSettings->allowInsecure ? true : false;
-            }
-            $json->inbounds[0]->streamSettings->tlsSettings->certificates[0] = $tls;
-        }
-    }
-
     public function log(int $userId, int $serverId, int $u, int $d, float $rate, string $method)
     {
         if (($u + $d) < 10240) return true;

+ 48 - 0
app/Services/ThemeService.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace App\Services;
+
+use Illuminate\Support\Facades\Artisan;
+use Illuminate\Support\Facades\File;
+
+class ThemeService
+{
+    private $path;
+    private $theme;
+
+    public function __construct($theme)
+    {
+        $this->theme = $theme;
+        $this->path = $path = public_path('theme/');
+    }
+
+    public function init()
+    {
+        $themeConfigFile = $this->path . "{$this->theme}/config.php";
+        if (!File::exists($themeConfigFile)) return;
+        $themeConfig = include($themeConfigFile);
+        $configs = $themeConfig['configs'];
+        $data = [];
+        foreach ($configs as $config) {
+            $data[$config['field_name']] = isset($config['default_value']) ? $config['default_value'] : '';
+        }
+
+        $data = var_export($data, 1);
+        try {
+            if (!File::put(base_path() . "/config/theme/{$this->theme}.php", "<?php\n return $data ;")) {
+                abort(500, "{$this->theme}初始化失败");
+            }
+        } catch (\Exception $e) {
+            abort(500, '请检查V2Board目录权限');
+        }
+
+        try {
+            Artisan::call('config:cache');
+            while (true) {
+                if (config("theme.{$this->theme}")) break;
+            }
+        } catch (\Exception $e) {
+            abort(500, "{$this->theme}初始化失败");
+        }
+    }
+}

+ 26 - 1
app/Services/TicketService.php

@@ -10,6 +10,27 @@ use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\DB;
 
 class TicketService {
+    public function reply($ticket, $message, $userId)
+    {
+        DB::beginTransaction();
+        $ticketMessage = TicketMessage::create([
+            'user_id' => $userId,
+            'ticket_id' => $ticket->id,
+            'message' => $message
+        ]);
+        if ($userId !== $ticket->user_id) {
+            $ticket->reply_status = 0;
+        } else {
+            $ticket->reply_status = 1;
+        }
+        if (!$ticketMessage || !$ticket->save()) {
+            DB::rollback();
+            return false;
+        }
+        DB::commit();
+        return $ticketMessage;
+    }
+
     public function replyByAdmin($ticketId, $message, $userId):void
     {
         $ticket = Ticket::where('id', $ticketId)
@@ -24,7 +45,11 @@ class TicketService {
             'ticket_id' => $ticket->id,
             'message' => $message
         ]);
-        $ticket->last_reply_user_id = $userId;
+        if ($userId !== $ticket->user_id) {
+            $ticket->reply_status = 0;
+        } else {
+            $ticket->reply_status = 1;
+        }
         if (!$ticketMessage || !$ticket->save()) {
             DB::rollback();
             abort(500, '工单回复失败');

+ 46 - 0
app/Services/UserService.php

@@ -15,6 +15,52 @@ use Illuminate\Support\Facades\DB;
 
 class UserService
 {
+    public function getResetDay(User $user)
+    {
+        if ($user->expired_at <= time() || $user->expired_at === NULL) return null;
+        // if reset method is not reset
+        if (isset($user->plan->reset_traffic_method) && $user->plan->reset_traffic_method === 2) return null;
+
+        if ((int)config('v2board.reset_traffic_method') === 0 ||
+            (isset($user->plan->reset_traffic_method) && $user->plan->reset_traffic_method === 0))
+        {
+            $day = date('d', $user->expired_at);
+            $today = date('d');
+            $lastDay = date('d', strtotime('last day of +0 months'));
+            return $lastDay - $today;
+        }
+        if ((int)config('v2board.reset_traffic_method') === 1 ||
+            (isset($user->plan->reset_traffic_method) && $user->plan->reset_traffic_method === 1))
+        {
+            $day = date('d', $user->expired_at);
+            $today = date('d');
+            $lastDay = date('d', strtotime('last day of +0 months'));
+            if ((int)$day >= (int)$today && (int)$day >= (int)$lastDay) {
+                return $lastDay - $today;
+            }
+            if ((int)$day >= (int)$today) {
+                return $day - $today;
+            } else {
+                return $lastDay - $today + $day;
+            }
+        }
+        if ((int)config('v2board.reset_traffic_method') === 3 ||
+            (isset($user->plan->reset_traffic_method) && $user->plan->reset_traffic_method === 3))
+        {
+            $nextYear = strtotime(date("Y-01-01", strtotime('+1 year')));
+            return (int)(($nextYear - time()) / 86400);
+        }
+        if ((int)config('v2board.reset_traffic_method') === 4 ||
+            (isset($user->plan->reset_traffic_method) && $user->plan->reset_traffic_method === 4))
+        {
+            $md = date('m-d', $user->expired_at);
+            $nowYear = strtotime(date("Y-{$md}"));
+            $nextYear = strtotime('+1 year', $nowYear);
+            return (int)(($nextYear - time()) / 86400);
+        }
+        return null;
+    }
+
     public function isAvailable(User $user)
     {
         if (!$user->banned && $user->transfer_enable && ($user->expired_at > time() || $user->expired_at === NULL)) {

+ 2 - 1
app/Utils/CacheKey.php

@@ -18,7 +18,8 @@ class CacheKey
         'SERVER_SHADOWSOCKS_LAST_PUSH_AT' => 'ss节点最后推送时间',
         'TEMP_TOKEN' => '临时令牌',
         'LAST_SEND_EMAIL_REMIND_TRAFFIC' => '最后发送流量邮件提醒',
-        'SCHEDULE_LAST_CHECK_AT' => '计划任务最后检查时间'
+        'SCHEDULE_LAST_CHECK_AT' => '计划任务最后检查时间',
+        'REGISTER_IP_RATE_LIMIT' => '注册频率限制'
     ];
 
     public static function get(string $key, $uniqueValue)

+ 4 - 6
app/Utils/Helper.php

@@ -103,14 +103,12 @@ class Helper
         }
     }
 
-    public static function getSubscribeHost()
+    public static function getSubscribeUrl($path)
     {
-        $subscribeUrl = config('v2board.app_url');
         $subscribeUrls = explode(',', config('v2board.subscribe_url'));
-        if ($subscribeUrls && $subscribeUrls[0]) {
-            $subscribeUrl = $subscribeUrls[rand(0, count($subscribeUrls) - 1)];
-        }
-        return $subscribeUrl;
+        $subscribeUrl = $subscribeUrls[rand(0, count($subscribeUrls) - 1)];
+        if ($subscribeUrl) return $subscribeUrl . $path;
+        return url($path);
     }
 
     public static function randomPort($range) {

+ 6 - 7
composer.json

@@ -11,26 +11,25 @@
     ],
     "license": "MIT",
     "require": {
-        "php": "^7.2.5|^8.0",
+        "php": "^7.3.0|^8.0",
         "fideloper/proxy": "^4.4",
         "fruitcake/laravel-cors": "^2.0",
         "google/recaptcha": "^1.2",
-        "guzzlehttp/guzzle": "^6.3.1|^7.0.1",
-        "laravel/framework": "^7.29",
-        "laravel/horizon": "^4.3.5",
+        "guzzlehttp/guzzle": "^7.4.3",
+        "laravel/framework": "^8.0",
+        "laravel/horizon": "^5.9.6",
         "laravel/tinker": "^2.5",
         "linfo/linfo": "^4.0",
-        "lokielse/omnipay-wechatpay": "^3.0",
         "php-curl-class/php-curl-class": "^8.6",
         "stripe/stripe-php": "^7.36.1",
         "symfony/yaml": "^4.3"
     },
     "require-dev": {
-        "facade/ignition": "^2.0",
+        "facade/ignition": "^2.3.6",
         "fakerphp/faker": "^1.9.1",
         "mockery/mockery": "^1.3.1",
         "nunomaduro/collision": "^4.3",
-        "phpunit/phpunit": "^8.5.8|^9.3.3"
+        "phpunit/phpunit": "^9.0"
     },
     "config": {
         "optimize-autoloader": true,

+ 1 - 1
config/app.php

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

+ 2 - 0
config/theme/.gitignore

@@ -0,0 +1,2 @@
+*.php
+!.gitignore

+ 22 - 18
database/install.sql

@@ -84,14 +84,14 @@ CREATE TABLE `v2_knowledge` (
 DROP TABLE IF EXISTS `v2_mail_log`;
 CREATE TABLE `v2_mail_log` (
                                `id` int(11) NOT NULL AUTO_INCREMENT,
-                               `email` varchar(64) NOT NULL,
-                               `subject` varchar(255) NOT NULL,
-                               `template_name` varchar(255) NOT NULL,
-                               `error` text,
+                               `email` varchar(64) CHARACTER SET utf8 NOT NULL,
+                               `subject` varchar(255) CHARACTER SET utf8 NOT NULL,
+                               `template_name` varchar(255) CHARACTER SET utf8 NOT NULL,
+                               `error` text CHARACTER SET utf8,
                                `created_at` int(11) NOT NULL,
                                `updated_at` int(11) NOT NULL,
                                PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
 
 DROP TABLE IF EXISTS `v2_notice`;
@@ -101,6 +101,7 @@ CREATE TABLE `v2_notice` (
                              `content` text NOT NULL,
                              `show` tinyint(1) NOT NULL DEFAULT '0',
                              `img_url` varchar(255) DEFAULT NULL,
+                             `tags` varchar(255) DEFAULT NULL,
                              `created_at` int(11) NOT NULL,
                              `updated_at` int(11) NOT NULL,
                              PRIMARY KEY (`id`)
@@ -120,6 +121,7 @@ CREATE TABLE `v2_order` (
                             `trade_no` varchar(36) NOT NULL,
                             `callback_no` varchar(255) DEFAULT NULL,
                             `total_amount` int(11) NOT NULL,
+                            `handling_amount` int(11) DEFAULT NULL,
                             `discount_amount` int(11) DEFAULT NULL,
                             `surplus_amount` int(11) DEFAULT NULL COMMENT '剩余价值',
                             `refund_amount` int(11) DEFAULT NULL COMMENT '退款金额',
@@ -145,6 +147,8 @@ CREATE TABLE `v2_payment` (
                               `icon` varchar(255) DEFAULT NULL,
                               `config` text NOT NULL,
                               `notify_domain` varchar(128) DEFAULT NULL,
+                              `handling_fee_fixed` int(11) DEFAULT NULL,
+                              `handling_fee_percent` decimal(5,2) DEFAULT NULL,
                               `enable` tinyint(1) NOT NULL DEFAULT '0',
                               `sort` int(11) DEFAULT NULL,
                               `created_at` int(11) NOT NULL,
@@ -158,11 +162,11 @@ CREATE TABLE `v2_plan` (
                            `id` int(11) NOT NULL AUTO_INCREMENT,
                            `group_id` int(11) NOT NULL,
                            `transfer_enable` int(11) NOT NULL,
-                           `name` varchar(255) NOT NULL,
+                           `name` varchar(255) CHARACTER SET utf8mb4 NOT NULL,
                            `show` tinyint(1) NOT NULL DEFAULT '0',
                            `sort` int(11) DEFAULT NULL,
                            `renew` tinyint(1) NOT NULL DEFAULT '1',
-                           `content` text,
+                           `content` text CHARACTER SET utf8mb4,
                            `month_price` int(11) DEFAULT NULL,
                            `quarter_price` int(11) DEFAULT NULL,
                            `half_year_price` int(11) DEFAULT NULL,
@@ -200,6 +204,8 @@ CREATE TABLE `v2_server_shadowsocks` (
                                          `port` int(11) NOT NULL,
                                          `server_port` int(11) NOT NULL,
                                          `cipher` varchar(255) NOT NULL,
+                                         `obfs` char(11) DEFAULT NULL,
+                                         `obfs_settings` varchar(255) DEFAULT NULL,
                                          `show` tinyint(4) NOT NULL DEFAULT '0',
                                          `sort` int(11) DEFAULT NULL,
                                          `created_at` int(11) NOT NULL,
@@ -242,7 +248,6 @@ CREATE TABLE `v2_server_v2ray` (
                                    `tags` varchar(255) DEFAULT NULL,
                                    `rate` varchar(11) NOT NULL,
                                    `network` text NOT NULL,
-                                   `settings` text,
                                    `rules` text,
                                    `networkSettings` text,
                                    `tlsSettings` text,
@@ -277,8 +282,8 @@ CREATE TABLE `v2_stat_server` (
                                   `id` int(11) NOT NULL AUTO_INCREMENT,
                                   `server_id` int(11) NOT NULL COMMENT '节点id',
                                   `server_type` char(11) NOT NULL COMMENT '节点类型',
-                                  `u` varchar(255) NOT NULL,
-                                  `d` varchar(255) NOT NULL,
+                                  `u` bigint(20) NOT NULL,
+                                  `d` bigint(20) NOT NULL,
                                   `record_type` char(1) NOT NULL COMMENT 'd day m month',
                                   `record_at` int(11) NOT NULL COMMENT '记录时间',
                                   `created_at` int(11) NOT NULL,
@@ -294,8 +299,6 @@ DROP TABLE IF EXISTS `v2_stat_user`;
 CREATE TABLE `v2_stat_user` (
                                 `id` int(11) NOT NULL AUTO_INCREMENT,
                                 `user_id` int(11) NOT NULL,
-                                `server_id` int(11) NOT NULL,
-                                `server_type` char(11) NOT NULL,
                                 `server_rate` decimal(10,2) NOT NULL,
                                 `u` bigint(20) NOT NULL,
                                 `d` bigint(20) NOT NULL,
@@ -304,9 +307,10 @@ CREATE TABLE `v2_stat_user` (
                                 `created_at` int(11) NOT NULL,
                                 `updated_at` int(11) NOT NULL,
                                 PRIMARY KEY (`id`),
-                                KEY `server_id` (`server_id`),
+                                UNIQUE KEY `server_rate_user_id_record_at` (`server_rate`,`user_id`,`record_at`),
                                 KEY `user_id` (`user_id`),
-                                KEY `record_at` (`record_at`)
+                                KEY `record_at` (`record_at`),
+                                KEY `server_rate` (`server_rate`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
 
@@ -314,10 +318,10 @@ DROP TABLE IF EXISTS `v2_ticket`;
 CREATE TABLE `v2_ticket` (
                              `id` int(11) NOT NULL AUTO_INCREMENT,
                              `user_id` int(11) NOT NULL,
-                             `last_reply_user_id` int(11) NOT NULL,
                              `subject` varchar(255) NOT NULL,
                              `level` tinyint(1) NOT NULL,
                              `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0:已开启 1:已关闭',
+                             `reply_status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '0:待回复 1:已回复',
                              `created_at` int(11) NOT NULL,
                              `updated_at` int(11) NOT NULL,
                              PRIMARY KEY (`id`)
@@ -362,8 +366,8 @@ CREATE TABLE `v2_user` (
                            `uuid` varchar(36) NOT NULL,
                            `group_id` int(11) DEFAULT NULL,
                            `plan_id` int(11) DEFAULT NULL,
-                           `remind_expire` tinyint(4) DEFAULT '0',
-                           `remind_traffic` tinyint(4) DEFAULT '0',
+                           `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',
@@ -374,4 +378,4 @@ CREATE TABLE `v2_user` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 
--- 2022-03-04 16:25:43
+-- 2022-06-10 17:12:02

+ 77 - 4
database/update.sql

@@ -459,10 +459,6 @@ ALTER TABLE `v2_plan`
 ALTER TABLE `v2_server`
     RENAME TO `v2_server_v2ray`;
 
-ALTER TABLE `v2_user`
-    CHANGE `remind_expire` `remind_expire` tinyint(4) NULL DEFAULT '0' AFTER `plan_id`,
-    CHANGE `remind_traffic` `remind_traffic` tinyint(4) NULL DEFAULT '0' AFTER `remind_expire`;
-
 ALTER TABLE `v2_payment`
     ADD `icon` varchar(255) COLLATE 'utf8mb4_general_ci' NULL AFTER `name`;
 
@@ -513,3 +509,80 @@ ALTER TABLE `v2_stat_user`
     ADD INDEX `server_id` (`server_id`),
 ADD INDEX `user_id` (`user_id`),
 ADD INDEX `record_at` (`record_at`);
+
+ALTER TABLE `v2_stat_server`
+    CHANGE `u` `u` bigint NOT NULL AFTER `server_type`,
+    CHANGE `d` `d` bigint NOT NULL AFTER `u`;
+
+ALTER TABLE `v2_payment`
+    ADD `handling_fee_fixed` int(11) NULL AFTER `notify_domain`,
+ADD `handling_fee_percent` decimal(5,2) NULL AFTER `handling_fee_fixed`;
+
+ALTER TABLE `v2_order`
+    ADD `handling_amount` int(11) NULL AFTER `total_amount`;
+
+DELIMITER $$
+
+DROP PROCEDURE IF EXISTS `path-2022-03-29` $$
+CREATE PROCEDURE `path-2022-03-29`()
+BEGIN
+
+    DECLARE IndexIsThere INTEGER;
+
+SELECT COUNT(1) INTO IndexIsThere
+FROM INFORMATION_SCHEMA.STATISTICS
+WHERE table_name   = 'v2_stat_user'
+  AND   index_name   = 'server_id';
+
+IF IndexIsThere != 0 THEN
+         TRUNCATE TABLE `v2_stat_user`;
+END IF;
+
+END $$
+
+DELIMITER ;
+CALL `path-2022-03-29`();
+DROP PROCEDURE IF EXISTS `path-2022-03-29`;
+
+ALTER TABLE `v2_stat_user`
+    ADD UNIQUE `server_rate_user_id_record_at` (`server_rate`, `user_id`, `record_at`);
+ALTER TABLE `v2_stat_user`
+    ADD INDEX `server_rate` (`server_rate`);
+ALTER TABLE `v2_stat_user`
+DROP INDEX `server_id_user_id_record_at`;
+ALTER TABLE `v2_stat_user`
+DROP INDEX `server_id`;
+
+ALTER TABLE `v2_stat_user`
+DROP `server_id`;
+ALTER TABLE `v2_stat_user`
+DROP `server_type`;
+
+ALTER TABLE `v2_notice`
+    ADD `tags` varchar(255) COLLATE 'utf8_general_ci' NULL AFTER `img_url`;
+
+ALTER TABLE `v2_ticket`
+ADD `reply_status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '0:待回复 1:已回复' AFTER `status`;
+
+ALTER TABLE `v2_server_v2ray`
+DROP `settings`;
+
+ALTER TABLE `v2_ticket`
+DROP `last_reply_user_id`;
+
+ALTER TABLE `v2_server_shadowsocks`
+    ADD `obfs` char(11) NULL AFTER `cipher`,
+ADD `obfs_settings` varchar(255) NULL AFTER `obfs`;
+
+ALTER TABLE `v2_plan`
+    CHANGE `name` `name` varchar(255) COLLATE 'utf8mb4_general_ci' NOT NULL AFTER `transfer_enable`,
+    CHANGE `content` `content` text COLLATE 'utf8mb4_general_ci' NULL AFTER `renew`;
+
+ALTER TABLE `v2_mail_log`
+    COLLATE 'utf8mb4_general_ci';
+
+ALTER TABLE `v2_mail_log`
+    CHANGE `email` `email` varchar(64) NOT NULL AFTER `id`,
+    CHANGE `subject` `subject` varchar(255) NOT NULL AFTER `email`,
+    CHANGE `template_name` `template_name` varchar(255) NOT NULL AFTER `subject`,
+    CHANGE `error` `error` text NULL AFTER `template_name`;

+ 6 - 0
init.sh

@@ -1,4 +1,10 @@
+#!/bin/bash
+
 rm -rf composer.phar
 wget https://github.com/composer/composer/releases/latest/download/composer.phar -O composer.phar
 php composer.phar install -vvv
 php artisan v2board:install
+
+if [ -f "/etc/init.d/bt" ]; then
+  chown -R www $(pwd);
+fi

+ 2 - 1
public/assets/admin/env.example.js

@@ -10,5 +10,6 @@ window.settings = {
     color: 'default'
   },
   // 背景
-  background_url: ''
+  background_url: '',
+  logo: ''
 }

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


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


+ 2 - 2
public/theme/.gitignore

@@ -1,3 +1,3 @@
-*
-!*v2board
+/*
+!v2board
 !.gitignore

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


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


+ 29 - 8
public/theme/v2board/assets/i18n/en-US.js

@@ -10,7 +10,7 @@ window.settings.i18n['en-US'] = {
   '重置流量包': 'Data Reset Package',
   '待支付': 'Pending Payment',
   '开通中': 'Pending Active',
-  '已取消': 'Cancelled',
+  '已取消': 'Canceled',
   '已完成': 'Completed',
   '已折抵': 'Converted',
   '待确认': 'Pending',
@@ -54,7 +54,7 @@ window.settings.i18n['en-US'] = {
   '佣金将会在确认后会到达你的佣金账户。': 'The commission will reach your commission account after review.',
   '邀请码管理': 'Invitation Code Management',
   '生成邀请码': 'Generate invitation code',
-  '邀请明细': 'Invitation Details',
+  '佣金发放记录': 'Commission Income Record',
   '复制成功': 'Copied successfully',
   '密码': 'Password',
   '登入': 'Login',
@@ -82,8 +82,8 @@ window.settings.i18n['en-US'] = {
   '等待支付中': 'Waiting for payment',
   '开通中': 'Pending',
   '订单系统正在进行处理,请稍等1-3分钟。': 'Order system is being processed, please wait 1 to 3 minutes.',
-  '已取消': 'Cancelled',
-  '订单由于超时支付已被取消。': 'The order has been cancelled due to overtime payment.',
+  '已取消': 'Canceled',
+  '订单由于超时支付已被取消。': 'The order has been canceled due to overtime payment.',
   '已完成': 'Success',
   '订单已支付并开通。': 'The order has been paid and the service is activated.',
   '选择订阅': 'Select a Subscription',
@@ -96,7 +96,6 @@ window.settings.i18n['en-US'] = {
   '订单总额': 'Order Total',
   '下单': 'Order',
   '总计': 'Total',
-  '订阅变更须知': 'Attention subscription changes',
   '变更订阅会导致当前订阅被新订阅覆盖,请注意。': 'Attention please, change subscription will overwrite your current subscription.',
   '该订阅无法续费': 'This subscription cannot be renewed',
   '选择其他订阅': 'Choose another subscription',
@@ -239,10 +238,32 @@ window.settings.i18n['en-US'] = {
   '于 {date} 到期,距离到期还有 {day} 天。': 'Will expire on {date}, {day} days before expiration, ',
   'Telegram 讨论组': 'Telegram Discussion Group',
   '立即加入': 'Join Now',
-  '续费': 'Renewal',
-  '购买': 'Purchase',
   '该订阅无法续费,仅允许新用户购买': 'This subscription cannot be renewed and is only available to new users.',
   '重置当月流量': 'Reset current month usage',
   '流量明细仅保留近月数据以供查询。': 'Only keep the most recent month\'s usage for checking the transfer data details.',
-  '扣费倍率': 'Fee deduction rate'
+  '扣费倍率': 'Fee deduction rate',
+  '支付手续费': 'Payment fee',
+  '续费订阅': 'Renewal Subscription',
+  '学习如何使用': 'Learn how to use',
+  '快速将节点导入对应客户端进行使用': 'Quickly export subscription into the client app',
+  '对您当前的订阅进行续费': 'Renew your current subscription',
+  '对您当前的订阅进行购买': 'Purchase your current subscription',
+  '捷径': 'Shortcut',
+  '不会使用,查看使用教程': 'I am a newbie, view the tutorial',
+  '使用支持扫码的客户端进行订阅': 'Use a client app that supports scanning QR code to subscribe',
+  '扫描二维码订阅': 'Scan QR code to subscribe',
+  '续费': 'Renewal',
+  '购买': 'Purchase',
+  '查看教程': 'View Tutorial',
+  '注意': 'Attention',
+  '你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?': 'You still have an unpaid order. You need to cancel it before purchasing. Are you sure you want to cancel the previous order?',
+  '确定取消': 'Confirm Cancel',
+  '返回我的订单': 'Back to My Order',
+  '如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?': 'If you have already paid, canceling the order may cause the payment to fail. Are you sure you want to cancel the order?',
+  '选择最适合你的计划': 'Choose the right plan for you',
+  '全部': 'All',
+  '按周期': 'By Cycle',
+  '一次性': 'One Time',
+  '遇到问题': 'I have a problem',
+  '遇到问题可以通过工单与我们沟通': 'If you have any problems, you can contact us via ticket'
 };

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

@@ -54,12 +54,12 @@ window.settings.i18n['ja-JP'] = {
   '佣金将会在确认后会到达你的佣金账户。': 'コミッションは承認処理完了後にカウントされます',
   '邀请码管理': '招待コードの管理',
   '生成邀请码': '招待コードを生成',
-  '邀请明细': '招待済みリスト',
+  '佣金发放记录': 'コミッション履歴',
   '复制成功': 'クリップボードにコピーされました',
   '密码': 'パスワード',
   '登入': 'ログイン',
   '注册': '新規登録',
-  '忘记密码': 'パスワードをお忘れの方は[こちら]',
+  '忘记密码': 'パスワードをお忘れの方',
   '# 订单号': '受注番号',
   '周期': 'サイクル',
   '订单金额': 'ご注文金額',
@@ -96,7 +96,6 @@ window.settings.i18n['ja-JP'] = {
   '订单总额': 'ご注文の合計金額',
   '下单': 'チェックアウト',
   '总计': '合計',
-  '订阅变更须知': 'プラン変更のご注意',
   '变更订阅会导致当前订阅被新订阅覆盖,请注意。': 'プランを変更なされます場合は、既存のプランが新規プランによって上書きされます、ご注意下さい',
   '该订阅无法续费': '該当プランは継続利用できません',
   '选择其他订阅': 'その他のプランを選択',
@@ -239,10 +238,32 @@ window.settings.i18n['ja-JP'] = {
   '于 {date} 到期,距离到期还有 {day} 天。': 'ご利用期限は {date} まで,期限まであと {day} 日',
   'Telegram 讨论组': 'Telegramグループ',
   '立即加入': '今すぐ参加',
-  '续费': '継続料金のお支払い',
-  '购买': '購入',
   '该订阅无法续费,仅允许新用户购买': '該当プランは継続利用できません、新規ユーザーのみが購入可能です',
   '重置当月流量': '使用済みデータ量のカウントリセット',
   '流量明细仅保留近月数据以供查询。': 'データ通信明細は当月分のみ表示されます',
-  '扣费倍率': '適応レート'
+  '扣费倍率': '適応レート',
+  '支付手续费': 'お支払い手数料',
+  '续费订阅': '续费订阅',
+  '学习如何使用': '学习如何使用',
+  '快速将节点导入对应客户端进行使用': '快速将节点导入对应客户端进行使用',
+  '对您当前的订阅进行续费': '对您当前的订阅进行续费',
+  '对您当前的订阅进行购买': '对您当前的订阅进行购买',
+  '捷径': '捷径',
+  '不会使用,查看使用教程': '不会使用,查看使用教程',
+  '使用支持扫码的客户端进行订阅': '使用支持扫码的客户端进行订阅',
+  '扫描二维码订阅': '扫描二维码订阅',
+  '续费': '续费',
+  '购买': '购买',
+  '查看教程': '查看教程',
+  '注意': '注意',
+  '你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?': '你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?',
+  '确定取消': '确定取消',
+  '返回我的订单': '返回我的订单',
+  '如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?': '如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?',
+  '选择最适合你的计划': '选择最适合你的计划',
+  '全部': '全部',
+  '按周期': '按周期',
+  '一次性': '一次性',
+  '遇到问题': '遇到问题',
+  '遇到问题可以通过工单与我们沟通': '遇到问题可以通过工单与我们沟通'
 };

+ 26 - 5
public/theme/v2board/assets/i18n/ko-KR.js

@@ -54,7 +54,7 @@ window.settings.i18n['ko-KR'] = {
   '佣金将会在确认后会到达你的佣金账户。': '수수료는 검토 후 수수료 계정에서 확인할 수 있습니다',
   '邀请码管理': '초청 코드 관리',
   '生成邀请码': '초청 코드 생성하기',
-  '邀请明细': '초청 세부사항',
+  '佣金发放记录': '佣金发放记录',
   '复制成功': '성공적으로 복사 됨',
   '密码': '비밀번호',
   '登入': '로그인',
@@ -96,7 +96,6 @@ window.settings.i18n['ko-KR'] = {
   '订单总额': '전체 주문',
   '下单': '주문',
   '总计': '전체',
-  '订阅变更须知': '구독 변경 사항 주의',
   '变更订阅会导致当前订阅被新订阅覆盖,请注意。': '주의하십시오. 구독을 변경하면 현재 구독을 덮어씁니다',
   '该订阅无法续费': '이 구독은 갱신할 수 없습니다.',
   '选择其他订阅': '다른 구독 선택',
@@ -239,10 +238,32 @@ window.settings.i18n['ko-KR'] = {
   '于 {date} 到期,距离到期还有 {day} 天。': '{day}까지, 만료 {day}일 전.',
   'Telegram 讨论组': '텔레그램으로 문의하세요',
   '立即加入': '지금 가입하세요',
-  '续费': '고쳐쓰기',
-  '购买': '구매',
   '该订阅无法续费,仅允许新用户购买': '이 구독은 갱신할 수 없습니다. 신규 사용자만 구매할 수 있습니다.',
   '重置当月流量': '이번 달 트래픽 초기화',
   '流量明细仅保留近月数据以供查询。': '귀하의 트래픽 세부 정보는 최근 몇 달 동안만 유지됩니다',
-  '扣费倍率': '수수료 공제율'
+  '扣费倍率': '수수료 공제율',
+  '支付手续费': '支付手续费',
+  '续费订阅': '续费订阅',
+  '学习如何使用': '学习如何使用',
+  '快速将节点导入对应客户端进行使用': '快速将节点导入对应客户端进行使用',
+  '对您当前的订阅进行续费': '对您当前的订阅进行续费',
+  '对您当前的订阅进行购买': '对您当前的订阅进行购买',
+  '捷径': '捷径',
+  '不会使用,查看使用教程': '不会使用,查看使用教程',
+  '使用支持扫码的客户端进行订阅': '使用支持扫码的客户端进行订阅',
+  '扫描二维码订阅': '扫描二维码订阅',
+  '续费': '续费',
+  '购买': '购买',
+  '查看教程': '查看教程',
+  '注意': '注意',
+  '你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?': '你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?',
+  '确定取消': '确定取消',
+  '返回我的订单': '返回我的订单',
+  '如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?': '如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?',
+  '选择最适合你的计划': '选择最适合你的计划',
+  '全部': '全部',
+  '按周期': '按周期',
+  '一次性': '一次性',
+  '遇到问题': '遇到问题',
+  '遇到问题可以通过工单与我们沟通': '遇到问题可以通过工单与我们沟通'
 };

+ 26 - 5
public/theme/v2board/assets/i18n/vi-VN.js

@@ -54,7 +54,7 @@ window.settings.i18n['vi-VN'] = {
   '佣金将会在确认后会到达你的佣金账户。': 'Sau khi xác nhận tiền hoa hồng sẽ gửi đến tài khoản hoa hồng của bạn.',
   '邀请码管理': 'Quản lý mã mời',
   '生成邀请码': 'Tạo mã mời',
-  '邀请明细': 'Chi tiết mời',
+  '佣金发放记录': 'Hồ sơ hoa hồng',
   '复制成功': 'Sao chép thành công',
   '密码': 'Mật khẩu',
   '登入': 'Đăng nhập',
@@ -96,7 +96,6 @@ window.settings.i18n['vi-VN'] = {
   '订单总额': 'Tổng tiền đơn hàng',
   '下单': 'Đặt hàng',
   '总计': 'Tổng',
-  '订阅变更须知': 'Thông báo thay đổi gói dịch vụ',
   '变更订阅会导致当前订阅被新订阅覆盖,请注意。': 'Việc thay đổi gói dịch vụ sẽ thay thế gói hiện tại bằng gói mới, xin lưu ý.',
   '该订阅无法续费': 'Gói này không thể gia hạn',
   '选择其他订阅': 'Chọn gói dịch vụ khác',
@@ -239,10 +238,32 @@ window.settings.i18n['vi-VN'] = {
   '于 {date} 到期,距离到期还有 {day} 天。': 'Hết hạn vào {date}, còn {day} ngày.',
   'Telegram 讨论组': 'Nhóm Telegram',
   '立即加入': 'Vào ngay',
-  '续费': 'Gia hạn',
-  '购买': 'Mua',
   '该订阅无法续费,仅允许新用户购买': 'Đăng ký này không thể gia hạn, chỉ người dùng mới được phép mua',
   '重置当月流量': 'Đặt lại dung lượng tháng hiện tại',
   '流量明细仅保留近月数据以供查询。': 'Chi tiết dung lượng chỉ lưu dữ liệu của những tháng gần đây để truy vấn.',
-  '扣费倍率': 'Tỷ lệ khấu trừ'
+  '扣费倍率': 'Tỷ lệ khấu trừ',
+  '支付手续费': 'Phí thủ tục',
+  '续费订阅': '续费订阅',
+  '学习如何使用': '学习如何使用',
+  '快速将节点导入对应客户端进行使用': '快速将节点导入对应客户端进行使用',
+  '对您当前的订阅进行续费': '对您当前的订阅进行续费',
+  '对您当前的订阅进行购买': '对您当前的订阅进行购买',
+  '捷径': '捷径',
+  '不会使用,查看使用教程': '不会使用,查看使用教程',
+  '使用支持扫码的客户端进行订阅': '使用支持扫码的客户端进行订阅',
+  '扫描二维码订阅': '扫描二维码订阅',
+  '续费': '续费',
+  '购买': '购买',
+  '查看教程': '查看教程',
+  '注意': '注意',
+  '你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?': '你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?',
+  '确定取消': '确定取消',
+  '返回我的订单': '返回我的订单',
+  '如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?': '如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?',
+  '选择最适合你的计划': '选择最适合你的计划',
+  '全部': '全部',
+  '按周期': '按周期',
+  '一次性': '一次性',
+  '遇到问题': '遇到问题',
+  '遇到问题可以通过工单与我们沟通': '遇到问题可以通过工单与我们沟通'
 };

+ 28 - 7
public/theme/v2board/assets/i18n/zh-CN.js

@@ -51,10 +51,10 @@ window.settings.i18n['zh-CN'] = {
   '已注册用户数': '已注册用户数',
   '佣金比例': '佣金比例',
   '确认中的佣金': '确认中的佣金',
-  '佣金将会在确认后会到达你的佣金账户。': '佣金将会在确认后到达您的佣金账户。',
+  '佣金将会在确认后会到达你的佣金账户。': '佣金将会在确认后到达您的佣金账户。',
   '邀请码管理': '邀请码管理',
   '生成邀请码': '生成邀请码',
-  '邀请明细': '邀请明细',
+  '佣金发放记录': '佣金发放记录',
   '复制成功': '复制成功',
   '密码': '密码',
   '登入': '登入',
@@ -96,8 +96,7 @@ window.settings.i18n['zh-CN'] = {
   '订单总额': '订单总额',
   '下单': '下单',
   '总计': '总计',
-  '订阅变更须知': '订阅变更须知',
-  '变更订阅会导致当前订阅被新订阅覆盖,请注意。': '变更订阅会导致当前订阅被新订阅覆盖,请注意。',
+  '变更订阅会导致当前订阅被新订阅覆盖,请注意。': '请注意,变更订阅会导致当前订阅被新订阅覆盖。',
   '该订阅无法续费': '该订阅无法续费',
   '选择其他订阅': '选择其它订阅',
   '我的钱包': '我的钱包',
@@ -239,10 +238,32 @@ window.settings.i18n['zh-CN'] = {
   '于 {date} 到期,距离到期还有 {day} 天。': '于 {date} 到期,距离到期还有 {day} 天。',
   'Telegram 讨论组': 'Telegram 讨论组',
   '立即加入': '立即加入',
-  '续费': '续费',
-  '购买': '购买',
   '该订阅无法续费,仅允许新用户购买': '该订阅无法续费,仅允许新用户购买',
   '重置当月流量': '重置当月流量',
   '流量明细仅保留近月数据以供查询。': '流量明细仅保留近一个月数据以供查询。',
-  '扣费倍率': '扣费倍率'
+  '扣费倍率': '扣费倍率',
+  '支付手续费': '支付手续费',
+  '续费订阅': '续费订阅',
+  '学习如何使用': '学习如何使用',
+  '快速将节点导入对应客户端进行使用': '快速将节点导入对应客户端进行使用',
+  '对您当前的订阅进行续费': '对您当前的订阅进行续费',
+  '对您当前的订阅进行购买': '对您当前的订阅进行购买',
+  '捷径': '捷径',
+  '不会使用,查看使用教程': '不会使用,查看使用教程',
+  '使用支持扫码的客户端进行订阅': '使用支持扫码的客户端进行订阅',
+  '扫描二维码订阅': '扫描二维码订阅',
+  '续费': '续费',
+  '购买': '购买',
+  '查看教程': '查看教程',
+  '注意': '注意',
+  '你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?': '您还有未完成的订单,购买前需要先取消,确定要取消之前的订单吗?',
+  '确定取消': '确定取消',
+  '返回我的订单': '返回我的订单',
+  '如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?': '如果您已经付款,取消订单可能会导致支付失败,确定要取消订单吗?',
+  '选择最适合你的计划': '选择最适合您的计划',
+  '全部': '全部',
+  '按周期': '按周期',
+  '一次性': '一次性',
+  '遇到问题': '遇到问题',
+  '遇到问题可以通过工单与我们沟通': '遇到问题可以通过工单与我们沟通'
 };

+ 198 - 177
public/theme/v2board/assets/i18n/zh-TW.js

@@ -1,15 +1,15 @@
 window.settings.i18n['zh-TW'] = {
   '请求失败': '請求失敗',
   '月付': '月繳制',
-  '季付': '季',
-  '半年付': '半年',
+  '季付': '季',
+  '半年付': '半年',
   '年付': '年繳',
-  '两年付': '两年付',
-  '三年付': '三年',
+  '两年付': '兩年繳',
+  '三年付': '三年',
   '一次性': '一次性',
   '重置流量包': '重置流量包',
   '待支付': '待支付',
-  '开通中': '通中',
+  '开通中': '通中',
   '已取消': '已取消',
   '已完成': '已完成',
   '已折抵': '已折抵',
@@ -28,221 +28,242 @@ window.settings.i18n['zh-TW'] = {
   '我的订单': '我的訂單',
   '我的邀请': '我的邀請',
   '用户': '使用者',
-  '我的工单': '我的工',
-  '流量明细': '流量明',
+  '我的工单': '我的工',
+  '流量明细': '流量明',
   '使用文档': '說明文件',
-  '绑定Telegram获取更多服务': '绑定Telegram获取更多服务',
-  '点击这里进行绑定': '点击这里进行绑定',
+  '绑定Telegram获取更多服务': '綁定 Telegram 獲取更多服務',
+  '点击这里进行绑定': '點擊這裡進行綁定',
   '公告': '公告',
   '总览': '總覽',
-  '该订阅长期有效': '该订阅长期有效',
+  '该订阅长期有效': '該訂閱長期有效',
   '已过期': '已過期',
-  '已用 {used} / 总计 {total}': '已用 {used} / 总计 {total}',
+  '已用 {used} / 总计 {total}': '已用 {used} / 總計 {total}',
   '查看订阅': '查看訂閱',
   '邮箱': '郵箱',
-  '邮箱验证码': '邮箱验证码',
+  '邮箱验证码': '郵箱驗證碼',
   '发送': '傳送',
   '重置密码': '重設密碼',
-  '返回登入': '返回登',
+  '返回登入': '返回登',
   '邀请码': '邀請碼',
   '复制链接': '複製鏈接',
   '完成时间': '完成時間',
   '佣金': '佣金',
-  '已注册用户数': '已注册用户数',
+  '已注册用户数': '已註冊用戶數',
   '佣金比例': '佣金比例',
-  '确认中的佣金': '确认中的佣金',
-  '佣金将会在确认后会到达你的佣金账户。': '佣金将会在确认后会到达你的佣金账户。',
-  '邀请码管理': '邀请码管理',
-  '生成邀请码': '生成邀请码',
-  '邀请明细': '邀请明细',
+  '确认中的佣金': '確認中的佣金',
+  '佣金将会在确认后会到达你的佣金账户。': '佣金將會在確認後到達您的佣金帳戶。',
+  '邀请码管理': '邀請碼管理',
+  '生成邀请码': '生成邀請碼',
+  '佣金发放记录': '佣金發放記錄',
   '复制成功': '複製成功',
   '密码': '密碼',
   '登入': '登入',
   '注册': '註冊',
   '忘记密码': '忘記密碼',
-  '# 订单号': '# 订单号',
+  '# 订单号': '# 訂單號',
   '周期': '週期',
   '订单金额': '訂單金額',
   '订单状态': '訂單狀態',
-  '创建时间': '创建时间',
+  '创建时间': '創建時間',
   '操作': '操作',
-  '查看详情': '查看情',
-  '请选择支付方式': '请选择支付方式',
-  '请检查信用卡支付信息': '请检查信用卡支付信息',
-  '订单详情': '订单详情',
+  '查看详情': '查看情',
+  '请选择支付方式': '請選擇支付方式',
+  '请检查信用卡支付信息': '請檢查信用卡支付資訊',
+  '订单详情': '訂單詳情',
   '折扣': '折扣',
   '折抵': '折抵',
   '退款': '退款',
   '支付方式': '支付方式',
-  '填写信用卡支付信息': '填写信用卡支付信息',
-  '您的信用卡信息只会被用作当次扣款,系统并不会保存,这是我们认为最安全的。': '您的信用卡信息只会被用作当次扣款,系统并不会保存,这是我们认为最安全的。',
-  '订单总额': '订单总额',
-  '总计': '总计',
-  '结账': '结账',
+  '填写信用卡支付信息': '填寫信用卡支付資訊',
+  '您的信用卡信息只会被用作当次扣款,系统并不会保存,这是我们认为最安全的。': '您的信用卡資訊只會被用作當次扣款,系統並不會保存,我們認為這是最安全的。',
+  '订单总额': '訂單總額',
+  '总计': '總計',
+  '结账': '結賬',
   '等待支付中': '等待支付中',
-  '开通中': '通中',
-  '订单系统正在进行处理,请稍等1-3分钟。': '订单系统正在进行处理,请稍等1-3分钟。',
+  '开通中': '通中',
+  '订单系统正在进行处理,请稍等1-3分钟。': '訂單系統正在進行處理,請稍等 1-3 分鐘。',
   '已取消': '已取消',
-  '订单由于超时支付已被取消。': '订单由于超时支付已被取消。',
+  '订单由于超时支付已被取消。': '訂單由於支付超時已被取消',
   '已完成': '已完成',
-  '订单已支付并开通。': '订单已支付并开通。',
-  '选择订阅': '选择订阅',
-  '立即订阅': '立即订阅',
-  '配置订阅': '配置订阅',
+  '订单已支付并开通。': '訂單已支付並開通',
+  '选择订阅': '選擇訂閱',
+  '立即订阅': '立即訂閱',
+  '配置订阅': '配置訂閱',
   '折扣': '折扣',
-  '付款周期': '付款周期',
-  '有优惠券?': '有优惠券?',
-  '验证': '验证',
-  '订单总额': '订单总额',
-  '下单': '下单',
-  '总计': '总计',
-  '订阅变更须知': '订阅变更须知',
-  '变更订阅会导致当前订阅被新订阅覆盖,请注意。': '变更订阅会导致当前订阅被新订阅覆盖,请注意。',
-  '该订阅无法续费': '该订阅无法续费',
-  '选择其他订阅': '选择其他订阅',
-  '我的钱包': '我的钱包',
-  '账户余额(仅消费)': '账户余额(仅消费)',
-  '推广佣金(可提现)': '推广佣金(可提现)',
-  '钱包组成部分': '钱包组成部分',
-  '划转': '划转',
-  '推广佣金提现': '推广佣金提现',
-  '修改密码': '修改密码',
-  '保存': '保存',
-  '旧密码': '旧密码',
-  '新密码': '新密码',
-  '请输入旧密码': '请输入旧密码',
-  '请输入新密码': '请输入新密码',
+  '付款周期': '付款週期',
+  '有优惠券?': '有優惠券?',
+  '验证': '驗證',
+  '订单总额': '訂單總額',
+  '下单': '下單',
+  '总计': '總計',
+  '变更订阅会导致当前订阅被新订阅覆盖,请注意。': '請注意,變更訂閱會導致當前訂閱被新訂閱覆蓋。',
+  '该订阅无法续费': '該訂閱無法續費',
+  '选择其他订阅': '選擇其它訂閱',
+  '我的钱包': '我的錢包',
+  '账户余额(仅消费)': '賬戶餘額(僅消費)',
+  '推广佣金(可提现)': '推廣佣金(可提現)',
+  '钱包组成部分': '錢包組成部分',
+  '划转': '劃轉',
+  '推广佣金提现': '推廣佣金提現',
+  '修改密码': '修改密碼',
+  '保存': '儲存',
+  '旧密码': '舊密碼',
+  '新密码': '新密碼',
+  '请输入旧密码': '請輸入舊密碼',
+  '请输入新密码': '請輸入新密碼',
   '通知': '通知',
-  '到期邮件提醒': '到期件提醒',
-  '流量邮件提醒': '流量件提醒',
-  '绑定Telegram': '绑定Telegram',
-  '立即开始': '立即始',
-  '重置订阅信息': '重置订阅信息',
+  '到期邮件提醒': '到期件提醒',
+  '流量邮件提醒': '流量件提醒',
+  '绑定Telegram': '綁定 Telegram',
+  '立即开始': '立即始',
+  '重置订阅信息': '重置訂閲資訊',
   '重置': '重置',
-  '确定要重置订阅信息?': '确定要重置订阅信息?',
-  '如果你的订阅地址或信息泄露可以进行此操作。重置后你的UUID及订阅将会变更,需要重新进行订阅。': '如果你的订阅地址或信息泄露可以进行此操作。重置后你的UUID及订阅将会变更,需要重新进行订阅。',
+  '确定要重置订阅信息?': '確定要重置訂閱資訊?',
+  '如果你的订阅地址或信息泄露可以进行此操作。重置后你的UUID及订阅将会变更,需要重新进行订阅。': '如果您的訂閱位址或資訊發生洩露可以執行此操作。重置後您的 UUID 及訂閱將會變更,需要重新導入訂閱。',
   '重置成功': '重置成功',
-  '两次新密码输入不同': '两次新密码输入不同',
-  '两次密码输入不同': '两次密码输入不同',
+  '两次新密码输入不同': '兩次新密碼輸入不同',
+  '两次密码输入不同': '兩次密碼輸入不同',
   '邮箱': '郵箱',
-  '邮箱验证码': '邮箱验证码',
-  '发送': '送',
-  '邀请码': '邀请码',
-  '邀请码(选填)': '邀请码(选填)',
-  '注册': '注册',
-  '返回登入': '返回登',
-  '我已阅读并同意 <a target="_blank" href="{url}">服务条款</a>': '我已阅读并同意 <a target="_blank" href="{url}">服务条款</a>',
-  '请同意服务条款': '请同意服务条款',
-  '名称': '名',
-  '标签': '标签',
-  '状态': '状态',
-  '节点五分钟内节点在线情况': '节点五分钟内节点在线情况',
+  '邮箱验证码': '郵箱驗證碼',
+  '发送': '送',
+  '邀请码': '邀請碼',
+  '邀请码(选填)': '邀請碼(選填)',
+  '注册': '註冊',
+  '返回登入': '返回登',
+  '我已阅读并同意 <a target="_blank" href="{url}">服务条款</a>': '我已閱讀並同意 <a target="_blank" href="{url}">服務條款</a>',
+  '请同意服务条款': '請同意服務條款',
+  '名称': '名',
+  '标签': '標籤',
+  '状态': '狀態',
+  '节点五分钟内节点在线情况': '五分鐘內節點線上情況',
   '倍率': '倍率',
-  '使用的流量将乘以倍率进行扣除': '使用的流量将乘以倍率进行扣除',
+  '使用的流量将乘以倍率进行扣除': '使用的流量將乘以倍率進行扣除',
   '更多操作': '更多操作',
-  '复制成功': '复制成功',
-  '复制链接': '复制链接',
-  '该订阅长期有效': '该订阅长期有效',
-  '已过期': '已期',
-  '已用 {used} / 总计 {total}': '已用 {used} / 总计 {total}',
-  '重置订阅信息': '重置订阅信息',
-  '没有可用节点,如果您未订阅或已过期请': '没有可用节点,如果您未订阅或已过期请',
-  '订阅': '订阅',
-  '确定要重置当月流量?': '确定要重置当月流量?',
-  '点击「确定」将会跳转到收银台,支付订单后系统将会清空您当月已使用流量。': '点击「确定」将会跳转到收银台,支付订单后系统将会清空您当月已使用流量。',
-  '确定': '定',
-  '确定要重置订阅信息?': '确定要重置订阅信息?',
-  '如果你的订阅地址或信息泄露可以进行此操作。重置后你的UUID及订阅将会变更,需要重新进行订阅。': '如果你的订阅地址或信息泄露可以进行此操作。重置后你的UUID及订阅将会变更,需要重新进行订阅。',
+  '复制成功': '複製成功',
+  '复制链接': '複製鏈接',
+  '该订阅长期有效': '該訂閱長期有效',
+  '已过期': '已期',
+  '已用 {used} / 总计 {total}': '已用 {used} / 總計 {total}',
+  '重置订阅信息': '重置訂閲資訊',
+  '没有可用节点,如果您未订阅或已过期请': '沒有可用節點,如果您未訂閱或已過期請',
+  '订阅': '訂閱',
+  '确定要重置当月流量?': '確定要重置當月流量?',
+  '点击「确定」将会跳转到收银台,支付订单后系统将会清空您当月已使用流量。': '點擊「確定」將會跳轉到收銀台,支付訂單後系統將會清空您當月已使用流量。',
+  '确定': '定',
+  '确定要重置订阅信息?': '確定要重置訂閱資訊?',
+  '如果你的订阅地址或信息泄露可以进行此操作。重置后你的UUID及订阅将会变更,需要重新进行订阅。': '如果您的訂閱位址或資訊發生洩露可以執行此操作。重置後您的 UUID 及訂閱將會變更,需要重新導入訂閱。',
   '重置成功': '重置成功',
   '低': '低',
   '中': '中',
   '高': '高',
-  '主题': '主',
-  '工单级别': '工单级别',
-  '工单状态': '工单状态',
-  '最后回复': '最后回复',
-  '已关闭': '已关闭',
-  '待回复': '待回',
-  '已回复': '已回',
-  '查看': '查看',
-  '关闭': '关闭',
-  '新的工单': '新的工',
-  '新的工单': '新的工',
-  '确认': '确认',
-  '主题': '主',
-  '请输入工单主题': '请输入工单主题',
-  '工单等级': '工单等级',
-  '请选择工单等级': '请选择工单等级',
-  '消息': '息',
-  '请描述你遇到的问题': '请描述你遇到的问题',
-  '记录时间': '记录时间',
-  '实际上行': '实际上行',
-  '实际下行': '实际下行',
-  '合计': '合',
-  '公式:(实际上行 + 实际下行) x 扣费倍率 = 扣除流量': '公式:(实际上行 + 实际下行) x 扣费倍率 = 扣除流量',
-  '复制成功': '复制成功',
-  '复制订阅地址': '复制订阅地址',
+  '主题': '主',
+  '工单级别': '工單級別',
+  '工单状态': '工單狀態',
+  '最后回复': '最新回復',
+  '已关闭': '已關閉',
+  '待回复': '待回',
+  '已回复': '已回',
+  '查看': '檢視',
+  '关闭': '關閉',
+  '新的工单': '新的工',
+  '新的工单': '新的工',
+  '确认': '確認',
+  '主题': '主',
+  '请输入工单主题': '請輸入工單主題',
+  '工单等级': '工單等級',
+  '请选择工单等级': '請選擇工單等級',
+  '消息': '息',
+  '请描述你遇到的问题': '請描述您遇到的問題',
+  '记录时间': '記錄時間',
+  '实际上行': '實際上行',
+  '实际下行': '實際下行',
+  '合计': '合',
+  '公式:(实际上行 + 实际下行) x 扣费倍率 = 扣除流量': '公式:(實際上行 + 實際下行) x 扣費倍率 = 扣除流量',
+  '复制成功': '複製成功',
+  '复制订阅地址': '複製訂閲位址',
   '导入到': '导入到',
-  '一键订阅': '一键订阅',
-  '复制订阅': '复制订阅',
-  '推广佣金划转至余额': '推广佣金划转至余额',
-  '确认': '确认',
-  '划转后的余额仅用于{title}消费使用': '划转后的余额仅用于{title}消费使用',
-  '当前推广佣金余额': '当前推广佣金余额',
-  '划转金额': '划转金额',
-  '请输入需要划转到余额的金额': '请输入需要划转到余额的金额',
-  '输入内容回复工单...': '输入内容回复工单...',
-  '申请提现': '申请提现',
-  '确认': '确认',
+  '一键订阅': '一鍵訂閲',
+  '复制订阅': '複製訂閲',
+  '推广佣金划转至余额': '推廣佣金劃轉至餘額',
+  '确认': '確認',
+  '划转后的余额仅用于{title}消费使用': '劃轉后的餘額僅用於 {title} 消費使用',
+  '当前推广佣金余额': '當前推廣佣金餘額',
+  '划转金额': '劃轉金額',
+  '请输入需要划转到余额的金额': '請輸入需要劃轉到餘額的金額',
+  '输入内容回复工单...': '輸入内容回復工單…',
+  '申请提现': '申請提現',
+  '确认': '確認',
   '取消': '取消',
-  '提现方式': '提方式',
-  '请选择提现方式': '请选择提现方式',
-  '提现账号': '提现账号',
-  '请输入提现账号': '请输入提现账号',
+  '提现方式': '提方式',
+  '请选择提现方式': '請選擇提現方式',
+  '提现账号': '提現賬號',
+  '请输入提现账号': '請輸入提現賬號',
   '我知道了': '我知道了',
-  '绑定Telegram': '绑定Telegram',
-  '第一步': '第一步',
-  '第二步': '第二步',
-  '打开Telegram搜索': '打开Telegram搜索',
-  '向机器人发送你的': '向机器人发送你的',
-  '使用文档': '使用文档',
-  '最后更新: {date}': '最更新: {date}',
-  '复制成功': '复制成功',
-  '我的订阅': '我的订阅',
-  '还有没支付的订单': '还有没支付的订单',
+  '绑定Telegram': '綁定 Telegram',
+  '第一步': '步驟一',
+  '第二步': '步驟二',
+  '打开Telegram搜索': '打開 Telegram 並搜索',
+  '向机器人发送你的': '向機器人發送您的',
+  '使用文档': '使用檔案',
+  '最后更新: {date}': '最更新: {date}',
+  '复制成功': '複製成功',
+  '我的订阅': '我的訂閱',
+  '还有没支付的订单': '還有未支付的訂單',
   '立即支付': '立即支付',
-  '条工单正在处理中': '条工单正在处理中',
-  '立即查看': '立即查看',
-  '购买订阅': '购买订阅',
-  '使用文档': '使用文档',
-  '我的订单': '我的订单',
-  '流量明细': '流量明',
-  '配置订阅': '配置订阅',
-  '我的邀请': '我的邀',
-  '节点状态': '节点状态',
-  '复制成功': '复制成功',
-  '商品信息': '商品信息',
-  '产品名称': '产品名称',
-  '类型/周期': '类型/周期',
-  '产品流量': '品流量',
-  '订单信息': '订单信息',
-  '关闭订单': '关闭订单',
-  '订单号': '订单号',
-  '优惠金额': '优惠金额',
-  '旧订阅折抵金额': '旧订阅折抵金额',
-  '退款金额': '退款金',
-  '余额支付': '余额支付',
-  '我的工单': '我的工',
-  '工单历史': '工单历史',
-  '{reset_day} 日后重置流量': '{reset_day} 日重置流量',
-  '节点名称': '节点名称',
-  '于 {date} 到期,距离到期还有 {day} 天。': '于 {date} 到期,距离到期还有 {day} 天。',
-  'Telegram 讨论组': 'Telegram 讨论组',
+  '条工单正在处理中': '條工單正在處理中',
+  '立即查看': '立即檢視',
+  '购买订阅': '購買訂閲',
+  '使用文档': '使用檔案',
+  '我的订单': '我的訂單',
+  '流量明细': '流量明',
+  '配置订阅': '配置訂閱',
+  '我的邀请': '我的邀',
+  '节点状态': '節點狀態',
+  '复制成功': '複製成功',
+  '商品信息': '商品資訊',
+  '产品名称': '產品名稱',
+  '类型/周期': '類型/週期',
+  '产品流量': '品流量',
+  '订单信息': '訂單信息',
+  '关闭订单': '關閉訂單',
+  '订单号': '訂單號',
+  '优惠金额': '優惠金額',
+  '旧订阅折抵金额': '舊訂閲折抵金額',
+  '退款金额': '退款金',
+  '余额支付': '餘額支付',
+  '我的工单': '我的工',
+  '工单历史': '工單歷史',
+  '{reset_day} 日后重置流量': '{reset_day} 日重置流量',
+  '节点名称': '節點名稱',
+  '于 {date} 到期,距离到期还有 {day} 天。': '於 {date} 到期,距離到期還有 {day} 天。',
+  'Telegram 讨论组': 'Telegram 討論組',
   '立即加入': '立即加入',
-  '续费': '续费',
-  '购买': '购买',
-  '该订阅无法续费,仅允许新用户购买': '该订阅无法续费,仅允许新用户购买',
-  '重置当月流量': '重置当月流量',
-  '流量明细仅保留近月数据以供查询。': '流量明细仅保留近月数据以供查询。',
-  '扣费倍率': '扣费倍率'
+  '该订阅无法续费,仅允许新用户购买': '該訂閲無法續費,僅允許新用戶購買',
+  '重置当月流量': '重置當月流量',
+  '流量明细仅保留近月数据以供查询。': '流量明細僅保留近一個月資料以供查詢。',
+  '扣费倍率': '扣费倍率',
+  '支付手续费': '支付手續費',
+  '续费订阅': '續費訂閲',
+  '学习如何使用': '學習如何使用',
+  '快速将节点导入对应客户端进行使用': '快速將訂閲導入對應的客戶端進行使用',
+  '对您当前的订阅进行续费': '對您的當前訂閲進行續費',
+  '对您当前的订阅进行购买': '重新購買您的當前訂閲',
+  '捷径': '捷徑',
+  '不会使用,查看使用教程': '不會使用,檢視使用檔案',
+  '使用支持扫码的客户端进行订阅': '使用支持掃碼的客戶端進行訂閲',
+  '扫描二维码订阅': '掃描二維碼訂閲',
+  '续费': '續費',
+  '购买': '購買',
+  '查看教程': '查看教程',
+  '注意': '注意',
+  '你还有未完成的订单,购买前需要先进行取消,确定取消先前的订单吗?': '您还有未完成的订单,购买前需要先取消,确定要取消之前的订单吗?',
+  '确定取消': '確定取消',
+  '返回我的订单': '返回我的訂單',
+  '如果你已经付款,取消订单可能会导致支付失败,确定取消订单吗?': '如果您已經付款,取消訂單可能會導致支付失敗,確定要取消訂單嗎?',
+  '选择最适合你的计划': '選擇最適合您的計劃',
+  '全部': '全部',
+  '按周期': '按週期',
+  '一次性': '一次性',
+  '遇到问题': '遇到問題',
+  '遇到问题可以通过工单与我们沟通': '遇到問題您可以通過工單與我們溝通'
 };

BIN
public/theme/v2board/assets/images/icon/Clash For Android.png


BIN
public/theme/v2board/assets/images/icon/Clash For Windows.png


BIN
public/theme/v2board/assets/images/icon/ClashX.png


BIN
public/theme/v2board/assets/images/icon/QuantumultX.png


BIN
public/theme/v2board/assets/images/icon/Shadowrocket.png


BIN
public/theme/v2board/assets/images/icon/Stash.png


BIN
public/theme/v2board/assets/images/icon/Surfboard.png


BIN
public/theme/v2board/assets/images/icon/Surge.png


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


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


+ 53 - 0
public/theme/v2board/config.php

@@ -0,0 +1,53 @@
+<?php
+
+return [
+    'name' => 'V2board',
+    'description' => 'V2board默认主题',
+    'version' => '1.5.6',
+    'images' => 'https://images.unsplash.com/photo-1515405295579-ba7b45403062?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2160&q=80',
+    'configs' => [
+        [
+            'label' => '主题色',                               // 标签
+            'placeholder' => '请选择主题颜色',                   // 描述
+            'field_name' => 'theme_color',                    // 字段名 作为数据key使用
+            'field_type' => 'select',                         // 字段类型: select,input,switch
+            'select_options' => [                             // 当字段类型为select时有效
+                'default' => '默认(蓝色)',
+                'green' => '奶绿色',
+                'black' => '黑色',
+                'darkblue' => '暗蓝色',
+            ],
+            'default_value' => 'default'                       // 字段默认值,将会在首次进行初始化
+        ], [
+            'label' => '背景',
+            'placeholder' => '请输入背景图片URL',
+            'field_name' => 'background_url',
+            'field_type' => 'input'
+        ], [
+            'label' => '边栏风格',
+            'placeholder' => '请选择边栏风格',
+            'field_name' => 'theme_sidebar',
+            'field_type' => 'select',
+            'select_options' => [
+                'light' => '亮',
+                'dark' => '暗'
+            ],
+            'default_value' => 'light'
+        ], [
+            'label' => '顶部风格',
+            'placeholder' => '请选择顶部风格',
+            'field_name' => 'theme_header',
+            'field_type' => 'select',
+            'select_options' => [
+                'light' => '亮',
+                'dark' => '暗'
+            ],
+            'default_value' => 'dark'
+        ], [
+            'label' => '自定义页脚HTML',
+            'placeholder' => '可以实现客服JS代码的加入等',
+            'field_name' => 'custom_html',
+            'field_type' => 'textarea'
+        ]
+    ]
+];

+ 9 - 7
public/theme/v2board/dashboard.blade.php

@@ -15,7 +15,7 @@
         'default' => '#0665d0',
         'green' => '#319795'
     ])
-    <meta name="theme-color" content="{{$colors[$theme_color]}}">
+    <meta name="theme-color" content="{{$colors[$theme_config['theme_color']]}}">
 
     <title>{{$title}}</title>
     <!-- <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Nunito+Sans:300,400,400i,600,700"> -->
@@ -23,15 +23,15 @@
     <script>
         window.settings = {
             title: '{{$title}}',
+            theme_path: '{{$theme_path}}',
             theme: {
-                sidebar: '{{$theme_sidebar}}',
-                header: '{{$theme_header}}',
-                color: '{{$theme_color}}',
+                sidebar: '{{$theme_config['theme_sidebar']}}',
+                header: '{{$theme_config['theme_header']}}',
+                color: '{{$theme_config['theme_color']}}',
             },
             version: '{{$version}}',
-            background_url: '{{$background_url}}',
+            background_url: '{{$theme_config['background_url']}}',
             description: '{{$description}}',
-            crisp_id: '{{$crisp_id}}',
             i18n: [
                 'zh-CN',
                 'en-US',
@@ -39,7 +39,8 @@
                 'vi-VN',
                 'ko-KR',
                 'zh-TW'
-            ]
+            ],
+            logo: '{{$logo}}'
         }
     </script>
     <script src="/theme/{{$theme}}/assets/i18n/zh-CN.js?v={{$version}}"></script>
@@ -52,6 +53,7 @@
 
 <body>
 <div id="root"></div>
+{!! $theme_config['custom_html'] !!}
 <script src="/theme/{{$theme}}/assets/vendors.async.js?v={{$version}}"></script>
 <script src="/theme/{{$theme}}/assets/components.async.js?v={{$version}}"></script>
 <script src="/theme/{{$theme}}/assets/umi.js?v={{$version}}"></script>

File diff suppressed because it is too large
+ 1 - 5
public/vendor/horizon/app-dark.css


File diff suppressed because it is too large
+ 1 - 5
public/vendor/horizon/app.css


File diff suppressed because it is too large
+ 0 - 0
public/vendor/horizon/app.js


+ 4 - 3
public/vendor/horizon/mix-manifest.json

@@ -1,5 +1,6 @@
 {
-    "/app.js": "/app.js?id=a2e36b7a4f248973b22b",
-    "/app.css": "/app.css?id=9ce01eaaba790566b895",
-    "/app-dark.css": "/app-dark.css?id=821c845f9bf3b7853c33"
+    "/app.js": "/app.js?id=9db6ba6424a3d1048c194c9c1e4429fe",
+    "/app-dark.css": "/app-dark.css?id=ff172044c4efc9f08f12c0eb824b0226",
+    "/app.css": "/app.css?id=a38514598173eedd6b8575a77bc1ead4",
+    "/img/favicon.png": "/img/favicon.png?id=1542bfe8a0010dcbee710da13cce367f"
 }

+ 5 - 1
resources/lang/en-US.json

@@ -86,5 +86,9 @@
     "The traffic usage in :app_name has reached 80%": "The traffic usage in :app_name has reached 80%",
     "The service in :app_name is about to expire": "The service in :app_name is about to expire",
     "The coupon can only be used :limit_use_with_user per person": "The coupon can only be used :limit_use_with_user per person",
-    "The coupon code cannot be used for this period": "The coupon code cannot be used for this period"
+    "The coupon code cannot be used for this period": "The coupon code cannot be used for this period",
+    "Request failed, please try again later": "Request failed, please try again later",
+    "Register frequently, please try again after 1 hour": "Register frequently, please try again after 1 hour",
+    "Uh-oh, we've had some problems, we're working on it.": "Uh-oh, we've had some problems, we're working on it",
+    "This subscription reset package does not apply to your subscription": "This subscription reset package does not apply to your subscription"
 }

+ 5 - 1
resources/lang/zh-CN.json

@@ -86,5 +86,9 @@
     "The traffic usage in :app_name has reached 80%": "在 :app_name 的已用流量已达到 80%",
     "The service in :app_name is about to expire": "在 :app_name 的服务即将到期",
     "The coupon can only be used :limit_use_with_user per person": "该优惠券每人只能用 :limit_use_with_user 次",
-    "The coupon code cannot be used for this period": "此优惠券无法用于该付款周期"
+    "The coupon code cannot be used for this period": "此优惠券无法用于该付款周期",
+    "Request failed, please try again later": "请求失败,请稍后再试",
+    "Register frequently, please try again after 1 hour": "注册频繁,请等待1小时后再次尝试",
+    "Uh-oh, we've had some problems, we're working on it.": "遇到了些问题,我们正在进行处理",
+    "This subscription reset package does not apply to your subscription": "该订阅重置包不适用于你的订阅"
 }

+ 5 - 3
resources/rules/app.clash.yaml

@@ -60,9 +60,6 @@ rules:
   # - DOMAIN,e.crashlytics.com,REJECT //注释此选项有助于大多数App开发者分析崩溃信息;如果您拒绝一切崩溃数据统计、搜集,请取消 # 注释。
 
   # 国内网站
-  - DOMAIN-SUFFIX,cn,DIRECT
-  - DOMAIN-KEYWORD,-cn,DIRECT
-
   - DOMAIN-SUFFIX,126.com,DIRECT
   - DOMAIN-SUFFIX,126.net,DIRECT
   - DOMAIN-SUFFIX,127.net,DIRECT
@@ -366,6 +363,7 @@ rules:
   - DOMAIN-SUFFIX,klip.me,SELECT
   - DOMAIN-SUFFIX,libsyn.com,SELECT
   - DOMAIN-SUFFIX,linkedin.com,SELECT
+  - DOMAIN-SUFFIX,line-apps.com,SELECT
   - DOMAIN-SUFFIX,linode.com,SELECT
   - DOMAIN-SUFFIX,lithium.com,SELECT
   - DOMAIN-SUFFIX,littlehj.com,SELECT
@@ -550,6 +548,10 @@ rules:
   - IP-CIDR,224.0.0.0/4,DIRECT
   - IP-CIDR6,fe80::/10,DIRECT
 
+  # 剩余未匹配的国内网站
+  - DOMAIN-SUFFIX,cn,DIRECT
+  - DOMAIN-KEYWORD,-cn,DIRECT
+
   # 最终规则
   - GEOIP,CN,DIRECT
   - MATCH,SELECT

+ 9 - 6
resources/rules/default.clash.yaml

@@ -24,9 +24,10 @@ dns:
     - https://doh.pub/dns-query
     - https://dns.alidns.com/dns-query
   fallback:
-    - tls://1.0.0.1:853
-    - https://cloudflare-dns.com/dns-query
-    - https://dns.google/dns-query
+    - https://doh.dns.sb/dns-query
+    - https://dns.cloudflare.com/dns-query
+    - https://dns.twnic.tw/dns-query
+    - tls://8.8.4.4:853
   fallback-filter:
     geoip: true
     ipcidr:
@@ -84,9 +85,6 @@ rules:
   # - DOMAIN,e.crashlytics.com,REJECT //注释此选项有助于大多数App开发者分析崩溃信息;如果您拒绝一切崩溃数据统计、搜集,请取消 # 注释。
 
   # 国内网站
-  - DOMAIN-SUFFIX,cn,DIRECT
-  - DOMAIN-KEYWORD,-cn,DIRECT
-
   - DOMAIN-SUFFIX,126.com,DIRECT
   - DOMAIN-SUFFIX,126.net,DIRECT
   - DOMAIN-SUFFIX,127.net,DIRECT
@@ -390,6 +388,7 @@ rules:
   - DOMAIN-SUFFIX,klip.me,$app_name
   - DOMAIN-SUFFIX,libsyn.com,$app_name
   - DOMAIN-SUFFIX,linkedin.com,$app_name
+  - DOMAIN-SUFFIX,line-apps.com,$app_name
   - DOMAIN-SUFFIX,linode.com,$app_name
   - DOMAIN-SUFFIX,lithium.com,$app_name
   - DOMAIN-SUFFIX,littlehj.com,$app_name
@@ -574,6 +573,10 @@ rules:
   - IP-CIDR,224.0.0.0/4,DIRECT
   - IP-CIDR6,fe80::/10,DIRECT
 
+  # 剩余未匹配的国内网站
+  - DOMAIN-SUFFIX,cn,DIRECT
+  - DOMAIN-KEYWORD,-cn,DIRECT
+
   # 最终规则
   - GEOIP,CN,DIRECT
   - MATCH,$app_name

+ 5 - 2
resources/rules/default.surfboard.conf

@@ -78,8 +78,6 @@ DOMAIN-SUFFIX,apple-mapkit.com,DIRECT
 USER-AGENT,MicroMessenger Client*,DIRECT
 USER-AGENT,WeChat*,DIRECT
 
-DOMAIN-SUFFIX,cn,DIRECT
-DOMAIN-KEYWORD,-cn,DIRECT
 DOMAIN-SUFFIX,126.com,DIRECT
 DOMAIN-SUFFIX,126.net,DIRECT
 DOMAIN-SUFFIX,127.net,DIRECT
@@ -382,6 +380,7 @@ DOMAIN-SUFFIX,kat.cr,Proxy
 DOMAIN-SUFFIX,klip.me,Proxy
 DOMAIN-SUFFIX,libsyn.com,Proxy
 DOMAIN-SUFFIX,linkedin.com,Proxy
+DOMAIN-SUFFIX,line-apps.com,Proxy
 DOMAIN-SUFFIX,linode.com,Proxy
 DOMAIN-SUFFIX,lithium.com,Proxy
 DOMAIN-SUFFIX,littlehj.com,Proxy
@@ -565,6 +564,10 @@ IP-CIDR,100.64.0.0/10,DIRECT
 IP-CIDR,224.0.0.0/4,DIRECT
 IP-CIDR6,fe80::/10,DIRECT
 
+# 剩余未匹配的国内网站
+DOMAIN-SUFFIX,cn,DIRECT
+DOMAIN-KEYWORD,-cn,DIRECT
+
 # 最终规则
 GEOIP,CN,DIRECT
 FINAL,Proxy

+ 5 - 2
resources/rules/default.surge.conf

@@ -103,8 +103,6 @@ DOMAIN-SUFFIX,apple-mapkit.com,DIRECT
 USER-AGENT,MicroMessenger Client*,DIRECT
 USER-AGENT,WeChat*,DIRECT
 
-DOMAIN-SUFFIX,cn,DIRECT
-DOMAIN-KEYWORD,-cn,DIRECT
 DOMAIN-SUFFIX,126.com,DIRECT
 DOMAIN-SUFFIX,126.net,DIRECT
 DOMAIN-SUFFIX,127.net,DIRECT
@@ -407,6 +405,7 @@ DOMAIN-SUFFIX,kat.cr,Proxy
 DOMAIN-SUFFIX,klip.me,Proxy
 DOMAIN-SUFFIX,libsyn.com,Proxy
 DOMAIN-SUFFIX,linkedin.com,Proxy
+DOMAIN-SUFFIX,line-apps.com,Proxy
 DOMAIN-SUFFIX,linode.com,Proxy
 DOMAIN-SUFFIX,lithium.com,Proxy
 DOMAIN-SUFFIX,littlehj.com,Proxy
@@ -581,6 +580,10 @@ IP-CIDR,220.181.174.34/32,Proxy,no-resolve
 
 RULE-SET,LAN,DIRECT
 
+# 剩余未匹配的国内网站
+DOMAIN-SUFFIX,cn,DIRECT
+DOMAIN-KEYWORD,-cn,DIRECT
+
 # 最终规则
 GEOIP,CN,DIRECT
 FINAL,Proxy,dns-failed

+ 9 - 8
resources/views/admin.blade.php

@@ -2,9 +2,9 @@
 <html>
 
 <head>
-    <link rel="stylesheet" href="/assets/admin/components.chunk.css?v={{$verison}}">
-    <link rel="stylesheet" href="/assets/admin/umi.css?v={{$verison}}">
-    <link rel="stylesheet" href="/assets/admin/custom.css?v={{$verison}}">
+    <link rel="stylesheet" href="/assets/admin/components.chunk.css?v={{$version}}">
+    <link rel="stylesheet" href="/assets/admin/umi.css?v={{$version}}">
+    <link rel="stylesheet" href="/assets/admin/custom.css?v={{$version}}">
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no">
     <title>{{$title}}</title>
@@ -18,17 +18,18 @@
                 header: '{{$theme_header}}',
                 color: '{{$theme_color}}',
             },
-            verison: '{{$verison}}',
-            background_url: '{{$backgroun_url}}'
+            version: '{{$version}}',
+            background_url: '{{$background_url}}',
+            logo: '{{$logo}}'
         }
     </script>
 </head>
 
 <body>
 <div id="root"></div>
-<script src="/assets/admin/vendors.async.js?v={{$verison}}"></script>
-<script src="/assets/admin/components.async.js?v={{$verison}}"></script>
-<script src="/assets/admin/umi.js?v={{$verison}}"></script>
+<script src="/assets/admin/vendors.async.js?v={{$version}}"></script>
+<script src="/assets/admin/components.async.js?v={{$version}}"></script>
+<script src="/assets/admin/umi.js?v={{$version}}"></script>
 <!-- Global site tag (gtag.js) - Google Analytics -->
 <script async src="https://www.googletagmanager.com/gtag/js?id=G-P1E9Z5LRRK"></script>
 <script>

+ 13 - 7
routes/web.php

@@ -1,5 +1,6 @@
 <?php
 
+use App\Services\ThemeService;
 use Illuminate\Http\Request;
 
 /*
@@ -22,14 +23,18 @@ Route::get('/', function (Request $request) {
     $renderParams = [
         'title' => config('v2board.app_name', 'V2Board'),
         'theme' => config('v2board.frontend_theme', 'v2board'),
-        'theme_sidebar' => config('v2board.frontend_theme_sidebar', 'light'),
-        'theme_header' => config('v2board.frontend_theme_header', 'dark'),
-        'theme_color' => config('v2board.frontend_theme_color', 'default'),
-        'background_url' => config('v2board.frontend_background_url'),
+        'theme_path' => '/theme/' . config('v2board.frontend_theme', 'v2board') . '/assets/',
         'version' => config('app.version'),
         'description' => config('v2board.app_description', 'V2Board is best'),
-        'crisp_id' => config('v2board.frontend_customer_service_method') === 'crisp' ? config('v2board.frontend_customer_service_id') : ''
+        'logo' => config('v2board.logo')
     ];
+
+    if (!config("theme.{$renderParams['theme']}")) {
+        $themeService = new ThemeService($renderParams['theme']);
+        $themeService->init();
+    }
+
+    $renderParams['theme_config'] = config('theme.' . config('v2board.frontend_theme', 'v2board'));
     return view('theme::' . config('v2board.frontend_theme', 'v2board') . '.dashboard', $renderParams);
 });
 
@@ -39,7 +44,8 @@ Route::get('/' . config('v2board.frontend_admin_path', 'admin'), function () {
         'theme_sidebar' => config('v2board.frontend_theme_sidebar', 'light'),
         'theme_header' => config('v2board.frontend_theme_header', 'dark'),
         'theme_color' => config('v2board.frontend_theme_color', 'default'),
-        'backgroun_url' => config('v2board.frontend_background_url'),
-        'verison' => config('app.version')
+        'background_url' => config('v2board.frontend_background_url'),
+        'version' => config('app.version'),
+        'logo' => config('v2board.logo')
     ]);
 });

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