2026-03-10 22:47:43 +07:00
|
|
|
package ru.soune.nocopy.service.violation;
|
|
|
|
|
|
|
|
|
|
import lombok.AllArgsConstructor;
|
2026-03-11 15:42:06 +07:00
|
|
|
import org.apache.commons.codec.digest.DigestUtils;
|
2026-03-10 22:47:43 +07:00
|
|
|
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;
|
2026-03-26 14:34:34 +07:00
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
2026-03-10 22:47:43 +07:00
|
|
|
import ru.soune.nocopy.dto.file.YandexSearchResponse;
|
2026-03-13 13:55:10 +07:00
|
|
|
import ru.soune.nocopy.dto.violation.FileViolationSummaryDTO;
|
2026-03-12 00:21:19 +07:00
|
|
|
import ru.soune.nocopy.dto.violation.ViolationStatisticsResponse;
|
2026-03-10 22:47:43 +07:00
|
|
|
import ru.soune.nocopy.entity.file.FileEntity;
|
|
|
|
|
import ru.soune.nocopy.entity.search.GlobalSearchResult;
|
|
|
|
|
import ru.soune.nocopy.entity.violation.Violation;
|
2026-03-12 00:21:19 +07:00
|
|
|
import ru.soune.nocopy.repository.FileEntityRepository;
|
2026-03-10 22:47:43 +07:00
|
|
|
import ru.soune.nocopy.repository.ViolationRepository;
|
2026-03-12 00:41:04 +07:00
|
|
|
import ru.soune.nocopy.service.file.FileEntityService;
|
2026-03-10 22:47:43 +07:00
|
|
|
|
|
|
|
|
import java.time.LocalDateTime;
|
2026-03-13 14:14:37 +07:00
|
|
|
import java.util.*;
|
2026-03-10 22:47:43 +07:00
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
|
|
@Service
|
|
|
|
|
@AllArgsConstructor
|
|
|
|
|
public class ViolationService {
|
|
|
|
|
|
|
|
|
|
private final ViolationRepository violationRepository;
|
|
|
|
|
|
2026-03-12 00:21:19 +07:00
|
|
|
private final FileEntityRepository fileEntityRepository;
|
|
|
|
|
|
2026-03-12 00:41:04 +07:00
|
|
|
private final FileEntityService fileEntityService;
|
|
|
|
|
|
2026-03-10 22:47:43 +07:00
|
|
|
public void processViolation(YandexSearchResponse.ImageResult imageResult, FileEntity file,
|
|
|
|
|
GlobalSearchResult result) {
|
|
|
|
|
String url = imageResult.getUrl();
|
2026-03-11 15:42:06 +07:00
|
|
|
String urlHash = DigestUtils.sha256Hex(url);
|
2026-03-10 22:47:43 +07:00
|
|
|
|
2026-03-11 15:42:06 +07:00
|
|
|
if (!violationRepository.existsByUrlHash(urlHash)) {
|
2026-03-10 22:47:43 +07:00
|
|
|
Violation violation = new Violation();
|
|
|
|
|
|
|
|
|
|
if (result != null) {
|
|
|
|
|
violation.setGlobalSearchResult(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
violation.setHost(imageResult.getHost());
|
|
|
|
|
violation.setUrl(url);
|
2026-03-11 15:42:06 +07:00
|
|
|
violation.setUrlHash(urlHash);
|
2026-03-10 22:47:43 +07:00
|
|
|
violation.setPageUrl(imageResult.getPageUrl());
|
|
|
|
|
violation.setPageTitle(imageResult.getPageTitle());
|
|
|
|
|
violation.setFileEntity(file);
|
2026-03-26 14:34:34 +07:00
|
|
|
violation.setStatus(ViolationStatus.NEW.name());
|
2026-03-10 22:47:43 +07:00
|
|
|
|
|
|
|
|
violationRepository.save(violation);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 12:16:37 +07:00
|
|
|
public Violation getById(Long violationId) {
|
|
|
|
|
return violationRepository.findById(violationId).orElse(null);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 13:55:10 +07:00
|
|
|
public List<FileViolationSummaryDTO> getFileViolationsSummary(Long userId) {
|
2026-03-13 13:48:03 +07:00
|
|
|
List<FileEntity> userFiles = fileEntityService.getAllUserFiles(userId);
|
2026-04-24 14:14:04 +07:00
|
|
|
List<Violation> violations = violationRepository.findByFileEntityInAndStatusNot(userFiles,
|
|
|
|
|
ViolationStatus.NOT_OWNER_FILE.name());
|
2026-03-13 13:48:03 +07:00
|
|
|
|
|
|
|
|
Map<String, List<Violation>> violationsByFileId = violations.stream()
|
|
|
|
|
.collect(Collectors.groupingBy(v -> v.getFileEntity().getId()));
|
|
|
|
|
|
2026-03-13 13:55:10 +07:00
|
|
|
return userFiles.stream()
|
|
|
|
|
.map(file -> {
|
|
|
|
|
List<Violation> fileViolations = violationsByFileId.getOrDefault(file.getId(),
|
|
|
|
|
Collections.emptyList());
|
2026-03-13 13:48:03 +07:00
|
|
|
|
2026-03-13 14:14:37 +07:00
|
|
|
if (fileViolations.isEmpty()) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 13:55:10 +07:00
|
|
|
LocalDateTime latestDate = fileViolations.stream()
|
|
|
|
|
.map(Violation::getCreatedDate)
|
|
|
|
|
.max(LocalDateTime::compareTo)
|
|
|
|
|
.orElse(null);
|
2026-03-13 13:48:03 +07:00
|
|
|
|
2026-03-13 13:55:10 +07:00
|
|
|
return FileViolationSummaryDTO.builder()
|
|
|
|
|
.fileId(file.getId())
|
|
|
|
|
.supportId(file.getSupportId())
|
|
|
|
|
.fileName(file.getOriginalFileName())
|
|
|
|
|
.fileSize(file.getFileSize())
|
|
|
|
|
.mimeType(file.getMimeType())
|
|
|
|
|
.status(file.getStatus())
|
|
|
|
|
.createdAt(file.getCreatedAt())
|
|
|
|
|
.violationCount(fileViolations.size())
|
|
|
|
|
.latestViolationDate(latestDate)
|
|
|
|
|
.build();
|
|
|
|
|
})
|
2026-03-13 14:14:37 +07:00
|
|
|
.filter(Objects::nonNull)
|
2026-03-13 13:55:10 +07:00
|
|
|
.collect(Collectors.toList());
|
2026-03-13 13:48:03 +07:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 14:34:34 +07:00
|
|
|
@Transactional
|
|
|
|
|
public void changeStatus(ViolationStatus status, Long violationId) {
|
|
|
|
|
Violation violation = violationRepository.findById(violationId).orElseThrow();
|
|
|
|
|
violation.setStatus(status.name());
|
|
|
|
|
|
|
|
|
|
violationRepository.save(violation);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 17:07:44 +07:00
|
|
|
public Page<Violation> getViolationsByFiles(List<FileEntity> files, int page, int size, String sortDirection) {
|
2026-04-24 13:11:49 +07:00
|
|
|
Sort sort = Sort.by(sortDirection.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC,
|
|
|
|
|
"createdDate");
|
2026-03-18 17:07:44 +07:00
|
|
|
Pageable pageable = PageRequest.of(page, size, sort);
|
2026-04-24 13:11:49 +07:00
|
|
|
|
|
|
|
|
return violationRepository.findByFileEntityInAndStatusNot(files, ViolationStatus.NOT_OWNER_FILE.name(), pageable);
|
2026-03-18 17:07:44 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Page<Violation> getViolationsByFilesAndStatus(List<FileEntity> files, String status, int page, int size, String sortDirection) {
|
2026-04-24 13:11:49 +07:00
|
|
|
Sort sort = Sort.by(sortDirection.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC,
|
|
|
|
|
"createdDate");
|
2026-03-18 17:07:44 +07:00
|
|
|
Pageable pageable = PageRequest.of(page, size, sort);
|
2026-04-24 13:11:49 +07:00
|
|
|
|
2026-03-18 17:07:44 +07:00
|
|
|
return violationRepository.findByFileEntityInAndStatus(files, status, pageable);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Page<Violation> getViolationsByFilesAndDateRange(List<FileEntity> files, LocalDateTime startDate, LocalDateTime endDate,
|
|
|
|
|
int page, int size, String sortDirection) {
|
2026-04-24 13:11:49 +07:00
|
|
|
Sort sort = Sort.by(sortDirection.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC,
|
|
|
|
|
"createdDate");
|
2026-03-18 17:07:44 +07:00
|
|
|
Pageable pageable = PageRequest.of(page, size, sort);
|
2026-04-24 13:11:49 +07:00
|
|
|
|
|
|
|
|
return violationRepository.findByFileEntityInAndStatusNotAndCreatedDateBetween(files,
|
|
|
|
|
ViolationStatus.NOT_OWNER_FILE.name(), startDate, endDate, pageable);
|
2026-03-18 17:07:44 +07:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 17:55:14 +07:00
|
|
|
public Map<String, Long> getGroupedViolations(List<FileEntity> files, String groupBy, String status,
|
|
|
|
|
LocalDateTime startDate, LocalDateTime endDate,
|
|
|
|
|
String sortDirection) {
|
|
|
|
|
List<Violation> violations;
|
|
|
|
|
|
|
|
|
|
if (status != null && !status.isEmpty() && startDate != null && endDate != null) {
|
2026-04-24 13:24:50 +07:00
|
|
|
violations = violationRepository.findByFileEntityInAndStatusAndCreatedDateBetween(files, status,
|
|
|
|
|
startDate, endDate);
|
2026-03-18 17:55:14 +07:00
|
|
|
} else if (status != null && !status.isEmpty()) {
|
2026-03-18 18:21:33 +07:00
|
|
|
violations = violationRepository.findByFileEntityInAndStatus(files, status);
|
2026-03-18 17:55:14 +07:00
|
|
|
} else if (startDate != null && endDate != null) {
|
2026-04-24 13:24:50 +07:00
|
|
|
violations = violationRepository.findByFileEntityInAndCreatedDateBetweenAndStatusNot(files, startDate,
|
|
|
|
|
endDate, ViolationStatus.NOT_OWNER_FILE.name());
|
2026-03-18 17:55:14 +07:00
|
|
|
} else {
|
2026-04-24 13:24:50 +07:00
|
|
|
violations = violationRepository.findByFileEntityInAndStatusNot(files,
|
|
|
|
|
ViolationStatus.NOT_OWNER_FILE.name());
|
2026-03-18 17:55:14 +07:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 18:21:33 +07:00
|
|
|
Map<String, Long> grouped = violations.stream()
|
2026-03-18 17:55:14 +07:00
|
|
|
.collect(Collectors.groupingBy(
|
|
|
|
|
v -> extractGroupKey(v.getPageUrl(), groupBy),
|
|
|
|
|
Collectors.counting()));
|
2026-03-18 18:21:33 +07:00
|
|
|
|
|
|
|
|
return grouped.entrySet().stream()
|
|
|
|
|
.sorted((e1, e2) -> {
|
|
|
|
|
if (sortDirection.equalsIgnoreCase("desc")) {
|
|
|
|
|
return e2.getValue().compareTo(e1.getValue());
|
|
|
|
|
} else {
|
|
|
|
|
return e1.getValue().compareTo(e2.getValue());
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect(Collectors.toMap(
|
|
|
|
|
Map.Entry::getKey,
|
|
|
|
|
Map.Entry::getValue,
|
|
|
|
|
(e1, e2) -> e1,
|
|
|
|
|
LinkedHashMap::new));
|
2026-03-18 17:07:44 +07:00
|
|
|
}
|
|
|
|
|
|
2026-03-12 00:21:19 +07:00
|
|
|
public ViolationStatisticsResponse getViolationStatistics(Long userId, String fileId,
|
|
|
|
|
LocalDateTime startDate, LocalDateTime endDate) {
|
|
|
|
|
|
|
|
|
|
List<FileEntity> userFiles;
|
|
|
|
|
|
|
|
|
|
if (fileId != null && !fileId.isEmpty()) {
|
|
|
|
|
FileEntity file = fileEntityRepository.findById(fileId)
|
|
|
|
|
.orElseThrow(() -> new IllegalArgumentException("File not found with id: " + fileId));
|
|
|
|
|
|
|
|
|
|
if (!file.getUserId().equals(userId)) {
|
|
|
|
|
throw new IllegalArgumentException("File does not belong to user");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userFiles = List.of(file);
|
|
|
|
|
} else {
|
2026-03-12 02:52:38 +07:00
|
|
|
userFiles = fileEntityService.getAllUserFiles(userId);
|
2026-03-12 00:21:19 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (userFiles.isEmpty()) {
|
|
|
|
|
return ViolationStatisticsResponse.builder()
|
|
|
|
|
.totalViolations(0)
|
|
|
|
|
.newViolations(0)
|
|
|
|
|
.inProgressViolations(0)
|
|
|
|
|
.resolvedViolations(0)
|
|
|
|
|
.build();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<Violation> violations;
|
|
|
|
|
|
|
|
|
|
if (startDate != null && endDate != null) {
|
2026-04-24 13:36:30 +07:00
|
|
|
violations = violationRepository.findByFileEntityInAndCreatedDateBetweenAndStatusNot(userFiles,
|
|
|
|
|
startDate, endDate, ViolationStatus.NOT_OWNER_FILE.name());
|
2026-03-12 00:21:19 +07:00
|
|
|
} else {
|
2026-04-24 13:36:30 +07:00
|
|
|
violations = violationRepository.findByFileEntityInAndStatusNot(userFiles,
|
|
|
|
|
ViolationStatus.NOT_OWNER_FILE.name());
|
2026-03-12 00:21:19 +07:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 14:42:38 +07:00
|
|
|
Set<String> inProgressStatuses = Set.of(
|
|
|
|
|
ViolationStatus.LEGAL_IN_WORK.name(),
|
|
|
|
|
ViolationStatus.COMPLAINT_IN_WORK.name(),
|
|
|
|
|
ViolationStatus.COMPLAINT_AND_LEGAL_IN_WORK.name()
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-12 00:21:19 +07:00
|
|
|
long newViolations = violations.stream()
|
2026-03-26 14:42:38 +07:00
|
|
|
.filter(v -> v.getStatus().equals(ViolationStatus.NEW.name()))
|
2026-03-12 00:21:19 +07:00
|
|
|
.count();
|
|
|
|
|
|
|
|
|
|
long inProgressViolations = violations.stream()
|
2026-03-26 14:42:38 +07:00
|
|
|
.filter(v -> inProgressStatuses.contains(v.getStatus()))
|
2026-03-12 00:21:19 +07:00
|
|
|
.count();
|
|
|
|
|
|
|
|
|
|
long resolvedViolations = violations.stream()
|
2026-03-26 14:42:38 +07:00
|
|
|
.filter(v -> v.getStatus().equals(ViolationStatus.AUTHORIZED_USE.name()))
|
2026-03-12 00:21:19 +07:00
|
|
|
.count();
|
|
|
|
|
|
|
|
|
|
return ViolationStatisticsResponse.builder()
|
|
|
|
|
.totalViolations(violations.size())
|
|
|
|
|
.newViolations(newViolations)
|
|
|
|
|
.inProgressViolations(inProgressViolations)
|
|
|
|
|
.resolvedViolations(resolvedViolations)
|
|
|
|
|
.build();
|
|
|
|
|
}
|
2026-03-13 13:48:03 +07:00
|
|
|
|
|
|
|
|
private String extractGroupKey(String url, String groupBy) {
|
|
|
|
|
try {
|
|
|
|
|
String domain = url.replaceAll("https?://(www\\.)?", "").split("/")[0];
|
|
|
|
|
if ("tld".equalsIgnoreCase(groupBy)) {
|
|
|
|
|
int lastDot = domain.lastIndexOf('.');
|
|
|
|
|
return lastDot > 0 ? domain.substring(lastDot + 1) : domain;
|
|
|
|
|
}
|
|
|
|
|
return domain;
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
return "unknown";
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-10 22:47:43 +07:00
|
|
|
}
|