2026-03-10 22:47:43 +07:00
|
|
|
package ru.soune.nocopy.service.violation;
|
|
|
|
|
|
|
|
|
|
import lombok.AllArgsConstructor;
|
2026-04-28 14:15:29 +07:00
|
|
|
import lombok.extern.slf4j.Slf4j;
|
2026-03-11 15:42:06 +07:00
|
|
|
import org.apache.commons.codec.digest.DigestUtils;
|
2026-04-28 13:30:00 +07:00
|
|
|
import org.springframework.data.domain.*;
|
2026-03-10 22:47:43 +07:00
|
|
|
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;
|
2026-04-28 13:30:00 +07:00
|
|
|
import ru.soune.nocopy.entity.file.FileStatus;
|
2026-03-10 22:47:43 +07:00
|
|
|
import ru.soune.nocopy.entity.search.GlobalSearchResult;
|
|
|
|
|
import ru.soune.nocopy.entity.violation.Violation;
|
2026-04-28 13:30:00 +07:00
|
|
|
import ru.soune.nocopy.repository.ComplaintEntityRepository;
|
2026-03-12 00:21:19 +07:00
|
|
|
import ru.soune.nocopy.repository.FileEntityRepository;
|
2026-04-28 13:30:00 +07:00
|
|
|
import ru.soune.nocopy.repository.LawCaseRepository;
|
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
|
2026-04-28 14:15:29 +07:00
|
|
|
@Slf4j
|
2026-03-10 22:47:43 +07:00
|
|
|
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-04-28 13:30:00 +07:00
|
|
|
private final ComplaintEntityRepository complaintRepository;
|
|
|
|
|
|
|
|
|
|
private final LawCaseRepository lawCaseRepository;
|
|
|
|
|
|
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-04-28 09:06:38 +07:00
|
|
|
violation.setCreatedDate(LocalDateTime.now());
|
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-04-28 13:30:00 +07:00
|
|
|
public Page<FileViolationSummaryDTO> getFileViolationsSummary(Long userId, String fileName, String sortDirection, Integer page,
|
|
|
|
|
Integer size, LocalDateTime start, LocalDateTime end) {
|
|
|
|
|
List<FileEntity> userFiles = fileName == null || fileName.isEmpty() ? fileEntityService.getAllUserFiles(userId)
|
|
|
|
|
: fileEntityService.getAllUserFilesWithFileName(userId, fileName,
|
|
|
|
|
List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION));
|
2026-03-13 13:48:03 +07:00
|
|
|
|
2026-04-28 13:30:00 +07:00
|
|
|
Sort sort = Sort.by(sortDirection.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC,
|
|
|
|
|
"createdDate");
|
|
|
|
|
Pageable pageable = PageRequest.of(page, size, sort);
|
|
|
|
|
|
|
|
|
|
Page<Violation> violations = violationRepository.findByFileEntityInAndStatusNotAndCreatedDateBetween(
|
|
|
|
|
userFiles, ViolationStatus.NOT_OWNER_FILE.name(), start, end, pageable);
|
|
|
|
|
|
|
|
|
|
Map<String, List<Violation>> violationsByFileId = violations.getContent().stream()
|
2026-03-13 13:48:03 +07:00
|
|
|
.collect(Collectors.groupingBy(v -> v.getFileEntity().getId()));
|
|
|
|
|
|
2026-04-28 13:30:00 +07:00
|
|
|
List<FileViolationSummaryDTO> dtoList = violationsByFileId.entrySet().stream()
|
|
|
|
|
.map(entry -> {
|
|
|
|
|
List<Violation> fileViolations = entry.getValue();
|
|
|
|
|
FileEntity file = fileViolations.get(0).getFileEntity();
|
2026-03-13 14:14:37 +07:00
|
|
|
|
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();
|
|
|
|
|
})
|
|
|
|
|
.collect(Collectors.toList());
|
2026-04-28 13:30:00 +07:00
|
|
|
|
|
|
|
|
return new PageImpl<>(dtoList, pageable, violations.getTotalElements());
|
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(),
|
2026-04-28 14:15:29 +07:00
|
|
|
ViolationStatus.COMPLAINT_AND_LEGAL_IN_WORK.name());
|
2026-03-26 14:42:38 +07:00
|
|
|
|
2026-04-28 14:03:43 +07:00
|
|
|
List<Long> violationIds = violations.stream().map(Violation::getId).toList();
|
2026-04-28 14:25:31 +07:00
|
|
|
|
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();
|
|
|
|
|
|
2026-04-28 14:15:29 +07:00
|
|
|
Long totalComplaints = 0L;
|
|
|
|
|
Long totalLawCases = 0L;
|
2026-04-28 13:30:00 +07:00
|
|
|
|
2026-04-28 13:53:51 +07:00
|
|
|
if (!violationIds.isEmpty()) {
|
|
|
|
|
totalComplaints = complaintRepository.countComplaintByViolations(violationIds);
|
|
|
|
|
totalLawCases = lawCaseRepository.countLawCaseByViolationIds(violationIds);
|
|
|
|
|
}
|
2026-04-28 14:25:31 +07:00
|
|
|
|
2026-03-12 00:21:19 +07:00
|
|
|
return ViolationStatisticsResponse.builder()
|
|
|
|
|
.totalViolations(violations.size())
|
|
|
|
|
.newViolations(newViolations)
|
|
|
|
|
.inProgressViolations(inProgressViolations)
|
|
|
|
|
.resolvedViolations(resolvedViolations)
|
2026-04-28 14:25:31 +07:00
|
|
|
.totalComplaints(77L)
|
|
|
|
|
.totalLawCases(44L)
|
2026-03-12 00:21:19 +07:00
|
|
|
.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
|
|
|
}
|