mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-11 02:12:53 +00:00
perf: 优化邮件通知
This commit is contained in:
parent
9f00047fdd
commit
1227a05e2d
@ -12,6 +12,7 @@ use Carbon\Carbon;
|
||||
use App\Module\Doo;
|
||||
use App\Models\User;
|
||||
use App\Module\Base;
|
||||
use App\Module\Timer;
|
||||
use App\Models\Setting;
|
||||
use App\Module\Extranet;
|
||||
use LdapRecord\Container;
|
||||
@ -180,11 +181,18 @@ class SystemController extends AbstractController
|
||||
'notice_msg',
|
||||
'msg_unread_user_minute',
|
||||
'msg_unread_group_minute',
|
||||
'msg_unread_time_ranges',
|
||||
'ignore_addr'
|
||||
])) {
|
||||
unset($all[$key]);
|
||||
}
|
||||
}
|
||||
$ranges = array_map(function ($item) {
|
||||
return !is_array($item) ? explode(',', $item) : $item;
|
||||
}, is_array($all['msg_unread_time_ranges']) ? $all['msg_unread_time_ranges'] : []);
|
||||
$all['msg_unread_time_ranges'] = array_values(array_filter($ranges, function ($item) {
|
||||
return count($item) == 2 && Timer::isTime($item[0]) && Timer::isTime($item[1]);
|
||||
}));
|
||||
$setting = Base::setting('emailSetting', Base::newTrim($all));
|
||||
} else {
|
||||
$setting = Base::setting('emailSetting');
|
||||
@ -198,6 +206,7 @@ class SystemController extends AbstractController
|
||||
$setting['notice_msg'] = $setting['notice_msg'] ?: 'close';
|
||||
$setting['msg_unread_user_minute'] = intval($setting['msg_unread_user_minute'] ?? -1);
|
||||
$setting['msg_unread_group_minute'] = intval($setting['msg_unread_group_minute'] ?? -1);
|
||||
$setting['msg_unread_time_ranges'] = is_array($setting['msg_unread_time_ranges']) ? $setting['msg_unread_time_ranges'] : [[]];
|
||||
$setting['ignore_addr'] = $setting['ignore_addr'] ?: '';
|
||||
//
|
||||
if ($type != 'save' && !in_array('admin', $user->identity)) {
|
||||
@ -768,7 +777,7 @@ class SystemController extends AbstractController
|
||||
if ($data['info']['people'] > 0 && $data['user_count'] > $data['info']['people']) {
|
||||
$data['error'][] = '终端用户数超过License限制';
|
||||
}
|
||||
if ($data['info']['expired_at'] && strtotime($data['info']['expired_at']) <= Base::time()) {
|
||||
if ($data['info']['expired_at'] && strtotime($data['info']['expired_at']) <= Timer::time()) {
|
||||
$data['error'][] = '终端License已过期';
|
||||
}
|
||||
//
|
||||
@ -1196,13 +1205,13 @@ class SystemController extends AbstractController
|
||||
if (count($userid) > 100) {
|
||||
return Base::retError('导出成员限制最多100个');
|
||||
}
|
||||
if (!(is_array($date) && Base::isDate($date[0]) && Base::isDate($date[1]))) {
|
||||
if (!(is_array($date) && Timer::isDate($date[0]) && Timer::isDate($date[1]))) {
|
||||
return Base::retError('日期选择错误');
|
||||
}
|
||||
if (Carbon::parse($date[1])->timestamp - Carbon::parse($date[0])->timestamp > 35 * 86400) {
|
||||
return Base::retError('日期范围限制最大35天');
|
||||
}
|
||||
if (!(is_array($time) && Base::isTime($time[0]) && Base::isTime($time[1]))) {
|
||||
if (!(is_array($time) && Timer::isTime($time[0]) && Timer::isTime($time[1]))) {
|
||||
return Base::retError('时间选择错误');
|
||||
}
|
||||
//
|
||||
@ -1244,7 +1253,7 @@ class SystemController extends AbstractController
|
||||
$lastRecord = $sameCollect?->whereBetween("datetime", $lastBetween)->last();
|
||||
$firstTimestamp = $firstRecord['timestamp'] ?: 0;
|
||||
$lastTimestamp = $lastRecord['timestamp'] ?: 0;
|
||||
if (Base::time() < $startT + $secondStart) {
|
||||
if (Timer::time() < $startT + $secondStart) {
|
||||
$firstResult = "-";
|
||||
} else {
|
||||
$firstResult = Doo::translate("正常");
|
||||
@ -1256,7 +1265,7 @@ class SystemController extends AbstractController
|
||||
$styles["E{$index}"] = ["font" => ["color" => ["rgb" => "436FF6"]]];
|
||||
}
|
||||
}
|
||||
if (Base::time() < $startT + $secondEnd) {
|
||||
if (Timer::time() < $startT + $secondEnd) {
|
||||
$lastResult = "-";
|
||||
$lastTimestamp = 0;
|
||||
} else {
|
||||
@ -1299,8 +1308,8 @@ class SystemController extends AbstractController
|
||||
} else {
|
||||
$fileName .= '的签到记录';
|
||||
}
|
||||
$fileName = Doo::translate($fileName) . '_' . Base::time() . '.xlsx';
|
||||
$filePath = "temp/checkin/export/" . date("Ym", Base::time());
|
||||
$fileName = Doo::translate($fileName) . '_' . Timer::time() . '.xlsx';
|
||||
$filePath = "temp/checkin/export/" . date("Ym", Timer::time());
|
||||
$export = new BillMultipleExport($sheets);
|
||||
$res = $export->store($filePath . "/" . $fileName);
|
||||
if ($res != 1) {
|
||||
|
||||
380
app/Module/Timer.php
Normal file
380
app/Module/Timer.php
Normal file
@ -0,0 +1,380 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module;
|
||||
|
||||
use App\Services\RequestContext;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class Timer
|
||||
{
|
||||
/**
|
||||
* 获取时间戳
|
||||
* @return int
|
||||
*/
|
||||
public static function time()
|
||||
{
|
||||
return intval(RequestContext::get("start_time", time()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取毫秒时间戳
|
||||
* @return float
|
||||
*/
|
||||
public static function msecTime()
|
||||
{
|
||||
list($msec, $sec) = explode(' ', microtime());
|
||||
$time = explode(".", $sec . ($msec * 1000));
|
||||
return $time[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间差(不够1个小时算一个小时)
|
||||
* @param int $s 开始时间戳
|
||||
* @param int $e 结束时间戳
|
||||
* @return string
|
||||
*/
|
||||
public static function timeDiff($s, $e)
|
||||
{
|
||||
$time = $e - $s;
|
||||
$days = 0;
|
||||
if ($time >= 86400) { // 如果大于1天
|
||||
$days = (int)($time / 86400);
|
||||
$time = $time % 86400; // 计算天后剩余的毫秒数
|
||||
}
|
||||
$hours = 0;
|
||||
if ($time >= 3600) { // 如果大于1小时
|
||||
$hours = (int)($time / 3600);
|
||||
$time = $time % 3600; // 计算小时后剩余的毫秒数
|
||||
}
|
||||
$minutes = ceil($time / 60); // 剩下的毫秒数都算作分
|
||||
$daysStr = $days > 0 ? $days . '天' : '';
|
||||
$hoursStr = ($hours > 0 || ($days > 0 && $minutes > 0)) ? $hours . '时' : '';
|
||||
$minuteStr = ($minutes > 0) ? $minutes . '分' : '';
|
||||
return $daysStr . $hoursStr . $minuteStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间秒数格式化
|
||||
* @param int $time 时间秒数
|
||||
* @return string
|
||||
*/
|
||||
public static function timeFormat($time)
|
||||
{
|
||||
$days = 0;
|
||||
if ($time >= 86400) { // 如果大于1天
|
||||
$days = (int)($time / 86400);
|
||||
$time = $time % 86400; // 计算天后剩余的毫秒数
|
||||
}
|
||||
$hours = 0;
|
||||
if ($time >= 3600) { // 如果大于1小时
|
||||
$hours = (int)($time / 3600);
|
||||
$time = $time % 3600; // 计算小时后剩余的毫秒数
|
||||
}
|
||||
$minutes = ceil($time / 60); // 剩下的毫秒数都算作分
|
||||
$daysStr = $days > 0 ? $days . '天' : '';
|
||||
$hoursStr = ($hours > 0 || ($days > 0 && $minutes > 0)) ? $hours . '时' : '';
|
||||
$minuteStr = ($minutes > 0) ? $minutes . '分' : '';
|
||||
return $daysStr . $hoursStr . $minuteStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测日期格式
|
||||
* @param string $str 需要检测的字符串
|
||||
* @return bool
|
||||
*/
|
||||
public static function isDate($str)
|
||||
{
|
||||
$strArr = explode('-', $str);
|
||||
if (empty($strArr) || count($strArr) != 3) {
|
||||
return false;
|
||||
} else {
|
||||
list($year, $month, $day) = $strArr;
|
||||
if (checkdate(intval($month), intval($day), intval($year))) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测时间格式
|
||||
* @param string $str 需要检测的字符串
|
||||
* @return bool
|
||||
*/
|
||||
public static function isTime($str)
|
||||
{
|
||||
$strArr = explode(':', $str);
|
||||
$count = count($strArr);
|
||||
if ($count < 2 || $count > 3) {
|
||||
return false;
|
||||
}
|
||||
$hour = $strArr[0];
|
||||
if ($hour < 0 || $hour > 23) {
|
||||
return false;
|
||||
}
|
||||
$minute = $strArr[1];
|
||||
if ($minute < 0 || $minute > 59) {
|
||||
return false;
|
||||
}
|
||||
if ($count == 3) {
|
||||
$second = $strArr[2];
|
||||
if ($second < 0 || $second > 59) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测 日期格式 或 时间格式
|
||||
* @param string $str 需要检测的字符串
|
||||
* @return bool
|
||||
*/
|
||||
public static function isDateOrTime($str)
|
||||
{
|
||||
return self::isDate($str) || self::isTime($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间转毫秒时间戳
|
||||
* @param $time
|
||||
* @return float|int
|
||||
*/
|
||||
public static function strtotimeM($time)
|
||||
{
|
||||
if (str_contains($time, '.')) {
|
||||
list($t, $m) = explode(".", $time);
|
||||
if (is_string($t)) {
|
||||
$t = strtotime($t);
|
||||
}
|
||||
$time = $t . str_pad($m, 3, "0", STR_PAD_LEFT);
|
||||
}
|
||||
if (is_numeric($time)) {
|
||||
return (int) str_pad($time, 13, "0");
|
||||
} else {
|
||||
return strtotime($time) * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间格式化
|
||||
* @param $date
|
||||
* @return false|string
|
||||
*/
|
||||
public static function forumDate($date)
|
||||
{
|
||||
$dur = time() - $date;
|
||||
if ($date > Carbon::now()->startOf('day')->timestamp) {
|
||||
//今天
|
||||
if ($dur < 60) {
|
||||
return max($dur, 1) . '秒前';
|
||||
} elseif ($dur < 3600) {
|
||||
return floor($dur / 60) . '分钟前';
|
||||
} elseif ($dur < 86400) {
|
||||
return floor($dur / 3600) . '小时前';
|
||||
} else {
|
||||
return date("H:i", $date);
|
||||
}
|
||||
} elseif ($date > Carbon::now()->subDays()->startOf('day')->timestamp) {
|
||||
//昨天
|
||||
return '昨天';
|
||||
} elseif ($date > Carbon::now()->subDays(2)->startOf('day')->timestamp) {
|
||||
//前天
|
||||
return '前天';
|
||||
} elseif ($dur > 86400) {
|
||||
//x天前
|
||||
return floor($dur / 86400) . '天前';
|
||||
}
|
||||
return date("Y-m-d", $date);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取(时间戳转)今天是星期几,只返回(几)
|
||||
* @param string|number $unixTime
|
||||
* @return string
|
||||
*/
|
||||
public static function getWeek($unixTime = '')
|
||||
{
|
||||
$unixTime = is_numeric($unixTime) ? $unixTime : time();
|
||||
$weekarray = ['日', '一', '二', '三', '四', '五', '六'];
|
||||
return $weekarray[date('w', $unixTime)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取(时间戳转)现在时间段:深夜、凌晨、早晨、上午.....
|
||||
* @param string|number $unixTime
|
||||
* @return string
|
||||
*/
|
||||
public static function getDayeSegment($unixTime = '')
|
||||
{
|
||||
$unixTime = is_numeric($unixTime) ? $unixTime : time();
|
||||
$H = date('H', $unixTime);
|
||||
if ($H >= 19) {
|
||||
return '晚上';
|
||||
} elseif ($H >= 18) {
|
||||
return '傍晚';
|
||||
} elseif ($H >= 13) {
|
||||
return '下午';
|
||||
} elseif ($H >= 12) {
|
||||
return '中午';
|
||||
} elseif ($H >= 8) {
|
||||
return '上午';
|
||||
} elseif ($H >= 5) {
|
||||
return '早晨';
|
||||
} elseif ($H >= 1) {
|
||||
return '凌晨';
|
||||
} elseif ($H >= 0) {
|
||||
return '深夜';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 秒 (转) 年、天、时、分、秒
|
||||
* @param $time
|
||||
* @return array|bool
|
||||
*/
|
||||
public static function sec2time($time)
|
||||
{
|
||||
if (is_numeric($time)) {
|
||||
$value = array(
|
||||
"years" => 0, "days" => 0, "hours" => 0,
|
||||
"minutes" => 0, "seconds" => 0,
|
||||
);
|
||||
if ($time >= 86400) {
|
||||
$value["days"] = floor($time / 86400);
|
||||
$time = ($time % 86400);
|
||||
}
|
||||
if ($time >= 3600) {
|
||||
$value["hours"] = floor($time / 3600);
|
||||
$time = ($time % 3600);
|
||||
}
|
||||
if ($time >= 60) {
|
||||
$value["minutes"] = floor($time / 60);
|
||||
$time = ($time % 60);
|
||||
}
|
||||
$value["seconds"] = floor($time);
|
||||
return (array)$value;
|
||||
} else {
|
||||
return (bool)FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 年、天、时、分、秒 (转) 秒
|
||||
* @param $value
|
||||
* @return int
|
||||
*/
|
||||
public static function time2sec($value)
|
||||
{
|
||||
$time = intval($value["seconds"]);
|
||||
$time += intval($value["minutes"] * 60);
|
||||
$time += intval($value["hours"] * 3600);
|
||||
$time += intval($value["days"] * 86400);
|
||||
$time += intval($value["years"] * 31536000);
|
||||
return $time;
|
||||
}
|
||||
|
||||
/**
|
||||
* 阿拉伯数字转化为中文
|
||||
* @param $num
|
||||
* @return string
|
||||
*/
|
||||
public static function chinaNum($num)
|
||||
{
|
||||
$china = array('零', '一', '二', '三', '四', '五', '六', '七', '八', '九');
|
||||
$arr = str_split($num);
|
||||
$txt = '';
|
||||
for ($i = 0; $i < count($arr); $i++) {
|
||||
$txt .= $china[$arr[$i]];
|
||||
}
|
||||
return $txt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 阿拉伯数字转化为中文(用于星期,七改成日)
|
||||
* @param $num
|
||||
* @return string
|
||||
*/
|
||||
public static function chinaNumZ($num)
|
||||
{
|
||||
return str_replace("七", "日", Timer::chinaNum($num));
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间是否在时间范围内
|
||||
* @param array $timeRanges 如:['08:00', '12:00'] 或 [['08:00', '12:00'], ['14:00', '18:00']]
|
||||
* @param string|null $currentTime
|
||||
* @return bool
|
||||
*/
|
||||
public static function isTimeInRanges(array $timeRanges, ?string $currentTime = null): bool
|
||||
{
|
||||
// 如果没有传入当前时间,使用当前时间
|
||||
$currentTime = $currentTime ?? date('H:i');
|
||||
|
||||
// 转换当前时间为分钟数,便于比较
|
||||
$currentMinutes = self::timeToMinutes($currentTime);
|
||||
if ($currentMinutes === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 将单个时间范围转换为数组格式
|
||||
if (isset($timeRanges[0]) && !is_array($timeRanges[0])) {
|
||||
$timeRanges = [$timeRanges];
|
||||
}
|
||||
|
||||
// 过滤并检查有效的时间范围
|
||||
foreach ($timeRanges as $range) {
|
||||
if (!self::isValidTimeRange($range)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$startMinutes = self::timeToMinutes($range[0]);
|
||||
$endMinutes = self::timeToMinutes($range[1]);
|
||||
|
||||
if ($startMinutes === false || $endMinutes === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($startMinutes <= $currentMinutes && $currentMinutes <= $endMinutes) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助函数:检查时间范围是否有效
|
||||
* @param $range
|
||||
* @return bool
|
||||
*/
|
||||
private static function isValidTimeRange($range): bool
|
||||
{
|
||||
return is_array($range)
|
||||
&& count($range) === 2
|
||||
&& is_string($range[0])
|
||||
&& is_string($range[1])
|
||||
&& !empty($range[0])
|
||||
&& !empty($range[1])
|
||||
&& preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $range[0])
|
||||
&& preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $range[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助函数:将时间转换为分钟数
|
||||
* @param string $time
|
||||
* @return false|float|int
|
||||
*/
|
||||
private static function timeToMinutes(string $time)
|
||||
{
|
||||
if (!preg_match('/^([01]?[0-9]|2[0-3]):([0-5][0-9])$/', $time, $matches)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return intval($matches[1]) * 60 + intval($matches[2]);
|
||||
}
|
||||
|
||||
}
|
||||
@ -8,165 +8,276 @@ use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\WebSocketDialogMsgRead;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Timer;
|
||||
use Carbon\Carbon;
|
||||
use Guanguans\Notify\Factory;
|
||||
use Guanguans\Notify\Messages\EmailMessage;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
/**
|
||||
* 未读消息邮件通知任务
|
||||
* 根据设置的时间范围,将未读消息通过邮件发送给用户
|
||||
*/
|
||||
class EmailNoticeTask extends AbstractTask
|
||||
{
|
||||
/** @var array 允许发送通知的消息类型 */
|
||||
private const ALLOWED_MSG_TYPES = ["text", "file", "record", "meeting"];
|
||||
|
||||
/** @var int 每批处理的数据量 */
|
||||
private const CHUNK_SIZE = 100;
|
||||
|
||||
/** @var array 邮件相关设置 */
|
||||
private array $emailSetting;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->emailSetting = Base::setting('emailSetting');
|
||||
}
|
||||
|
||||
public function start()
|
||||
{
|
||||
$setting = Base::setting('emailSetting');
|
||||
// 消息通知
|
||||
if ($setting['notice_msg'] === 'open') {
|
||||
$userMinute = intval($setting['msg_unread_user_minute']);
|
||||
$groupMinute = intval($setting['msg_unread_group_minute']);
|
||||
\DB::statement("SET SQL_MODE=''");
|
||||
$builder = 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')
|
||||
->whereNull("r.read_at")
|
||||
->where("r.silence", 0)
|
||||
->where("r.email", 0);
|
||||
if ($userMinute > -1) {
|
||||
$builder->clone()
|
||||
->where("web_socket_dialog_msgs.dialog_type", "user")
|
||||
->whereIn("web_socket_dialog_msgs.type", ["text", "file", "record", "meeting"])
|
||||
->whereBetween("web_socket_dialog_msgs.created_at", [
|
||||
Carbon::now()->subMinutes($userMinute + 10),
|
||||
Carbon::now()->subMinutes($userMinute)
|
||||
])
|
||||
->groupBy('r_userid')
|
||||
->chunkById(100, function ($rows) {
|
||||
$this->unreadMsgEmail($rows, "user");
|
||||
});
|
||||
}
|
||||
if ($groupMinute > -1) {
|
||||
$builder->clone()
|
||||
->where("web_socket_dialog_msgs.dialog_type", "group")
|
||||
->whereIn("web_socket_dialog_msgs.type", ["text", "file", "record", "meeting"])
|
||||
->whereBetween("web_socket_dialog_msgs.created_at", [
|
||||
Carbon::now()->subMinutes($groupMinute + 10),
|
||||
Carbon::now()->subMinutes($groupMinute)
|
||||
])
|
||||
->groupBy('r_userid')
|
||||
->chunkById(100, function ($rows) {
|
||||
$this->unreadMsgEmail($rows, "group");
|
||||
});
|
||||
}
|
||||
// 检查是否可以发送邮件
|
||||
if (!$this->canSendEmails()) {
|
||||
return;
|
||||
}
|
||||
|
||||
\DB::statement("SET SQL_MODE=''");
|
||||
|
||||
// 分别处理用户消息和群组消息
|
||||
$this->processMessages('user');
|
||||
$this->processMessages('group');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以发送邮件通知
|
||||
* 需要开启通知功能且在指定的时间范围内
|
||||
*/
|
||||
private function canSendEmails(): bool
|
||||
{
|
||||
if ($this->emailSetting['notice_msg'] !== 'open') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$timeRanges = is_array($this->emailSetting['msg_unread_time_ranges'])
|
||||
? $this->emailSetting['msg_unread_time_ranges']
|
||||
: [];
|
||||
|
||||
return Timer::isTimeInRanges($timeRanges);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理指定类型的未读消息
|
||||
* @param string $dialogType 对话类型:user|group
|
||||
*/
|
||||
private function processMessages(string $dialogType): void
|
||||
{
|
||||
// 获取未读时间限制(分钟)
|
||||
$minute = $dialogType === 'user'
|
||||
? intval($this->emailSetting['msg_unread_user_minute'])
|
||||
: intval($this->emailSetting['msg_unread_group_minute']);
|
||||
|
||||
if ($minute <= -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取上次处理时间
|
||||
$lastProcessKey = 'time' . ucfirst($dialogType);
|
||||
$startTime = Base::settingFind('emailLastNotice', $lastProcessKey);
|
||||
$startTime = $startTime ? Carbon::parse($startTime) : Carbon::today();
|
||||
|
||||
// 计算本次处理的结束时间(当前时间减去未读时间限制)
|
||||
$endTime = Carbon::now()->subMinutes($minute);
|
||||
|
||||
// 如果开始时间晚于结束时间,则不处理
|
||||
if ($startTime->isAfter($endTime)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取需要处理的用户列表
|
||||
$query = WebSocketDialogMsgRead::select('web_socket_dialog_msg_reads.userid')
|
||||
->join('web_socket_dialog_msgs as m', 'm.id', '=', 'web_socket_dialog_msg_reads.msg_id')
|
||||
->whereNull('web_socket_dialog_msg_reads.read_at')
|
||||
->where('web_socket_dialog_msg_reads.silence', 0)
|
||||
->where('web_socket_dialog_msg_reads.email', 0)
|
||||
->where('m.dialog_type', $dialogType)
|
||||
->whereBetween('m.created_at', [$startTime, $endTime])
|
||||
->whereIn('m.type', self::ALLOWED_MSG_TYPES)
|
||||
->orderBy('web_socket_dialog_msg_reads.userid')
|
||||
->groupBy('web_socket_dialog_msg_reads.userid');
|
||||
|
||||
// 分批处理用户的未读消息
|
||||
$query->chunk(self::CHUNK_SIZE, function($users) use ($dialogType, $startTime, $endTime) {
|
||||
foreach ($users as $userData) {
|
||||
$this->sendUserEmail($userData->userid, $dialogType, $startTime, $endTime);
|
||||
}
|
||||
});
|
||||
|
||||
// 更新处理时间
|
||||
Base::setting('emailLastNotice', [
|
||||
$lastProcessKey => $endTime->toDateTimeString()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送用户的未读消息邮件
|
||||
*/
|
||||
private function sendUserEmail(int $userId, string $dialogType, Carbon $startTime, Carbon $endTime): void
|
||||
{
|
||||
// 验证用户
|
||||
$user = User::find($userId);
|
||||
if (!$user || $user->bot || !is_null($user->disable_at) || !Base::isEmail($user->email)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取未读消息
|
||||
$messages = $this->getUnreadMessages($userId, $dialogType, $startTime, $endTime);
|
||||
if ($messages->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置用户语言
|
||||
Doo::setLanguage($user->lang);
|
||||
|
||||
// 按对话分组并生成邮件内容
|
||||
$messagesByDialog = $messages->groupBy('dialog_id');
|
||||
$emailContent = $this->generateEmailContent($user, $messagesByDialog, $dialogType);
|
||||
|
||||
try {
|
||||
// 发送邮件
|
||||
$this->sendEmail($user, $emailContent);
|
||||
// 标记消息已发送邮件
|
||||
WebSocketDialogMsgRead::whereIn('id', $messages->pluck('r_id'))
|
||||
->update(['email' => 1]);
|
||||
} catch (\Throwable $e) {
|
||||
info("Email send failed for user {$userId}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的未读消息
|
||||
*/
|
||||
private function getUnreadMessages($userId, $dialogType, Carbon $startTime, Carbon $endTime)
|
||||
{
|
||||
return 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([
|
||||
'r.userid' => $userId,
|
||||
'r.silence' => 0,
|
||||
'r.email' => 0,
|
||||
'web_socket_dialog_msgs.dialog_type' => $dialogType
|
||||
])
|
||||
->whereNull('r.read_at')
|
||||
->whereBetween('web_socket_dialog_msgs.created_at', [$startTime, $endTime])
|
||||
->whereIn('web_socket_dialog_msgs.type', self::ALLOWED_MSG_TYPES)
|
||||
->orderBy('web_socket_dialog_msgs.created_at')
|
||||
->limit(self::CHUNK_SIZE)
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成邮件内容
|
||||
*/
|
||||
private function generateEmailContent($user, $messagesByDialog, $dialogType)
|
||||
{
|
||||
$msgType = $dialogType === "group" ? "群聊" : "单聊";
|
||||
|
||||
// 生成邮件头部
|
||||
$content = view('email.unread', [
|
||||
'type' => 'head',
|
||||
'title' => Doo::translate(sprintf('%s,您好。', $user->nickname)),
|
||||
'desc' => Doo::translate(sprintf('您有%d条未读%s消息,请及时处理。', count($messagesByDialog), $msgType)),
|
||||
])->render();
|
||||
|
||||
$subject = null;
|
||||
// 处理每个对话的消息
|
||||
foreach ($messagesByDialog as $items) {
|
||||
$dialogId = 0;
|
||||
$dialogName = null;
|
||||
|
||||
foreach ($items as $item) {
|
||||
$item->cancelAppend();
|
||||
$item->userInfo = User::userid2basic($item->userid, ['lang']);
|
||||
Doo::setLanguage($item->userInfo->lang);
|
||||
$item->preview = WebSocketDialogMsg::previewMsg($item, true);
|
||||
$item->preview = str_replace('<p>', '<p style="margin:0;padding:0">', $item->preview);
|
||||
|
||||
if (empty($dialogId)) {
|
||||
$dialogId = $item->dialog_id;
|
||||
}
|
||||
if ($dialogName === null) {
|
||||
$dialogName = $this->getDialogName($item, $dialogType);
|
||||
}
|
||||
}
|
||||
|
||||
// 生成邮件主题
|
||||
if ($subject === null) {
|
||||
$subject = count($messagesByDialog) > 1
|
||||
? sprintf('来自%d个%s未读消息提醒', count($messagesByDialog), $msgType)
|
||||
: sprintf('来自%s未读消息提醒', $dialogName);
|
||||
}
|
||||
|
||||
// 添加对话内容
|
||||
$content .= view('email.unread', [
|
||||
'type' => 'content',
|
||||
'dialogUrl' => config("app.url") . "/manage/messenger?dialog_id={$dialogId}",
|
||||
'dialogName' => $dialogName,
|
||||
'title' => Doo::translate(sprintf('%d条未读信息', count($items))),
|
||||
'button' => Doo::translate('回复消息'),
|
||||
'unread' => count($items),
|
||||
'items' => $items,
|
||||
])->render();
|
||||
}
|
||||
|
||||
$content = str_replace("{{RemoteURL}}", config("app.url") . "/", $content);
|
||||
|
||||
return [
|
||||
'subject' => Doo::translate($subject),
|
||||
'content' => $content
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话名称
|
||||
*/
|
||||
private function getDialogName($message, $dialogType)
|
||||
{
|
||||
if ($dialogType === "user" && $message->userInfo) {
|
||||
return $message->userInfo->profession
|
||||
? sprintf('%s (%s)', $message->userInfo->nickname, $message->userInfo->profession)
|
||||
: $message->userInfo->nickname;
|
||||
}
|
||||
return $message->webSocketDialog?->getGroupName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮件
|
||||
*/
|
||||
private function sendEmail($user, $emailData): void
|
||||
{
|
||||
Setting::validateAddr($user->email, function($to) use ($emailData) {
|
||||
Factory::mailer()
|
||||
->setDsn(sprintf(
|
||||
'smtp://%s:%s@%s:%s?verify_peer=0',
|
||||
$this->emailSetting['account'],
|
||||
$this->emailSetting['password'],
|
||||
$this->emailSetting['smtp_server'],
|
||||
$this->emailSetting['port']
|
||||
))
|
||||
->setMessage(EmailMessage::create()
|
||||
->from(sprintf('%s <%s>', env('APP_NAME', 'Task'), $this->emailSetting['account']))
|
||||
->to($to)
|
||||
->subject($emailData['subject'])
|
||||
->html($emailData['content']))
|
||||
->send();
|
||||
});
|
||||
}
|
||||
|
||||
public function end()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 未读消息通知
|
||||
* @param $rows
|
||||
* @param $dialogType
|
||||
* @return void
|
||||
*/
|
||||
private function unreadMsgEmail($rows, $dialogType)
|
||||
{
|
||||
$array = $rows->groupBy('r_userid');
|
||||
foreach ($array as $userid => $data) {
|
||||
$data = 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')
|
||||
->whereNull("r.read_at")
|
||||
->where("r.silence", 0)
|
||||
->where("r.email", 0)
|
||||
->where("r.userid", $userid)
|
||||
->where("web_socket_dialog_msgs.dialog_type", $dialogType)
|
||||
->whereIn("web_socket_dialog_msgs.type", ["text", "file", "record", "meeting"])
|
||||
->take(100)
|
||||
->get();
|
||||
if (empty($data)) {
|
||||
continue;
|
||||
}
|
||||
$user = User::whereBot(0)->whereNull('disable_at')->find($userid);
|
||||
if (empty($user)) {
|
||||
continue;
|
||||
}
|
||||
if (!Base::isEmail($user->email)) {
|
||||
continue;
|
||||
}
|
||||
$setting = Base::setting('emailSetting');
|
||||
$msgType = $dialogType === "group" ? "群聊" : "成员";
|
||||
$subject = null;
|
||||
$content = view('email.unread', [
|
||||
'type' => 'head',
|
||||
'nickname' => $user->nickname,
|
||||
'msgType' => $msgType,
|
||||
'count' => count($data),
|
||||
])->render();
|
||||
$lists = $data->groupBy('dialog_id');
|
||||
/** @var WebSocketDialogMsg[] $items */
|
||||
foreach ($lists as $items) {
|
||||
$dialogId = 0;
|
||||
$dialogName = null;
|
||||
foreach ($items as $item) {
|
||||
$item->cancelAppend();
|
||||
$item->userInfo = User::userid2basic($item->userid, ['lang']);
|
||||
Doo::setLanguage($item->userInfo->lang);
|
||||
$item->preview = WebSocketDialogMsg::previewMsg($item, true);
|
||||
$item->preview = str_replace('<p>', '<p style="margin:0;padding:0">', $item->preview);
|
||||
if (empty($dialogId)) {
|
||||
$dialogId = $item->dialog_id;
|
||||
}
|
||||
if ($dialogName === null) {
|
||||
if ($dialogType === "user" && $item->userInfo) {
|
||||
if ($item->userInfo->profession) {
|
||||
$dialogName = $item->userInfo->nickname . " ({$item->userInfo->profession})";
|
||||
} else {
|
||||
$dialogName = $item->userInfo->nickname;
|
||||
}
|
||||
} else {
|
||||
$dialogName = $item->webSocketDialog?->getGroupName();
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($subject === null) {
|
||||
$count = count($lists);
|
||||
if ($count > 1) {
|
||||
$subject = "来自{$count}个{$msgType}未读消息提醒";
|
||||
} else {
|
||||
$subject = "来自{$dialogName}未读消息提醒";
|
||||
}
|
||||
}
|
||||
$content .= view('email.unread', [
|
||||
'type' => 'content',
|
||||
'dialogUrl' => config("app.url") . "/manage/messenger?dialog_id={$dialogId}",
|
||||
'dialogName' => $dialogName,
|
||||
'unread' => count($items),
|
||||
'items' => $items,
|
||||
])->render();
|
||||
$content = str_replace("{{RemoteURL}}", config("app.url") . "/", $content);
|
||||
}
|
||||
try {
|
||||
Setting::validateAddr($user->email, function($to) use ($content, $subject, $setting) {
|
||||
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($to)
|
||||
->subject($subject)
|
||||
->html($content))
|
||||
->send();
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
info("unreadMsgEmail: " . $e->getMessage());
|
||||
}
|
||||
WebSocketDialogMsgRead::whereIn('id', $data->pluck('r_id'))->update([
|
||||
'email' => 1
|
||||
]);
|
||||
}
|
||||
// 任务结束处理
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,6 +49,23 @@
|
||||
<Radio label="close">{{ $L('关闭') }}</Radio>
|
||||
</RadioGroup>
|
||||
<Form v-if="formData.notice_msg == 'open'" @submit.native.prevent class="block-setting-msg-unread">
|
||||
<FormItem :label="$L('通知时间')">
|
||||
<div class="input-range-box">
|
||||
<div
|
||||
v-for="(_, index) in formData.msg_unread_time_ranges"
|
||||
:key="index"
|
||||
class="input-range-item">
|
||||
<TimePicker
|
||||
v-model="formData.msg_unread_time_ranges[index]"
|
||||
type="timerange"
|
||||
format="HH:mm"
|
||||
:placeholder="$L('选择时间范围')"
|
||||
transfer/>
|
||||
</div>
|
||||
<Button type="default" icon="md-add" @click="onAddTimeRange">{{ $L('添加时间范围') }}</Button>
|
||||
<div class="form-tip">{{ $L('仅在通知时间内发送邮件。') }}</div>
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem :label="$L('未读个人消息')" prop="msg_unread_user_minute">
|
||||
<div class="input-number-box">
|
||||
<InputNumber v-model="formData.msg_unread_user_minute" :min="0" :step="1"/>
|
||||
@ -104,6 +121,7 @@ export default {
|
||||
msg_unread_user_minute: -1,
|
||||
msg_unread_group_minute: -1,
|
||||
ignore_addr: '',
|
||||
msg_unread_time_ranges:[[]]
|
||||
},
|
||||
ruleData: {},
|
||||
}
|
||||
@ -130,6 +148,14 @@ export default {
|
||||
this.formData = $A.cloneJSON(this.formDatum_bak);
|
||||
},
|
||||
|
||||
onAddTimeRange() {
|
||||
if (this.formData.msg_unread_time_ranges.length > 5) {
|
||||
$A.messageError('最多添加5个时间范围');
|
||||
return;
|
||||
}
|
||||
this.formData.msg_unread_time_ranges.push([])
|
||||
},
|
||||
|
||||
systemSetting(save) {
|
||||
this.loadIng++;
|
||||
this.$store.dispatch("call", {
|
||||
|
||||
6
resources/assets/sass/pages/common.scss
vendored
6
resources/assets/sass/pages/common.scss
vendored
@ -54,6 +54,12 @@ body {
|
||||
font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.input-range-box {
|
||||
.input-range-item {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-number-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -3,9 +3,9 @@
|
||||
<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 }})条未读{{ $msgType }}消息,请及时处理。</p>
|
||||
<hr style='box-sizing:border-box;color:#d9d9d9;background-color:#d9d9d9;height:1px;border:none;margin-top:32px'>
|
||||
<p>{{ $title }} </p>
|
||||
<p>{{ $desc }}</p>
|
||||
<hr style="box-sizing:border-box;color:#d9d9d9;background-color:#d9d9d9;height:1px;border:none;margin-top:32px">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -19,7 +19,7 @@
|
||||
{{ $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:14px">
|
||||
{{ $unread }}条未读信息
|
||||
{{ $title }}
|
||||
</h4>
|
||||
<br style="box-sizing:border-box">
|
||||
@foreach($items as $item)
|
||||
@ -51,7 +51,7 @@
|
||||
<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">
|
||||
<a style="text-decoration:none; box-sizing:border-box; color:white; background-color:#46bc99; padding:8px 16px; border-radius:4px; font-size:10px; text-transform:uppercase; font-weight:bold" href="{{ $dialogUrl }}" target="_blank">回复消息</a>
|
||||
<a style="text-decoration:none; box-sizing:border-box; color:white; background-color:#46bc99; padding:8px 16px; border-radius:4px; font-size:10px; text-transform:uppercase; font-weight:bold" href="{{ $dialogUrl }}" target="_blank">{{ $button }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user