Система уведомлений с использованием php и mysql


Я хотел внедрить систему уведомлений для нашей школы, это веб-приложение php/mysql, которое не открыто для публики, поэтому оно не получает большого трафика. "ежедневно 500-1000 посетителей".

1. Мой первоначальный подход заключался в использовании триггеров MYSQL:

Я использовал Mysql AFTER INSERT trigger для добавления записей в таблицу с именем 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);'

Этот вид черной магии работал очень хорошо, но я не мог отслеживать, является ли это уведомление новым", чтобы показать количество новых уведомления для пользователя". поэтому я добавил страницу с именем уведомления.

Уведомления извлекаются с помощью чего-то вроде

SELECT n.* from notifications n 
JOIN user_class on user_class.class_id = n.class_id where user_class.user_id = X;

Примечание: таблица user_class связывает пользователя с классом "идентификатор пользователя, идентификатор класса, идентификатор субъекта" - субъект равен нулю, если пользователь не является учителем"

Теперь мои следующие задачи.

  1. как отслеживать новые и старые уведомления для каждого пользователя?
  2. как я могу объединить уведомления, похожие на пользовательские, в одну строку?

Пример, если 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
Author: e4c5, 2015-09-22

3 answers

Ну, этому вопросу 9 месяцев, поэтому я не уверен, что ОП все еще нуждается в ответе, но из-за множества просмотров и вкусной щедрости я хотел бы также добавить свою горчицу (немецкая поговорка..).

В этом посте я постараюсь привести простой объясненный пример того, как начать создавать систему уведомлений.

Редактировать: Ну ладно, это оказалось намного, намного, намного дольше, чем я ожидал. В конце концов я действительно устал, я извините.

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 для этого примера:

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

Вам придется добавить конструктор , добытчики, сеттеры и тому подобное самостоятельно в своем собственном стиле, я не собираюсь предоставлять готовую к использованию систему уведомлений.

Типы уведомлений

Теперь вы можете создать новый подкласс Notification для каждого типа. Этот следующий пример будет обрабатывать подобное действие комментария:

  • Рэю понравился ваш комментарий. (1 уведомление)
  • Джону и Джейн понравился ваш комментарий. (2 уведомления)
  • Джейн, Джонни, Джеймсу и Дженни понравился ваш комментарий. (4 уведомления)
  • Джонни, Джеймсу и еще 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(),
        ]);
    }
}

За add методом notificationAdapter может стоять необработанная команда вставки mysql. Использование этой абстракции адаптера позволяет легко переключаться с mysql на базу данных на основе документов, такую как mongodb, что имело бы смысл для системы уведомлений.

Метод isDoublicate в notificationAdapter должен просто проверить, есть ли уже уведомление с тем же 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 заключается в том, что вам придется загружать все уведомления текущего пользователя, чтобы правильно их сгруппировать. Это было бы плохо, ну, если бы у вас было всего несколько пользователей, это, вероятно, все равно не было бы проблемой, но это не делает это хорошим.

Простое решение состоит в том, чтобы просто ограничить количество запрашиваемых уведомлений и группировать только их. Это будет хорошо работать, когда подобных уведомлений не так много (например, 3-4 на 20). Но допустим, сообщение пользователя/студента получает около сотни лайков, а вы выбираете только последние 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;
}

Теперь я продолжу предполагать, что метод notificationAdapters 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);
    }

    // forward all other calls to the first notification
    public function __call($method, $arguments)
    {
        return call_user_func_array([$this->notifications[0], $method], $arguments);
    }
}

И, наконец, мы действительно можем собрать большую часть материала вместе. Вот как может выглядеть функция get на NotificationManager:

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);
}
 91
Author: Mario, 2017-07-31 11:11:47

Ответ:

  1. Введите в уведомление переменную "прочитано/непрочитано". Затем вы можете извлекать только непрочитанные уведомления, выполнив это... ГДЕ статус = "НЕПРОЧИТАННЫЙ" в вашем sql.

  2. Ты действительно не можешь... вы захотите нажать на это уведомление. Что вы все еще можете сделать, так это объединить их, используя GROUP BY. Вы, вероятно, захотите сгруппироваться по чему-то уникальному, например, по новому домашнему заданию, так что это может быть что-то вроде... СГРУППИРОВАТЬ ПО homework.id

 2
Author: geggleto, 2015-09-22 13:18:22

Вы можете решить проблему, создав таблицу NotificationsRead, содержащую идентификатор пользователя и идентификатор уведомления, которое пользователь хотел отметить как прочитанное. (Таким образом, вы можете разделить каждого ученика и учителя.) Затем вы можете присоединить эту таблицу к таблице вашего уведомления, и вы узнаете, следует ли ее считать старой или новой.

Ответ Гегглето был правильным относительно второй части, вы можете получать уведомления с помощью SELECT *, COUNT(*) AS counter WHERE ... GROUP BY 'type', тогда вы узнаете, сколько однотипных у вас есть там, и вы можете подготовить "userx и 1 другой комментарий к hw" для просмотра.

Я бы также посоветовал вам не хранить весь текст, который вы хотите отобразить, вместо этого храните необходимую информацию, например: from_id, class_id, тип, имя и т. Д. - Таким образом, вы сможете легче изменить механизмы позже, если вам нужно, и вам придется хранить меньше.

 1
Author: Razor_alpha, 2016-07-03 04:08:53