dev add vio
Test Workflow / test (push) Successful in 14s

This commit is contained in:
vladp
2026-03-12 01:32:11 +07:00
parent 869a25043a
commit 65bbb00abf
10 changed files with 321 additions and 6 deletions
@@ -30,7 +30,8 @@ public class HandlerConfig {
CostHandler costHandler, CostHandler costHandler,
MonitoringHandler monitoringHandler, MonitoringHandler monitoringHandler,
ViolationHandler violationHandler, ViolationHandler violationHandler,
ViolationStatisticsHandler violationStatisticsHandler ViolationStatisticsHandler violationStatisticsHandler,
GlobalSearchHandler globalSearchHandler
) { ) {
Map<Integer, RequestHandler> map = new HashMap<>(); Map<Integer, RequestHandler> map = new HashMap<>();
map.put(20001, login); map.put(20001, login);
@@ -52,6 +53,7 @@ public class HandlerConfig {
map.put(30008, costHandler); map.put(30008, costHandler);
map.put(30009, violationHandler); map.put(30009, violationHandler);
map.put(30010, violationStatisticsHandler); map.put(30010, violationStatisticsHandler);
map.put(30011, globalSearchHandler);
return map; return map;
} }
@@ -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;
}
@@ -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<SearchStatDto> 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();
}
}
}
@@ -6,8 +6,8 @@ import lombok.Data;
@Data @Data
public class ViolationStatisticsRequest { public class ViolationStatisticsRequest {
@JsonProperty("user_id") @JsonProperty("token")
private Long userId; private String token;
@JsonProperty("file_id") @JsonProperty("file_id")
private String fileId; private String fileId;
@@ -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<AuthToken> 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();
}
}
@@ -8,11 +8,14 @@ import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.violation.ViolationStatisticsRequest; import ru.soune.nocopy.dto.violation.ViolationStatisticsRequest;
import ru.soune.nocopy.dto.violation.ViolationStatisticsResponse; 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 ru.soune.nocopy.service.violation.ViolationService;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
import java.util.Optional;
@Slf4j @Slf4j
@Component @Component
@@ -22,6 +25,8 @@ public class ViolationStatisticsHandler implements RequestHandler {
private final ViolationService violationService; private final ViolationService violationService;
private final AuthTokenRepository authTokenRepository;
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");
@Override @Override
@@ -43,8 +48,17 @@ public class ViolationStatisticsHandler implements RequestHandler {
} }
} }
Optional<AuthToken> 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( ViolationStatisticsResponse statistics = violationService.getViolationStatistics(
statRequest.getUserId(), userId,
statRequest.getFileId(), statRequest.getFileId(),
startDate, startDate,
endDate endDate
@@ -57,8 +71,8 @@ public class ViolationStatisticsHandler implements RequestHandler {
} }
private void validateRequest(ViolationStatisticsRequest request) { private void validateRequest(ViolationStatisticsRequest request) {
if (request.getUserId() == null) { if (request.getToken() == null) {
throw new IllegalArgumentException("User ID is required"); throw new IllegalArgumentException("Token is required");
} }
} }
@@ -1,6 +1,8 @@
package ru.soune.nocopy.repository; package ru.soune.nocopy.repository;
import org.springframework.data.jpa.repository.JpaRepository; 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 org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.search.GlobalSearchResult; import ru.soune.nocopy.entity.search.GlobalSearchResult;
@@ -9,5 +11,13 @@ import java.util.List;
@Repository @Repository
public interface GlobalSearchResultRepository extends JpaRepository<GlobalSearchResult, Long> { public interface GlobalSearchResultRepository extends JpaRepository<GlobalSearchResult, Long> {
List<GlobalSearchResult> findByTaskIdOrderByIdAsc(String taskId); List<GlobalSearchResult> findByTaskIdOrderByIdAsc(String taskId);
void deleteByTaskId(String taskId); void deleteByTaskId(String taskId);
List<GlobalSearchResult> findByTaskId(String taskId);
@Query("SELECT g.fileId FROM GlobalSearchResult g WHERE g.taskId = :taskId")
List<String> findFileIdsByTaskId(@Param("taskId") String taskId);
long countByTaskId(String taskId);
} }
@@ -1,13 +1,31 @@
package ru.soune.nocopy.repository; package ru.soune.nocopy.repository;
import org.springframework.data.jpa.repository.JpaRepository; 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 org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.search.GlobalSearchTask; import ru.soune.nocopy.entity.search.GlobalSearchTask;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional; import java.util.Optional;
@Repository @Repository
public interface GlobalSearchTaskRepository extends JpaRepository<GlobalSearchTask, String> { public interface GlobalSearchTaskRepository extends JpaRepository<GlobalSearchTask, String> {
Optional<GlobalSearchTask> findByTaskIdAndUserId(String taskId, Long userId); Optional<GlobalSearchTask> findByTaskIdAndUserId(String taskId, Long userId);
List<GlobalSearchTask> findByUserIdOrderByCreatedAtDesc(Long userId);
List<GlobalSearchTask> findByUserIdAndCreatedAtAfterOrderByCreatedAtDesc(Long userId, LocalDateTime date);
@Query("SELECT COUNT(t) FROM GlobalSearchTask t WHERE t.userId IN :userIds")
long countByUserId(@Param("userIds") List<Long> userIds);
@Query("SELECT t.status, COUNT(t) FROM GlobalSearchTask t WHERE t.userId IN :userIds GROUP BY t.status")
List<Object[]> countByStatusAndUserId(@Param("userIds") List<Long> userIds);
@Query("SELECT t FROM GlobalSearchTask t WHERE t.userId = :userId ORDER BY t.createdAt DESC")
List<GlobalSearchTask> findTopByUserIdOrderByCreatedAtDesc(@Param("userId") Long userId,
org.springframework.data.domain.Pageable pageable);
} }
@@ -3,6 +3,8 @@ package ru.soune.nocopy.repository;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; 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 org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.violation.Violation; import ru.soune.nocopy.entity.violation.Violation;
@@ -42,4 +44,10 @@ public interface ViolationRepository extends JpaRepository<Violation, Long> {
List<Violation> findByFileEntityInAndCreatedDateBetween(List<FileEntity> files, List<Violation> findByFileEntityInAndCreatedDateBetween(List<FileEntity> files,
LocalDateTime startDate, LocalDateTime startDate,
LocalDateTime endDate); LocalDateTime endDate);
@Query("SELECT COUNT(v) FROM Violation v WHERE v.fileEntity.id IN :fileIds")
long countByFileEntityIdIn(@Param("fileIds") List<String> fileIds);
@Query("SELECT COUNT(v) FROM Violation v WHERE v.globalSearchResult.taskId = :taskId")
long countByGlobalSearchResultTaskId(@Param("taskId") String taskId);
} }
@@ -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<Long> userIds = company == null ? Arrays.asList(userId):
user.getCompany()
.getUsers()
.stream()
.map(User::getId).toList();
long totalSearches = taskRepository.countByUserId(userIds);
List<Object[]> 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<GlobalSearchTask> 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<GlobalSearchStatisticsResponse.SearchStatDto> 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<String> fileIds = resultRepository.findByTaskId(taskId).stream()
.map(result -> result.getFileId())
.toList();
if (fileIds.isEmpty()) {
return 0;
}
return violationRepository.countByFileEntityIdIn(fileIds);
}
}