فهرست منبع

feat: notification service

M1Screw 2 سال پیش
والد
کامیت
570cb6e4ef

+ 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');

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

@@ -287,6 +287,7 @@ 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 '等级过期时间',

+ 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;
+    }
+};

+ 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();
         }
 

+ 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
+                    ]
                 );
             }
         }

+ 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));

+ 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;

+ 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([

+ 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();
+    }
 }

+ 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 - 94
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;
@@ -439,23 +439,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 +475,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 +498,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 +521,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 +558,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);
+            }
+        }
+    }
+}