这个问题已经 9 个月大了,所以我不确定 OP 是否仍然需要答案,但由于有很多观点和美味的赏金,我还想添加我的芥末(德国谚语..)。
在这篇文章中,我将尝试做一个简单的解释示例,说明如何开始构建通知系统。
编辑:好吧,结果比我预期的要长得多。最后真的累了,对不起。
WTLDR;
问题 1:在每个通知上都有一个标记。
问题 2: 仍然将每个通知作为单个记录存储在数据库中,并在请求时将它们分组。
结构
我假设通知看起来像:
+---------------------------------------------+
| ▣ James has uploaded new Homework: Math 1+1 |
+---------------------------------------------+
| ▣ Jane and John liked your comment: Im s... |
+---------------------------------------------+
| ▢ The School is closed on independence day. |
+---------------------------------------------+
在窗帘后面可能看起来像这样:
+--------+-----------+--------+-----------------+-------------------------------------------+
| unread | recipient | sender | type | reference |
+--------+-----------+--------+-----------------+-------------------------------------------+
| true | me | James | homework.create | Math 1 + 1 |
+--------+-----------+--------+-----------------+-------------------------------------------+
| true | me | Jane | comment.like | Im sick of school |
+--------+-----------+--------+-----------------+-------------------------------------------+
| true | me | John | comment.like | Im sick of school |
+--------+-----------+--------+-----------------+-------------------------------------------+
| false | me | system | message | The School is closed on independence day. |
+--------+-----------+--------+-----------------+-------------------------------------------+
注意:我不建议在数据库中对通知进行分组,在运行时这样做可以使事情更加灵活。
-
未读
每个通知都应该有一个标志来指示收件人是否已经打开了通知。
-
收件人
定义接收通知的人员。
-
发件人
定义谁触发了通知。
-
类型
而不是让数据库中的每条消息都以纯文本形式创建类型。通过这种方式,您可以为后端内的不同通知类型创建特殊处理程序。将减少存储在数据库中的数据量,并为您提供更大的灵活性,可以轻松翻译通知、更改过去的消息等。
-
参考
大多数通知都会引用您的数据库或应用程序中的记录。
我一直在研究的每个系统在通知上都有一个简单的 1 对 1 引用关系,您可能会有 1 对 n 记住,我将以 1:1 继续我的示例。这也意味着我不需要定义引用什么类型的对象的字段,因为这是由通知类型定义的。
SQL 表
现在,在为 SQL 定义真正的表结构时,我们在数据库设计方面做出了一些决定。我将采用最简单的解决方案,如下所示:
+--------------+--------+---------------------------------------------------------+
| column | type | description |
+--------------+--------+---------------------------------------------------------+
| id | int | Primary key |
+--------------+--------+---------------------------------------------------------+
| recipient_id | int | The receivers user id. |
+--------------+--------+---------------------------------------------------------+
| sender_id | int | The sender's user id. |
+--------------+--------+---------------------------------------------------------+
| unread | bool | Flag if the recipient has already read the notification |
+--------------+--------+---------------------------------------------------------+
| type | string | The notification type. |
+--------------+--------+---------------------------------------------------------+
| parameters | array | Additional data to render different notification types. |
+--------------+--------+---------------------------------------------------------+
| reference_id | int | The primary key of the referencing object. |
+--------------+--------+---------------------------------------------------------+
| created_at | int | Timestamp of the notification creation date. |
+--------------+--------+---------------------------------------------------------+
或者对于懒人来说,SQL 创建表命令:
CREATE TABLE `notifications` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`recipient_id` int(11) NOT NULL,
`sender_id` int(11) NOT NULL,
`unread` tinyint(1) NOT NULL DEFAULT '1',
`type` varchar(255) NOT NULL DEFAULT '',
`parameters` text NOT NULL,
`reference_id` int(11) NOT NULL,
`created_at` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
PHP 服务
此实现完全取决于您的应用程序的需求,注意:这不是关于如何在 PHP 中构建通知系统的黄金标准的示例。
通知模型
这是一个通知本身的基本模型示例,只是需要的属性和抽象方法messageForNotification 和messageForNotifications 我们期望在不同的通知类型中实现。
abstract class Notification
{
protected $recipient;
protected $sender;
protected $unread;
protected $type;
protected $parameters;
protected $referenceId;
protected $createdAt;
/**
* Message generators that have to be defined in subclasses
*/
public function messageForNotification(Notification $notification) : string;
public function messageForNotifications(array $notifications) : string;
/**
* Generate message of the current notification.
*/
public function message() : string
{
return $this->messageForNotification($this);
}
}
你必须自己添加一个constructor、getters、setters之类的东西,我是不会提供现成的通知系统。
通知类型
现在您可以为每种类型创建一个新的Notification 子类。以下示例将处理评论的 like 操作:
- Ray 喜欢您的评论。 (1 条通知)
- John 和 Jane 喜欢您的评论。 (2 个通知)
- Jane、Johnny、James 和 Jenny 喜欢您的评论。 (4 条通知)
- Jonny、James 和其他 12 人喜欢您的评论。 (14 条通知)
示例实现:
namespace Notification\Comment;
class CommentLikedNotification extends \Notification
{
/**
* Generate a message for a single notification
*
* @param Notification $notification
* @return string
*/
public function messageForNotification(Notification $notification) : string
{
return $this->sender->getName() . 'has liked your comment: ' . substr($this->reference->text, 0, 10) . '...';
}
/**
* Generate a message for a multiple notifications
*
* @param array $notifications
* @return string
*/
public function messageForNotifications(array $notifications, int $realCount = 0) : string
{
if ($realCount === 0) {
$realCount = count($notifications);
}
// when there are two
if ($realCount === 2) {
$names = $this->messageForTwoNotifications($notifications);
}
// less than five
elseif ($realCount < 5) {
$names = $this->messageForManyNotifications($notifications);
}
// to many
else {
$names = $this->messageForManyManyNotifications($notifications, $realCount);
}
return $names . ' liked your comment: ' . substr($this->reference->text, 0, 10) . '...';
}
/**
* Generate a message for two notifications
*
* John and Jane has liked your comment.
*
* @param array $notifications
* @return string
*/
protected function messageForTwoNotifications(array $notifications) : string
{
list($first, $second) = $notifications;
return $first->getName() . ' and ' . $second->getName(); // John and Jane
}
/**
* Generate a message many notifications
*
* Jane, Johnny, James and Jenny has liked your comment.
*
* @param array $notifications
* @return string
*/
protected function messageForManyNotifications(array $notifications) : string
{
$last = array_pop($notifications);
foreach($notifications as $notification) {
$names .= $notification->getName() . ', ';
}
return substr($names, 0, -2) . ' and ' . $last->getName(); // Jane, Johnny, James and Jenny
}
/**
* Generate a message for many many notifications
*
* Jonny, James and 12 other have liked your comment.
*
* @param array $notifications
* @return string
*/
protected function messageForManyManyNotifications(array $notifications, int $realCount) : string
{
list($first, $second) = array_slice($notifications, 0, 2);
return $first->getName() . ', ' . $second->getName() . ' and ' . $realCount . ' others'; // Jonny, James and 12 other
}
}
通知管理器
要在应用程序中处理通知,请创建通知管理器之类的东西:
class NotificationManager
{
protected $notificationAdapter;
public function add(Notification $notification);
public function markRead(array $notifications);
public function get(User $user, $limit = 20, $offset = 0) : array;
}
在此示例 mysql 中,notificationAdapter 属性应包含与您的数据后端直接通信的逻辑。
创建通知
使用mysql触发器并没有错,因为没有错误的解决方案。 什么有效,什么有效.. 但我强烈建议不要让数据库处理应用程序逻辑。
所以在通知管理器中你可能想做这样的事情:
public function add(Notification $notification)
{
// only save the notification if no possible duplicate is found.
if (!$this->notificationAdapter->isDoublicate($notification))
{
$this->notificationAdapter->add([
'recipient_id' => $notification->recipient->getId(),
'sender_id' => $notification->sender->getId()
'unread' => 1,
'type' => $notification->type,
'parameters' => $notification->parameters,
'reference_id' => $notification->reference->getId(),
'created_at' => time(),
]);
}
}
notificationAdapter 的 add 方法后面可以是原始的 mysql 插入命令。使用此适配器抽象,您可以轻松地从 mysql 切换到基于文档的数据库,例如 mongodb,这对于通知系统来说是有意义的。
notificationAdapter 上的isDoublicate 方法应该简单地检查是否已经存在具有相同recipient、sender、type 和reference 的通知。
我不能指出这只是一个例子。(另外我真的必须缩短接下来的步骤,这篇文章变得非常长 -.-)
所以假设你有某种控制器,当老师上传作业时有一个动作:
function uploadHomeworkAction(Request $request)
{
// handle the homework and have it stored in the var $homework.
// how you handle your services is up to you...
$notificationManager = new NotificationManager;
foreach($homework->teacher->students as $student)
{
$notification = new Notification\Homework\HomeworkUploadedNotification;
$notification->sender = $homework->teacher;
$notification->recipient = $student;
$notification->reference = $homework;
// send the notification
$notificationManager->add($notification);
}
}
会在每位教师的学生上传新作业时创建通知。
阅读通知
现在是困难的部分。在 PHP 端进行分组的问题是您必须加载当前用户的 all 通知才能正确分组。这会很糟糕,如果你只有几个用户,它可能仍然没有问题,但这并不能使它变得好。
简单的解决方案是简单地限制请求的通知数量并仅对它们进行分组。当没有太多类似的通知(例如每 20 个通知 3-4 个)时,这将正常工作。但是假设用户/学生的帖子获得了大约一百个喜欢,而您只选择了最后 20 个通知。然后用户只会看到 20 人喜欢他的帖子,这也是他唯一的通知。
“正确”的解决方案是对数据库中已有的通知进行分组,并为每个通知组选择一些样本。比您只需将实际计数注入您的通知消息。
您可能没有阅读下面的文字,所以让我继续sn-p:
select *, count(*) as count from notifications
where recipient_id = 1
group by `type`, `reference_id`
order by created_at desc, unread desc
limit 20
现在您知道给定用户应该有哪些通知以及该组包含多少通知。
现在是糟糕的部分。如果不对每个组进行查询,我仍然找不到更好的方法来为每个组选择有限数量的通知。 这里的所有建议都非常欢迎。
所以我做了类似的事情:
$notifcationGroups = [];
foreach($results as $notification)
{
$notifcationGroup = ['count' => $notification['count']];
// when the group only contains one item we don't
// have to select it's children
if ($notification['count'] == 1)
{
$notifcationGroup['items'] = [$notification];
}
else
{
// example with query builder
$notifcationGroup['items'] = $this->select('notifications')
->where('recipient_id', $recipient_id)
->andWehere('type', $notification['type'])
->andWhere('reference_id', $notification['reference_id'])
->limit(5);
}
$notifcationGroups[] = $notifcationGroup;
}
我现在继续假设 notificationAdapters get 方法实现了这个分组并返回一个像这样的数组:
[
{
count: 12,
items: [Note1, Note2, Note3, Note4, Note5]
},
{
count: 1,
items: [Note1]
},
{
count: 3,
items: [Note1, Note2, Note3]
}
]
因为我们的组中总是至少有一个通知,并且我们的排序更喜欢 Unread 和 New 通知,所以我们可以只使用第一个通知作为呈现示例。
因此,为了能够处理这些分组通知,我们需要一个新对象:
class NotificationGroup
{
protected $notifications;
protected $realCount;
public function __construct(array $notifications, int $count)
{
$this->notifications = $notifications;
$this->realCount = $count;
}
public function message()
{
return $this->notifications[0]->messageForNotifications($this->notifications, $this->realCount);
}
// forward all other calls to the first notification
public function __call($method, $arguments)
{
return call_user_func_array([$this->notifications[0], $method], $arguments);
}
}
最后,我们实际上可以将大部分内容放在一起。 NotificationManager 上的 get 函数可能如下所示:
public function get(User $user, $limit = 20, $offset = 0) : array
{
$groups = [];
foreach($this->notificationAdapter->get($user->getId(), $limit, $offset) as $group)
{
$groups[] = new NotificationGroup($group['notifications'], $group['count']);
}
return $gorups;
}
最后在一个可能的控制器动作中:
public function viewNotificationsAction(Request $request)
{
$notificationManager = new NotificationManager;
foreach($notifications = $notificationManager->get($this->getUser()) as $group)
{
echo $group->unread . ' | ' . $group->message() . ' - ' . $group->createdAt() . "\n";
}
// mark them as read
$notificationManager->markRead($notifications);
}