Merge branch 'dev' into NCBACK-35

# Conflicts:
#	src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java
This commit is contained in:
vladp
2026-04-04 11:23:05 +07:00
32 changed files with 689 additions and 43 deletions
@@ -1,7 +1,5 @@
package ru.soune.nocopy.service.complaint;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.ComplaintStatus;
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.exception.ComplaintNotFoundException;
import ru.soune.nocopy.exception.DuplicateComplaintException;
import ru.soune.nocopy.exception.ViolationNotFoundException;
import ru.soune.nocopy.repository.ComplaintEntityRepository;
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.ViolationStatus;
@@ -37,6 +37,8 @@ public class ComplaintEntityService {
private final ViolationService violationService;
private final NotificationService notificationService;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public ComplaintResponse createComplaint(ComplaintRequest request) {
@@ -101,7 +103,12 @@ public class ComplaintEntityService {
}
if (request.getComplaintText() != null) complaint.setComplaintText(request.getComplaintText());
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));
}
@@ -82,11 +82,11 @@ public class FileEntityService {
.map(User::getId)
.toList()) {
allFiles.addAll(fileEntityRepository.findByUserIdAndStatusIn(uId,
List.of(FileStatus.ACTIVE, FileStatus.BLOCKED)));
List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION)));
}
} else {
allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId,
List.of(FileStatus.ACTIVE, FileStatus.BLOCKED));
List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION));
}
return allFiles;
@@ -102,12 +102,12 @@ public class FileEntityService {
.map(User::getId)
.toList()) {
allFiles.addAll(fileEntityRepository.findByUserIdAndStatusIn(uId,
List.of(FileStatus.ACTIVE, FileStatus.BLOCKED)));
List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION)));
}
} else {
allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId,
List.of(FileStatus.ACTIVE, FileStatus.BLOCKED));
List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION));
}
int start = (page - 1) * pageSize;
@@ -18,6 +18,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import ru.soune.nocopy.dto.file.UploadProgressResponse;
import ru.soune.nocopy.entity.file.*;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.exception.*;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.FileUploadSessionRepository;
@@ -29,6 +30,7 @@ import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileUploadService;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import ru.soune.nocopy.service.tariff.TariffConstants;
import ru.soune.nocopy.service.notification.NotificationService;
import ru.soune.nocopy.service.tariff.TariffInfoService;
import ru.soune.nocopy.util.FileUtil;
@@ -87,6 +89,8 @@ public class FileUploadServiceImpl implements FileUploadService {
private final TariffInfoService tariffInfoService;
private final NotificationService notificationService;
private final CloudStorageService cloudStorageService;
@PostConstruct
@@ -374,6 +378,8 @@ public class FileUploadServiceImpl implements FileUploadService {
session.setStatus(UploadStatus.COMPLETED);
session.setFilePath(finalFilePath);
notificationService.addNotification(NotificationType.FILE_ADDED_TO_SYSTEM, session.getUserId());
FileStatus status = findSimilar == 0 ? FileStatus.MODERATION: 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.entity.file.*;
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.InvalidAppealException;
@@ -18,6 +19,7 @@ import ru.soune.nocopy.repository.FileAppealRepository;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.ModerationLogRepository;
import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.notification.NotificationService;
import java.io.IOException;
import java.time.LocalDateTime;
@@ -35,6 +37,8 @@ public class ModerationService {
private final FileEntityService fileEntityService;
private final NotificationService notificationService;
private static final List<FileStatus> MODERATION_NEEDED_STATUSES = Arrays.asList(
FileStatus.MODERATION,
FileStatus.BLOCKED
@@ -84,6 +88,8 @@ public class ModerationService {
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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
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.entity.monitoring.FileMonitoringEntity;
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.TariffType;
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.UserRepository;
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.tariff.TariffInfoService;
import ru.soune.nocopy.service.tariff.TariffService;
@@ -48,6 +52,10 @@ public class MonitoringSearchService {
private final SearchProperties searchProperties;
private final NotificationService notificationService;
private final MessageSource messageSource;
@Transactional(noRollbackFor = TariffNotFoundException.class)
public void processFileSearch(FileMonitoringEntity monitoring) throws MessagingException, IOException {
monitoring.setLastRun(LocalDateTime.now());
@@ -123,7 +131,8 @@ public class MonitoringSearchService {
}
monitoring.setLastRunStatus("SUCCESS");
updateNextRun(monitoring);
monitoringRepository.save(monitoring);
if (uniqueResults.isEmpty()) {
log.info("No results found for monitoring file {}", monitoring.getFile().getId());
}
@@ -134,15 +143,24 @@ public class MonitoringSearchService {
int currentTokens = activeTariffInfo.getTokens() + activeTariffInfo.getBoughtTokens();
emailService.sendTokensNotFoundEmail(user, currentTokens, tariffMonitoring.getTokens());
monitoring.setLastRunStatus("ERROR: " + e.getMessage());
updateNextRun(monitoring);
monitoringRepository.save(monitoring);
notificationService.addNotification(NotificationType.TOKEN_NOT_FOUND, user.getId(),
NotificationMessage.FILE_SEARCH.getMessageKey());
} catch (Exception e) {
User user = userRepository.findById(monitoring.getUserId()).orElseThrow();
log.error("Error processing monitoring search", e);
monitoring.setLastRunStatus("ERROR: " + e.getMessage());
updateNextRun(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.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.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
@Slf4j
@RequiredArgsConstructor
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 org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.ReferralRepo;
import ru.soune.ReferralService;
import ru.soune.nocopy.client.YooKassaClient;
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.PaymentMethod;
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.UserNotFoundException;
import ru.soune.nocopy.repository.*;
import ru.soune.nocopy.service.notification.NotificationService;
import ru.soune.nocopy.service.tariff.TariffInfoService;
import java.time.LocalDateTime;
@@ -46,6 +49,10 @@ public class PaymentService {
private final YooKassaClient yooKassaClient;
private final ReferralRepo referralRepo;
private final NotificationService notificationService;
@Transactional
public Payment createPayment(String email, Long tariffId, String operationType, String paymentUuid) {
User user = userRepository.findByEmail(email);
@@ -158,15 +165,19 @@ public class PaymentService {
savePaymentMethod(payment.getUser(), paymentMethod);
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)) {
payment.setStatus(PaymentStatus.CANCELED);
Map<String, Object> cancellationDetails = (Map<String, Object>) object.get("cancellation_details");
if (cancellationDetails != null) {
String reason = (String) cancellationDetails.get("reason");
payment.setCancellationReason(reason);
}
String reason = (String) cancellationDetails.get("reason");
payment.setCancellationReason(reason);
notificationService.addNotification(NotificationType.PAYMENT_RESULT, payment.getUser().getId(), reason);
} else if ("payment.waiting_for_capture".equals(eventType)) {
payment.setStatus(PaymentStatus.WAITING);
payment.setCancellationReason(null);
@@ -175,10 +186,11 @@ public class PaymentService {
payment.setStatus(PaymentStatus.FAILED);
Map<String, Object> cancellationDetails = (Map<String, Object>) object.get("cancellation_details");
if (cancellationDetails != null) {
String reason = (String) cancellationDetails.get("reason");
payment.setCancellationReason(reason);
}
String reason = (String) cancellationDetails.get("reason");
payment.setCancellationReason(reason);
notificationService.addNotification(NotificationType.PAYMENT_RESULT, payment.getUser().getId(), reason);
}
paymentRepository.save(payment);
@@ -11,9 +11,11 @@ import org.springframework.transaction.annotation.Transactional;
import ru.soune.Referral;
import ru.soune.ReferralInvitee;
import ru.soune.ReferralRepo;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.repository.ReferralJpaRepository;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.notification.NotificationService;
import java.lang.reflect.Constructor;
@@ -31,6 +33,8 @@ public class ReferralRepoImpl implements ReferralRepo {
private final UserRepository userRepository;
private final NotificationService notificationService;
@Override
public @NotNull String getUserReferralLink(long userId) {
ru.soune.nocopy.entity.referral.Referral referral =
@@ -150,6 +154,8 @@ public class ReferralRepoImpl implements ReferralRepo {
@Override
public void createReferralEntity(@NotNull Referral entity) {
ru.soune.nocopy.entity.referral.Referral referral = new ru.soune.nocopy.entity.referral.Referral();
Long inviter = entity.getInviter();
referral.setUserId(entity.getUserId());
referral.setReferralLink(entity.getReferralLink());
referral.setInviterId(entity.getInviter());
@@ -160,6 +166,10 @@ public class ReferralRepoImpl implements ReferralRepo {
referral.setActive(entity.getActive());
referralRepository.save(referral);
if (inviter != null){
notificationService.addNotification(NotificationType.REFERRAL_REGISTERED, inviter);
}
}
@Override
@@ -106,6 +106,26 @@ public class AuthService {
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) {
User user = userRepository.findByEmail(request.getEmail());
@@ -161,7 +181,7 @@ public class AuthService {
}
public AuthToken getAuthToken(String token) {
return authTokenRepository.findByToken(token).get();
return authTokenRepository.findByToken(token).orElse(null);
}
private AuthToken genereateAuthToken(User user) {
@@ -8,11 +8,14 @@ import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.configuration.search.SearchProperties;
import ru.soune.nocopy.dto.file.YandexSearchResponse;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.search.GlobalSearchResult;
import ru.soune.nocopy.entity.search.GlobalSearchTask;
import ru.soune.nocopy.entity.search.SearchStatus;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.GlobalSearchResultRepository;
import ru.soune.nocopy.repository.GlobalSearchTaskRepository;
import ru.soune.nocopy.service.notification.NotificationService;
import ru.soune.nocopy.service.tariff.TariffConstants;
import ru.soune.nocopy.service.tariff.TariffInfoService;
import ru.soune.nocopy.service.violation.ViolationService;
@@ -41,31 +44,38 @@ public class GlobalSearchAsyncProcessor {
private final SearchProperties searchProperties;
private final NotificationService notificationService;
private final FileEntityRepository fileEntityRepository;
@Async
@Transactional
public void processFilesAsync(String taskId, List<FileEntity> filesToProcess, Long userId) {
public void processFilesAsync(String taskId, List<String> filesToProcess, Long userId) {
GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow();
try {
for (int i = 0; i < filesToProcess.size(); i++) {
FileEntity file = filesToProcess.get(i);
String uuid = filesToProcess.get(i);
FileEntity file = fileEntityRepository.findById(uuid)
.orElseThrow(() -> new RuntimeException("File not found: " + uuid));
tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH);
GlobalSearchResult result = processFile(file, taskId, userId);
globalSearchResultRepository.save(result);
GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow();
task.setProcessedFiles(i + 1);
task.setUpdatedAt(LocalDateTime.now());
globalSearchTaskRepository.save(task);
}
GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow();
task.setStatus(SearchStatus.COMPLETED.name());
task.setUpdatedAt(LocalDateTime.now());
globalSearchTaskRepository.save(task);
notificationService.addNotification(NotificationType.SEARCH_RESULT, userId);
} catch (Exception e) {
log.error("Global search failed for task: {}", taskId, e);
GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow();
task.setStatus(SearchStatus.FAILED.name());
task.setUpdatedAt(LocalDateTime.now());
globalSearchTaskRepository.save(task);
@@ -46,7 +46,9 @@ public class GlobalSearchService {
globalSearchTaskRepository.save(task);
asyncProcessor.processFilesAsync(task.getTaskId(), filesToProcess, userId);
List<String> fileIds = filesToProcess.stream().map(FileEntity::getId).toList();
asyncProcessor.processFilesAsync(task.getTaskId(), fileIds, userId);
return task.getTaskId();
}