这个问题已经九个月了,所以我不确定 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;
public function messageForNotification(Notification $notification) : string;
public function messageForNotifications(array $notifications) : string;
public function message() : string
{
return $this->messageForNotification($this);
}
}
你需要自己添加构造函数、获取器和设置器等方面的内容,以你自己的风格来完成。我不会提供一个可直接使用的通知系统。
通知类型
现在,您可以为每种类型创建一个新的Notification
子类。以下示例将处理评论的喜欢操作:
- Ray喜欢了你的评论。(1条通知)
- John和Jane喜欢了你的评论。(2条通知)
- Jane、Johnny、James和Jenny喜欢了你的评论。(4条通知)
- Jonny、James和其他12人喜欢了你的评论。(14条通知)
示例实现:
namespace Notification\Comment;
class CommentLikedNotification extends \Notification
{
public function messageForNotification(Notification $notification) : string
{
return $this->sender->getName() . 'has liked your comment: ' . substr($this->reference->text, 0, 10) . '...';
}
public function messageForNotifications(array $notifications, int $realCount = 0) : string
{
if ($realCount === 0) {
$realCount = count($notifications);
}
if ($realCount === 2) {
$names = $this->messageForTwoNotifications($notifications);
}
elseif ($realCount < 5) {
$names = $this->messageForManyNotifications($notifications);
}
else {
$names = $this->messageForManyManyNotifications($notifications, $realCount);
}
return $names . ' liked your comment: ' . substr($this->reference->text, 0, 10) . '...';
}
protected function messageForTwoNotifications(array $notifications) : string
{
list($first, $second) = $notifications;
return $first->getName() . ' and ' . $second->getName();
}
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();
}
protected function messageForManyManyNotifications(array $notifications, int $realCount) : string
{
list($first, $second) = array_slice($notifications, 0, 2);
return $first->getName() . ', ' . $second->getName() . ' and ' . $realCount . ' others';
}
}
通知管理器
为了在应用程序中处理通知,请创建类似通知管理器的东西:
class NotificationManager
{
protected $notificationAdapter;
public function add(Notification $notification);
public function markRead(array $notifications);
public function get(User $user, $limit = 20, $offset = 0) : array;
}
notificationAdapter
属性应包含与您的数据后端直接通信的逻辑,例如此示例中的 mysql。
创建通知
使用 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端进行分组的问题在于您必须加载当前用户的所有通知以正确地对它们进行分组。这很糟糕,如果您只有少数用户,那可能还没什么问题,但这并不是好的解决方案。
简单的解决方案是仅限制请求的通知数量并仅对这些通知进行分组。当没有太多类似的通知(如20个中的3-4个)时,这将运行良好。但假设用户/学生的帖子获得了大约100个赞,并且您仅选择了最后20个通知。然后用户将只看到有20人喜欢他的帖子,而这将是他唯一的通知。
“正确”的解决方案是在数据库上已经对通知进行分组,并仅选择每个通知组的一些样本。然后,您只需将实际计数注入到通知消息中即可。
您可能没有阅读下面的文本,所以让我继续放一段代码:
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']];
if ($notification['count'] == 1)
{
$notifcationGroup['items'] = [$notification];
}
else
{
$notifcationGroup['items'] = $this->select('notifications')
->where('recipient_id', $recipient_id)
->andWehere('type', $notification['type'])
->andWhere('reference_id', $notification['reference_id'])
->limit(5);
}
$notifcationGroups[] = $notifcationGroup;
}
我现在假设notificationAdapter
的get
方法实现了分组,并返回以下类似数组:
[
{
count: 12,
items: [Note1, Note2, Note3, Note4, Note5]
},
{
count: 1,
items: [Note1]
},
{
count: 3,
items: [Note1, Note2, Note3]
}
]
因为我们的小组中总是至少有一条通知,而且我们偏爱未读和新的通知,所以我们可以只使用第一条通知作为渲染的样例。
因此,要想处理这些分组通知,我们需要一个新的对象:
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);
}
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);
}