소스 검색

💪🏼 Improve Code

BrettonYe 1 년 전
부모
커밋
2ca6965074
43개의 변경된 파일613개의 추가작업 그리고 541개의 파일을 삭제
  1. 82 114
      app/Channels/Library/WeChat.php
  2. 1 1
      app/Channels/WeChatChannel.php
  3. 4 4
      app/Console/Commands/AutoClearLogs.php
  4. 12 7
      app/Console/Commands/DailyNodeReport.php
  5. 31 35
      app/Console/Commands/NodeStatusDetection.php
  6. 156 104
      app/Console/Commands/PanelInstallation.php
  7. 42 20
      app/Console/Commands/PanelUpdate.php
  8. 3 5
      app/Console/Commands/ServiceTimer.php
  9. 50 71
      app/Console/Commands/TaskAuto.php
  10. 91 78
      app/Console/Commands/TaskDaily.php
  11. 45 22
      app/Console/Commands/TaskHourly.php
  12. 3 2
      app/Console/Commands/TaskMonthly.php
  13. 3 3
      app/Helpers/ClientApiResponse.php
  14. 1 1
      app/Helpers/ClientConfig.php
  15. 3 3
      app/Helpers/WebApiResponse.php
  16. 1 1
      app/Http/Controllers/Admin/LogsController.php
  17. 1 1
      app/Http/Controllers/Admin/NodeAuthController.php
  18. 2 2
      app/Http/Controllers/Admin/ToolsController.php
  19. 1 1
      app/Http/Controllers/Api/Client/ClientController.php
  20. 1 1
      app/Http/Controllers/User/SubscribeController.php
  21. 1 1
      app/Models/Article.php
  22. 1 1
      app/Models/User.php
  23. 4 0
      app/Observers/UserObserver.php
  24. 20 14
      app/Services/ArticleService.php
  25. 4 4
      app/Services/CouponService.php
  26. 1 1
      app/Services/OrderService.php
  27. 5 5
      app/Services/ProxyService.php
  28. 1 1
      app/Services/TelegramService.php
  29. 1 1
      app/Services/UserService.php
  30. 2 2
      app/Utils/CurrencyExchange.php
  31. 1 1
      app/Utils/DDNS/CloudFlare.php
  32. 7 7
      app/Utils/Helpers.php
  33. 1 1
      app/Utils/Payments/F2Fpay.php
  34. 6 17
      app/helpers.php
  35. 1 1
      install.sh
  36. 2 0
      resources/lang/zh_CN.json
  37. 1 1
      resources/views/_layout.blade.php
  38. 1 3
      resources/views/admin/node/auth.blade.php
  39. 7 1
      resources/views/admin/node/index.blade.php
  40. 1 1
      resources/views/user/layouts.blade.php
  41. 10 0
      scripts/download_utils.sh
  42. 1 1
      scripts/lib.sh
  43. 1 1
      update.sh

+ 82 - 114
app/Channels/Library/WeChat.php

@@ -9,47 +9,45 @@ use Str;
 
 class WeChat
 {
-    public function EncryptMsg(string $sReplyMsg, ?int $sTimeStamp, string $sNonce, string &$sEncryptMsg)
+    public string $key;
+
+    public string $iv;
+
+    public function __construct()
+    {
+        $this->key = base64_decode(sysConfig('wechat_encodingAESKey').'=');
+        $this->iv = substr($this->key, 0, 16);
+    }
+
+    public function encryptMsg(string $sReplyMsg, ?int $sTimeStamp, string $sNonce, string &$sEncryptMsg): int
     { //将公众平台回复用户的消息加密打包.
-        //加密
-        $array = (new Prpcrypt())->encrypt($sReplyMsg);
-        $ret = $array[0];
-        if ($ret !== 0) {
-            return $ret;
-        }
+        $array = $this->prpcrypt_encrypt($sReplyMsg); //加密
 
-        if ($sTimeStamp === null) {
-            $sTimeStamp = time();
+        if ($array[0] !== 0) {
+            return $array[0];
         }
-        $encrypt = $array[1];
 
-        //生成安全签名
+        $encrypt = $array[1];
+        $sTimeStamp = $sTimeStamp ?? time();
         $array = $this->getSHA1($sTimeStamp, $sNonce, $encrypt);
-        $ret = $array[0];
-        if ($ret !== 0) {
-            return $ret;
+
+        if ($array[0] !== 0) {
+            return $array[0];
         }
-        $signature = $array[1];
 
-        //生成发送的xml
+        $signature = $array[1];
         $sEncryptMsg = $this->generate($encrypt, $signature, $sTimeStamp, $sNonce);
 
         return 0;
     }
 
-    public function getSHA1(string $timestamp, string $nonce, string $encrypt_msg): array
+    public function getSHA1(string $timestamp, string $nonce, string $encryptMsg): array
     {
-        //排序
-        try {
-            $array = [$encrypt_msg, sysConfig('wechat_token'), $timestamp, $nonce];
-            sort($array, SORT_STRING);
-
-            return [0, sha1(implode($array))];
-        } catch (Exception $e) {
-            Log::critical('企业微信消息推送异常:'.var_export($e->getMessage(), true));
+        $data = [$encryptMsg, sysConfig('wechat_token'), $timestamp, $nonce];
+        sort($data, SORT_STRING);
+        $signature = sha1(implode($data));
 
-            return [-40003, null]; // ComputeSignatureError
-        }
+        return [0, $signature];
     }
 
     /**
@@ -62,34 +60,31 @@ class WeChat
      */
     public function generate(string $encrypt, string $signature, string $timestamp, string $nonce): string
     {
-        $format = '<xml>
-<Encrypt><![CDATA[%s]]></Encrypt>
-<MsgSignature><![CDATA[%s]]></MsgSignature>
-<TimeStamp>%s</TimeStamp>
-<Nonce><![CDATA[%s]]></Nonce>
-</xml>';
+        $format = <<<'XML'
+<xml>
+    <Encrypt><![CDATA[%s]]></Encrypt>
+    <MsgSignature><![CDATA[%s]]></MsgSignature>
+    <TimeStamp>%s</TimeStamp>
+    <Nonce><![CDATA[%s]]></Nonce>
+</xml>
+XML;
 
         return sprintf($format, $encrypt, $signature, $timestamp, $nonce);
     }
 
-    public function DecryptMsg(string $sMsgSignature, ?int $sTimeStamp, string $sNonce, string $sPostData, string &$sMsg)
+    public function decryptMsg(string $sMsgSignature, ?int $sTimeStamp, string $sNonce, string $sPostData, string &$sMsg)
     { // 检验消息的真实性,并且获取解密后的明文.
         //提取密文
         $array = $this->extract($sPostData);
-        $ret = $array[0];
-
-        if ($ret !== 0) {
-            return $ret;
-        }
 
-        if ($sTimeStamp === null) {
-            $sTimeStamp = time();
+        if ($array[0] !== 0) {
+            return $array[0];
         }
 
+        $sTimeStamp = $sTimeStamp ?? time();
         $encrypt = $array[1];
 
-        //验证安全签名
-        $this->verifyURL($sMsgSignature, $sTimeStamp, $sNonce, $encrypt, $sMsg);
+        $this->verifySignature($sMsgSignature, $sTimeStamp, $sNonce, $encrypt, $sMsg); // 验证安全签名
     }
 
     /**
@@ -103,8 +98,7 @@ class WeChat
         try {
             $xml = new DOMDocument();
             $xml->loadXML($xmlText);
-            $array_e = $xml->getElementsByTagName('Encrypt');
-            $encrypt = $array_e->item(0)->nodeValue;
+            $encrypt = $xml->getElementsByTagName('Encrypt')->item(0)->nodeValue;
 
             return [0, $encrypt];
         } catch (Exception $e) {
@@ -114,91 +108,43 @@ class WeChat
         }
     }
 
-    public function verifyURL(string $sMsgSignature, string $sTimeStamp, string $sNonce, string $sEchoStr, string &$sReplyEchoStr)
+    public function verifySignature(string $sMsgSignature, string $sTimeStamp, string $sNonce, string $sEcho, string &$sMsg): int
     { // 验证URL
         //verify msg_signature
-        $array = $this->getSHA1($sTimeStamp, $sNonce, $sEchoStr);
-        $ret = $array[0];
+        $array = $this->extract($sEcho);
 
-        if ($ret !== 0) {
-            return $ret;
+        if ($array[0] !== 0) {
+            return $array[0];
         }
 
-        $signature = $array[1];
-        if ($signature !== $sMsgSignature) {
-            return -40001; // ValidateSignatureError
-        }
-
-        $result = (new Prpcrypt())->decrypt($sEchoStr);
-        if ($result[0] !== 0) {
-            return $result[0];
-        }
-        $sReplyEchoStr = $result[1];
-
-        return 0;
-    }
-}
-
-/**
- * PKCS7Encoder class.
- *
- * 提供基于PKCS7算法的加解密接口.
- */
-class PKCS7Encoder
-{
-    public static int $block_size = 32;
-
-    public function encode(string $text): string
-    { // 对需要加密的明文进行填充补位
-        //计算需要填充的位数
-        $amount_to_pad = self::$block_size - (strlen($text) % self::$block_size);
-        if ($amount_to_pad === 0) {
-            $amount_to_pad = self::$block_size;
-        }
+        $encrypt = $array[1];
 
-        return $text.str_repeat(chr($amount_to_pad), $amount_to_pad); // 获得补位所用的字符
-    }
+        $array = $this->getSHA1($sTimeStamp, $sNonce, $encrypt);
 
-    public function decode(string $text): string
-    { // 对解密后的明文进行补位删除
-        $pad = ord(substr($text, -1));
-        if ($pad < 1 || $pad > self::$block_size) {
-            $pad = 0;
+        if ($array[0] !== 0) {
+            return $array[0];
         }
 
-        return substr($text, 0, strlen($text) - $pad);
-    }
-}
+        $signature = $array[1];
 
-/**
- * Prpcrypt class.
- *
- * 提供接收和推送给公众平台消息的加解密接口.
- */
-class Prpcrypt
-{
-    public string $key;
+        if ($sMsgSignature !== $signature) {
+            Log::critical('企业微信消息推送异常:安全签名验证失败');
 
-    public string $iv;
+            return -40004; // ValidateSignatureError
+        }
 
-    public function __construct()
-    {
-        $this->key = base64_decode(sysConfig('wechat_encodingAESKey').'=');
-        $this->iv = substr($this->key, 0, 16);
+        $sMsg = $encrypt;
     }
 
-    /**
-     * 加密.
-     */
-    public function encrypt(string $text): array
+    public function prpcrypt_encrypt(string $data): array
     {
         try {
             //拼接
-            $text = Str::random().pack('N', strlen($text)).$text.sysConfig('wechat_cid');
+            $data = Str::random().pack('N', strlen($data)).$data.sysConfig('wechat_cid');
             //添加PKCS#7填充
-            $text = (new PKCS7Encoder)->encode($text);
+            $data = $this->pkcs7_encode($data);
             //加密
-            $encrypted = openssl_encrypt($text, 'AES-256-CBC', $this->key, OPENSSL_ZERO_PADDING, $this->iv);
+            $encrypted = openssl_encrypt($data, 'AES-256-CBC', $this->key, OPENSSL_ZERO_PADDING, $this->iv);
 
             return [0, $encrypted];
         } catch (Exception $e) {
@@ -208,8 +154,18 @@ class Prpcrypt
         }
     }
 
-    public function decrypt(string $encrypted): array
-    { // 解密
+    public function pkcs7_encode(string $data): string
+    {// 对需要加密的明文进行填充补位
+        //计算需要填充的位数
+        $padding = 32 - (strlen($data) % 32);
+        $padding = ($padding === 0) ? 32 : $padding;
+        $pattern = chr($padding);
+
+        return $data.str_repeat($pattern, $padding); // 获得补位所用的字符
+    }
+
+    public function prpcrypt_decrypt(string $encrypted): array
+    {
         try {
             //解密
             $decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', $this->key, OPENSSL_ZERO_PADDING, $this->iv);
@@ -220,7 +176,7 @@ class Prpcrypt
         }
         try {
             //删除PKCS#7填充
-            $result = (new PKCS7Encoder)->decode($decrypted);
+            $result = $this->pkcs7_decode($decrypted);
             if (strlen($result) < 16) {
                 return [];
             }
@@ -242,4 +198,16 @@ class Prpcrypt
 
         return [0, $xml_content];
     }
+
+    public function pkcs7_decode(string $encrypted): string
+    {// 对解密后的明文进行补位删除
+        $length = strlen($encrypted);
+        $padding = ord($encrypted[$length - 1]);
+
+        if ($padding < 1 || $padding > 32) {
+            return $encrypted;
+        }
+
+        return substr($encrypted, 0, $length - $padding);
+    }
 }

+ 1 - 1
app/Channels/WeChatChannel.php

@@ -111,7 +111,7 @@ class WeChatChannel
 
     public function verify(Request $request): void
     {
-        $errCode = (new WeChat())->verifyURL($request->input('msg_signature'), $request->input('timestamp'), $request->input('nonce'), $request->input('echostr'), $sEchoStr);
+        $errCode = (new WeChat())->verifySignature($request->input('msg_signature'), $request->input('timestamp'), $request->input('nonce'), $request->input('echostr'), $sEchoStr);
         if ($errCode === 0) {
             exit($sEchoStr);
         }

+ 4 - 4
app/Console/Commands/AutoClearLogs.php

@@ -14,6 +14,7 @@ use App\Models\UserHourlyDataFlow;
 use App\Models\UserSubscribeLog;
 use Exception;
 use Illuminate\Console\Command;
+use Illuminate\Database\Eloquent\Builder;
 use Log;
 
 class AutoClearLogs extends Command
@@ -50,10 +51,9 @@ class AutoClearLogs extends Command
 
             NodeOnlineIp::where('created_at', '<=', strtotime(config('tasks.clean.node_online_ips')))->delete(); // 清除用户连接IP
 
-            UserDailyDataFlow::where('node_id', '<>', null)
-                ->where('created_at', '<=', date('Y-m-d H:i:s', strtotime(config('tasks.clean.user_daily_logs_nodes'))))
-                ->orWhere('created_at', '<=', date('Y-m-d H:i:s', strtotime(config('tasks.clean.user_daily_logs_total'))))
-                ->delete(); // 清除用户各节点 / 节点总计的每天流量数据日志
+            UserDailyDataFlow::where(static function (Builder $query) {
+                $query->where('node_id', '<>', null)->where('created_at', '<=', date('Y-m-d H:i:s', strtotime(config('tasks.clean.user_daily_logs_nodes'))));
+            })->orWhere('created_at', '<=', date('Y-m-d H:i:s', strtotime(config('tasks.clean.user_daily_logs_total'))))->delete(); // 清除用户各节点 / 节点总计的每天流量数据日志
 
             UserHourlyDataFlow::where('created_at', '<=', date('Y-m-d H:i:s', strtotime(config('tasks.clean.user_hourly_logs'))))->delete(); // 清除用户每时各流量数据日志
 

+ 12 - 7
app/Console/Commands/DailyNodeReport.php

@@ -20,9 +20,11 @@ class DailyNodeReport extends Command
         $jobTime = microtime(true);
 
         if (sysConfig('node_daily_notification')) {
-            $nodeDailyLogs = NodeDailyDataFlow::with('node:id,name')->has('node')->orderBy('node_id')->whereDate('created_at', date('Y-m-d', strtotime('yesterday')))->get();
+            $nodeDailyLogs = NodeDailyDataFlow::with('node:id,name')->has('node')->whereDate('created_at', date('Y-m-d', strtotime('yesterday')))->orderBy('node_id')->get();
 
             $data = [];
+            $sum_u = 0;
+            $sum_d = 0;
             foreach ($nodeDailyLogs as $log) {
                 $data[] = [
                     'name' => $log->node->name,
@@ -30,19 +32,22 @@ class DailyNodeReport extends Command
                     'download' => formatBytes($log->d),
                     'total' => formatBytes($log->u + $log->d),
                 ];
+                $sum_u += $log->u;
+                $sum_d += $log->d;
             }
 
             if ($data) {
-                $u = $nodeDailyLogs->sum('u');
-                $d = $nodeDailyLogs->sum('d');
                 $data[] = [
                     'name' => trans('notification.node.total'),
-                    'upload' => formatBytes($u),
-                    'download' => formatBytes($d),
-                    'total' => formatBytes($u + $d),
+                    'upload' => formatBytes($sum_u),
+                    'download' => formatBytes($sum_d),
+                    'total' => formatBytes($sum_u + $sum_d),
                 ];
 
-                Notification::send(User::role('Super Admin')->get(), new NodeDailyReport($data));
+                $superAdmins = User::role('Super Admin')->get();
+                if ($superAdmins->isNotEmpty()) {
+                    Notification::send($superAdmins, new NodeDailyReport($data));
+                }
             }
         }
 

+ 31 - 35
app/Console/Commands/NodeStatusDetection.php

@@ -28,10 +28,12 @@ class NodeStatusDetection extends Command
         }
 
         if (sysConfig('node_blocked_notification')) {// 监测节点网络状态
-            if (! Cache::has('LastCheckTime') || Cache::get('LastCheckTime') <= time()) {
+            $lastCheckTime = Cache::get('LastCheckTime');
+
+            if (! $lastCheckTime || $lastCheckTime <= time()) {
                 $this->checkNodeNetwork();
             } else {
-                Log::info('下次节点阻断检测时间:'.date('Y-m-d H:i:s', Cache::get('LastCheckTime')));
+                Log::info('下次节点阻断检测时间:'.date('Y-m-d H:i:s', $lastCheckTime));
             }
         }
 
@@ -42,22 +44,21 @@ class NodeStatusDetection extends Command
     private function checkNodeStatus(): void
     {
         $offlineCheckTimes = sysConfig('offline_check_times');
-        $onlineNode = NodeHeartbeat::recently()->distinct()->pluck('node_id')->toArray();
+        $onlineNode = NodeHeartbeat::recently()->distinct()->pluck('node_id');
+
+        $data = [];
         foreach (Node::whereRelayNodeId(null)->whereStatus(1)->whereNotIn('id', $onlineNode)->get() as $node) {
             // 近期无节点负载信息则认为是后端炸了
-            if ($offlineCheckTimes) {
-                // 已通知次数
+            if ($offlineCheckTimes > 0) {
                 $cacheKey = 'offline_check_times'.$node->id;
-                $times = 1;
-                if (Cache::has($cacheKey)) {
-                    $times = Cache::get($cacheKey);
+                if (! Cache::has($cacheKey)) { // 已通知次数
+                    Cache::put($cacheKey, 1, now()->addDay()); // 键将保留24小时
                 } else {
-                    Cache::put($cacheKey, 1, Day); // 键将保留24小时
-                }
-                if ($times > $offlineCheckTimes) {
-                    continue;
+                    $times = Cache::increment($cacheKey);
+                    if ($times > $offlineCheckTimes) {
+                        continue;
+                    }
                 }
-                Cache::increment($cacheKey);
             }
             $data[] = [
                 'name' => $node->name,
@@ -65,7 +66,7 @@ class NodeStatusDetection extends Command
             ];
         }
 
-        if (isset($data)) {
+        if (! empty($data)) {
             Notification::send(User::find(1), new NodeOffline($data));
         }
     }
@@ -76,6 +77,7 @@ class NodeStatusDetection extends Command
 
         foreach (Node::whereStatus(1)->where('detection_type', '<>', 0)->get() as $node) {
             $node_id = $node->id;
+            $data = [];
             // 使用DDNS的node先通过gethostbyname获取ipv4地址
             foreach ($node->ips() as $ip) {
                 if ($node->detection_type) {
@@ -94,32 +96,29 @@ class NodeStatusDetection extends Command
             }
 
             // 节点检测次数
-            if (isset($data[$node_id]) && $detectionCheckTimes) {
+            if (! empty($data[$node_id]) && $detectionCheckTimes) {
                 // 已通知次数
                 $cacheKey = 'detection_check_times'.$node_id;
-                if (Cache::has($cacheKey)) {
-                    $times = Cache::get($cacheKey);
-                } else {
-                    // 键将保留12小时,多10分钟防意外
-                    Cache::put($cacheKey, 1, 43800);
-                    $times = 1;
-                }
 
-                if ($times < $detectionCheckTimes) {
-                    Cache::increment($cacheKey);
+                if (! Cache::has($cacheKey)) { // 已通知次数
+                    Cache::put($cacheKey, 1, now()->addHours(12)); // 键将保留12小时
+                    $times = 1;
                 } else {
+                    $times = Cache::increment($cacheKey);
+                }
+                if ($times > $detectionCheckTimes) {
                     Cache::forget($cacheKey);
                     $node->update(['status' => 0]);
                     $data[$node_id]['message'] = '自动进入维护状态';
                 }
             }
 
-            if (isset($data[$node_id])) {
+            if (! empty($data[$node_id])) {
                 $data[$node_id]['name'] = $node->name;
             }
         }
 
-        if (isset($data)) { //只有在出现阻断线路时,才会发出警报
+        if (! empty($data)) { //只有在出现阻断线路时,才会发出警报
             Notification::send(User::find(1), new NodeBlocked($data));
 
             Log::notice("节点状态日志: \r\n".var_export($data, true));
@@ -132,27 +131,24 @@ class NodeStatusDetection extends Command
 
     private function reliveNode(): void
     {
-        $onlineNode = NodeHeartbeat::recently()->distinct()->pluck('node_id')->toArray();
+        $onlineNode = NodeHeartbeat::recently()->distinct()->pluck('node_id');
         foreach (Node::whereRelayNodeId(null)->whereStatus(0)->whereIn('id', $onlineNode)->where('detection_type', '<>', 0)->get() as $node) {
             $ips = $node->ips();
-            $result = count($ips);
+            $reachableIPs = 0;
+
             foreach ($ips as $ip) {
                 if ($node->detection_type) {
                     $status = (new NetworkDetection)->networkStatus($ip, $node->port ?? 22);
 
-                    if ($node->detection_type === 1 && $status['tcp'] === 1) {
-                        $result--;
-                    } elseif ($node->detection_type === 2 && $status['icmp'] === 1) {
-                        $result--;
-                    } elseif ($status['tcp'] === 1 && $status['icmp'] === 1) {
-                        $result--;
+                    if (($node->detection_type === 1 && $status['tcp'] === 1) || ($node->detection_type === 2 && $status['icmp'] === 1) || ($status['tcp'] === 1 && $status['icmp'] === 1)) {
+                        $reachableIPs++;
                     }
 
                     sleep(1);
                 }
             }
 
-            if ($result === 0) {
+            if ($reachableIPs === count($ips)) {
                 $node->update(['status' => 1]);
             }
         }

+ 156 - 104
app/Console/Commands/PanelInstallation.php

@@ -7,7 +7,9 @@ use Artisan;
 use Exception;
 use File;
 use Illuminate\Console\Command;
+use Illuminate\Contracts\Filesystem\FileNotFoundException;
 use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Str;
 
 class PanelInstallation extends Command
 {
@@ -17,132 +19,182 @@ class PanelInstallation extends Command
 
     public function handle(): int
     {
-        try {
-            $bar = $this->output->createProgressBar(7);
-            $bar->minSecondsBetweenRedraws(0);
-
-            $this->info(PHP_EOL.'   ___                              ___                      _ '.PHP_EOL."  / _ \ _ __   ___  __  __ _   _   / _ \  __ _  _ __    ___ | |".PHP_EOL." / /_)/| '__| / _ \ \ \/ /| | | | / /_)/ / _` || '_ \  / _ \| |".PHP_EOL.'/ ___/ | |   | (_) | >  < | |_| |/ ___/ | (_| || | | ||  __/| |'.PHP_EOL."\/     |_|    \___/ /_/\_\ \__, |\/      \__,_||_| |_| \___||_|".PHP_EOL.'                           |___/                               '.PHP_EOL);
-
-            $bar->start();
-            $this->line(' 创建.env');
-            if (File::exists(base_path().'/.env')) {
-                $this->error('.env existed | .env已存在');
-                if ($this->confirm('Do you wish to continue by deleting current exist .env file? | 是否删除已存.env文件, 并继续安装?', true)) {
-                    File::delete(base_path().'/.env');
-                } else {
-                    abort(500, 'Installation aborted by user decision. | 安装程序终止');
-                }
-            }
-            if (! copy(base_path().'/.env.example', base_path().'/.env')) {
-                abort(500, 'copy .env.example to .env failed, please check file permissions | 复制环境文件失败,请检查目录权限');
-            }
-            $bar->advance();
-
-            // 设置数据库信息
-            $this->line(' 设置数据库信息');
-            $this->saveToEnv([
-                'DB_HOST' => $this->ask('请输入数据库地址(默认:localhost)', 'localhost'),
-                'DB_PORT' => $this->ask('请输入数据库地址(默认:3306)', 3306),
-                'DB_DATABASE' => $this->ask('请输入数据库名'),
-                'DB_USERNAME' => $this->ask('请输入数据库用户名'),
-                'DB_PASSWORD' => $this->ask('请输入数据库密码'),
-            ]);
-            $bar->advance();
-
-            // 设置 app key
-            $this->line(' 设置 app key');
-            Artisan::call('key:generate');
-            $bar->advance();
-
-            // 测试数据库连通性
-            $this->line(' 测试数据库连通性');
-            try {
-                Artisan::call('config:cache');
-                DB::connection()->getPdo();
-            } catch (Exception $e) {
-                File::delete(base_path().'/.env');
-                abort(500, '数据库连接失败'.$e->getMessage());
-            }
-            $bar->advance();
-
-            // 初始化数据库
-            $this->line(' 导入数据库');
-            try {
-                Artisan::call('migrate --seed');
-            } catch (Exception $e) {
-                Artisan::call('db:wipe');
-                abort(500, '导入数据库失败'.$e->getMessage());
-            }
-            $this->info('数据库导入完成');
-            $bar->advance();
+        $bar = $this->output->createProgressBar(7);
+        $bar->minSecondsBetweenRedraws(0);
 
-            // 文件夹软连接
-            $this->line(' 建立文件夹软连接');
-            Artisan::call('storage:link');
-            $bar->advance();
-
-            // 设置 管理员基础信息
-            $this->line(' 设置管理员基础信息');
-            $email = '';
-            while (! $email) {
-                $email = $this->ask('Please set your administrator account email address | 请输入[管理员]邮箱 默认: [email protected]', '[email protected]');
-                $this->info('[管理员] 账号:'.$email);
-            }
-            $password = '';
-            while (! $password) {
-                $password = $this->ask('Please set your administrator account password | 请输入[管理员]密码 默认: 123456', '123456');
-                $this->info('[管理员]密码:'.$password);
-            }
-            if (! $this->editAdmin($email, $password)) {
-                abort(500, '管理员账号注册失败,请重试');
+        $this->displayBanner();
+        $bar->start();
+
+        $this->createEnvironmentFile();
+        $bar->advance();
+
+        $this->setDatabaseInfo(); // 设置数据库信息
+        $bar->advance();
+
+        $this->generateAppKey(); // 设置 app key
+        $bar->advance();
+
+        $this->testDatabaseConnection(); // 测试数据库连通性
+        $bar->advance();
+
+        $this->importDatabase(); // 初始化数据库
+        $bar->advance();
+
+        $this->createStorageLink(); // 文件夹软连接
+        $bar->advance();
+
+        $this->setAdminAccount(); // 设置管理员信息
+        $bar->finish();
+        $this->info('Initial installation completed! | 初步安装完毕');
+
+        $this->line(PHP_EOL.'View your http(s)://[website url]/admin to insert Administrator Dashboard | 访问 http(s)://[你的站点]/admin 进入管理面板');
+
+        return 0;
+    }
+
+    private function displayBanner(): void
+    {
+        $banner = <<<BANNER
+   ___                              ___                      _ 
+  / _ \ _ __   ___  __  __ _   _   / _ \  __ _  _ __    ___ | |
+ / /_)/| '__| / _ \ \ \/ /| | | | / /_)/ / _` || '_ \  / _ \| |
+/ ___/ | |   | (_) | >  < | |_| |/ ___/ | (_| || | | ||  __/| |
+\/     |_|    \___/ /_/\_\ \__, |\/      \__,_||_| |_| \___||_|
+                           |___/                               
+
+BANNER;
+
+        $this->info($banner);
+    }
+
+    private function createEnvironmentFile(): void
+    {
+        $this->line('  Creating .env | 创建.env');
+        $envPath = app()->environmentFilePath();
+
+        if (File::exists($envPath)) {
+            $this->error('.env file already exists. | .env已存在');
+
+            if (! $this->confirm('Do you wish to continue by deleting the existing .env file? | 是否删除已存.env文件, 并继续安装?')) {
+                abort(500, 'Installation aborted by user decision. | 安装程序终止');
             }
-            $bar->finish();
-            $this->info(' Initial installation Completed! | 初步安装完毕');
 
-            $this->line(PHP_EOL.'View your http(s)://[website url]/admin to insert Administrator Dashboard | 访问 http(s)://[你的站点]/admin 进入管理面板');
+            File::delete($envPath);
+        }
+
+        try {
+            File::copy(base_path('.env.example'), $envPath);
         } catch (Exception $e) {
-            $this->error($e->getMessage());
+            abort(500, 'Failed to copy .env.example to .env file. Please check file permissions. | 复制环境文件失败, 请检查目录权限 '.$e->getMessage());
         }
+    }
 
-        return 0;
+    /**
+     * @throws FileNotFoundException
+     */
+    private function setDatabaseInfo(): void
+    {
+        $this->line(' Setting up database information | 设置数据库信息');
+
+        $databaseInfo = [
+            'DB_HOST' => $this->ask('Enter the database host (default: localhost) | 请输入数据库地址(默认:localhost)', 'localhost'),
+            'DB_PORT' => $this->ask('Enter the database port (default: 3306) | 请输入数据库地址(默认:3306)', 3306),
+            'DB_DATABASE' => $this->ask('Enter the database name | 请输入数据库名'),
+            'DB_USERNAME' => $this->ask('Enter the database username | 请输入数据库用户名'),
+            'DB_PASSWORD' => $this->ask('Enter the database password | 请输入数据库密码'),
+        ];
+
+        $this->saveToEnv($databaseInfo);
     }
 
-    private function saveToEnv($data = []): void
+    /**
+     * @throws FileNotFoundException
+     */
+    private function saveToEnv(array $data = []): void
     {
-        function set_env_var($key, $value): void
-        {
-            if (! is_bool(strpos($value, ' '))) {
-                $value = '"'.$value.'"';
-            }
+        $envPath = app()->environmentFilePath();
+        $contents = File::get($envPath);
+
+        foreach ($data as $key => $value) {
             $key = strtoupper($key);
+            $value = Str::contains($value, ' ') ? '"'.$value.'"' : $value;
+            $line = $key.'='.$value;
 
-            $envPath = app()->environmentFilePath();
-            $contents = file_get_contents($envPath);
+            $contents = preg_replace("/^$key=[^\r\n]*/m", $line, $contents, -1, $count);
 
-            preg_match("/^$key=[^\r\n]*/m", $contents, $matches);
+            if ($count === 0) {
+                $contents .= "\n".$line;
+            }
+        }
 
-            $oldValue = count($matches) ? $matches[0] : '';
+        File::put($envPath, $contents);
+        $this->line('.env file updated successfully. | .env文件更新成功');
+    }
 
-            if ($oldValue) {
-                $contents = str_replace($oldValue, "$key=$value", $contents);
-            } else {
-                $contents .= "\n$key=$value\n";
-            }
+    private function generateAppKey(): void
+    {
+        $this->line('  Generating app key | 设置 app key');
 
-            $file = fopen($envPath, 'wb');
-            fwrite($file, $contents);
-            fclose($file);
+        Artisan::call('key:generate');
+    }
+
+    private function testDatabaseConnection(): void
+    {
+        $this->line(' Testing database connection | 测试数据库连通性');
+        try {
+            Artisan::call('config:cache');
+            DB::connection()->getPdo();
+        } catch (Exception $e) {
+            File::delete(app()->environmentFilePath());
+            abort(500, 'Failed to connect to the database: | 数据库连接失败: '.$e->getMessage());
         }
+    }
 
-        foreach ($data as $key => $value) {
-            set_env_var($key, $value);
+    private function importDatabase(): void
+    {
+        $this->line(' Importing database | 导入数据库');
+        try {
+            Artisan::call('migrate --seed');
+        } catch (Exception $e) {
+            Artisan::call('db:wipe');
+            abort(500, 'Failed to import database: | 导入数据库失败: '.$e->getMessage());
+        }
+        $this->info('Database loaded! | 数据库导入完成');
+    }
+
+    private function createStorageLink(): void
+    {
+        $this->line(' Creating storage link | 建立文件夹软连接');
+        try {
+            Artisan::call('storage:link');
+        } catch (Exception $e) {
+            abort(500, 'Failed to create storage link: | 建立文件夹软连接失败: '.$e->getMessage());
         }
     }
 
-    private function editAdmin($email, $password): bool
+    private function setAdminAccount(): void
+    {
+        $this->line(' Setting up admin account | 设置管理员基础信息');
+
+        $username = $this->ask('Please set your administrator account email address | 请输入[管理员]邮箱 默认: [email protected]', '[email protected]');
+        $this->info('[管理员] 账号: '.$username);
+
+        $password = $this->ask('Please set your administrator account password | 请输入[管理员]密码 默认: 123456', '123456');
+        $this->info('[管理员] 密码: '.$password);
+
+        if ($this->editAdmin($username, $password)) {
+            $this->info('Admin account created successfully. | 管理员账号创建成功');
+        } else {
+            abort(500, '管理员账号注册失败,请重试');
+        }
+
+        $this->info('Admin account created successfully.');
+    }
+
+    private function editAdmin(string $username, string $password): bool
     {
         $user = User::find(1);
-        $user->username = $email;
+        $user->username = $username;
         $user->password = $password;
 
         return $user->save();

+ 42 - 20
app/Console/Commands/PanelUpdate.php

@@ -3,7 +3,6 @@
 namespace App\Console\Commands;
 
 use Artisan;
-use Exception;
 use Illuminate\Console\Command;
 
 class PanelUpdate extends Command
@@ -14,25 +13,48 @@ class PanelUpdate extends Command
 
     public function handle(): void
     {
-        try {
-            $bar = $this->output->createProgressBar(2);
-            $bar->minSecondsBetweenRedraws(0);
-            $this->info('   ___                              ___                      _ '.PHP_EOL."  / _ \ _ __   ___  __  __ _   _   / _ \  __ _  _ __    ___ | |".PHP_EOL." / /_)/| '__| / _ \ \ \/ /| | | | / /_)/ / _` || '_ \  / _ \| |".PHP_EOL.'/ ___/ | |   | (_) | >  < | |_| |/ ___/ | (_| || | | ||  __/| |'.PHP_EOL."\/     |_|    \___/ /_/\_\ \__, |\/      \__,_||_| |_| \___||_|".PHP_EOL.'                           |___/                               '.PHP_EOL);
-            $bar->start();
-            $this->line(' 更新数据库...');
-            Artisan::call('migrate --force');
-
-            if (config('app.env') === 'demo' && $this->confirm('检测到您在DEMO模式, 是否重置数据库?')) {
-                Artisan::call('migrate:fresh --seed --force');
-            }
-
-            $bar->advance();
-            $this->line(' 更新缓存...');
-            Artisan::call('optimize');
-            $bar->finish();
-            $this->info(' 更新完毕! ');
-        } catch (Exception $e) {
-            $this->error($e->getMessage());
+        $bar = $this->output->createProgressBar(2);
+        $bar->minSecondsBetweenRedraws(0);
+        $this->displayBanner();
+        $bar->start();
+
+        $this->updateDatabase();
+        $bar->advance();
+
+        $this->updateCache();
+        $bar->finish();
+
+        $this->info(' 更新完毕! ');
+    }
+
+    private function displayBanner(): void
+    {
+        $banner = <<<BANNER
+   ___                              ___                      _ 
+  / _ \ _ __   ___  __  __ _   _   / _ \  __ _  _ __    ___ | |
+ / /_)/| '__| / _ \ \ \/ /| | | | / /_)/ / _` || '_ \  / _ \| |
+/ ___/ | |   | (_) | >  < | |_| |/ ___/ | (_| || | | ||  __/| |
+\/     |_|    \___/ /_/\_\ \__, |\/      \__,_||_| |_| \___||_|
+                           |___/                               
+
+BANNER;
+
+        $this->info($banner);
+    }
+
+    private function updateDatabase(): void
+    {
+        $this->line('更新数据库...');
+        Artisan::call('migrate --force');
+
+        if (config('app.env') === 'demo' && $this->confirm('检测到您在DEMO模式, 是否重置数据库?')) {
+            Artisan::call('migrate:fresh --seed --force');
         }
     }
+
+    private function updateCache(): void
+    {
+        $this->line('更新缓存...');
+        Artisan::call('optimize');
+    }
 }

+ 3 - 5
app/Console/Commands/ServiceTimer.php

@@ -24,10 +24,8 @@ class ServiceTimer extends Command
 
     private function expiredPlan(): void
     {
-        Order::activePlan()
-            ->where('expired_at', '<=', date('Y-m-d H:i:s'))
-            ->chunk(config('tasks.chunk'), function ($orders) {
-                $orders->each->expired(); // 过期订单
-            });
+        Order::activePlan()->where('expired_at', '<=', now())->chunk(config('tasks.chunk'), function ($orders) {
+            $orders->each->expired(); // 过期订单
+        });
     }
 }

+ 50 - 71
app/Console/Commands/TaskAuto.php

@@ -45,91 +45,76 @@ class TaskAuto extends Command
 
     private function orderTimer(): void
     {
-        Order::recentUnPay()->chunk(config('tasks.chunk'), function ($orders) {
+        Order::where(function (Builder $query) {
+            $query->recentUnPay(); // 关闭超时未支付本地订单
+        })->orWhere(function (Builder $query) {
+            $query->whereStatus(1)->where('created_at', '<=', date('Y-m-d H:i:s', strtotime('-'.config('tasks.close.confirmation_orders').' hours'))); // 关闭未处理的人工支付订单
+        })->chunk(config('tasks.chunk'), function ($orders) {
             $orders->each->close();
-        }); // 关闭超时未支付本地订单
-
-        Order::whereStatus(1)->where('created_at', '<=', date('Y-m-d H:i:s', strtotime('-'.config('tasks.close.confirmation_orders').' hours')))->chunk(config('tasks.chunk'), function ($orders) {
-            $orders->each->close();
-        }); // 关闭未处理的人工支付订单
+        });
     }
 
     private function expireCode(): void
     { // 注册验证码自动置无效 & 优惠券无效化
-        // 注册验证码自动置无效
+        // 注册验证码过时 置无效
         VerifyCode::recentUnused()->update(['status' => 2]);
 
-        // 优惠券到期 / 用尽的 自动置无效
-        Coupon::withTrashed()
-            ->whereStatus(0)
-            ->where('end_time', '<=', time())
-            ->orWhereIn('type', [1, 2])
-            ->whereUsableTimes(0)
-            ->update(['status' => 2]);
-
-        // 邀请码到期自动置无效
-        Invite::whereStatus(0)
-            ->where('dateline', '<=', date('Y-m-d H:i:s'))
-            ->update(['status' => 2]);
+        // 优惠券过时 置无效
+        Coupon::withTrashed()->where('status', '<>', 2)->where('end_time', '<=', time())->update(['status' => 2]);
+
+        // 邀请码过时 置无效
+        Invite::whereStatus(0)->where('dateline', '<=', now())->update(['status' => 2]);
     }
 
     private function blockSubscribes(): void
-    { // 封禁访问异常的订阅链接
-        User::activeUser()
-            ->with(['subscribe', 'subscribeLogs'])
-            ->whereHas('subscribe', function (Builder $query) {
-                $query->whereStatus(1); // 获取有订阅且未被封禁用户
-            })
-            ->whereHas('subscribeLogs', function (Builder $query) {
-                $query->whereDate('request_time', '>=', now()->subDay()); //    ->distinct()->count('request_ip');
-            }, '>=', sysConfig('subscribe_ban_times'))
-            ->chunk(config('tasks.chunk'), function ($users) {
-                $trafficBanTime = sysConfig('traffic_ban_time');
-                $ban_time = strtotime($trafficBanTime.' minutes');
-                $dirtyWorks = ['status' => 0, 'ban_time' => $ban_time, 'ban_desc' => 'Subscription link receive abnormal access and banned by the system'];
-                $banMsg = ['time' => $trafficBanTime, 'description' => __('[Auto Task] Blocked Subscription: Subscription with abnormal requests within 24 hours')];
-                foreach ($users as $user) {
-                    $user->subscribe->update($dirtyWorks);
-                    $user->banedLogs()->create($banMsg); // 记录封禁日志
-                }
-            });
+    { // 封禁访问异常地订阅链接
+        $trafficBanTime = sysConfig('traffic_ban_time');
+        $ban_time = strtotime($trafficBanTime.' minutes');
+        $dirtyWorks = ['status' => 0, 'ban_time' => $ban_time, 'ban_desc' => 'Subscription link receive abnormal access and banned by the system'];
+        $banMsg = ['time' => $trafficBanTime, 'description' => __('[Auto Task] Blocked Subscription: Subscription with abnormal requests within 24 hours')];
+
+        User::activeUser()->with(['subscribe', 'subscribeLogs'])->whereHas('subscribe', function (Builder $query) {
+            $query->whereStatus(1); // 获取有订阅且未被封禁用户
+        })->whereHas('subscribeLogs', function (Builder $query) {
+            $query->whereDate('request_time', '>=', now()->subDay()); //    ->distinct()->count('request_ip');
+        }, '>=', sysConfig('subscribe_ban_times'))->chunk(config('tasks.chunk'), function ($users) use ($banMsg, $dirtyWorks) {
+            foreach ($users as $user) {
+                $user->subscribe->update($dirtyWorks);
+                $user->banedLogs()->create($banMsg); // 记录封禁日志
+            }
+        });
     }
 
     private function unblockSubscribes(): void
     {
         UserSubscribe::whereStatus(0)->where('ban_time', '<=', time())->chunk(config('tasks.chunk'), function ($subscribes) {
-            foreach ($subscribes as $subscribe) {
-                $subscribe->update(['status' => 1, 'ban_time' => null, 'ban_desc' => null]);
-            }
+            $subscribes->each->update(['status' => 1, 'ban_time' => null, 'ban_desc' => null]);
         });
     }
 
     private function blockUsers(): void
     { // 封禁账号
         // 禁用流量超限用户
-        User::activeUser()
-            ->whereRaw('u + d >= transfer_enable')
-            ->chunk(config('tasks.chunk'), function ($users) {
-                foreach ($users as $user) {
-                    $user->update(['enable' => 0]);
-                    $user->banedLogs()->create(['description' => __('[Auto Task] Blocked service: Run out of traffic')]); // 写入日志
-                }
+        User::activeUser()->whereRaw('u + d >= transfer_enable')->chunk(config('tasks.chunk'), function ($users) {
+            $users->each(function ($user) {
+                $user->update(['enable' => 0]);
+                $user->banedLogs()->create(['description' => __('[Auto Task] Blocked service: Run out of traffic')]);
             });
+        });
 
         // 封禁1小时内流量异常账号
         if (sysConfig('is_traffic_ban')) {
             $trafficBanTime = sysConfig('traffic_ban_time');
-            User::activeUser()
-                ->whereBanTime(null)
-                ->chunk(config('tasks.chunk'), function ($users) use ($trafficBanTime) {
-                    $ban_time = strtotime($trafficBanTime.' minutes');
-                    foreach ($users as $user) {
-                        // 多往前取5分钟,防止数据统计任务执行时间过长导致没有数据
+            $ban_time = strtotime($trafficBanTime.' minutes');
+
+            User::activeUser()->whereBanTime(null)->where('t', '>=', strtotime('-5 minutes')) // 只检测最近5分钟有流量使用的用户
+                ->chunk(config('tasks.chunk'), function ($users) use ($ban_time, $trafficBanTime) {
+                    $users->each(function ($user) use ($ban_time, $trafficBanTime) {
                         if ($user->isTrafficWarning()) {
                             $user->update(['enable' => 0, 'ban_time' => $ban_time]);
                             $user->banedLogs()->create(['time' => $trafficBanTime, 'description' => __('[Auto Task] Blocked service: Abnormal traffic within 1 hour')]); // 写入日志
                         }
-                    }
+                    });
                 });
         }
     }
@@ -137,25 +122,19 @@ class TaskAuto extends Command
     private function unblockUsers(): void
     { // 解封账号
         // 解封被临时封禁的账号
-        User::bannedUser()
-            ->where('ban_time', '<', time())
-            ->chunk(config('tasks.chunk'), function ($users) {
-                foreach ($users as $user) {
-                    $user->update(['enable' => 1, 'ban_time' => null]);
-                    $user->banedLogs()->create(['description' => '[自动任务]解封服务: 封禁到期']); // 写入操作日志
-                }
+        User::bannedUser()->where('ban_time', '<', time())->chunk(config('tasks.chunk'), function ($users) {
+            $users->each(function ($user) {
+                $user->update(['enable' => 1, 'ban_time' => null]);
+                $user->banedLogs()->create(['description' => __('[Auto Task] Unblocked Service: Account ban expired')]);
             });
+        });
 
         // 可用流量大于已用流量也解封(比如:邀请返利加了流量)
-        User::bannedUser()
-            ->whereBanTime(null)
-            ->where('expired_at', '>=', date('Y-m-d'))
-            ->whereRaw('u + d < transfer_enable')
-            ->chunk(config('tasks.chunk'), function ($users) {
-                foreach ($users as $user) {
-                    $user->update(['enable' => 1]);
-                    $user->banedLogs()->create(['description' => '[自动任务]解封服务: 出现可用流量']); // 写入操作日志
-                }
+        User::bannedUser()->whereBanTime(null)->where('expired_at', '>=', date('Y-m-d'))->whereRaw('u + d < transfer_enable')->chunk(config('tasks.chunk'), function ($users) {
+            $users->each(function ($user) {
+                $user->update(['enable' => 1]);
+                $user->banedLogs()->create(['description' => __('[Auto Task] Unblocked Service: Account has available data traffic')]);
             });
+        });
     }
 }

+ 91 - 78
app/Console/Commands/TaskDaily.php

@@ -8,6 +8,7 @@ use App\Models\User;
 use App\Notifications\TicketClosed;
 use App\Services\OrderService;
 use App\Utils\Helpers;
+use DB;
 use Illuminate\Console\Command;
 use Illuminate\Database\Eloquent\Builder;
 use Log;
@@ -40,88 +41,77 @@ class TaskDaily extends Command
     private function expireUser(): void
     { // 过期用户处理
         $isBanStatus = sysConfig('is_ban_status');
-        User::activeUser()
-            ->where('expired_at', '<', date('Y-m-d')) // 过期
-            ->chunk(config('tasks.chunk'), function ($users) use ($isBanStatus) {
-                $dirtyWorks = [
-                    'u' => 0,
-                    'd' => 0,
-                    'transfer_enable' => 0,
-                    'enable' => 0,
-                    'level' => 0,
-                    'reset_time' => null,
-                    'ban_time' => null,
-                ]; // 停止服务
-                $banMsg = __('[Daily Task] Account Expiration: Stop Service');
-                if ($isBanStatus) {
-                    $dirtyWorks['status'] = -1; // 封禁账号
-                    $banMsg = __('[Daily Task] Account Expiration: Block Login & Clear Account');
-                }
-                foreach ($users as $user) {
+
+        $dirtyWorks = [
+            'u' => 0,
+            'd' => 0,
+            'transfer_enable' => 0,
+            'enable' => 0,
+            'level' => 0,
+            'reset_time' => null,
+            'ban_time' => null,
+        ]; // 清理账号 & 停止服务
+
+        $banMsg = __('[Daily Task] Account Expiration: Stop Service');
+        if ($isBanStatus) {
+            $dirtyWorks['status'] = -1; // 封禁账号
+            $banMsg = __('[Daily Task] Account Expiration: Block Login & Clear Account');
+        }
+
+        User::activeUser()->where('expired_at', '<', date('Y-m-d')) // 过期
+            ->chunk(config('tasks.chunk'), function ($users) use ($banMsg, $dirtyWorks) {
+                $users->each(function ($user) use ($banMsg, $dirtyWorks) {
                     $user->update($dirtyWorks);
                     Helpers::addUserTrafficModifyLog($user->id, $user->transfer_enable, 0, $banMsg);
                     $user->banedLogs()->create(['description' => $banMsg]);
-                    if ($isBanStatus) {
-                        $user->invites()->whereStatus(0)->update(['status' => 2]); // 废除其名下邀请码
-                    }
-                }
+                });
             });
     }
 
     private function closeTickets(): void
     { // 关闭用户超时未处理的工单
-        Ticket::whereStatus(1)->with('reply')
-            ->whereHas('reply', function ($q) {
-                $q->where('admin_id', '<>', null);
-            })
-            ->where('updated_at', '<=', date('Y-m-d', strtotime('-'.config('tasks.close.tickets').' hours')))
-            ->chunk(config('tasks.chunk'), function ($tickets) {
-                foreach ($tickets as $ticket) {
-                    if ($ticket->close()) {
-                        $ticket->user->notify(new TicketClosed($ticket->id, $ticket->title,
-                            route('replyTicket', ['id' => $ticket->id]),
-                            __('You have not responded this ticket in :num hours, System has closed your ticket.', ['num' => config('tasks.close.tickets')])));
-                    }
+        $closeTicketsHours = config('tasks.close.tickets');
+
+        Ticket::whereStatus(1)->with('reply')->whereHas('reply', function ($query) {
+            $query->where('admin_id', '<>', null);
+        })->where('updated_at', '<=', now()->subHours($closeTicketsHours))->chunk(config('tasks.chunk'), function ($tickets) use ($closeTicketsHours) {
+            $tickets->each(function ($ticket) use ($closeTicketsHours) {
+                if ($ticket->close()) {
+                    $ticket->user->notify(new TicketClosed($ticket->id, $ticket->title, route('replyTicket', ['id' => $ticket->id]),
+                        __('You have not responded this ticket in :num hours, System has closed your ticket.', ['num' => $closeTicketsHours])));
                 }
             });
+        });
     }
 
     private function resetUserTraffic(): void
     { // 重置用户流量
-        User::where('status', '<>', -1)
-            ->where('expired_at', '>', date('Y-m-d'))
-            ->where('reset_time', '<=', date('Y-m-d'))
-            ->with('orders')->has('orders')
-            ->chunk(config('tasks.chunk'), function ($users) {
-                foreach ($users as $user) {
-                    $order = $user->orders()->activePlan()->first(); // 取出用户正在使用的套餐
-
-                    if (! $order) {// 无套餐用户跳过
-                        Log::error('用户[ID:'.$user->id.'] 流量重置, 用户订单获取失败');
-
-                        continue;
-                    }
-
-                    $user->orders()->activePackage()->update(['is_expire' => 1]); // 过期生效中的加油包
-
-                    $oldData = $user->transfer_enable;
-                    // 重置流量与重置日期
-                    if ($user->update((new OrderService($order))->resetTimeAndData($user->expired_at))) {
-                        Helpers::addUserTrafficModifyLog($order->user_id, $oldData, $user->transfer_enable, __('[Daily Task] Reset Account Traffic, Next Reset Date: :date', ['date' => $user->reset_date]), $order->id);
-                    } else {
-                        Log::error("[每日任务]用户ID: $user->id | 邮箱: $user->username 流量重置失败");
-                    }
+        $today = date('Y-m-d');
+        User::where('status', '<>', -1)->where('expired_at', '>', $today)->where('reset_time', '<=', $today)->with([
+            'orders' => function ($query) {
+                $query->activePlan();
+            },
+        ])->has('orders')->chunk(config('tasks.chunk'), function ($users) {
+            $users->each(function ($user) {
+                $user->orders()->activePackage()->update(['is_expire' => 1]); // 过期生效中的加油包
+                $order = $user->orders()->activePlan()->first(); // 取出用户正在使用的套餐
+
+                $oldData = $user->transfer_enable;
+                // 重置流量与重置日期
+                if ($user->update((new OrderService($order))->resetTimeAndData($user->expired_at))) {
+                    Helpers::addUserTrafficModifyLog($order->user_id, $oldData, $user->transfer_enable, __('[Daily Task] Reset Account Traffic, Next Reset Date: :date', ['date' => $user->reset_date]), $order->id);
+                } else {
+                    Log::error("[每日任务]用户ID: $user->id | 邮箱: $user->username 流量重置失败");
                 }
             });
+        });
     }
 
     private function releaseAccountPort(): void
     { // 被封禁 / 过期N天 的账号自动释放端口
-        User::where('port', '<>', 0)
-            ->where(function ($query) {
-                $query->whereStatus(-1)->orWhere('expired_at', '<=', date('Y-m-d', strtotime('-'.config('tasks.release_port').' days')));
-            })
-            ->update(['port' => 0]);
+        User::where('port', '<>', 0)->where(function (Builder $query) {
+            $query->whereStatus(-1)->orWhere('expired_at', '<=', date('Y-m-d', strtotime('-'.config('tasks.release_port').' days')));
+        })->update(['port' => 0]);
     }
 
     private function userTrafficStatistics(): void
@@ -130,24 +120,36 @@ class TaskDaily extends Command
         $end = strtotime($created_at);
         $start = $end - 86399;
 
-        User::activeUser()->with('dataFlowLogs')->whereHas('dataFlowLogs', function (Builder $query) use ($start, $end) {
+        User::activeUser()->whereHas('dataFlowLogs', function (Builder $query) use ($start, $end) {
             $query->whereBetween('log_time', [$start, $end]);
-        })->chunk(config('tasks.chunk'), function ($users) use ($start, $end, $created_at) {
+        })->with([
+            'dataFlowLogs' => function ($query) use ($start, $end) {
+                $query->whereBetween('log_time', [$start, $end]);
+            },
+        ])->chunk(config('tasks.chunk'), function ($users) use ($created_at) {
             foreach ($users as $user) {
-                $logs = $user->dataFlowLogs()
-                    ->whereBetween('log_time', [$start, $end])
-                    ->groupBy('node_id')
-                    ->selectRaw('node_id, sum(`u`) as u, sum(`d`) as d')
-                    ->get();
+                $dataFlowLogs = $user->dataFlowLogs->groupBy('node_id');
+
+                $data = $dataFlowLogs->map(function ($logs, $nodeId) use ($created_at) {
+                    $totals = $logs->reduce(function ($carry, $log) {
+                        $carry['u'] += $log['u'];
+                        $carry['d'] += $log['d'];
 
-                $data = $logs->each(function ($log) use ($created_at) {
-                    $log->created_at = $created_at;
-                })->toArray();
+                        return $carry;
+                    }, ['u' => 0, 'd' => 0]);
+
+                    return [
+                        'node_id' => $nodeId,
+                        'u' => $totals['u'],
+                        'd' => $totals['d'],
+                        'created_at' => $created_at,
+                    ];
+                })->values()->all();
 
                 $data[] = [ // 每日节点流量合计
                     'node_id' => null,
-                    'u' => $logs->sum('u'),
-                    'd' => $logs->sum('d'),
+                    'u' => array_sum(array_column($data, 'u')),
+                    'd' => array_sum(array_column($data, 'd')),
                     'created_at' => $created_at,
                 ];
 
@@ -162,12 +164,23 @@ class TaskDaily extends Command
         $end = strtotime($created_at);
         $start = $end - 86399;
 
-        Node::with('userDataFlowLogs')->whereHas('userDataFlowLogs', function (Builder $query) use ($start, $end) {
+        Node::whereHas('userDataFlowLogs', function (Builder $query) use ($start, $end) {
             $query->whereBetween('log_time', [$start, $end]);
-        })->chunk(config('tasks.chunk'), function ($nodes) use ($start, $end, $created_at) {
+        })->withCount([
+            'userDataFlowLogs as u_sum' => function ($query) use ($start, $end) {
+                $query->select(DB::raw('SUM(u)'))->whereBetween('log_time', [$start, $end]);
+            },
+        ])->withCount([
+            'userDataFlowLogs as d_sum' => function ($query) use ($start, $end) {
+                $query->select(DB::raw('SUM(d)'))->whereBetween('log_time', [$start, $end]);
+            },
+        ])->chunk(config('tasks.chunk'), function ($nodes) use ($created_at) {
             foreach ($nodes as $node) {
-                $log = $node->userDataFlowLogs()->whereBetween('log_time', [$start, $end])->selectRaw('sum(`u`) as u, sum(`d`) as d')->first();
-                $node->dailyDataFlows()->create(['u' => $log->u, 'd' => $log->d, 'created_at' => $created_at]);
+                $node->dailyDataFlows()->create([
+                    'u' => $node->u_sum,
+                    'd' => $node->d_sum,
+                    'created_at' => $created_at,
+                ]);
             }
         });
     }

+ 45 - 22
app/Console/Commands/TaskHourly.php

@@ -5,6 +5,7 @@ namespace App\Console\Commands;
 use App\Models\Node;
 use App\Models\User;
 use App\Notifications\DataAnomaly;
+use DB;
 use Illuminate\Console\Command;
 use Illuminate\Database\Eloquent\Builder;
 use Log;
@@ -34,32 +35,47 @@ class TaskHourly extends Command
         $start = $end - 3599;
         $data_anomaly_notification = sysConfig('data_anomaly_notification');
         $traffic_ban_value = (int) sysConfig('traffic_ban_value') * GiB;
-        User::activeUser()->with('dataFlowLogs')->WhereHas('dataFlowLogs')->whereHas('dataFlowLogs', function (Builder $query) use ($start, $end) {
+        User::activeUser()->whereHas('dataFlowLogs', function (Builder $query) use ($start, $end) {
             $query->whereBetween('log_time', [$start, $end]);
-        })->chunk(config('tasks.chunk'), function ($users) use ($traffic_ban_value, $start, $end, $created_at, $data_anomaly_notification) {
+        })->with([
+            'dataFlowLogs' => function ($query) use ($start, $end) {
+                $query->whereBetween('log_time', [$start, $end]);
+            },
+        ])->chunk(config('tasks.chunk'), function ($users) use ($traffic_ban_value, $created_at, $data_anomaly_notification) {
             foreach ($users as $user) {
-                $logs = $user->dataFlowLogs()
-                    ->whereBetween('log_time', [$start, $end])
-                    ->groupBy('node_id')
-                    ->selectRaw('node_id, sum(`u`) as u, sum(`d`) as d')
-                    ->get();
-
-                $data = $logs->each(function ($log) use ($created_at) {
-                    $log->created_at = $created_at;
-                })->toArray();
-                $overall = [ // 每小时节点流量合计
+                $dataFlowLogs = $user->dataFlowLogs->groupBy('node_id');
+
+                $data = $dataFlowLogs->map(function ($logs, $nodeId) use ($created_at) {
+                    $totals = $logs->reduce(function ($carry, $log) {
+                        $carry['u'] += $log['u'];
+                        $carry['d'] += $log['d'];
+
+                        return $carry;
+                    }, ['u' => 0, 'd' => 0]);
+
+                    return [
+                        'node_id' => $nodeId,
+                        'u' => $totals['u'],
+                        'd' => $totals['d'],
+                        'created_at' => $created_at,
+                    ];
+                })->values()->all();
+
+                $sum_u = array_sum(array_column($data, 'u'));
+                $sum_d = array_sum(array_column($data, 'd'));
+                $data[] = [ // 每小时节点流量合计
                     'node_id' => null,
-                    'u' => $logs->sum('u'),
-                    'd' => $logs->sum('d'),
+                    'u' => $sum_u,
+                    'd' => $sum_d,
                     'created_at' => $created_at,
                 ];
-                $data[] = $overall;
+
                 $user->hourlyDataFlows()->createMany($data);
-                $overall['total'] = $overall['u'] + $overall['d'];
+                $sum_all = $sum_u + $sum_d;
 
                 // 用户流量异常警告
-                if ($data_anomaly_notification && $overall['total'] >= $traffic_ban_value) {
-                    Notification::send(User::find(1), new DataAnomaly($user->id, formatBytes($overall['u']), formatBytes($overall['d']), formatBytes($overall['total'])));
+                if ($data_anomaly_notification && $sum_all >= $traffic_ban_value) {
+                    Notification::send(User::find(1), new DataAnomaly($user->id, formatBytes($sum_u), formatBytes($sum_d), formatBytes($sum_all)));
                 }
             }
         });
@@ -71,12 +87,19 @@ class TaskHourly extends Command
         $end = strtotime($created_at);
         $start = $end - 3599;
 
-        Node::orderBy('id')->with('userDataFlowLogs')->whereHas('userDataFlowLogs', function (Builder $query) use ($start, $end) {
+        Node::whereHas('userDataFlowLogs', function (Builder $query) use ($start, $end) {
             $query->whereBetween('log_time', [$start, $end]);
-        })->chunk(config('tasks.chunk'), function ($nodes) use ($start, $end, $created_at) {
+        })->withCount([
+            'userDataFlowLogs as u_sum' => function ($query) use ($start, $end) {
+                $query->select(DB::raw('SUM(u)'))->whereBetween('log_time', [$start, $end]);
+            },
+        ])->withCount([
+            'userDataFlowLogs as d_sum' => function ($query) use ($start, $end) {
+                $query->select(DB::raw('SUM(d)'))->whereBetween('log_time', [$start, $end]);
+            },
+        ])->chunk(config('tasks.chunk'), function ($nodes) use ($created_at) {
             foreach ($nodes as $node) {
-                $traffic = $node->userDataFlowLogs()->whereBetween('log_time', [$start, $end])->selectRaw('sum(`u`) as u, sum(`d`) as d')->first();
-                $node->hourlyDataFlows()->create(['u' => $traffic->u, 'd' => $traffic->d, 'created_at' => $created_at]);
+                $node->hourlyDataFlows()->create(['u' => $node->u_sum, 'd' => $node->d_sum, 'created_at' => $created_at]);
             }
         });
     }

+ 3 - 2
app/Console/Commands/TaskMonthly.php

@@ -34,8 +34,9 @@ class TaskMonthly extends Command
 
     private function cleanAccounts(): void
     {
-        User::where('expired_at', '<', date('Y-m-d'))->where('transfer_enable', '==', 0)->whereEnable(0)
-            ->whereRaw('u + d > transfer_enable')->update(['u' => 0, 'd' => 0]);
+        User::where('expired_at', '<', date('Y-m-d'))->where('transfer_enable', '=', 0)->whereEnable(0)->where(function ($query) {
+            $query->where('u', '>', 0)->orWhere('d', '>', 0);
+        })->update(['u' => 0, 'd' => 0]);
     }
 
     private function clearLog(): void

+ 3 - 3
app/Helpers/ClientApiResponse.php

@@ -21,12 +21,12 @@ trait ClientApiResponse
         self::$client = $client;
     }
 
-    public function succeed(array $data = null, array $addition = null, array $codeResponse = ResponseEnum::HTTP_OK): JsonResponse
+    public function succeed(?array $data = null, ?array $addition = null, array $codeResponse = ResponseEnum::HTTP_OK): JsonResponse
     {
         return $this->jsonResponse(1, $codeResponse, $data, $addition);
     }
 
-    private function jsonResponse(int $status, array $codeResponse, array|string $data = null, array $addition = null): JsonResponse
+    private function jsonResponse(int $status, array $codeResponse, array|string|null $data = null, ?array $addition = null): JsonResponse
     {
         [$code, $message] = $codeResponse;
         $code = $code > 1000 ? (int) ($code / 1000) : $code;
@@ -47,7 +47,7 @@ trait ClientApiResponse
         return response()->json($result, $code, ['content-type' => 'application/json']);
     }
 
-    public function failed(array $codeResponse = ResponseEnum::HTTP_ERROR, array|string $data = null): JsonResponse
+    public function failed(array $codeResponse = ResponseEnum::HTTP_ERROR, array|string|null $data = null): JsonResponse
     {
         return $this->jsonResponse(0, $codeResponse, is_array($data) ? $data[0] : $data);
     }

+ 1 - 1
app/Helpers/ClientConfig.php

@@ -100,7 +100,7 @@ trait ClientConfig
         return $encode ? base64_encode($uri) : $uri;
     }
 
-    private function clash(string $client = null): string
+    private function clash(?string $client = null): string
     {
         $user = $this->getUser();
         $webName = sysConfig('website_name');

+ 3 - 3
app/Helpers/WebApiResponse.php

@@ -6,12 +6,12 @@ use Illuminate\Http\JsonResponse;
 
 trait WebApiResponse
 {
-    public function succeed(array $data = null, array $addition = null, array $codeResponse = ResponseEnum::HTTP_OK): JsonResponse // 成功
+    public function succeed(?array $data = null, ?array $addition = null, array $codeResponse = ResponseEnum::HTTP_OK): JsonResponse // 成功
     {
         return $this->jsonResponse('success', $codeResponse, $data, $addition);
     }
 
-    private function jsonResponse(string $status, array $codeResponse, array $data = null, array $addition = null): JsonResponse // 返回数据
+    private function jsonResponse(string $status, array $codeResponse, ?array $data = null, ?array $addition = null): JsonResponse // 返回数据
     {
         [$code, $message] = $codeResponse;
         if ($status === 'success') {
@@ -42,7 +42,7 @@ trait WebApiResponse
         return $etag;
     }
 
-    public function failed(array $codeResponse = ResponseEnum::HTTP_ERROR, array $data = null): JsonResponse // 失败
+    public function failed(array $codeResponse = ResponseEnum::HTTP_ERROR, ?array $data = null): JsonResponse // 失败
     {
         return $this->jsonResponse('fail', $codeResponse, $data);
     }

+ 1 - 1
app/Http/Controllers/Admin/LogsController.php

@@ -142,7 +142,7 @@ class LogsController extends Controller
     }
 
     // 在线IP监控(实时)
-    public function onlineIPMonitor(Request $request, int $id = null)
+    public function onlineIPMonitor(Request $request, ?int $id = null)
     {
         $query = NodeOnlineIp::with(['node:id,name', 'user:id,username'])->where('created_at', '>=', strtotime('-2 minutes'));
 

+ 1 - 1
app/Http/Controllers/Admin/NodeAuthController.php

@@ -15,7 +15,7 @@ class NodeAuthController extends Controller
     // 节点授权列表
     public function index()
     {
-        return view('admin.node.auth', ['authorizations' => NodeAuth::with('node:id,name,type,server,ip')->has('node')->orderBy('node_id')->paginate()->appends(request('page'))]);
+        return view('admin.node.auth', ['authorizations' => NodeAuth::with('node:id,name,type,server,ip,ipv6')->has('node')->orderBy('node_id')->paginate()->appends(request('page'))]);
     }
 
     // 添加节点授权

+ 2 - 2
app/Http/Controllers/Admin/ToolsController.php

@@ -168,8 +168,8 @@ class ToolsController extends Controller
                     $obj->obfs = $user->obfs;
                     $obj->expired_at = '2099-01-01';
                     $obj->reg_ip = IP::getClientIp();
-                    $obj->created_at = date('Y-m-d H:i:s');
-                    $obj->updated_at = date('Y-m-d H:i:s');
+                    $obj->created_at = now();
+                    $obj->updated_at = now();
                     $obj->save();
                 }
 

+ 1 - 1
app/Http/Controllers/Api/Client/ClientController.php

@@ -245,7 +245,7 @@ class ClientController extends Controller
         return $this->succeed(null, ['config' => $config]);
     }
 
-    private function clientConfig(string $key = null)
+    private function clientConfig(?string $key = null)
     {
         if (! config('client')) {
             Artisan::call('config:cache');

+ 1 - 1
app/Http/Controllers/User/SubscribeController.php

@@ -84,7 +84,7 @@ class SubscribeController extends Controller
         $log = new UserSubscribeLog();
         $log->user_subscribe_id = $subscribeId;
         $log->request_ip = $ip;
-        $log->request_time = date('Y-m-d H:i:s');
+        $log->request_time = now();
         $log->request_header = $headers;
         $log->save();
     }

+ 1 - 1
app/Models/Article.php

@@ -25,7 +25,7 @@ class Article extends Model
         return $query->whereType($type);
     }
 
-    public function scopeLang(Builder $query, string $language = null): Builder
+    public function scopeLang(Builder $query, ?string $language = null): Builder
     {
         return $query->whereLanguage($language ?? app()->getLocale());
     }

+ 1 - 1
app/Models/User.php

@@ -224,7 +224,7 @@ class User extends Authenticatable
         return $query->where('status', '<>', -1)->whereEnable(0);
     }
 
-    public function nodes(int $userLevel = null, int $userGroupId = null): Node|Builder|BelongsToMany
+    public function nodes(?int $userLevel = null, ?int $userGroupId = null): Node|Builder|BelongsToMany
     {
         if ($userGroupId === null && $this->user_group_id) { // 使用默认的用户分组
             $query = $this->userGroup->nodes();

+ 4 - 0
app/Observers/UserObserver.php

@@ -63,6 +63,10 @@ class UserObserver
                 }
             }
         }
+
+        if ($user->status === -1 && Arr::has($changes, ['status'])) {
+            $user->invites()->whereStatus(0)->update(['status' => 2]); // 废除其名下邀请码
+        }
     }
 
     public function deleted(User $user): void

+ 20 - 14
app/Services/ArticleService.php

@@ -38,30 +38,36 @@ class ArticleService
     private function formatAccessible(string &$body): void
     {
         $noAccess = ! (new UserService)->isActivePaying();
+        $mode1Start = '<!--access_mode_1 start-->';
+        $mode1End = '<!--access_mode_1 end-->';
+        $mode2Start = '<!--access_mode_2 start-->';
+        $mode2End = '<!--access_mode_2 end-->';
+        $mode2Else = '<!--access_mode_2 else-->';
 
         if ($noAccess) {
-            while ($this->getInBetween($body, '<!--access_mode_1 start-->', '<!--access_mode_1 end-->', true) !== '') {
-                $accessArea = $this->getInBetween($body, '<!--access_mode_1 start-->', '<!--access_mode_1 end-->');
-                if ($accessArea) {
-                    $body = strtr($body,
-                        [$accessArea => '<div class="user-no-access"><i class="icon wb-lock" aria-hidden="true"></i>'.__('You must have a valid subscription to view the content in this area!').'</div>']);
-                }
+            while (($accessArea = $this->getInBetween($body, $mode1Start, $mode1End)) !== '') {
+                $replacement = '<div class="user-no-access"><i class="icon wb-lock" aria-hidden="true"></i>'.__('You must have a valid subscription to view the content in this area!').'</div>';
+                $body = str_replace($mode1Start.$accessArea.$mode1End, $replacement, $body);
             }
         }
 
-        while ($this->getInBetween($body, '<!--access_mode_2 start-->', '<!--access_mode_2 end-->', true) !== '') {
-            $accessArea = $this->getInBetween($body, '<!--access_mode_2 start-->', '<!--access_mode_2 end-->');
-            $hasAccessArea = $this->getInBetween($accessArea, '<!--access_mode_2 start-->', '<!--access_mode_2 else-->', true);
-            $noAccessArea = $this->getInBetween($accessArea, '<!--access_mode_2 else-->', '<!--access_mode_2 end-->', true);
-            $body = strtr($body, [$accessArea => $accessArea && $noAccess ? $noAccessArea : $hasAccessArea]);
+        while (($accessArea = $this->getInBetween($body, $mode2Start, $mode2End)) !== '') {
+            $hasAccessArea = $this->getInBetween($accessArea, '', $mode2Else);
+            $noAccessArea = $this->getInBetween($accessArea, $mode2Else, '');
+            $body = strtr($body, [$mode2Start.$accessArea.$mode2End => $noAccess ? $noAccessArea : $hasAccessArea]);
         }
     }
 
-    private function getInBetween(string $input, string $start, string $end, bool $bodyOnly = false): string
+    private function getInBetween(string $input, string $start, string $end): string
     {
-        $substr = substr($input, strpos($input, $start) + strlen($start), strpos($input, $end) - strlen($input));
+        $startPos = stripos($input, $start);
+        $endPos = stripos($input, $end, $startPos !== false ? $startPos + strlen($start) : 0);
 
-        return $bodyOnly ? $substr : $start.$substr.$end;
+        if ($startPos === false || $endPos === false) {
+            return '';
+        }
+
+        return substr($input, $startPos + strlen($start), $endPos - ($startPos + strlen($start)));
     }
 
     private function formatValuables(string &$body): void

+ 4 - 4
app/Services/CouponService.php

@@ -39,6 +39,10 @@ class CouponService
     private function check(Goods $goods, Coupon $coupon): JsonResponse|bool
     { // 检查券合规性
         $user = $this->user;
+        if ($coupon->status === 2) {
+            return $this->failedReturn(trans('common.sorry'), trans('user.coupon.error.expired'));
+        }
+
         if ($coupon->status === 1) {
             return $this->failedReturn(trans('common.sorry'), trans('user.coupon.error.used'));
         }
@@ -53,10 +57,6 @@ class CouponService
             return $this->failedReturn(trans('common.sorry'), trans('user.coupon.error.run_out'));
         }
 
-        if ($coupon->status === 2) {
-            return $this->failedReturn(trans('common.sorry'), trans('user.coupon.error.expired'));
-        }
-
         if (time() < $coupon->getRawOriginal('start_time')) {
             return $this->failedReturn(trans('user.coupon.error.inactive'), trans('user.coupon.error.wait', ['time' => $coupon->start_time]));
         }

+ 1 - 1
app/Services/OrderService.php

@@ -100,7 +100,7 @@ class OrderService
         return false;
     }
 
-    public function resetTimeAndData(string $expired_at = null): array
+    public function resetTimeAndData(?string $expired_at = null): array
     { // 计算下次重置与账号过期时间
         if (! $expired_at) { // 账号有效期
             $expired_at = $this->getFinallyExpiredTime();

+ 5 - 5
app/Services/ProxyService.php

@@ -17,12 +17,12 @@ class ProxyService
 
     private static array $servers;
 
-    public function __construct(User $user = null)
+    public function __construct(?User $user = null)
     {
         $this->setUser($user ?? auth()->user());
     }
 
-    public function setUser(User $user = null): void
+    public function setUser(?User $user = null): void
     {
         self::$user = $user;
     }
@@ -37,7 +37,7 @@ class ProxyService
         return self::$servers;
     }
 
-    public function getProxyText(string $target, int $type = null): string
+    public function getProxyText(string $target, ?int $type = null): string
     {
         $servers = $this->getNodeList($type);
         if (empty($servers)) {
@@ -58,7 +58,7 @@ class ProxyService
         return $this->clientConfig($target);
     }
 
-    public function getNodeList(int $type = null, bool $isConfig = true): array
+    public function getNodeList(?int $type = null, bool $isConfig = true): array
     {
         $query = self::$user->nodes()->whereIn('is_display', [2, 3]); // 获取这个账号可用节点
 
@@ -171,7 +171,7 @@ class ProxyService
         self::$servers = $servers;
     }
 
-    public function getProxyCode(string $target, int $type = null): ?string
+    public function getProxyCode(string $target, ?int $type = null): ?string
     {// 客户端用代理信息
         $servers = $this->getNodeList($type);
         if (empty($servers)) {

+ 1 - 1
app/Services/TelegramService.php

@@ -8,7 +8,7 @@ class TelegramService
 {
     private static string $api;
 
-    public function __construct(string $token = null)
+    public function __construct(?string $token = null)
     {
         self::$api = 'https://api.telegram.org/bot'.($token ?? sysConfig('telegram_token')).'/';
     }

+ 1 - 1
app/Services/UserService.php

@@ -9,7 +9,7 @@ class UserService
 {
     private static User $user;
 
-    public function __construct(User $user = null)
+    public function __construct(?User $user = null)
     {
         self::$user = $user ?? auth()->user();
     }

+ 2 - 2
app/Utils/CurrencyExchange.php

@@ -20,7 +20,7 @@ class CurrencyExchange
      * @param  string|null  $base  Base Currency
      * @return float|null amount in target currency
      */
-    public static function convert(string $target, float|int $amount, string $base = null): ?float
+    public static function convert(string $target, float|int $amount, ?string $base = null): ?float
     {
         if ($base === null) {
             $base = (string) sysConfig('standard_currency');
@@ -297,7 +297,7 @@ class CurrencyExchange
         return null;
     }
 
-    public static function unionTest(string $target, string $base = null): void
+    public static function unionTest(string $target, ?string $base = null): void
     {
         self::setClient();
         foreach (self::$apis as $api) {

+ 1 - 1
app/Utils/DDNS/CloudFlare.php

@@ -38,7 +38,7 @@ class CloudFlare implements DNS
         exit(400);
     }
 
-    private function send(string $action, array $parameters = [], string $identifier = null): array
+    private function send(string $action, array $parameters = [], ?string $identifier = null): array
     {
         $client = Http::timeout(10)->retry(3, 1000)->withHeaders($this->auth)->baseUrl($this->apiHost)->asJson();
 

+ 7 - 7
app/Utils/Helpers.php

@@ -53,7 +53,7 @@ class Helpers
      * @param  int|null  $inviter_id  邀请人
      * @param  string|null  $nickname  昵称
      */
-    public static function addUser(string $username, string $password, int $transfer_enable = 0, int $date = 0, int $inviter_id = null, string $nickname = null): User
+    public static function addUser(string $username, string $password, int $transfer_enable = 0, int $date = 0, ?int $inviter_id = null, ?string $nickname = null): User
     {
         return User::create([
             'nickname' => $nickname ?? $username,
@@ -133,7 +133,7 @@ class Helpers
      * @param  string|null  $msgId  对公查询ID
      * @param  string  $address  收信方
      */
-    public static function addNotificationLog(string $title, string $content, int $type, int $status = 1, string $error = null, string $msgId = null, string $address = 'admin'): int
+    public static function addNotificationLog(string $title, string $content, int $type, int $status = 1, ?string $error = null, ?string $msgId = null, string $address = 'admin'): int
     {
         $log = new NotificationLog();
         $log->type = $type;
@@ -156,7 +156,7 @@ class Helpers
      * @param  int|null  $goodsId  商品ID
      * @param  int|null  $orderId  订单ID
      */
-    public static function addCouponLog(string $description, int $couponId, int $goodsId = null, int $orderId = null): bool
+    public static function addCouponLog(string $description, int $couponId, ?int $goodsId = null, ?int $orderId = null): bool
     {
         $log = new CouponLog();
         $log->coupon_id = $couponId;
@@ -177,7 +177,7 @@ class Helpers
      * @param  float|int  $amount  发生金额
      * @param  string|null  $description  描述
      */
-    public static function addUserCreditLog(int $userId, ?int $orderId, float|int $before, float|int $after, float|int $amount, string $description = null): bool
+    public static function addUserCreditLog(int $userId, ?int $orderId, float|int $before, float|int $after, float|int $amount, ?string $description = null): bool
     {
         $log = new UserCreditLog();
         $log->user_id = $userId;
@@ -186,7 +186,7 @@ class Helpers
         $log->after = $after;
         $log->amount = $amount;
         $log->description = $description;
-        $log->created_at = date('Y-m-d H:i:s');
+        $log->created_at = now();
 
         return $log->save();
     }
@@ -200,7 +200,7 @@ class Helpers
      * @param  string|null  $description  描述
      * @param  int|null  $orderId  订单ID
      */
-    public static function addUserTrafficModifyLog(int $userId, int $before, int $after, string $description = null, int $orderId = null): bool
+    public static function addUserTrafficModifyLog(int $userId, int $before, int $after, ?string $description = null, ?int $orderId = null): bool
     {
         $log = new UserDataModifyLog();
         $log->user_id = $userId;
@@ -222,7 +222,7 @@ class Helpers
      * @param  int  $status  状态
      * @param  string|null  $error  报错
      */
-    public static function addMarketing(string $receiver, int $type, string $title, string $content, int $status = 1, string $error = null): bool
+    public static function addMarketing(string $receiver, int $type, string $title, string $content, int $status = 1, ?string $error = null): bool
     {
         $marketing = new Marketing();
         $marketing->type = $type;

+ 1 - 1
app/Utils/Payments/F2Fpay.php

@@ -73,7 +73,7 @@ class F2Fpay extends PaymentService implements Gateway
         exit('fail');
     }
 
-    public function capture(string $trade_no = null, string $ali_trade_no = null): bool
+    public function capture(?string $trade_no = null, ?string $ali_trade_no = null): bool
     {
         $result = self::$aliClient->tradeQuery(array_filter([
             'out_trade_no' => $trade_no,

+ 6 - 17
app/helpers.php

@@ -1,5 +1,7 @@
 <?php
 
+use Carbon\CarbonInterval;
+
 const MiB = 1048576;
 const GiB = 1073741824;
 
@@ -27,7 +29,7 @@ if (! function_exists('base64url_decode')) {
 
 // 根据流量值自动转换单位输出
 if (! function_exists('formatBytes')) {
-    function formatBytes(int $bytes, string $base = null, int $precision = 2): string
+    function formatBytes(int $bytes, ?string $base = null, int $precision = 2): string
     {
         $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
         $bytes = max($bytes, 0);
@@ -47,28 +49,15 @@ if (! function_exists('formatBytes')) {
 if (! function_exists('formatTime')) {
     function formatTime(int $seconds): string
     {
-        $timeString = '';
-        $units = [
-            trans('validation.attributes.day') => 86400,
-            trans('validation.attributes.hour') => 3600,
-            trans('validation.attributes.minute') => 60,
-            trans('validation.attributes.second') => 1,
-        ];
-
-        foreach ($units as $unitName => $secondsInUnit) {
-            if ($seconds >= $secondsInUnit) {
-                $timeString .= floor($seconds / $secondsInUnit).' '.$unitName.' ';
-                $seconds %= $secondsInUnit;
-            }
-        }
+        $interval = CarbonInterval::seconds($seconds);
 
-        return trim($timeString);
+        return $interval->cascade()->forHumans();
     }
 }
 
 // 获取系统设置
 if (! function_exists('sysConfig')) {
-    function sysConfig(string $key = null, string $default = null): array|string|null
+    function sysConfig(?string $key = null, ?string $default = null): array|string|null
     {
         return $key ? config("settings.$key", $default) : config('settings');
     }

+ 1 - 1
install.sh

@@ -27,7 +27,7 @@ check_composer
 
 # 执行Composer安装
 echo -e "\e[34m========= Installing packages via Composer... | 通过Composer安装程序包... =========\e[0m"
-yes | composer install
+composer install --no-interaction --no-dev --optimize-autoloader
 
 # 执行Panel安装
 php artisan panel:install

+ 2 - 0
resources/lang/zh_CN.json

@@ -40,6 +40,8 @@
     "[Auto Task] Blocked service: Abnormal traffic within 1 hour": "[自动任务] 封禁服务:1小时内流量异常",
     "[Auto Task] Blocked service: Run out of traffic": "[自动任务] 封禁服务:流量耗尽",
     "[Auto Task] Blocked Subscription: Subscription with abnormal requests within 24 hours": "[自动任务] 封禁订阅:24小时内请求异常",
+    "[Auto Task] Unblocked Service: Account ban expired" : "[自动任务]解封服务: 封禁到期",
+    "[Auto Task] Unblocked Service: Account has available data traffic" : "[自动任务]解封服务: 出现可用流量",
     "[Daily Task] Account Expiration: Block Login & Clear Account": "[每日任务] 账号过期:禁止登录,清空账户",
     "[Daily Task] Account Expiration: Stop Service": "[每日任务] 账号过期:停止服务",
     "[Daily Task] Reset Account Traffic, Next Reset Date: :date": "[每日任务] 重置账号流量,下次重置日::date",

+ 1 - 1
resources/views/_layout.blade.php

@@ -14,7 +14,7 @@
           content="An account management Panel based on Laravel7 framework. Include multiple payment, account management, system caching, admin notification, products models, and more.">
     <meta name="keywords" content="ProxyPanel Laravel Shadowsocks ShadowsocksR V2Ray Trojan VNET VPN">
     <meta name="author" content="ZBrettonYe">
-    <meta name="copyright" content="2017-2023©ProxyPanel">
+    <meta name="copyright" content="2017-2024©ProxyPanel">
     <title>@yield('title')</title>
     <link href="{{asset('favicon.ico')}}" rel="shortcut icon apple-touch-icon">
     <!-- 样式表/Stylesheets -->

+ 1 - 3
resources/views/admin/node/auth.blade.php

@@ -23,7 +23,6 @@
                         <th> {{ trans('model.common.type') }}</th>
                         <th> {{ trans('model.node.name') }}</th>
                         <th> {{ trans('model.node.domain') }}</th>
-                        <th> {{ trans('model.node.ipv4') }}</th>
                         <th> {!! trans('model.node_auth.key') !!}</th>
                         <th> {{ trans('model.node_auth.secret') }}</th>
                         <th> {{trans('common.action')}}</th>
@@ -35,8 +34,7 @@
                             <td> {{$auth->node_id}} </td>
                             <td> {{$auth->node->type_label}} </td>
                             <td> {{Str::limit($auth->node->name, 20)}} </td>
-                            <td> {{$auth->node->server}} </td>
-                            <td> {{$auth->node->ip}} </td>
+                            <td> {{$auth->node->server ?? $auth->node->ip ?? $auth->node->ipv6}} </td>
                             <td><span class="badge badge-lg badge-info"> {{$auth->key}} </span></td>
                             <td><span class="badge badge-lg badge-info"> {{$auth->secret}} </span></td>
                             <td>

+ 7 - 1
resources/views/admin/node/index.blade.php

@@ -71,7 +71,7 @@
                             <td>
                                 @isset($node->profile['passwd'])
                                     {{-- 单端口 --}}
-                                    <span class="badge badge-lg badge-info"><i class="fa-solid fa-arrows-left-right-to-line" aria-hidden="true"></i></span>
+                                    <span class="badge badge-lg badge-info"><i class="fa-solid fa-1" aria-hidden="true"></i></span>
                                 @endisset
                                 @if($node->is_display === 0)
                                     {{-- 节点完全不可见 --}}
@@ -83,6 +83,12 @@
                                     {{-- 节点只可被订阅到 --}}
                                     <span class="badge badge-lg badge-danger"><i class="fa-solid fa-store-slash" aria-hidden="true"></i></span>
                                 @endif
+                                @if($node->ip)
+                                    <span class="badge badge-md badge-info"><i class="fa-solid fa-4" aria-hidden="true"></i></span>
+                                @endif
+                                @if($node->ipv6)
+                                    <span class="badge badge-md badge-info"><i class="fa-solid fa-6" aria-hidden="true"></i></span>
+                                @endif
                             </td>
                             <td>
                                 @if($node->isOnline)

+ 1 - 1
resources/views/user/layouts.blade.php

@@ -195,7 +195,7 @@
     </div>
     <footer class="site-footer">
         <div class="site-footer-legal">
-            © 2017 - 2023 <a href="https://github.com/ProxyPanel/ProxyPanel" target="_blank">{{config('version.name')}} {{__('All rights reserved.')}}</a>
+            © 2017 - 2024 <a href="https://github.com/ProxyPanel/ProxyPanel" target="_blank">{{config('version.name')}} {{__('All rights reserved.')}}</a>
             🚀 Version: <code> {{config('version.number')}} </code>
         </div>
         <div class="site-footer-right">

+ 10 - 0
scripts/download_utils.sh

@@ -56,6 +56,16 @@ download_file() {
       return 1
     fi
 
+    # 验证文件完整性
+    local actual_size=$(du -b "$tmp_file" | awk '{print $1}')
+    local min_size=$((1048576)) # 1MB minimum file size
+
+    if [ "$actual_size" -lt "$min_size" ]; then
+      echo -e "\e[31m[取消]下载的文件大小小于1MB,文件可能不完整\e[0m"
+      rm -f "$tmp_file"
+      return 1
+    fi
+
     # 下载成功,重命名文件
     mv "$tmp_file" "$FILE_DIR/$name"
     echo -e "\e[32m成功更新 $name 到版本 $version\e[0m"

+ 1 - 1
scripts/lib.sh

@@ -70,7 +70,7 @@ clean_files() {
 # 检查软件是否安装
 check_available() {
   tools=$1
-  if command -v $tools >/dev/null 2>&1; then
+  if command -v "$tools" >/dev/null 2>&1; then
     echo -e "\e[32m$tools Installed! | $tools 已安装!\e[0m"
   else
     echo -e "\e[31m$tools did not installed! | $tools 未安装!\e[0m"

+ 1 - 1
update.sh

@@ -19,7 +19,7 @@ php artisan optimize:clear
 
 # 执行Composer更新
 echo -e "\e[34m========= Updating packages via Composer... | 通过Composer更新程序包... =========\e[0m"
-yes | composer update
+composer update --no-interaction --no-dev --optimize-autoloader
 
 # 执行Panel更新
 php artisan panel:update