From 65bbb00abf0962ed3c13d58c6606f2af190720dc Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 12 Mar 2026 01:32:11 +0700 Subject: [PATCH] dev add vio --- .../nocopy/configuration/HandlerConfig.java | 4 +- .../search/GlobalSearchStatisticsRequest.java | 17 +++ .../GlobalSearchStatisticsResponse.java | 83 +++++++++++++ .../violation/ViolationStatisticsRequest.java | 4 +- .../nocopy/handler/GlobalSearchHandler.java | 54 +++++++++ .../handler/ViolationStatisticsHandler.java | 20 +++- .../GlobalSearchResultRepository.java | 10 ++ .../GlobalSearchTaskRepository.java | 18 +++ .../repository/ViolationRepository.java | 8 ++ .../search/GlobalSearchStatisticsService.java | 109 ++++++++++++++++++ 10 files changed, 321 insertions(+), 6 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsResponse.java create mode 100644 src/main/java/ru/soune/nocopy/handler/GlobalSearchHandler.java create mode 100644 src/main/java/ru/soune/nocopy/service/search/GlobalSearchStatisticsService.java diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index 223c94d..2b02251 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -30,7 +30,8 @@ public class HandlerConfig { CostHandler costHandler, MonitoringHandler monitoringHandler, ViolationHandler violationHandler, - ViolationStatisticsHandler violationStatisticsHandler + ViolationStatisticsHandler violationStatisticsHandler, + GlobalSearchHandler globalSearchHandler ) { Map map = new HashMap<>(); map.put(20001, login); @@ -52,6 +53,7 @@ public class HandlerConfig { map.put(30008, costHandler); map.put(30009, violationHandler); map.put(30010, violationStatisticsHandler); + map.put(30011, globalSearchHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsRequest.java b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsRequest.java new file mode 100644 index 0000000..ba11c41 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsRequest.java @@ -0,0 +1,17 @@ +package ru.soune.nocopy.dto.search; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class GlobalSearchStatisticsRequest { + + @JsonProperty("token") + private String token; + + @JsonProperty("limit") + private Integer limit = 10; + + @JsonProperty("days") + private Integer days; +} diff --git a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsResponse.java b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsResponse.java new file mode 100644 index 0000000..69444ef --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsResponse.java @@ -0,0 +1,83 @@ +package ru.soune.nocopy.dto.search; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import ru.soune.nocopy.entity.search.GlobalSearchTask; + +import java.time.format.DateTimeFormatter; +import java.util.List; + +@Data +@Builder +public class GlobalSearchStatisticsResponse { + + @JsonProperty("total_searches") + private long totalSearches; + + @JsonProperty("searches_by_status") + private StatusCountDto searchesByStatus; + + @JsonProperty("recent_searches") + private List recentSearches; + + @Data + @Builder + public static class StatusCountDto { + @JsonProperty("processing") + private long processing; + + @JsonProperty("completed") + private long completed; + + @JsonProperty("failed") + private long failed; + + @JsonProperty("total") + private long total; + } + + @Data + @Builder + public static class SearchStatDto { + @JsonProperty("task_id") + private String taskId; + + @JsonProperty("search_type") + private String searchType; + + @JsonProperty("status") + private String status; + + @JsonProperty("total_files") + private Integer totalFiles; + + @JsonProperty("violations_found") + private long violationsFound; + + @JsonProperty("created_at") + private String createdAt; + + @JsonProperty("duration_seconds") + private Long durationSeconds; + + public static SearchStatDto fromEntity(GlobalSearchTask task, long violationsCount) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + Long duration = null; + if (task.getUpdatedAt() != null && task.getCreatedAt() != null) { + duration = java.time.Duration.between(task.getCreatedAt(), task.getUpdatedAt()).getSeconds(); + } + + return SearchStatDto.builder() + .taskId(task.getTaskId()) + .searchType(task.getSearchType()) + .status(task.getStatus()) + .totalFiles(task.getTotalFiles()) + .violationsFound(violationsCount) + .createdAt(task.getCreatedAt() != null ? task.getCreatedAt().format(formatter) : null) + .durationSeconds(duration) + .build(); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsRequest.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsRequest.java index c9246e7..04cbac6 100644 --- a/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsRequest.java @@ -6,8 +6,8 @@ import lombok.Data; @Data public class ViolationStatisticsRequest { - @JsonProperty("user_id") - private Long userId; + @JsonProperty("token") + private String token; @JsonProperty("file_id") private String fileId; diff --git a/src/main/java/ru/soune/nocopy/handler/GlobalSearchHandler.java b/src/main/java/ru/soune/nocopy/handler/GlobalSearchHandler.java new file mode 100644 index 0000000..a332c2e --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/GlobalSearchHandler.java @@ -0,0 +1,54 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.search.GlobalSearchStatisticsRequest; +import ru.soune.nocopy.dto.search.GlobalSearchStatisticsResponse; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.repository.AuthTokenRepository; +import ru.soune.nocopy.service.search.GlobalSearchStatisticsService; + +import java.util.Optional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class GlobalSearchHandler implements RequestHandler { + + private final ObjectMapper objectMapper; + + private final GlobalSearchStatisticsService statisticsService; + + private final AuthTokenRepository authTokenRepository; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + GlobalSearchStatisticsRequest statRequest = objectMapper.convertValue( + request.getMessageBody(), GlobalSearchStatisticsRequest.class); + + if (statRequest.getToken() == null) { + throw new IllegalArgumentException("User token is required"); + } + + Optional tokenOptional = + authTokenRepository.findByToken(statRequest.getToken()); + + if (tokenOptional.isPresent()) { + throw new IllegalArgumentException("User token is required"); + } + + Long userId = tokenOptional.orElseThrow().getUser().getId(); + + GlobalSearchStatisticsResponse statistics = statisticsService.getStatistics( + userId, statRequest.getLimit(), statRequest.getDays()); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(statistics) + .build(); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java b/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java index a12b6c5..60dfb88 100644 --- a/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java @@ -8,11 +8,14 @@ import ru.soune.nocopy.dto.BaseRequest; import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.violation.ViolationStatisticsRequest; import ru.soune.nocopy.dto.violation.ViolationStatisticsResponse; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.service.violation.ViolationService; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; +import java.util.Optional; @Slf4j @Component @@ -22,6 +25,8 @@ public class ViolationStatisticsHandler implements RequestHandler { private final ViolationService violationService; + private final AuthTokenRepository authTokenRepository; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @Override @@ -43,8 +48,17 @@ public class ViolationStatisticsHandler implements RequestHandler { } } + Optional tokenOptional = + authTokenRepository.findByToken(statRequest.getToken()); + + if (tokenOptional.isPresent()) { + throw new IllegalArgumentException("User token is required"); + } + + Long userId = tokenOptional.orElseThrow().getUser().getId(); + ViolationStatisticsResponse statistics = violationService.getViolationStatistics( - statRequest.getUserId(), + userId, statRequest.getFileId(), startDate, endDate @@ -57,8 +71,8 @@ public class ViolationStatisticsHandler implements RequestHandler { } private void validateRequest(ViolationStatisticsRequest request) { - if (request.getUserId() == null) { - throw new IllegalArgumentException("User ID is required"); + if (request.getToken() == null) { + throw new IllegalArgumentException("Token is required"); } } diff --git a/src/main/java/ru/soune/nocopy/repository/GlobalSearchResultRepository.java b/src/main/java/ru/soune/nocopy/repository/GlobalSearchResultRepository.java index dc41337..42eec8a 100644 --- a/src/main/java/ru/soune/nocopy/repository/GlobalSearchResultRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/GlobalSearchResultRepository.java @@ -1,6 +1,8 @@ package ru.soune.nocopy.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import ru.soune.nocopy.entity.search.GlobalSearchResult; @@ -9,5 +11,13 @@ import java.util.List; @Repository public interface GlobalSearchResultRepository extends JpaRepository { List findByTaskIdOrderByIdAsc(String taskId); + void deleteByTaskId(String taskId); + + List findByTaskId(String taskId); + + @Query("SELECT g.fileId FROM GlobalSearchResult g WHERE g.taskId = :taskId") + List findFileIdsByTaskId(@Param("taskId") String taskId); + + long countByTaskId(String taskId); } diff --git a/src/main/java/ru/soune/nocopy/repository/GlobalSearchTaskRepository.java b/src/main/java/ru/soune/nocopy/repository/GlobalSearchTaskRepository.java index 908a659..e969384 100644 --- a/src/main/java/ru/soune/nocopy/repository/GlobalSearchTaskRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/GlobalSearchTaskRepository.java @@ -1,13 +1,31 @@ package ru.soune.nocopy.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import ru.soune.nocopy.entity.search.GlobalSearchTask; +import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; @Repository public interface GlobalSearchTaskRepository extends JpaRepository { Optional findByTaskIdAndUserId(String taskId, Long userId); + + List findByUserIdOrderByCreatedAtDesc(Long userId); + + List findByUserIdAndCreatedAtAfterOrderByCreatedAtDesc(Long userId, LocalDateTime date); + + @Query("SELECT COUNT(t) FROM GlobalSearchTask t WHERE t.userId IN :userIds") + long countByUserId(@Param("userIds") List userIds); + + @Query("SELECT t.status, COUNT(t) FROM GlobalSearchTask t WHERE t.userId IN :userIds GROUP BY t.status") + List countByStatusAndUserId(@Param("userIds") List userIds); + + @Query("SELECT t FROM GlobalSearchTask t WHERE t.userId = :userId ORDER BY t.createdAt DESC") + List findTopByUserIdOrderByCreatedAtDesc(@Param("userId") Long userId, + org.springframework.data.domain.Pageable pageable); } diff --git a/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java b/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java index 1679f9f..942b31c 100644 --- a/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java @@ -3,6 +3,8 @@ 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.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.violation.Violation; @@ -42,4 +44,10 @@ public interface ViolationRepository extends JpaRepository { List findByFileEntityInAndCreatedDateBetween(List files, LocalDateTime startDate, LocalDateTime endDate); + + @Query("SELECT COUNT(v) FROM Violation v WHERE v.fileEntity.id IN :fileIds") + long countByFileEntityIdIn(@Param("fileIds") List fileIds); + + @Query("SELECT COUNT(v) FROM Violation v WHERE v.globalSearchResult.taskId = :taskId") + long countByGlobalSearchResultTaskId(@Param("taskId") String taskId); } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchStatisticsService.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchStatisticsService.java new file mode 100644 index 0000000..383f8b4 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchStatisticsService.java @@ -0,0 +1,109 @@ +package ru.soune.nocopy.service.search; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.dto.search.GlobalSearchStatisticsResponse; +import ru.soune.nocopy.entity.company.Company; +import ru.soune.nocopy.entity.search.GlobalSearchTask; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.repository.GlobalSearchResultRepository; +import ru.soune.nocopy.repository.GlobalSearchTaskRepository; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.repository.ViolationRepository; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class GlobalSearchStatisticsService { + + private final GlobalSearchTaskRepository taskRepository; + + private final GlobalSearchResultRepository resultRepository; + + private final ViolationRepository violationRepository; + + private final UserRepository userRepository; + + @Transactional(readOnly = true) + public GlobalSearchStatisticsResponse getStatistics(Long userId, Integer limit, Integer days) { + User user = userRepository.findById(userId).orElseThrow(); + Company company = user.getCompany(); + List userIds = company == null ? Arrays.asList(userId): + user.getCompany() + .getUsers() + .stream() + .map(User::getId).toList(); + + long totalSearches = taskRepository.countByUserId(userIds); + List statusCounts = taskRepository.countByStatusAndUserId(userIds); + + GlobalSearchStatisticsResponse.StatusCountDto statusCountDto = + GlobalSearchStatisticsResponse.StatusCountDto.builder() + .processing(0) + .completed(0) + .failed(0) + .total(totalSearches) + .build(); + + for (Object[] row : statusCounts) { + String status = (String) row[0]; + Long count = (Long) row[1]; + + if ("PROCESSING".equalsIgnoreCase(status)) { + statusCountDto.setProcessing(count); + } else if ("COMPLETED".equalsIgnoreCase(status) || "SUCCESS".equalsIgnoreCase(status)) { + statusCountDto.setCompleted(statusCountDto.getCompleted() + count); + } else if ("FAILED".equalsIgnoreCase(status) || "TIMEOUT".equalsIgnoreCase(status)) { + statusCountDto.setFailed(statusCountDto.getFailed() + count); + } + } + + List recentTasks; + + if (days != null && days > 0) { + LocalDateTime fromDate = LocalDateTime.now().minusDays(days); + recentTasks = taskRepository.findByUserIdAndCreatedAtAfterOrderByCreatedAtDesc(userId, fromDate); + + if (limit != null && limit > 0 && recentTasks.size() > limit) { + recentTasks = recentTasks.subList(0, limit); + } + } else { + org.springframework.data.domain.Pageable pageable = + org.springframework.data.domain.PageRequest.of(0, limit != null ? limit : 10); + recentTasks = taskRepository.findTopByUserIdOrderByCreatedAtDesc(userId, pageable); + } + + List recentSearchDtos = new ArrayList<>(); + + for (GlobalSearchTask task : recentTasks) { + long violationsCount = getViolationsCountForTask(task.getTaskId()); + + recentSearchDtos.add(GlobalSearchStatisticsResponse.SearchStatDto.fromEntity(task, violationsCount)); + } + + return GlobalSearchStatisticsResponse.builder() + .totalSearches(totalSearches) + .searchesByStatus(statusCountDto) + .recentSearches(recentSearchDtos) + .build(); + } + + @Transactional(readOnly = true) + public long getViolationsCountForTask(String taskId) { + List fileIds = resultRepository.findByTaskId(taskId).stream() + .map(result -> result.getFileId()) + .toList(); + + if (fileIds.isEmpty()) { + return 0; + } + return violationRepository.countByFileEntityIdIn(fileIds); + } +}