瀏覽代碼

🚀 Refactor Blade

- Optimize Blade JavaScript code.
- Refactored multiple admin controllers for improved validation, error handling, and query efficiency.
- Added ProxyConfig trait to centralize proxy configuration options.
- Updated NodeStatusDetection to use model relationships for heartbeat checks.
- Improved category, label, and country management logic and error logging.
- Added new Blade components for admin UI and updated language files and assets for better localization and frontend support.
- Bug fixed & introduced more bug :)
BrettonYe 1 月之前
父節點
當前提交
448ffff623
共有 100 個文件被更改,包括 991 次插入570 次删除
  1. 22 26
      app/Channels/Library/WeChat.php
  2. 4 7
      app/Console/Commands/NodeStatusDetection.php
  3. 26 0
      app/Helpers/ProxyConfig.php
  4. 2 3
      app/Http/Controllers/Admin/AffiliateController.php
  5. 3 8
      app/Http/Controllers/Admin/ArticleController.php
  6. 1 13
      app/Http/Controllers/Admin/CertController.php
  7. 17 7
      app/Http/Controllers/Admin/Config/CategoryController.php
  8. 8 2
      app/Http/Controllers/Admin/Config/CountryController.php
  9. 1 1
      app/Http/Controllers/Admin/Config/EmailFilterController.php
  10. 24 6
      app/Http/Controllers/Admin/Config/LabelController.php
  11. 16 4
      app/Http/Controllers/Admin/Config/LevelController.php
  12. 21 4
      app/Http/Controllers/Admin/Config/SsConfigController.php
  13. 4 11
      app/Http/Controllers/Admin/CouponController.php
  14. 20 11
      app/Http/Controllers/Admin/InviteController.php
  15. 8 6
      app/Http/Controllers/Admin/LogsController.php
  16. 6 6
      app/Http/Controllers/Admin/MarketingController.php
  17. 20 15
      app/Http/Controllers/Admin/NodeAuthController.php
  18. 36 27
      app/Http/Controllers/Admin/NodeController.php
  19. 15 7
      app/Http/Controllers/Admin/PermissionController.php
  20. 46 10
      app/Http/Controllers/Admin/RoleController.php
  21. 18 6
      app/Http/Controllers/Admin/RuleController.php
  22. 28 9
      app/Http/Controllers/Admin/RuleGroupController.php
  23. 24 7
      app/Http/Controllers/Admin/ShopController.php
  24. 16 2
      app/Http/Controllers/Admin/SubscribeController.php
  25. 50 42
      app/Http/Controllers/Admin/SystemController.php
  26. 17 5
      app/Http/Controllers/Admin/TicketController.php
  27. 41 28
      app/Http/Controllers/Admin/ToolsController.php
  28. 41 25
      app/Http/Controllers/Admin/UserController.php
  29. 6 1
      app/Http/Controllers/Admin/UserGroupController.php
  30. 8 13
      app/Http/Controllers/AdminController.php
  31. 2 3
      app/Http/Controllers/Api/Client/ClientController.php
  32. 11 19
      app/Http/Controllers/AuthController.php
  33. 55 1
      app/Http/Controllers/OAuthController.php
  34. 4 2
      app/Http/Controllers/User/ArticleController.php
  35. 16 7
      app/Http/Controllers/User/InviteController.php
  36. 13 12
      app/Http/Controllers/User/NodeController.php
  37. 24 20
      app/Http/Controllers/User/ShopController.php
  38. 40 22
      app/Http/Controllers/User/SubscribeController.php
  39. 50 24
      app/Http/Controllers/User/TicketController.php
  40. 4 4
      app/Http/Controllers/UserController.php
  41. 1 1
      app/Http/Requests/Admin/PermissionRequest.php
  42. 1 1
      app/Http/Requests/Admin/UserGroupRequest.php
  43. 1 1
      app/Http/Requests/Admin/UserStoreRequest.php
  44. 1 1
      app/Http/Requests/Admin/UserUpdateRequest.php
  45. 2 0
      app/Models/CouponLog.php
  46. 5 0
      app/Models/Goods.php
  47. 20 0
      app/Models/Node.php
  48. 53 0
      app/Models/NodeCertificate.php
  49. 0 6
      app/Models/NodeHeartbeat.php
  50. 3 1
      app/Models/NotificationLog.php
  51. 5 0
      app/Models/User.php
  52. 2 2
      app/Services/OrderService.php
  53. 2 1
      app/Services/ProxyService.php
  54. 35 70
      app/Utils/Helpers.php
  55. 1 1
      app/Utils/Payments/PaymentManager.php
  56. 0 23
      app/View/Components/Alert.php
  57. 29 4
      app/helpers.php
  58. 1 3
      composer.json
  59. 18 26
      config/common.php
  60. 0 0
      public/assets/bundle/app.min.css
  61. 3 1
      public/assets/global/js/Plugin/bootstrap-datepicker.js
  62. 2 2
      public/assets/global/vendor/bootstrap-datepicker/bootstrap-datepicker.min.js
  63. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker-en-CA.min.js
  64. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.ar-DZ.min.js
  65. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.ar-tn.min.js
  66. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.ar.min.js
  67. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.az.min.js
  68. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.bg.min.js
  69. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.bm.min.js
  70. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.bn.min.js
  71. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.br.min.js
  72. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.bs.min.js
  73. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.ca.min.js
  74. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.cs.min.js
  75. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.cy.min.js
  76. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.da.min.js
  77. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.de.min.js
  78. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.el.min.js
  79. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.en-AU.min.js
  80. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.en-CA.min.js
  81. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.en-GB.min.js
  82. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.en-IE.min.js
  83. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.en-NZ.min.js
  84. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.en-US.min.js
  85. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.en-ZA.min.js
  86. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.eo.min.js
  87. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.es.min.js
  88. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.et.min.js
  89. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.eu.min.js
  90. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.fa.min.js
  91. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.fi.min.js
  92. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.fo.min.js
  93. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.fr-CH.min.js
  94. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.fr.min.js
  95. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.gl.min.js
  96. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.he.min.js
  97. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.hi.min.js
  98. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.hr.min.js
  99. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.hu.min.js
  100. 1 0
      public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.hy.min.js

+ 22 - 26
app/Channels/Library/WeChat.php

@@ -20,8 +20,8 @@ class WeChat
     }
 
     public function encryptMsg(string $sReplyMsg, ?int $sTimeStamp, string $sNonce, string &$sEncryptMsg): int
-    { //将公众平台回复用户的消息加密打包.
-        $array = $this->prpcrypt_encrypt($sReplyMsg); //加密
+    { // 将公众平台回复用户的消息加密打包.
+        $array = $this->prpcrypt_encrypt($sReplyMsg); // 加密
 
         if ($array[0] !== 0) {
             return $array[0];
@@ -44,11 +44,11 @@ class WeChat
     public function prpcrypt_encrypt(string $data): array
     {
         try {
-            //拼接
+            // 拼接
             $data = Str::random().pack('N', strlen($data)).$data.sysConfig('wechat_cid');
-            //添加PKCS#7填充
+            // 添加PKCS#7填充
             $data = $this->pkcs7_encode($data);
-            //加密
+            // 加密
             $encrypted = openssl_encrypt($data, 'AES-256-CBC', $this->key, OPENSSL_ZERO_PADDING, $this->iv);
 
             return [0, $encrypted];
@@ -61,7 +61,7 @@ class WeChat
 
     public function pkcs7_encode(string $data): string
     {// 对需要加密的明文进行填充补位
-        //计算需要填充的位数
+        // 计算需要填充的位数
         $padding = 32 - (strlen($data) % 32);
         $padding = ($padding === 0) ? 32 : $padding;
         $pattern = chr($padding);
@@ -102,15 +102,13 @@ XML;
 
     public function decryptMsg(string $sMsgSignature, ?int $sTimeStamp, string $sNonce, string $sPostData, string &$sMsg)
     { // 检验消息的真实性,并且获取解密后的明文.
-        //提取密文
-        $array = $this->extract($sPostData);
-
-        if ($array[0] !== 0) {
-            return $array[0];
+        // 提取密文
+        [$code, $encrypt] = $this->extract($sPostData);
+        if ($code !== 0) {
+            return $code;
         }
 
         $sTimeStamp = $sTimeStamp ?? time();
-        $encrypt = $array[1];
 
         $this->verifySignature($sMsgSignature, $sTimeStamp, $sNonce, $encrypt, $sMsg); // 验证安全签名
     }
@@ -138,23 +136,19 @@ XML;
 
     public function verifySignature(string $sMsgSignature, string $sTimeStamp, string $sNonce, string $sEcho, string &$sMsg): int
     { // 验证URL
-        //verify msg_signature
-        $array = $this->extract($sEcho);
+        // verify msg_signature
+        [$code, $encrypt] = $this->extract($sEcho);
 
-        if ($array[0] !== 0) {
-            return $array[0];
+        if ($code !== 0) {
+            return $code;
         }
 
-        $encrypt = $array[1];
+        [$code, $signature] = $this->getSHA1($sTimeStamp, $sNonce, $encrypt);
 
-        $array = $this->getSHA1($sTimeStamp, $sNonce, $encrypt);
-
-        if ($array[0] !== 0) {
-            return $array[0];
+        if ($code !== 0) {
+            return $code;
         }
 
-        $signature = $array[1];
-
         if ($sMsgSignature !== $signature) {
             Log::critical(trans('notification.error', ['channel' => trans('admin.system.notification.channel.wechat'), 'reason' => trans('notification.sign_failed')]));
 
@@ -162,12 +156,14 @@ XML;
         }
 
         $sMsg = $encrypt;
+
+        return 0;
     }
 
     public function prpcrypt_decrypt(string $encrypted): array
     {
         try {
-            //解密
+            // 解密
             $decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', $this->key, OPENSSL_ZERO_PADDING, $this->iv);
         } catch (Exception $e) {
             Log::critical(trans('notification.error', ['channel' => trans('admin.system.notification.channel.wechat'), 'reason' => var_export($e->getMessage(), true)]));
@@ -175,12 +171,12 @@ XML;
             return [-40007, null]; // DecryptAESError
         }
         try {
-            //删除PKCS#7填充
+            // 删除PKCS#7填充
             $result = $this->pkcs7_decode($decrypted);
             if (strlen($result) < 16) {
                 return [];
             }
-            //拆分
+            // 拆分
             $content = substr($result, 16, strlen($result));
             $len_list = unpack('N', substr($content, 0, 4));
             $xml_len = $len_list[1];

+ 4 - 7
app/Console/Commands/NodeStatusDetection.php

@@ -3,7 +3,6 @@
 namespace App\Console\Commands;
 
 use App\Models\Node;
-use App\Models\NodeHeartbeat;
 use App\Models\User;
 use App\Notifications\NodeBlocked;
 use App\Notifications\NodeOffline;
@@ -44,10 +43,9 @@ class NodeStatusDetection extends Command
     private function checkNodeStatus(): void
     {
         $offlineCheckTimes = sysConfig('offline_check_times');
-        $onlineNode = NodeHeartbeat::recently()->distinct()->pluck('node_id');
 
         $data = [];
-        foreach (Node::whereRelayNodeId(null)->whereStatus(1)->whereNotIn('id', $onlineNode)->get() as $node) {
+        foreach (Node::whereRelayNodeId(null)->whereStatus(1)->whereDoesntHave('latestHeartbeat')->get() as $node) {
             // 近期无节点负载信息则认为是后端炸了
             if ($offlineCheckTimes > 0) {
                 $times = $this->updateCache('offline_check_times'.$node->id, 24);
@@ -90,11 +88,11 @@ class NodeStatusDetection extends Command
                     $status = (new NetworkDetection)->networkStatus($ip, $node->port ?? 22);
 
                     if ($node->detection_type !== 1 && $status['icmp'] !== 1) {
-                        $data[$node_id][$ip]['icmp'] = config('common.network_status')[$status['icmp']];
+                        $data[$node_id][$ip]['icmp'] = trans("admin.network_status.{$status['icmp']}");
                     }
 
                     if ($node->detection_type !== 2 && $status['tcp'] !== 1) {
-                        $data[$node_id][$ip]['tcp'] = config('common.network_status')[$status['tcp']];
+                        $data[$node_id][$ip]['tcp'] = trans("admin.network_status.{$status['tcp']}");
                     }
 
                     sleep(2);
@@ -132,8 +130,7 @@ class NodeStatusDetection extends Command
 
     private function reliveNode(): void
     {
-        $onlineNode = NodeHeartbeat::recently()->distinct()->pluck('node_id');
-        foreach (Node::whereRelayNodeId(null)->whereStatus(0)->whereIn('id', $onlineNode)->where('detection_type', '<>', 0)->get() as $node) {
+        foreach (Node::whereRelayNodeId(null)->whereStatus(0)->where('detection_type', '<>', 0)->whereHas('latestHeartbeat')->get() as $node) {
             $ips = $node->ips();
             $reachableIPs = 0;
 

+ 26 - 0
app/Helpers/ProxyConfig.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace App\Helpers;
+
+use App\Models\SsConfig;
+
+trait ProxyConfig
+{
+    private function proxyConfigOptions(): array
+    {
+        // 一次性获取所有配置数据
+        $configs = SsConfig::get(['name', 'type'])->groupBy('type')->map(fn ($items) => $items->pluck('name', 'name'));
+
+        // 获取默认配置项
+        $defaults = SsConfig::where('is_default', 1)->pluck('name', 'type');
+
+        return [
+            'methods' => $configs->get(1, []),
+            'protocols' => $configs->get(2, []),
+            'obfs' => $configs->get(3, []),
+            'methodDefault' => $defaults->get(1),
+            'protocolDefault' => $defaults->get(2),
+            'obfsDefault' => $defaults->get(3),
+        ];
+    }
+}

+ 2 - 3
app/Http/Controllers/Admin/AffiliateController.php

@@ -14,6 +14,7 @@ class AffiliateController extends Controller
     public function index(Request $request): View
     { // 提现申请列表
         $query = ReferralApply::with('user:id,username');
+
         $request->whenFilled('username', function ($username) use ($query) {
             $query->whereHas('user', function ($query) use ($username) {
                 $query->where('username', 'like', "%$username%");
@@ -42,9 +43,7 @@ class AffiliateController extends Controller
         if ($aff->update(['status' => $status])) {
             // 将关联的返现单更新状态
             if ($status === 1 || $status === 2) {
-                if ($aff->referral_logs()->update(['status' => $status])) {
-                    return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.action')])]);
-                }
+                $aff->referral_logs()->update(['status' => $status]);
             }
 
             return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.action')])]);

+ 3 - 8
app/Http/Controllers/Admin/ArticleController.php

@@ -19,7 +19,6 @@ class ArticleController extends Controller
 {
     public function index(Request $request): View
     { // 文章列表
-        $categories = Article::whereNotNull('category')->distinct()->get('category');
         $articles = Article::query();
 
         foreach (['id', 'category', 'language', 'type'] as $field) {
@@ -28,9 +27,7 @@ class ArticleController extends Controller
             });
         }
 
-        $articles = $articles->latest()->orderByDesc('sort')->paginate()->appends($request->except('page'));
-
-        return view('admin.article.index', compact('articles', 'categories'));
+        return view('admin.article.index', ['articles' => $articles->latest()->orderByDesc('sort')->paginate()->appends($request->except('page')), 'categories' => Article::whereNotNull('category')->distinct()->pluck('category', 'category')]);
     }
 
     public function store(ArticleRequest $request): RedirectResponse
@@ -67,9 +64,7 @@ class ArticleController extends Controller
 
     public function create(): View
     { // 添加文章页面
-        $categories = Article::whereNotNull('category')->distinct()->get('category');
-
-        return view('admin.article.info', compact('categories'));
+        return view('admin.article.info', ['categories' => Article::whereNotNull('category')->distinct()->pluck('category')]);
     }
 
     public function show(Article $article): View
@@ -81,7 +76,7 @@ class ArticleController extends Controller
 
     public function edit(Article $article): View
     { // 编辑文章页面
-        $categories = Article::whereNotNull('category')->distinct()->get('category');
+        $categories = Article::whereNotNull('category')->distinct()->pluck('category');
 
         return view('admin.article.info', compact('article', 'categories'));
     }

+ 1 - 13
app/Http/Controllers/Admin/CertController.php

@@ -15,19 +15,7 @@ class CertController extends Controller
 {
     public function index(): View
     {
-        $certs = NodeCertificate::orderBy('id')->paginate()->appends(request('page'));
-        foreach ($certs as $cert) {
-            if ($cert->pem) {
-                $certInfo = openssl_x509_parse($cert->pem);
-                if ($certInfo) {
-                    $cert->issuer = $certInfo['issuer']['O'] ?? null;
-                    $cert->from = date('Y-m-d', $certInfo['validFrom_time_t']) ?: null;
-                    $cert->to = date('Y-m-d', $certInfo['validTo_time_t']) ?: null;
-                }
-            }
-        }
-
-        return view('admin.node.cert.index', ['certs' => $certs]);
+        return view('admin.node.cert.index', ['certs' => NodeCertificate::orderBy('id')->paginate()->appends(request('page'))]);
     }
 
     public function store(CertRequest $request): RedirectResponse

+ 17 - 7
app/Http/Controllers/Admin/Config/CategoryController.php

@@ -13,14 +13,23 @@ use Validator;
 class CategoryController extends Controller
 {
     public function store(Request $request): JsonResponse
-    { // 添加等级
-        $validator = Validator::make($request->all(), ['name' => 'required']);
+    { // 添加分类
+        $validator = Validator::make($request->all(), [
+            'name' => 'required',
+            'sort' => 'nullable|numeric',
+        ]);
 
         if ($validator->fails()) {
             return response()->json(['status' => 'fail', 'message' => $validator->errors()->all()]);
         }
 
-        if (GoodsCategory::create($validator->validated())) {
+        $data = $validator->validated();
+        // 如果没有提供sort值,则设为0
+        if (! isset($data['sort'])) {
+            $data['sort'] = 0;
+        }
+
+        if (GoodsCategory::create($data)) {
             return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.add')])]);
         }
 
@@ -28,7 +37,7 @@ class CategoryController extends Controller
     }
 
     public function update(Request $request, GoodsCategory $category): JsonResponse
-    { // 编辑等级
+    { // 编辑分类
         $validator = Validator::make($request->all(), [
             'name' => 'required',
             'sort' => 'required|numeric',
@@ -37,6 +46,7 @@ class CategoryController extends Controller
         if ($validator->fails()) {
             return response()->json(['status' => 'fail', 'message' => $validator->errors()->all()]);
         }
+
         if ($category->update($validator->validated())) {
             return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.edit')])]);
         }
@@ -45,8 +55,8 @@ class CategoryController extends Controller
     }
 
     public function destroy(GoodsCategory $category): JsonResponse
-    { // 删除等级
-        // 校验该等级下是否存在关联账号
+    { // 删除分类
+        // 校验该分类下是否存在关联商品
         if ($category->goods()->exists()) {
             return response()->json(['status' => 'fail', 'message' => trans('common.exists_error', ['attribute' => trans('model.goods.category')])]);
         }
@@ -56,7 +66,7 @@ class CategoryController extends Controller
                 return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.delete')])]);
             }
         } catch (Exception $e) {
-            Log::error(trans('common.error_action_item', ['action' => trans('common.delete'), 'attribute' => trans('model.common.level')]).': '.$e->getMessage());
+            Log::error(trans('common.error_action_item', ['action' => trans('common.delete'), 'attribute' => trans('model.goods.category')]).': '.$e->getMessage());
 
             return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.delete')]).', '.$e->getMessage()]);
         }

+ 8 - 2
app/Http/Controllers/Admin/Config/CountryController.php

@@ -23,8 +23,14 @@ class CountryController extends Controller
             return response()->json(['status' => 'fail', 'message' => $validator->errors()->all()]);
         }
 
-        if (Country::create($validator->validated())) {
-            return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.add')])]);
+        try {
+            if (Country::create($validator->validated())) {
+                return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.add')])]);
+            }
+        } catch (Exception $e) {
+            Log::error(trans('common.error_action_item', ['action' => trans('common.add'), 'attribute' => trans('model.node.country')]).': '.$e->getMessage());
+
+            return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.add')]).', '.$e->getMessage()]);
         }
 
         return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.add')])]);

+ 1 - 1
app/Http/Controllers/Admin/Config/EmailFilterController.php

@@ -15,7 +15,7 @@ class EmailFilterController extends Controller
 {
     public function index(): View
     { // 邮箱过滤列表
-        return view('admin.config.emailFilter', ['filters' => EmailFilter::orderByDesc('id')->paginate()]);
+        return view('admin.config.emailFilter', ['filters' => EmailFilter::select(['id', 'type', 'words'])->orderByDesc('id')->paginate()]);
     }
 
     public function store(Request $request): JsonResponse

+ 24 - 6
app/Http/Controllers/Admin/Config/LabelController.php

@@ -23,8 +23,14 @@ class LabelController extends Controller
             return response()->json(['status' => 'fail', 'message' => $validator->errors()->all()]);
         }
 
-        if (Label::create($validator->validated())) {
-            return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.add')])]);
+        try {
+            if (Label::create($validator->validated())) {
+                return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.add')])]);
+            }
+        } catch (Exception $e) {
+            Log::error(trans('common.error_action_item', ['action' => trans('common.add'), 'attribute' => trans('model.node.label')]).': '.$e->getMessage());
+
+            return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.add')]).', '.$e->getMessage()]);
         }
 
         return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.add')])]);
@@ -41,8 +47,14 @@ class LabelController extends Controller
             return response()->json(['status' => 'fail', 'message' => $validator->errors()->all()]);
         }
 
-        if ($label->update($validator->validated())) {
-            return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.edit')])]);
+        try {
+            if ($label->update($validator->validated())) {
+                return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.edit')])]);
+            }
+        } catch (Exception $e) {
+            Log::error(trans('common.error_action_item', ['action' => trans('common.edit'), 'attribute' => trans('model.node.label')]).': '.$e->getMessage());
+
+            return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.edit')]).', '.$e->getMessage()]);
         }
 
         return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.edit')])]);
@@ -51,13 +63,19 @@ class LabelController extends Controller
     public function destroy(Label $label): JsonResponse
     { // 删除标签
         try {
-            $label->delete();
+            // 先从所有节点中移除该标签
+            $label->nodes()->detach();
 
-            return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.delete')])]);
+            // 然后删除标签
+            if ($label->delete()) {
+                return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.delete')])]);
+            }
         } catch (Exception $e) {
             Log::error(trans('common.error_action_item', ['action' => trans('common.delete'), 'attribute' => trans('model.node.label')]).': '.$e->getMessage());
 
             return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.delete')]).', '.$e->getMessage()]);
         }
+
+        return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.delete')])]);
     }
 }

+ 16 - 4
app/Http/Controllers/Admin/Config/LevelController.php

@@ -23,8 +23,14 @@ class LevelController extends Controller
             return response()->json(['status' => 'fail', 'message' => $validator->errors()->all()]);
         }
 
-        if (Level::create($validator->validated())) {
-            return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.add')])]);
+        try {
+            if (Level::create($validator->validated())) {
+                return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.add')])]);
+            }
+        } catch (Exception $e) {
+            Log::error(trans('common.error_action_item', ['action' => trans('common.add'), 'attribute' => trans('model.common.level')]).': '.$e->getMessage());
+
+            return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.add')]).', '.$e->getMessage()]);
         }
 
         return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.add')])]);
@@ -41,8 +47,14 @@ class LevelController extends Controller
             return response()->json(['status' => 'fail', 'message' => $validator->errors()->all()]);
         }
 
-        if ($level->update($validator->validated())) {
-            return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.edit')])]);
+        try {
+            if ($level->update($validator->validated())) {
+                return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.edit')])]);
+            }
+        } catch (Exception $e) {
+            Log::error(trans('common.error_action_item', ['action' => trans('common.edit'), 'attribute' => trans('model.common.level')]).': '.$e->getMessage());
+
+            return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.edit')]).', '.$e->getMessage()]);
         }
 
         return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.edit')])]);

+ 21 - 4
app/Http/Controllers/Admin/Config/SsConfigController.php

@@ -23,8 +23,14 @@ class SsConfigController extends Controller
             return response()->json(['status' => 'fail', 'message' => $validator->errors()->all()]);
         }
 
-        if (SsConfig::create($validator->validated())) {
-            return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.add')])]);
+        try {
+            if (SsConfig::create($validator->validated())) {
+                return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.add')])]);
+            }
+        } catch (Exception $e) {
+            Log::error(trans('common.error_action_item', ['action' => trans('common.add'), 'attribute' => trans('user.node.info')]).': '.$e->getMessage());
+
+            return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.add')]).', '.$e->getMessage()]);
         }
 
         return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.add')])]);
@@ -32,8 +38,14 @@ class SsConfigController extends Controller
 
     public function update(SsConfig $ss): JsonResponse
     { // 设置SS默认配置
-        if ($ss->setDefault()) {
-            return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.edit')])]);
+        try {
+            if ($ss->setDefault()) {
+                return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.edit')])]);
+            }
+        } catch (Exception $e) {
+            Log::error(trans('common.error_action_item', ['action' => trans('common.edit'), 'attribute' => trans('user.node.info')]).': '.$e->getMessage());
+
+            return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.edit')]).', '.$e->getMessage()]);
         }
 
         return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.edit')])]);
@@ -41,6 +53,11 @@ class SsConfigController extends Controller
 
     public function destroy(SsConfig $ss): JsonResponse
     { // 删除SS配置
+        // 检查是否为默认配置
+        if ($ss->is_default) {
+            return response()->json(['status' => 'fail', 'message' => trans('admin.setting.common.config_default_cannot_delete')]);
+        }
+
         try {
             if ($ss->delete()) {
                 return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.delete')])]);

+ 4 - 11
app/Http/Controllers/Admin/CouponController.php

@@ -38,12 +38,8 @@ class CouponController extends Controller
     }
 
     public function show(Coupon $coupon): View
-    { // 优惠券列表
-        return view('admin.coupon.show', [
-            'coupon' => $coupon,
-            'userGroups' => UserGroup::all()->pluck('name', 'id')->toArray(),
-            'levels' => Level::all()->pluck('name', 'level')->toArray(),
-        ]);
+    { // 优惠券详情
+        return view('admin.coupon.show', ['coupon' => $coupon, 'userGroups' => UserGroup::pluck('name', 'id'), 'levels' => Level::pluck('name', 'level')]);
     }
 
     public function store(CouponRequest $request): RedirectResponse
@@ -94,16 +90,13 @@ class CouponController extends Controller
         } catch (Exception $e) {
             Log::error(trans('common.error_action_item', ['action' => trans('common.generate'), 'attribute' => trans('model.coupon.attribute')]).': '.$e->getMessage());
 
-            return redirect()->back()->withInput()->withInput()->withErrors(trans('common.failed_item', ['attribute' => trans('common.generate')]).', '.$e->getMessage());
+            return redirect()->back()->withInput()->withErrors(trans('common.failed_item', ['attribute' => trans('common.generate')]).', '.$e->getMessage());
         }
     }
 
     public function create(): View
     { // 添加优惠券页面
-        return view('admin.coupon.create', [
-            'userGroups' => UserGroup::all()->pluck('name', 'id')->toArray(),
-            'levels' => Level::all()->pluck('name', 'level')->toArray(),
-        ]);
+        return view('admin.coupon.create', ['userGroups' => UserGroup::pluck('name', 'id'), 'levels' => Level::pluck('name', 'level')]);
     }
 
     public function destroy(Coupon $coupon): JsonResponse

+ 20 - 11
app/Http/Controllers/Admin/InviteController.php

@@ -16,30 +16,39 @@ class InviteController extends Controller
 {
     public function index(): View
     { // 邀请码列表
-        return view('admin.aff.invite', [
-            'inviteList' => Invite::with(['invitee:id,username', 'inviter:id,username'])->orderBy('status')->orderByDesc('id')->paginate(15)->appends(request('page')),
-        ]);
+        return view('admin.aff.invite', ['inviteList' => Invite::with(['invitee:id,username', 'inviter:id,username'])->orderBy('status')->orderByDesc('id')->paginate(15)->appends(request('page'))]);
     }
 
     public function generate(): JsonResponse
     { // 生成邀请码
+        $invites = [];
+        $expirationDate = date('Y-m-d H:i:s', strtotime(sysConfig('admin_invite_days').' days'));
+
         for ($i = 0; $i < 10; $i++) {
-            $obj = new Invite;
-            $obj->code = strtoupper(substr(md5(microtime().Str::random(6)), 8, 12));
-            $obj->dateline = date('Y-m-d H:i:s', strtotime(sysConfig('admin_invite_days').' days'));
-            $obj->save();
+            $invites[] = [
+                'code' => strtoupper(substr(md5(microtime().Str::random(6)), 8, 12)),
+                'dateline' => $expirationDate,
+            ];
         }
 
+        Invite::insert($invites);
+
         return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.generate')])]);
     }
 
     public function export(): void
     { // 导出邀请码
-        $inviteList = Invite::whereStatus(0)->orderBy('id')->get();
+        $inviteList = Invite::whereStatus(0)->select(['code', 'dateline'])->get();
+
         $filename = trans('user.invite.attribute').'_'.date('Ymd').'.xlsx';
 
         $spreadsheet = new Spreadsheet;
-        $spreadsheet->getProperties()->setCreator('ProxyPanel')->setLastModifiedBy('ProxyPanel')->setTitle(trans('user.invite.attribute'))->setSubject(trans('user.invite.attribute'));
+        $spreadsheet->getProperties()
+            ->setCreator('ProxyPanel')
+            ->setLastModifiedBy('ProxyPanel')
+            ->setTitle(trans('user.invite.attribute'))
+            ->setSubject(trans('user.invite.attribute'));
+
         $spreadsheet->setActiveSheetIndex(0);
         $sheet = $spreadsheet->getActiveSheet();
         $sheet->setTitle(trans('user.invite.attribute'));
@@ -49,10 +58,10 @@ class InviteController extends Controller
             $sheet->fromArray([$vo->code, $vo->dateline], null, 'A'.($k + 2));
         }
 
-        header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); // 输出07Excel文件
-        // header('Content-Type:application/vnd.ms-excel'); // 输出Excel03版本文件
+        header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
         header('Content-Disposition: attachment;filename="'.$filename.'"');
         header('Cache-Control: max-age=0');
+
         try {
             $writer = new Xlsx($spreadsheet);
             $writer->save('php://output');

+ 8 - 6
app/Http/Controllers/Admin/LogsController.php

@@ -50,7 +50,7 @@ class LogsController extends Controller
             if ($value) {
                 $query->where('coupon_id', '<>', null);
             } else {
-                $query->where('coupon_id', 'null');
+                $query->where('coupon_id', null);
             }
         });
 
@@ -85,7 +85,7 @@ class LogsController extends Controller
 
     public function trafficLog(Request $request): View
     { // 流量日志
-        $query = UserDataFlowLog::with(['user', 'node']);
+        $query = UserDataFlowLog::with(['user:id,username,port', 'node:id,name']);
 
         $request->whenFilled('port', function ($value) use ($query) {
             $query->whereHas('user', function ($query) use ($value) {
@@ -120,7 +120,7 @@ class LogsController extends Controller
             $log->d = formatBytes($log->d);
             $log->log_time = date('Y-m-d H:i:s', $log->log_time);
         }
-        $nodes = Node::whereStatus(1)->orderByDesc('sort')->latest()->get();
+        $nodes = Node::whereStatus(1)->orderByDesc('sort')->latest()->pluck('name', 'id');
 
         return view('admin.logs.traffic', compact(['totalTraffic', 'dataFlowLogs', 'nodes']));
     }
@@ -142,7 +142,7 @@ class LogsController extends Controller
 
     public function onlineIPMonitor(Request $request, ?int $id = null): View
     { // 在线IP监控(实时)
-        $query = NodeOnlineIp::with(['node:id,name', 'user:id,username'])->where('created_at', '>=', strtotime('-2 minutes'));
+        $query = NodeOnlineIp::with(['node:id,name', 'user:id,username,port'])->where('created_at', '>=', strtotime('-2 minutes'));
 
         if ($id !== null) {
             $query->whereHas('user', static function ($query) use ($id) {
@@ -183,7 +183,7 @@ class LogsController extends Controller
 
         return view('admin.logs.onlineIPMonitor', [
             'onlineIPLogs' => $onlineIPLogs,
-            'nodes' => Node::whereStatus(1)->orderByDesc('sort')->latest()->get(),
+            'nodes' => Node::whereStatus(1)->orderByDesc('sort')->latest()->pluck('name', 'id'),
         ]);
     }
 
@@ -244,7 +244,9 @@ class LogsController extends Controller
 
         $userList = $query->orderBy('id')->paginate(15)->appends($request->except('page'));
 
-        $nodeOnlineIPs = NodeOnlineIp::with('node:id,name')->where('created_at', '>=', strtotime('-10 minutes'))->latest()->distinct()->get();
+        // 获取最近10分钟的在线IP记录
+        $nodeOnlineIPs = NodeOnlineIp::with('node:id,name')->where('created_at', '>=', strtotime('-10 minutes'))->latest()->distinct()->get(['user_id', 'node_id', 'port', 'ip', 'type', 'created_at']);
+
         foreach ($userList as $user) {
             // 最近5条在线IP记录,如果后端设置为60秒上报一次,则为10分钟内的在线IP
             $user->onlineIPList = $nodeOnlineIPs->where('port', $user->port)->take(5);

+ 6 - 6
app/Http/Controllers/Admin/MarketingController.php

@@ -29,8 +29,8 @@ class MarketingController extends Controller
 
         return view('admin.article.marketing', [
             'marketingMessages' => $query->latest()->paginate(15)->appends($request->except('page')),
-            'userGroups' => UserGroup::all()->pluck('name', 'id')->toArray(),
-            'levels' => Level::all()->pluck('name', 'level')->toArray(),
+            'userGroups' => UserGroup::pluck('name', 'id'),
+            'levels' => Level::pluck('name', 'level'),
         ]);
     }
 
@@ -57,7 +57,7 @@ class MarketingController extends Controller
             $users = $this->userStat($request);
             if ($users->isNotEmpty()) {
                 Notification::send($users, new Custom($title, $content, ['mail']));
-                Helpers::addMarketing($users->pluck('id')->toJson(), '1', $title, $content);
+                Helpers::addMarketing($users->pluck('id')->toJson(), 1, $title, $content);
 
                 return response()->json(['status' => 'success', 'message' => trans('admin.marketing.processed')]);
             }
@@ -74,7 +74,7 @@ class MarketingController extends Controller
 
         foreach (['id', 'username', 'status', 'enable', 'user_group_id', 'level'] as $field) {
             $request->whenFilled($field, function ($value) use ($users, $field) {
-                $users->whereIn($field, array_map('trim', explode(',', $value)));
+                $users->whereIn($field, is_string($value) ? array_map('trim', explode(',', $value)) : (array) $value);
             });
         }
 
@@ -93,7 +93,7 @@ class MarketingController extends Controller
 
         // 最近N分钟活跃过
         $request->whenFilled('lastAlive', function ($value) use ($users) {
-            $users->where('t', '>=', now()->subMinutes($value)->timestamp);
+            $users->where('t', '>=', now()->subMinutes((int) $value)->timestamp);
         });
 
         $paidOrderCondition = function ($query) {
@@ -129,6 +129,6 @@ class MarketingController extends Controller
             $users->whereIn('id', (new UserHourlyDataFlow)->trafficAbnormal());
         });
 
-        return $request->isMethod('POST') ? $users->get() : $users->count();
+        return $request->isMethod('POST') ? $users->select('id')->get() : $users->count();
     }
 }

+ 20 - 15
app/Http/Controllers/Admin/NodeAuthController.php

@@ -5,29 +5,40 @@ namespace App\Http\Controllers\Admin;
 use App\Http\Controllers\Controller;
 use App\Models\Node;
 use App\Models\NodeAuth;
-use Exception;
 use Illuminate\Contracts\View\View;
 use Illuminate\Http\JsonResponse;
-use Log;
 use Str;
 
 class NodeAuthController extends Controller
 {
     public function index(): View
     { // 节点授权列表
-        return view('admin.node.auth', ['authorizations' => NodeAuth::with('node:id,name,type,server,ip,ipv6')->has('node')->orderBy('node_id')->paginate()->appends(request('page'))]);
+        $authorizations = NodeAuth::with(['node:id,name,type,server,ip,ipv6'])
+            ->orderBy('node_id')
+            ->paginate()
+            ->appends(request('page'));
+
+        return view('admin.node.auth', compact('authorizations'));
     }
 
     public function store(): JsonResponse
     { // 添加节点授权
-        $nodes = Node::whereStatus(1)->doesntHave('auth')->orderBy('id')->get();
+        $nodes = Node::whereStatus(1)->doesntHave('auth')->pluck('id');
 
         if ($nodes->isEmpty()) {
             return response()->json(['status' => 'success', 'message' => trans('admin.node.auth.empty')]);
         }
-        $nodes->each(function ($node) {
-            $node->auth()->create(['key' => Str::random(), 'secret' => Str::random(8)]);
-        });
+
+        $authData = [];
+        foreach ($nodes as $node_id) {
+            $authData[] = [
+                'node_id' => $node_id,
+                'key' => Str::random(),
+                'secret' => Str::random(8),
+            ];
+        }
+
+        NodeAuth::insert($authData);
 
         return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.generate')])]);
     }
@@ -43,14 +54,8 @@ class NodeAuthController extends Controller
 
     public function destroy(NodeAuth $auth): JsonResponse
     { // 删除节点授权
-        try {
-            if ($auth->delete()) {
-                return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.delete')])]);
-            }
-        } catch (Exception $e) {
-            Log::error(trans('common.error_action_item', ['action' => trans('common.delete'), 'attribute' => trans('admin.menu.node.auth')]).': '.$e->getMessage());
-
-            return response()->json(['status' => 'fail', 'message' => trans('common.error_action_item', ['action' => trans('common.delete'), 'attribute' => trans('admin.menu.node.auth')]).', '.$e->getMessage()]);
+        if ($auth->delete()) {
+            return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.delete')])]);
         }
 
         return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.delete')])]);

+ 36 - 27
app/Http/Controllers/Admin/NodeController.php

@@ -3,6 +3,7 @@
 namespace App\Http\Controllers\Admin;
 
 use App\Helpers\DataChart;
+use App\Helpers\ProxyConfig;
 use App\Http\Controllers\Controller;
 use App\Http\Requests\Admin\NodeRequest;
 use App\Jobs\VNet\reloadNode;
@@ -13,6 +14,7 @@ use App\Models\Node;
 use App\Models\NodeCertificate;
 use App\Models\RuleGroup;
 use App\Utils\NetworkDetection;
+use Arr;
 use Exception;
 use Illuminate\Contracts\View\View;
 use Illuminate\Http\JsonResponse;
@@ -22,7 +24,7 @@ use Log;
 
 class NodeController extends Controller
 {
-    use DataChart;
+    use DataChart, ProxyConfig;
 
     public function index(Request $request): View
     { // 节点列表
@@ -34,15 +36,10 @@ class NodeController extends Controller
                 'hourlyDataFlows' => function ($query) {
                     $query->whereDate('created_at', now()->toDateString());
                 },
-                'onlineLogs' => function ($query) {
-                    $query->where('log_time', '>=', strtotime('-5 minutes'))->orderBy('log_time', 'desc');
-                },
-                'heartbeats' => function ($query) {
-                    $query->where('log_time', '>=', strtotime('-'.sysConfig('recently_heartbeat').' minutes'))->orderBy('log_time', 'desc');
-                },
+                'latestOnlineLog',
+                'latestHeartbeat',
                 'childNodes',
-            ])
-            ->withCount('onlineLogs'); // 提前统计在线人数
+            ]);
 
         $request->whenFilled('status', function ($value) use ($query) {
             $query->where('status', $value);
@@ -50,16 +47,17 @@ class NodeController extends Controller
 
         $nodeList = $query->orderByDesc('sort')->orderBy('id')->paginate(15)->appends($request->except('page'))->through(function ($node) {
             // 预处理每个节点的数据
-            $node->online_users = $node->onlineLogs->first()?->online_user; // 在线人数
-            $node->transfer = formatBytes(
-                $node->dailyDataFlows->sum(fn ($item) => $item->u + $item->d) +
-                $node->hourlyDataFlows->sum(fn ($item) => $item->u + $item->d)
-            ); // 已产生流量
+            $node->online_users = $node->latestOnlineLog?->online_user; // 在线人数
+
+            // 计算流量总和
+            $dailyTransfer = $node->dailyDataFlows->sum(fn ($item) => $item->u + $item->d);
+            $hourlyTransfer = $node->hourlyDataFlows->sum(fn ($item) => $item->u + $item->d);
+            $node->transfer = formatBytes($dailyTransfer + $hourlyTransfer); // 已产生流量
 
-            $node_info = $node->heartbeats->first(); // 近期负载
+            $node_info = $node->latestHeartbeat; // 近期负载
             $node->isOnline = ! empty($node_info?->load);
-            $node->load = $node_info->load ?? false;
-            $node->uptime = formatTime($node_info->uptime ?? 0);
+            $node->load = $node_info?->load ?? false;
+            $node->uptime = formatTime($node_info?->uptime);
 
             return $node;
         });
@@ -91,10 +89,11 @@ class NodeController extends Controller
         return view('admin.node.info', [
             'nodes' => Node::orderBy('id')->pluck('id', 'name'),
             'countries' => Country::orderBy('code')->get(),
-            'levels' => Level::orderBy('level')->get(),
-            'ruleGroups' => RuleGroup::orderBy('id')->get(),
-            'labels' => Label::orderByDesc('sort')->orderBy('id')->get(),
-            'certs' => NodeCertificate::orderBy('id')->get(),
+            'levels' => Level::orderBy('level')->pluck('name', 'level'),
+            'ruleGroups' => RuleGroup::orderBy('id')->pluck('name', 'id'),
+            'labels' => Label::orderByDesc('sort')->orderBy('id')->pluck('name', 'id'),
+            'certs' => NodeCertificate::orderBy('id')->pluck('domain', 'id'),
+            ...$this->proxyConfigOptions(),
         ]);
     }
 
@@ -189,14 +188,23 @@ class NodeController extends Controller
 
     public function edit(Node $node): View
     { // 编辑节点页面
+        $node->load('labels:id');
+        $nodeArray = $node->toArray();
+
         return view('admin.node.info', [
-            'node' => $node->load('labels'),
+            'node' => array_merge(
+                Arr::except($nodeArray, ['details', 'profile']),
+                $nodeArray['details'] ?? [],
+                $nodeArray['profile'] ?? [],
+                ['labels' => $node->labels->pluck('id')->toArray()]// 将标签ID列表作为一维数组
+            ),
             'nodes' => Node::whereNotIn('id', [$node->id])->orderBy('id')->pluck('id', 'name'),
             'countries' => Country::orderBy('code')->get(),
-            'levels' => Level::orderBy('level')->get(),
-            'ruleGroups' => RuleGroup::orderBy('id')->get(),
-            'labels' => Label::orderByDesc('sort')->orderBy('id')->get(),
-            'certs' => NodeCertificate::orderBy('id')->get(),
+            'levels' => Level::orderBy('level')->pluck('name', 'level'),
+            'ruleGroups' => RuleGroup::orderBy('id')->pluck('name', 'id'),
+            'labels' => Label::orderByDesc('sort')->orderBy('id')->pluck('name', 'id'),
+            'certs' => NodeCertificate::orderBy('id')->pluck('domain', 'id'),
+            ...$this->proxyConfigOptions(),
         ]);
     }
 
@@ -247,7 +255,8 @@ class NodeController extends Controller
     { // 刷新节点地理位置
         $ret = false;
         if ($id) {
-            $ret = Node::findOrFail($id)->refresh_geo();
+            $node = Node::findOrFail($id);
+            $ret = $node->refresh_geo();
         } else {
             foreach (Node::whereStatus(1)->get() as $node) {
                 $result = $node->refresh_geo();

+ 15 - 7
app/Http/Controllers/Admin/PermissionController.php

@@ -29,11 +29,15 @@ class PermissionController extends Controller
 
     public function store(PermissionRequest $request): RedirectResponse
     {
-        if ($permission = Permission::create($request->validated())) {
+        try {
+            $permission = Permission::create($request->validated());
+
             return redirect()->route('admin.permission.edit', $permission)->with('successMsg', trans('common.success_item', ['attribute' => trans('common.add')]));
-        }
+        } catch (Exception $e) {
+            Log::error(trans('common.error_action_item', ['action' => trans('common.add'), 'attribute' => trans('model.permission.attribute')]).': '.$e->getMessage());
 
-        return redirect()->back()->withInput()->withErrors(trans('common.failed_item', ['attribute' => trans('common.add')]));
+            return redirect()->back()->withInput()->withErrors(trans('common.failed_item', ['attribute' => trans('common.add')]).', '.$e->getMessage());
+        }
     }
 
     public function create(): View
@@ -43,16 +47,20 @@ class PermissionController extends Controller
 
     public function edit(Permission $permission): View
     {
-        return view('admin.permission.info', compact('permission'));
+        return view('admin.permission.info', ['permission' => $permission->makeHidden(['created_at', 'updated_at', 'guard_name'])]);
     }
 
     public function update(PermissionRequest $request, Permission $permission): RedirectResponse
     {
-        if ($permission->update($request->validated())) {
+        try {
+            $permission->update($request->validated());
+
             return redirect()->back()->with('successMsg', trans('common.success_item', ['attribute' => trans('common.update')]));
-        }
+        } catch (Exception $e) {
+            Log::error(trans('common.error_action_item', ['action' => trans('common.update'), 'attribute' => trans('model.permission.attribute')]).': '.$e->getMessage());
 
-        return redirect()->back()->withInput()->withErrors(trans('common.failed_item', ['attribute' => trans('common.update')]));
+            return redirect()->back()->withInput()->withErrors(trans('common.failed_item', ['attribute' => trans('common.update')]).', '.$e->getMessage());
+        }
     }
 
     public function destroy(Permission $permission): JsonResponse

+ 46 - 10
app/Http/Controllers/Admin/RoleController.php

@@ -16,15 +16,39 @@ class RoleController extends Controller
 {
     public function index(): View
     {
-        return view('admin.role.index', ['roles' => Role::with('permissions')->paginate(15)]);
+        // 预加载角色权限,但只选择需要的字段
+        $roles = Role::with('permissions:description,name')->paginate(15);
+
+        // 预先处理权限描述,避免在 Blade 模板中重复处理
+        $processedRoles = $roles->through(function ($role) {
+            if ($role->name !== 'Super Admin') {
+                // 提前获取权限描述集合,避免在模板中重复调用
+                $role->permission_descriptions = $role->permissions->pluck('description');
+            }
+
+            return $role;
+        });
+
+        return view('admin.role.index', ['roles' => $processedRoles]);
     }
 
     public function store(RoleRequest $request): RedirectResponse
     {
-        if ($role = Role::create($request->only(['name', 'description']))) {
-            $role->givePermissionTo($request->input('permissions') ?? []);
+        try {
+            $role = Role::create($request->only(['name', 'description']));
+
+            if ($role) {
+                $permissions = $request->input('permissions') ?? [];
+                if (! empty($permissions)) {
+                    $role->givePermissionTo($permissions);
+                }
 
-            return redirect()->route('admin.role.edit', $role)->with('successMsg', trans('common.success_item', ['attribute' => trans('common.add')]));
+                return redirect()->route('admin.role.edit', $role)->with('successMsg', trans('common.success_item', ['attribute' => trans('common.add')]));
+            }
+        } catch (Exception $e) {
+            Log::error(trans('common.error_action_item', ['action' => trans('common.add'), 'attribute' => trans('model.role.attribute')]).': '.$e->getMessage());
+
+            return redirect()->back()->withInput()->withErrors(trans('common.failed_item', ['attribute' => trans('common.add')]).', '.$e->getMessage());
         }
 
         return redirect()->back()->withInput()->withErrors(trans('common.failed_item', ['attribute' => trans('common.add')]));
@@ -32,14 +56,19 @@ class RoleController extends Controller
 
     public function create(): View
     {
-        return view('admin.role.info', ['permissions' => Permission::all()->pluck('description', 'name')]);
+        return view('admin.role.info', ['permissions' => Permission::orderBy('name')->pluck('description', 'name')]);
     }
 
     public function edit(Role $role): View
     {
+        $role->load('permissions:name');
+
         return view('admin.role.info', [
-            'role' => $role->load('permissions'),
-            'permissions' => Permission::all()->pluck('description', 'name'),
+            'role' => array_merge(
+                $role->toArray(),
+                ['permissions' => $role->permissions->pluck('name')->toArray()]
+            ),
+            'permissions' => Permission::orderBy('name')->pluck('description', 'name'),
         ]);
     }
 
@@ -49,10 +78,16 @@ class RoleController extends Controller
             return redirect()->back()->withInput()->withErrors(trans('admin.role.modify_admin_error'));
         }
 
-        if ($role->update($request->only(['name', 'description']))) {
-            $role->syncPermissions($request->input('permissions') ?: []);
+        try {
+            if ($role->update($request->only(['name', 'description']))) {
+                $role->syncPermissions($request->input('permissions', []));
 
-            return redirect()->back()->with('successMsg', trans('common.success_item', ['attribute' => trans('common.edit')]));
+                return redirect()->back()->with('successMsg', trans('common.success_item', ['attribute' => trans('common.edit')]));
+            }
+        } catch (Exception $e) {
+            Log::error(trans('common.error_action_item', ['action' => trans('common.edit'), 'attribute' => trans('model.role.attribute')]).': '.$e->getMessage());
+
+            return redirect()->back()->withInput()->withErrors(trans('common.failed_item', ['attribute' => trans('common.edit')]).', '.$e->getMessage());
         }
 
         return redirect()->back()->withInput()->withErrors(trans('common.failed_item', ['attribute' => trans('common.edit')]));
@@ -64,6 +99,7 @@ class RoleController extends Controller
             if ($role->name === 'Super Admin') {
                 return response()->json(['status' => 'fail', 'message' => trans('admin.role.modify_admin_error')]);
             }
+
             $role->delete();
         } catch (Exception $e) {
             Log::error(trans('common.error_action_item', ['action' => trans('common.delete'), 'attribute' => trans('model.role.attribute')]).': '.$e->getMessage());

+ 18 - 6
app/Http/Controllers/Admin/RuleController.php

@@ -28,8 +28,14 @@ class RuleController extends Controller
 
     public function store(RuleRequest $request): JsonResponse
     { // 添加审计规则
-        if (Rule::create($request->validated())) {
-            return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.add')])]);
+        try {
+            if (Rule::create($request->validated())) {
+                return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.add')])]);
+            }
+        } catch (Exception $e) {
+            Log::error(trans('common.error_action_item', ['action' => trans('common.add'), 'attribute' => trans('model.rule.attribute')]).': '.$e->getMessage());
+
+            return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.add')]).', '.$e->getMessage()]);
         }
 
         return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.add')])]);
@@ -37,8 +43,14 @@ class RuleController extends Controller
 
     public function update(RuleRequest $request, Rule $rule): JsonResponse
     { // 编辑审计规则
-        if ($rule->update($request->validated())) {
-            return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.edit')])]);
+        try {
+            if ($rule->update($request->validated())) {
+                return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.edit')])]);
+            }
+        } catch (Exception $e) {
+            Log::error(trans('common.error_action_item', ['action' => trans('common.edit'), 'attribute' => trans('model.rule.attribute')]).': '.$e->getMessage());
+
+            return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.edit')]).', '.$e->getMessage()]);
         }
 
         return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.edit')])]);
@@ -76,8 +88,8 @@ class RuleController extends Controller
         });
 
         return view('admin.rule.log', [
-            'nodes' => Node::all(),
-            'rules' => Rule::all(),
+            'nodes' => Node::pluck('name', 'id'),
+            'rules' => Rule::pluck('name', 'id'),
             'ruleLogs' => $query->latest()->paginate(15)->appends($request->except('page')),
         ]);
     }

+ 28 - 9
app/Http/Controllers/Admin/RuleGroupController.php

@@ -21,10 +21,21 @@ class RuleGroupController extends Controller
 
     public function store(RuleGroupRequest $request): RedirectResponse
     {
-        if ($group = RuleGroup::create($request->only('name', 'type'))) {
-            $group->rules()->attach($request->input('rules'));
+        try {
+            $group = RuleGroup::create($request->only('name', 'type'));
+
+            if ($group) {
+                $rules = $request->input('rules');
+                if (! empty($rules)) {
+                    $group->rules()->attach($rules);
+                }
+
+                return redirect(route('admin.rule.group.edit', $group))->with('successMsg', trans('common.success_item', ['attribute' => trans('common.add')]));
+            }
+        } catch (Exception $e) {
+            Log::error(trans('common.error_action_item', ['action' => trans('common.add'), 'attribute' => trans('model.rule_group.attribute')]).': '.$e->getMessage());
 
-            return redirect(route('admin.rule.group.edit', $group))->with('successMsg', trans('common.success_item', ['attribute' => trans('common.add')]));
+            return redirect()->back()->withInput()->withErrors(trans('common.failed_item', ['attribute' => trans('common.add')]).', '.$e->getMessage());
         }
 
         return redirect()->back()->withInput()->withErrors(trans('common.failed_item', ['attribute' => trans('common.add')]));
@@ -32,23 +43,31 @@ class RuleGroupController extends Controller
 
     public function create(): View
     {
-        return view('admin.rule.group.info', ['rules' => Rule::all()]);
+        return view('admin.rule.group.info', ['rules' => Rule::pluck('name', 'id')]);
     }
 
     public function edit(RuleGroup $group): View
     {
+        $group->load('rules:id');
+
         return view('admin.rule.group.info', [
-            'ruleGroup' => $group,
-            'rules' => Rule::all(),
+            'ruleGroup' => array_merge($group->toArray(), ['rules' => $group->rules->pluck('id')->map('strval')->toArray()]),
+            'rules' => Rule::pluck('name', 'id'),
         ]);
     }
 
     public function update(RuleGroupRequest $request, RuleGroup $group): RedirectResponse
     {
-        if ($group->update($request->only(['name', 'type']))) {
-            $group->rules()->sync($request->input('rules'));
+        try {
+            if ($group->update($request->only(['name', 'type']))) {
+                $group->rules()->sync($request->input('rules', []));
+
+                return redirect()->back()->with('successMsg', trans('common.success_item', ['attribute' => trans('common.edit')]));
+            }
+        } catch (Exception $e) {
+            Log::error(trans('common.error_action_item', ['action' => trans('common.edit'), 'attribute' => trans('model.rule_group.attribute')]).': '.$e->getMessage());
 
-            return redirect()->back()->with('successMsg', trans('common.success_item', ['attribute' => trans('common.edit')]));
+            return redirect()->back()->withInput()->withErrors(trans('common.failed_item', ['attribute' => trans('common.edit')]).', '.$e->getMessage());
         }
 
         return redirect()->back()->withInput()->withErrors(trans('common.failed_item', ['attribute' => trans('common.edit')]));

+ 24 - 7
app/Http/Controllers/Admin/ShopController.php

@@ -8,6 +8,7 @@ use App\Http\Requests\Admin\ShopUpdateRequest;
 use App\Models\Goods;
 use App\Models\GoodsCategory;
 use App\Models\Level;
+use App\Models\Order;
 use Arr;
 use Exception;
 use Illuminate\Contracts\View\View;
@@ -32,12 +33,28 @@ class ShopController extends Controller
 
         $goodsList = $query->orderByDesc('status')->paginate(10)->appends($request->except('page'));
 
-        foreach ($goodsList->load('orders') as $goods) {
-            $goods->use_count = $goods->orders->whereIn('status', [2, 3])->where('is_expire', 0)->count();
-            $goods->total_count = $goods->orders->whereIn('status', [2, 3])->count();
+        // 优化订单统计查询,使用更高效的方式
+        $goodsIds = $goodsList->pluck('id')->toArray();
+
+        // 批量获取订单统计数据
+        $orderStats = Order::whereIn('goods_id', $goodsIds)
+            ->whereIn('status', [2, 3])
+            ->selectRaw('goods_id, is_expire, count(*) as count')
+            ->groupBy('goods_id', 'is_expire')
+            ->get()
+            ->groupBy('goods_id');
+
+        // 为每个商品设置使用统计
+        foreach ($goodsList as $goods) {
+            $stats = $orderStats->get($goods->id, collect());
+            $usedCount = $stats->where('is_expire', 0)->sum('count');
+            $totalCount = $stats->sum('count');
+
+            $goods->use_count = $usedCount;
+            $goods->total_count = $totalCount;
         }
 
-        return view('admin.shop.index', ['goodsList' => $goodsList]);
+        return view('admin.shop.index', compact('goodsList'));
     }
 
     public function store(ShopStoreRequest $request): RedirectResponse
@@ -84,15 +101,15 @@ class ShopController extends Controller
 
     public function create(): View
     {
-        return view('admin.shop.info', ['levels' => Level::orderBy('level')->get(), 'categories' => GoodsCategory::all()]);
+        return view('admin.shop.info', ['levels' => Level::orderBy('level')->pluck('name', 'id'), 'categories' => GoodsCategory::pluck('name', 'id')]);
     }
 
     public function edit(Goods $good): View
     {
         return view('admin.shop.info', [
             'good' => $good,
-            'levels' => Level::orderBy('level')->get(),
-            'categories' => GoodsCategory::all(),
+            'levels' => Level::orderBy('level')->pluck('name', 'id'),
+            'categories' => GoodsCategory::pluck('name', 'id'),
         ]);
     }
 

+ 16 - 2
app/Http/Controllers/Admin/SubscribeController.php

@@ -46,8 +46,22 @@ class SubscribeController extends Controller
             $query->whereBetween('request_time', [$request->input('start').' 00:00:00', $request->input('end').' 23:59:59']);
         }
 
-        $subscribeLogs = $query->latest()->paginate(20)->appends($request->except('page'))->through(function ($log) {
-            $log->ipInfo = $log->request_ip ? optional(IP::getIPInfo($log->request_ip))['address'] ?? null : null;
+        $subscribeLogs = $query->latest()->paginate(20)->appends($request->except('page'));
+
+        // 批量获取 IP 信息以减少查询次数
+        $ipList = $subscribeLogs->pluck('request_ip')->filter()->unique()->toArray();
+        $ipInfoMap = [];
+
+        foreach ($ipList as $ip) {
+            if ($ip) {
+                $ipInfo = IP::getIPInfo($ip);
+                $ipInfoMap[$ip] = $ipInfo ? ($ipInfo['address'] ?? null) : null;
+            }
+        }
+
+        // 将 IP 信息附加到日志记录中
+        $subscribeLogs->getCollection()->transform(function ($log) use ($ipInfoMap) {
+            $log->ipInfo = $log->request_ip ? ($ipInfoMap[$log->request_ip] ?? null) : null;
 
             return $log;
         });

+ 50 - 42
app/Http/Controllers/Admin/SystemController.php

@@ -54,8 +54,8 @@ class SystemController extends Controller
 
         // 预处理复杂数据
         // 解析时间类配置
-        $config['tasks_clean'] = parseTime($config['tasks_clean']);
-        $config['tasks_close'] = parseTime($config['tasks_close']);
+        $config['tasks_clean'] = parseTime($config['tasks_clean'] ?? []);
+        $config['tasks_close'] = parseTime($config['tasks_close'] ?? []);
 
         $paymentForms = PaymentManager::getSettingsFormData();
 
@@ -98,10 +98,13 @@ class SystemController extends Controller
 
         $channels = ['database', 'mail'];
 
+        // 预先获取所有配置值,减少数据库查询
+        $configValues = Config::whereIn('name', array_merge(...array_values($configMap)))->pluck('value', 'name')->toArray();
+
         // 遍历映射,检查配置项是否存在
         foreach ($configMap as $channel => $configKeys) {
-            $allConfigsExist = array_reduce($configKeys, static function ($carry, $configKey) {
-                return $carry && sysConfig($configKey);
+            $allConfigsExist = array_reduce($configKeys, static function ($carry, $configKey) use ($configValues) {
+                return $carry && ! empty($configValues[$configKey]);
             }, true);
 
             if ($allConfigsExist) {
@@ -134,63 +137,68 @@ class SystemController extends Controller
 
     public function setExtend(Request $request): RedirectResponse  // 设置涉及到上传的设置
     {
-        if ($request->hasAny(['website_home_logo', 'website_home_logo'])) { // 首页LOGO
-            if ($request->hasFile('website_home_logo')) {
-                $validator = validator()->make($request->all(), ['website_home_logo' => 'image|mimes:jpeg,png,jpg,gif,svg|max:2048']);
+        // 处理LOGO上传
+        if ($request->hasAny(['website_home_logo', 'website_logo'])) {
+            $logoType = null;
+            $file = null;
 
-                if ($validator->fails()) {
-                    return redirect()->route('admin.system.index', '#other')->withErrors($validator->errors());
-                }
+            if ($request->hasFile('website_home_logo')) {
+                $logoType = 'website_home_logo';
                 $file = $request->file('website_home_logo');
-                $file->move('uploads/logo', $file->getClientOriginalName());
-                if (Config::findOrNew('website_home_logo')->update(['value' => 'uploads/logo/'.$file->getClientOriginalName()])) {
-                    return redirect()->route('admin.system.index', '#other')->with('successMsg', trans('common.success_item', ['attribute' => trans('common.update')]));
-                }
+            } elseif ($request->hasFile('website_logo')) {
+                $logoType = 'website_logo';
+                $file = $request->file('website_logo');
             }
-            if ($request->hasFile('website_logo')) { // 站内LOGO
-                $validator = validator()->make($request->all(), ['website_logo' => 'image|mimes:jpeg,png,jpg,gif,svg|max:2048']);
+
+            if ($logoType && $file) {
+                $validator = validator()->make($request->all(), [$logoType => 'image|mimes:jpeg,png,jpg,gif,svg|max:2048']);
 
                 if ($validator->fails()) {
                     return redirect()->route('admin.system.index', '#other')->withErrors($validator->errors());
                 }
-                $file = $request->file('website_logo');
-                $file->move('uploads/logo', $file->getClientOriginalName());
-                if (Config::findOrNew('website_logo')->update(['value' => 'uploads/logo/'.$file->getClientOriginalName()])) {
+
+                $fileName = $file->getClientOriginalName();
+                $file->move('uploads/logo', $fileName);
+
+                $configKey = $logoType;
+                if (Config::findOrNew($configKey)->update(['value' => 'uploads/logo/'.$fileName])) {
                     return redirect()->route('admin.system.index', '#other')->with('successMsg', trans('common.success_item', ['attribute' => trans('common.update')]));
                 }
-            }
 
-            return redirect()->route('admin.system.index', '#other')->withErrors(trans('common.failed_item', ['attribute' => trans('common.update')]));
+                return redirect()->route('admin.system.index', '#other')->withErrors(trans('common.failed_item', ['attribute' => trans('common.update')]));
+            }
         }
 
+        // 处理支付二维码上传
         if ($request->hasAny(['alipay_qrcode', 'wechat_qrcode'])) {
-            if ($request->hasFile('alipay_qrcode')) {
-                $validator = validator()->make($request->all(), ['alipay_qrcode' => 'image|mimes:jpeg,png,jpg,gif,svg|max:2048']);
+            $qrcodeType = null;
+            $file = null;
 
-                if ($validator->fails()) {
-                    return redirect()->route('admin.system.index', '#payment')->withErrors($validator->errors());
-                }
+            if ($request->hasFile('alipay_qrcode')) {
+                $qrcodeType = 'alipay_qrcode';
                 $file = $request->file('alipay_qrcode');
-                $file->move('uploads/images', $file->getClientOriginalName());
-                if (Config::findOrNew('alipay_qrcode')->update(['value' => 'uploads/images/'.$file->getClientOriginalName()])) {
-                    return redirect()->route('admin.system.index', '#payment')->with('successMsg', trans('common.success_item', ['attribute' => trans('common.update')]));
-                }
+            } elseif ($request->hasFile('wechat_qrcode')) {
+                $qrcodeType = 'wechat_qrcode';
+                $file = $request->file('wechat_qrcode');
             }
 
-            if ($request->hasFile('wechat_qrcode')) { // 站内LOGO
-                $validator = validator()->make($request->all(), ['wechat_qrcode' => 'image|mimes:jpeg,png,jpg,gif,svg|max:2048']);
+            if ($qrcodeType && $file) {
+                $validator = validator()->make($request->all(), [$qrcodeType => 'image|mimes:jpeg,png,jpg,gif,svg|max:2048']);
 
                 if ($validator->fails()) {
                     return redirect()->route('admin.system.index', '#payment')->withErrors($validator->errors());
                 }
-                $file = $request->file('wechat_qrcode');
-                $file->move('uploads/images', $file->getClientOriginalName());
-                if (Config::findOrNew('wechat_qrcode')->update(['value' => 'uploads/images/'.$file->getClientOriginalName()])) {
+
+                $fileName = $file->getClientOriginalName();
+                $file->move('uploads/images', $fileName);
+
+                $configKey = $qrcodeType;
+                if (Config::findOrNew($configKey)->update(['value' => 'uploads/images/'.$fileName])) {
                     return redirect()->route('admin.system.index', '#payment')->with('successMsg', trans('common.success_item', ['attribute' => trans('common.update')]));
                 }
-            }
 
-            return redirect()->route('admin.system.index', '#payment')->withErrors(trans('common.failed_item', ['attribute' => trans('common.update')]));
+                return redirect()->route('admin.system.index', '#payment')->withErrors(trans('common.failed_item', ['attribute' => trans('common.update')]));
+            }
         }
 
         return redirect()->route('admin.system.index');
@@ -278,13 +286,13 @@ class SystemController extends Controller
     public function common(): View
     {
         return view('admin.config.common', [
-            'methods' => SsConfig::type(1)->get(),
-            'protocols' => SsConfig::type(2)->get(),
-            'categories' => GoodsCategory::all(),
-            'obfsList' => SsConfig::type(3)->get(),
+            'methods' => SsConfig::select(['id', 'name', 'is_default'])->type(1)->get(),
+            'protocols' => SsConfig::select(['id', 'name', 'is_default'])->type(2)->get(),
+            'obfsList' => SsConfig::select(['id', 'name', 'is_default'])->type(3)->get(),
+            'categories' => GoodsCategory::select(['id', 'name', 'sort'])->get(),
             'countries' => Country::all(),
             'levels' => Level::all(),
-            'labels' => Label::with('nodes')->get(),
+            'labels' => Label::withCount('nodes')->get(),
         ]);
     }
 }

+ 17 - 5
app/Http/Controllers/Admin/TicketController.php

@@ -15,8 +15,8 @@ class TicketController extends Controller
 {
     public function index(Request $request): View
     { // 工单列表
-        $query = Ticket::where(static function ($query) {
-            $query->whereAdminId(auth()->id())->orwhere('admin_id');
+        $query = Ticket::where(function ($query) {
+            $query->where('admin_id', auth()->id())->orWhereNull('admin_id');
         })->with('user');
 
         $request->whenFilled('username', function ($username) use ($query) {
@@ -31,9 +31,19 @@ class TicketController extends Controller
     public function store(TicketRequest $request): JsonResponse
     { // 创建工单
         $data = $request->validated();
-        $user = User::find($data['uid']) ?: User::whereUsername($data['username'])->first();
 
-        if ($user === auth()->user()) {
+        $user = null;
+        if (! empty($data['uid'])) {
+            $user = User::find($data['uid']);
+        } elseif (! empty($data['username'])) {
+            $user = User::whereUsername($data['username'])->first();
+        }
+
+        if (! $user) {
+            return response()->json(['status' => 'fail', 'message' => trans('admin.marketing.targeted_users_not_found')]);
+        }
+
+        if ($user->id === auth()->id()) {
             return response()->json(['status' => 'fail', 'message' => trans('admin.ticket.self_send')]);
         }
 
@@ -49,7 +59,7 @@ class TicketController extends Controller
         return view('admin.ticket.reply', [
             'ticket' => $ticket,
             'user' => $ticket->user,
-            'replyList' => $ticket->reply()->with('ticket:id,status', 'admin:id,username,qq', 'user:id,username,qq')->oldest()->get(),
+            'replyList' => $ticket->reply()->with(['ticket:id,status', 'admin:id,username,qq', 'user:id,username,qq'])->oldest()->get(),
         ]);
     }
 
@@ -58,6 +68,8 @@ class TicketController extends Controller
         $content = substr(str_replace(['atob', 'eval'], '', clean($request->input('content'))), 0, 300);
 
         if ($ticket->reply()->create(['admin_id' => auth()->id(), 'content' => $content])) {
+            $ticket->update(['status' => 1]);
+
             return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('user.ticket.reply')])]);
         }
 

+ 41 - 28
app/Http/Controllers/Admin/ToolsController.php

@@ -2,6 +2,7 @@
 
 namespace App\Http\Controllers\Admin;
 
+use App\Helpers\ProxyConfig;
 use App\Http\Controllers\Controller;
 use App\Models\User;
 use App\Utils\IP;
@@ -16,6 +17,8 @@ use Symfony\Component\HttpFoundation\BinaryFileResponse;
 
 class ToolsController extends Controller
 {
+    use ProxyConfig;
+
     public function decompile(Request $request): JsonResponse|View
     { // SS(R)链接反解析
         if ($request->isMethod('POST')) {
@@ -41,9 +44,6 @@ class ToolsController extends Controller
                 $txt .= "\r\n".base64url_decode($str);
             }
 
-            // 生成转换好的JSON文件
-            // file_put_contents(public_path('downloads/decompile.json'), $txt);
-
             return response()->json(['status' => 'success', 'data' => $txt, 'message' => trans('common.success_item', ['attribute' => trans('admin.tools.decompile.attribute')])]);
         }
 
@@ -67,13 +67,13 @@ class ToolsController extends Controller
 
             // 校验格式
             $content = json_decode($content, true);
-            if (empty($content->port_password)) {
+            if (! isset($content['port_password']) || ! is_array($content['port_password'])) {
                 return response()->json(['status' => 'fail', 'message' => trans('admin.tools.convert.missing_error')]);
             }
 
             // 转换成SSR格式JSON
             $data = [];
-            foreach ($content->port_password as $port => $passwd) {
+            foreach ($content['port_password'] as $port => $passwd) {
                 $data[] = [
                     'u' => 0,
                     'd' => 0,
@@ -98,14 +98,14 @@ class ToolsController extends Controller
             return response()->json(['status' => 'success', 'data' => $json, 'message' => trans('common.success_item', ['attribute' => trans('common.convert')])]);
         }
 
-        return view('admin.tools.convert');
+        return view('admin.tools.convert', $this->proxyConfigOptions());
     }
 
     public function download(Request $request): BinaryFileResponse
     { // 下载转换好的JSON文件
         $type = (int) $request->input('type');
         if (empty($type)) {
-            abort(trans('admin.tools.convert.params_unknown'));
+            abort(400, trans('admin.tools.convert.params_unknown'));
         }
 
         if ($type === 1) {
@@ -115,7 +115,7 @@ class ToolsController extends Controller
         }
 
         if (! file_exists($filePath)) {
-            abort(trans('admin.tools.convert.file_missing'));
+            abort(404, trans('admin.tools.convert.file_missing'));
         }
 
         return response()->download($filePath);
@@ -141,34 +141,44 @@ class ToolsController extends Controller
 
             $save_path = realpath(storage_path('uploads'));
             $new_name = md5($file->getClientOriginalExtension()).'.json';
-            $file->move($save_path, $new_name);
+
+            try {
+                $file->move($save_path, $new_name);
+            } catch (Exception $e) {
+                Log::error(trans('common.error_action_item', ['action' => trans('common.import'), 'attribute' => trans('admin.menu.tools.import')]).': '.$e->getMessage());
+
+                return redirect()->back()->withErrors(trans('admin.tools.import.file_error'));
+            }
 
             // 读取文件内容
-            $data = file_get_contents($save_path.'/'.$new_name);
+            $file_path = $save_path.'/'.$new_name;
+            $data = file_get_contents($file_path);
+
+            // 删除临时文件
+            @unlink($file_path);
+
             $data = json_decode($data, true);
-            if (! $data) {
+            if (! $data || ! is_array($data)) {
                 return redirect()->back()->withErrors(trans('admin.tools.import.format_error', ['type' => 'JSON']));
             }
 
             try {
                 DB::beginTransaction();
                 foreach ($data as $user) {
-                    $obj = new User;
-                    $obj->nickname = $user->user;
-                    $obj->username = $user->user;
-                    $obj->password = '123456';
-                    $obj->port = $user->port;
-                    $obj->passwd = $user->passwd;
-                    $obj->vmess_id = $user->uuid;
-                    $obj->transfer_enable = $user->transfer_enable;
-                    $obj->method = $user->method;
-                    $obj->protocol = $user->protocol;
-                    $obj->obfs = $user->obfs;
-                    $obj->expired_at = '2099-01-01';
-                    $obj->reg_ip = IP::getClientIp();
-                    $obj->created_at = now();
-                    $obj->updated_at = now();
-                    $obj->save();
+                    User::create([
+                        'nickname' => $user['user'] ?? ('User_'.time()),
+                        'username' => $user['user'] ?? ('user_'.time().'_'.rand(1000, 9999)),
+                        'password' => bcrypt('123456'),
+                        'port' => $user['port'] ?? 0,
+                        'passwd' => $user['passwd'] ?? '',
+                        'vmess_id' => $user['uuid'] ?? '',
+                        'transfer_enable' => $user['transfer_enable'] ?? 0,
+                        'method' => $user['method'] ?? '',
+                        'protocol' => $user['protocol'] ?? '',
+                        'obfs' => $user['obfs'] ?? '',
+                        'expired_at' => '2099-01-01',
+                        'reg_ip' => IP::getClientIp(),
+                    ]);
                 }
 
                 DB::commit();
@@ -195,6 +205,7 @@ class ToolsController extends Controller
         }
 
         $logs = $this->tail($file, 10000);
+        $url = [];
         if ($logs) {
             foreach ($logs as $log) {
                 if (str_contains($log, 'TCP connecting')) {
@@ -217,7 +228,7 @@ class ToolsController extends Controller
             }
         }
 
-        return view('admin.tools.analysis', ['urlList' => array_unique($url ?? [])]);
+        return view('admin.tools.analysis', ['urlList' => array_unique($url)]);
     }
 
     private function tail(string $file, int $n, int $base = 5): array|false
@@ -246,6 +257,8 @@ class ToolsController extends Controller
             }
         }
 
+        fclose($fp);
+
         return array_slice($lines, 0, $n);
     }
 

+ 41 - 25
app/Http/Controllers/Admin/UserController.php

@@ -2,6 +2,7 @@
 
 namespace App\Http\Controllers\Admin;
 
+use App\Helpers\ProxyConfig;
 use App\Http\Controllers\Controller;
 use App\Http\Requests\Admin\UserStoreRequest;
 use App\Http\Requests\Admin\UserUpdateRequest;
@@ -21,16 +22,17 @@ use Exception;
 use Illuminate\Contracts\View\View;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
-use Illuminate\Support\Collection;
 use Log;
 use Spatie\Permission\Models\Role;
 use Str;
 
 class UserController extends Controller
 {
+    use ProxyConfig;
+
     public function index(Request $request): View
     {
-        $query = User::with('subscribe');
+        $query = User::with(['subscribe:user_id,code']);
 
         foreach (['id', 'port', 'status', 'enable', 'user_group_id', 'level'] as $field) {
             $request->whenFilled($field, function ($value) use ($query, $field) {
@@ -76,7 +78,7 @@ class UserController extends Controller
         });
 
         return view('admin.user.index', [
-            'userList' => $query->with('subscribe:user_id,code')->sortable(['id' => 'desc'])->paginate(15)->appends($request->except('page')),
+            'userList' => $query->sortable(['id' => 'desc'])->paginate(15)->appends($request->except('page')),
             'userGroups' => UserGroup::pluck('name', 'id'),
             'levels' => Level::orderBy('level')->pluck('name', 'level'),
         ]);
@@ -89,7 +91,7 @@ class UserController extends Controller
         $data['password'] = $data['password'] ?? Str::random();
         $data['port'] = $data['port'] ?? Helpers::getPort();
         $data['passwd'] = $data['passwd'] ?? Str::random();
-        $data['vmess_id'] = $data['uuid'] ?? Str::uuid();
+        $data['vmess_id'] = $data['vmess_id'] ?: Str::uuid();
         Arr::forget($data, 'uuid');
         $data['transfer_enable'] *= GiB;
         $data['expired_at'] = $data['expired_at'] ?? date('Y-m-d', strtotime('next year'));
@@ -122,35 +124,35 @@ class UserController extends Controller
 
     public function create(): View
     {
-        return view('admin.user.info', [
-            'levels' => Level::orderBy('level')->pluck('name', 'level'),
-            'userGroups' => UserGroup::orderBy('id')->pluck('name', 'id'),
-            'roles' => $this->getAvailableRoles(),
-        ]);
+        return view('admin.user.info', $this->getUserViewData());
+    }
+
+    public function edit(User $user): View
+    {
+        return view('admin.user.info', [...$this->getUserViewData(), 'user' => $user->load('inviter:id,username')]);
     }
 
-    private function getAvailableRoles(): ?Collection
+    /**
+     * 获取用户创建/编辑页面的共享数据.
+     */
+    private function getUserViewData(): array
     {
         $editor = auth()->user();
+        $roles = null;
         if ($editor->hasRole('Super Admin')) { // 超级管理员直接获取全部角色
-            return Role::pluck('description', 'name');
+            $roles = Role::pluck('description', 'name');
         }
 
         if ($editor->can('give roles')) { // 有权者只能获得已有角色,防止权限泛滥
-            return $editor->roles()->pluck('description', 'name');
+            $roles = $editor->roles()->pluck('description', 'name');
         }
 
-        return null;
-    }
-
-    public function edit(User $user): View
-    {
-        return view('admin.user.info', [
-            'user' => $user->load('inviter:id,username'),
+        return [
             'levels' => Level::orderBy('level')->pluck('name', 'level'),
             'userGroups' => UserGroup::orderBy('id')->pluck('name', 'id'),
-            'roles' => $this->getAvailableRoles(),
-        ]);
+            'roles' => $roles,
+            ...$this->proxyConfigOptions(),
+        ];
     }
 
     public function destroy(User $user): JsonResponse
@@ -216,7 +218,7 @@ class UserController extends Controller
     {
         $data = $request->validated();
         $data['passwd'] = $request->input('passwd') ?? Str::random();
-        $data['vmess_id'] = $data['uuid'] ?? Str::uuid();
+        $data['vmess_id'] = $data['vmess_id'] ?: Str::uuid();
         Arr::forget($data, ['roles', 'uuid', 'password']);
         $data['transfer_enable'] *= GiB;
         $data['enable'] = $data['status'] < 0 ? 0 : $data['enable'];
@@ -299,11 +301,25 @@ class UserController extends Controller
         return response()->json(['status' => 'success', 'data' => $proxyService->getUserProxyConfig($server, $request->input('type') !== 'text'), 'title' => $server['type']]);
     }
 
-    public function oauth(): View
+    public function oauth(Request $request): View
     {
-        $list = UserOauth::with('user:id,username')->paginate(15)->appends(\request('page'));
+        $query = UserOauth::with('user:id,username');
 
-        return view('admin.user.oauth', compact('list'));
+        // 用户名过滤
+        $request->whenFilled('username', function ($value) use ($query) {
+            $query->whereHas('user', function ($userQuery) use ($value) {
+                $userQuery->where('username', 'like', "%$value%");
+            });
+        });
+
+        // 类型过滤
+        $request->whenFilled('type', function ($value) use ($query) {
+            $query->where('type', $value);
+        });
+
+        return view('admin.user.oauth', [
+            'list' => $query->paginate(15)->appends(\request('page')),
+        ]);
     }
 
     public function VNetInfo(User $user): JsonResponse

+ 6 - 1
app/Http/Controllers/Admin/UserGroupController.php

@@ -37,8 +37,13 @@ class UserGroupController extends Controller
 
     public function edit(UserGroup $group): View
     {
+        $group->load('nodes:id');
+
         return view('admin.user.group.info', [
-            'group' => $group,
+            'group' => array_merge(
+                $group->toArray(),
+                ['nodes' => $group->nodes->pluck('id')->map('strval')->toArray()]
+            ),
             'nodes' => Node::whereStatus(1)->pluck('name', 'id'),
         ]);
     }

+ 8 - 13
app/Http/Controllers/AdminController.php

@@ -20,15 +20,10 @@ class AdminController extends Controller
         $past = strtotime('-'.sysConfig('expire_days').' days');
         $today = today();
 
-        $stats = cache()->remember('user_stats', now()->addMinutes(5), function () use ($today, $past) {
+        $stats = cache()->remember('user_stats', now()->addMinutes(5), function () use ($today) {
             $dailyTrafficUsage = NodeHourlyDataFlow::whereDate('created_at', $today)->sum(DB::raw('u + d'));
 
             return [
-                'activeUserCount' => User::where('t', '>=', $past)->count(), // 活跃用户数
-                'inactiveUserCount' => User::whereEnable(1)->where('t', '<', $past)->count(), // 不活跃用户数
-                'expireWarningUserCount' => User::whereBetween('expired_at', [$today, today()->addDays(sysConfig('expire_days'))])->count(), // 临近过期用户数
-                'largeTrafficUserCount' => User::whereRaw('(u + d)/transfer_enable >= 0.9')->where('status', '<>', -1)->count(), // 流量使用超过90%的用户
-                'flowAbnormalUserCount' => count((new UserHourlyDataFlow)->trafficAbnormal()), // 1小时内流量异常用户
                 'monthlyTrafficUsage' => formatBytes(NodeDailyDataFlow::whereNull('node_id')->whereMonth('created_at', now()->month)->sum(DB::raw('u + d'))),
                 'dailyTrafficUsage' => $dailyTrafficUsage ? formatBytes($dailyTrafficUsage) : 0,
                 'totalTrafficUsage' => formatBytes(NodeDailyDataFlow::whereNull('node_id')->where('created_at', '>=', now()->subDays(30))->sum(DB::raw('u + d'))),
@@ -39,14 +34,14 @@ class AdminController extends Controller
             'totalUserCount' => User::count(), // 总用户数
             'todayRegister' => User::whereDate('created_at', $today)->count(), // 今日注册用户
             'enableUserCount' => User::whereEnable(1)->count(), // 有效用户数
-            'activeUserCount' => $stats['activeUserCount'],
+            'activeUserCount' => User::where('t', '>=', $past)->count(), // 活跃用户数
             'payingUserCount' => User::has('paidOrders')->count(), // 付费用户数
-            'payingNewUserCount' => User::whereDate('created_at', $today)->has('paidOrders')->count(), // 不活跃用户数
-            'inactiveUserCount' => $stats['inactiveUserCount'],
-            'onlineUserCount' => User::where('t', '>=', strtotime('-10 minutes'))->count(), // 10分钟内在线用户数,
-            'expireWarningUserCount' => $stats['expireWarningUserCount'],
-            'largeTrafficUserCount' => $stats['largeTrafficUserCount'],
-            'flowAbnormalUserCount' => $stats['flowAbnormalUserCount'],
+            'payingNewUserCount' => User::whereDate('created_at', $today)->has('paidOrders')->count(), // 今日新增付费用户
+            'inactiveUserCount' => User::whereEnable(1)->where('t', '<', $past)->count(), // 不活跃用户数
+            'onlineUserCount' => User::where('t', '>=', strtotime('-10 minutes'))->count(), // 10分钟内在线用户数
+            'expireWarningUserCount' => User::whereBetween('expired_at', [$today, today()->addDays(sysConfig('expire_days'))])->count(), // 临近过期用户数
+            'largeTrafficUserCount' => User::whereRaw('(u + d)/transfer_enable >= 0.9')->where('status', '<>', -1)->count(), // 流量使用超过90%的用户
+            'flowAbnormalUserCount' => count((new UserHourlyDataFlow)->trafficAbnormal()), // 1小时内流量异常用户
             'nodeCount' => Node::count(),
             'abnormalNodeCount' => Node::whereStatus(0)->count(),
             'monthlyTrafficUsage' => $stats['monthlyTrafficUsage'],

+ 2 - 3
app/Http/Controllers/Api/Client/ClientController.php

@@ -198,15 +198,14 @@ class ClientController extends Controller
     public function getProxyList(ProxyService $proxyService): JsonResponse
     {
         $servers = [];
-        foreach ($proxyService->getNodeList(null, false) as $node) {
+        foreach ($proxyService->getNodeList(null, false)->load('latestOnlineLog') as $node) {
             $server = $proxyService->getProxyConfig($node);
             if ($server['type'] === '`shadowsocks`' || $server['type'] === 'shadowsocksr') {
                 $server['type'] = 1;
             }
 
-            $online_log = $node->onlineLogs->where('log_time', '>=', strtotime('-5 minutes'))->sortBy('log_time')->first(); // 在线人数
             $server['node_ip'] = filter_var($server['host'], FILTER_VALIDATE_IP) ? $server['host'] : gethostbyname($server['host']);
-            $server['online'] = $online_log->online_user ?? 0;
+            $server['online'] = $node->latestOnlineLog?->online_user ?? 0; // 在线人数
             $this->getOnlineCount($server, $server['online']);
             $servers[] = $server;
         }

+ 11 - 19
app/Http/Controllers/AuthController.php

@@ -56,8 +56,8 @@ class AuthController extends Controller
         if (! auth()->attempt($data, $request->has('remember'))) {
             return redirect()->back()->withInput()->withErrors(trans('auth.error.login_failed'));
         }
-        $user = auth()->getUser();
 
+        $user = auth()->getUser();
         if (! $user) {
             return redirect()->back()->withInput()->withErrors(trans('auth.error.login_error'));
         }
@@ -131,7 +131,7 @@ class AuthController extends Controller
     {
         session()->put('register_token', Str::random());
 
-        return view('auth.register', ['emailList' => (int) sysConfig('is_email_filtering') !== 2 ? false : EmailFilter::whereType(2)->get()]);
+        return view('auth.register', ['emailList' => sysConfig('is_email_filtering') === '2' ? EmailFilter::whereType(2)->get() : false]);
     }
 
     public function register(RegisterRequest $request): RedirectResponse
@@ -185,8 +185,7 @@ class AuthController extends Controller
                 return redirect()->back()->withInput($request->except('verify_code'))->withErrors(trans('auth.captcha.error.timeout'));
             }
 
-            $verifyCode->status = 1;
-            $verifyCode->save();
+            $verifyCode->update(['status' => 1]);
         }
 
         // 是否校验验证码
@@ -342,13 +341,10 @@ class AuthController extends Controller
 
     private function addVerifyUrl(int $uid, string $email): string
     { // 生成申请的请求地址
-        $token = md5(sysConfig('website_name').$email.microtime());
-        $verify = new Verify;
-        $verify->user_id = $uid;
-        $verify->token = $token;
-        $verify->save();
-
-        return $token;
+        return Verify::create([
+            'user_id' => $uid,
+            'token' => md5(sysConfig('website_name').$email.microtime()),
+        ])->token;
     }
 
     public function resetPassword(Request $request): RedirectResponse|View
@@ -436,8 +432,7 @@ class AuthController extends Controller
             }
 
             // 置为已使用
-            $verify->status = 1;
-            $verify->save();
+            $verify->update(['status' => 1]);
 
             return redirect()->route('login')->with('successMsg', trans('auth.password.reset.success'));
         }
@@ -449,8 +444,7 @@ class AuthController extends Controller
 
         if (time() - strtotime($verify->created_at) >= 1800) {
             // 置为已失效
-            $verify->status = 2;
-            $verify->save();
+            $verify->update(['status' => 2]);
         }
 
         return view('auth.reset', ['verify' => Verify::type(1)->whereToken($token)->first()]); // 重新获取一遍verify
@@ -531,8 +525,7 @@ class AuthController extends Controller
             session()->flash('errorMsg', trans('auth.error.url_timeout'));
 
             // 置为已失效
-            $verify->status = 2;
-            $verify->save();
+            $verify->update(['status' => 2]);
 
             return view('auth.active');
         }
@@ -545,8 +538,7 @@ class AuthController extends Controller
         }
 
         // 置为已使用
-        $verify->status = 1;
-        $verify->save();
+        $verify->update(['status' => 1]);
 
         // 账号激活后给邀请人送流量
         $inviter = $user->inviter;

+ 55 - 1
app/Http/Controllers/OAuthController.php

@@ -2,10 +2,12 @@
 
 namespace App\Http\Controllers;
 
+use App\Models\Invite;
 use App\Models\User;
 use App\Models\UserOauth;
 use App\Utils\Helpers;
 use App\Utils\IP;
+use Hashids\Hashids;
 use Illuminate\Http\RedirectResponse;
 use Laravel\Socialite\Facades\Socialite;
 use Str;
@@ -85,7 +87,31 @@ class OAuthController extends Controller
             $userAuth = UserOauth::whereType($provider)->whereIdentifier($registerInfo->getId())->first();
 
             if (! $userAuth) { // 第三方账号未被绑定
-                $user = Helpers::addUser($registerInfo->getEmail(), Str::random(), MiB * sysConfig('default_traffic'), (int) sysConfig('default_days'), $registerInfo->getNickname(), 1);
+                // 获取邀请信息
+                $affArr = $this->getAff();
+                $inviter_id = $affArr['inviter_id'];
+
+                // 计算流量值(包括邀请奖励流量)
+                $transfer_enable = MiB * ((int) sysConfig('default_traffic') + ($inviter_id ? (int) sysConfig('referral_traffic') : 0));
+
+                // 创建用户并传入邀请者 ID
+                $user = Helpers::addUser($registerInfo->getEmail(), Str::random(), $transfer_enable, (int) sysConfig('default_days'), $inviter_id, $registerInfo->getNickname(), 1);
+
+                // 更新邀请码(如果使用了邀请码)
+                if ($affArr['code_id'] && sysConfig('is_invite_register')) {
+                    Invite::find($affArr['code_id'])?->update(['invitee_id' => $user->id, 'status' => 1]);
+                }
+
+                // 清除邀请人Cookie
+                cookie()->unqueue('register_aff');
+
+                // 给邀请人增加流量(如果有的话)
+                if ($inviter_id) {
+                    $referralUser = User::find($inviter_id);
+                    if ($referralUser && $referralUser->expiration_date >= date('Y-m-d')) {
+                        $referralUser->incrementData(sysConfig('referral_traffic') * MiB);
+                    }
+                }
 
                 $user->userAuths()->create([
                     'type' => $provider,
@@ -100,6 +126,34 @@ class OAuthController extends Controller
         return redirect()->route('login')->withErrors(trans('auth.oauth.registered'));
     }
 
+    private function getAff(): array
+    { // 获取邀请信息
+        $data = ['inviter_id' => null, 'code_id' => 0]; // 邀请人ID 与 邀请码ID
+
+        // 检查cookie中的邀请信息(通过Affiliate中间件设置)
+        $cookieAff = request()?->cookie('register_aff');
+        if ($cookieAff) {
+            $data['inviter_id'] = $this->setInviter($cookieAff);
+        }
+
+        return $data;
+    }
+
+    private function setInviter(string|int $aff): ?int
+    {
+        $uid = 0;
+        if (is_numeric($aff)) {
+            $uid = (int) $aff;
+        } else {
+            $decode = (new Hashids(sysConfig('affiliate_link_salt'), 8))->decode($aff);
+            if ($decode) {
+                $uid = $decode[0];
+            }
+        }
+
+        return $uid && User::whereId($uid)->exists() ? $uid : null;
+    }
+
     private function handleLogin(User $user): RedirectResponse
     {
         auth()->login($user);

+ 4 - 2
app/Http/Controllers/User/ArticleController.php

@@ -25,8 +25,10 @@ class ArticleController extends Controller
 
     public function show(Article $article): JsonResponse
     { // 公告详情
-        $articleService = new ArticleService($article);
+        $content = cache()->remember("article.content.{$article->id}", 3600, function () use ($article) {
+            return (new ArticleService($article))->getContent();
+        });
 
-        return response()->json(['title' => $article->title, 'content' => $articleService->getContent()]);
+        return response()->json(['title' => $article->title, 'content' => $content]);
     }
 }

+ 16 - 7
app/Http/Controllers/User/InviteController.php

@@ -30,17 +30,26 @@ class InviteController extends Controller
     public function store(): JsonResponse
     { // 生成邀请码
         $user = auth()->user();
+
+        // 检查用户是否还有邀请码配额
         if ($user->invite_num <= 0) {
             return response()->json(['status' => 'fail', 'message' => trans('user.invite.generate_failed')]);
         }
-        $invite = $user->invites()->create([
-            'code' => strtoupper(mb_substr(md5(microtime().Str::random()), 8, 12)),
-            'dateline' => date('Y-m-d H:i:s', strtotime(sysConfig('user_invite_days').' days')),
-        ]);
-        if ($invite) {
-            $user->decrement('invite_num');
 
-            return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.generate')])]);
+        try {
+            $invite = $user->invites()->create([
+                'code' => strtoupper(Str::random(12)), // 简化邀请码生成逻辑
+                'dateline' => now()->addDays((int) sysConfig('user_invite_days')),
+            ]);
+
+            if ($invite) {
+                $user->decrement('invite_num');
+
+                return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.generate')])]);
+            }
+        } catch (\Exception $e) {
+            // 记录异常但不暴露给用户
+            \Log::error('Failed to generate invite code: '.$e->getMessage());
         }
 
         return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.generate')])]);

+ 13 - 12
app/Http/Controllers/User/NodeController.php

@@ -4,7 +4,6 @@ namespace App\Http\Controllers\User;
 
 use App\Http\Controllers\Controller;
 use App\Models\Node;
-use App\Models\NodeHeartbeat;
 use App\Services\ProxyService;
 use Illuminate\Contracts\View\View;
 use Illuminate\Http\JsonResponse;
@@ -14,20 +13,22 @@ class NodeController extends Controller
 {
     public function index(): View
     { // 节点列表
-        $nodeList = auth()->user()->nodes()->whereIn('is_display', [1, 3])->with(['labels', 'level_table'])->get(); // 获取当前用户可用节点
-        $onlineNode = NodeHeartbeat::recently()->distinct()->pluck('node_id')->toArray();
-        foreach ($nodeList as $node) {
-            $node->offline = ! in_array($node->id, $onlineNode, true); // 节点在线状态
-        }
-
-        return view('user.nodeList', [
-            'nodesGeo' => $nodeList->pluck('name', 'geo')->toArray(),
-            'nodeList' => $nodeList,
-        ]);
+        $nodes = auth()->user()->nodes()->whereIn('is_display', [1, 3])->with(['labels', 'level_table:level,name', 'latestHeartbeat'])->orderByDesc('sort')->orderBy('id')->get(); // 获取当前用户可用节点
+
+        // 直接在节点集合上标记在线状态和标签名称
+        $nodes->each(function ($node) {
+            $node->offline = is_null($node->latestHeartbeat);
+            $node->label_names = $node->labels->sortByDesc('sort')->sortBy('id')->pluck('name');
+        });
+
+        // 提取节点地理位置信息用于地图显示
+        $nodesGeo = $nodes->pluck('name', 'geo');
+
+        return view('user.nodeList', compact('nodesGeo', 'nodes'));
     }
 
     public function show(Request $request, Node $node, ProxyService $proxyServer): JsonResponse
-    { // 节点详细
+    { // 节点详细信息
         $server = $proxyServer->getProxyConfig($node);
 
         return response()->json(['status' => 'success', 'data' => $proxyServer->getUserProxyConfig($server, $request->input('type') !== 'text'), 'title' => $server['type']]);

+ 24 - 20
app/Http/Controllers/User/ShopController.php

@@ -20,29 +20,32 @@ class ShopController extends Controller
     public function index(): View
     { // 商品列表
         $user = auth()->user();
-        // 余额充值商品,只取10个
-        $renewOrder = Order::userActivePlan($user->id)->first();
-        $renewPrice = $renewOrder->goods->renew ?? 0;
-        // 有重置日时按照重置日为标准,否则就以过期日为标准
-        $dataPlusDays = $user->reset_time ?? $user->expired_at;
 
-        $goodsList = Goods::whereStatus(1)->where('type', '<=', '2')->orderByDesc('type')->orderByDesc('sort')->get();
+        // 获取可用商品列表
+        $goodsList = Goods::whereStatus(1)->where('type', '<=', 2)->orderByDesc('type')->orderByDesc('sort')->get();
 
-        if ($user && $nodes = $user->userGroup) {
-            $nodes = $nodes->nodes();
-        } else {
-            $nodes = Node::all();
-        }
-        foreach ($goodsList as $goods) {
-            $goods->node_count = $nodes->where('level', '<=', $goods->level)->where('status', 1)->count();
-            $goods->node_countries = $nodes->where('level', '<=', $goods->level)->where('status', 1)->pluck('country_code')->unique();
-        }
+        // 获取用户节点信息
+        $nodes = $user->userGroup ? $user->userGroup->nodes() : Node::query();
+
+        // 为每个商品计算节点数量和国家
+        $goodsList->each(function ($goods) use ($nodes) {
+            $filteredNodes = $nodes->where('level', '<=', $goods->level)->where('status', 1);
+            $goods->node_count = $filteredNodes->count();
+            $goods->node_countries = $filteredNodes->pluck('country_code')->unique();
+        });
+
+        // 获取续费订单和价格
+        $renewOrder = Order::userActivePlan($user->id)->first();
+        $renewPrice = $renewOrder?->goods->renew ?? 0;
+
+        // 计算数据增加天数
+        $dataPlusDays = $user->reset_time ?? $user->expired_at;
 
         return view('user.services', [
             'chargeGoodsList' => Goods::type(3)->orderBy('price')->get(),
             'goodsList' => $goodsList,
             'renewTraffic' => $renewPrice ? Helpers::getPriceTag($renewPrice) : 0,
-            'dataPlusDays' => $dataPlusDays > date('Y-m-d') ? $dataPlusDays->diffInDays() : 0,
+            'dataPlusDays' => $dataPlusDays > now() ? $dataPlusDays->diffInDays() : 0,
         ]);
     }
 
@@ -51,16 +54,17 @@ class ShopController extends Controller
         $user = auth()->user();
         $order = Order::userActivePlan()->firstOrFail();
         $renewCost = $order->goods->renew;
+
+        // 检查余额是否足够
         if ($user->credit < $renewCost) {
             return response()->json(['status' => 'fail', 'message' => trans('user.payment.insufficient_balance')]);
         }
 
+        // 重置用户流量
         $user->update(['u' => 0, 'd' => 0]);
 
-        // 记录余额操作日志
+        // 记录余额操作日志并扣费
         Helpers::addUserCreditLog($user->id, null, $user->credit, $user->credit - $renewCost, -1 * $renewCost, 'The user manually reset the data.');
-
-        // 扣余额
         $user->updateCredit(-$renewCost);
 
         return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.reset')])]);
@@ -96,7 +100,7 @@ class ShopController extends Controller
         $dataPlusDays = $user->reset_time ?? $user->expired_at;
 
         return view('user.buy', [
-            'dataPlusDays' => $dataPlusDays > date('Y-m-d') ? $dataPlusDays->diffInDays() : 0,
+            'dataPlusDays' => $dataPlusDays > now() ? $dataPlusDays->diffInDays() : 0,
             'activePlan' => Order::userActivePlan()->exists(),
             'goods' => $good,
         ]);

+ 40 - 22
app/Http/Controllers/User/SubscribeController.php

@@ -24,6 +24,7 @@ class SubscribeController extends Controller
 
     public function index(Request $request, string $code)
     {
+        // 检查订阅码格式
         if (! preg_match('/^[0-9A-Za-z]+$/', $code)) {
             return redirect()->route('login');
         }
@@ -43,65 +44,82 @@ class SubscribeController extends Controller
 
     public function getSubscribeByCode(Request $request, string $code): RedirectResponse|string
     { // 通过订阅码获取订阅信息
-        self::$subType = is_numeric($request->input('type')) ? $request->input('type') : null;
-        // 检查订阅码是否有效
+        self::$subType = is_numeric($request->input('type')) ? (int) $request->input('type') : null;
+
+        // 检查订阅码格式
         if (! preg_match('/^[0-9A-Za-z]+$/', $code)) {
-            $this->failed(trans('errors.subscribe.unknown'));
+            return $this->failed(trans('errors.subscribe.unknown'));
         }
 
+        // 检查订阅是否存在
         $subscribe = UserSubscribe::whereCode($code)->first();
         if (! $subscribe) {
-            $this->failed(trans('errors.subscribe.unknown'));
+            return $this->failed(trans('errors.subscribe.unknown'));
         }
 
+        // 检查订阅状态
         if ($subscribe->status !== 1) {
-            $this->failed(trans('errors.subscribe.sub_banned'));
+            return $this->failed(trans('errors.subscribe.sub_banned'));
         }
 
+        // 检查用户是否有效
         $user = $subscribe->user;
-        if (! $user) { // 检查用户是否有效
-            $this->failed(trans('errors.subscribe.user'));
+        if (! $user) {
+            return $this->failed(trans('errors.subscribe.user'));
         }
 
+        // 检查用户状态
         if ($user->status === -1) {
-            $this->failed(trans('errors.subscribe.user_disabled'));
+            return $this->failed(trans('errors.subscribe.user_disabled'));
         }
 
         if ($user->enable !== 1) {
             if ($user->ban_time) {
-                $this->failed(trans('errors.subscribe.banned_until', ['time' => $user->ban_time]));
+                return $this->failed(trans('errors.subscribe.banned_until', ['time' => $user->ban_time]));
             }
 
             if ($user->unused_traffic <= 0) {
-                $this->failed(trans('errors.subscribe.out'));
+                return $this->failed(trans('errors.subscribe.out'));
             }
 
             if ($user->expiration_date < now()->toDateString()) {
-                $this->failed(trans('errors.subscribe.expired'));
+                return $this->failed(trans('errors.subscribe.expired'));
             }
 
-            $this->failed(trans('errors.subscribe.question'));
+            return $this->failed(trans('errors.subscribe.question'));
         }
-        $this->proxyServer->setUser($user);
 
+        // 设置用户并更新订阅信息
+        $this->proxyServer->setUser($user);
         $subscribe->increment('times'); // 更新访问次数
-        $this->subscribeLog($subscribe->id, IP::getClientIp(), json_encode(['Host' => $request->getHost(), 'User-Agent' => $request->userAgent()])); // 记录每次请求
 
-        return $this->proxyServer->getProxyText(strtolower($request->input('target') ?? ($request->userAgent() ?? '')), self::$subType);
+        // 记录订阅日志
+        $this->subscribeLog($subscribe->id, IP::getClientIp(), json_encode([
+            'Host' => $request->getHost(),
+            'User-Agent' => $request->userAgent(),
+        ]));
+
+        // 返回订阅内容
+        return $this->proxyServer->getProxyText(
+            strtolower($request->input('target') ?? ($request->userAgent() ?? '')),
+            self::$subType
+        );
     }
 
-    private function failed(string $text): void
+    private function failed(string $text): string
     { // 抛出错误的节点信息,用于兼容防止客户端订阅失败
         $this->proxyServer->failedProxyReturn($text, self::$subType ?? 1);
+
+        return '';
     }
 
     private function subscribeLog(int $subscribeId, ?string $ip, string $headers): void
     { // 写入订阅访问日志
-        $log = new UserSubscribeLog;
-        $log->user_subscribe_id = $subscribeId;
-        $log->request_ip = $ip;
-        $log->request_time = now();
-        $log->request_header = $headers;
-        $log->save();
+        UserSubscribeLog::create([
+            'user_subscribe_id' => $subscribeId,
+            'request_ip' => $ip,
+            'request_time' => now(),
+            'request_header' => $headers,
+        ]);
     }
 }

+ 50 - 24
app/Http/Controllers/User/TicketController.php

@@ -19,54 +19,80 @@ class TicketController extends Controller
 
     public function store(Request $request): JsonResponse
     { // 添加工单
-        $title = $request->input('title');
-        $content = substr(str_replace(['atob', 'eval'], '', clean($request->input('content'))), 0, 300);
+        $validatedData = $request->validate([
+            'title' => 'required|string|max:255',
+            'content' => 'required|string|max:300',
+        ]);
 
-        if (empty($title) || empty($content)) {
-            return response()->json([
-                'status' => 'fail', 'message' => trans('validation.required', ['attribute' => ucfirst(trans('validation.attributes.title')).'&'.ucfirst(trans('validation.attributes.content'))]),
-            ]);
-        }
+        // 清理内容,防止恶意代码
+        $title = $validatedData['title'];
+        $content = substr(str_replace(['atob', 'eval'], '', clean($validatedData['content'])), 0, 300);
+
+        $ticket = auth()->user()->tickets()->create(compact('title', 'content'));
 
-        if (auth()->user()->tickets()->create(compact('title', 'content'))) {
+        if ($ticket) {
             // 通知相关管理员
-            return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.submit')])]);
+            return response()->json([
+                'status' => 'success',
+                'message' => trans('common.success_item', ['attribute' => trans('common.submit')]),
+            ]);
         }
 
-        return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.create')])]);
+        return response()->json([
+            'status' => 'fail',
+            'message' => trans('common.failed_item', ['attribute' => trans('common.create')]),
+        ]);
     }
 
     public function edit(Ticket $ticket): View
     { // 回复工单
-        return view('user.replyTicket', [
-            'ticket' => $ticket,
-            'replyList' => $ticket->reply()->with('ticket:id,status', 'admin:id,username,qq', 'user:id,username,qq')->oldest()->get(),
-        ]);
+        $replyList = $ticket->reply()
+            ->with('ticket:id,status', 'admin:id,username,qq', 'user:id,username,qq')
+            ->oldest()
+            ->get();
+
+        return view('user.replyTicket', compact('ticket', 'replyList'));
     }
 
     public function reply(Request $request, Ticket $ticket): JsonResponse
     {
-        $content = substr(str_replace(['atob', 'eval'], '', clean($request->input('content'))), 0, 300);
+        $validatedData = $request->validate([
+            'content' => 'required|string|max:300',
+        ]);
+
+        // 清理内容,防止恶意代码
+        $content = substr(str_replace(['atob', 'eval'], '', clean($validatedData['content'])), 0, 300);
 
-        if (empty($content)) {
+        $reply = $ticket->reply()->create([
+            'user_id' => auth()->id(),
+            'content' => $content,
+        ]);
+
+        if ($reply) {
             return response()->json([
-                'status' => 'fail', 'message' => trans('validation.required', ['attribute' => ucfirst(trans('validation.attributes.title')).'&'.ucfirst(trans('validation.attributes.content'))]),
+                'status' => 'success',
+                'message' => trans('common.success_item', ['attribute' => trans('user.ticket.reply')]),
             ]);
         }
 
-        if ($ticket->reply()->create(['user_id' => auth()->id(), 'content' => $content])) {
-            return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('user.ticket.reply')])]);
-        }
-
-        return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('user.ticket.reply')])]);
+        return response()->json([
+            'status' => 'fail',
+            'message' => trans('common.failed_item', ['attribute' => trans('user.ticket.reply')]),
+        ]);
     }
 
     public function close(Ticket $ticket): JsonResponse
     { // 关闭工单
         if ($ticket->close()) {
-            return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.close')])]);
+            return response()->json([
+                'status' => 'success',
+                'message' => trans('common.success_item', ['attribute' => trans('common.close')]),
+            ]);
         }
 
-        return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.close')])]);
+        return response()->json([
+            'status' => 'fail',
+            'message' => trans('common.failed_item', ['attribute' => trans('common.close')]),
+        ]);
     }
 }

+ 4 - 4
app/Http/Controllers/UserController.php

@@ -28,9 +28,7 @@ class UserController extends Controller
         }
         $user = auth()->user();
 
-        $user->load(['subscribe', 'loginLogs' => function ($query) {
-            $query->latest()->first();
-        }]);
+        $user->load(['subscribe', 'latestLoginLog']);
 
         return view('user.index', [
             'remainDays' => $userService->getRemainingDays(),
@@ -42,7 +40,7 @@ class UserController extends Controller
             'isTrafficWarning' => $userService->isTrafficWarning(), // 流量异常判断
             'paying_user' => $userService->isActivePaying(), // 付费用户判断
             'user' => $user->only(['sub_url', 'unused_traffic', 'expiration_date', 'ban_time']),
-            'userLoginLog' => $user->loginLogs->first(), // 近期登录日志
+            'userLoginLog' => $user->latestLoginLog,
             'subType' => $nodeService->getActiveNodeTypes($user->nodes()),
             'subscribe' => $user->subscribe->only(['status', 'ban_desc']),
             ...$this->dataFlowChart($user->id)]);
@@ -67,6 +65,7 @@ class UserController extends Controller
         if (! $user->incrementData($traffic)) {
             return response()->json(['status' => 'fail', 'title' => trans('common.failed'), 'message' => trans('user.home.attendance.failed')]);
         }
+
         Helpers::addUserTrafficModifyLog($user->id, $user->transfer_enable, $user->transfer_enable + $traffic, trans('user.home.attendance.attribute'));
 
         cache()->put('userCheckIn_'.$user->id, '1', sysConfig('checkin_interval') ? sysConfig('checkin_interval') * Minute : Day); // 多久后可以再签到
@@ -84,6 +83,7 @@ class UserController extends Controller
     {
         $user = auth()->user();
         $url = null;
+
         if ($request->has(['password', 'new_password'])) { // 修改密码
             $url = url()->previous().'#account';
             $data = $request->only(['password', 'new_password']);

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

@@ -9,7 +9,7 @@ class PermissionRequest extends FormRequest
     public function rules(): array
     {
         return [
-            'name' => 'required|string',
+            'name' => 'required|string|unique:permissions,name',
             'description' => 'required|string',
         ];
     }

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

@@ -9,7 +9,7 @@ class UserGroupRequest extends FormRequest
     public function rules(): array
     {
         return [
-            'name' => 'required|string',
+            'name' => 'required|string|unique:user_group,name',
             'nodes' => 'nullable|exists:node,id',
         ];
     }

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

@@ -14,7 +14,7 @@ class UserStoreRequest extends FormRequest
             'password' => 'nullable|string',
             'port' => 'nullable|numeric',
             'passwd' => 'nullable|string',
-            'uuid' => 'nullable|uuid',
+            'vmess_id' => 'nullable|uuid',
             'transfer_enable' => 'required|numeric|min:0',
             'enable' => 'required|boolean',
             'method' => 'required|exists:ss_config,name',

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

@@ -14,7 +14,7 @@ class UserUpdateRequest extends FormRequest
             'password' => 'nullable|string',
             'port' => 'required|numeric|exclude_if:port,0|gt:0|unique:user,port,'.$this->user->id,
             'passwd' => 'required|string',
-            'uuid' => 'required|uuid',
+            'vmess_id' => 'nullable|uuid',
             'transfer_enable' => 'required|numeric|min:0',
             'enable' => 'required|boolean',
             'method' => 'required|exists:ss_config,name',

+ 2 - 0
app/Models/CouponLog.php

@@ -14,6 +14,8 @@ class CouponLog extends Model
 
     protected $table = 'coupon_log';
 
+    protected $guarded = [];
+
     public function coupon(): BelongsTo
     {
         return $this->belongsTo(User::class);

+ 5 - 0
app/Models/Goods.php

@@ -29,6 +29,11 @@ class Goods extends Model
         return $this->hasMany(Order::class);
     }
 
+    public function category(): BelongsTo
+    {
+        return $this->belongsTo(GoodsCategory::class, 'category_id');
+    }
+
     public function scopeType(Builder $query, int $type): Builder
     {
         return $query->whereType($type)->whereStatus(1)->orderByDesc('sort');

+ 20 - 0
app/Models/Node.php

@@ -37,6 +37,16 @@ class Node extends Model
         return $this->hasMany(NodeHeartbeat::class);
     }
 
+    public function latestHeartbeat(): HasOne
+    {
+        return $this->hasOne(NodeHeartbeat::class)->ofMany(
+            ['log_time' => 'max'],
+            function ($query) {
+                $query->where('log_time', '>=', strtotime(sysConfig('recently_heartbeat')));
+            }
+        );
+    }
+
     public function onlineIps(): HasMany
     {
         return $this->hasMany(NodeOnlineIp::class);
@@ -47,6 +57,16 @@ class Node extends Model
         return $this->hasMany(NodeOnlineLog::class);
     }
 
+    public function latestOnlineLog(): HasOne
+    {
+        return $this->hasOne(NodeOnlineLog::class)->ofMany(
+            ['log_time' => 'max'],
+            function ($query) {
+                $query->where('log_time', '>=', strtotime('-5 minutes'));
+            }
+        );
+    }
+
     public function userDataFlowLogs(): HasMany
     {
         return $this->hasMany(UserDataFlowLog::class);

+ 53 - 0
app/Models/NodeCertificate.php

@@ -2,6 +2,7 @@
 
 namespace App\Models;
 
+use Illuminate\Database\Eloquent\Casts\Attribute;
 use Illuminate\Database\Eloquent\Model;
 
 /**
@@ -12,4 +13,56 @@ class NodeCertificate extends Model
     protected $table = 'node_certificate';
 
     protected $guarded = [];
+
+    protected $appends = ['issuer', 'from', 'to'];
+
+    private $certInfo = null;
+
+    protected function getCertInfo(): ?array
+    {
+        if ($this->certInfo === null && $this->pem) {
+            $this->certInfo = openssl_x509_parse($this->pem) ?: false;
+        }
+
+        return $this->certInfo ?: null;
+    }
+
+    protected function issuer(): Attribute
+    {
+        return Attribute::make(
+            get: function () {
+                $certInfo = $this->getCertInfo();
+
+                return $certInfo ? ($certInfo['issuer']['O'] ?? null) : null;
+            }
+        );
+    }
+
+    protected function from(): Attribute
+    {
+        return Attribute::make(
+            get: function () {
+                $certInfo = $this->getCertInfo();
+                if ($certInfo && isset($certInfo['validFrom_time_t'])) {
+                    return date('Y-m-d', $certInfo['validFrom_time_t']);
+                }
+
+                return null;
+            }
+        );
+    }
+
+    protected function to(): Attribute
+    {
+        return Attribute::make(
+            get: function () {
+                $certInfo = $this->getCertInfo();
+                if ($certInfo && isset($certInfo['validTo_time_t'])) {
+                    return date('Y-m-d', $certInfo['validTo_time_t']);
+                }
+
+                return null;
+            }
+        );
+    }
 }

+ 0 - 6
app/Models/NodeHeartbeat.php

@@ -2,7 +2,6 @@
 
 namespace App\Models;
 
-use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Model;
 
 /**
@@ -15,9 +14,4 @@ class NodeHeartbeat extends Model
     protected $table = 'node_heartbeat';
 
     protected $guarded = [];
-
-    public function scopeRecently(Builder $query): Builder
-    {
-        return $query->where('log_time', '>=', strtotime('-'.sysConfig('recently_heartbeat').' minutes'))->latest('log_time');
-    }
 }

+ 3 - 1
app/Models/NotificationLog.php

@@ -16,6 +16,8 @@ class NotificationLog extends Model
     // 通知类型
     public function getTypeLabelAttribute(): string
     {
-        return config('common.notification.labels')[$this->type] ?? trans('common.status.unknown');
+        $type = config('common.notification.labels')[$this->type];
+
+        return trans("admin.system.notification.channel.{$type}");
     }
 }

+ 5 - 0
app/Models/User.php

@@ -113,6 +113,11 @@ class User extends Authenticatable
         return $this->HasMany(UserLoginLog::class);
     }
 
+    public function latestLoginLog(): HasOne
+    {
+        return $this->hasOne(UserLoginLog::class)->latestOfMany();
+    }
+
     public function subscribe(): HasOne
     {
         return $this->hasOne(UserSubscribe::class);

+ 2 - 2
app/Services/OrderService.php

@@ -71,7 +71,7 @@ class OrderService
     private function activatePackage(): bool
     { // 激活流量包
         if (self::$user->incrementData(self::$goods->traffic * MiB)) {
-            return Helpers::addUserTrafficModifyLog($this->order->user_id, self::$user->transfer_enable - self::$goods->traffic * MiB, self::$user->transfer_enable, trans('[:payment] plus the user’s purchased data plan.', ['payment' => $this->order->pay_way]));
+            return Helpers::addUserTrafficModifyLog($this->order->user_id, self::$user->transfer_enable - self::$goods->traffic * MiB, self::$user->transfer_enable, trans("[:payment] plus the user's purchased data plan.", ['payment' => $this->order->pay_way]));
         }
 
         return false;
@@ -95,7 +95,7 @@ class OrderService
         }
 
         if (self::$user->update($updateData)) {
-            return Helpers::addUserTrafficModifyLog($this->order->user_id, $oldData, self::$user->transfer_enable, trans('[:payment] plus the user’s purchased data plan.', ['payment' => $this->order->pay_way]), $this->order->id);
+            return Helpers::addUserTrafficModifyLog($this->order->user_id, $oldData, self::$user->transfer_enable, trans("[:payment] plus the user's purchased data plan.", ['payment' => $this->order->pay_way]), $this->order->id);
         }
 
         return false;

+ 2 - 1
app/Services/ProxyService.php

@@ -8,6 +8,7 @@ use App\Utils\Clients\Protocols\Text;
 use App\Utils\Clients\Protocols\URLSchemes;
 use Arr;
 use Exception;
+use Illuminate\Database\Eloquent\Collection;
 use ReflectionClass;
 use RuntimeException;
 
@@ -50,7 +51,7 @@ class ProxyService
         return self::$servers ?? [];
     }
 
-    public function getNodeList(?int $type = null, bool $isConfig = true): array
+    public function getNodeList(?int $type = null, bool $isConfig = true): array|Collection
     {
         $query = $this->getUser()->nodes()->whereIn('is_display', [2, 3]); // 获取这个账号可用节点
 

+ 35 - 70
app/Utils/Helpers.php

@@ -19,27 +19,11 @@ class Helpers
 {
     private static array $denyPorts = [1068, 1109, 1434, 3127, 3128, 3129, 3130, 3332, 4444, 5554, 6669, 8080, 8081, 8082, 8181, 8282, 9996, 17185, 24554, 35601, 60177, 60179]; // 不生成的端口
 
-    public static function methodList()
-    { // 加密方式
-        return SsConfig::type(1)->get();
-    }
-
-    public static function protocolList()
-    { // 协议
-        return SsConfig::type(2)->get();
-    }
-
-    public static function obfsList()
-    { // 混淆
-        return SsConfig::type(3)->get();
-    }
-
     public static function makeSubscribeCode(): string
     { // 生成用户的订阅码
-        $code = Str::random();
-        if (UserSubscribe::whereCode($code)->exists()) {
-            $code = self::makeSubscribeCode();
-        }
+        do {
+            $code = Str::random();
+        } while (UserSubscribe::whereCode($code)->exists());
 
         return $code;
     }
@@ -91,8 +75,14 @@ class Helpers
         }
 
         if ($isRandPort) {
+            $attempts = 0;
             do {
                 $port = random_int($minPort, $maxPort);
+                $attempts++;
+                // 防止无限循环
+                if ($attempts > 100) {
+                    throw new RuntimeException('Unable to find available port after 100 attempts.');
+                }
             } while (in_array($port, $occupiedPorts, true));
         } else {
             $port = $minPort;
@@ -109,23 +99,38 @@ class Helpers
 
     public static function getDefaultMethod(): string
     { // 获取默认加密方式
-        $config = SsConfig::default()->type(1)->first();
+        static $method = null;
+
+        if ($method === null) {
+            $config = SsConfig::default()->type(1)->first();
+            $method = $config->name ?? 'aes-256-cfb';
+        }
 
-        return $config->name ?? 'aes-256-cfb';
+        return $method;
     }
 
     public static function getDefaultProtocol(): string
     { // 获取默认协议
-        $config = SsConfig::default()->type(2)->first();
+        static $protocol = null;
 
-        return $config->name ?? 'origin';
+        if ($protocol === null) {
+            $config = SsConfig::default()->type(2)->first();
+            $protocol = $config->name ?? 'origin';
+        }
+
+        return $protocol;
     }
 
     public static function getDefaultObfs(): string
     { // 获取默认混淆
-        $config = SsConfig::default()->type(3)->first();
+        static $obfs = null;
 
-        return $config->name ?? 'plain';
+        if ($obfs === null) {
+            $config = SsConfig::default()->type(3)->first();
+            $obfs = $config->name ?? 'plain';
+        }
+
+        return $obfs;
     }
 
     /**
@@ -141,17 +146,7 @@ class Helpers
      */
     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;
-        $log->msg_id = $msgId;
-        $log->address = $address;
-        $log->title = $title;
-        $log->content = $content;
-        $log->status = $status;
-        $log->error = $error;
-        $log->save();
-
-        return $log->id;
+        return NotificationLog::create(['type' => $type, 'msg_id' => $msgId, 'address' => $address, 'title' => $title, 'content' => $content, 'status' => $status, 'error' => $error])->id;
     }
 
     /**
@@ -164,13 +159,7 @@ class Helpers
      */
     public static function addCouponLog(string $description, int $couponId, ?int $goodsId = null, ?int $orderId = null): bool
     {
-        $log = new CouponLog;
-        $log->coupon_id = $couponId;
-        $log->goods_id = $goodsId;
-        $log->order_id = $orderId;
-        $log->description = $description;
-
-        return $log->save();
+        return CouponLog::create(['coupon_id' => $couponId, 'goods_id' => $goodsId, 'order_id' => $orderId, 'description' => $description])->wasRecentlyCreated;
     }
 
     /**
@@ -185,16 +174,7 @@ class Helpers
      */
     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;
-        $log->order_id = $orderId;
-        $log->before = $before;
-        $log->after = $after;
-        $log->amount = $amount;
-        $log->description = $description;
-        $log->created_at = now();
-
-        return $log->save();
+        return UserCreditLog::create(['user_id' => $userId, 'order_id' => $orderId, 'before' => $before, 'after' => $after, 'amount' => $amount, 'description' => $description, 'created_at' => now()])->wasRecentlyCreated;
     }
 
     /**
@@ -208,14 +188,7 @@ class Helpers
      */
     public static function addUserTrafficModifyLog(int $userId, int $before, int $after, ?string $description = null, ?int $orderId = null): bool
     {
-        $log = new UserDataModifyLog;
-        $log->user_id = $userId;
-        $log->order_id = $orderId;
-        $log->before = $before;
-        $log->after = $after;
-        $log->description = $description;
-
-        return $log->save();
+        return UserDataModifyLog::create(['user_id' => $userId, 'order_id' => $orderId, 'before' => $before, 'after' => $after, 'description' => $description])->wasRecentlyCreated;
     }
 
     /**
@@ -230,15 +203,7 @@ class Helpers
      */
     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;
-        $marketing->receiver = $receiver;
-        $marketing->title = $title;
-        $marketing->content = $content;
-        $marketing->error = $error;
-        $marketing->status = $status;
-
-        return $marketing->save();
+        return Marketing::create(['type' => $type, 'receiver' => $receiver, 'title' => $title, 'content' => $content, 'error' => $error, 'status' => $status])->wasRecentlyCreated;
     }
 
     /**

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

@@ -70,7 +70,7 @@ class PaymentManager
 
     public static function getLabels(bool $history = false): array
     {
-        return cache()->rememberForever('payment_labels', function () use ($history) {
+        return cache()->rememberForever('payment_labels'.app()->getLocale(), function () use ($history) {
             if ($history) {
                 $labels = [
                     'bitpayx' => trans('admin.system.payment.channel.bitpayx'),

+ 0 - 23
app/View/Components/Alert.php

@@ -1,23 +0,0 @@
-<?php
-
-namespace App\View\Components;
-
-use Illuminate\View\Component;
-
-class Alert extends Component
-{
-    public $type;
-
-    public $message;
-
-    public function __construct($type, $message)
-    {
-        $this->type = $type;
-        $this->message = $message;
-    }
-
-    public function render()
-    {
-        return view('components.alert');
-    }
-}

+ 29 - 4
app/helpers.php

@@ -49,8 +49,11 @@ if (! function_exists('formatBytes')) {
 
 // 秒转时间
 if (! function_exists('formatTime')) {
-    function formatTime(int $seconds): string
+    function formatTime(?int $seconds): string
     {
+        if (! $seconds) {
+            return '-';
+        }
         $interval = CarbonInterval::seconds($seconds);
 
         return $interval->cascade()->forHumans();
@@ -101,10 +104,32 @@ if (! function_exists('string_urlsafe')) {
 if (! function_exists('localized_date')) {
     function localized_date($date): string
     {
-        $locale = app()->getLocale();
+        if (! $date) {
+            return '';
+        }
+
         $carbon = Carbon::parse($date);
-        $format = config("common.language.$locale.3") ?? 'Y-m-d';
+        $locale = app()->getLocale();
+        $carbon->setLocale($locale);
+
+        // 获取原始字符串表示
+        $dateStr = is_string($date) ? $date : $date->format('Y-m-d H:i:s');
+
+        // 使用正则检测精度
+        if (preg_match('/(\d{4}-\d{2}-\d{2}) (\d{2}):(\d{2}):(\d{2})/', $dateStr, $matches)) {
+            $hours = (int) $matches[2];
+            $minutes = (int) $matches[3];
+            $seconds = (int) $matches[4];
+
+            if ($seconds > 0) {
+                return $carbon->isoFormat('LL LTS'); // 显示完整时间
+            }
+
+            if ($minutes > 0 || $hours > 0) {
+                return $carbon->isoFormat('LL LT'); // 显示到分钟
+            }
+        }
 
-        return $carbon->format($format);
+        return $carbon->isoFormat('LL'); // 只显示日期
     }
 }

+ 1 - 3
composer.json

@@ -78,9 +78,7 @@
       "@php artisan package:discover --ansi"
     ],
     "post-update-cmd": [
-      "@php artisan vendor:publish --tag=laravel-assets --ansi --force",
-      "@php artisan ide-helper:generate",
-      "@php artisan ide-helper:meta"
+      "@php artisan vendor:publish --tag=laravel-assets --ansi --force"
     ],
     "post-root-package-install": [
       "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""

+ 18 - 26
config/common.php

@@ -38,38 +38,30 @@ return [
             'telegram' => 'fa-telegram',
         ],
     ],
-
-    'network_status' => [
-        1 => '✔️正 常',
-        2 => '🛑 海外阻断',
-        3 => '🛑 国内阻断',
-        4 => '❌ 断 连',
-    ],
-
     'notification' => [
         'labels' => [
-            1 => '邮件',
-            2 => 'ServerChan',
-            3 => 'Bark',
-            4 => 'Telegram',
-            5 => '微信企业',
-            6 => 'TG酱',
-            7 => 'PushPlus',
-            8 => '爱语飞飞',
-            9 => 'PushDear',
-            10 => '钉钉',
+            1 => 'email',
+            2 => 'serverchan',
+            3 => 'bark',
+            4 => 'telegram',
+            5 => 'wechat',
+            6 => 'tg_chat',
+            7 => 'pushplus',
+            8 => 'iyuu',
+            9 => 'pushdeer',
+            10 => 'dingtalk',
         ],
     ],
 
     'language' => [
-        'de' => ['Deutsch', 'de', 'de-DE', 'd.m.Y'],
-        'en' => ['English', 'us', 'en-US', 'F d, Y'],
-        'fa' => ['فارسی', 'ir', 'fa-IR', 'Y/m/d'],
-        'ja' => ['日本語', 'jp', 'ja-JP', 'Y年m月d日'],
-        'ko' => ['한국어', 'kr', 'ko-KR', 'Y년 m월 d일'],
-        'vi' => ['Tiếng Việt', 'vn', 'vi-VN', 'd/m/Y'],
-        'zh_CN' => ['简体中文', 'cn', 'zh-CN', 'Y年m月d日'],
-        'ru' => ['Русский', 'ru', 'ru', 'd.m.Y'],
+        'de' => ['Deutsch', 'de', 'de-DE'],
+        'en' => ['English', 'us', 'en-US'],
+        'fa' => ['فارسی', 'ir', 'fa-IR'],
+        'ja' => ['日本語', 'jp', 'ja-JP'],
+        'ko' => ['한국어', 'kr', 'ko-KR'],
+        'vi' => ['Tiếng Việt', 'vn', 'vi-VN'],
+        'zh_CN' => ['简体中文', 'cn', 'zh-CN'],
+        'ru' => ['Русский', 'ru', 'ru'],
     ],
 
     'currency' => [

File diff suppressed because it is too large
+ 0 - 0
public/assets/bundle/app.min.css


+ 3 - 1
public/assets/global/js/Plugin/bootstrap-datepicker.js

@@ -39,7 +39,9 @@
       key: "getDefaults",
       value: function getDefaults() {
         return {
-          autoclose: true
+          language: document.documentElement.lang || 'en',
+          autoclose: true,
+          todayHighlight: true
         };
       }
     }]);

File diff suppressed because it is too large
+ 2 - 2
public/assets/global/vendor/bootstrap-datepicker/bootstrap-datepicker.min.js


+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker-en-CA.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates["en-CA"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:0,format:"yyyy-mm-dd"},a.fn.datepicker.deprecated("This filename doesn't follow the convention, use bootstrap-datepicker.en-CA.js instead.")}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.ar-DZ.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates["ar-DZ"]={days:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت","الأحد"],daysShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت","أحد"],daysMin:["ح","ن","ث","ع","خ","ج","س","ح"],months:["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthsShort:["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],today:"هذا اليوم",rtl:!0,monthsTitle:"أشهر",clear:"إزالة",format:"yyyy/mm/dd",weekStart:0}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.ar-tn.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates["ar-tn"]={days:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت","الأحد"],daysShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت","أحد"],daysMin:["ح","ن","ث","ع","خ","ج","س","ح"],months:["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthsShort:["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],today:"هذا اليوم",rtl:!0}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.ar.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.ar={days:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت","الأحد"],daysShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت","أحد"],daysMin:["ح","ن","ث","ع","خ","ج","س","ح"],months:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthsShort:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],today:"هذا اليوم",rtl:!0}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.az.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.az={days:["Bazar","Bazar ertəsi","Çərşənbə axşamı","Çərşənbə","Cümə axşamı","Cümə","Şənbə"],daysShort:["B.","B.e","Ç.a","Ç.","C.a","C.","Ş."],daysMin:["B.","B.e","Ç.a","Ç.","C.a","C.","Ş."],months:["Yanvar","Fevral","Mart","Aprel","May","İyun","İyul","Avqust","Sentyabr","Oktyabr","Noyabr","Dekabr"],monthsShort:["Yan","Fev","Mar","Apr","May","İyun","İyul","Avq","Sen","Okt","Noy","Dek"],today:"Bu gün",weekStart:1,clear:"Təmizlə",monthsTitle:"Aylar"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.bg.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.bg={days:["Неделя","Понеделник","Вторник","Сряда","Четвъртък","Петък","Събота"],daysShort:["Нед","Пон","Вто","Сря","Чет","Пет","Съб"],daysMin:["Н","П","В","С","Ч","П","С"],months:["Януари","Февруари","Март","Април","Май","Юни","Юли","Август","Септември","Октомври","Ноември","Декември"],monthsShort:["Ян","Фев","Мар","Апр","Май","Юни","Юли","Авг","Сеп","Окт","Ное","Дек"],today:"днес"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.bm.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.bm={days:["Kari","Ntɛnɛn","Tarata","Araba","Alamisa","Juma","Sibiri"],daysShort:["Kar","Ntɛ","Tar","Ara","Ala","Jum","Sib"],daysMin:["Ka","Nt","Ta","Ar","Al","Ju","Si"],months:["Zanwuyekalo","Fewuruyekalo","Marisikalo","Awirilikalo","Mɛkalo","Zuwɛnkalo","Zuluyekalo","Utikalo","Sɛtanburukalo","ɔkutɔburukalo","Nowanburukalo","Desanburukalo"],monthsShort:["Zan","Few","Mar","Awi","Mɛ","Zuw","Zul","Uti","Sɛt","ɔku","Now","Des"],today:"Bi",monthsTitle:"Kalo",clear:"Ka jɔsi",weekStart:1,format:"dd/mm/yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.bn.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.bn={days:["রবিবার","সোমবার","মঙ্গলবার","বুধবার","বৃহস্পতিবার","শুক্রবার","শনিবার"],daysShort:["রবিবার","সোমবার","মঙ্গলবার","বুধবার","বৃহস্পতিবার","শুক্রবার","শনিবার"],daysMin:["রবি","সোম","মঙ্গল","বুধ","বৃহস্পতি","শুক্র","শনি"],months:["জানুয়ারী","ফেব্রুয়ারি","মার্চ","এপ্রিল","মে","জুন","জুলাই","অগাস্ট","সেপ্টেম্বর","অক্টোবর","নভেম্বর","ডিসেম্বর"],monthsShort:["জানুয়ারী","ফেব্রুয়ারি","মার্চ","এপ্রিল","মে","জুন","জুলাই","অগাস্ট","সেপ্টেম্বর","অক্টোবর","নভেম্বর","ডিসেম্বর"],today:"আজ",monthsTitle:"মাস",clear:"পরিষ্কার",weekStart:0,format:"mm/dd/yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.br.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.br={days:["Sul","Lun","Meurzh","Merc'her","Yaou","Gwener","Sadorn"],daysShort:["Sul","Lun","Meu.","Mer.","Yao.","Gwe.","Sad."],daysMin:["Su","L","Meu","Mer","Y","G","Sa"],months:["Genver","C'hwevrer","Meurzh","Ebrel","Mae","Mezheven","Gouere","Eost","Gwengolo","Here","Du","Kerzu"],monthsShort:["Genv.","C'hw.","Meur.","Ebre.","Mae","Mezh.","Goue.","Eost","Gwen.","Here","Du","Kerz."],today:"Hiziv",monthsTitle:"Miz",clear:"Dilemel",weekStart:1,format:"dd/mm/yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.bs.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.bs={days:["Nedjelja","Ponedjeljak","Utorak","Srijeda","Četvrtak","Petak","Subota"],daysShort:["Ned","Pon","Uto","Sri","Čet","Pet","Sub"],daysMin:["N","Po","U","Sr","Č","Pe","Su"],months:["Januar","Februar","Mart","April","Maj","Juni","Juli","August","Septembar","Oktobar","Novembar","Decembar"],monthsShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],today:"Danas",weekStart:1,format:"dd.mm.yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.ca.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.ca={days:["diumenge","dilluns","dimarts","dimecres","dijous","divendres","dissabte"],daysShort:["dg.","dl.","dt.","dc.","dj.","dv.","ds."],daysMin:["dg","dl","dt","dc","dj","dv","ds"],months:["gener","febrer","març","abril","maig","juny","juliol","agost","setembre","octubre","novembre","desembre"],monthsShort:["gen.","febr.","març","abr.","maig","juny","jul.","ag.","set.","oct.","nov.","des."],today:"Avui",monthsTitle:"Mesos",clear:"Esborra",weekStart:1,format:"dd/mm/yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.cs.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.cs={days:["Neděle","Pondělí","Úterý","Středa","Čtvrtek","Pátek","Sobota"],daysShort:["Ned","Pon","Úte","Stř","Čtv","Pát","Sob"],daysMin:["Ne","Po","Út","St","Čt","Pá","So"],months:["Leden","Únor","Březen","Duben","Květen","Červen","Červenec","Srpen","Září","Říjen","Listopad","Prosinec"],monthsShort:["Led","Úno","Bře","Dub","Kvě","Čer","Čnc","Srp","Zář","Říj","Lis","Pro"],today:"Dnes",clear:"Vymazat",monthsTitle:"Měsíc",weekStart:1,format:"dd.mm.yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.cy.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.cy={days:["Sul","Llun","Mawrth","Mercher","Iau","Gwener","Sadwrn"],daysShort:["Sul","Llu","Maw","Mer","Iau","Gwe","Sad"],daysMin:["Su","Ll","Ma","Me","Ia","Gwe","Sa"],months:["Ionawr","Chewfror","Mawrth","Ebrill","Mai","Mehefin","Gorfennaf","Awst","Medi","Hydref","Tachwedd","Rhagfyr"],monthsShort:["Ion","Chw","Maw","Ebr","Mai","Meh","Gor","Aws","Med","Hyd","Tach","Rha"],today:"Heddiw"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.da.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.da={days:["Søndag","Mandag","Tirsdag","Onsdag","Torsdag","Fredag","Lørdag"],daysShort:["Søn","Man","Tir","Ons","Tor","Fre","Lør"],daysMin:["Sø","Ma","Ti","On","To","Fr","Lø"],months:["Januar","Februar","Marts","April","Maj","Juni","Juli","August","September","Oktober","November","December"],monthsShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],today:"I Dag",weekStart:1,clear:"Nulstil",format:"dd/mm/yyyy",monthsTitle:"Måneder"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.de.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.de={days:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],daysShort:["So","Mo","Di","Mi","Do","Fr","Sa"],daysMin:["So","Mo","Di","Mi","Do","Fr","Sa"],months:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthsShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],today:"Heute",monthsTitle:"Monate",clear:"Löschen",weekStart:1,format:"dd.mm.yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.el.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.el={days:["Κυριακή","Δευτέρα","Τρίτη","Τετάρτη","Πέμπτη","Παρασκευή","Σάββατο"],daysShort:["Κυρ","Δευ","Τρι","Τετ","Πεμ","Παρ","Σαβ"],daysMin:["Κυ","Δε","Τρ","Τε","Πε","Πα","Σα"],months:["Ιανουάριος","Φεβρουάριος","Μάρτιος","Απρίλιος","Μάιος","Ιούνιος","Ιούλιος","Αύγουστος","Σεπτέμβριος","Οκτώβριος","Νοέμβριος","Δεκέμβριος"],monthsShort:["Ιαν","Φεβ","Μαρ","Απρ","Μάι","Ιουν","Ιουλ","Αυγ","Σεπ","Οκτ","Νοε","Δεκ"],today:"Σήμερα",clear:"Καθαρισμός",weekStart:1,format:"d/m/yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.en-AU.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates["en-AU"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:1,format:"d/mm/yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.en-CA.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates["en-CA"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:0,format:"yyyy-mm-dd"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.en-GB.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates["en-GB"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:1,format:"dd/mm/yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.en-IE.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates["en-IE"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:1,format:"dd/mm/yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.en-NZ.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates["en-NZ"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:1,format:"d/mm/yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.en-US.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates["en-US"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:0,format:"m/d/yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.en-ZA.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates["en-ZA"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:1,format:"yyyy/mm/d"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.eo.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.eo={days:["dimanĉo","lundo","mardo","merkredo","ĵaŭdo","vendredo","sabato"],daysShort:["dim.","lun.","mar.","mer.","ĵaŭ.","ven.","sam."],daysMin:["d","l","ma","me","ĵ","v","s"],months:["januaro","februaro","marto","aprilo","majo","junio","julio","aŭgusto","septembro","oktobro","novembro","decembro"],monthsShort:["jan.","feb.","mar.","apr.","majo","jun.","jul.","aŭg.","sep.","okt.","nov.","dec."],today:"Hodiaŭ",clear:"Nuligi",weekStart:1,format:"yyyy-mm-dd"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.es.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.es={days:["Domingo","Lunes","Martes","Miércoles","Jueves","Viernes","Sábado"],daysShort:["Dom","Lun","Mar","Mié","Jue","Vie","Sáb"],daysMin:["Do","Lu","Ma","Mi","Ju","Vi","Sa"],months:["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"],monthsShort:["Ene","Feb","Mar","Abr","May","Jun","Jul","Ago","Sep","Oct","Nov","Dic"],today:"Hoy",monthsTitle:"Meses",clear:"Borrar",weekStart:1,format:"dd/mm/yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.et.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.et={days:["Pühapäev","Esmaspäev","Teisipäev","Kolmapäev","Neljapäev","Reede","Laupäev"],daysShort:["Pühap","Esmasp","Teisip","Kolmap","Neljap","Reede","Laup"],daysMin:["P","E","T","K","N","R","L"],months:["Jaanuar","Veebruar","Märts","Aprill","Mai","Juuni","Juuli","August","September","Oktoober","November","Detsember"],monthsShort:["Jaan","Veebr","Märts","Apr","Mai","Juuni","Juuli","Aug","Sept","Okt","Nov","Dets"],today:"Täna",clear:"Tühjenda",weekStart:1,format:"dd.mm.yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.eu.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.eu={days:["Igandea","Astelehena","Asteartea","Asteazkena","Osteguna","Ostirala","Larunbata"],daysShort:["Ig","Al","Ar","Az","Og","Ol","Lr"],daysMin:["Ig","Al","Ar","Az","Og","Ol","Lr"],months:["Urtarrila","Otsaila","Martxoa","Apirila","Maiatza","Ekaina","Uztaila","Abuztua","Iraila","Urria","Azaroa","Abendua"],monthsShort:["Urt","Ots","Mar","Api","Mai","Eka","Uzt","Abu","Ira","Urr","Aza","Abe"],today:"Gaur",monthsTitle:"Hilabeteak",clear:"Ezabatu",weekStart:1,format:"yyyy/mm/dd"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.fa.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.fa={days:["یک‌شنبه","دوشنبه","سه‌شنبه","چهارشنبه","پنج‌شنبه","جمعه","شنبه","یک‌شنبه"],daysShort:["یک","دو","سه","چهار","پنج","جمعه","شنبه","یک"],daysMin:["ی","د","س","چ","پ","ج","ش","ی"],months:["ژانویه","فوریه","مارس","آوریل","مه","ژوئن","ژوئیه","اوت","سپتامبر","اکتبر","نوامبر","دسامبر"],monthsShort:["ژان","فور","مار","آور","مه","ژون","ژوی","اوت","سپت","اکت","نوا","دسا"],today:"امروز",clear:"پاک کن",weekStart:1,format:"yyyy/mm/dd"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.fi.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.fi={days:["sunnuntai","maanantai","tiistai","keskiviikko","torstai","perjantai","lauantai"],daysShort:["sun","maa","tii","kes","tor","per","lau"],daysMin:["su","ma","ti","ke","to","pe","la"],months:["tammikuu","helmikuu","maaliskuu","huhtikuu","toukokuu","kesäkuu","heinäkuu","elokuu","syyskuu","lokakuu","marraskuu","joulukuu"],monthsShort:["tammi","helmi","maalis","huhti","touko","kesä","heinä","elo","syys","loka","marras","joulu"],today:"tänään",clear:"Tyhjennä",weekStart:1,format:"d.m.yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.fo.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.fo={days:["Sunnudagur","Mánadagur","Týsdagur","Mikudagur","Hósdagur","Fríggjadagur","Leygardagur"],daysShort:["Sun","Mán","Týs","Mik","Hós","Frí","Ley"],daysMin:["Su","Má","Tý","Mi","Hó","Fr","Le"],months:["Januar","Februar","Marts","Apríl","Mei","Juni","Juli","August","Septembur","Oktobur","Novembur","Desembur"],monthsShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Aug","Sep","Okt","Nov","Des"],today:"Í Dag",clear:"Reinsa"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.fr-CH.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.fr={days:["Dimanche","Lundi","Mardi","Mercredi","Jeudi","Vendredi","Samedi"],daysShort:["Dim","Lun","Mar","Mer","Jeu","Ven","Sam"],daysMin:["D","L","Ma","Me","J","V","S"],months:["Janvier","Février","Mars","Avril","Mai","Juin","Juillet","Août","Septembre","Octobre","Novembre","Décembre"],monthsShort:["Jan","Fév","Mar","Avr","Mai","Jui","Jul","Aou","Sep","Oct","Nov","Déc"],today:"Aujourd'hui",monthsTitle:"Mois",clear:"Effacer",weekStart:1,format:"dd.mm.yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.fr.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.fr={days:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],daysShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],daysMin:["d","l","ma","me","j","v","s"],months:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthsShort:["janv.","févr.","mars","avril","mai","juin","juil.","août","sept.","oct.","nov.","déc."],today:"Aujourd'hui",monthsTitle:"Mois",clear:"Effacer",weekStart:1,format:"dd/mm/yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.gl.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.gl={days:["Domingo","Luns","Martes","Mércores","Xoves","Venres","Sábado"],daysShort:["Dom","Lun","Mar","Mér","Xov","Ven","Sáb"],daysMin:["Do","Lu","Ma","Me","Xo","Ve","Sa"],months:["Xaneiro","Febreiro","Marzo","Abril","Maio","Xuño","Xullo","Agosto","Setembro","Outubro","Novembro","Decembro"],monthsShort:["Xan","Feb","Mar","Abr","Mai","Xun","Xul","Ago","Sep","Out","Nov","Dec"],today:"Hoxe",clear:"Limpar",weekStart:1,format:"dd/mm/yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.he.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.he={days:["ראשון","שני","שלישי","רביעי","חמישי","שישי","שבת","ראשון"],daysShort:["א","ב","ג","ד","ה","ו","ש","א"],daysMin:["א","ב","ג","ד","ה","ו","ש","א"],months:["ינואר","פברואר","מרץ","אפריל","מאי","יוני","יולי","אוגוסט","ספטמבר","אוקטובר","נובמבר","דצמבר"],monthsShort:["ינו","פבר","מרץ","אפר","מאי","יונ","יול","אוג","ספט","אוק","נוב","דצמ"],today:"היום",rtl:!0}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.hi.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.hi={days:["रविवार","सोमवार","मंगलवार","बुधवार","गुरुवार","शुक्रवार","शनिवार"],daysShort:["सूर्य","सोम","मंगल","बुध","गुरु","शुक्र","शनि"],daysMin:["र","सो","मं","बु","गु","शु","श"],months:["जनवरी","फ़रवरी","मार्च","अप्रैल","मई","जून","जुलाई","अगस्त","सितम्बर","अक्टूबर","नवंबर","दिसम्बर"],monthsShort:["जन","फ़रवरी","मार्च","अप्रैल","मई","जून","जुलाई","अगस्त","सितं","अक्टूबर","नवं","दिसम्बर"],today:"आज",monthsTitle:"महीने",clear:"साफ",weekStart:1,format:"dd / mm / yyyy"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.hr.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.hr={days:["Nedjelja","Ponedjeljak","Utorak","Srijeda","Četvrtak","Petak","Subota"],daysShort:["Ned","Pon","Uto","Sri","Čet","Pet","Sub"],daysMin:["Ne","Po","Ut","Sr","Če","Pe","Su"],months:["Siječanj","Veljača","Ožujak","Travanj","Svibanj","Lipanj","Srpanj","Kolovoz","Rujan","Listopad","Studeni","Prosinac"],monthsShort:["Sij","Velj","Ožu","Tra","Svi","Lip","Srp","Kol","Ruj","Lis","Stu","Pro"],today:"Danas"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.hu.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.hu={days:["vasárnap","hétfő","kedd","szerda","csütörtök","péntek","szombat"],daysShort:["vas","hét","ked","sze","csü","pén","szo"],daysMin:["V","H","K","Sze","Cs","P","Szo"],months:["január","február","március","április","május","június","július","augusztus","szeptember","október","november","december"],monthsShort:["jan","feb","már","ápr","máj","jún","júl","aug","sze","okt","nov","dec"],today:"ma",weekStart:1,clear:"töröl",titleFormat:"yyyy. MM",format:"yyyy.mm.dd"}}(jQuery);

+ 1 - 0
public/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.hy.min.js

@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates.hy={days:["Կիրակի","Երկուշաբթի","Երեքշաբթի","Չորեքշաբթի","Հինգշաբթի","Ուրբաթ","Շաբաթ"],daysShort:["Կիր","Երկ","Երե","Չոր","Հին","Ուրբ","Շաբ"],daysMin:["Կի","Եկ","Եք","Չո","Հի","Ու","Շա"],months:["Հունվար","Փետրվար","Մարտ","Ապրիլ","Մայիս","Հունիս","Հուլիս","Օգոստոս","Սեպտեմբեր","Հոկտեմբեր","Նոյեմբեր","Դեկտեմբեր"],monthsShort:["Հնվ","Փետ","Մար","Ապր","Մայ","Հուն","Հուլ","Օգս","Սեպ","Հոկ","Նոյ","Դեկ"],today:"Այսօր",clear:"Ջնջել",format:"dd.mm.yyyy",weekStart:1,monthsTitle:"Ամիսնէր"}}(jQuery);

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