Explorar o código

Merge pull request #2135 from sspanel-uim/dev

Dev 20230820
M1Screw %!s(int64=2) %!d(string=hai) anos
pai
achega
bd38d7aede

+ 3 - 2
app/routes.php

@@ -54,13 +54,14 @@ return static function (Slim\App $app): void {
         $group->post('/username', App\Controllers\User\InfoController::class . ':updateUsername');
         $group->post('/unbind_im', App\Controllers\User\InfoController::class . ':unbindIM');
         $group->post('/password', App\Controllers\User\InfoController::class . ':updatePassword');
-        $group->post('/theme', App\Controllers\User\InfoController::class . ':updateTheme');
-        $group->post('/daily_mail', App\Controllers\User\InfoController::class . ':updateDailyMail');
         $group->post('/passwd_reset', App\Controllers\User\InfoController::class . ':resetPasswd');
         $group->post('/apitoken_reset', App\Controllers\User\InfoController::class . ':resetApiToken');
         $group->post('/method', App\Controllers\User\InfoController::class . ':updateMethod');
         $group->post('/url_reset', App\Controllers\User\InfoController::class . ':resetURL');
         $group->post('/invite_reset', App\Controllers\User\InfoController::class . ':resetInviteURL');
+        $group->post('/daily_mail', App\Controllers\User\InfoController::class . ':updateDailyMail');
+        $group->post('/contact_method', App\Controllers\User\InfoController::class . ':updateContactMethod');
+        $group->post('/theme', App\Controllers\User\InfoController::class . ':updateTheme');
         $group->post('/kill', App\Controllers\User\InfoController::class . ':sendToGulag');
         // 发送验证邮件
         $group->post('/send', App\Controllers\AuthController::class . ':sendVerify');

+ 40 - 32
composer.lock

@@ -123,16 +123,16 @@
         },
         {
             "name": "aws/aws-sdk-php",
-            "version": "3.278.3",
+            "version": "3.279.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/aws/aws-sdk-php.git",
-                "reference": "596534c0627d8b38597061341e99b460437d1a16"
+                "reference": "ebd5e47c5be0425bb5cf4f80737850ed74767107"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/596534c0627d8b38597061341e99b460437d1a16",
-                "reference": "596534c0627d8b38597061341e99b460437d1a16",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/ebd5e47c5be0425bb5cf4f80737850ed74767107",
+                "reference": "ebd5e47c5be0425bb5cf4f80737850ed74767107",
                 "shasum": ""
             },
             "require": {
@@ -141,11 +141,11 @@
                 "ext-pcre": "*",
                 "ext-simplexml": "*",
                 "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5",
-                "guzzlehttp/promises": "^1.4.0",
+                "guzzlehttp/promises": "^1.4.0 || ^2.0",
                 "guzzlehttp/psr7": "^1.9.1 || ^2.4.5",
                 "mtdowling/jmespath.php": "^2.6",
-                "php": ">=5.5",
-                "psr/http-message": "^1.0"
+                "php": ">=7.2.5",
+                "psr/http-message": "^1.0 || ^2.0"
             },
             "require-dev": {
                 "andrewsville/php-token-reflection": "^1.4",
@@ -160,7 +160,7 @@
                 "ext-sockets": "*",
                 "nette/neon": "^2.3",
                 "paragonie/random_compat": ">= 2",
-                "phpunit/phpunit": "^4.8.35 || ^5.6.3 || ^9.5",
+                "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5",
                 "psr/cache": "^1.0",
                 "psr/simple-cache": "^1.0",
                 "sebastian/comparator": "^1.2.3 || ^4.0",
@@ -212,9 +212,9 @@
             "support": {
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.278.3"
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.279.2"
             },
-            "time": "2023-08-15T18:07:55+00:00"
+            "time": "2023-08-18T18:13:09+00:00"
         },
         {
             "name": "bacon/bacon-qr-code",
@@ -980,29 +980,33 @@
         },
         {
             "name": "guzzlehttp/promises",
-            "version": "1.5.3",
+            "version": "2.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/promises.git",
-                "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e"
+                "reference": "111166291a0f8130081195ac4556a5587d7f1b5d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e",
-                "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e",
+                "url": "https://api.github.com/repos/guzzle/promises/zipball/111166291a0f8130081195ac4556a5587d7f1b5d",
+                "reference": "111166291a0f8130081195ac4556a5587d7f1b5d",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5"
+                "php": "^7.2.5 || ^8.0"
             },
             "require-dev": {
-                "symfony/phpunit-bridge": "^4.4 || ^5.1"
+                "bamarni/composer-bin-plugin": "^1.8.1",
+                "phpunit/phpunit": "^8.5.29 || ^9.5.23"
             },
             "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": false
+                }
+            },
             "autoload": {
-                "files": [
-                    "src/functions_include.php"
-                ],
                 "psr-4": {
                     "GuzzleHttp\\Promise\\": "src/"
                 }
@@ -1039,7 +1043,7 @@
             ],
             "support": {
                 "issues": "https://github.com/guzzle/promises/issues",
-                "source": "https://github.com/guzzle/promises/tree/1.5.3"
+                "source": "https://github.com/guzzle/promises/tree/2.0.1"
             },
             "funding": [
                 {
@@ -1055,7 +1059,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-05-21T12:31:43+00:00"
+            "time": "2023-08-03T15:11:55+00:00"
         },
         {
             "name": "guzzlehttp/psr7",
@@ -2243,16 +2247,16 @@
         },
         {
             "name": "moneyphp/money",
-            "version": "v4.1.1",
+            "version": "v4.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moneyphp/money.git",
-                "reference": "9682220995ffd396843be5b4ee1e5f2c2d6ecee2"
+                "reference": "f660ab7f1d7a4c2ffdd30f50c55ed2c95c26fc3f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/moneyphp/money/zipball/9682220995ffd396843be5b4ee1e5f2c2d6ecee2",
-                "reference": "9682220995ffd396843be5b4ee1e5f2c2d6ecee2",
+                "url": "https://api.github.com/repos/moneyphp/money/zipball/f660ab7f1d7a4c2ffdd30f50c55ed2c95c26fc3f",
+                "reference": "f660ab7f1d7a4c2ffdd30f50c55ed2c95c26fc3f",
                 "shasum": ""
             },
             "require": {
@@ -2326,9 +2330,9 @@
             ],
             "support": {
                 "issues": "https://github.com/moneyphp/money/issues",
-                "source": "https://github.com/moneyphp/money/tree/v4.1.1"
+                "source": "https://github.com/moneyphp/money/tree/v4.2.0"
             },
-            "time": "2023-04-11T09:18:34+00:00"
+            "time": "2023-08-16T14:31:24+00:00"
         },
         {
             "name": "mtdowling/jmespath.php",
@@ -2393,25 +2397,29 @@
         },
         {
             "name": "nesbot/carbon",
-            "version": "2.68.1",
+            "version": "2.69.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/briannesbitt/Carbon.git",
-                "reference": "4f991ed2a403c85efbc4f23eb4030063fdbe01da"
+                "reference": "4308217830e4ca445583a37d1bf4aff4153fa81c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4f991ed2a403c85efbc4f23eb4030063fdbe01da",
-                "reference": "4f991ed2a403c85efbc4f23eb4030063fdbe01da",
+                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4308217830e4ca445583a37d1bf4aff4153fa81c",
+                "reference": "4308217830e4ca445583a37d1bf4aff4153fa81c",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
                 "php": "^7.1.8 || ^8.0",
+                "psr/clock": "^1.0",
                 "symfony/polyfill-mbstring": "^1.0",
                 "symfony/polyfill-php80": "^1.16",
                 "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0"
             },
+            "provide": {
+                "psr/clock-implementation": "1.0"
+            },
             "require-dev": {
                 "doctrine/dbal": "^2.0 || ^3.1.4",
                 "doctrine/orm": "^2.7",
@@ -2491,7 +2499,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-06-20T18:29:04+00:00"
+            "time": "2023-08-03T09:00:52+00:00"
         },
         {
             "name": "nikic/fast-route",

+ 2 - 12
config/settings.json

@@ -1202,23 +1202,13 @@
     {
         "id": null,
         "item": "sign_up_for_free_traffic",
-        "value": "20",
+        "value": "0",
         "class": "register",
         "is_public": 0,
         "type": "int",
-        "default": "20",
+        "default": "0",
         "mark": "注册时赠送的流量(GB)"
     },
-    {
-        "id": null,
-        "item": "sign_up_for_free_time",
-        "value": "365",
-        "class": "register",
-        "is_public": 0,
-        "type": "int",
-        "default": "365",
-        "mark": "注册时设定的账户有效期(天)"
-    },
     {
         "id": null,
         "item": "connection_ip_limit",

+ 1 - 2
db/migrations/2023020100-init.php

@@ -287,10 +287,10 @@ return new class() implements MigrationInterface {
                 `is_admin` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '是否管理员',
                 `im_type` smallint(6) unsigned NOT NULL DEFAULT 0 COMMENT '联系方式类型',
                 `im_value` varchar(255) NOT NULL DEFAULT '' COMMENT '联系方式',
+                `contact_method` smallint(6) NOT NULL DEFAULT 1 COMMENT '偏好的联系方式',
                 `daily_mail_enable` tinyint(1) NOT NULL DEFAULT 0 COMMENT '每日报告开关',
                 `class` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT '等级',
                 `class_expire` datetime NOT NULL DEFAULT '1989-06-04 00:05:00' COMMENT '等级过期时间',
-                `expire_in` datetime NOT NULL DEFAULT '2199-01-01 00:00:00' COMMENT '账户过期时间',
                 `theme` varchar(255) NOT NULL DEFAULT 'tabler' COMMENT '网站主题',
                 `ga_token` varchar(255) NOT NULL DEFAULT '' COMMENT 'GA密钥',
                 `ga_enable` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT 'GA开关',
@@ -299,7 +299,6 @@ return new class() implements MigrationInterface {
                 `is_banned` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '是否封禁',
                 `banned_reason` varchar(255) NOT NULL DEFAULT '' COMMENT '封禁理由',
                 `is_shadow_banned` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '是否处于账户异常状态',
-                `telegram_id` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT 'Telegram ID',
                 `expire_notified` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '过期提醒',
                 `traffic_notified` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '流量提醒',
                 `forbidden_ip` varchar(255) NOT NULL DEFAULT '' COMMENT '禁止访问IP',

+ 0 - 2
db/migrations/2023060300-add_user_locale_and_update_data_type.php

@@ -34,7 +34,6 @@ return new class() implements MigrationInterface {
         ALTER TABLE user MODIFY COLUMN `im_value` varchar(255) NOT NULL DEFAULT '' COMMENT '联系方式';
         ALTER TABLE user MODIFY COLUMN `class` smallint(6) unsigned NOT NULL DEFAULT 0 COMMENT '等级';
         ALTER TABLE user MODIFY COLUMN `class_expire` datetime NOT NULL DEFAULT '1989-06-04 00:05:00' COMMENT '等级过期时间';
-        ALTER TABLE user MODIFY COLUMN `expire_in` datetime NOT NULL DEFAULT '2199-01-01 00:00:00' COMMENT '账户过期时间';
         ALTER TABLE user MODIFY COLUMN `theme` varchar(255) NOT NULL DEFAULT 'tabler' COMMENT '网站主题';
         ALTER TABLE user MODIFY COLUMN `ga_token` varchar(255) NOT NULL DEFAULT '' COMMENT 'GA密钥';
         ALTER TABLE user MODIFY COLUMN `ga_enable` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT 'GA开关';
@@ -42,7 +41,6 @@ return new class() implements MigrationInterface {
         ALTER TABLE user MODIFY COLUMN `node_group` smallint(6) unsigned NOT NULL DEFAULT 0 COMMENT '节点分组';
         ALTER TABLE user MODIFY COLUMN `is_banned` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '是否封禁';
         ALTER TABLE user MODIFY COLUMN `banned_reason` varchar(255) NOT NULL DEFAULT '' COMMENT '封禁理由';
-        UPDATE user SET telegram_id = 0 WHERE telegram_id IS NULL;
         ALTER TABLE user MODIFY COLUMN `telegram_id` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT 'Telegram ID';
         ALTER TABLE user MODIFY COLUMN `expire_notified` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '过期提醒';
         ALTER TABLE user MODIFY COLUMN `traffic_notified` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '流量提醒';

+ 26 - 0
db/migrations/2023081800-add_user_contact_method.php

@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+use App\Interfaces\MigrationInterface;
+use App\Services\DB;
+
+return new class() implements MigrationInterface {
+    public function up(): int
+    {
+        DB::getPdo()->exec("
+            ALTER TABLE user ADD COLUMN IF NOT EXISTS `contact_method` smallint(6) NOT NULL DEFAULT 1 COMMENT '偏好的联系方式';
+        ");
+
+        return 2023081800;
+    }
+
+    public function down(): int
+    {
+        DB::getPdo()->exec("
+            ALTER TABLE user DROP COLUMN IF EXISTS `contact_method`;
+        ");
+
+        return 2023080900;
+    }
+};

+ 28 - 0
db/migrations/2023082000-remove_user_expire_in.php

@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+use App\Interfaces\MigrationInterface;
+use App\Services\DB;
+
+return new class() implements MigrationInterface {
+    public function up(): int
+    {
+        DB::getPdo()->exec("
+            ALTER TABLE user DROP COLUMN IF EXISTS `expire_in`;
+            ALTER TABLE user DROP COLUMN IF EXISTS `telegram_id`;
+        ");
+
+        return 2023082000;
+    }
+
+    public function down(): int
+    {
+        DB::getPdo()->exec("
+            ALTER TABLE user ADD COLUMN IF NOT EXISTS `expire_in` datetime NOT NULL DEFAULT '2199-01-01 00:00:00' COMMENT '账户过期时间';
+            ALTER TABLE user ADD COLUMN IF NOT EXISTS `telegram_id` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT 'Telegram ID';
+        ");
+
+        return 2023081800;
+    }
+};

+ 0 - 6
resources/views/tabler/admin/setting/reg.tpl

@@ -113,12 +113,6 @@
                                         <input id="free_user_reset_bandwidth" type="text" class="form-control" value="{$settings['free_user_reset_bandwidth']}">
                                     </div>
                                 </div>
-                                <div class="form-group mb-3 row">
-                                    <label class="form-label col-3 col-form-label">注册时设定的账户有效期(天)</label>
-                                    <div class="col">
-                                        <input id="sign_up_for_free_time" type="text" class="form-control" value="{$settings['sign_up_for_free_time']}">
-                                    </div>
-                                </div>
                                 <div class="form-group mb-3 row">
                                     <label class="form-label col-3 col-form-label">注册时设定的等级</label>
                                     <div class="col">

+ 0 - 7
resources/views/tabler/admin/user/edit.tpl

@@ -76,13 +76,6 @@
                                         value="{$edit_user->class_expire}">
                                 </div>
                             </div>
-                            <div class="form-group mb-3 row">
-                                <label class="form-label col-4 col-form-label">账户过期时间</label>
-                                <div class="col">
-                                    <input id="expire_in" type="text" class="form-control"
-                                        value="{$edit_user->expire_in}">
-                                </div>
-                            </div>
                             <div class="form-group mb-3 row">
                                 <label class="form-label col-4 col-form-label">免费用户流量重置日</label>
                                 <div class="col">

+ 1 - 3
resources/views/tabler/live_chat.tpl

@@ -30,7 +30,6 @@
                   ["user_id","{$user->id}"],
                   ["user_class","{$user->class}"],
                   ["reg_email","{$user->email}"],
-                  ["expire_in","{$user->expire_in}"],
                   ["class_expire_time","{$user->class_expire}"],
                   ["available_traffic","{$user->unusedTraffic()}"],
                   ["balance","{$user->money}"]
@@ -47,7 +46,6 @@ window.__lc.params = [
     { name: '用户编号', value: '{$user->id}' },
     { name: '用户类别', value: '{$user->class}' },
     { name: '注册邮箱', value: '{$user->email}' },
-    { name: '到期时间', value: '{$user->expire_in}' },
     { name: '等级时间', value: '{$user->class_expire}' },
     { name: '剩余流量', value: '{$user->unusedTraffic()}' },
     { name: '账户余额', value: '{$user->money}' }
@@ -101,4 +99,4 @@ window.__lc.params = [
         ct.parentNode.insertBefore(nt, ct);
     })();
 </script>
-{/if}
+{/if}

+ 71 - 27
resources/views/tabler/user/edit.tpl

@@ -318,7 +318,7 @@
                                                                 邮件接收
                                                             </option>
                                                             <option value="2" {if $user->daily_mail_enable === 2}selected{/if}>
-                                                                Telegram Bot 接收
+                                                                IM 接收
                                                             </option>
                                                         </select>
                                                     </div>
@@ -331,6 +331,30 @@
                                                 </div>
                                             </div>
                                         </div>
+                                        <div class="col-sm-12 col-md-6">
+                                            <div class="card">
+                                                <div class="card-body">
+                                                    <h3 class="card-title">偏好的联系方式</h3>
+                                                    <p>当 IM 未绑定时站点依然会向账户邮箱发送通知信息</p>
+                                                    <div class="mb-3">
+                                                        <select id="contact-method" class="form-select">
+                                                            <option value="1" {if $user->contact_method === 1}selected{/if}>
+                                                                邮件
+                                                            </option>
+                                                            <option value="2" {if $user->contact_method === 2}selected{/if}>
+                                                                IM
+                                                            </option>
+                                                        </select>
+                                                    </div>
+                                                </div>
+                                                <div class="card-footer">
+                                                    <div class="d-flex">
+                                                        <a id="modify-contact-method"
+                                                           class="btn btn-primary ms-auto">修改</a>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                        </div>
                                         <div class="col-sm-12 col-md-6">
                                             <div class="card">
                                                 <div class="card-body">
@@ -587,19 +611,15 @@
             })
         });
 
-        $("#modify-user-theme").click(function() {
+        $("#reset-passwd").click(function() {
             $.ajax({
                 type: "POST",
-                url: "/user/theme",
+                url: "/user/passwd_reset",
                 dataType: "json",
-                data: {
-                    theme: $('#user-theme').val()
-                },
                 success: function(data) {
                     if (data.ret === 1) {
                         $('#success-message').text(data.msg);
                         $('#success-dialog').modal('show');
-                        window.setTimeout("location.reload()", {$config['jump_delay']});
                     } else {
                         $('#fail-message').text(data.msg);
                         $('#fail-dialog').modal('show');
@@ -608,13 +628,15 @@
             })
         });
 
-        $("#modify-daily-report").click(function() {
+        $("#modify-login-passwd").click(function() {
             $.ajax({
                 type: "POST",
-                url: "/user/daily_mail",
+                url: "/user/password",
                 dataType: "json",
                 data: {
-                    mail: $('#daily-report').val()
+                    pwd: $('#new-password').val(),
+                    repwd: $('#again-new-password').val(),
+                    oldpwd: $('#password').val()
                 },
                 success: function(data) {
                     if (data.ret === 1) {
@@ -628,10 +650,10 @@
             })
         });
 
-        $("#reset-passwd").click(function() {
+        $("#unbind-im").click(function() {
             $.ajax({
                 type: "POST",
-                url: "/user/passwd_reset",
+                url: "/user/unbind_im",
                 dataType: "json",
                 success: function(data) {
                     if (data.ret === 1) {
@@ -645,15 +667,30 @@
             })
         });
 
-        $("#modify-login-passwd").click(function() {
+        $("#reset-2fa").click(function() {
             $.ajax({
                 type: "POST",
-                url: "/user/password",
+                url: "/user/ga_reset",
+                dataType: "json",
+                success: function(data) {
+                    if (data.ret === 1) {
+                        $('#success-message').text(data.msg);
+                        $('#success-dialog').modal('show');
+                    } else {
+                        $('#fail-message').text(data.msg);
+                        $('#fail-dialog').modal('show');
+                    }
+                }
+            })
+        });
+
+        $("#test-2fa").click(function() {
+            $.ajax({
+                type: "POST",
+                url: "/user/ga_check",
                 dataType: "json",
                 data: {
-                    pwd: $('#new-password').val(),
-                    repwd: $('#again-new-password').val(),
-                    oldpwd: $('#password').val()
+                    code: $('#2fa-test-code').val()
                 },
                 success: function(data) {
                     if (data.ret === 1) {
@@ -667,11 +704,14 @@
             })
         });
 
-        $("#unbind-im").click(function() {
+        $("#save-2fa").click(function() {
             $.ajax({
                 type: "POST",
-                url: "/user/unbind_im",
+                url: "/user/ga_set",
                 dataType: "json",
+                data: {
+                    enable: $('#ga-enable').val()
+                },
                 success: function(data) {
                     if (data.ret === 1) {
                         $('#success-message').text(data.msg);
@@ -684,11 +724,14 @@
             })
         });
 
-        $("#reset-2fa").click(function() {
+        $("#modify-daily-report").click(function() {
             $.ajax({
                 type: "POST",
-                url: "/user/ga_reset",
+                url: "/user/daily_mail",
                 dataType: "json",
+                data: {
+                    mail: $('#daily-report').val()
+                },
                 success: function(data) {
                     if (data.ret === 1) {
                         $('#success-message').text(data.msg);
@@ -701,13 +744,13 @@
             })
         });
 
-        $("#test-2fa").click(function() {
+        $("#modify-contact-method").click(function() {
             $.ajax({
                 type: "POST",
-                url: "/user/ga_check",
+                url: "/user/contact_method",
                 dataType: "json",
                 data: {
-                    code: $('#2fa-test-code').val()
+                    contact: $('#contact-method').val()
                 },
                 success: function(data) {
                     if (data.ret === 1) {
@@ -721,18 +764,19 @@
             })
         });
 
-        $("#save-2fa").click(function() {
+        $("#modify-user-theme").click(function() {
             $.ajax({
                 type: "POST",
-                url: "/user/ga_set",
+                url: "/user/theme",
                 dataType: "json",
                 data: {
-                    enable: $('#ga-enable').val()
+                    theme: $('#user-theme').val()
                 },
                 success: function(data) {
                     if (data.ret === 1) {
                         $('#success-message').text(data.msg);
                         $('#success-dialog').modal('show');
+                        window.setTimeout("location.reload()", {$config['jump_delay']});
                     } else {
                         $('#fail-message').text(data.msg);
                         $('#fail-dialog').modal('show');

+ 5 - 5
src/Command/Cron.php

@@ -5,8 +5,8 @@ declare(strict_types=1);
 namespace App\Command;
 
 use App\Models\Setting;
-use App\Services\CronDetect;
-use App\Services\CronJob;
+use App\Services\Cron as CronService;
+use App\Services\Detect;
 use Exception;
 use Telegram\Bot\Exceptions\TelegramSDKException;
 use function mktime;
@@ -30,7 +30,7 @@ EOL;
         $hour = (int) date('H');
         $minute = (int) date('i');
 
-        $jobs = new CronJob();
+        $jobs = new CronService();
 
         // Run new shop related jobs
         $jobs->processPendingOrder();
@@ -119,14 +119,14 @@ EOL;
         // Detect GFW
         if (Setting::obtain('enable_detect_gfw') && $minute === 0
         ) {
-            $detect = new CronDetect();
+            $detect = new Detect();
             $detect->gfw();
         }
 
         // Detect ban
         if (Setting::obtain('enable_detect_ban') && $minute === 0
         ) {
-            $detect = new CronDetect();
+            $detect = new Detect();
             $detect->ban();
         }
 

+ 0 - 1
src/Command/Tool.php

@@ -330,7 +330,6 @@ EOL;
             $user->invite_num = 0;
             $user->ref_by = 0;
             $user->is_admin = 1;
-            $user->expire_in = date('Y-m-d H:i:s');
             $user->reg_date = date('Y-m-d H:i:s');
             $user->money = 0;
             $user->im_type = 0;

+ 4 - 4
src/Controllers/Admin/AnnController.php

@@ -6,6 +6,7 @@ namespace App\Controllers\Admin;
 
 use App\Controllers\BaseController;
 use App\Models\Ann;
+use App\Models\EmailQueue;
 use App\Models\Setting;
 use App\Models\User;
 use App\Services\IM\Telegram;
@@ -101,15 +102,14 @@ final class AnnController extends BaseController
                 ->get();
 
             foreach ($users as $user) {
-                $user->sendMail(
+                (new EmailQueue())->add(
+                    $user->email,
                     $subject,
                     'warn.tpl',
                     [
                         'user' => $user,
                         'text' => $content,
-                    ],
-                    [],
-                    true
+                    ]
                 );
             }
         }

+ 0 - 1
src/Controllers/Admin/Setting/RegController.php

@@ -21,7 +21,6 @@ final class RegController extends BaseController
         'sign_up_for_free_traffic',
         'free_user_reset_day',
         'free_user_reset_bandwidth',
-        'sign_up_for_free_time',
         'sign_up_for_class',
         'sign_up_for_class_time',
         'sign_up_for_method',

+ 18 - 17
src/Controllers/Admin/TicketController.php

@@ -8,11 +8,15 @@ use App\Controllers\BaseController;
 use App\Models\Ticket;
 use App\Models\User;
 use App\Services\LLM;
+use App\Services\Notification;
 use App\Utils\Tools;
 use Exception;
+use GuzzleHttp\Exception\GuzzleException;
+use Psr\Http\Client\ClientExceptionInterface;
 use Psr\Http\Message\ResponseInterface;
 use Slim\Http\Response;
 use Slim\Http\ServerRequest;
+use Telegram\Bot\Exceptions\TelegramSDKException;
 use function array_merge;
 use function count;
 use function json_decode;
@@ -49,7 +53,9 @@ final class TicketController extends BaseController
     }
 
     /**
-     * 后台更新工单内容
+     * @throws TelegramSDKException
+     * @throws GuzzleException
+     * @throws ClientExceptionInterface
      */
     public function update(ServerRequest $request, Response $response, array $args): Response|ResponseInterface
     {
@@ -83,14 +89,11 @@ final class TicketController extends BaseController
         ];
 
         $user = User::find($ticket->userid);
-        $user->sendMail(
+
+        Notification::notifyUser(
+            $user,
             $_ENV['appName'] . '-工单被回复',
-            'warn.tpl',
-            [
-                'text' => '你好,有人回复了<a href="' .
-                    $_ENV['baseUrl'] . '/user/ticket/' . $ticket->id . '/view">工单</a>,请你查看。',
-            ],
-            []
+            '你好,有人回复了<a href="' . $_ENV['baseUrl'] . '/user/ticket/' . $ticket->id . '/view">工单</a>,请你查看。'
         );
 
         $ticket->content = json_encode(array_merge($content_old, $content_new));
@@ -104,7 +107,9 @@ final class TicketController extends BaseController
     }
 
     /**
-     * 喊 LLM 帮忙回复工单
+     * @throws GuzzleException
+     * @throws TelegramSDKException
+     * @throws ClientExceptionInterface
      */
     public function updateAI(ServerRequest $request, Response $response, array $args): Response|ResponseInterface
     {
@@ -122,7 +127,6 @@ final class TicketController extends BaseController
         $content_old = json_decode($ticket->content, true);
         // 获取用户的第一个问题,作为 LLM 的输入
         $ai_reply = LLM::genTextResponse($content_old[0]['comment']);
-
         $content_new = [
             [
                 'comment_id' => $content_old[count($content_old) - 1]['comment_id'] + 1,
@@ -133,14 +137,11 @@ final class TicketController extends BaseController
         ];
 
         $user = User::find($ticket->userid);
-        $user->sendMail(
+
+        Notification::notifyUser(
+            $user,
             $_ENV['appName'] . '-工单被回复',
-            'warn.tpl',
-            [
-                'text' => '你好,AI 回复了<a href="' .
-                    $_ENV['baseUrl'] . '/user/ticket/' . $ticket->id . '/view">工单</a>,请你查看。',
-            ],
-            []
+            '你好,AI 回复了<a href="' . $_ENV['baseUrl'] . '/user/ticket/' . $ticket->id . '/view">工单</a>,请你查看。'
         );
 
         $ticket->content = json_encode(array_merge($content_old, $content_new));

+ 0 - 2
src/Controllers/Admin/UserController.php

@@ -79,7 +79,6 @@ final class UserController extends BaseController
         'invite_num',
         'ref_by',
         'class_expire',
-        'expire_in',
         'node_group',
         'class',
         'auto_reset_day',
@@ -197,7 +196,6 @@ final class UserController extends BaseController
         $user->invite_num = $request->getParam('invite_num');
         $user->ref_by = $request->getParam('ref_by');
         $user->class_expire = $request->getParam('class_expire');
-        $user->expire_in = $request->getParam('expire_in');
         $user->node_group = $request->getParam('node_group');
         $user->class = $request->getParam('class');
         $user->auto_reset_day = $request->getParam('auto_reset_day');

+ 0 - 1
src/Controllers/AuthController.php

@@ -287,7 +287,6 @@ final class AuthController extends BaseController
         $user->class = $configs['sign_up_for_class'];
         $user->node_iplimit = $configs['connection_ip_limit'];
         $user->node_speedlimit = $configs['connection_rate_limit'];
-        $user->expire_in = date('Y-m-d H:i:s', time() + (int) $configs['sign_up_for_free_time'] * 86400);
         $user->reg_date = date('Y-m-d H:i:s');
         $user->reg_ip = $_SERVER['REMOTE_ADDR'];
         $user->theme = $_ENV['theme'];

+ 55 - 45
src/Controllers/User/InfoController.php

@@ -166,51 +166,6 @@ final class InfoController extends BaseController
         return ResponseHelper::success($response, '修改成功');
     }
 
-    public function updateTheme(ServerRequest $request, Response $response, array $args): Response|ResponseInterface
-    {
-        $antiXss = new AntiXSS();
-        $theme = $antiXss->xss_clean($request->getParam('theme'));
-        $user = $this->user;
-
-        if ($theme === '') {
-            return ResponseHelper::error($response, '主题不能为空');
-        }
-
-        $user->theme = $theme;
-
-        if (! $user->save()) {
-            return ResponseHelper::error($response, '修改失败');
-        }
-
-        return ResponseHelper::success($response, '修改成功');
-    }
-
-    public function updateDailyMail(ServerRequest $request, Response $response, array $args): ResponseInterface
-    {
-        $value = (int) $request->getParam('mail');
-
-        if (! in_array($value, [0, 1, 2])) {
-            return ResponseHelper::error($response, '参数错误');
-        }
-
-        $user = $this->user;
-
-        if ($value === 2 && ! Setting::obtain('enable_telegram')) {
-            return ResponseHelper::error(
-                $response,
-                '修改失败,当前无法使用 Telegram 接收每日报告'
-            );
-        }
-
-        $user->daily_mail_enable = $value;
-
-        if (! $user->save()) {
-            return ResponseHelper::error($response, '修改失败');
-        }
-
-        return ResponseHelper::success($response, '修改成功');
-    }
-
     public function resetPasswd(ServerRequest $request, Response $response, array $args): ResponseInterface
     {
         $user = $this->user;
@@ -275,6 +230,61 @@ final class InfoController extends BaseController
         return ResponseHelper::success($response, '重置成功');
     }
 
+    public function updateDailyMail(ServerRequest $request, Response $response, array $args): ResponseInterface
+    {
+        $value = (int) $request->getParam('mail');
+
+        if (! in_array($value, [0, 1, 2])) {
+            return ResponseHelper::error($response, '参数错误');
+        }
+
+        $user = $this->user;
+        $user->daily_mail_enable = $value;
+
+        if (! $user->save()) {
+            return ResponseHelper::error($response, '修改失败');
+        }
+
+        return ResponseHelper::success($response, '修改成功');
+    }
+
+    public function updateContactMethod(ServerRequest $request, Response $response, array $args): ResponseInterface
+    {
+        $value = (int) $request->getParam('contact');
+
+        if (! in_array($value, [1, 2])) {
+            return ResponseHelper::error($response, '参数错误');
+        }
+
+        $user = $this->user;
+        $user->contact_method = $value;
+
+        if (! $user->save()) {
+            return ResponseHelper::error($response, '修改失败');
+        }
+
+        return ResponseHelper::success($response, '修改成功');
+    }
+
+    public function updateTheme(ServerRequest $request, Response $response, array $args): Response|ResponseInterface
+    {
+        $antiXss = new AntiXSS();
+        $theme = $antiXss->xss_clean($request->getParam('theme'));
+        $user = $this->user;
+
+        if ($theme === '') {
+            return ResponseHelper::error($response, '主题不能为空');
+        }
+
+        $user->theme = $theme;
+
+        if (! $user->save()) {
+            return ResponseHelper::error($response, '修改失败');
+        }
+
+        return ResponseHelper::success($response, '修改成功');
+    }
+
     public function sendToGulag(ServerRequest $request, Response $response, array $args): ResponseInterface
     {
         $user = $this->user;

+ 4 - 0
src/Controllers/User/ServerController.php

@@ -35,6 +35,10 @@ final class ServerController extends BaseController
         $all_node = [];
 
         foreach ($nodes as $node) {
+            if ($node->node_bandwidth_limit !== 0 && $node->node_bandwidth_limit <= $node->node_bandwidth) {
+                continue;
+            }
+
             $array_node = [];
             $array_node['id'] = $node->id;
             $array_node['name'] = $node->name;

+ 22 - 26
src/Controllers/User/TicketController.php

@@ -7,14 +7,17 @@ namespace App\Controllers\User;
 use App\Controllers\BaseController;
 use App\Models\Setting;
 use App\Models\Ticket;
-use App\Models\User;
+use App\Services\Notification;
 use App\Services\RateLimit;
 use App\Utils\Tools;
 use Exception;
+use GuzzleHttp\Exception\GuzzleException;
+use Psr\Http\Client\ClientExceptionInterface;
 use Psr\Http\Message\ResponseInterface;
 use RedisException;
 use Slim\Http\Response;
 use Slim\Http\ServerRequest;
+use Telegram\Bot\Exceptions\TelegramSDKException;
 use voku\helper\AntiXSS;
 use function array_merge;
 use function count;
@@ -53,6 +56,9 @@ final class TicketController extends BaseController
 
     /**
      * @throws RedisException
+     * @throws ClientExceptionInterface
+     * @throws TelegramSDKException
+     * @throws GuzzleException
      */
     public function ticketAdd(ServerRequest $request, Response $response, array $args): ResponseInterface
     {
@@ -98,18 +104,10 @@ final class TicketController extends BaseController
         $ticket->save();
 
         if (Setting::obtain('mail_ticket')) {
-            $adminUser = User::where('is_admin', 1)->get();
-
-            foreach ($adminUser as $user) {
-                $user->sendMail(
-                    $_ENV['appName'] . '-新工单被开启',
-                    'warn.tpl',
-                    [
-                        'text' => '管理员,有人开启了新的工单,请你及时处理。',
-                    ],
-                    []
-                );
-            }
+            Notification::notifyAdmin(
+                $_ENV['appName'] . '-新工单被开启',
+                '管理员,有人开启了新的工单,请你及时处理。'
+            );
         }
 
         return $response->withJson([
@@ -118,6 +116,11 @@ final class TicketController extends BaseController
         ]);
     }
 
+    /**
+     * @throws GuzzleException
+     * @throws TelegramSDKException
+     * @throws ClientExceptionInterface
+     */
     public function ticketUpdate(ServerRequest $request, Response $response, array $args): ResponseInterface
     {
         if (! Setting::obtain('enable_ticket')) {
@@ -170,19 +173,12 @@ final class TicketController extends BaseController
         $ticket->save();
 
         if (Setting::obtain('mail_ticket')) {
-            $adminUser = User::where('is_admin', 1)->get();
-            foreach ($adminUser as $user) {
-                $user->sendMail(
-                    $_ENV['appName'] . '-工单被回复',
-                    'warn.tpl',
-                    [
-                        'text' => '管理员,有人回复了 <a href="' .
-                            $_ENV['baseUrl'] . '/admin/ticket/' . $ticket->id . '/view">#' . $ticket->id .
-                            '</a> 工单,请你及时处理。',
-                    ],
-                    []
-                );
-            }
+            Notification::notifyAdmin(
+                $_ENV['appName'] . '-工单被回复',
+                '管理员,有人回复了 <a href="' .
+                $_ENV['baseUrl'] . '/admin/ticket/' . $ticket->id . '/view">#' . $ticket->id .
+                '</a> 工单,请你及时处理。'
+            );
         }
 
         return $response->withJson([

+ 0 - 4
src/Controllers/UserController.php

@@ -152,10 +152,6 @@ final class UserController extends BaseController
             }
         }
 
-        if (strtotime($this->user->expire_in) < time()) {
-            return ResponseHelper::error($response, '没有过期的账户才可以签到');
-        }
-
         $checkin = $this->user->checkin();
 
         if (! $checkin['ok']) {

+ 0 - 1
src/Controllers/WebAPI/UserController.php

@@ -76,7 +76,6 @@ final class UserController extends BaseController
                 ) AS online_log ON online_log.user_id = user.id
             WHERE
                 user.is_banned = 0
-                AND user.expire_in > CURRENT_TIMESTAMP()
                 AND user.class_expire > CURRENT_TIMESTAMP()
                 AND (
                     (

+ 13 - 0
src/Models/EmailQueue.php

@@ -4,6 +4,9 @@ declare(strict_types=1);
 
 namespace App\Models;
 
+use function json_encode;
+use function time;
+
 /**
  * EmailQueue Model
  */
@@ -11,4 +14,14 @@ final class EmailQueue extends Model
 {
     protected $connection = 'default';
     protected $table = 'email_queue';
+
+    public function add($to, $subject, $template, $array): void
+    {
+        $this->to_email = $to;
+        $this->subject = $subject;
+        $this->template = $template;
+        $this->time = time();
+        $this->array = json_encode($array);
+        $this->save();
+    }
 }

+ 0 - 4
src/Models/Node.php

@@ -27,10 +27,6 @@ final class Node extends Model
      */
     public function getColorAttribute(): string
     {
-        if ($this->node_bandwidth_limit !== 0 && $this->node_bandwidth_limit <= $this->node_bandwidth) {
-            return 'yellow';
-        }
-
         return match ($this->getNodeOnlineStatus()) {
             0 => 'orange',
             1 => 'green',

+ 32 - 113
src/Models/User.php

@@ -5,17 +5,15 @@ declare(strict_types=1);
 namespace App\Models;
 
 use App\Services\DB;
-use App\Services\IM\Telegram;
-use App\Services\Mail;
+use App\Services\IM;
 use App\Utils\Hash;
 use App\Utils\Tools;
 use Exception;
-use Psr\Http\Client\ClientExceptionInterface;
+use GuzzleHttp\Exception\GuzzleException;
 use Ramsey\Uuid\Uuid;
-use function array_merge;
+use Telegram\Bot\Exceptions\TelegramSDKException;
 use function date;
 use function is_null;
-use function json_encode;
 use function md5;
 use function random_int;
 use function round;
@@ -66,17 +64,6 @@ final class User extends Model
         };
     }
 
-    /**
-     * 联系方式
-     */
-    public function imValue(): string
-    {
-        return match ($this->im_type) {
-            1, 2, 5 => $this->im_value,
-            default => '<a href="https://telegram.me/' . $this->im_value . '">' . $this->im_value . '</a>',
-        };
-    }
-
     /**
      * 最后使用时间
      */
@@ -341,72 +328,6 @@ final class User extends Model
         return $this->save();
     }
 
-    /**
-     * 发送邮件
-     */
-    public function sendMail(
-        string $subject,
-        string $template,
-        array $array = [],
-        array $files = [],
-        $is_queue = false
-    ): bool {
-        if ($is_queue) {
-            $emailqueue = new EmailQueue();
-            $emailqueue->to_email = $this->email;
-            $emailqueue->subject = $subject;
-            $emailqueue->template = $template;
-            $emailqueue->time = time();
-            $array = array_merge(['user' => $this], $array);
-            $emailqueue->array = json_encode($array);
-            $emailqueue->save();
-            return true;
-        }
-        // 验证邮箱地址是否正确
-        if (Tools::isEmail($this->email)) {
-            // 发送邮件
-            try {
-                Mail::send(
-                    $this->email,
-                    $subject,
-                    $template,
-                    array_merge(
-                        [
-                            'user' => $this,
-                        ],
-                        $array
-                    ),
-                    $files
-                );
-                return true;
-            } catch (Exception | ClientExceptionInterface $e) {
-                echo $e->getMessage();
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * 发送 Telegram 讯息
-     */
-    public function sendTelegram(string $text): bool
-    {
-        try {
-            if ($this->im_type === 4 && $this->im_value !== '') {
-                (new Telegram())->send(
-                    (int) $this->im_value,
-                    $text,
-                );
-                return true;
-            }
-        } catch (Exception $e) {
-            echo $e->getMessage();
-        }
-
-        return false;
-    }
-
     /**
      * 发送每日流量报告
      *
@@ -419,37 +340,35 @@ final class User extends Model
         $used_traffic = $this->usedTraffic();
         $unused_traffic = $this->unusedTraffic();
 
-        switch ($this->daily_mail_enable) {
-            case 1:
-                echo 'Send daily mail to user: ' . $this->id . PHP_EOL;
-                $this->sendMail(
-                    $_ENV['appName'] . '-每日流量报告以及公告',
-                    'traffic_report.tpl',
-                    [
-                        'user' => $this,
-                        'text' => '下面是系统中目前的最新公告:<br><br>' . $ann . '<br><br>晚安!',
-                        'lastday_traffic' => $lastday_traffic,
-                        'enable_traffic' => $enable_traffic,
-                        'used_traffic' => $used_traffic,
-                        'unused_traffic' => $unused_traffic,
-                    ],
-                    [],
-                    true
-                );
-                break;
-            case 2:
-                echo 'Send daily Telegram message to user: ' . $this->id . PHP_EOL;
-                $text = date('Y-m-d') . ' 流量使用报告' . PHP_EOL . PHP_EOL;
-                $text .= '流量总计:' . $enable_traffic . PHP_EOL;
-                $text .= '已用流量:' . $used_traffic . PHP_EOL;
-                $text .= '剩余流量:' . $unused_traffic . PHP_EOL;
-                $text .= '今日使用:' . $lastday_traffic;
-                $this->sendTelegram(
-                    $text
-                );
-                break;
-            case 0:
-            default:
+        if ($this->daily_mail_enable === 1) {
+            echo 'Send daily mail to user: ' . $this->id . PHP_EOL;
+
+            (new EmailQueue())->add(
+                $this->email,
+                $_ENV['appName'] . '-每日流量报告以及公告',
+                'traffic_report.tpl',
+                [
+                    'user' => $this,
+                    'text' => '下面是系统中目前的最新公告:<br><br>' . $ann . '<br><br>晚安!',
+                    'lastday_traffic' => $lastday_traffic,
+                    'enable_traffic' => $enable_traffic,
+                    'used_traffic' => $used_traffic,
+                    'unused_traffic' => $unused_traffic,
+                ]
+            );
+        } else {
+            echo 'Send daily IM message to user: ' . $this->id . PHP_EOL;
+            $text = date('Y-m-d') . ' 流量使用报告' . PHP_EOL . PHP_EOL;
+            $text .= '流量总计:' . $enable_traffic . PHP_EOL;
+            $text .= '已用流量:' . $used_traffic . PHP_EOL;
+            $text .= '剩余流量:' . $unused_traffic . PHP_EOL;
+            $text .= '今日使用:' . $lastday_traffic;
+
+            try {
+                IM::send($this->im_value, $text, $this->im_type);
+            } catch (GuzzleException|TelegramSDKException $e) {
+                echo $e->getMessage() . PHP_EOL;
+            }
         }
     }
 

+ 86 - 100
src/Services/CronJob.php → src/Services/Cron.php

@@ -20,6 +20,7 @@ use App\Services\IM\Telegram;
 use App\Utils\Tools;
 use DateTime;
 use Exception;
+use GuzzleHttp\Exception\GuzzleException;
 use Psr\Http\Client\ClientExceptionInterface;
 use Telegram\Bot\Exceptions\TelegramSDKException;
 use function array_map;
@@ -31,7 +32,7 @@ use function strtotime;
 use function time;
 use const PHP_EOL;
 
-final class CronJob
+final class Cron
 {
     public static function addTrafficLog(): void
     {
@@ -97,13 +98,9 @@ final class CronJob
             ' 检测到 ' . User::where('is_inactive', 1)->count() . ' 个账户处于闲置状态' . PHP_EOL;
     }
 
-    /**
-     * @throws TelegramSDKException
-     */
     public static function detectNodeOffline(): void
     {
         $nodes = Node::where('type', 1)->get();
-        $adminUsers = User::where('is_admin', 1)->get();
 
         foreach ($nodes as $node) {
             if ($node->getNodeOnlineStatus() >= 0 && $node->online === 1) {
@@ -111,26 +108,29 @@ final class CronJob
             }
 
             if ($node->getNodeOnlineStatus() === -1 && $node->online === 1) {
-                foreach ($adminUsers as $user) {
-                    echo 'Send Node Offline Email to admin user: ' . $user->id . PHP_EOL;
-                    $user->sendMail(
+                echo 'Send Node Offline Email to admin users' . PHP_EOL;
+
+                try {
+                    Notification::notifyAdmin(
                         $_ENV['appName'] . '-系统警告',
-                        'warn.tpl',
-                        [
-                            'text' => '管理员你好,系统发现节点 ' . $node->name . ' 掉线了,请你及时处理。',
-                        ],
-                        [],
-                        false
+                        '管理员你好,系统发现节点 ' . $node->name . ' 掉线了,请你及时处理。'
                     );
+                } catch (GuzzleException|ClientExceptionInterface|TelegramSDKException $e) {
+                    echo $e->getMessage() . PHP_EOL;
+                }
+
+                if (Setting::obtain('telegram_node_offline')) {
                     $notice_text = str_replace(
                         '%node_name%',
                         $node->name,
                         Setting::obtain('telegram_node_offline_text')
                     );
-                }
 
-                if (Setting::obtain('telegram_node_offline')) {
-                    (new Telegram())->send(0, $notice_text);
+                    try {
+                        (new Telegram())->send(0, $notice_text);
+                    } catch (TelegramSDKException $e) {
+                        echo $e->getMessage();
+                    }
                 }
 
                 $node->online = 0;
@@ -140,32 +140,36 @@ final class CronJob
             }
 
             if ($node->getNodeOnlineStatus() === 1 && $node->online === 0) {
-                foreach ($adminUsers as $user) {
-                    echo 'Send Node Online Email to admin user: ' . $user->id . PHP_EOL;
-                    $user->sendMail(
+                echo 'Send Node Online Email to admin user' . PHP_EOL;
+
+                try {
+                    Notification::notifyAdmin(
                         $_ENV['appName'] . '-系统提示',
-                        'warn.tpl',
-                        [
-                            'text' => '管理员你好,系统发现节点 ' . $node->name . ' 恢复上线了。',
-                        ],
-                        [],
-                        false
+                        '管理员你好,系统发现节点 ' . $node->name . ' 恢复上线了。'
                     );
+                } catch (GuzzleException|ClientExceptionInterface|TelegramSDKException $e) {
+                    echo $e->getMessage() . PHP_EOL;
+                }
+
+                if (Setting::obtain('telegram_node_online')) {
                     $notice_text = str_replace(
                         '%node_name%',
                         $node->name,
                         Setting::obtain('telegram_node_online_text')
                     );
-                }
 
-                if (Setting::obtain('telegram_node_online')) {
-                    (new Telegram())->send(0, $notice_text);
+                    try {
+                        (new Telegram())->send(0, $notice_text);
+                    } catch (TelegramSDKException $e) {
+                        echo $e->getMessage();
+                    }
                 }
 
                 $node->online = 1;
                 $node->save();
             }
         }
+
         echo Tools::toDateTime(time()) . ' 节点离线检测完成' . PHP_EOL;
     }
 
@@ -180,18 +184,14 @@ final class CronJob
 
                 if ($reset_traffic >= 0) {
                     $user->transfer_enable = Tools::toGB($reset_traffic);
-                    $text .= '流量已经被重置为' . $reset_traffic . 'GB';
+                    $text .= '流量已经被重置为' . $reset_traffic . 'GB';
                 }
 
-                $user->sendMail(
-                    $_ENV['appName'] . '-你的账户等级已经过期了',
-                    'warn.tpl',
-                    [
-                        'text' => $text,
-                    ],
-                    [],
-                    true
-                );
+                try {
+                    Notification::notifyUser($user, $_ENV['appName'] . '-你的账号等级已经过期了', $text);
+                } catch (GuzzleException|ClientExceptionInterface|TelegramSDKException $e) {
+                    echo $e->getMessage() . PHP_EOL;
+                }
 
                 $user->u = 0;
                 $user->d = 0;
@@ -280,10 +280,7 @@ final class CronJob
                 $user->transfer_today = 0;
                 $user->transfer_enable = Tools::toGB($content->bandwidth);
                 $user->class = $content->class;
-                $old_expire_in = new DateTime();
                 $old_class_expire = new DateTime();
-                $user->expire_in = $old_expire_in
-                    ->modify('+' . $content->time . ' days')->format('Y-m-d H:i:s');
                 $user->class_expire = $old_class_expire
                     ->modify('+' . $content->class_time . ' days')->format('Y-m-d H:i:s');
                 $user->node_group = $content->node_group;
@@ -365,10 +362,7 @@ final class CronJob
                 }
                 // 激活时间包
                 $user->class = $content->class;
-                $old_expire_in = new DateTime($user->expire_in);
                 $old_class_expire = new DateTime($user->class_expire);
-                $user->expire_in = $old_expire_in
-                    ->modify('+' . $content->time . ' days')->format('Y-m-d H:i:s');
                 $user->class_expire = $old_class_expire
                     ->modify('+' . $content->class_time . ' days')->format('Y-m-d H:i:s');
                 $user->node_group = $content->node_group;
@@ -439,23 +433,23 @@ final class CronJob
         $freeUsers = User::where('class', 0)->where('auto_reset_day', date('d'))->get();
 
         foreach ($freeUsers as $user) {
+            try {
+                Notification::notifyUser(
+                    $user,
+                    $_ENV['appName'] . '-免费流量重置通知',
+                    '你好,你的免费流量已经被重置为' . $user->auto_reset_bandwidth . 'GB。'
+                );
+            } catch (GuzzleException|ClientExceptionInterface|TelegramSDKException $e) {
+                echo $e->getMessage() . PHP_EOL;
+            }
+
             $user->u = 0;
             $user->d = 0;
             $user->transfer_enable = $user->auto_reset_bandwidth * 1024 * 1024 * 1024;
             $user->save();
-
-            $user->sendMail(
-                $_ENV['appName'] . '-你的免费流量被重置了',
-                'warn.tpl',
-                [
-                    'text' => '你好,你的免费流量已经被重置为' . $user->auto_reset_bandwidth . 'GB',
-                ],
-                [],
-                true
-            );
         }
 
-        echo Tools::toDateTime(time()) . ' 重设免费用户流量完成' . PHP_EOL;
+        echo Tools::toDateTime(time()) . ' 免费用户流量重置完成' . PHP_EOL;
     }
 
     public static function sendDailyFinanceMail(): void
@@ -475,19 +469,16 @@ final class CronJob
 
         $text_html .= '</table>';
         $text_html .= '<br>昨日总收入笔数:' . count($paylists) . '<br>昨日总收入金额:' . $paylists->sum('total');
-        $adminUser = User::where('is_admin', '=', '1')->get();
+        echo 'Sending daily finance email to admin user' . PHP_EOL;
 
-        foreach ($adminUser as $user) {
-            echo 'Sending daily finance email to admin user: ' . $user->id . PHP_EOL;
-            $user->sendMail(
-                $_ENV['appName'] . '-财务日报',
-                'finance.tpl',
-                [
-                    'title' => '财务日报',
-                    'text' => $text_html,
-                ],
-                []
+        try {
+            Notification::notifyAdmin(
+                '财务日报',
+                $text_html,
+                'finance.tpl'
             );
+        } catch (GuzzleException|ClientExceptionInterface|TelegramSDKException $e) {
+            echo $e->getMessage() . PHP_EOL;
         }
 
         echo Tools::toDateTime(time()) . ' 成功发送财务日报' . PHP_EOL;
@@ -501,19 +492,16 @@ final class CronJob
             ->get();
 
         $text_html = '<br>上周总收入笔数:' . count($paylists) . '<br>上周总收入金额:' . $paylists->sum('total');
-        $adminUser = User::where('is_admin', '=', '1')->get();
+        echo 'Sending weekly finance email to admin user' . PHP_EOL;
 
-        foreach ($adminUser as $user) {
-            echo 'Sending weekly finance email to admin user: ' . $user->id . PHP_EOL;
-            $user->sendMail(
-                $_ENV['appName'] . '-财务周报',
-                'finance.tpl',
-                [
-                    'title' => '财务周报',
-                    'text' => $text_html,
-                ],
-                []
+        try {
+            Notification::notifyAdmin(
+                '财务周报',
+                $text_html,
+                'finance.tpl'
             );
+        } catch (GuzzleException|ClientExceptionInterface|TelegramSDKException $e) {
+            echo $e->getMessage() . PHP_EOL;
         }
 
         echo Tools::toDateTime(time()) . ' 成功发送财务周报' . PHP_EOL;
@@ -527,19 +515,16 @@ final class CronJob
             ->get();
 
         $text_html = '<br>上月总收入笔数:' . count($paylists) . '<br>上月总收入金额:' . $paylists->sum('total');
-        $adminUser = User::where('is_admin', '=', '1')->get();
+        echo 'Sending monthly finance email to admin user' . PHP_EOL;
 
-        foreach ($adminUser as $user) {
-            echo 'Sending monthly finance email to admin user: ' . $user->id . PHP_EOL;
-            $user->sendMail(
-                $_ENV['appName'] . '-财务月报',
-                'finance.tpl',
-                [
-                    'title' => '财务月报',
-                    'text' => $text_html,
-                ],
-                []
+        try {
+            Notification::notifyAdmin(
+                '财务月报',
+                $text_html,
+                'finance.tpl'
             );
+        } catch (GuzzleException|ClientExceptionInterface|TelegramSDKException $e) {
+            echo $e->getMessage() . PHP_EOL;
         }
 
         echo Tools::toDateTime(time()) . ' 成功发送财务月报' . PHP_EOL;
@@ -567,19 +552,20 @@ final class CronJob
             }
 
             if ($under_limit && ! $user->traffic_notified) {
-                $result = $user->sendMail(
-                    $_ENV['appName'] . '-你的剩余流量过低',
-                    'warn.tpl',
-                    [
-                        'text' => '你好,系统发现你剩余流量已经低于 ' . $_ENV['notify_limit_value'] . $unit_text . ' 。',
-                    ],
-                    [],
-                    true
-                );
-                if ($result) {
+                try {
+                    Notification::notifyUser(
+                        $user,
+                        $_ENV['appName'] . '-你的剩余流量过低',
+                        '你好,系统发现你剩余流量已经低于 ' . $_ENV['notify_limit_value'] . $unit_text . ' 。',
+                    );
+
                     $user->traffic_notified = true;
-                    $user->save();
+                } catch (GuzzleException|ClientExceptionInterface|TelegramSDKException $e) {
+                    $user->traffic_notified = false;
+                    echo $e->getMessage() . PHP_EOL;
                 }
+
+                $user->save();
             } elseif (! $under_limit && $user->traffic_notified) {
                 $user->traffic_notified = false;
                 $user->save();

+ 37 - 36
src/Services/CronDetect.php → src/Services/Detect.php

@@ -11,6 +11,8 @@ use App\Models\Setting;
 use App\Models\User;
 use App\Services\IM\Telegram;
 use App\Utils\Tools;
+use GuzzleHttp\Exception\GuzzleException;
+use Psr\Http\Client\ClientExceptionInterface;
 use Telegram\Bot\Exceptions\TelegramSDKException;
 use function file_get_contents;
 use function in_array;
@@ -20,7 +22,7 @@ use function strtotime;
 use function time;
 use const PHP_EOL;
 
-final class CronDetect
+final class Detect
 {
     /**
      * @throws TelegramSDKException
@@ -28,7 +30,6 @@ final class CronDetect
     public static function gfw(): void
     {
         $nodes = Node::where('type', 1)->where('node_ip', '!=', '')->where('online', 1)->get();
-        $adminUser = User::where('is_admin', '1')->get();
 
         foreach ($nodes as $node) {
             $api_url = str_replace(
@@ -46,26 +47,25 @@ final class CronDetect
 
             if (! $result_tcping && ! $node->gfw_block) {
                 //被墙了
-                echo $node->id . ':false' . PHP_EOL;
+                echo '检测到节点 #' . $node->id . ' 被 GFW 封锁' . PHP_EOL;
+                echo 'Send gfw mail to admin' . PHP_EOL;
 
-                foreach ($adminUser as $user) {
-                    echo 'Send gfw mail to user: ' . $user->id . '-';
-                    $user->sendMail(
+                try {
+                    Notification::notifyAdmin(
                         $_ENV['appName'] . '-系统警告',
-                        'warn.tpl',
-                        [
-                            'text' => '管理员你好,系统发现节点 ' . $node->name . ' 被墙了,请你及时处理。',
-                        ],
-                        []
+                        '管理员你好,系统发现节点 ' . $node->name . ' 被墙了。'
                     );
+                } catch (GuzzleException|ClientExceptionInterface|TelegramSDKException $e) {
+                    echo $e->getMessage() . PHP_EOL;
+                }
+
+                if (Setting::obtain('telegram_node_gfwed')) {
                     $notice_text = str_replace(
                         '%node_name%',
                         $node->name,
                         Setting::obtain('telegram_node_gfwed_text')
                     );
-                }
 
-                if (Setting::obtain('telegram_node_gfwed')) {
                     (new Telegram())->send(0, $notice_text);
                 }
 
@@ -75,31 +75,32 @@ final class CronDetect
                 continue;
             }
 
-            echo $node->id . ':true' . PHP_EOL;
-
-            foreach ($adminUser as $user) {
-                echo 'Send gfw mail to user: ' . $user->id . '-';
-                $user->sendMail(
-                    $_ENV['appName'] . '-系统提示',
-                    'warn.tpl',
-                    [
-                        'text' => '管理员你好,系统发现节点 ' . $node->name . ' 溜出墙了。',
-                    ],
-                    []
-                );
-                $notice_text = str_replace(
-                    '%node_name%',
-                    $node->name,
-                    Setting::obtain('telegram_node_ungfwed_text')
-                );
-            }
+            if ($result_tcping && $node->gfw_block) {
+                echo '检测到节点 #' . $node->id . ' 被 GFW 解除封锁' . PHP_EOL;
+                echo 'Send gfw mail to admin' . PHP_EOL;
 
-            if (Setting::obtain('telegram_node_ungfwed')) {
-                (new Telegram())->send(0, $notice_text);
-            }
+                try {
+                    Notification::notifyAdmin(
+                        $_ENV['appName'] . '-系统提示',
+                        '管理员你好,系统发现节点 ' . $node->name . ' 溜出墙了。'
+                    );
+                } catch (GuzzleException|ClientExceptionInterface|TelegramSDKException $e) {
+                    echo $e->getMessage() . PHP_EOL;
+                }
 
-            $node->gfw_block = false;
-            $node->save();
+                if (Setting::obtain('telegram_node_ungfwed')) {
+                    $notice_text = str_replace(
+                        '%node_name%',
+                        $node->name,
+                        Setting::obtain('telegram_node_ungfwed_text')
+                    );
+
+                    (new Telegram())->send(0, $notice_text);
+                }
+
+                $node->gfw_block = false;
+                $node->save();
+            }
         }
     }
 

+ 3 - 4
src/Services/IM.php

@@ -4,16 +4,15 @@ declare(strict_types=1);
 
 namespace App\Services;
 
-/*
- * IM Service
- */
-
 use App\Services\IM\Discord;
 use App\Services\IM\Slack;
 use App\Services\IM\Telegram;
 use GuzzleHttp\Exception\GuzzleException;
 use Telegram\Bot\Exceptions\TelegramSDKException;
 
+/*
+ * IM Service
+ */
 final class IM
 {
     public static function getClient($type): Discord|Slack|Telegram

+ 4 - 4
src/Services/Mail.php

@@ -4,10 +4,6 @@ declare(strict_types=1);
 
 namespace App\Services;
 
-/*
- * Mail Service
- */
-
 use App\Models\Setting;
 use App\Services\Mail\Mailgun;
 use App\Services\Mail\NullMail;
@@ -19,6 +15,9 @@ use Exception;
 use Psr\Http\Client\ClientExceptionInterface;
 use Smarty;
 
+/*
+ * Mail Service
+ */
 final class Mail
 {
     public static function getClient(): Mailgun|Smtp|SendGrid|NullMail|Ses|Postal
@@ -45,6 +44,7 @@ final class Mail
         $smarty->setcachedir(BASE_PATH . '/storage/framework/smarty/cache/');
         // add config
         $smarty->assign('config', Config::getViewConfig());
+
         foreach ($ary as $key => $value) {
             $smarty->assign($key, $value);
         }

+ 87 - 0
src/Services/Notification.php

@@ -0,0 +1,87 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Services;
+
+use App\Models\EmailQueue;
+use App\Models\User;
+use GuzzleHttp\Exception\GuzzleException;
+use Psr\Http\Client\ClientExceptionInterface;
+use Telegram\Bot\Exceptions\TelegramSDKException;
+
+/*
+ * Notification Service
+ */
+final class Notification
+{
+    /**
+     * @throws GuzzleException
+     * @throws TelegramSDKException
+     * @throws ClientExceptionInterface
+     */
+    public static function notifyAdmin($title = '', $msg = '', $template = 'warn.tpl'): void
+    {
+        $admins = User::where('is_admin', 1)->get();
+
+        foreach ($admins as $admin) {
+            if ($admin->contact_method === 1 || $admin->im_type === 0) {
+                Mail::send(
+                    $admin->email,
+                    $title,
+                    $template,
+                    [
+                        'user' => $admin,
+                        'title' => $title,
+                        'text' => $msg,
+                    ]
+                );
+            } else {
+                IM::send($admin->im_value, $msg, $admin->im_type);
+            }
+        }
+    }
+
+    /**
+     * @throws GuzzleException
+     * @throws TelegramSDKException
+     * @throws ClientExceptionInterface
+     */
+    public static function notifyUser($user, $title = '', $msg = '', $template = 'warn.tpl'): void
+    {
+        if ($user->contact_method === 1 || $user->im_type === 0) {
+            $array = [
+                'user' => $user,
+                'title' => $title,
+                'text' => $msg,
+            ];
+
+            (new EmailQueue())->add($user->email, $title, $template, $array);
+        } else {
+            IM::send($user->im_value, $msg, $user->im_type);
+        }
+    }
+
+    /**
+     * @throws GuzzleException
+     * @throws TelegramSDKException
+     */
+    public static function notifyAllUser($title = '', $msg = '', $template = 'warn.tpl'): void
+    {
+        $users = User::all();
+
+        foreach ($users as $user) {
+            if ($user->contact_method === 1 || $user->im_type === 0) {
+                $array = [
+                    'user' => $user,
+                    'title' => $title,
+                    'text' => $msg,
+                ];
+
+                (new EmailQueue())->add($user->email, $title, $template, $array);
+            } else {
+                IM::send($user->im_value, $msg, $user->im_type);
+            }
+        }
+    }
+}