dev add notification entity
Test Workflow / test (push) Successful in 2s

This commit is contained in:
vladp
2026-04-01 15:32:36 +07:00
parent 549d248cb7
commit 051f1ef5d4
21 changed files with 543 additions and 28 deletions
@@ -34,7 +34,8 @@ public class HandlerConfig {
GlobalSearchHandler globalSearchHandler, GlobalSearchHandler globalSearchHandler,
ViolationNotionHandler violationNotionHandler, ViolationNotionHandler violationNotionHandler,
ComplaintEntityHandler complaintEntityHandler, ComplaintEntityHandler complaintEntityHandler,
UserInfoHandler userInfoHandler UserInfoHandler userInfoHandler,
NotificationHandler notificationHandler
) { ) {
Map<Integer, RequestHandler> map = new HashMap<>(); Map<Integer, RequestHandler> map = new HashMap<>();
map.put(20001, login); map.put(20001, login);
@@ -60,6 +61,7 @@ public class HandlerConfig {
map.put(30012, violationNotionHandler); map.put(30012, violationNotionHandler);
map.put(30013, complaintEntityHandler); map.put(30013, complaintEntityHandler);
map.put(30014, userInfoHandler); map.put(30014, userInfoHandler);
map.put(30015, notificationHandler);
return map; return map;
} }
@@ -0,0 +1,8 @@
package ru.soune.nocopy.dto.notification;
import lombok.Data;
@Data
public class ActivateNotificationRequest {
private String authToken;
}
@@ -0,0 +1,13 @@
package ru.soune.nocopy.dto.notification;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class ActiveNotificationsResponse {
private List<NotificationDto> notifications;
private long unreadCount;
}
@@ -0,0 +1,11 @@
package ru.soune.nocopy.dto.notification;
import lombok.Data;
import java.util.List;
@Data
public class MarkAsReadRequest {
private List<Long> notificationIds;
private String authToken;
}
@@ -0,0 +1,11 @@
package ru.soune.nocopy.dto.notification;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class MarkAsReadResponse {
private boolean success;
private int updatedCount;
}
@@ -0,0 +1,18 @@
package ru.soune.nocopy.dto.notification;
import lombok.Builder;
import lombok.Data;
import ru.soune.nocopy.entity.notification.NotificationStatus;
import ru.soune.nocopy.entity.notification.NotificationType;
import java.time.LocalDateTime;
@Data
@Builder
public class NotificationDto {
private Long id;
private String message;
private NotificationType type;
private NotificationStatus status;
private LocalDateTime createdAt;
}
@@ -0,0 +1,18 @@
package ru.soune.nocopy.dto.notification;
import lombok.Data;
import ru.soune.nocopy.entity.notification.NotificationStatus;
import ru.soune.nocopy.entity.notification.NotificationType;
import java.util.List;
@Data
public class NotificationListRequest {
private int page = 0;
private int size = 10;
private String sortBy = "createdAt";
private String sortDirection = "DESC";
private List<NotificationType> types;
private List<NotificationStatus> statuses;
private String authToken;
}
@@ -0,0 +1,15 @@
package ru.soune.nocopy.dto.notification;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class NotificationListResponse {
private List<NotificationDto> notifications;
private int currentPage;
private int totalPages;
private long totalElements;
}
@@ -0,0 +1,14 @@
package ru.soune.nocopy.entity.notification;
import lombok.Getter;
@Getter
public enum NotificationMessage {
FILE_SEARCH("notification.file.search");
private final String messageKey;
NotificationMessage(String messageKey) {
this.messageKey = messageKey;
}
}
@@ -1,19 +1,31 @@
package ru.soune.nocopy.entity.notification; package ru.soune.nocopy.entity.notification;
public enum NotificationType{ import lombok.Getter;
SEARCH_RESULT,
MONITORING_RESULT, @Getter
OPERATION_IMPOSSIBLE, public enum NotificationType {
START_FAILED_NOW, SEARCH_RESULT("notification.search.result"),
START_FAILED_NEXT, MONITORING_RESULT("notification.monitoring.result"),
PAYOUT_RESULT, OPERATION_IMPOSSIBLE("notification.operation.impossible"),
REFERRAL_DEPOSIT, START_FAILED_NOW("notification.start.failed.now"),
REFERRAL_REGISTERED, START_FAILED_NEXT("notification.start.failed.next"),
REFERRAL_ACTIVATED, PAYOUT_RESULT("notification.payout.result"),
COMPLAINT_STATUS_CHANGED, PAYMENT_RESULT("notification.payment.result"),
CASE_STATUS_CHANGED, REFERRAL_DEPOSIT("notification.referral.deposit"),
INCOMING_MESSAGE, REFERRAL_REGISTERED("notification.referral.registered"),
TARIFF_EXPIRING, REFERRAL_ACTIVATED("notification.referral.activated"),
FILE_MODERATION_EVENT, COMPLAINT_STATUS_CHANGED("notification.complaint.status"),
FILE_ADDED_TO_SYSTEM CASE_STATUS_CHANGED("notification.case.status"),
INCOMING_MESSAGE("notification.incoming.message"),
TARIFF_EXPIRING("notification.tariff.expiring"),
TOKEN_NOT_FOUND("notification.token.not.found"),
FILE_MODERATION_EVENT("notification.file.moderation"),
FILE_ADDED_TO_SYSTEM("notification.file.added"),
SEARCH_OPERATION_FAILED("notification.search.operation.filed");
private final String messageKey;
NotificationType(String messageKey) {
this.messageKey = messageKey;
}
} }
@@ -0,0 +1,147 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.file.ActionResponse;
import ru.soune.nocopy.service.notification.NotificationService;
import com.fasterxml.jackson.databind.type.TypeFactory;
import ru.soune.nocopy.dto.notification.*;
import ru.soune.nocopy.entity.notification.Notification;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.service.register.AuthService;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Component
@RequiredArgsConstructor
public class NotificationHandler implements RequestHandler {
private final NotificationService notificationService;
private final ObjectMapper objectMapper;
private final AuthService authService;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
Map<String, Object> bodyMap = objectMapper.convertValue(request.getMessageBody(),
TypeFactory.defaultInstance().constructMapType(Map.class, String.class, Object.class));
String action = (String) bodyMap.get("action");
if (action == null) {
ActionResponse response = ActionResponse.builder()
.action(action)
.availableActions(Arrays.asList("list", "active", "markRead"))
.build();
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
"Action is required",
response);
}
switch (action) {
case "list": {
NotificationListRequest listRequest = objectMapper.convertValue(request.getMessageBody(),
NotificationListRequest.class);
User user = authService.getUser(listRequest.getAuthToken());
Page<Notification> page = notificationService.getAllNotifications(
user,
listRequest.getPage(),
listRequest.getSize(),
listRequest.getSortBy(),
listRequest.getSortDirection(),
listRequest.getTypes(),
listRequest.getStatuses());
List<NotificationDto> dtos = page.getContent().stream()
.map(this::toDto)
.collect(Collectors.toList());
NotificationListResponse response = NotificationListResponse.builder()
.notifications(dtos)
.currentPage(page.getNumber())
.totalPages(page.getTotalPages())
.totalElements(page.getTotalElements())
.build();
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
response);
}
case "active": {
ActivateNotificationRequest activateRequest = objectMapper.convertValue(request.getMessageBody(),
ActivateNotificationRequest.class);
User user = authService.getUser(activateRequest.getAuthToken());
List<Notification> recent = notificationService.getActiveNotifications(user);
long unreadCount = notificationService.getUnreadCount(user);
ActiveNotificationsResponse response = ActiveNotificationsResponse.builder()
.notifications(recent.stream().map(this::toDto).collect(Collectors.toList()))
.unreadCount(unreadCount)
.build();
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
response);
}
case "markRead": {
MarkAsReadRequest markRequest = objectMapper.convertValue(request.getMessageBody(),
MarkAsReadRequest.class);
User user = authService.getUser(markRequest.getAuthToken());
int updated = notificationService.markAsRead(user, markRequest.getNotificationIds());
MarkAsReadResponse response = MarkAsReadResponse.builder()
.success(true)
.updatedCount(updated)
.build();
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
response);
}
default:
ActionResponse response = ActionResponse.builder()
.action(action)
.availableActions(Arrays.asList("list", "active", "markRead"))
.build();
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_ACTION.getCode(),
"Invalid action: " + action,
response);
}
}
private NotificationDto toDto(Notification notification) {
return NotificationDto.builder()
.id(notification.getId())
.message(notification.getMessage())
.type(notification.getNotificationType())
.status(notification.getStatus())
.createdAt(notification.getCreatedAt())
.build();
}
}
@@ -0,0 +1,40 @@
package ru.soune.nocopy.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import ru.soune.nocopy.entity.notification.Notification;
import ru.soune.nocopy.entity.notification.NotificationStatus;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.user.User;
import java.util.List;
public interface NotificationRepository extends JpaRepository<Notification, Long> {
Page<Notification> findByUser(User user, Pageable pageable);
Page<Notification> findByUserAndNotificationTypeIn(User user, List<NotificationType> types, Pageable pageable);
Page<Notification> findByUserAndStatusIn(User user, List<NotificationStatus> statuses, Pageable pageable);
Page<Notification> findByUserAndNotificationTypeInAndStatusIn(User user, List<NotificationType> types,
List<NotificationStatus> statuses,
Pageable pageable);
List<Notification> findTop5ByUserAndStatusOrderByCreatedAtDesc(User user, NotificationStatus status);
long countByUserAndStatus(User user, NotificationStatus status);
@Modifying
@Query("UPDATE Notification n SET n.status = :newStatus WHERE n.id IN :ids AND n.user = :user AND " +
"n.status = :oldStatus")
int updateStatus(@Param("ids") List<Long> ids,
@Param("user") User user,
@Param("oldStatus") NotificationStatus oldStatus,
@Param("newStatus") NotificationStatus newStatus);
Page<Notification> getNotificationsByUser(User user, Pageable pageable);
}
@@ -1,7 +1,5 @@
package ru.soune.nocopy.service.complaint; package ru.soune.nocopy.service.complaint;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
@@ -13,12 +11,14 @@ import ru.soune.nocopy.dto.complaint.ComplaintResponse;
import ru.soune.nocopy.entity.complaint.ComplaintEntity; import ru.soune.nocopy.entity.complaint.ComplaintEntity;
import ru.soune.nocopy.entity.complaint.ComplaintStatus; import ru.soune.nocopy.entity.complaint.ComplaintStatus;
import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.violation.Violation; import ru.soune.nocopy.entity.violation.Violation;
import ru.soune.nocopy.exception.ComplaintNotFoundException; import ru.soune.nocopy.exception.ComplaintNotFoundException;
import ru.soune.nocopy.exception.DuplicateComplaintException; import ru.soune.nocopy.exception.DuplicateComplaintException;
import ru.soune.nocopy.exception.ViolationNotFoundException; import ru.soune.nocopy.exception.ViolationNotFoundException;
import ru.soune.nocopy.repository.ComplaintEntityRepository; import ru.soune.nocopy.repository.ComplaintEntityRepository;
import ru.soune.nocopy.repository.ViolationRepository; import ru.soune.nocopy.repository.ViolationRepository;
import ru.soune.nocopy.service.notification.NotificationService;
import ru.soune.nocopy.service.violation.ViolationService; import ru.soune.nocopy.service.violation.ViolationService;
import ru.soune.nocopy.service.violation.ViolationStatus; import ru.soune.nocopy.service.violation.ViolationStatus;
@@ -37,6 +37,8 @@ public class ComplaintEntityService {
private final ViolationService violationService; private final ViolationService violationService;
private final NotificationService notificationService;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public ComplaintResponse createComplaint(ComplaintRequest request) { public ComplaintResponse createComplaint(ComplaintRequest request) {
@@ -101,7 +103,12 @@ public class ComplaintEntityService {
} }
if (request.getComplaintText() != null) complaint.setComplaintText(request.getComplaintText()); if (request.getComplaintText() != null) complaint.setComplaintText(request.getComplaintText());
if (request.getEmail() != null) complaint.setEmail(request.getEmail()); if (request.getEmail() != null) complaint.setEmail(request.getEmail());
if (request.getStatus() != null) complaint.setStatus(ComplaintStatus.valueOf(request.getStatus())); if (request.getStatus() != null) {
complaint.setStatus(ComplaintStatus.valueOf(request.getStatus()));
notificationService.addNotification(NotificationType.COMPLAINT_STATUS_CHANGED, complaint.getViolation()
.getFileEntity().getUserId(), request.getStatus());
}
return mapToResponse(complaintRepository.save(complaint)); return mapToResponse(complaintRepository.save(complaint));
} }
@@ -17,6 +17,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import ru.soune.nocopy.dto.file.UploadProgressResponse; import ru.soune.nocopy.dto.file.UploadProgressResponse;
import ru.soune.nocopy.entity.file.*; import ru.soune.nocopy.entity.file.*;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.exception.*; import ru.soune.nocopy.exception.*;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.FileUploadSessionRepository; import ru.soune.nocopy.repository.FileUploadSessionRepository;
@@ -26,6 +27,7 @@ import ru.soune.nocopy.service.ImageHashService;
import ru.soune.nocopy.service.cost.CostService; import ru.soune.nocopy.service.cost.CostService;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileUploadService; import ru.soune.nocopy.service.file.FileUploadService;
import ru.soune.nocopy.service.notification.NotificationService;
import ru.soune.nocopy.service.tariff.TariffInfoService; import ru.soune.nocopy.service.tariff.TariffInfoService;
import ru.soune.nocopy.util.FileUtil; import ru.soune.nocopy.util.FileUtil;
@@ -84,6 +86,8 @@ public class FileUploadServiceImpl implements FileUploadService {
private final TariffInfoService tariffInfoService; private final TariffInfoService tariffInfoService;
private final NotificationService notificationService;
@PostConstruct @PostConstruct
public void init() { public void init() {
try { try {
@@ -364,6 +368,8 @@ public class FileUploadServiceImpl implements FileUploadService {
session.setStatus(UploadStatus.COMPLETED); session.setStatus(UploadStatus.COMPLETED);
session.setFilePath(finalFilePath); session.setFilePath(finalFilePath);
notificationService.addNotification(NotificationType.FILE_ADDED_TO_SYSTEM, session.getUserId());
FileStatus status = findSimilar == 0 ? FileStatus.MODERATION: FileStatus.TEMP; FileStatus status = findSimilar == 0 ? FileStatus.MODERATION: FileStatus.TEMP;
if (status == FileStatus.TEMP) { if (status == FileStatus.TEMP) {
@@ -11,6 +11,7 @@ import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.file.*; import ru.soune.nocopy.dto.file.*;
import ru.soune.nocopy.entity.file.*; import ru.soune.nocopy.entity.file.*;
import ru.soune.nocopy.entity.file.moderation.ModerationLog; import ru.soune.nocopy.entity.file.moderation.ModerationLog;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.exception.FileEntityNotFoundException; import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.exception.InvalidAppealException; import ru.soune.nocopy.exception.InvalidAppealException;
@@ -18,6 +19,7 @@ import ru.soune.nocopy.repository.FileAppealRepository;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.ModerationLogRepository; import ru.soune.nocopy.repository.ModerationLogRepository;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.notification.NotificationService;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -35,6 +37,8 @@ public class ModerationService {
private final FileEntityService fileEntityService; private final FileEntityService fileEntityService;
private final NotificationService notificationService;
private static final List<FileStatus> MODERATION_NEEDED_STATUSES = Arrays.asList( private static final List<FileStatus> MODERATION_NEEDED_STATUSES = Arrays.asList(
FileStatus.MODERATION, FileStatus.MODERATION,
FileStatus.BLOCKED FileStatus.BLOCKED
@@ -84,6 +88,8 @@ public class ModerationService {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
notificationService.addNotification(NotificationType.FILE_MODERATION_EVENT, file.getUserId());
} }
/** /**
@@ -3,6 +3,7 @@ package ru.soune.nocopy.service.monitoring;
import jakarta.mail.MessagingException; import jakarta.mail.MessagingException;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.configuration.search.SearchProperties; import ru.soune.nocopy.configuration.search.SearchProperties;
@@ -10,6 +11,8 @@ import ru.soune.nocopy.dto.file.YandexSearchResponse;
import ru.soune.nocopy.dto.tarriff.TariffDTO; import ru.soune.nocopy.dto.tarriff.TariffDTO;
import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity;
import ru.soune.nocopy.entity.monitoring.MonitoringType; import ru.soune.nocopy.entity.monitoring.MonitoringType;
import ru.soune.nocopy.entity.notification.NotificationMessage;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.tarif.TariffInfo; import ru.soune.nocopy.entity.tarif.TariffInfo;
import ru.soune.nocopy.entity.tarif.TariffType; import ru.soune.nocopy.entity.tarif.TariffType;
import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.entity.user.User;
@@ -17,6 +20,7 @@ import ru.soune.nocopy.exception.TariffNotFoundException;
import ru.soune.nocopy.repository.FileMonitoringRepository; import ru.soune.nocopy.repository.FileMonitoringRepository;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.mail.EmailService; import ru.soune.nocopy.service.mail.EmailService;
import ru.soune.nocopy.service.notification.NotificationService;
import ru.soune.nocopy.service.search.SearchImageService; import ru.soune.nocopy.service.search.SearchImageService;
import ru.soune.nocopy.service.tariff.TariffInfoService; import ru.soune.nocopy.service.tariff.TariffInfoService;
import ru.soune.nocopy.service.tariff.TariffService; import ru.soune.nocopy.service.tariff.TariffService;
@@ -48,6 +52,10 @@ public class MonitoringSearchService {
private final SearchProperties searchProperties; private final SearchProperties searchProperties;
private final NotificationService notificationService;
private final MessageSource messageSource;
@Transactional(noRollbackFor = TariffNotFoundException.class) @Transactional(noRollbackFor = TariffNotFoundException.class)
public void processFileSearch(FileMonitoringEntity monitoring) throws MessagingException, IOException { public void processFileSearch(FileMonitoringEntity monitoring) throws MessagingException, IOException {
monitoring.setLastRun(LocalDateTime.now()); monitoring.setLastRun(LocalDateTime.now());
@@ -134,15 +142,24 @@ public class MonitoringSearchService {
int currentTokens = activeTariffInfo.getTokens() + activeTariffInfo.getBoughtTokens(); int currentTokens = activeTariffInfo.getTokens() + activeTariffInfo.getBoughtTokens();
emailService.sendTokensNotFoundEmail(user, currentTokens, tariffMonitoring.getTokens()); emailService.sendTokensNotFoundEmail(user, currentTokens, tariffMonitoring.getTokens());
monitoring.setLastRunStatus("ERROR: " + e.getMessage()); monitoring.setLastRunStatus("ERROR: " + e.getMessage());
updateNextRun(monitoring); updateNextRun(monitoring);
monitoringRepository.save(monitoring); monitoringRepository.save(monitoring);
notificationService.addNotification(NotificationType.TOKEN_NOT_FOUND, user.getId(),
NotificationMessage.FILE_SEARCH.getMessageKey());
} catch (Exception e) { } catch (Exception e) {
User user = userRepository.findById(monitoring.getUserId()).orElseThrow();
log.error("Error processing monitoring search", e); log.error("Error processing monitoring search", e);
monitoring.setLastRunStatus("ERROR: " + e.getMessage()); monitoring.setLastRunStatus("ERROR: " + e.getMessage());
updateNextRun(monitoring); updateNextRun(monitoring);
monitoringRepository.save(monitoring); monitoringRepository.save(monitoring);
notificationService.addNotification(NotificationType.SEARCH_OPERATION_FAILED, user.getId());
} }
} }
@@ -2,11 +2,115 @@ package ru.soune.nocopy.service.notification;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSource;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.entity.notification.Notification;
import ru.soune.nocopy.entity.notification.NotificationStatus;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.repository.NotificationRepository;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.user.UserService;
import java.util.List;
import java.util.Locale;
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public class NotificationService { public class NotificationService {
private final NotificationRepository notificationRepository;
private final UserRepository userRepository;
private final MessageSource messageSource;
@Transactional
public void addNotification(NotificationType notificationType, long userId, Object... args) {
Locale locale = Locale.forLanguageTag("ru");
String message = messageSource.getMessage(notificationType.getMessageKey(), args, locale);
Notification notification = Notification.builder()
.notificationType(notificationType)
.user(userRepository.findById(userId).orElseThrow())
.status(NotificationStatus.NEW)
.message(message)
.build();
notificationRepository.save(notification);
}
@Transactional
public void deleteNotification(Notification notification) {
notificationRepository.delete(notification);
}
/**
Получение всех нотификаций с сортировкой
*/
@Transactional
public Page<Notification> allUserNotifications(User user, int page, int size, String propertyToSort) {
Pageable pageable = PageRequest.of(page, size, Sort.by(propertyToSort).descending());
return notificationRepository.getNotificationsByUser(user, pageable);
}
public Page<Notification> getAllNotifications(User user,
int page,
int size,
String sortBy,
String sortDirection,
List<NotificationType> types,
List<NotificationStatus> statuses) {
Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), sortBy);
PageRequest pageable = PageRequest.of(page, size, sort);
boolean hasTypes = types != null && !types.isEmpty();
boolean hasStatuses = statuses != null && !statuses.isEmpty();
if (hasTypes && hasStatuses) {
return notificationRepository.findByUserAndNotificationTypeInAndStatusIn(user, types, statuses, pageable);
} else if (hasTypes) {
return notificationRepository.findByUserAndNotificationTypeIn(user, types, pageable);
} else if (hasStatuses) {
return notificationRepository.findByUserAndStatusIn(user, statuses, pageable);
} else {
return notificationRepository.findByUser(user, pageable);
}
}
/**
* 5 последних непрочитанных уведомлений.
*/
public List<Notification> getActiveNotifications(User user) {
return notificationRepository.findTop5ByUserAndStatusOrderByCreatedAtDesc(user, NotificationStatus.NEW);
}
/**
* Общее количество непрочитанных уведомлений.
*/
public long getUnreadCount(User user) {
return notificationRepository.countByUserAndStatus(user, NotificationStatus.NEW);
}
/**
* Пометить выбранные уведомления как прочитанные.
* @return количество обновлённых записей
*/
@Transactional
public int markAsRead(User user, List<Long> notificationIds) {
if (notificationIds == null || notificationIds.isEmpty()) {
return 0;
}
return notificationRepository.updateStatus(notificationIds, user, NotificationStatus.NEW,
NotificationStatus.READIED);
}
} }
@@ -4,9 +4,11 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.ReferralRepo;
import ru.soune.ReferralService; import ru.soune.ReferralService;
import ru.soune.nocopy.client.YooKassaClient; import ru.soune.nocopy.client.YooKassaClient;
import ru.soune.nocopy.dto.payment.PaymentMethodDTO; import ru.soune.nocopy.dto.payment.PaymentMethodDTO;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.payment.Payment; import ru.soune.nocopy.entity.payment.Payment;
import ru.soune.nocopy.entity.payment.PaymentMethod; import ru.soune.nocopy.entity.payment.PaymentMethod;
import ru.soune.nocopy.entity.payment.PaymentOperationType; import ru.soune.nocopy.entity.payment.PaymentOperationType;
@@ -19,6 +21,7 @@ import ru.soune.nocopy.exception.PaymentNotFoundException;
import ru.soune.nocopy.exception.TariffNotFoundException; import ru.soune.nocopy.exception.TariffNotFoundException;
import ru.soune.nocopy.exception.UserNotFoundException; import ru.soune.nocopy.exception.UserNotFoundException;
import ru.soune.nocopy.repository.*; import ru.soune.nocopy.repository.*;
import ru.soune.nocopy.service.notification.NotificationService;
import ru.soune.nocopy.service.tariff.TariffInfoService; import ru.soune.nocopy.service.tariff.TariffInfoService;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -46,6 +49,10 @@ public class PaymentService {
private final YooKassaClient yooKassaClient; private final YooKassaClient yooKassaClient;
private final ReferralRepo referralRepo;
private final NotificationService notificationService;
@Transactional @Transactional
public Payment createPayment(String email, Long tariffId, String operationType, String paymentUuid) { public Payment createPayment(String email, Long tariffId, String operationType, String paymentUuid) {
User user = userRepository.findByEmail(email); User user = userRepository.findByEmail(email);
@@ -158,15 +165,19 @@ public class PaymentService {
savePaymentMethod(payment.getUser(), paymentMethod); savePaymentMethod(payment.getUser(), paymentMethod);
referralService.onUserAccountRefill(payment.getUser().getId(), intPrice); referralService.onUserAccountRefill(payment.getUser().getId(), intPrice);
long inviterIdForUser = referralRepo.getInviterIdForUser(payment.getUser().getId());
notificationService.addNotification(NotificationType.REFERRAL_ACTIVATED, inviterIdForUser);
} else if ("payment.canceled".equals(eventType)) { } else if ("payment.canceled".equals(eventType)) {
payment.setStatus(PaymentStatus.CANCELED); payment.setStatus(PaymentStatus.CANCELED);
Map<String, Object> cancellationDetails = (Map<String, Object>) object.get("cancellation_details"); Map<String, Object> cancellationDetails = (Map<String, Object>) object.get("cancellation_details");
if (cancellationDetails != null) {
String reason = (String) cancellationDetails.get("reason"); String reason = (String) cancellationDetails.get("reason");
payment.setCancellationReason(reason); payment.setCancellationReason(reason);
}
notificationService.addNotification(NotificationType.PAYOUT_RESULT, payment.getUser().getId(), reason);
} else if ("payment.waiting_for_capture".equals(eventType)) { } else if ("payment.waiting_for_capture".equals(eventType)) {
payment.setStatus(PaymentStatus.WAITING); payment.setStatus(PaymentStatus.WAITING);
payment.setCancellationReason(null); payment.setCancellationReason(null);
@@ -175,10 +186,11 @@ public class PaymentService {
payment.setStatus(PaymentStatus.FAILED); payment.setStatus(PaymentStatus.FAILED);
Map<String, Object> cancellationDetails = (Map<String, Object>) object.get("cancellation_details"); Map<String, Object> cancellationDetails = (Map<String, Object>) object.get("cancellation_details");
if (cancellationDetails != null) {
String reason = (String) cancellationDetails.get("reason"); String reason = (String) cancellationDetails.get("reason");
payment.setCancellationReason(reason); payment.setCancellationReason(reason);
}
notificationService.addNotification(NotificationType.PAYOUT_RESULT, payment.getUser().getId(), reason);
} }
paymentRepository.save(payment); paymentRepository.save(payment);
@@ -106,6 +106,26 @@ public class AuthService {
return authToken.getUser().getId(); return authToken.getUser().getId();
} }
@Transactional
public User getUser(String token) {
if (token == null || token.isBlank()) {
throw new NotFoundAuthToken("Token is required");
}
if (token.startsWith("Bearer ")) {
token = token.replace("Bearer ", "");
}
AuthToken authToken = authTokenRepository.findByToken(token)
.orElseThrow(() -> new NotFoundAuthToken("Token not found"));
authToken.setLastUsedAt(LocalDateTime.now());
authToken.setExpiresAt(LocalDateTime.now().plusHours(1));
authTokenRepository.save(authToken);
return authToken.getUser();
}
public AuthToken login(LoginRequest request) { public AuthToken login(LoginRequest request) {
User user = userRepository.findByEmail(request.getEmail()); User user = userRepository.findByEmail(request.getEmail());
+19
View File
@@ -0,0 +1,19 @@
notification.search.result=Результат поиска готов
notification.monitoring.result=Результат мониторинга доступен
notification.operation.impossible=Операция невозможна
notification.start.failed.now=Запуск не удался
notification.start.failed.next=Запуск не удался
notification.payout.result=Результат выплаты {0}
notification.payment.result=Результат оплаты {0}
notification.referral.deposit=Реферальный депозит
notification.referral.registered=Новый реферал зарегистрирован
notification.referral.activated=Реферал активирован
notification.complaint.status=Статус жалобы изменён: {0}
notification.case.status=Статус дела изменён
notification.incoming.message=Новое сообщение
notification.tariff.expiring=Тариф скоро истекает
notification.token.not.found=Недостаточно токенов для операции: {0}
notification.file.moderation=Файл проходит модерацию
notification.file.added=Файл {0} добавлен в систему
notification.file.search=Поиск файла
notification.search.operation.filed=Поиск завершился непредвиденной ошибкой
+15
View File
@@ -0,0 +1,15 @@
notification.search.result=Search result is ready
notification.monitoring.result=Monitoring result is available
notification.operation.impossible=Operation impossible
notification.start.failed.now=Start failed
notification.start.failed.next=Start failed
notification.payout.result=Payout result
notification.referral.deposit=Referral deposit
notification.referral.registered=New referral registered
notification.referral.activated=Referral activated
notification.complaint.status=Complaint status changed
notification.case.status=Case status changed
notification.incoming.message=New message
notification.tariff.expiring=Tariff expiring soon
notification.file.moderation=File is being moderated
notification.file.added=File {0} added to the system