feat: 邮件通知未读消息

This commit is contained in:
kuaifan 2022-05-05 19:24:17 +08:00
parent 43531d9b8d
commit 632f68660b
10 changed files with 407 additions and 12 deletions

View File

@ -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('{}'));
}

View File

@ -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";
}

View File

@ -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("/<img\s+class=\"emoticon\"[^>]*?alt=\"(\S+)\"[^>]*?>/", "[$1]", $text);
$text = preg_replace("/<img\s+class=\"emoticon\"[^>]*?>/", "[表情]", $text);
$text = preg_replace("/<img\s+class=\"browse\"[^>]*?>/", "[图片]", $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));

View File

@ -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)

View File

@ -0,0 +1,219 @@
<?php
namespace App\Tasks;
use App\Models\ProjectTask;
use App\Models\ProjectTaskMailLog;
use App\Models\User;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogMsgRead;
use App\Module\Base;
use Carbon\Carbon;
use Guanguans\Notify\Factory;
use Guanguans\Notify\Messages\EmailMessage;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
class EmailNoticeTask extends AbstractTask
{
public function __construct()
{
//
}
public function start()
{
$setting = Base::setting('emailSetting');
// 任务通知
if ($setting['notice'] === 'open') {
$hours = floatval($setting['task_remind_hours']);
$hours2 = floatval($setting['task_remind_hours2']);
if ($hours > 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 = "<p>{$user->nickname} 您好:</p><p>您有一个任务【{$task->name}】还有{$hours}小时即将超时,请及时处理。</p>";
} else {
$subject = env('APP_NAME') . " 任务过期提醒";
$content = "<p>{$user->nickname} 您好:</p><p>您的任务【{$task->name}】已经超时{$hours2}小时,请及时处理。</p>";
}
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
]);
}
}

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddWebSocketDialogMsgsDialogType extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
if (!Schema::hasColumn('web_socket_dialog_msgs', 'dialog_type')) {
$table->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");
});
}
}

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddWebSocketDialogMsgReadsEmail extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_msg_reads', function (Blueprint $table) {
if (!Schema::hasColumn('web_socket_dialog_msg_reads', 'email')) {
$table->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");
});
}
}

View File

@ -39,12 +39,12 @@
<Form v-if="formData.notice == 'open'" label-width="auto" @submit.native.prevent>
<FormItem :label="$L('第一次通知:')" prop="task_remind_hours">
<label>{{ $L('到期前') }}</label>
<InputNumber v-model="formData.task_remind_hours" :min="0.5" :step="0.5" @on-change="hoursChange($event, 'task_remind_hours')"/>
<InputNumber v-model="formData.task_remind_hours" :min="0" :step="0.5" @on-change="hoursChange($event, 'task_remind_hours')"/>
<label>{{ $L('小时') }}</label>
</FormItem>
<FormItem :label="$L('第二次通知:')" prop="task_remind_hours2">
<label>{{ $L('到期后') }}</label>
<InputNumber v-model="formData.task_remind_hours2" :min="0.5" :step="0.5" @on-change="hoursChange($event, 'task_remind_hours2')"/>
<InputNumber v-model="formData.task_remind_hours2" :min="0" :step="0.5" @on-change="hoursChange($event, 'task_remind_hours2')"/>
<label>{{ $L('小时') }}</label>
</FormItem>
</Form>
@ -57,12 +57,12 @@
<Form v-if="formData.notice_msg == 'open'" label-width="auto" @submit.native.prevent>
<FormItem :label="$L('个人消息:')" prop="msg_unread_user_minute">
<label>{{ $L('未读时长') }}</label>
<InputNumber v-model="formData.msg_unread_user_minute" :min="1" :step="1"/>
<InputNumber v-model="formData.msg_unread_user_minute" :min="0" :step="1"/>
<label>{{ $L('分钟') }}</label>
</FormItem>
<FormItem :label="$L('群聊消息:')" prop="msg_unread_group_minute">
<label>{{ $L('未读时长') }}</label>
<InputNumber v-model="formData.msg_unread_group_minute" :min="1" :step="1"/>
<InputNumber v-model="formData.msg_unread_group_minute" :min="0" :step="1"/>
<label>{{ $L('分钟') }}</label>
</FormItem>
</Form>
@ -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: {},
}

View File

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

View File

@ -0,0 +1,55 @@
@if ($type === 'head')
<table style="box-sizing:border-box;border-spacing:0;border-collapse:collapse;padding:0;vertical-align:top;text-align:left;margin:0 auto 24px;width:580px">
<tbody>
<tr style="box-sizing:border-box;padding:0;vertical-align:top;text-align:left">
<td style="box-sizing:border-box;word-break:break-word;border-collapse:collapse;padding:0 0 10px;vertical-align:top;text-align:left;color:#202020;font-weight:normal;margin:0;line-height:19px;font-size:14px">
<p>{{ $nickname }} 您好:</p>
<p>您有({{ $count }})条未读消息,请及时处理。</p>
<hr style='box-sizing:border-box;color:#d9d9d9;background-color:#d9d9d9;height:1px;border:none;margin-top:32px'>
</td>
</tr>
</tbody>
</table>
@else
<table style="box-sizing:border-box;border-spacing:0;border-collapse:collapse;padding:0;vertical-align:top;text-align:left;margin:0 auto;width:580px">
<tbody>
<tr style="box-sizing:border-box;padding:0;vertical-align:top;text-align:left">
<td style="box-sizing:border-box;word-break:break-word;border-collapse:collapse;padding:0 0 10px;vertical-align:top;text-align:left;color:#202020;font-weight:normal;margin:0;line-height:19px;font-size:14px">
<h2 style="box-sizing:border-box;color:#202020;font-weight:normal;padding:0 0 8px 0;margin:0;text-align:left;line-height:1.3;word-break:normal;font-size:26px">
{{ $dialogName }}
</h2>
<h4 style="box-sizing:border-box;color:#202020;font-weight:normal;padding:0;margin:0;text-align:left;line-height:1.3;word-break:normal;font-size:18px">
{{ $unread }}条未读信息
</h4>
<br style="box-sizing:border-box">
@foreach($items as $item)
<table style="box-sizing:border-box;border-spacing:0;border-collapse:collapse;padding:0;vertical-align:top;text-align:left">
<tbody>
<tr style="box-sizing:border-box;padding:0;vertical-align:top;text-align:left">
<td style="box-sizing:border-box;word-break:break-word;border-collapse:collapse;vertical-align:middle;text-align:left;padding: 0 10px 10px 0;min-width:24px;color:#202020;font-weight:normal;margin:0;line-height:19px;font-size:14px;border-radius:4px">
<img data-imagetype="External" src="{{$item->userInfo["userimg"]}}" style="box-sizing:border-box;outline:none;text-decoration:none;width:24px;max-width:100%;float:left;clear:both;display:block;border-radius:4px;height:24px">
</td>
<td style="box-sizing:border-box;word-break:break-word;border-collapse:collapse;padding:0 0 10px;vertical-align:middle;text-align:left;color:#202020;font-weight:normal;margin:0;line-height:19px;font-size:14px">
<strong style="box-sizing:border-box">
{{$item->userInfo["nickname"]}}
</strong>
</td>
</tr>
<tr style="box-sizing:border-box;padding:0;vertical-align:top;text-align:left">
<td style="box-sizing:border-box;word-break:break-word;border-collapse:collapse;padding:0 0 10px;vertical-align:top;text-align:left;color:#202020;font-weight:normal;margin:0;line-height:19px;font-size:14px">
</td>
<td style="box-sizing:border-box;word-break:break-word;border-collapse:collapse;padding:0 0 10px;vertical-align:top;text-align:left;color:#202020;font-weight:normal;margin:0;line-height:19px;font-size:14px">
{!! $item->preview !!}
</td>
</tr>
</tbody>
</table>
@endforeach
<br style="box-sizing:border-box">
<hr style="box-sizing:border-box;color:#d9d9d9;background-color:#d9d9d9;height:1px;border:none">
<br style="box-sizing:border-box">
</td>
</tr>
</tbody>
</table>
@endif