Уведомления - oyboy/Jora GitHub Wiki
Они не являются ни пуш-уведомлениями, ни почтовыми сообщениями и т.п. По сути это просто записи в базе данных, которые у каждого пользователя обновляются на странице через определённый интервал времени. По нажатию на специальную иконку у пользователя появляются все непрочитанные уведомления, нажатие на которые обновляет их статус и скрывает.
Само уведомление, которое передаётся по http-запросам:
@Entity
@Data
public class Notification {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String message;
}
А это уже связь уведомления с пользователем. Здесь хранится информация о том, кем оно прочитано:
@Entity
@Data
public class UserNotification {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@ToString.Exclude
@EqualsAndHashCode.Exclude
@JsonBackReference
@JoinColumn(name = "user_id")
private User user;
@ManyToOne
@JoinColumn(name = "notification_id")
private Notification notification;
private boolean is_read;
}
Работа с запросами сама по себе предполагает создание уже не Controller, а RestContoller. Как можно заметить, в контроллерах не передаются атрибуты в модель, такую задачу на себя берёт отдельный скрипт notification_script.js.
@RestController
@RequestMapping("/api/notifications")
public class NotificationController {
@Autowired
NotificationService notificationService;
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
@GetMapping("/unread/{userId}")
public ResponseEntity<List<Notification>> getUnreadNotifications(@PathVariable Long userId) {
List<Notification> notifications = notificationService.getUnreadNotificationsForUser(userId);
return ResponseEntity.ok(notifications);
}
@PostMapping("/read/{id}")
public ResponseEntity<Void> markAsRead(@PathVariable("id") Long id,
@AuthenticationPrincipal User user){
Notification notification = notificationService.getNotificationById(id);
/*if (notification == null || !Objects.equals(notification.getUser().getId(), user.getId())) {
return ResponseEntity.notFound().build();
}*/
if (notification == null ) return ResponseEntity.notFound().build();
notificationService.markNotificationAsRead(notification, user);
simpMessagingTemplate.convertAndSendToUser(user.getId().toString(), "/topic/notifications", notification);
return ResponseEntity.ok().build();
}
}
Может быть неожиданно, но в контроллере нет метода для самой отправки сообщения. Для этого создан метод в сервисе, который отправляет сообщение по сокету и сохраняет его в таблице, а после сохранения скрипт использует get-метод для получения нужных записей из базы.
@Service
@Slf4j
public class NotificationService {
@Autowired
private NotificationRepository notificationRepository;
@Autowired
private UserNotificationRepository userNotificationRepository;
@Autowired
ProjectRepository projectRepository;
@Autowired
UserProjectRoleReposirory userProjectRoleReposirory;
@Autowired
private SimpMessagingTemplate messagingTemplate;
public void sendNotificationToAll(String project_hash, String title, String message){
Long project_id = projectRepository.findIdByHash(project_hash);
Project project = projectRepository.findById(project_id).orElse(null);
List<User> users = userProjectRoleReposirory.findUsersByProjectId(project_id);
Notification notification = new Notification();
notification.setTitle(title);
notification.setMessage(message);
project.getNotifications().add(notification);
log.info("Saving new notification {}", notification);
notificationRepository.save(notification);
log.info("Trying to send all notification {}", users);
for (User user : users){
UserNotification userNotification = new UserNotification();
userNotification.setUser(user);
userNotification.setNotification(notification);
userNotificationRepository.save(userNotification);
messagingTemplate.convertAndSendToUser(user.getId().toString(), "/topic/notifications", notification);
}
}
public void sendNotificationTo(User user, String title, String message) throws
CustomException.ObjectExistsException{
if (userNotificationRepository.existsByUserIdAndMessage(user.getId(), message))
throw new CustomException.ObjectExistsException("Запрос уже существует");
Notification notification = new Notification();
notification.setTitle(title);
notification.setMessage(message);
UserNotification userNotification = new UserNotification();
userNotification.setUser(user);
userNotification.setNotification(notification);
log.info("Saving new notification {}", notification);
notificationRepository.save(notification);
userNotificationRepository.save(userNotification);
log.info("trying to send notif to {}", user);
messagingTemplate.convertAndSendToUser(user.getId().toString(), "/topic/notifications", notification);
}
public List<Notification> getUnreadNotificationsForUser(Long user_id) {
return userNotificationRepository.findByUserIdAndReadIsFalse(user_id);
}
public void markNotificationAsRead(Notification n, User user) {
UserNotification notification = userNotificationRepository.findByNotificationIdAndUserId(n.getId(), user.getId());
log.info("Marking as read notification id {}", n.getId());
notification.set_read(true);
userNotificationRepository.save(notification);
}
public Notification getNotificationById(Long id){
return notificationRepository.findById(id).orElse(null);
}
}
Интерес представляет только связь уведомлений с пользователем:
public interface UserNotificationRepository extends CrudRepository<UserNotification, Long> {
//Получить список непрочитанных сообщений для определённого пользователя
@Query("SELECT n " +
"FROM UserNotification un " +
"JOIN Notification n " +
"ON n.id = un.notification.id " +
"WHERE un.user.id = :userId AND un.is_read = false")
List<Notification> findByUserIdAndReadIsFalse(@Param("userId") Long userId);
//Используется для пометки сообщения как прочитанного (обновления записи)
@Query("SELECT un " +
"FROM UserNotification un " +
"JOIN Notification n " +
"ON n.id = un.notification.id " +
"WHERE n.id = :id AND un.user.id = :userId")
UserNotification findByNotificationIdAndUserId(@Param("id") Long id,
@Param("userId") Long userId);
//Проверка на наличие уже отправленных уведомлений.
//В случае истинности в сервисе выбрасывался ObjectExistsException
@Query("SELECT COUNT(*) > 0 " +
"FROM UserNotification un " +
"JOIN Notification n " +
"ON n.id = un.notification.id " +
"WHERE un.user.id = :userId AND n.message = :message")
boolean existsByUserIdAndMessage(@Param("userId") Long userId,
@Param("message") String message);
}
<div class="notification-container">
<div class="bell" id="notification-bell">
<span class="notification-count" id="notification-count" style="display:none;"></span>
🔔
</div>
<div class="dropdown" id="notification-dropdown" style="display:none;">
<ul id="notification-list"></ul>
</div>
<script src="/static/scripts/notification_script.js"></script>
<input type="hidden" id="currentUserId" value="${user.id}">
<input type="hidden" name="_csrf" value="${_csrf.token}">
</div>
-
const userId = $("#currentUserId").val();
: Сохраняет ID текущего пользователя, полученный из скрытого поля на странице. -
const csrfToken = $('input[name="_csrf"]').val();
: Получает токен CSRF для защиты от атак. Этот токен добавляется в заголовки AJAX-запросов. -
let isDropdownOpen = false;
: Переменная для отслеживания состояния выпадающего меню уведомлений (открыто или закрыто).
-
function fetchNotifications() {...}
: Загружает непрочитанные уведомления для текущего пользователя с сервера путем выполнения AJAX-запроса через метод$.get()
.- Если уведомления существуют, обновляет количество уведомлений в интерфейсе.
- Для каждого уведомления добавляется HTML-элемент
<li>
в список уведомлений, отображая заголовок и сообщение.
-
setInterval(fetchNotifications, 3000);
: Устанавливает интервал, каждые 3000 миллисекунд (или 3 секунды) выполняется функцияfetchNotifications
, чтобы проверять наличие новых уведомлений.
-
$('#notification-bell').click(function() {...});
: Обработчик события клика по значку колокольчика, который открывает или закрывает меню уведомлений и также вызываетfetchNotifications
, чтобы обновить список уведомлений.
-
$(document).on('click', '#notification-list li', function() {...});
: Обработчик клика на отдельном уведомлении, который помечает его как прочитанное, отправляя POST-запрос на сервер с использованием AJAX.- Если запрос успешен, уведомление удаляется из списка, и если больше нет уведомлений, скрывается счетчик.
-
$(document).click(function(event) {...});
: Обработчик клика по документу, который закрывает выпадающее меню уведомлений, если клик происходит вне иконки колокольчика и самого меню.
- Обработчик ошибок в AJAX-запросе:
error: function(xhr, status, error) {...}
. Выводит сообщение об ошибке в консоль в случае неудачного запроса при пометке уведомления как прочитанное.