From 632f68660b8746350f193026debde4cb8d2e8d2e Mon Sep 17 00:00:00 2001 From: kuaifan Date: Thu, 5 May 2022 19:24:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=82=AE=E4=BB=B6=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E6=9C=AA=E8=AF=BB=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/Api/SystemController.php | 10 +- app/Http/Controllers/IndexController.php | 10 +- app/Models/WebSocketDialogMsg.php | 42 ++++ app/Models/WebSocketDialogMsgRead.php | 2 + app/Tasks/EmailNoticeTask.php | 219 ++++++++++++++++++ ...add_web_socket_dialog_msgs_dialog_type.php | 34 +++ ..._add_web_socket_dialog_msg_reads_email.php | 34 +++ .../setting/components/SystemEmailSetting.vue | 11 +- resources/assets/sass/pages/page-setting.scss | 2 +- resources/views/unread.blade.php | 55 +++++ 10 files changed, 407 insertions(+), 12 deletions(-) create mode 100644 app/Tasks/EmailNoticeTask.php create mode 100644 database/migrations/2022_05_05_115726_add_web_socket_dialog_msgs_dialog_type.php create mode 100644 database/migrations/2022_05_05_135535_add_web_socket_dialog_msg_reads_email.php create mode 100755 resources/views/unread.blade.php diff --git a/app/Http/Controllers/Api/SystemController.php b/app/Http/Controllers/Api/SystemController.php index e1e5fbacf..789866b56 100755 --- a/app/Http/Controllers/Api/SystemController.php +++ b/app/Http/Controllers/Api/SystemController.php @@ -99,7 +99,7 @@ class SystemController extends AbstractController * * @apiParam {String} type * - get: 获取(默认) - * - save: 保存设置(参数:['smtp_server', 'port', 'account', 'password', 'reg_verify', 'notice', 'task_remind_hours', 'task_remind_hours2']) + * - save: 保存设置(参数:['smtp_server', 'port', 'account', 'password', 'reg_verify', 'notice', 'task_remind_hours', 'task_remind_hours2', 'notice_msg', 'msg_unread_user_minute', 'msg_unread_group_minute']) * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) * @apiSuccess {Object} data 返回数据 @@ -123,7 +123,10 @@ class SystemController extends AbstractController 'reg_verify', 'notice', 'task_remind_hours', - 'task_remind_hours2' + 'task_remind_hours2', + 'notice_msg', + 'msg_unread_user_minute', + 'msg_unread_group_minute' ])) { unset($all[$key]); } @@ -141,6 +144,9 @@ class SystemController extends AbstractController $setting['notice'] = $setting['notice'] ?: 'open'; $setting['task_remind_hours'] = floatval($setting['task_remind_hours']) ?: 0; $setting['task_remind_hours2'] = floatval($setting['task_remind_hours2']) ?: 0; + $setting['notice_msg'] = $setting['notice_msg'] ?: 'open'; + $setting['msg_unread_user_minute'] = floatval($setting['msg_unread_user_minute']) ?: 0; + $setting['msg_unread_group_minute'] = floatval($setting['msg_unread_group_minute']) ?: 0; // return Base::retSuccess('success', $setting ?: json_decode('{}')); } diff --git a/app/Http/Controllers/IndexController.php b/app/Http/Controllers/IndexController.php index 78465e6b4..71cd96023 100755 --- a/app/Http/Controllers/IndexController.php +++ b/app/Http/Controllers/IndexController.php @@ -5,7 +5,7 @@ namespace App\Http\Controllers; use App\Module\Base; use App\Tasks\AutoArchivedTask; use App\Tasks\DeleteTmpTask; -use App\Tasks\OverdueRemindEmailTask; +use App\Tasks\EmailNoticeTask; use Arr; use Hhxsv5\LaravelS\Swoole\Task\Task; use Redirect; @@ -140,13 +140,13 @@ class IndexController extends InvokeController // 限制内网访问 return "Forbidden Access"; } + // 自动归档 + Task::deliver(new AutoArchivedTask()); + // 邮件通知 + Task::deliver(new EmailNoticeTask()); // 删除过期的临时表数据 Task::deliver(new DeleteTmpTask('wg_tmp_msgs', 1)); Task::deliver(new DeleteTmpTask('tmp', 24)); - // 自动归档任务 - Task::deliver(new AutoArchivedTask()); - // 任务到期邮件提醒 - Task::deliver(new OverdueRemindEmailTask()); return "success"; } diff --git a/app/Models/WebSocketDialogMsg.php b/app/Models/WebSocketDialogMsg.php index 4167e88ea..465f516aa 100644 --- a/app/Models/WebSocketDialogMsg.php +++ b/app/Models/WebSocketDialogMsg.php @@ -15,6 +15,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; * * @property int $id * @property int|null $dialog_id 对话ID + * @property string|null $dialog_type 对话类型 * @property int|null $userid 发送会员ID * @property string|null $type 消息类型 * @property array|mixed $msg 详细消息 @@ -32,6 +33,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereCreatedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDeletedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogId($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogType($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereId($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereMsg($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereRead($value) @@ -198,6 +200,45 @@ class WebSocketDialogMsg extends AbstractModel }); } + /** + * 预览消息 + * @param bool $preserveHtml 保留html格式 + * @return string + */ + public function previewMsg($preserveHtml = false) + { + switch ($this->type) { + case 'text': + return $this->previewTextMsg($this->msg['text'], $preserveHtml); + case 'file': + if ($this->msg['type'] == 'img') { + return "[图片]"; + } + return "[文件] {$this->msg['name']}"; + default: + return "[未知的消息]"; + } + } + + /** + * 返回文本预览消息 + * @param $text + * @param bool $preserveHtml 保留html格式 + * @return string|string[]|null + */ + private function previewTextMsg($text, $preserveHtml = false) + { + if (!$text) return ''; + $text = preg_replace("/]*?alt=\"(\S+)\"[^>]*?>/", "[$1]", $text); + $text = preg_replace("/]*?>/", "[表情]", $text); + $text = preg_replace("/]*?>/", "[图片]", $text); + if ($preserveHtml) { + return $text; + } else { + return strip_tags($text); + } + } + /** * 处理文本消息内容,用于发送前 * @param $text @@ -288,6 +329,7 @@ class WebSocketDialogMsg extends AbstractModel $dialog->save(); $dialogMsg->send = 1; $dialogMsg->dialog_id = $dialog->id; + $dialogMsg->dialog_type = $dialog->type; $dialogMsg->save(); }); Task::deliver(new WebSocketDialogMsgTask($dialogMsg->id)); diff --git a/app/Models/WebSocketDialogMsgRead.php b/app/Models/WebSocketDialogMsgRead.php index e0406bdb1..8a1a0c7c3 100644 --- a/app/Models/WebSocketDialogMsgRead.php +++ b/app/Models/WebSocketDialogMsgRead.php @@ -12,6 +12,7 @@ use Carbon\Carbon; * @property int|null $msg_id 消息ID * @property int|null $userid 发送会员ID * @property int|null $mention 是否提及(被@) + * @property int|null $email 是否发了邮件 * @property int|null $after 在阅读之后才添加的记录 * @property string|null $read_at 阅读时间 * @property-read \App\Models\WebSocketDialogMsg|null $webSocketDialogMsg @@ -20,6 +21,7 @@ use Carbon\Carbon; * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead query() * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereAfter($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereDialogId($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereEmail($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereId($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereMention($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereMsgId($value) diff --git a/app/Tasks/EmailNoticeTask.php b/app/Tasks/EmailNoticeTask.php new file mode 100644 index 000000000..1976e91a9 --- /dev/null +++ b/app/Tasks/EmailNoticeTask.php @@ -0,0 +1,219 @@ + 0) { + ProjectTask::whereNull("complete_at") + ->whereNull("archived_at") + ->whereBetween("end_at", [ + Carbon::now()->addMinutes($hours * 60), + Carbon::now()->addMinutes($hours * 60 + 10) + ])->chunkById(100, function ($tasks) { + /** @var ProjectTask $task */ + foreach ($tasks as $task) { + $this->overdueBeforeAfterEmail($task, true); + } + }); + } + if ($hours2 > 0) { + ProjectTask::whereNull("complete_at") + ->whereNull("archived_at") + ->whereBetween("end_at", [ + Carbon::now()->subMinutes($hours2 * 60 + 10), + Carbon::now()->subMinutes($hours2 * 60) + ])->chunkById(100, function ($tasks) { + /** @var ProjectTask $task */ + foreach ($tasks as $task) { + $this->overdueBeforeAfterEmail($task, false); + } + }); + } + } + // 消息通知 + if ($setting['notice_msg'] === 'open') { + $userMinute = floatval($setting['msg_unread_user_minute']); + $groupMinute = floatval($setting['msg_unread_group_minute']); + if ($userMinute > 0) { + WebSocketDialogMsg::select(['web_socket_dialog_msgs.*', 'r.id as r_id', 'r.userid as r_userid']) + ->join('web_socket_dialog_msg_reads as r', 'web_socket_dialog_msgs.id', '=', 'r.msg_id') + ->where("web_socket_dialog_msgs.dialog_type", "user") + ->where("r.email", 0) + ->whereNull("r.read_at") + ->whereBetween("web_socket_dialog_msgs.created_at", [ + Carbon::now()->subMinutes($userMinute + 10), + Carbon::now()->subMinutes($userMinute) + ])->chunkById(100, function ($rows) { + $this->unreadMsgEmail($rows); + }); + } + if ($groupMinute > 0) { + WebSocketDialogMsg::select(['web_socket_dialog_msgs.*', 'r.id as r_id', 'r.userid as r_userid']) + ->join('web_socket_dialog_msg_reads as r', 'web_socket_dialog_msgs.id', '=', 'r.msg_id') + ->where("web_socket_dialog_msgs.dialog_type", "group") + ->where("r.email", 0) + ->whereNull("r.read_at") + ->whereBetween("web_socket_dialog_msgs.created_at", [ + Carbon::now()->subMinutes($groupMinute + 10), + Carbon::now()->subMinutes($groupMinute) + ])->chunkById(100, function ($rows) { + $this->unreadMsgEmail($rows); + }); + } + } + } + + /** + * 任务过期前、超期后提醒 + * @param ProjectTask $task + * @param $isBefore + * @return void + */ + private function overdueBeforeAfterEmail(ProjectTask $task, $isBefore) + { + $userids = $task->taskUser->where('owner', 1)->pluck('userid')->toArray(); + if (empty($userids)) { + return; + } + $users = User::whereIn('userid', $userids)->get(); + if (empty($users)) { + return; + } + + $setting = Base::setting('emailSetting'); + $hours = floatval($setting['task_remind_hours']); + $hours2 = floatval($setting['task_remind_hours2']); + + /** @var User $user */ + foreach ($users as $user) { + $data = [ + 'type' => $isBefore ? 1 : 2, + 'userid' => $user->userid, + 'task_id' => $task->id, + ]; + $emailLog = ProjectTaskMailLog::where($data)->first(); + if ($emailLog) { + continue; + } + try { + if (!Base::isEmail($user->email)) { + throw new \Exception("User email '{$user->email}' address error"); + } + if ($isBefore) { + $subject = env('APP_NAME') . " 任务提醒"; + $content = "

{$user->nickname} 您好:

您有一个任务【{$task->name}】还有{$hours}小时即将超时,请及时处理。

"; + } else { + $subject = env('APP_NAME') . " 任务过期提醒"; + $content = "

{$user->nickname} 您好:

您的任务【{$task->name}】已经超时{$hours2}小时,请及时处理。

"; + } + Factory::mailer() + ->setDsn("smtp://{$setting['account']}:{$setting['password']}@{$setting['smtp_server']}:{$setting['port']}?verify_peer=0") + ->setMessage(EmailMessage::create() + ->from(env('APP_NAME', 'Task') . " <{$setting['account']}>") + ->to($user->email) + ->subject($subject) + ->html($content)) + ->send(); + $data['is_send'] = 1; + } catch (\Exception $e) { + $data['send_error'] = $e->getMessage(); + } + $data['email'] = $user->email; + ProjectTaskMailLog::createInstance($data)->save(); + } + } + + /** + * 未读消息通知 + * @param \Illuminate\Database\Eloquent\Collection|EmailNoticeTask[] $rows + * @return void + */ + private function unreadMsgEmail($rows) + { + $array = $rows->groupBy('r_userid'); + foreach ($array as $userid => $data) { + $user = User::find($userid); + if (empty($user)) { + continue; + } + if (!Base::isEmail($user->email)) { + continue; + } + if (count($data) === 0) { + continue; + } + $setting = Base::setting('emailSetting'); + $subject = env('APP_NAME') . " 未读消息提醒(" . count($data) . ")条"; + $content = view('unread', [ + 'type' => 'head', + 'nickname' => $user->nickname, + 'count' => count($data), + ]); + $lists = $data->groupBy('dialog_id'); + /** @var WebSocketDialogMsg[] $items */ + foreach ($lists as $items) { + $dialogName = null; + foreach ($items as $item) { + $item->userInfo = User::userid2basic($item->userid); + $item->preview = $item->previewMsg(true); + if ($dialogName === null) { + switch ($item->dialog_type) { + case "user": + $dialogName = $item->userInfo['nickname']; + break; + case "group": + $dialogName = $item->webSocketDialog?->name; + break; + } + } + } + $content .= view('unread', [ + 'type' => 'content', + 'dialogName' => $dialogName, + 'unread' => count($items), + 'items' => $items, + ]); + } + try { + Factory::mailer() + ->setDsn("smtp://{$setting['account']}:{$setting['password']}@{$setting['smtp_server']}:{$setting['port']}?verify_peer=0") + ->setMessage(EmailMessage::create() + ->from(env('APP_NAME', 'Task') . " <{$setting['account']}>") + ->to($user->email) + ->subject($subject) + ->html($content)) + ->send(); + } catch (\Exception $e) { + info("unreadMsgEmail: " . $e->getMessage()); + } + } + WebSocketDialogMsgRead::whereIn('id', $rows->pluck('r_id'))->update([ + 'email' => 1 + ]); + } +} diff --git a/database/migrations/2022_05_05_115726_add_web_socket_dialog_msgs_dialog_type.php b/database/migrations/2022_05_05_115726_add_web_socket_dialog_msgs_dialog_type.php new file mode 100644 index 000000000..41ecc86a9 --- /dev/null +++ b/database/migrations/2022_05_05_115726_add_web_socket_dialog_msgs_dialog_type.php @@ -0,0 +1,34 @@ +string('dialog_type', 50)->nullable()->default('')->after('dialog_id')->comment('对话类型'); + } + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('web_socket_dialog_msgs', function (Blueprint $table) { + $table->dropColumn("dialog_type"); + }); + } +} diff --git a/database/migrations/2022_05_05_135535_add_web_socket_dialog_msg_reads_email.php b/database/migrations/2022_05_05_135535_add_web_socket_dialog_msg_reads_email.php new file mode 100644 index 000000000..0db49b85a --- /dev/null +++ b/database/migrations/2022_05_05_135535_add_web_socket_dialog_msg_reads_email.php @@ -0,0 +1,34 @@ +boolean('email')->default(0)->after('mention')->nullable()->comment('是否发了邮件'); + } + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('web_socket_dialog_msg_reads', function (Blueprint $table) { + $table->dropColumn("email"); + }); + } +} diff --git a/resources/assets/js/pages/manage/setting/components/SystemEmailSetting.vue b/resources/assets/js/pages/manage/setting/components/SystemEmailSetting.vue index abfc10491..92cfedb24 100644 --- a/resources/assets/js/pages/manage/setting/components/SystemEmailSetting.vue +++ b/resources/assets/js/pages/manage/setting/components/SystemEmailSetting.vue @@ -39,12 +39,12 @@
- + - +
@@ -57,12 +57,12 @@
- + - +
@@ -91,6 +91,9 @@ export default { notice: 'open', task_remind_hours: 0, task_remind_hours2: 0, + notice_msg: 'open', + msg_unread_user_minute: 0, + msg_unread_group_minute: 0, }, ruleData: {}, } diff --git a/resources/assets/sass/pages/page-setting.scss b/resources/assets/sass/pages/page-setting.scss index fc7fe020b..315c53765 100755 --- a/resources/assets/sass/pages/page-setting.scss +++ b/resources/assets/sass/pages/page-setting.scss @@ -211,7 +211,7 @@ } .email-setting-box { position: relative; - padding: 36px 24px 4px; + padding: 44px 24px 4px; margin: 24px 0 12px; border-radius: 8px; border: 1px solid #eeeeee; diff --git a/resources/views/unread.blade.php b/resources/views/unread.blade.php new file mode 100755 index 000000000..9274c191e --- /dev/null +++ b/resources/views/unread.blade.php @@ -0,0 +1,55 @@ +@if ($type === 'head') + + + + + + +
+

{{ $nickname }} 您好:

+

您有({{ $count }})条未读消息,请及时处理。

+
+
+@else + + + + + + +
+

+ {{ $dialogName }} +

+

+ {{ $unread }}条未读信息 +

+
+ @foreach($items as $item) + + + + + + + + + + + +
+ + + + {{$item->userInfo["nickname"]}} + +
+ + {!! $item->preview !!} +
+ @endforeach +
+
+
+
+@endif