使用PHP和MySQL的通知系统

54

我想为我们学校实现一个通知系统,这是一个使用php/mysql创建的非公开网络应用程序,因此没有太多的流量,“每天500-1000个访问者”。

1. 我最初的方法是使用MYSQL触发器:

我使用了Mysql的AFTER INSERT触发器将记录添加到名为notifications的表中。大致如下。

'CREATE TRIGGER `notify_new_homwork` AFTER INSERT ON `homeworks`
 FOR EACH ROW INSERT INTO `notifications` 
    ( `from_id`, `note`, `class_id`) 
 VALUES 
    (new.user_id, 
        concat('A New homework Titled: "',left(new.title,'50'),
        '".. was added' )
    ,new.subject_id , 11);'

这种黑魔法非常有效,但我无法跟踪是否有新的通知要显示给用户。

因此,我添加了一个名为“notifications”的页面。

类似以下方式检索通知:

SELECT n.* from notifications n 
JOIN user_class on user_class.class_id = n.class_id where user_class.user_id = X;
注意:表user_class将用户链接到班级,"user_id、class_id、subject_id",当用户是老师时,subject为null。
现在我的下一个挑战是:
  1. 如何跟踪每个用户的新旧通知?
  2. 如何将类似的通知聚合成一行?
例如,如果有两个用户评论了某件事情,那么不要插入新行,只需更新旧行并添加像"userx和1其他人评论了hw"这样的内容。
非常感谢!
编辑:
根据下面的答案,为了在行上设置已读/未读标志,我需要为每个学生设置一行,而不仅仅是整个班级的一行。 这意味着需要将触发器编辑为类似以下内容:
insert into notifications (from_id,note,student_id,isread)
select new.user_id,new.note,user_id,'0' from user_class where user_class.class_id = new.class_id group by user_class.user_id

你有任何关于这个问题的更新或自己的答案吗?你已经尝试过九个月前的方法了吗? - rhavendc
为了让别人给你一个确定的答案,你应该包括你的班级和学生表。 - e4c5
你找到了@Zalaboza想要的东西吗?还是你需要另一个解决方案?如果下面的答案没有帮助到你,我不确定你具体想要什么。请告诉我,这样我才能帮助你。 - Aprilsnar
4个回答

202

这个问题已经九个月了,所以我不确定 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中构建通知系统的黄金标准。

通知模型

这是通知本身的基础模型示例,没有什么花哨的东西,只有所需属性和抽象方法messageForNotificationmessageForNotifications,我们期望在不同类型的通知中实现它们。

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);
    }
}

你需要自己添加构造函数获取器设置器等方面的内容,以你自己的风格来完成。我不会提供一个可直接使用的通知系统。

通知类型

现在,您可以为每种类型创建一个新的Notification子类。以下示例将处理评论的喜欢操作

  • 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;
}

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(),
        ]);
    }
}
notificationAdapteradd 方法背后可能是原始的 mysql 插入命令。使用此适配器抽象可以轻松地从 mysql 切换到基于文档的数据库,如 mongodb,这对于通知系统是有意义的。 notificationAdapter 上的 isDoublicate 方法应该简单地检查是否已经存在具有相同的 recipientsendertypereference 的通知。
我无法强调这只是一个示例。(同时我真的必须缩短下一步,否则这篇文章会变得非常长 -.-)
所以假设您有某种控制器并在老师上传作业时执行操作:
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']];

    // 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;
}

我现在假设notificationAdapterget方法实现了分组,并返回以下类似数组:

[
    {
        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);
    }

    // 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);
}

30
这样的回答应该得到比+25更多的赞赏。 - jQuerybeast
@Mario 很棒的帖子,即使它只是一个例子也展示了一个很好的解决方案。谢谢。这对我非常有帮助。 - Anibal Mauricio
很棒的答案。这真的帮助我理解如何将对许多不同类型内容的引用组织到一个表中,而不使用严格的外键关系。 - Ginger and Lavender
1
如果一个用户有1k个关注者,我们需要在用户表中添加1k条记录吗? - Zia
1
在这种情况下,我使用了“groups”,如果用户关注者,则通过发送到类似于“following_12234”的组来发送通知。 已经过去了将近5年,我仍然回到这篇帖子进行参考 :D! - Zalaboza

2

答案:

  1. 在通知中引入一个已读/未读变量。您可以通过在sql中执行... WHERE status = 'UNREAD' 来获取仅未读通知。

  2. 你不能真正做到...你会想要推送那个通知。不过,你仍然可以通过使用 GROUP BY 来聚合它们。你可能想要基于某些唯一的东西进行分组,比如新的作业,所以它可能是这样的... GROUP BY homework.id


读取/未读列会将通知标记为所有用户已读,而不仅仅是看到它的用户。请注意,我将通知定向到整个班级,然后在获取通知时,我只获取与我的班级相关的通知。 - Zalaboza
啊,您需要添加一个新列来识别不同的用户... 学生ID似乎是合适的。 - geggleto
那么通知将针对学生而不是整个班级,我想我需要创建一个新表格来关联学生和通知,并设置已读标志。但如果这样做,为每个学生插入一行可能会更好 :/,我感到困惑。 - Zalaboza
一个学生是班级的一部分。你的系统需要为该班级中的每个学生生成通知。 - geggleto

1
您可以通过创建一个NotificationsRead表来解决问题,其中包含用户ID和用户想要标记为已读的通知的ID。(这样您就可以将每个学生和老师分开。)然后,您可以将该表连接到您的通知表中,这样您就会知道它是否应被视为旧的或新的。
geggleto的答案在第二部分是正确的,您可以使用SELECT *,COUNT(*) AS counter WHERE ... GROUP BY 'type'来获取通知,然后您就会知道有多少相同类型的通知,您可以在视图中准备'userx和1个其他人评论了作业'。
我还建议您不要存储要显示的整个文本,而是存储所需的信息,如:from_id、class_id、type、name等-这样如果以后需要更改机制,您就可以更轻松地进行,而且需要存储的内容更少。

-1

对于那些不想使用应用程序的人,可以使用纯PHP和MySQL而不使用应用程序。在添加一项作业时,您可以添加通知。(是的,我知道这容易受到SQL注入攻击,但出于简单起见,我正在使用常规MySQL) 当有人添加作业时的SQL:

$noti_homework = "INSERT INTO homework(unread, username, recipient, topic) VALUES(true, user_who_added_hw, user_who_receives_notification, homework_name);

然后你可以检查通知是否未读:

$noti_select = "SELECT FROM homework WHERE recipient='".$_SESSION['username']."' AND unread='true'";
$noti_s_result = $conn->query($noti_select);
$noti_s_row = $noti_s_result = $noti_s_result->fetch_assoc();
if ($noti_s_row['unread'] == true) {
}

最后,如果通知为真,你可以回显它。
if ($noti_s_row['unread'] == true) {
  echo $noti_s_row['username'] . " has sent out the homework " . $noti_s_row['homework'] . ".";
}

点赞评论的方法基本相同,事实上更加简单。

$noti_like = "INSERT INTO notification_like(unread, username, recepient), VALUES(true, user_who_followed, user_who_recieves_notification);

然后你只需要按照相同的模式回显行,就像这样:

$noti_like = "INSERT INTO notification_like(unread, username, recipient), VALUES(true, user_who_followed, user_who_receives_notification);
$noti_like_select = "SELECT FROM notification_like WHERE recipient='".$_SESSION['username']."' AND unread='true'";
$noti_l_result = $conn->query($noti_like_select);
$noti_l_row = $noti_s_result = $noti_l_result->fetch_assoc();
if ($noti_l_row['unread'] == true) {
  echo $noti_l_row['username'] . " has liked your topic!";
}

然后获取通知的数量:

$count_like = "SELECT COUNT(*) FROM notification_like WHERE recipient='".$_SESSION['username']."' AND unread='true'";
$num_count = $count_like->fetch_row();
$count_hw = "SELECT COUNT(*) FROM homework WHERE recipient='".$_SESSION['username']."' AND unread='true'";
$num_count_hw = $count_hw->fetch_row();

使用PHP,您可以打印出两个变量$num_count和$num_count_hw。
$num_count_real = $num_count[0];
$num_count_hw_real = $num_count_hw[0];
echo $num_count_real + $num_count_hw_real;

这段代码没有经过测试,供参考。希望对你有所帮助 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接