Merge branch 'dev' into NCBACK-35
# Conflicts: # README.md # src/main/java/ru/soune/nocopy/controller/ApiController.java # src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java # src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java # src/main/java/ru/soune/nocopy/service/file/FileEntityService.java # src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java # src/main/java/ru/soune/nocopy/service/file/impl/ProtectionFileProviderImpl.java
This commit is contained in:
@@ -10,14 +10,19 @@ import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.soune.nocopy.dto.file.SimilarFileDTO;
|
||||
import ru.soune.nocopy.dto.file.SimilarityFilter;
|
||||
import ru.soune.nocopy.entity.company.Company;
|
||||
import ru.soune.nocopy.entity.file.FileEntity;
|
||||
import ru.soune.nocopy.entity.file.FileStatus;
|
||||
import ru.soune.nocopy.entity.file.ImageHashEntity;
|
||||
import ru.soune.nocopy.repository.AuthTokenRepository;
|
||||
import ru.soune.nocopy.repository.ImageHashRepository;
|
||||
import ru.soune.nocopy.repository.ImageSimilarityRepository;
|
||||
import ru.soune.nocopy.repository.SimilarImageProjection;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
import ru.soune.nocopy.exception.DuplicateImageException;
|
||||
import ru.soune.nocopy.repository.*;
|
||||
import ru.soune.nocopy.util.FileUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -34,48 +39,11 @@ public class FileSimilarityService {
|
||||
|
||||
private final AuthTokenRepository authTokenRepository;
|
||||
|
||||
private final FileEntityRepository fileEntityRepository;
|
||||
|
||||
@Value("${server.baseurl}")
|
||||
private String baseUrl;
|
||||
|
||||
public List<SimilarFileDTO> findSimilarFiles(String fileId) {
|
||||
var imageHashEntity = hashRepository.findById(fileId)
|
||||
.orElseThrow(() -> new RuntimeException("Hash not found"));
|
||||
|
||||
Long hash64Hi = imageHashEntity.getHash64Hi();
|
||||
Long hash64Lo = imageHashEntity.getHash64Lo();
|
||||
|
||||
List<SimilarImageProjection> candidates = repository.findCandidates(fileId);
|
||||
|
||||
return candidates.stream()
|
||||
.map(c -> {
|
||||
Long cHi = c.getHash64Hi();
|
||||
Long cLo = c.getHash64Lo();
|
||||
|
||||
int hamming = fileUtil.hamming64(hash64Hi, hash64Lo, cHi, cLo);
|
||||
|
||||
String level;
|
||||
if (hamming <= 5) {
|
||||
level = "DUPLICATE";
|
||||
} else if (hamming <= 12) {
|
||||
level = "SIMILAR";
|
||||
} else {
|
||||
level = "DIFFERENT";
|
||||
}
|
||||
|
||||
return SimilarFileDTO.builder()
|
||||
.fileId(c.getId())
|
||||
.ownerId(c.getUserId())
|
||||
.originalFileName(c.getOriginalFileName())
|
||||
.fileSize(c.getFileSize())
|
||||
.hammingDistance(hamming)
|
||||
.similarityLevel(level)
|
||||
.build();
|
||||
})
|
||||
.sorted((a, b) ->
|
||||
Integer.compare(a.getHammingDistance(), b.getHammingDistance()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<SimilarFileDTO> findDuplicateByHammingDistance(String fileId, int hammingDistance,
|
||||
int duplicate, int similar) {
|
||||
List<SimilarImageProjection> candidates = repository.findCandidates(fileId);
|
||||
@@ -137,40 +105,114 @@ public class FileSimilarityService {
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
public Page<SimilarFileDTO> findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable, String authToken) {
|
||||
var imageHashEntity = hashRepository.findById(fileId)
|
||||
.orElseThrow(() -> new RuntimeException("Hash not found"));
|
||||
public void hasDuplicatesByHash(String path, String mimeType, Long userId) throws Exception {
|
||||
FileEntity duplicateByHash = findDuplicateByHash(path, mimeType, userId);
|
||||
if (duplicateByHash != null) {
|
||||
throw new DuplicateImageException("Duplicate", duplicateByHash.getId(),
|
||||
duplicateByHash.getUserId());
|
||||
}
|
||||
}
|
||||
|
||||
Long hash64Hi = imageHashEntity.getHash64Hi();
|
||||
Long hash64Lo = imageHashEntity.getHash64Lo();
|
||||
public FileEntity findDuplicateByHash(String path, String mimeType, Long userId) throws Exception {
|
||||
String hash = calculateFileHash(path);
|
||||
|
||||
List<SimilarImageProjection> candidates = authToken.equals("all") ? repository.findCandidates(fileId):
|
||||
repository.findCandidatesFromUserFiles(fileId, authTokenRepository.findUserIdByToken(authToken));
|
||||
User user = userRepository.findById(userId).orElseThrow();
|
||||
Company company = user.getCompany();
|
||||
List<Long> userIds = new ArrayList<>();
|
||||
|
||||
List<String> similarityLevels = (filter != null && filter.getSimilarityLevels() != null)
|
||||
? filter.getSimilarityLevels()
|
||||
: List.of("DUPLICATE", "SIMILAR", "DIFFERENT");
|
||||
if (company != null) {
|
||||
userIds.addAll(company.getUsers().stream().map(User::getId).toList());
|
||||
} else {
|
||||
userIds.add(userId);
|
||||
}
|
||||
|
||||
List<SimilarFileDTO> allResults = candidates.stream()
|
||||
.map(c -> createSimilarFileResponse(c, hash64Hi, hash64Lo))
|
||||
.filter(response -> similarityLevels.contains(response.getSimilarityLevel()))
|
||||
.sorted(Comparator.comparingInt(SimilarFileDTO::getHammingDistance))
|
||||
.collect(Collectors.toList());
|
||||
List<FileEntity> fileEntityList = fileEntityRepository.findByMimeTypeAndUserIds(mimeType, userIds);
|
||||
|
||||
int total = allResults.size();
|
||||
int page = (pageable != null) ? pageable.getPageNumber() : 0;
|
||||
int size = (pageable != null) ? pageable.getPageSize() : 20;
|
||||
for (FileEntity file : fileEntityList) {
|
||||
if (file.getProtectedFilePath() == null) continue;
|
||||
File existingFile = new File(file.getProtectedFilePath());
|
||||
File newFile = new File(path);
|
||||
|
||||
int fromIndex = Math.min(page * size, total);
|
||||
int toIndex = Math.min(fromIndex + size, total);
|
||||
if (existingFile.length() == newFile.length()) {
|
||||
if (calculateFileHash(file.getProtectedFilePath()).equals(hash)) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<SimilarFileDTO> pageContent = allResults.subList(fromIndex, toIndex);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PageImpl<>(pageContent, pageable, total);
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public Page<SimilarFileDTO> findSimilarFiles(
|
||||
String fileId,
|
||||
List<String> similarityLevels,
|
||||
Pageable pageable,
|
||||
String authToken) {
|
||||
|
||||
try {
|
||||
var imageHashEntity = hashRepository.findById(fileId)
|
||||
.orElseThrow(() -> new RuntimeException("Hash not found for file: " + fileId));
|
||||
|
||||
|
||||
List<Long> userIds;
|
||||
List<SimilarImageProjection> candidates = repository.findCandidates(fileId);
|
||||
|
||||
Long userId = authTokenRepository.findUserIdByToken(authToken);
|
||||
User user = userRepository.findById(userId).orElseThrow(() -> new RuntimeException("User not found"));
|
||||
Company company = user.getCompany();
|
||||
|
||||
if (company != null) {
|
||||
userIds = company.getUsers().stream().map(User::getId).collect(Collectors.toList());
|
||||
} else {
|
||||
userIds = List.of(userId);
|
||||
}
|
||||
|
||||
List<String> levels = (similarityLevels != null && !similarityLevels.isEmpty())
|
||||
? similarityLevels
|
||||
: List.of("DUPLICATE", "SIMILAR", "DIFFERENT");
|
||||
|
||||
List<SimilarFileDTO> allResults = candidates.stream()
|
||||
.map(c -> createSimilarFileResponse(c,
|
||||
imageHashEntity.getHash64Hi(),
|
||||
imageHashEntity.getHash64Lo(), userIds))
|
||||
.filter(dto -> levels.contains(dto.getSimilarityLevel()))
|
||||
.filter(dto -> !(dto.getFileStatus().equals(FileStatus.MODERATION.name()) && !dto.getOwner()))
|
||||
.sorted(Comparator.comparingInt(SimilarFileDTO::getHammingDistance))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
int start = (int) pageable.getOffset();
|
||||
int end = Math.min((start + pageable.getPageSize()), allResults.size());
|
||||
List<SimilarFileDTO> pageContent = start < allResults.size() ?
|
||||
allResults.subList(start, end) : Collections.emptyList();
|
||||
|
||||
return new PageImpl<>(pageContent, pageable, allResults.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error finding similar files for fileId: {}", fileId, e);
|
||||
return new PageImpl<>(Collections.emptyList(), pageable, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private String calculateFileHash(String path) throws Exception {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
|
||||
try (InputStream is = new FileInputStream(path)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int read;
|
||||
while ((read = is.read(buffer)) > 0) {
|
||||
digest.update(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
|
||||
byte[] hashBytes = digest.digest();
|
||||
return Base64.getEncoder().encodeToString(hashBytes);
|
||||
}
|
||||
|
||||
private SimilarFileDTO createSimilarFileResponse(SimilarImageProjection similarImageProjection,
|
||||
Long hash64Hi, Long hash64Lo) {
|
||||
Long hash64Hi, Long hash64Lo, List<Long> userIds) {
|
||||
Long imageProjectionHash64Hi = similarImageProjection.getHash64Hi();
|
||||
Long similarImageProjectionHash64Lo = similarImageProjection.getHash64Lo();
|
||||
int hamming = PerceptualHashHelper.INSTANCE.hammingDistance(new PHash(hash64Hi, hash64Lo),
|
||||
@@ -185,17 +227,40 @@ public class FileSimilarityService {
|
||||
level = "DIFFERENT";
|
||||
}
|
||||
|
||||
boolean owner = userIds.contains(similarImageProjection.getUserId());
|
||||
|
||||
|
||||
return SimilarFileDTO.builder()
|
||||
.fileId(similarImageProjection.getId())
|
||||
.ownerId(similarImageProjection.getUserId())
|
||||
.originalFileName(similarImageProjection.getOriginalFileName())
|
||||
.originalFileName(similarImageProjection.getOriginalFileName().replace("." +
|
||||
similarImageProjection.getExtension(), "") + "_nocopy_protected" +
|
||||
"." + similarImageProjection.getExtension())
|
||||
.fileSize(similarImageProjection.getFileSize())
|
||||
.hammingDistance(hamming)
|
||||
.similarityLevel(level)
|
||||
.supportId(similarImageProjection.getSupportId())
|
||||
.uploadDate(similarImageProjection.getUploadDate())
|
||||
.status(similarImageProjection.getProtectionStatus())
|
||||
.url(baseUrl + "/api/files/protected/" + similarImageProjection.getId())
|
||||
.owner(owner)
|
||||
// .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId())
|
||||
.url(baseUrl + "/api/files/protected/" + similarImageProjection.getId() + "/thumbnail")
|
||||
.fileStatus(similarImageProjection.getFileStatus())
|
||||
.build();
|
||||
}
|
||||
|
||||
public SimilarFileDTO buildDTO(FileEntity fileEntity) {
|
||||
return SimilarFileDTO.builder()
|
||||
.fileId(fileEntity.getId())
|
||||
.ownerId(fileEntity.getUserId())
|
||||
.originalFileName(fileEntity.getOriginalFileName().replace("." +
|
||||
fileEntity.getFileExtension(), "") + "_nocopy_protected" +
|
||||
"." + fileEntity.getFileExtension())
|
||||
.fileSize(fileEntity.getFileSize())
|
||||
.uploadDate(fileEntity.getCreatedAt())
|
||||
.fileStatus(fileEntity.getStatus().name())
|
||||
.status(fileEntity.getProtectionStatus())
|
||||
.supportId(Long.valueOf(fileEntity.getSupportId()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package ru.soune.nocopy.service.auth;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
import ru.soune.nocopy.entity.user.UserNotActive;
|
||||
import ru.soune.nocopy.repository.UserNotActiveRepository;
|
||||
import ru.soune.nocopy.repository.UserRepository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CleanUpNotActiveUser {
|
||||
|
||||
@Autowired
|
||||
private UserNotActiveRepository userNotActiveRepository;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Transactional
|
||||
@Scheduled(fixedDelay = 600000)
|
||||
public void cleanupNotActiveUsers() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
List<UserNotActive> expiredTokens = userNotActiveRepository.findByBlockedUntilBefore(now);
|
||||
|
||||
for (UserNotActive userNotActive : expiredTokens) {
|
||||
User user = userNotActive.getUser();
|
||||
user.setActive(true);
|
||||
user.setFailedLoginAttempts(0);
|
||||
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
if (!expiredTokens.isEmpty()) {
|
||||
userNotActiveRepository.deleteAll(expiredTokens);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
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;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.nocopy.dto.complaint.ComplaintRequest;
|
||||
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.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.violation.ViolationService;
|
||||
import ru.soune.nocopy.service.violation.ViolationStatus;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class ComplaintEntityService {
|
||||
private final ComplaintEntityRepository complaintRepository;
|
||||
|
||||
private final ViolationRepository violationRepository;
|
||||
|
||||
private final ViolationService violationService;
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
public ComplaintResponse createComplaint(ComplaintRequest request) {
|
||||
Violation violation = violationRepository.findById(request.getViolationId())
|
||||
.orElseThrow(() -> new ViolationNotFoundException("Violation not found: " + request.getViolationId()));
|
||||
|
||||
if (complaintRepository.existsByViolationId(request.getViolationId())) {
|
||||
throw new DuplicateComplaintException("Complaint already exists for violation: " + request.getViolationId());
|
||||
}
|
||||
|
||||
ComplaintEntity complaint = ComplaintEntity.builder()
|
||||
.status(ComplaintStatus.CREATED)
|
||||
.complaintText(request.getComplaintText())
|
||||
.email(request.getEmail())
|
||||
.violation(violation)
|
||||
.not_moderated_file(violation.getFileEntity().getStatus().equals(FileStatus.MODERATION))
|
||||
.build();
|
||||
|
||||
violationService.changeStatus(ViolationStatus.COMPLAINT_IN_WORK, request.getViolationId());
|
||||
|
||||
return mapToResponse(complaintRepository.save(complaint));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ComplaintResponse getComplaintById(Long id) {
|
||||
return complaintRepository.findById(id)
|
||||
.map(this::mapToResponse)
|
||||
.orElseThrow(() -> new ComplaintNotFoundException("Complaint not found: " + id));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<ComplaintResponse> getAllComplaints(Pageable pageable) {
|
||||
return complaintRepository.findAll(pageable).map(this::mapToResponse);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ComplaintResponse> getComplaintsByViolationId(Long violationId) {
|
||||
if (!violationRepository.existsById(violationId)) {
|
||||
throw new ViolationNotFoundException("Violation not found: " + violationId);
|
||||
}
|
||||
return complaintRepository.findByViolationId(violationId)
|
||||
.stream()
|
||||
.map(this::mapToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public ComplaintResponse updateComplaintStatus(Long id, ComplaintStatus status) {
|
||||
ComplaintEntity complaint = complaintRepository.findById(id)
|
||||
.orElseThrow(() -> new ComplaintNotFoundException("Complaint not found: " + id));
|
||||
complaint.setStatus(status);
|
||||
return mapToResponse(complaintRepository.save(complaint));
|
||||
}
|
||||
|
||||
public ComplaintResponse updateComplaint(Long id, ComplaintRequest request) {
|
||||
ComplaintEntity complaint = complaintRepository.findById(id)
|
||||
.orElseThrow(() -> new ComplaintNotFoundException("Complaint not found: " + id));
|
||||
|
||||
if (request.getViolationId() != null) {
|
||||
Violation violation = violationRepository.findById(request.getViolationId())
|
||||
.orElseThrow(() -> new ViolationNotFoundException("Violation not found: " + request.getViolationId()));
|
||||
complaint.setViolation(violation);
|
||||
}
|
||||
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()));
|
||||
|
||||
return mapToResponse(complaintRepository.save(complaint));
|
||||
}
|
||||
|
||||
public ComplaintResponse deleteComplaint(Long id) {
|
||||
ComplaintEntity complaint = complaintRepository.findById(id)
|
||||
.orElseThrow(() -> new ComplaintNotFoundException("Complaint not found: " + id));
|
||||
complaint.setStatus(ComplaintStatus.CANCELLED);
|
||||
return mapToResponse(complaintRepository.save(complaint));
|
||||
}
|
||||
|
||||
private ComplaintResponse mapToResponse(ComplaintEntity entity) {
|
||||
|
||||
return ComplaintResponse.builder()
|
||||
.id(entity.getId())
|
||||
.status(entity.getStatus() != null ? entity.getStatus().name() : null)
|
||||
.complaintText(entity.getComplaintText())
|
||||
.email(entity.getEmail())
|
||||
.violationId(entity.getViolation() != null ? entity.getViolation().getId() : null)
|
||||
.createdAt(entity.getCreatedAt() != null ? entity.getCreatedAt().format(DATE_FORMATTER) : null)
|
||||
.updatedAt(entity.getUpdatedAt() != null ? entity.getUpdatedAt().format(DATE_FORMATTER) : null)
|
||||
.notModerated(entity.getNot_moderated_file() != null ? entity.getNot_moderated_file() : null)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package ru.soune.nocopy.service.cost;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import ru.soune.nocopy.service.tariff.TariffConstants;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CostService {
|
||||
|
||||
public int protectFileCost(String fileType) {
|
||||
int cost = 0;
|
||||
|
||||
switch (fileType) {
|
||||
case "video":
|
||||
return TariffConstants.VIDEO_TOKEN_VALUE;
|
||||
case "image":
|
||||
return TariffConstants.IMAGE_TOKEN_VALUE;
|
||||
case "audio":
|
||||
return TariffConstants.AUDIO_TOKEN_VALUE;
|
||||
case "document":
|
||||
return TariffConstants.PDF_TOKEN_VALUE;
|
||||
default:
|
||||
return cost;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ public class DaDataService {
|
||||
private String apiKey ;
|
||||
|
||||
@Value("${dadata.url}")
|
||||
private String url ;
|
||||
private String dadataUrl;
|
||||
|
||||
private final OkHttpClient httpClient;
|
||||
|
||||
@@ -44,7 +44,7 @@ public class DaDataService {
|
||||
String jsonBody = String.format("{\"query\": \"%s\"}", inn);
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url("https://suggestions.dadata.ru/suggestions/api/4_1/rs/findById/party")
|
||||
.url(dadataUrl)
|
||||
.post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"), jsonBody))
|
||||
.header("Accept", "application/json")
|
||||
.header("Authorization", "Token " + apiKey)
|
||||
@@ -52,6 +52,7 @@ public class DaDataService {
|
||||
|
||||
try (Response response = httpClient.newCall(request).execute()) {
|
||||
ResponseBody body = response.body();
|
||||
|
||||
if (body == null) {
|
||||
throw new IOException("Empty response body");
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ public class FileCleanupService {
|
||||
public void cleanupExpiredFiles() {
|
||||
log.info("Starting cleanup of expired temporary files");
|
||||
|
||||
cleanupOldPeriods();
|
||||
cleanupTempFilesNightly();
|
||||
|
||||
Path tempDir = Paths.get(basePath, "temp");
|
||||
@@ -91,19 +90,6 @@ public class FileCleanupService {
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupOldPeriods() {
|
||||
LocalDate thirtyDaysAgo = LocalDate.now().minusDays(30);
|
||||
|
||||
List<ProtectedFileCheck> checks = protectedFileCheckRepository.findByPeriodStartDateBefore(thirtyDaysAgo);
|
||||
|
||||
for (ProtectedFileCheck check : checks) {
|
||||
check.cleanupOldData();
|
||||
}
|
||||
|
||||
protectedFileCheckRepository.saveAll(checks);
|
||||
log.info("Cleaned up {} old protection usage records", checks.size());
|
||||
}
|
||||
|
||||
private void cleanupTempFilesNightly() {
|
||||
log.info("Starting nightly cleanup of temporary files");
|
||||
|
||||
|
||||
@@ -2,19 +2,20 @@ package ru.soune.nocopy.service.file;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.nocopy.dto.file.FileEntityResponse;
|
||||
import ru.soune.nocopy.dto.file.FileResponse;
|
||||
import ru.soune.nocopy.entity.file.FileEntity;
|
||||
import ru.soune.nocopy.entity.file.FileStatus;
|
||||
import ru.soune.nocopy.entity.file.FileUploadSession;
|
||||
import ru.soune.nocopy.entity.file.ProtectionStatus;
|
||||
import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity;
|
||||
import ru.soune.nocopy.entity.monitoring.MonitoringType;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
import ru.soune.nocopy.exception.DuplicateImageException;
|
||||
import ru.soune.nocopy.exception.FileEntityNotFoundException;
|
||||
import ru.soune.nocopy.repository.FileEntityRepository;
|
||||
import ru.soune.nocopy.repository.SimilarImageProjection;
|
||||
import ru.soune.nocopy.repository.FileMonitoringRepository;
|
||||
import ru.soune.nocopy.repository.UserRepository;
|
||||
import ru.soune.nocopy.service.FileSimilarityService;
|
||||
import ru.soune.nocopy.service.ImageHashService;
|
||||
@@ -27,7 +28,7 @@ import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@@ -37,70 +38,14 @@ public class FileEntityService {
|
||||
|
||||
private final FileEntityRepository fileEntityRepository;
|
||||
|
||||
private final ImageHashService imageHashService;
|
||||
|
||||
private final FileSimilarityService fileSimilarityService;
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
private final ImageResizeService imageResizeService;
|
||||
|
||||
private final CloudStorageService cloudStorageService;
|
||||
|
||||
@Transactional(noRollbackFor = DuplicateImageException.class)
|
||||
public FileEntity createFromUploadSession(FileUploadSession session, String checksum) {
|
||||
log.info("Creating FileEntity for upload session: {}", session.getUploadId());
|
||||
|
||||
try {
|
||||
Path filePath = Paths.get(session.getFilePath());
|
||||
|
||||
if (!Files.exists(filePath)) {
|
||||
throw new IOException("File not found on disk: " + filePath);
|
||||
}
|
||||
|
||||
Map<String, Long> imageHash = Map.of();
|
||||
|
||||
if (!session.getFileType().startsWith("video")) {
|
||||
imageHash = imageHashService.calculateHash(filePath);
|
||||
List<SimilarImageProjection> duplicatedByHash = fileSimilarityService.findDuplicatedByHash(
|
||||
imageHash.get("hi"), imageHash.get("low"));
|
||||
|
||||
if (!duplicatedByHash.isEmpty()) {
|
||||
SimilarImageProjection similarImageProjection = duplicatedByHash.get(0);
|
||||
|
||||
throw new DuplicateImageException("Duplicate", similarImageProjection.getId(),
|
||||
similarImageProjection.getUserId());
|
||||
}
|
||||
}
|
||||
|
||||
long fileSize = Files.size(filePath);
|
||||
String originalName = session.getFileName();
|
||||
String storedName = filePath.getFileName().toString();
|
||||
|
||||
FileEntity fileEntity = FileEntity.builder()
|
||||
.userId(session.getUserId())
|
||||
.originalFileName(originalName)
|
||||
.storedFileName(storedName)
|
||||
.filePath(session.getFilePath())
|
||||
.fileSize(fileSize)
|
||||
.mimeType(session.getFileType())
|
||||
.fileExtension(session.getExtension())
|
||||
.checksum(checksum)
|
||||
.uploadSessionId(session.getUploadId())
|
||||
.status(FileStatus.ACTIVE)
|
||||
.build();
|
||||
|
||||
FileEntity saved = fileEntityRepository.save(fileEntity);
|
||||
|
||||
if (!imageHash.isEmpty()) {
|
||||
imageHashService.create(saved, imageHash);
|
||||
}
|
||||
|
||||
return saved;
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to create FileEntity for session {}: {}",
|
||||
session.getUploadId(), e.getMessage(), e);
|
||||
throw new RuntimeException("Failed to create file entity: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
@Value("${server.baseurl}")
|
||||
private String baseUrl;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public FileEntityResponse getById(String fileId, int version) {
|
||||
@@ -119,6 +64,34 @@ public class FileEntityService {
|
||||
return convertToResponse(fileEntity, version);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public FileEntityResponse getByFilePath(String filePath, int version) {
|
||||
FileEntity fileEntity = fileEntityRepository.findByFilePath(filePath)
|
||||
.orElseThrow(() -> new FileEntityNotFoundException("Path: " + filePath));
|
||||
|
||||
return convertToResponse(fileEntity, version);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<FileEntity> getAllUserFiles(Long userId) {
|
||||
User user = userRepository.findById(userId).orElseThrow();
|
||||
List<FileEntity> allFiles = new ArrayList<>();
|
||||
|
||||
if (user.getCompany() != null) {
|
||||
for (Long uId : userRepository.findByCompanyId(user.getCompany().getId()).stream()
|
||||
.map(User::getId)
|
||||
.toList()) {
|
||||
allFiles.addAll(fileEntityRepository.findByUserIdAndStatusIn(uId,
|
||||
List.of(FileStatus.ACTIVE, FileStatus.BLOCKED)));
|
||||
}
|
||||
} else {
|
||||
allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId,
|
||||
List.of(FileStatus.ACTIVE, FileStatus.BLOCKED));
|
||||
}
|
||||
|
||||
return allFiles;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public FileResponse getUserFiles(Long userId, int page, int pageSize, int version) {
|
||||
User user = userRepository.findById(userId).orElseThrow();
|
||||
@@ -128,12 +101,13 @@ public class FileEntityService {
|
||||
for (Long uId : userRepository.findByCompanyId(user.getCompany().getId()).stream()
|
||||
.map(User::getId)
|
||||
.toList()) {
|
||||
allFiles.addAll(fileEntityRepository.findByUserIdAndStatus(uId, FileStatus.ACTIVE));
|
||||
allFiles.addAll(fileEntityRepository.findByUserIdAndStatusIn(uId,
|
||||
List.of(FileStatus.ACTIVE, FileStatus.BLOCKED)));
|
||||
}
|
||||
|
||||
} else {
|
||||
allFiles = fileEntityRepository.findByUserIdAndStatus(
|
||||
userId, FileStatus.ACTIVE);
|
||||
allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId,
|
||||
List.of(FileStatus.ACTIVE, FileStatus.BLOCKED));
|
||||
}
|
||||
|
||||
int start = (page - 1) * pageSize;
|
||||
@@ -175,13 +149,13 @@ public class FileEntityService {
|
||||
fileEntity.setImageHash(null);
|
||||
}
|
||||
|
||||
fileEntity.setStatus(FileStatus.DELETED);
|
||||
fileEntity.setStatus(FileStatus.REMOVED);
|
||||
fileEntity.setUpdatedAt(LocalDateTime.now());
|
||||
fileEntity.setProtectionStatus(ProtectionStatus.NOT_PROTECTED);
|
||||
// fileEntity.setProtectionStatus(ProtectionStatus.NOT_PROTECTED);
|
||||
|
||||
Path path = Paths.get(fileEntity.getProtectedFilePath());
|
||||
Files.deleteIfExists(path);
|
||||
fileEntity.setProtectedFilePath("");
|
||||
// Path path = Paths.get(fileEntity.getProtectedFilePath());
|
||||
// Files.deleteIfExists(path);
|
||||
// fileEntity.setProtectedFilePath("");
|
||||
|
||||
fileEntityRepository.save(fileEntity);
|
||||
}
|
||||
@@ -196,7 +170,7 @@ public class FileEntityService {
|
||||
return totalSize != null ? totalSize : 0L;
|
||||
}
|
||||
|
||||
public void changeStatus(ProtectionStatus newStatus, String fileId) {
|
||||
public void changeProtectionStatus(ProtectionStatus newStatus, String fileId) {
|
||||
FileEntity fileEntity = fileEntityRepository.findById(fileId)
|
||||
.orElseThrow(() -> new FileEntityNotFoundException(fileId));
|
||||
fileEntity.setProtectionStatus(newStatus);
|
||||
@@ -204,6 +178,14 @@ public class FileEntityService {
|
||||
fileEntityRepository.save(fileEntity);
|
||||
}
|
||||
|
||||
public void changeFileStatus(FileStatus newStatus, String fileId) {
|
||||
FileEntity fileEntity = fileEntityRepository.findById(fileId)
|
||||
.orElseThrow(() -> new FileEntityNotFoundException(fileId));
|
||||
fileEntity.setStatus(newStatus);
|
||||
|
||||
fileEntityRepository.save(fileEntity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public FileEntity writeProtectedFile(String id, byte[] data, String fileExt) throws IOException {
|
||||
FileEntity fileEntity = fileEntityRepository.findById(id)
|
||||
@@ -218,6 +200,10 @@ public class FileEntityService {
|
||||
|
||||
Files.write(protectedFilePath, data);
|
||||
|
||||
if (imageResizeService.isImage(extension)) {
|
||||
imageResizeService.generateSizes(fileEntity, data);
|
||||
}
|
||||
|
||||
fileEntity.setProtectedFilePath(protectedFilePath.toString());
|
||||
fileEntity.setProtectedAt(LocalDateTime.now());
|
||||
fileEntity.setUpdatedAt(LocalDateTime.now());
|
||||
@@ -308,14 +294,29 @@ public class FileEntityService {
|
||||
}
|
||||
}
|
||||
|
||||
private final FileMonitoringRepository fileMonitoringRepository;
|
||||
|
||||
private FileEntityResponse convertToResponse(FileEntity fileEntity, int version) {
|
||||
boolean existsOnDisk = checkFileExistsOnDisk(fileEntity.getFilePath());
|
||||
User user = userRepository.findById(fileEntity.getUserId()).get();
|
||||
String fileName = fileEntity.getOriginalFileName();
|
||||
String name = fileName;
|
||||
Optional<FileMonitoringEntity> fileMonitoringRepositoryByFileId =
|
||||
fileMonitoringRepository.findByFileId(fileEntity.getId());
|
||||
|
||||
String monitoring = fileMonitoringRepositoryByFileId.map(fileMonitoringEntity ->
|
||||
fileMonitoringEntity.getMonitoringType().name())
|
||||
.orElseGet(MonitoringType.NONE::name);
|
||||
|
||||
int lastDotIndex = fileName.lastIndexOf(".");
|
||||
if (lastDotIndex > 0) {
|
||||
name = fileName.substring(0, lastDotIndex);
|
||||
}
|
||||
|
||||
return FileEntityResponse.builder()
|
||||
.id(fileEntity.getId())
|
||||
.userId(fileEntity.getUserId())
|
||||
.originalFileName(fileEntity.getOriginalFileName())
|
||||
.originalFileName(name + "." + fileEntity.getFileExtension())
|
||||
.storedFileName(fileEntity.getStoredFileName())
|
||||
.filePath(fileEntity.getProtectedFilePath())
|
||||
.fileSize(fileEntity.getFileSize())
|
||||
@@ -331,16 +332,16 @@ public class FileEntityService {
|
||||
.existsOnDisk(existsOnDisk)
|
||||
.supportId(fileEntity.getSupportId())
|
||||
.protectStatus(fileEntity.getProtectionStatus().toString())
|
||||
.fileName(fileEntity.getOriginalFileName().replace("." +
|
||||
fileEntity.getFileExtension(), "") + "_nocopy_protected" +
|
||||
"." + fileEntity.getFileExtension())
|
||||
.fileName(name + "_nocopy_protected" + "." + fileEntity.getFileExtension())
|
||||
.ownerName(user.getFullName())
|
||||
.ownerEmail(user.getEmail())
|
||||
.ownerCompany(user.getCompanyName())
|
||||
.monitoring(monitoring)
|
||||
//TODO fix after add logic for check for protect file
|
||||
.checksCount(0)
|
||||
.fileUploadDate(fileEntity.getCreatedAt())
|
||||
.protectedFilePath(fileEntity.getProtectedFilePath())
|
||||
.thumbnailFileUrl(baseUrl + "/api/files/protected/" + fileEntity.getId() + "/thumbnail")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package ru.soune.nocopy.service.file;
|
||||
|
||||
import com.vrt.NoCopyFileService;
|
||||
import com.vrt.fileprotection.FileProtector;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.soune.nocopy.entity.file.FileEntity;
|
||||
import ru.soune.nocopy.entity.file.ProtectionStatus;
|
||||
import ru.soune.nocopy.repository.FileEntityRepository;
|
||||
import ru.soune.nocopy.util.FileUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FileProcessingOrchestrator {
|
||||
|
||||
private final NoCopyFileService noCopyFileService;
|
||||
|
||||
private final FileEntityRepository fileRepository;
|
||||
|
||||
private final FileUtil fileUtil;
|
||||
|
||||
public void initializeProcessingQueue() {
|
||||
List<FileEntity> filesToProtect = fileRepository.findByProtectionStatus(ProtectionStatus.NOT_PROTECTED);
|
||||
|
||||
for (FileEntity fileEntity : filesToProtect) {
|
||||
try {
|
||||
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity);
|
||||
|
||||
noCopyFileService.addFile(fileInfo);
|
||||
|
||||
log.info("Add to query: {}", fileEntity.getOriginalFileName());
|
||||
} catch (Exception e) {
|
||||
log.error("Fail add to query: {}", fileEntity.getId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelay = 120000)
|
||||
public void checkNewFilesForProtection() {
|
||||
List<FileEntity> newFiles = fileRepository.findAllActiveFilesAndNotProtected();
|
||||
|
||||
for (FileEntity fileEntity : newFiles) {
|
||||
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity);
|
||||
|
||||
noCopyFileService.addFile(fileInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
package ru.soune.nocopy.service.file;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.soune.nocopy.configuration.file.FileStorageConfig;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
@@ -16,11 +15,11 @@ import java.nio.file.Paths;
|
||||
@Slf4j
|
||||
public class FileStorageService {
|
||||
|
||||
@Autowired
|
||||
private FileStorageConfig storageConfig;
|
||||
@Value("${file.storage.base-path}")
|
||||
private String basePath;
|
||||
|
||||
public Resource loadFileAsResource(String filePath) throws IOException {
|
||||
Path path = Paths.get(storageConfig.getBasePath()).resolve(filePath).normalize();
|
||||
Path path = Paths.get(basePath).resolve(filePath).normalize();
|
||||
Resource resource = new UrlResource(path.toUri());
|
||||
|
||||
if (!resource.exists()) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import java.io.IOException;
|
||||
|
||||
public interface FileUploadService {
|
||||
FileUploadSession initUpload(Long userId, String fileName,
|
||||
String fileType, String extension, long fileSize);
|
||||
String fileType, String extension, String convertTo, long fileSize);
|
||||
|
||||
void handleExpiredSession(FileUploadSession session);
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package ru.soune.nocopy.service.file;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.nocopy.entity.file.FileEntity;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ImageResizeService {
|
||||
|
||||
private static final int THUMBNAIL_SIZE = 150;
|
||||
private static final int MEDIUM_SIZE = 600;
|
||||
|
||||
@Value("${file.storage.base-path}")
|
||||
private String storagePath;
|
||||
|
||||
public boolean isImage(String extension) {
|
||||
return extension != null && List.of("jpg", "jpeg", "png", "webp")
|
||||
.contains(extension.toLowerCase());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void generateSizes(FileEntity file, byte[] data) throws IOException {
|
||||
if (!isImage(file.getFileExtension())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (ByteArrayInputStream bis = new ByteArrayInputStream(data)) {
|
||||
BufferedImage original = ImageIO.read(bis);
|
||||
|
||||
if (original == null) {
|
||||
log.warn("Cannot read image: {}", file.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
String baseName = file.getId().replace(".", "_");
|
||||
|
||||
if (original.getWidth() > THUMBNAIL_SIZE) {
|
||||
String thumbPath = resizeAndSave(original, baseName + "_thumb", THUMBNAIL_SIZE);
|
||||
file.setThumbnailPath(thumbPath);
|
||||
}
|
||||
|
||||
if (original.getWidth() > MEDIUM_SIZE) {
|
||||
String mediumPath = resizeAndSave(original, baseName + "_medium", MEDIUM_SIZE);
|
||||
file.setMediumPath(mediumPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String resizeAndSave(BufferedImage original, String fileName, int targetWidth) throws IOException {
|
||||
int targetHeight = (int) ((double) original.getHeight() / original.getWidth() * targetWidth);
|
||||
|
||||
BufferedImage resized = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g = resized.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g.drawImage(original, 0, 0, targetWidth, targetHeight, null);
|
||||
g.dispose();
|
||||
|
||||
Path dirPath = Paths.get(storagePath, "resized");
|
||||
Files.createDirectories(dirPath);
|
||||
|
||||
Path filePath = dirPath.resolve(fileName + ".jpg");
|
||||
ImageIO.write(resized, "jpg", filePath.toFile());
|
||||
|
||||
return filePath.toString();
|
||||
}
|
||||
|
||||
public String getPath(FileEntity file, String size) {
|
||||
|
||||
if ("thumbnail".equals(size) && file.getThumbnailPath() != null) {
|
||||
return file.getThumbnailPath();
|
||||
}
|
||||
|
||||
if ("medium".equals(size) && file.getMediumPath() != null) {
|
||||
return file.getMediumPath();
|
||||
}
|
||||
|
||||
return file.getProtectedFilePath();
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,9 @@ public class AudioLocalSearchImpl implements AudioLocalSearch {
|
||||
public @Nullable FileProtector.FileInfo findBySignature(@NotNull String signature) {
|
||||
FileEntity fileEntity = fileEntityService.findBySignature(signature);
|
||||
|
||||
return fileUtil.createFileInfo(fileEntity);
|
||||
if (fileEntity == null) return null;
|
||||
|
||||
return fileUtil.createFileInfo(fileEntity, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,7 +23,7 @@ public class DocumentLocalSearchImpl implements DocumentLocalSearch {
|
||||
@Override
|
||||
public @Nullable FileProtector.FileInfo findByIds(@NotNull String ownerId, @NotNull String fileId) {
|
||||
FileEntity fileEntity = fileEntityRepository.findByIdAndUserId(fileId, Long.valueOf(ownerId));
|
||||
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity);
|
||||
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity, null);
|
||||
log.info("fileInfo: {}", fileInfo);
|
||||
|
||||
return fileInfo;
|
||||
|
||||
@@ -2,27 +2,29 @@ package ru.soune.nocopy.service.file.impl;
|
||||
|
||||
import com.vrt.NoCopyFileService;
|
||||
import com.vrt.fileprotection.FileProtector;
|
||||
import com.vrt.fileprotection.NoCopyCheckResult;
|
||||
import com.vrt.fileprotection.audio.AudioCheckResult;
|
||||
import com.vrt.fileprotection.documents.DocumentCheckResult;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
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.exception.ChunkSizeExceededException;
|
||||
import ru.soune.nocopy.exception.DuplicateImageException;
|
||||
import ru.soune.nocopy.exception.FileUploadException;
|
||||
import ru.soune.nocopy.exception.UploadSessionNotFoundException;
|
||||
import ru.soune.nocopy.exception.*;
|
||||
import ru.soune.nocopy.repository.FileEntityRepository;
|
||||
import ru.soune.nocopy.repository.FileUploadSessionRepository;
|
||||
import ru.soune.nocopy.repository.SimilarImageProjection;
|
||||
import ru.soune.nocopy.service.FileSimilarityService;
|
||||
import ru.soune.nocopy.service.ImageHashService;
|
||||
import ru.soune.nocopy.service.cost.CostService;
|
||||
import ru.soune.nocopy.service.file.FileEntityService;
|
||||
import ru.soune.nocopy.service.file.FileUploadService;
|
||||
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
|
||||
@@ -59,9 +61,6 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
@Value("${file.storage.max-retry-attempts:3}")
|
||||
private int maxRetryAttempts;
|
||||
|
||||
@Value("${file.storage.chunk-timeout-ms:300000}") // 5 минут
|
||||
private long chunkTimeoutMs;
|
||||
|
||||
@Value("${file.storage.session-expiry-hours:24}")
|
||||
private int sessionExpiryHours;
|
||||
|
||||
@@ -79,6 +78,9 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
@Autowired
|
||||
private FileEntityService fileEntityService;
|
||||
|
||||
private final CostService costService;
|
||||
|
||||
@Lazy
|
||||
private final NoCopyFileService noCopyFileService;
|
||||
|
||||
private final FileUtil fileUtil;
|
||||
@@ -112,7 +114,7 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
@Override
|
||||
@Transactional
|
||||
public FileUploadSession initUpload(Long userId, String fileName,
|
||||
String fileType, String extension, long fileSize) {
|
||||
String fileType, String extension, String convertTo, long fileSize) {
|
||||
log.info("Initializing upload for user {}: {} ({} bytes, type: {})",
|
||||
userId, fileName, fileSize, fileType);
|
||||
|
||||
@@ -143,6 +145,7 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
.status(UploadStatus.INITIATED)
|
||||
.expiresAt(LocalDateTime.now().plusHours(sessionExpiryHours))
|
||||
.retryCount(0)
|
||||
.convertTo(convertTo)
|
||||
.build();
|
||||
|
||||
FileUploadSession savedSession = sessionRepository.save(session);
|
||||
@@ -252,6 +255,12 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
|
||||
cleanupSessionFiles(session);
|
||||
|
||||
if (status != FileStatus.TEMP) {
|
||||
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity, session.getConvertTo());
|
||||
|
||||
noCopyFileService.addFile(fileInfo);
|
||||
}
|
||||
|
||||
log.info("File processing completed for session: {}", session.getUploadId());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to complete file processing for session {}: {}",
|
||||
@@ -313,21 +322,58 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
chunkNumber, session.getChunksUploaded(), session.getTotalChunks());
|
||||
|
||||
boolean isLastChunk = session.getChunksUploaded().equals(session.getTotalChunks());
|
||||
|
||||
//TODO CHECK
|
||||
if (isLastChunk) {
|
||||
log.info("All chunks uploaded for session {}. Starting assembly...",
|
||||
session.getUploadId());
|
||||
|
||||
String finalFilePath = assembleFileSynchronously(session);
|
||||
|
||||
if (session.getFileType().startsWith("image") && findSimilar == 0) {
|
||||
checkForDuplicatesSynchronously(finalFilePath);
|
||||
if (findSimilar == 0) {
|
||||
if (session.getFileType().startsWith("image")) {
|
||||
checkForDuplicatesSynchronously(finalFilePath);
|
||||
} else if (session.getFileType().startsWith("audio") || session.getFileType().startsWith("document")) {
|
||||
fileSimilarityService.hasDuplicatesByHash(finalFilePath ,session.getFileType(),
|
||||
session.getUserId());
|
||||
|
||||
File file = new File(finalFilePath);
|
||||
|
||||
if (session.getFileType().startsWith("audio") && !isWavFile(file)) {
|
||||
session.setStatus(UploadStatus.FAILED);
|
||||
session.setLastError("wav file not have RIFF header");
|
||||
|
||||
sessionRepository.save(session);
|
||||
|
||||
throw new FileFormatException("Failed to upload chunk: .wav file not have RIFF header or " +
|
||||
"another problems with .wav file");
|
||||
}
|
||||
|
||||
FileProtector.Type type = "document".equals(session.getFileType()) ?
|
||||
FileProtector.Type.DOC : FileProtector.Type.valueOf(session.getFileType().toUpperCase());
|
||||
|
||||
NoCopyCheckResult checkResult = noCopyFileService.checkFile(file, type);
|
||||
|
||||
switch (checkResult) {
|
||||
case DocumentCheckResult.Success success -> {
|
||||
FileProtector.FileInfo info = success.getInfo();
|
||||
throw new DuplicateImageException("Duplicate", info.getId(),
|
||||
Long.valueOf(info.getOwnerId()));
|
||||
}
|
||||
case AudioCheckResult.Success success -> {
|
||||
FileProtector.FileInfo info = success.getInfo();
|
||||
throw new DuplicateImageException("Duplicate", info.getId(),
|
||||
Long.valueOf(info.getOwnerId()));
|
||||
}
|
||||
case DocumentCheckResult.Failed failed ->
|
||||
log.warn("Document check failed: {}", failed.getMessage());
|
||||
case AudioCheckResult.Failed failed ->
|
||||
log.warn("Audio check failed: {}", failed.getMessage());
|
||||
default -> log.warn("Unexpected result type: {}", checkResult.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
session.setStatus(UploadStatus.COMPLETED);
|
||||
session.setFilePath(finalFilePath);
|
||||
|
||||
FileStatus status = findSimilar == 0 ? FileStatus.ACTIVE: FileStatus.TEMP;
|
||||
FileStatus status = findSimilar == 0 ? FileStatus.MODERATION: FileStatus.TEMP;
|
||||
|
||||
if (status == FileStatus.TEMP) {
|
||||
fileEntityService.clearTempFiles(session.getUserId());
|
||||
@@ -351,7 +397,7 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
if (status != FileStatus.TEMP) {
|
||||
String fileType = session.getFileType();
|
||||
|
||||
tariffInfoService.writeOffTokens(session.getUserId(), protectCost(fileType));
|
||||
tariffInfoService.writeOffTokens(session.getUserId(), costService.protectFileCost(fileType));
|
||||
}
|
||||
} else {
|
||||
sessionRepository.save(session);
|
||||
@@ -388,21 +434,87 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
}
|
||||
}
|
||||
|
||||
private int protectCost(String fileType) {
|
||||
int cost = 0;
|
||||
|
||||
switch (fileType) {
|
||||
case "video":
|
||||
return TariffConstants.VIDEO_TOKEN_VALUE;
|
||||
case "image":
|
||||
return TariffConstants.IMAGE_TOKEN_VALUE;
|
||||
case "audio":
|
||||
return TariffConstants.AUDIO_TOKEN_VALUE;
|
||||
case "document":
|
||||
return TariffConstants.PDF_TOKEN_VALUE;
|
||||
//TODO убрать когда научимся защищать другие типы
|
||||
public boolean isWavFile(File file) {
|
||||
if (file == null || !file.exists() || file.length() < 12) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return cost;
|
||||
try (FileInputStream fis = new FileInputStream(file)) {
|
||||
byte[] header = new byte[4];
|
||||
int bytesRead = fis.read(header);
|
||||
|
||||
if (bytesRead < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return header[0] == 82 && header[1] == 73 &&
|
||||
header[2] == 70 && header[3] == 70;
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isValidWav(File file) {
|
||||
if (file == null || !file.exists() || file.length() < 44) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try (FileInputStream fis = new FileInputStream(file)) {
|
||||
byte[] header = new byte[44];
|
||||
if (fis.read(header) < 44) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header[0] != 'R' || header[1] != 'I' ||
|
||||
header[2] != 'F' || header[3] != 'F') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header[8] != 'W' || header[9] != 'A' ||
|
||||
header[10] != 'V' || header[11] != 'E') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header[12] != 'f' || header[13] != 'm' ||
|
||||
header[14] != 't' || header[15] != ' ') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header[36] != 'd' || header[37] != 'a' ||
|
||||
header[38] != 't' || header[39] != 'a') {
|
||||
return false;
|
||||
}
|
||||
|
||||
int audioFormat = (header[20] & 0xFF) | (header[21] & 0xFF) << 8;
|
||||
if (audioFormat != 1 && audioFormat != 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int numChannels = (header[22] & 0xFF) | (header[23] & 0xFF) << 8;
|
||||
if (numChannels < 1 || numChannels > 8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int sampleRate = (header[24] & 0xFF) | (header[25] & 0xFF) << 8 |
|
||||
(header[26] & 0xFF) << 16 | (header[27] & 0xFF) << 24;
|
||||
if (sampleRate < 8000 || sampleRate > 192000) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int bitsPerSample = (header[34] & 0xFF) | (header[35] & 0xFF) << 8;
|
||||
if (bitsPerSample < 8 || bitsPerSample > 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String assembleFileSynchronously(FileUploadSession session) throws IOException {
|
||||
@@ -510,76 +622,6 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
}
|
||||
}
|
||||
|
||||
private void assembleFile(FileUploadSession session) throws IOException {
|
||||
log.info("Starting file assembly for session: {} ({})",
|
||||
session.getUploadId(), session.getFileName());
|
||||
|
||||
Path finalFilePath = null;
|
||||
String checksum;
|
||||
|
||||
try {
|
||||
finalFilePath = prepareFinalFile(session);
|
||||
log.info("Final file path: {}", finalFilePath);
|
||||
|
||||
validateAllChunksExist(session);
|
||||
|
||||
mergeChunksToFile(session, finalFilePath);
|
||||
|
||||
validateFinalFile(session, finalFilePath);
|
||||
|
||||
checksum = calculateChecksum(finalFilePath);
|
||||
log.debug("File checksum calculated: {}", checksum);
|
||||
|
||||
session.setFilePath(finalFilePath.toString());
|
||||
session.setChecksum(checksum);
|
||||
session.setStatus(UploadStatus.COMPLETED);
|
||||
session.setCompletedAt(LocalDateTime.now());
|
||||
|
||||
sessionRepository.save(session);
|
||||
log.info("Upload session updated to COMPLETED: {}", session.getUploadId());
|
||||
|
||||
try {
|
||||
FileEntity fileEntity = fileEntityService.createFromUploadSession(session, checksum);
|
||||
|
||||
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity);
|
||||
|
||||
noCopyFileService.addFile(fileInfo);
|
||||
|
||||
log.info("FileEntity successfully created for session: {}",
|
||||
session.getUploadId());
|
||||
} catch (DuplicateImageException e) {
|
||||
throw new DuplicateImageException("Duplicate", e.duplicateFileId(), e.userId());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to create FileEntity for session {} (file uploaded, but metadata not saved): {}",
|
||||
session.getUploadId(), e.getMessage());
|
||||
}
|
||||
|
||||
cleanupSessionFiles(session);
|
||||
|
||||
log.info("File assembly completed successfully: {} -> {} ({} bytes)",
|
||||
session.getFileName(), finalFilePath, session.getFileSize());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("File assembly failed for session {}: {}",
|
||||
session.getUploadId(), e.getMessage(), e);
|
||||
|
||||
if (finalFilePath != null) {
|
||||
try {
|
||||
Files.deleteIfExists(finalFilePath);
|
||||
log.debug("Cleaned up partial file: {}", finalFilePath);
|
||||
} catch (IOException ioException) {
|
||||
log.warn("Failed to cleanup partial file: {}", finalFilePath, ioException);
|
||||
}
|
||||
}
|
||||
|
||||
session.setStatus(UploadStatus.FAILED);
|
||||
session.setLastError(e.getMessage());
|
||||
sessionRepository.save(session);
|
||||
|
||||
throw new IOException("File assembly failed: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private Path prepareFinalFile(FileUploadSession session) throws IOException {
|
||||
Path userUploadsDir = storageRoot.resolve("uploads")
|
||||
.resolve(String.valueOf(session.getUserId()))
|
||||
|
||||
@@ -44,11 +44,13 @@ public class ImageLocalSearchImpl implements ImageLocalSearch {
|
||||
@Override
|
||||
public @Nullable FileProtector.FileInfo findByPHash(@NotNull PHash pHash) {
|
||||
ImageHashEntity imageHashEntity =
|
||||
imageHashRepository.findByHash64HiAndHash64Lo(pHash.getFirstPart(),
|
||||
pHash.getSecondPart());
|
||||
imageHashRepository.findByHash64HiAndHash64Lo(pHash.getFirstPart(), pHash.getSecondPart());
|
||||
|
||||
if (imageHashEntity == null) return null;
|
||||
|
||||
FileEntity file = fileEntityRepository.findByFileId(imageHashEntity.getFileId());
|
||||
|
||||
return new FileProtector.FileInfo(FileProtector.Type.IMAGE, imageHashEntity.getFileId(),
|
||||
String.valueOf(file.getUserId()));
|
||||
String.valueOf(file.getUserId()), null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,21 +16,21 @@ public class NoCopyProcessingListenerImpl implements FileProtector.ProcessingLis
|
||||
|
||||
@Override
|
||||
public void onStartProcessing(FileProtector.FileInfo fileInfo) {
|
||||
fileEntityService.changeStatus(ProtectionStatus.PROCESSING, fileInfo.getId());
|
||||
fileEntityService.changeProtectionStatus(ProtectionStatus.PROCESSING, fileInfo.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProcessingFailed(FileProtector.FileInfo fileInfo) {
|
||||
fileEntityService.changeStatus(ProtectionStatus.FAILED, fileInfo.getId());
|
||||
fileEntityService.changeProtectionStatus(ProtectionStatus.FAILED, fileInfo.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSavingFailed(FileProtector.FileInfo fileInfo) {
|
||||
fileEntityService.changeStatus(ProtectionStatus.FAILED_SAVE, fileInfo.getId());
|
||||
fileEntityService.changeProtectionStatus(ProtectionStatus.FAILED_SAVE, fileInfo.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish(FileProtector.FileInfo fileInfo) {
|
||||
fileEntityService.changeStatus(ProtectionStatus.PROTECTED, fileInfo.getId());;
|
||||
fileEntityService.changeProtectionStatus(ProtectionStatus.PROTECTED, fileInfo.getId());;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@ import org.jetbrains.annotations.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import ru.soune.nocopy.entity.file.FileEntity;
|
||||
import ru.soune.nocopy.repository.FileEntityRepository;
|
||||
import ru.soune.nocopy.repository.FileUploadSessionRepository;
|
||||
import ru.soune.nocopy.entity.file.FileEntity;
|
||||
import ru.soune.nocopy.repository.FileEntityRepository;
|
||||
import ru.soune.nocopy.service.file.FileEntityService;
|
||||
import ru.soune.nocopy.service.file.FileUploadService;
|
||||
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
|
||||
|
||||
import java.io.File;
|
||||
@@ -27,6 +31,10 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
|
||||
|
||||
private final FileEntityService fileEntityService;
|
||||
|
||||
private final FileUploadSessionRepository fileUploadSessionRepository;
|
||||
|
||||
private final FileEntityRepository fileEntityRepository;
|
||||
|
||||
private final CloudStorageService cloudStorageService;
|
||||
|
||||
private final FileEntityRepository fileEntityRepository;
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
package ru.soune.nocopy.service.file.moderation;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.dto.file.*;
|
||||
import ru.soune.nocopy.entity.file.*;
|
||||
import ru.soune.nocopy.entity.file.moderation.ModerationLog;
|
||||
import ru.soune.nocopy.exception.FileEntityNotFoundException;
|
||||
|
||||
import ru.soune.nocopy.exception.InvalidAppealException;
|
||||
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 java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ModerationService {
|
||||
private final FileEntityRepository fileEntityRepository;
|
||||
|
||||
private final FileAppealRepository fileAppealRepository;
|
||||
|
||||
private final ModerationLogRepository moderationLogRepository;
|
||||
|
||||
private final FileEntityService fileEntityService;
|
||||
|
||||
private static final List<FileStatus> MODERATION_NEEDED_STATUSES = Arrays.asList(
|
||||
FileStatus.MODERATION,
|
||||
FileStatus.BLOCKED
|
||||
);
|
||||
|
||||
/**
|
||||
* Модерация файла администратором
|
||||
*/
|
||||
@Transactional
|
||||
public void moderateFile(ModerationActionRequest request, Long moderatorId) {
|
||||
FileEntity file = fileEntityRepository.findById(request.getFileId())
|
||||
.orElseThrow(() -> new FileEntityNotFoundException(request.getFileId()));
|
||||
|
||||
if (!MODERATION_NEEDED_STATUSES.contains(file.getStatus())) {
|
||||
throw new IllegalStateException("File is not in moderation state. Current status: " + file.getStatus());
|
||||
}
|
||||
|
||||
ModerationLog log = ModerationLog.builder()
|
||||
.fileId(file.getId())
|
||||
.moderatorId(moderatorId)
|
||||
.oldStatus(file.getStatus())
|
||||
.newStatus(request.getNewStatus())
|
||||
.reason(request.getComment())
|
||||
.comment(request.getComment())
|
||||
.build();
|
||||
|
||||
moderationLogRepository.save(log);
|
||||
|
||||
file.setStatus(request.getNewStatus());
|
||||
|
||||
fileEntityRepository.save(file);
|
||||
|
||||
if (request.getNewStatus() == FileStatus.BLOCKED ||
|
||||
request.getNewStatus() == FileStatus.VIOLATION) {
|
||||
|
||||
fileAppealRepository.findByFileIdAndStatus(file.getId(), AppealStatus.PENDING)
|
||||
.ifPresent(appeal -> {
|
||||
appeal.setStatus(AppealStatus.REJECTED);
|
||||
appeal.setAdminComment(request.getComment());
|
||||
appeal.setResolvedAt(LocalDateTime.now());
|
||||
fileAppealRepository.save(appeal);
|
||||
});
|
||||
} else if (request.getNewStatus() == FileStatus.REMOVED) {
|
||||
try {
|
||||
fileEntityService.softDeleteFileWithHash(file);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Подача апелляции пользователем
|
||||
*/
|
||||
@Transactional
|
||||
public AppealResponse submitAppeal(AppealRequest request, Long userId) {
|
||||
FileEntity file = fileEntityRepository.findById(request.getFileId())
|
||||
.orElseThrow(() -> new FileEntityNotFoundException(request.getFileId()));
|
||||
|
||||
if (!file.getUserId().equals(userId)) {
|
||||
throw new SecurityException("User is not the owner of this file");
|
||||
}
|
||||
|
||||
if (!canAppeal(file)) {
|
||||
throw new InvalidAppealException("Cannot appeal this file. Current status: " + file.getStatus());
|
||||
}
|
||||
|
||||
if (hasActiveAppeal(file.getId())) {
|
||||
throw new InvalidAppealException("Active appeal already exists for this file");
|
||||
}
|
||||
|
||||
FileAppeal appeal = FileAppeal.builder()
|
||||
.fileId(file.getId())
|
||||
.userId(userId)
|
||||
.appealReason(request.getAppealReason())
|
||||
.additionalInfo(request.getAdditionalInfo())
|
||||
.status(AppealStatus.PENDING)
|
||||
.moderationBeforeStatus(file.getStatus())
|
||||
.build();
|
||||
|
||||
FileAppeal saved = fileAppealRepository.save(appeal);
|
||||
|
||||
return convertToResponse(saved);
|
||||
}
|
||||
|
||||
/**
|
||||
* Рассмотрение апелляции администратором
|
||||
*/
|
||||
@Transactional
|
||||
public void reviewAppeal(String appealId, boolean approve, String comment, Long moderatorId) {
|
||||
FileAppeal appeal = fileAppealRepository.findById(appealId)
|
||||
.orElseThrow(() -> new RuntimeException("Appeal not found: " + appealId));
|
||||
|
||||
if (appeal.getStatus() != AppealStatus.PENDING) {
|
||||
throw new IllegalStateException("Appeal already processed");
|
||||
}
|
||||
|
||||
FileEntity file = fileEntityRepository.findById(appeal.getFileId())
|
||||
.orElseThrow(() -> new FileEntityNotFoundException(appeal.getFileId()));
|
||||
|
||||
appeal.setStatus(approve ? AppealStatus.APPROVED : AppealStatus.REJECTED);
|
||||
appeal.setAdminComment(comment);
|
||||
appeal.setModeratorId(moderatorId);
|
||||
appeal.setResolvedAt(LocalDateTime.now());
|
||||
|
||||
if (approve) {
|
||||
file.setStatus(FileStatus.ACTIVE);
|
||||
}
|
||||
|
||||
ModerationLog log = ModerationLog.builder()
|
||||
.fileId(file.getId())
|
||||
.moderatorId(moderatorId)
|
||||
.oldStatus(file.getStatus())
|
||||
.newStatus(file.getStatus())
|
||||
.comment("Appeal review: " + comment)
|
||||
.build();
|
||||
moderationLogRepository.save(log);
|
||||
|
||||
fileEntityRepository.save(file);
|
||||
|
||||
fileAppealRepository.save(appeal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка, можно ли подать апелляцию на файл
|
||||
*/
|
||||
public boolean canAppeal(FileEntity file) {
|
||||
return file.getStatus() == FileStatus.BLOCKED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка наличия активной апелляции
|
||||
*/
|
||||
public boolean hasActiveAppeal(String fileId) {
|
||||
return fileAppealRepository.existsByFileIdAndStatusIn(
|
||||
fileId,
|
||||
Arrays.asList(AppealStatus.PENDING, AppealStatus.IN_REVIEW)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение информации о файле для модерации
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public FileModerationInfo getFileModerationInfo(String fileId) {
|
||||
FileEntity file = fileEntityRepository.findById(fileId)
|
||||
.orElseThrow(() -> new FileEntityNotFoundException(fileId));
|
||||
|
||||
FileAppeal activeAppeal = fileAppealRepository.findByFileIdAndStatus(
|
||||
fileId, AppealStatus.PENDING).orElse(null);
|
||||
|
||||
return FileModerationInfo.builder()
|
||||
.fileId(file.getId())
|
||||
.fileName(file.getOriginalFileName())
|
||||
.currentStatus(file.getStatus())
|
||||
.uploadDate(file.getCreatedAt())
|
||||
.hasActiveAppeal(activeAppeal != null)
|
||||
.activeAppeal(activeAppeal != null ?
|
||||
AppealInfo.builder()
|
||||
.appealId(activeAppeal.getId())
|
||||
.status(activeAppeal.getStatus().name())
|
||||
.createdAt(activeAppeal.getCreatedAt())
|
||||
.build() : null)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение списка файлов на модерации
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Page<FileEntity> getFilesForModeration(int page, int size) {
|
||||
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
|
||||
return fileEntityRepository.findByStatusIn(
|
||||
Arrays.asList(FileStatus.MODERATION, FileStatus.BLOCKED),
|
||||
pageable
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение апелляций пользователя
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Page<AppealResponse> getUserAppeals(Long userId, int page, int size) {
|
||||
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
|
||||
return fileAppealRepository.findByUserId(userId, pageable)
|
||||
.map(this::convertToResponse);
|
||||
}
|
||||
|
||||
private AppealResponse convertToResponse(FileAppeal appeal) {
|
||||
return AppealResponse.builder()
|
||||
.appealId(appeal.getId())
|
||||
.fileId(appeal.getFileId())
|
||||
.appealReason(appeal.getAppealReason())
|
||||
.additionalInfo(appeal.getAdditionalInfo())
|
||||
.status(appeal.getStatus().name())
|
||||
.adminComment(appeal.getAdminComment())
|
||||
.createdAt(appeal.getCreatedAt())
|
||||
.resolvedAt(appeal.getResolvedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,9 @@ import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -38,22 +41,78 @@ public class EmailService {
|
||||
@Value("${app.email.verification.user}")
|
||||
private String user;
|
||||
|
||||
public void sendVerificationEmail(User user, String code) throws MessagingException, IOException {
|
||||
@Value("${app.email.verification.drop-password-subject}")
|
||||
private String dropPasswordSubject;
|
||||
|
||||
@Value("${server.baseurl}")
|
||||
private String baseUrl;
|
||||
|
||||
public void sendEmail(String from, String sendTo, String subject, String body) throws MessagingException {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||
|
||||
helper.setFrom(standartAdressFrom);
|
||||
helper.setTo(user.getEmail());
|
||||
helper.setSubject(standartSubject);
|
||||
|
||||
String htmlContent = loadAndProcessTemplate(user.getFullName(), code);
|
||||
helper.setText(htmlContent, true);
|
||||
helper.setFrom(from);
|
||||
helper.setTo(sendTo);
|
||||
helper.setSubject(subject);
|
||||
helper.setText(body, true);
|
||||
|
||||
mailSender.send(message);
|
||||
}
|
||||
|
||||
public void sendVerificationEmail(User user, String code) throws MessagingException, IOException {
|
||||
String htmlContent = loadAndProcessTemplate(Map.of("username", user.getFullName(), "code", code),
|
||||
"templates/email-verification.html");
|
||||
sendEmail("noreply@no-copy.ru", user.getEmail(), standartAdressFrom, htmlContent);
|
||||
}
|
||||
|
||||
public void sendTokensNotFoundEmail(User user, Integer currentTokens, Integer missingTokens)
|
||||
throws MessagingException, IOException {
|
||||
String htmlContent = loadAndProcessTemplate(Map.of("username", user.getFullName(),
|
||||
"current_tokens", String.valueOf(currentTokens), "missing_tokens",
|
||||
String.valueOf(missingTokens), "link_to_token_board", baseUrl + "/pages/payment"),
|
||||
"templates/token-not-found.html");
|
||||
|
||||
sendEmail("noreply@no-copy.ru", user.getEmail(), standartAdressFrom, htmlContent);
|
||||
}
|
||||
|
||||
public void sendAutoRenewalFailedEmail(User user, String tariffName, Double price)
|
||||
throws MessagingException, IOException {
|
||||
|
||||
Map<String, String> templateData = new HashMap<>();
|
||||
templateData.put("username", user.getFullName());
|
||||
templateData.put("tariff_name", tariffName);
|
||||
templateData.put("price", String.format("%.2f", price));
|
||||
templateData.put("support_email", "support@no-copy.ru");
|
||||
|
||||
String htmlContent = loadAndProcessTemplate(templateData, "templates/failed-template.html");
|
||||
|
||||
sendEmail("noreply@no-copy.ru", user.getEmail(), standartAdressFrom, htmlContent);
|
||||
}
|
||||
|
||||
public void sendAutoRenewalSuccessEmail(User user, String tariffName, Double price, LocalDateTime nextPaymentDate)
|
||||
throws MessagingException, IOException {
|
||||
|
||||
Map<String, String> templateData = new HashMap<>();
|
||||
templateData.put("username", user.getFullName());
|
||||
templateData.put("tariff_name", tariffName);
|
||||
templateData.put("price", String.format("%.2f", price));
|
||||
templateData.put("renewal_date", LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")));
|
||||
templateData.put("next_payment_date", nextPaymentDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")));
|
||||
|
||||
String htmlContent = loadAndProcessTemplate(templateData, "templates/auto-renewal-success.html");
|
||||
|
||||
sendEmail("noreply@no-copy.ru", user.getEmail(), standartAdressFrom, htmlContent);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public EmailVerificationToken createEmailVerificationToken(AuthToken authToken) {
|
||||
EmailVerificationToken existingToken = emailVerificationTokenRepository
|
||||
.findByUserId(authToken.getUser().getId());
|
||||
|
||||
if (existingToken != null) {
|
||||
emailVerificationTokenRepository.deleteById(existingToken.getId());
|
||||
}
|
||||
|
||||
EmailVerificationToken emailToken = new EmailVerificationToken();
|
||||
emailToken.setUser(authToken.getUser());
|
||||
emailToken.setToken(generateVerificationCode());
|
||||
@@ -69,8 +128,8 @@ public class EmailService {
|
||||
return String.valueOf(code);
|
||||
}
|
||||
|
||||
private String loadAndProcessTemplate(String username, String code) throws IOException {
|
||||
ClassPathResource resource = new ClassPathResource("templates/email-verification.html");
|
||||
private String loadAndProcessTemplate(String username, String code, String templatePath) throws IOException {
|
||||
ClassPathResource resource = new ClassPathResource(templatePath);
|
||||
String template = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
String safeUsername = escapeHtml(username != null ? username : user);
|
||||
String safeCode = escapeHtml(code);
|
||||
@@ -80,6 +139,18 @@ public class EmailService {
|
||||
.replace("{{code}}", safeCode);
|
||||
}
|
||||
|
||||
private String loadAndProcessTemplate(Map<String, String> fieldsForTemplate, String templatePath) throws IOException {
|
||||
ClassPathResource resource = new ClassPathResource(templatePath);
|
||||
String template = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
for (Map.Entry<String, String> entry : fieldsForTemplate.entrySet()) {
|
||||
template = template.replace("{{" + entry.getKey() + "}}", escapeHtml(entry.getValue()));
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
|
||||
private String escapeHtml(String text) {
|
||||
if (text == null) return "";
|
||||
return text
|
||||
@@ -89,4 +160,29 @@ public class EmailService {
|
||||
.replace("\"", """)
|
||||
.replace("'", "'");
|
||||
}
|
||||
|
||||
public void sendResetPasswordEmail(User user, String code) {
|
||||
try {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||
|
||||
helper.setFrom(standartAdressFrom);
|
||||
helper.setTo(user.getEmail());
|
||||
helper.setSubject(dropPasswordSubject);
|
||||
|
||||
String htmlContent = loadAndProcessTemplate(user.getFullName(), code,
|
||||
"templates/reset-password.html");
|
||||
//TODO frontPort убрать после настройки редиректа nginx
|
||||
String resetLink = escapeHtml(baseUrl + "/reset-password?email=" + user.getEmail());
|
||||
htmlContent = htmlContent.replace("{{resetLink}}", resetLink);
|
||||
|
||||
helper.setText(htmlContent, true);
|
||||
|
||||
mailSender.send(message);
|
||||
|
||||
log.info("Email sent to: {}", user.getEmail());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send email: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package ru.soune.nocopy.service.monitoring;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.nocopy.dto.monitoring.MonitoringStatusResponse;
|
||||
import ru.soune.nocopy.dto.tarriff.TariffDTO;
|
||||
import ru.soune.nocopy.entity.file.*;
|
||||
import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity;
|
||||
import ru.soune.nocopy.entity.monitoring.MonitoringType;
|
||||
import ru.soune.nocopy.entity.tarif.TariffInfo;
|
||||
import ru.soune.nocopy.entity.tarif.TariffType;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
import ru.soune.nocopy.exception.UserNotHavePermission;
|
||||
import ru.soune.nocopy.repository.FileEntityRepository;
|
||||
import ru.soune.nocopy.repository.FileMonitoringRepository;
|
||||
import ru.soune.nocopy.repository.UserRepository;
|
||||
import ru.soune.nocopy.service.tariff.TariffService;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.TemporalAdjusters;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FileMonitoringService {
|
||||
private final FileMonitoringRepository monitoringRepository;
|
||||
|
||||
private final FileEntityRepository fileRepository;
|
||||
|
||||
private final TariffService tariffService;
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Transactional
|
||||
public FileMonitoringEntity setMonitoring(Long userId, String fileId, MonitoringType type) throws FileNotFoundException {
|
||||
FileEntity file = fileRepository.findById(fileId)
|
||||
.orElseThrow(() -> new FileNotFoundException(fileId));
|
||||
|
||||
if (!file.getUserId().equals(userId)) {
|
||||
throw new UserNotHavePermission("User not Have permission for file");
|
||||
}
|
||||
|
||||
FileMonitoringEntity monitoring = monitoringRepository.findByFileId(fileId)
|
||||
.orElse(FileMonitoringEntity.builder()
|
||||
.file(file)
|
||||
.userId(userId)
|
||||
.build());
|
||||
|
||||
monitoring.setMonitoringType(type);
|
||||
monitoring.setActive(type != MonitoringType.NONE);
|
||||
monitoring.setNextRun(calculateNextRun(type, LocalDateTime.now()));
|
||||
monitoring.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
if (monitoring.getCreatedAt() == null) {
|
||||
monitoring.setCreatedAt(LocalDateTime.now());
|
||||
}
|
||||
|
||||
return monitoringRepository.save(monitoring);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateNextRun(FileMonitoringEntity monitoring) {
|
||||
if (monitoring.isActive() && monitoring.getMonitoringType() != MonitoringType.NONE) {
|
||||
LocalDateTime nextRun = calculateNextRun(monitoring.getMonitoringType(), LocalDateTime.now());
|
||||
monitoring.setNextRun(nextRun);
|
||||
monitoringRepository.save(monitoring);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private LocalDateTime calculateNextRun(MonitoringType type, LocalDateTime from) {
|
||||
LocalDateTime nextRun = from.withHour(2).withMinute(0).withSecond(0).withNano(0);
|
||||
|
||||
if (from.isAfter(nextRun)) {
|
||||
nextRun = nextRun.plusDays(1);
|
||||
}
|
||||
|
||||
return switch (type) {
|
||||
case MONITORING_DAILY -> nextRun;
|
||||
case MONITORING_WEEKLY -> nextRun.with(TemporalAdjusters.nextOrSame(java.time.DayOfWeek.MONDAY));
|
||||
case MONITORING_MONTHLY -> nextRun.with(TemporalAdjusters.firstDayOfNextMonth());
|
||||
case NONE -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public MonitoringStatusResponse getMonitoringStatus(String fileId, Long userId) {
|
||||
FileMonitoringEntity monitoring = monitoringRepository.findByFileId(fileId).orElse(null);
|
||||
FileEntity file = fileRepository.findById(fileId).orElseThrow();
|
||||
User user = userRepository.findById(userId).orElseThrow();
|
||||
|
||||
return MonitoringStatusResponse.builder()
|
||||
.fileId(fileId)
|
||||
.fileName(file.getOriginalFileName())
|
||||
.monitoringType(monitoring != null ? monitoring.getMonitoringType() : MonitoringType.NONE)
|
||||
.nextRun(monitoring != null ? monitoring.getNextRun() : null)
|
||||
.lastRun(monitoring != null ? monitoring.getLastRun() : null)
|
||||
.lastRunStatus(monitoring != null ? monitoring.getLastRunStatus() : null)
|
||||
.isActive(monitoring != null && monitoring.isActive())
|
||||
.needAddTokens(checkTokensForMonitoring(user, monitoring.getMonitoringType()))
|
||||
.build();
|
||||
}
|
||||
|
||||
public boolean checkTokensForMonitoring(User user, MonitoringType monitoringType) {
|
||||
if (monitoringType == MonitoringType.NONE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
TariffDTO tariff = tariffService.getTariffByType(TariffType.valueOf(monitoringType.name()));
|
||||
TariffInfo activeTariffInfo = user.getActiveTariffInfo();
|
||||
int currentUserCountTokens = activeTariffInfo.getTokens() + activeTariffInfo.getBoughtTokens();
|
||||
|
||||
return currentUserCountTokens > tariff.getTokens();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package ru.soune.nocopy.service.monitoring;
|
||||
|
||||
import jakarta.mail.MessagingException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.nocopy.configuration.search.SearchProperties;
|
||||
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.tarif.TariffInfo;
|
||||
import ru.soune.nocopy.entity.tarif.TariffType;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
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.search.SearchImageService;
|
||||
import ru.soune.nocopy.service.tariff.TariffInfoService;
|
||||
import ru.soune.nocopy.service.tariff.TariffService;
|
||||
import ru.soune.nocopy.service.violation.ViolationService;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MonitoringSearchService {
|
||||
private final SearchImageService searchImageService;
|
||||
|
||||
private final FileMonitoringRepository monitoringRepository;
|
||||
|
||||
private final TariffInfoService tariffInfoService;
|
||||
|
||||
private final TariffService tariffService;
|
||||
|
||||
private final EmailService emailService;
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
private final ViolationService violationService;
|
||||
|
||||
private final SearchProperties searchProperties;
|
||||
|
||||
@Transactional(noRollbackFor = TariffNotFoundException.class)
|
||||
public void processFileSearch(FileMonitoringEntity monitoring) throws MessagingException, IOException {
|
||||
monitoring.setLastRun(LocalDateTime.now());
|
||||
monitoring.setLastRunStatus("IN_PROGRESS");
|
||||
monitoringRepository.save(monitoring);
|
||||
|
||||
MonitoringType monitoringType = monitoring.getMonitoringType();
|
||||
TariffDTO tariffMonitoring = tariffService.getTariffByType(TariffType.valueOf(monitoringType.name()));
|
||||
|
||||
try {
|
||||
tariffInfoService.writeOffTokens(monitoring.getUserId(), tariffMonitoring.getTokens());
|
||||
|
||||
boolean useYandex = searchProperties.getEngines().getOrDefault("yandex",
|
||||
new SearchProperties.EngineConfig()).isEnabled();
|
||||
boolean useGoogle = searchProperties.getEngines().getOrDefault("google",
|
||||
new SearchProperties.EngineConfig()).isEnabled();
|
||||
|
||||
log.info("Monitoring search settings: useYandex={}, useGoogle={}", useYandex, useGoogle);
|
||||
|
||||
List<YandexSearchResponse.ImageResult> allResults = new ArrayList<>();
|
||||
|
||||
if (useYandex) {
|
||||
try {
|
||||
String yandexResponse = searchImageService.searchReverseByPublicUrl(
|
||||
monitoring.getFile(), "yandex_reverse_image", "visual_matches");
|
||||
|
||||
List<YandexSearchResponse.ImageResult> yandexImages =
|
||||
searchImageService.getAllImagesWithoutPagination(yandexResponse, "visual_matches");
|
||||
|
||||
allResults.addAll(yandexImages);
|
||||
log.info("Yandex search found {} results", yandexImages.size());
|
||||
|
||||
} catch (TimeoutException e) {
|
||||
log.warn("Yandex search timeout for monitoring file {}", monitoring.getFile().getId());
|
||||
} catch (IOException e) {
|
||||
log.error("Yandex search failed for monitoring file {}", monitoring.getFile().getId(), e);
|
||||
}
|
||||
} else {
|
||||
log.info("Yandex search is disabled by settings");
|
||||
}
|
||||
|
||||
if (useGoogle) {
|
||||
try {
|
||||
String googleResponse = searchImageService.searchReverseByPublicUrl(
|
||||
monitoring.getFile(), "google_lens", "exact_matches");
|
||||
|
||||
List<YandexSearchResponse.ImageResult> googleImages =
|
||||
searchImageService.getAllImagesWithoutPagination(googleResponse, "exact_matches");
|
||||
|
||||
allResults.addAll(googleImages);
|
||||
log.info("Google search found {} results", googleImages.size());
|
||||
|
||||
} catch (TimeoutException e) {
|
||||
log.warn("Google search timeout for monitoring file {}", monitoring.getFile().getId());
|
||||
} catch (IOException e) {
|
||||
log.error("Google search failed for monitoring file {}", monitoring.getFile().getId(), e);
|
||||
}
|
||||
} else {
|
||||
log.info("Google search is disabled by settings");
|
||||
}
|
||||
|
||||
List<YandexSearchResponse.ImageResult> uniqueResults = allResults;
|
||||
if (useYandex && useGoogle) {
|
||||
uniqueResults = searchImageService.removeDuplicateUrls(
|
||||
useYandex ? allResults : new ArrayList<>(),
|
||||
useGoogle ? allResults : new ArrayList<>()
|
||||
);
|
||||
log.info("After deduplication: {} unique results", uniqueResults.size());
|
||||
}
|
||||
|
||||
for (YandexSearchResponse.ImageResult imageResult : uniqueResults) {
|
||||
violationService.processViolation(imageResult, monitoring.getFile(), null);
|
||||
}
|
||||
|
||||
monitoring.setLastRunStatus("SUCCESS");
|
||||
|
||||
if (uniqueResults.isEmpty()) {
|
||||
log.info("No results found for monitoring file {}", monitoring.getFile().getId());
|
||||
}
|
||||
|
||||
} catch (TariffNotFoundException e) {
|
||||
User user = userRepository.findById(monitoring.getUserId()).orElseThrow();
|
||||
TariffInfo activeTariffInfo = user.getActiveTariffInfo();
|
||||
int currentTokens = activeTariffInfo.getTokens() + activeTariffInfo.getBoughtTokens();
|
||||
|
||||
emailService.sendTokensNotFoundEmail(user, currentTokens, tariffMonitoring.getTokens());
|
||||
monitoring.setLastRunStatus("ERROR: " + e.getMessage());
|
||||
updateNextRun(monitoring);
|
||||
monitoringRepository.save(monitoring);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing monitoring search", e);
|
||||
monitoring.setLastRunStatus("ERROR: " + e.getMessage());
|
||||
updateNextRun(monitoring);
|
||||
monitoringRepository.save(monitoring);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateNextRun(FileMonitoringEntity monitoring) {
|
||||
LocalDateTime nextRun = calculateNextRun(monitoring.getMonitoringType());
|
||||
monitoring.setNextRun(nextRun);
|
||||
}
|
||||
|
||||
private LocalDateTime calculateNextRun(MonitoringType type) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime nextRun = now.withHour(2).withMinute(0).withSecond(0).withNano(0);
|
||||
|
||||
if (now.isAfter(nextRun)) {
|
||||
nextRun = nextRun.plusDays(1);
|
||||
}
|
||||
|
||||
return switch (type) {
|
||||
case MONITORING_DAILY -> nextRun;
|
||||
case MONITORING_WEEKLY -> nextRun.plusWeeks(1);
|
||||
case MONITORING_MONTHLY -> nextRun.plusMonths(1);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.soune.nocopy.service.notification;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class NotificationService {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package ru.soune.nocopy.service.payment;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.ReferralService;
|
||||
import ru.soune.nocopy.client.YooKassaClient;
|
||||
import ru.soune.nocopy.dto.payment.PaymentMethodDTO;
|
||||
import ru.soune.nocopy.entity.payment.Payment;
|
||||
import ru.soune.nocopy.entity.payment.PaymentMethod;
|
||||
import ru.soune.nocopy.entity.payment.PaymentOperationType;
|
||||
import ru.soune.nocopy.entity.payment.PaymentStatus;
|
||||
import ru.soune.nocopy.entity.tarif.Tariff;
|
||||
import ru.soune.nocopy.entity.tarif.TariffInfo;
|
||||
import ru.soune.nocopy.entity.tarif.TariffStatus;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
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.tariff.TariffInfoService;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class PaymentService {
|
||||
private final PaymentRepository paymentRepository;
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
private final TariffRepository tariffRepository;
|
||||
|
||||
private final TariffInfoRepository tariffInfoRepository;
|
||||
|
||||
private final TariffInfoService tariffInfoService;
|
||||
|
||||
private final ReferralService referralService;
|
||||
|
||||
private final PaymentMethodRepository paymentMethodRepository;
|
||||
|
||||
private final YooKassaClient yooKassaClient;
|
||||
|
||||
@Transactional
|
||||
public Payment createPayment(String email, Long tariffId, String operationType, String paymentUuid) {
|
||||
User user = userRepository.findByEmail(email);
|
||||
|
||||
if (user == null) {
|
||||
throw new UserNotFoundException(email);
|
||||
}
|
||||
|
||||
Tariff tariff = tariffRepository.findById(tariffId).orElseThrow(() ->
|
||||
new TariffNotFoundException("Tariff not found"));
|
||||
|
||||
Payment payment = new Payment();
|
||||
payment.setStatus(PaymentStatus.PENDING);
|
||||
payment.setAmount(tariff.getPrice());
|
||||
payment.setOperationType(PaymentOperationType.valueOf(operationType));
|
||||
payment.setUser(user);
|
||||
payment.setTariff(tariff);
|
||||
payment.setPaymentUuid(paymentUuid);
|
||||
|
||||
return paymentRepository.save(payment);
|
||||
}
|
||||
|
||||
private void savePaymentMethod(User user, Map<String, Object> paymentMethodData) {
|
||||
if (paymentMethodData == null) return;
|
||||
|
||||
String paymentMethodId = (String) paymentMethodData.get("id");
|
||||
if (paymentMethodRepository.findByPaymentMethodId(paymentMethodId).isPresent()) return;
|
||||
|
||||
Map<String, Object> card = (Map<String, Object>) paymentMethodData.get("card");
|
||||
if (card == null) return;
|
||||
|
||||
PaymentMethod pm = new PaymentMethod();
|
||||
pm.setUser(user);
|
||||
pm.setPaymentMethodId(paymentMethodId);
|
||||
pm.setLastFour((String) card.get("last4"));
|
||||
pm.setCardType((String) card.get("card_type"));
|
||||
pm.setExpiryMonth((String) card.get("expiry_month"));
|
||||
pm.setExpiryYear((String) card.get("expiry_year"));
|
||||
pm.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
paymentMethodRepository.save(pm);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PaymentMethodDTO> getUserPaymentMethods(Long userId) {
|
||||
User user = userRepository.findById(userId).orElseThrow();
|
||||
|
||||
return paymentMethodRepository.findByUserAndActiveTrue(user)
|
||||
.stream()
|
||||
.map(pm -> new PaymentMethodDTO(
|
||||
pm.getPaymentMethodId(),
|
||||
pm.getLastFour(),
|
||||
pm.getCardType(),
|
||||
pm.getExpiryMonth(),
|
||||
pm.getExpiryYear()
|
||||
))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deletePaymentMethod(Long userId, String paymentMethodId) {
|
||||
User user = userRepository.findById(userId).orElseThrow();
|
||||
PaymentMethod pm = paymentMethodRepository.findByPaymentMethodId(paymentMethodId)
|
||||
.orElseThrow(() -> new RuntimeException("Payment method not found"));
|
||||
|
||||
if (!pm.getUser().getId().equals(user.getId())) {
|
||||
throw new RuntimeException("Access denied");
|
||||
}
|
||||
|
||||
pm.setActive(false);
|
||||
paymentMethodRepository.save(pm);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void changeAutoRenewal(Long userId, boolean autoRenewal) {
|
||||
User user = userRepository.findById(userId).orElseThrow();
|
||||
|
||||
TariffInfo tariffInfo = user.getActiveTariffInfo();
|
||||
if (tariffInfo != null) {
|
||||
tariffInfo.setAutoRenewal(autoRenewal);
|
||||
tariffInfoRepository.save(tariffInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void handlePaymentNotification(Map<String, Object> notification) {
|
||||
String eventType = (String) notification.get("event");
|
||||
Map<String, Object> object = (Map<String, Object>) notification.get("object");
|
||||
String paymentUuid = (String) object.get("id");
|
||||
|
||||
Payment payment = paymentRepository.findByPaymentUuid(paymentUuid)
|
||||
.orElseThrow(() -> new PaymentNotFoundException("Payment not found"));
|
||||
|
||||
if ("payment.succeeded".equals(eventType)) {
|
||||
Tariff tariff = payment.getTariff();
|
||||
double price = tariff.getPrice();
|
||||
int intPrice = (int) Math.round(price);
|
||||
|
||||
payment.setStatus(PaymentStatus.SUCCEEDED);
|
||||
payment.setCancellationReason(null);
|
||||
|
||||
Map<String, String> metadata = (Map<String, String>) object.get("metadata");
|
||||
Map<String, Object> paymentMethod = (Map<String, Object>) object.get("payment_method");
|
||||
String paymentMethodId = paymentMethod != null ? (String) paymentMethod.get("id") : null;
|
||||
|
||||
boolean autoRenewal = metadata != null && Boolean.parseBoolean(metadata.get("auto_renewal"));
|
||||
|
||||
activateTariffForUser(payment.getUser(), tariff, autoRenewal, paymentMethodId);
|
||||
|
||||
savePaymentMethod(payment.getUser(), paymentMethod);
|
||||
|
||||
referralService.onUserAccountRefill(payment.getUser().getId(), intPrice);
|
||||
} 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);
|
||||
}
|
||||
|
||||
} else if ("payment.waiting_for_capture".equals(eventType)) {
|
||||
payment.setStatus(PaymentStatus.WAITING);
|
||||
payment.setCancellationReason(null);
|
||||
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
paymentRepository.save(payment);
|
||||
}
|
||||
|
||||
public List<Payment> userPayments(String email) {
|
||||
User user = userRepository.findByEmail(email);
|
||||
|
||||
if (user == null) {
|
||||
throw new UserNotFoundException("User not found");
|
||||
}
|
||||
|
||||
return paymentRepository.findByUserId(user.getId());
|
||||
}
|
||||
|
||||
public Payment paymentInfo(String uuid) {
|
||||
return paymentRepository.findByPaymentUuid(uuid).orElseThrow(() ->
|
||||
new PaymentNotFoundException("Payment not found"));
|
||||
}
|
||||
|
||||
private void activateTariffForUser(User user, Tariff tariff, boolean autoRenewal, String paymentMethodId) {
|
||||
String type = tariff.getTariffAccountType();
|
||||
TariffInfo activeTariffInfo = user.getActiveTariffInfo();
|
||||
|
||||
if (type.equals("token")) {
|
||||
Integer tokens = activeTariffInfo.getBoughtTokens();
|
||||
activeTariffInfo.setBoughtTokens(tokens + tariff.getTokens());
|
||||
|
||||
tariffInfoRepository.save(activeTariffInfo);
|
||||
} else {
|
||||
TariffInfo tariffInfo = new TariffInfo();
|
||||
tariffInfo.setStatus(TariffStatus.ACTIVE);
|
||||
tariffInfo.setStartTariff(LocalDateTime.now());
|
||||
tariffInfo.setEndTariff(LocalDateTime.now().plusMonths(1));
|
||||
tariffInfo.setTokens(tariff.getTokens());
|
||||
tariffInfo.setBoughtTokens(activeTariffInfo.getBoughtTokens());
|
||||
tariffInfo.setTariff(tariff);
|
||||
activeTariffInfo.setAutoRenewal(autoRenewal);
|
||||
tariffInfo.setPaymentMethodId(paymentMethodId);
|
||||
|
||||
tariffInfoService.linkUserTariffInfo(user, tariffInfoService.addTariffInfo(tariffInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package ru.soune.nocopy.service.payout;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.nocopy.entity.referral.Referral;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
import ru.soune.nocopy.exception.InsufficientFundsException;
|
||||
import ru.soune.nocopy.repository.ReferralJpaRepository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class BalanceService {
|
||||
|
||||
private final ReferralJpaRepository referralJpaRepository;
|
||||
|
||||
public boolean hasSufficientFunds(User user, BigDecimal amount) {
|
||||
Referral referral = referralJpaRepository.findByUserId(user.getId());
|
||||
BigDecimal available = BigDecimal.valueOf(referral.getAvailableIncome());
|
||||
|
||||
return available.compareTo(amount) >= 0;
|
||||
}
|
||||
|
||||
public BigDecimal getBalance(User user) {
|
||||
Referral referral = referralJpaRepository.findByUserId(user.getId());
|
||||
return BigDecimal.valueOf(referral.getAvailableIncome());
|
||||
}
|
||||
|
||||
public BigDecimal getFrozenBalance(User user) {
|
||||
Referral referral = referralJpaRepository.findByUserId(user.getId());
|
||||
return BigDecimal.valueOf(referral.getHoldBalance());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void freezeAmount(User user, BigDecimal amount) {
|
||||
Referral referral = referralJpaRepository.findByUserId(user.getId());
|
||||
BigDecimal available = BigDecimal.valueOf(referral.getAvailableIncome());
|
||||
|
||||
if (available.compareTo(amount) < 0) {
|
||||
throw new InsufficientFundsException(
|
||||
String.format("Not have amount. Amount have: %.2f", available)
|
||||
);
|
||||
}
|
||||
|
||||
BigDecimal newAvailable = available.subtract(amount);
|
||||
BigDecimal newHold = BigDecimal.valueOf(referral.getHoldBalance()).add(amount);
|
||||
|
||||
referral.setAvailableIncome(newAvailable.intValue());
|
||||
referral.setHoldBalance(newHold.intValue());
|
||||
|
||||
referralJpaRepository.save(referral);
|
||||
|
||||
log.info("Amount freeze. Have: {}, Frozen: {}", newAvailable, newHold);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void unfreezeAmount(Long userId, BigDecimal amount) {
|
||||
log.info("Unfreeze {} for user {}", amount, userId);
|
||||
|
||||
Referral referral = referralJpaRepository.findByUserId(userId);
|
||||
BigDecimal hold = BigDecimal.valueOf(referral.getHoldBalance());
|
||||
|
||||
if (hold.compareTo(amount) < 0) {
|
||||
throw new IllegalStateException(
|
||||
String.format("Not have freeze amount. Frozen: %.2f", hold)
|
||||
);
|
||||
}
|
||||
|
||||
BigDecimal newAvailable = BigDecimal.valueOf(referral.getAvailableIncome()).add(amount);
|
||||
BigDecimal newHold = hold.subtract(amount);
|
||||
|
||||
referral.setAvailableIncome(newAvailable.intValue());
|
||||
referral.setHoldBalance(newHold.intValue());
|
||||
|
||||
referralJpaRepository.save(referral);
|
||||
|
||||
log.info("Amount frozen. Have: {}, Frozen: {}", newAvailable, newHold);
|
||||
}
|
||||
|
||||
public void withdrawAmount(Long userId, BigDecimal amount) {
|
||||
Referral referral = referralJpaRepository.findByUserId(userId);
|
||||
|
||||
if (referral.getHoldBalance() < amount.intValue()) {
|
||||
throw new IllegalStateException("Not have amount");
|
||||
}
|
||||
|
||||
referral.setHoldBalance(referral.getHoldBalance() - amount.intValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package ru.soune.nocopy.service.payout;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.nocopy.dto.payout.AddBankTransferRequest;
|
||||
import ru.soune.nocopy.dto.payout.AddCardRequest;
|
||||
import ru.soune.nocopy.dto.payout.PayoutMethodDTO;
|
||||
import ru.soune.nocopy.entity.payout.*;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
import ru.soune.nocopy.exception.DuplicateMethodException;
|
||||
import ru.soune.nocopy.repository.PayoutMethodRepository;
|
||||
import ru.soune.nocopy.repository.UserRepository;
|
||||
import ru.soune.nocopy.service.user.UserService;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PayoutMethodService {
|
||||
|
||||
private final PayoutMethodRepository payoutMethodRepository;
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Transactional
|
||||
public void initiatedStandartPayoutMethods(User user) {
|
||||
createInternalMethod(user);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InternalPayoutMethod createInternalMethod(User user) {
|
||||
if (payoutMethodRepository.findByUserAndType(user.getId(), InternalPayoutMethod.class).isPresent()) {
|
||||
return (InternalPayoutMethod) payoutMethodRepository.findByUserAndType(
|
||||
user.getId(), InternalPayoutMethod.class).get();
|
||||
}
|
||||
|
||||
InternalPayoutMethod internalMethod = new InternalPayoutMethod();
|
||||
internalMethod.setUser(user);
|
||||
internalMethod.setDefault(true);
|
||||
|
||||
return payoutMethodRepository.save(internalMethod);
|
||||
}
|
||||
|
||||
public List<PayoutMethodDTO> getUserMethods(long userId) {
|
||||
return payoutMethodRepository.findByUserId(userId)
|
||||
.stream()
|
||||
.map(this::convertToDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void setDefaultMethod(Long userId, Long methodId) {
|
||||
payoutMethodRepository.clearOtherDefault(userId, methodId);
|
||||
|
||||
PayoutMethod method = payoutMethodRepository.findByIdAndUserId(methodId, userId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Not found method"));
|
||||
method.setDefault(true);
|
||||
|
||||
payoutMethodRepository.save(method);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public BankTransferPayoutMethod addBankTransferMethod(Long userId, AddBankTransferRequest request) {
|
||||
User user = userRepository.findById(userId).orElseThrow();
|
||||
|
||||
BankTransferPayoutMethod bankMethod = new BankTransferPayoutMethod();
|
||||
bankMethod.setUser(user);
|
||||
bankMethod.setBankName(request.getBankName());
|
||||
bankMethod.setBic(request.getBic());
|
||||
bankMethod.setCorrespondentAccount(request.getCorrespondentAccount());
|
||||
bankMethod.setAccountNumber(request.getAccountNumber());
|
||||
bankMethod.setAccountHolder(request.getAccountHolder());
|
||||
bankMethod.setInn(request.getInn());
|
||||
bankMethod.setKpp(request.getKpp());
|
||||
bankMethod.setDefault(payoutMethodRepository.countByUserId(user.getId()) == 0);
|
||||
|
||||
return payoutMethodRepository.save(bankMethod);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CardPayoutMethod addCardMethod(Long userId, AddCardRequest request) {
|
||||
List<PayoutMethod> byUserId = payoutMethodRepository.findByUserId(userId);
|
||||
|
||||
if (!byUserId.isEmpty()) {
|
||||
for (PayoutMethod payoutMethod : byUserId) {
|
||||
if (payoutMethod.getMethodType().equals(PayoutType.CARD.getDisplayName().toUpperCase())) {
|
||||
payoutMethodRepository.delete(payoutMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String cardHash = generateCardHash(request.getCardNumber());
|
||||
|
||||
if (payoutMethodRepository.existsByCardHash(cardHash)) {
|
||||
throw new DuplicateMethodException("Card was added");
|
||||
}
|
||||
|
||||
User user = userRepository.findById(userId).orElseThrow();
|
||||
|
||||
CardPayoutMethod cardMethod = new CardPayoutMethod();
|
||||
cardMethod.setUser(user);
|
||||
cardMethod.setCardNumber(request.getCardNumber());
|
||||
cardMethod.setCardHash(cardHash);
|
||||
cardMethod.setDefault(payoutMethodRepository.countByUserId(user.getId()) == 0);
|
||||
|
||||
CardPayoutMethod saved = payoutMethodRepository.save(cardMethod);
|
||||
log.info("Added user card: {}", user.getId());
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
public PayoutMethod getDefaultMethod(User user) {
|
||||
return payoutMethodRepository.findByUserIdAndIsDefaultTrue(user.getId())
|
||||
.orElseGet(() -> {
|
||||
List<PayoutMethod> methods = payoutMethodRepository.findByUserId(user.getId());
|
||||
if (methods.isEmpty()) {
|
||||
return createInternalMethod(user);
|
||||
}
|
||||
return methods.get(0);
|
||||
});
|
||||
}
|
||||
|
||||
public PayoutMethodDTO convertToDto(PayoutMethod method) {
|
||||
PayoutMethodDTO payoutMethodDTO = PayoutMethodDTO.builder()
|
||||
.id(method.getId())
|
||||
.type(method.getClass().getSimpleName().replace("PayoutMethod", ""))
|
||||
.displayName(method.getDisplayName())
|
||||
.maskedDetails(method.getMaskedDetails())
|
||||
.isDefault(method.isDefault())
|
||||
.build();
|
||||
|
||||
if (method instanceof CardPayoutMethod cardMethod) {
|
||||
payoutMethodDTO.setCardNumber(cardMethod.getCardNumber());
|
||||
}
|
||||
|
||||
return payoutMethodDTO;
|
||||
}
|
||||
|
||||
private String generateCardHash(String cardNumber) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = md.digest(cardNumber.getBytes());
|
||||
return Base64.getEncoder().encodeToString(hash);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("Failed generate hash", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package ru.soune.nocopy.service.payout;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.ReferralService;
|
||||
import ru.soune.nocopy.dto.payout.PayoutRequestDTO;
|
||||
import ru.soune.nocopy.entity.payout.PayoutMethod;
|
||||
import ru.soune.nocopy.entity.payout.PayoutRequest;
|
||||
import ru.soune.nocopy.entity.payout.PayoutRequestStatus;
|
||||
import ru.soune.nocopy.entity.payout.PayoutType;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
import ru.soune.nocopy.exception.InsufficientFundsException;
|
||||
import ru.soune.nocopy.exception.InvalidPayoutMethodException;
|
||||
import ru.soune.nocopy.exception.PendingPayoutExistsException;
|
||||
import ru.soune.nocopy.repository.PayoutMethodRepository;
|
||||
import ru.soune.nocopy.repository.PayoutRequestRepository;
|
||||
import ru.soune.nocopy.repository.UserRepository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PayoutRequestService {
|
||||
private final PayoutRequestRepository payoutRequestRepository;
|
||||
|
||||
private final PayoutMethodRepository payoutMethodRepository;
|
||||
|
||||
private final BalanceService balanceService;
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
private static final BigDecimal MIN_PAYOUT_AMOUNT = new BigDecimal("1000.00");
|
||||
|
||||
private static final List<PayoutRequestStatus> BLOCKING_STATUSES = Arrays.asList(PayoutRequestStatus.PENDING,
|
||||
PayoutRequestStatus.PROCESSING);
|
||||
|
||||
@Transactional
|
||||
public PayoutRequest createPayoutRequest(Long userId, PayoutRequestDTO request) {
|
||||
log.info("Create request for money {}: sum {}", userId, request.getAmount());
|
||||
|
||||
validatePayoutAmount(request.getAmount());
|
||||
|
||||
User user = userRepository.findById(userId).orElseThrow();
|
||||
|
||||
if (!balanceService.hasSufficientFunds(user, request.getAmount())) {
|
||||
throw new InsufficientFundsException(String.format("Not have money for payout. Have: %.2f",
|
||||
balanceService.getBalance(user)));
|
||||
}
|
||||
|
||||
if (hasActivePayoutRequests(user)) {
|
||||
throw new PendingPayoutExistsException("Have active payout. Wait for end.");
|
||||
}
|
||||
|
||||
PayoutMethod payoutMethod = payoutMethodRepository
|
||||
.findByIdAndUserId(request.getPayoutMethodId(), user.getId())
|
||||
.orElseThrow(() -> new InvalidPayoutMethodException("Not found payout method"));
|
||||
|
||||
PayoutType payoutType = determinePayoutType(payoutMethod);
|
||||
|
||||
PayoutRequest payoutRequest = PayoutRequest.builder()
|
||||
.user(user)
|
||||
.payoutMethod(payoutMethod)
|
||||
.amount(request.getAmount())
|
||||
.status(PayoutRequestStatus.PENDING)
|
||||
.payoutType(payoutType)
|
||||
.build();
|
||||
|
||||
PayoutRequest savedRequest = payoutRequestRepository.save(payoutRequest);
|
||||
|
||||
balanceService.freezeAmount(user, request.getAmount());
|
||||
|
||||
return savedRequest;
|
||||
}
|
||||
|
||||
private void validatePayoutAmount(BigDecimal amount) {
|
||||
if (amount == null) {
|
||||
throw new IllegalArgumentException("Sum is null");
|
||||
}
|
||||
|
||||
if (amount.compareTo(MIN_PAYOUT_AMOUNT) < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Min sum for payout: %.2f rub.", MIN_PAYOUT_AMOUNT)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean hasActivePayoutRequests(User user) {
|
||||
return payoutRequestRepository.existsByUserIdAndStatusIn(user.getId(), BLOCKING_STATUSES);
|
||||
}
|
||||
|
||||
private PayoutType determinePayoutType(PayoutMethod payoutMethod) {
|
||||
String className = payoutMethod.getClass().getSimpleName();
|
||||
|
||||
switch (className) {
|
||||
case "CardPayoutMethod":
|
||||
return PayoutType.CARD;
|
||||
case "BankTransferPayoutMethod":
|
||||
return PayoutType.BANK_TRANSFER;
|
||||
case "InternalPayoutMethod":
|
||||
return PayoutType.INTERNAL;
|
||||
default:
|
||||
throw new InvalidPayoutMethodException("Unknown method");
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PayoutRequest> getUserPayoutRequests(Long userId) {
|
||||
return payoutRequestRepository.findByUserIdOrderByCreatedAtDesc(userId);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PayoutRequest getPayoutRequest(Long userId, Long requestId) {
|
||||
return payoutRequestRepository.findByIdAndUserId(requestId, userId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Payout not found"));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PayoutRequest cancelPayoutRequest(Long userId, Long requestId) {
|
||||
PayoutRequest request = getPayoutRequest(userId, requestId);
|
||||
|
||||
if (request.getStatus() != PayoutRequestStatus.PENDING) {
|
||||
throw new IllegalStateException(
|
||||
"Can't cnacel: " + request.getStatus()
|
||||
);
|
||||
}
|
||||
|
||||
request.setStatus(PayoutRequestStatus.CANCELLED);
|
||||
|
||||
balanceService.unfreezeAmount(userId, request.getAmount());
|
||||
|
||||
return payoutRequestRepository.save(request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PayoutRequest processRequest(Long requestId) {
|
||||
PayoutRequest request = payoutRequestRepository.findById(requestId)
|
||||
.orElseThrow();
|
||||
|
||||
if (request.getStatus() != PayoutRequestStatus.PENDING) {
|
||||
throw new IllegalStateException("Can't proceed in status: " + request.getStatus());
|
||||
}
|
||||
|
||||
request.setStatus(PayoutRequestStatus.COMPLETED);
|
||||
request.setProcessAt(LocalDateTime.now());
|
||||
|
||||
balanceService.withdrawAmount(request.getUser().getId(), request.getAmount());
|
||||
|
||||
return payoutRequestRepository.save(request);
|
||||
}
|
||||
}
|
||||
@@ -99,8 +99,7 @@ public class ReferralRepoImpl implements ReferralRepo {
|
||||
|
||||
@Override
|
||||
public int getAvailableIncome() {
|
||||
Integer holdBalance = entity.getHoldBalance();
|
||||
return entity.getTotalIncome() - (holdBalance != null ? holdBalance : 0);
|
||||
return entity.getAvailableIncome();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -109,6 +108,11 @@ public class ReferralRepoImpl implements ReferralRepo {
|
||||
return holdBalance != null ? holdBalance : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSpentBalance() {
|
||||
return entity.getTotalIncome() - entity.getAvailableIncome() - entity.getHoldBalance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getActive() { return entity.isActive(); }
|
||||
};
|
||||
@@ -151,6 +155,7 @@ public class ReferralRepoImpl implements ReferralRepo {
|
||||
referral.setInviterId(entity.getInviter());
|
||||
referral.setLevelId(entity.getCurrentLevel());
|
||||
referral.setTotalIncome(entity.getTotalIncome());
|
||||
referral.setAvailableIncome(entity.getAvailableIncome());
|
||||
referral.setHoldBalance(entity.getHoldBalance());
|
||||
referral.setActive(entity.getActive());
|
||||
|
||||
|
||||
@@ -11,22 +11,18 @@ import ru.soune.nocopy.dto.register.LoginAnswer;
|
||||
import ru.soune.nocopy.dto.register.LoginRequest;
|
||||
import ru.soune.nocopy.dto.register.RegRequest;
|
||||
import ru.soune.nocopy.entity.company.Company;
|
||||
import ru.soune.nocopy.entity.tarif.Tariff;
|
||||
import ru.soune.nocopy.entity.tarif.TariffInfo;
|
||||
import ru.soune.nocopy.entity.tarif.TariffType;
|
||||
import ru.soune.nocopy.entity.user.AuthToken;
|
||||
import ru.soune.nocopy.entity.user.Permission;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
import ru.soune.nocopy.exception.MaxUserCountIsOver;
|
||||
import ru.soune.nocopy.entity.user.UserNotActive;
|
||||
import ru.soune.nocopy.exception.CompanyAlreadyExist;
|
||||
import ru.soune.nocopy.exception.NotFoundAuthToken;
|
||||
import ru.soune.nocopy.exception.NotValidFieldException;
|
||||
import ru.soune.nocopy.repository.AuthTokenRepository;
|
||||
import ru.soune.nocopy.repository.CompanyRepository;
|
||||
import ru.soune.nocopy.repository.TariffInfoRepository;
|
||||
import ru.soune.nocopy.repository.UserRepository;
|
||||
import ru.soune.nocopy.repository.*;
|
||||
import ru.soune.nocopy.service.file.CheckCounterService;
|
||||
import ru.soune.nocopy.service.tariff.TariffInfoService;
|
||||
import ru.soune.nocopy.service.tariff.TariffService;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -50,21 +46,27 @@ public class AuthService {
|
||||
|
||||
private final TariffInfoService tariffInfoService;
|
||||
|
||||
private final UserNotActiveRepository userNotActiveRepository;
|
||||
|
||||
private final SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
@Transactional
|
||||
public AuthToken register(RegRequest registerRequest) {
|
||||
public AuthToken register(RegRequest registerRequest, boolean verified) {
|
||||
User user = new User();
|
||||
user.setFullName(registerRequest.getFullName());
|
||||
user.setEmail(registerRequest.getEmail());
|
||||
user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
|
||||
user.setActive(true);
|
||||
user.setEmailVerified(true);
|
||||
user.setActive(!verified);
|
||||
user.setEmailVerified(!verified);
|
||||
|
||||
AccountType accountType = AccountType.valueOf(registerRequest.getAccountType().toUpperCase());
|
||||
|
||||
if (accountType == AccountType.B2B && registerRequest.getCompanyName() != null) {
|
||||
createCompanyAccount(registerRequest, user);
|
||||
try {
|
||||
createCompanyAccount(registerRequest, user);
|
||||
} catch (CompanyAlreadyExist exist) {
|
||||
throw new CompanyAlreadyExist("Company already exist");
|
||||
}
|
||||
} else if (accountType == AccountType.B2C && registerRequest.getCompanyName() == null) {
|
||||
createIndividualAccount(user);
|
||||
} else {
|
||||
@@ -104,14 +106,30 @@ public class AuthService {
|
||||
return authToken.getUser().getId();
|
||||
}
|
||||
|
||||
|
||||
@Transactional
|
||||
public AuthToken login(LoginRequest request) {
|
||||
User user = userRepository.findByEmail(request.getEmail());
|
||||
|
||||
if (!user.isActive()) {
|
||||
throw new NotValidFieldException("User not active", new BaseResponse(20003,
|
||||
MessageCode.USER_NOT_ACTIVE.getCode(),
|
||||
MessageCode.USER_NOT_ACTIVE.getDescription(), Map.of("userId", user.getId())));
|
||||
}
|
||||
|
||||
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
|
||||
LoginAnswer loginAnswer = new LoginAnswer();
|
||||
loginAnswer.setFieldErrors(Arrays.asList(Map.of("password", request.getPassword())));
|
||||
loginAnswer.setVerified(user.isEmailVerified());
|
||||
loginAnswer.setActive(user.isActive());
|
||||
|
||||
int attempts = user.getFailedLoginAttempts() + 1;
|
||||
user.setFailedLoginAttempts(attempts);
|
||||
|
||||
if (attempts >= 5) {
|
||||
user.setActive(false);
|
||||
blockUser(user);
|
||||
}
|
||||
|
||||
userRepository.save(user);
|
||||
|
||||
throw new NotValidFieldException("Invalid password", new BaseResponse(20003,
|
||||
MessageCode.AUTH_PASSWORD_NOT_MATCHES.getCode(),
|
||||
@@ -119,6 +137,9 @@ public class AuthService {
|
||||
}
|
||||
|
||||
user.setLastLoginAt(LocalDateTime.now());
|
||||
user.setFailedLoginAttempts(0);
|
||||
user.setActive(true);
|
||||
|
||||
User savedUser = userRepository.save(user);
|
||||
|
||||
AuthToken authToken = new AuthToken();
|
||||
@@ -134,10 +155,15 @@ public class AuthService {
|
||||
.ifPresent(authTokenRepository::delete);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AuthToken generateAuthToken(User user) {
|
||||
return authTokenRepository.save(genereateAuthToken(user));
|
||||
}
|
||||
|
||||
public AuthToken getAuthToken(String token) {
|
||||
return authTokenRepository.findByToken(token).get();
|
||||
}
|
||||
|
||||
private AuthToken genereateAuthToken(User user) {
|
||||
AuthToken authToken = new AuthToken();
|
||||
authToken.setToken(generateAuthToken());
|
||||
@@ -153,8 +179,15 @@ public class AuthService {
|
||||
}
|
||||
|
||||
private void createCompanyAccount(RegRequest request, User user) {
|
||||
Optional<Company> byInn = companyRepository.findByInn(request.getInn());
|
||||
|
||||
if (byInn.isPresent()) {
|
||||
throw new CompanyAlreadyExist("Company was already created");
|
||||
}
|
||||
|
||||
Company company = new Company();
|
||||
company.setCompanyName(request.getCompanyName());
|
||||
company.setInn(request.getInn());
|
||||
|
||||
TariffInfo companyTariff = tariffInfoService.createTariffInfo(TariffType.DEMO);
|
||||
company.setTariffInfo(companyTariff);
|
||||
@@ -192,18 +225,33 @@ public class AuthService {
|
||||
throw new IllegalStateException("Company not have tariff");
|
||||
}
|
||||
|
||||
Tariff tariff = tariffInfo.getTariff();
|
||||
Long maxUsers = tariff.getMaxUsers();
|
||||
long currentCountUsers = userRepository.countByCompanyId(company.getId());
|
||||
|
||||
if (currentCountUsers >= maxUsers) {
|
||||
throw new NotValidFieldException("Max user is over.", new BaseResponse(20002,
|
||||
MessageCode.USER_LIMIT_IS_OVER.getCode(),
|
||||
MessageCode.USER_LIMIT_IS_OVER.getDescription(), Map.of(
|
||||
"limit", maxUsers, "countUser", currentCountUsers)));
|
||||
}
|
||||
// Tariff tariff = tariffInfo.getTariff();
|
||||
// Long maxUsers = tariff.getMaxUsers();
|
||||
// long currentCountUsers = userRepository.countByCompanyId(company.getId());
|
||||
//
|
||||
// if (currentCountUsers >= maxUsers) {
|
||||
// throw new NotValidFieldException("Max user is over.", new BaseResponse(20002,
|
||||
// MessageCode.USER_LIMIT_IS_OVER.getCode(),
|
||||
// MessageCode.USER_LIMIT_IS_OVER.getDescription(), Map.of(
|
||||
// "limit", maxUsers, "countUser", currentCountUsers)));
|
||||
// }
|
||||
|
||||
user.setCompany(company);
|
||||
user.grantPermission(Permission.DEFAULT_USER_PERMISSIONS);
|
||||
}
|
||||
|
||||
public void blockUser(User user) {
|
||||
Optional<UserNotActive> existing = userNotActiveRepository.findByUserId(user.getId());
|
||||
if (existing.isEmpty()) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
UserNotActive notActive = UserNotActive.builder()
|
||||
.user(user)
|
||||
.createdAt(now)
|
||||
.blockedUntil(now.plusMinutes(15))
|
||||
.build();
|
||||
|
||||
userNotActiveRepository.save(notActive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ru.soune.nocopy.service.register;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -12,6 +13,7 @@ import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class CleanUpVerifyTokens {
|
||||
|
||||
private final EmailVerificationTokenRepository emailVerificationTokenRepository;
|
||||
@@ -20,8 +22,7 @@ public class CleanUpVerifyTokens {
|
||||
@Scheduled(fixedDelay = 30000)
|
||||
public void cleanupExpiredTokens() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
|
||||
log.info("Check time for tokens");
|
||||
List<EmailVerificationToken> expiredTokens = emailVerificationTokenRepository.findByExpiresAtBefore(now);
|
||||
|
||||
if (!expiredTokens.isEmpty()) {
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
package ru.soune.nocopy.service.search;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
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.search.GlobalSearchResult;
|
||||
import ru.soune.nocopy.entity.search.GlobalSearchTask;
|
||||
import ru.soune.nocopy.entity.search.SearchStatus;
|
||||
import ru.soune.nocopy.repository.GlobalSearchResultRepository;
|
||||
import ru.soune.nocopy.repository.GlobalSearchTaskRepository;
|
||||
import ru.soune.nocopy.service.tariff.TariffConstants;
|
||||
import ru.soune.nocopy.service.tariff.TariffInfoService;
|
||||
import ru.soune.nocopy.service.violation.ViolationService;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class GlobalSearchAsyncProcessor {
|
||||
|
||||
private final GlobalSearchResultRepository globalSearchResultRepository;
|
||||
|
||||
private final SearchImageService searchImageService;
|
||||
|
||||
private final TariffInfoService tariffInfoService;
|
||||
|
||||
private final GlobalSearchTaskRepository globalSearchTaskRepository;
|
||||
|
||||
private final ViolationService violationService;
|
||||
|
||||
private final SearchProperties searchProperties;
|
||||
|
||||
@Async
|
||||
@Transactional
|
||||
public void processFilesAsync(String taskId, List<FileEntity> filesToProcess, Long userId) {
|
||||
try {
|
||||
for (int i = 0; i < filesToProcess.size(); i++) {
|
||||
FileEntity file = filesToProcess.get(i);
|
||||
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);
|
||||
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
private GlobalSearchResult processFile(FileEntity file, String taskId, Long userId) {
|
||||
GlobalSearchResult result = new GlobalSearchResult();
|
||||
result.setTaskId(taskId);
|
||||
result.setFileId(file.getId());
|
||||
result.setFileName(file.getOriginalFileName());
|
||||
|
||||
result = globalSearchResultRepository.save(result);
|
||||
|
||||
boolean useGoogle = searchProperties.getEngines().getOrDefault("google",
|
||||
new SearchProperties.EngineConfig()).isEnabled();
|
||||
boolean useYandex = searchProperties.getEngines().getOrDefault("yandex",
|
||||
new SearchProperties.EngineConfig()).isEnabled();
|
||||
|
||||
log.info("Global search settings for file {}: useGoogle={}, useYandex={}",
|
||||
file.getId(), useGoogle, useYandex);
|
||||
|
||||
List<YandexSearchResponse.ImageResult> googleImages = new ArrayList<>();
|
||||
List<YandexSearchResponse.ImageResult> yandexImages = new ArrayList<>();
|
||||
List<YandexSearchResponse.ImageResult> allResults = new ArrayList<>();
|
||||
boolean hasTimeout = false;
|
||||
|
||||
if (useGoogle) {
|
||||
try {
|
||||
String searchResponseGoogle = searchImageService.searchReverseByPublicUrl(
|
||||
file, "google_lens", "exact_matches");
|
||||
|
||||
googleImages = searchImageService.getAllImagesWithoutPagination(searchResponseGoogle,
|
||||
"exact_matches");
|
||||
|
||||
allResults.addAll(googleImages);
|
||||
log.info("Google search OK for file {}, found {} results", file.getId(), googleImages.size());
|
||||
|
||||
} catch (TimeoutException e) {
|
||||
log.warn("Google search timeout for file {}", file.getId());
|
||||
hasTimeout = true;
|
||||
} catch (IOException e) {
|
||||
log.error("Google search failed for file {}", file.getId(), e);
|
||||
}
|
||||
} else {
|
||||
log.info("Google search is disabled by settings for file {}", file.getId());
|
||||
}
|
||||
|
||||
if (useYandex) {
|
||||
try {
|
||||
String searchResponseYandex = searchImageService.searchReverseByPublicUrl(
|
||||
file, "yandex_reverse_image", "visual_matches");
|
||||
|
||||
yandexImages = searchImageService.getAllImagesWithoutPagination(searchResponseYandex, "visual_matches");
|
||||
|
||||
allResults.addAll(yandexImages);
|
||||
log.info("Yandex search OK for file {}, found {} results", file.getId(), yandexImages.size());
|
||||
|
||||
} catch (TimeoutException e) {
|
||||
log.warn("Yandex search timeout for file {}", file.getId());
|
||||
hasTimeout = true;
|
||||
} catch (IOException e) {
|
||||
log.error("Yandex search failed for file {}", file.getId(), e);
|
||||
}
|
||||
} else {
|
||||
log.info("Yandex search is disabled by settings for file {}", file.getId());
|
||||
}
|
||||
|
||||
List<YandexSearchResponse.ImageResult> allUniqueImages;
|
||||
if (useGoogle && useYandex) {
|
||||
allUniqueImages = searchImageService.removeDuplicateUrls(
|
||||
yandexImages.stream()
|
||||
.filter(img -> img.getUrl() != null)
|
||||
.collect(Collectors.toList()),
|
||||
googleImages.stream()
|
||||
.filter(img -> img.getUrl() != null)
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
log.info("Merged results from both engines: {} unique images", allUniqueImages.size());
|
||||
} else if (useGoogle) {
|
||||
allUniqueImages = googleImages.stream()
|
||||
.filter(img -> img.getUrl() != null)
|
||||
.collect(Collectors.toList());
|
||||
log.info("Results only from Google: {} images", allUniqueImages.size());
|
||||
} else if (useYandex) {
|
||||
allUniqueImages = yandexImages.stream()
|
||||
.filter(img -> img.getUrl() != null)
|
||||
.collect(Collectors.toList());
|
||||
log.info("Results only from Yandex: {} images", allUniqueImages.size());
|
||||
} else {
|
||||
allUniqueImages = new ArrayList<>();
|
||||
log.warn("All search engines are disabled for file {}", file.getId());
|
||||
}
|
||||
|
||||
for (YandexSearchResponse.ImageResult imageResult : allUniqueImages) {
|
||||
violationService.processViolation(imageResult, file, result);
|
||||
}
|
||||
|
||||
if (allUniqueImages.isEmpty() && hasTimeout) {
|
||||
result.setFileStatus(SearchStatus.TIMEOUT.name());
|
||||
} else if (allUniqueImages.isEmpty() && (!useGoogle && !useYandex)) {
|
||||
result.setFileStatus(SearchStatus.FAILED.name());
|
||||
log.warn("No results for file {} because all engines are disabled", file.getId());
|
||||
} else {
|
||||
result.setFileStatus(SearchStatus.SUCCESS.name());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
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.GlobalSearchStartRequest;
|
||||
import ru.soune.nocopy.entity.file.FileEntity;
|
||||
import ru.soune.nocopy.entity.file.FileType;
|
||||
import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity;
|
||||
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.FileMonitoringRepository;
|
||||
import ru.soune.nocopy.repository.GlobalSearchTaskRepository;
|
||||
import ru.soune.nocopy.service.file.FileEntityService;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GlobalSearchService {
|
||||
|
||||
private final FileEntityRepository fileEntityRepository;
|
||||
|
||||
private final FileMonitoringRepository fileMonitoringRepository;
|
||||
|
||||
private final GlobalSearchTaskRepository globalSearchTaskRepository;
|
||||
|
||||
private final GlobalSearchAsyncProcessor asyncProcessor;
|
||||
|
||||
private final FileEntityService fileEntityService;
|
||||
|
||||
@Transactional
|
||||
public String startSearch(GlobalSearchStartRequest request, Long userId, List<FileEntity> filesToProcess) {
|
||||
GlobalSearchTask task = new GlobalSearchTask();
|
||||
task.setUserId(userId);
|
||||
task.setSearchType(request.getSearchType());
|
||||
task.setStatus(SearchStatus.PROCESSING.name());
|
||||
task.setTotalFiles(filesToProcess.size());
|
||||
task.setProcessedFiles(0);
|
||||
task.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
globalSearchTaskRepository.save(task);
|
||||
|
||||
asyncProcessor.processFilesAsync(task.getTaskId(), filesToProcess, userId);
|
||||
|
||||
return task.getTaskId();
|
||||
}
|
||||
|
||||
public List<FileEntity> getFilesToProcess(GlobalSearchStartRequest request, Long userId) {
|
||||
if ("monitoring".equals(request.getSearchType())) {
|
||||
List<FileMonitoringEntity> monitoringFiles =
|
||||
fileMonitoringRepository.findByUserIdAndIsActiveTrue(userId);
|
||||
|
||||
return monitoringFiles.isEmpty() ? new ArrayList<>(): monitoringFiles.stream()
|
||||
.map(FileMonitoringEntity::getFile)
|
||||
.toList();
|
||||
} else {
|
||||
List<String> fileIds = request.getFileIds().isEmpty() ? fileEntityService.getAllUserFiles(userId)
|
||||
.stream()
|
||||
.filter(f -> f.getMimeType().equals(FileType.IMAGE.getDisplayName()))
|
||||
.map(FileEntity::getId)
|
||||
.toList(): request.getFileIds();
|
||||
|
||||
return fileEntityRepository.findAllById(fileIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
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.GlobalSearchResult;
|
||||
import ru.soune.nocopy.entity.search.GlobalSearchTask;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
import ru.soune.nocopy.repository.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
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 ? List.of(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) {
|
||||
List<GlobalSearchResult> globalSearchResults = resultRepository.findByTaskId(task.getTaskId());
|
||||
List<String> fileIds = globalSearchResults.stream()
|
||||
.map(GlobalSearchResult::getFileId)
|
||||
.toList();
|
||||
|
||||
GlobalSearchStatisticsResponse.SearchStatDto searchStatDto =
|
||||
GlobalSearchStatisticsResponse.SearchStatDto.fromEntity(task,
|
||||
violationRepository.countByFileEntityIdIn(fileIds), fileIds);
|
||||
|
||||
recentSearchDtos.add(searchStatDto);
|
||||
}
|
||||
|
||||
return GlobalSearchStatisticsResponse.builder()
|
||||
.totalSearches(totalSearches)
|
||||
.searchesByStatus(statusCountDto)
|
||||
.recentSearches(recentSearchDtos)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.*;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.soune.nocopy.configuration.search.SearchProperties;
|
||||
import ru.soune.nocopy.dto.BaseResponse;
|
||||
import ru.soune.nocopy.dto.MessageCode;
|
||||
import ru.soune.nocopy.dto.file.YandexSearchResponse;
|
||||
@@ -15,30 +16,23 @@ import ru.soune.nocopy.entity.file.FileEntity;
|
||||
import ru.soune.nocopy.exception.NotValidFieldException;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
|
||||
public class SearchImageService {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private final OkHttpClient httpClient;
|
||||
|
||||
public SearchImageService() {
|
||||
this.httpClient = new OkHttpClient.Builder()
|
||||
.connectTimeout(5, TimeUnit.SECONDS)
|
||||
.writeTimeout(5, TimeUnit.SECONDS)
|
||||
.readTimeout(15, TimeUnit.SECONDS)
|
||||
.callTimeout(20, TimeUnit.SECONDS)
|
||||
.retryOnConnectionFailure(true)
|
||||
.build();
|
||||
private final SearchProperties searchProperties;
|
||||
|
||||
public SearchImageService(SearchProperties searchProperties) {
|
||||
this.searchProperties = searchProperties;
|
||||
this.objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
@@ -95,19 +89,41 @@ public class SearchImageService {
|
||||
return mimeType != null && mimeType.startsWith("image");
|
||||
}
|
||||
|
||||
private String callReverseImageApiByUrl(String imageUrl, String engine, String searchType) throws IOException {
|
||||
private OkHttpClient createHttpClient() {
|
||||
boolean useProxy = searchProperties.getProxy().isEnabled();
|
||||
OkHttpClient.Builder builder = new OkHttpClient.Builder()
|
||||
.connectTimeout(35, TimeUnit.SECONDS)
|
||||
.writeTimeout(35, TimeUnit.SECONDS)
|
||||
.readTimeout(35, TimeUnit.SECONDS)
|
||||
.callTimeout(35, TimeUnit.SECONDS)
|
||||
.followRedirects(true)
|
||||
.followSslRedirects(true)
|
||||
.retryOnConnectionFailure(true);
|
||||
|
||||
if (useProxy && searchProperties.getProxy().isEnabled()) {
|
||||
Proxy proxy = new Proxy(Proxy.Type.HTTP,
|
||||
new InetSocketAddress(searchProperties.getProxy().getHost(),
|
||||
searchProperties.getProxy().getPort()));
|
||||
builder.proxy(proxy);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private String callReverseImageApiByUrl(String imageUrl, String engine, String searchType) throws IOException {
|
||||
if (searchApiKey == null || searchApiKey.isBlank()) {
|
||||
throw new IllegalStateException("SearchAPI key not configured");
|
||||
}
|
||||
|
||||
HttpUrl url = HttpUrl.parse("https://searchapi.io/api/v1/search")
|
||||
OkHttpClient client = createHttpClient();
|
||||
|
||||
HttpUrl url = HttpUrl.parse("https://www.searchapi.io/api/v1/search")
|
||||
.newBuilder()
|
||||
.addQueryParameter("engine", engine)
|
||||
.addQueryParameter("api_key", searchApiKey)
|
||||
.addQueryParameter("url", imageUrl)
|
||||
.addQueryParameter("search_type", searchType)
|
||||
.addQueryParameter("wait", "true")
|
||||
.addQueryParameter("t_", String.valueOf(System.currentTimeMillis()))
|
||||
.build();
|
||||
|
||||
Request request = new Request.Builder()
|
||||
@@ -118,16 +134,17 @@ public class SearchImageService {
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
try (Response response = httpClient.newCall(request).execute()) {
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
ResponseBody body = response.body();
|
||||
long duration = System.currentTimeMillis() - start;
|
||||
log.info("Yandex response code={}, duration={}ms", response.code(), duration);
|
||||
log.info("SearchAPI response code={}, duration={}ms, engine={}",
|
||||
response.code(), duration, engine);
|
||||
|
||||
if (!response.isSuccessful()) {
|
||||
throw new IOException("API error: " + response.code());
|
||||
String errorBody = response.body() != null ? response.body().string() : "null";
|
||||
throw new IOException("API error " + response.code() + ": " + errorBody);
|
||||
}
|
||||
|
||||
ResponseBody body = response.body();
|
||||
if (body == null) {
|
||||
throw new IOException("Empty response body");
|
||||
}
|
||||
@@ -149,8 +166,13 @@ public class SearchImageService {
|
||||
|
||||
if ("exact_matches".equals(findType)) {
|
||||
JsonNode thumbnail = match.path("thumbnail");
|
||||
if (!thumbnail.isMissingNode() && thumbnail.asText().startsWith("data:image")) {
|
||||
result.setUrl(thumbnail.asText());
|
||||
if (!thumbnail.isMissingNode()) {
|
||||
String thumbnailUrl = thumbnail.asText();
|
||||
if (thumbnailUrl.startsWith("data:image")) {
|
||||
result.setUrl(thumbnailUrl);
|
||||
} else if (!thumbnailUrl.isBlank()) {
|
||||
result.setUrl(thumbnailUrl);
|
||||
}
|
||||
}
|
||||
|
||||
JsonNode imageNode = match.path("image");
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package ru.soune.nocopy.service.tariff;
|
||||
|
||||
import jakarta.mail.MessagingException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.nocopy.dto.tarriff.TariffInfoDTO;
|
||||
@@ -13,13 +15,16 @@ import ru.soune.nocopy.exception.TariffNotFoundException;
|
||||
import ru.soune.nocopy.repository.TariffInfoRepository;
|
||||
import ru.soune.nocopy.repository.TariffRepository;
|
||||
import ru.soune.nocopy.repository.UserRepository;
|
||||
import ru.soune.nocopy.service.mail.EmailService;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public class TariffInfoService {
|
||||
|
||||
private TariffInfoRepository tariffInfoRepository;
|
||||
@@ -28,6 +33,8 @@ public class TariffInfoService {
|
||||
|
||||
private UserRepository userRepository;
|
||||
|
||||
private EmailService emailService;
|
||||
|
||||
public TariffInfo createTariffInfo(LocalDateTime startTariff, LocalDateTime endTariff, TariffStatus tariffStatus,
|
||||
Integer tokens) {
|
||||
TariffInfo tariffInfo = new TariffInfo();
|
||||
@@ -39,9 +46,9 @@ public class TariffInfoService {
|
||||
|
||||
return tariffInfoRepository.save(tariffInfo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void writeOffTokens(long userId, Integer value) {
|
||||
|
||||
// @Transactional
|
||||
public void writeOffTokens(long userId, Integer value) throws MessagingException, IOException {
|
||||
User user = userRepository.findById(userId).orElseThrow();
|
||||
|
||||
TariffInfo activeTariffInfo;
|
||||
@@ -52,6 +59,10 @@ public class TariffInfoService {
|
||||
activeTariffInfo = user.getCompany().getTariffInfo();
|
||||
}
|
||||
|
||||
writeOffTokensForTariff(activeTariffInfo, value);
|
||||
}
|
||||
|
||||
public void writeOffTokensForTariff(TariffInfo activeTariffInfo, Integer value) throws MessagingException, IOException {
|
||||
Integer tokens = activeTariffInfo.getTokens();
|
||||
Integer boughtTokens = activeTariffInfo.getBoughtTokens();
|
||||
|
||||
@@ -64,6 +75,12 @@ public class TariffInfoService {
|
||||
|
||||
tariffInfoRepository.save(activeTariffInfo);
|
||||
} else {
|
||||
int needTokens = boughtTokens - value;
|
||||
User user = userRepository.findByPersonalTariffInfo(activeTariffInfo);
|
||||
|
||||
emailService.sendTokensNotFoundEmail(user, tokens, Math.abs(needTokens));
|
||||
log.error("User:" + user.getId() + " not have tokens for protect");
|
||||
|
||||
throw new TariffNotFoundException("User not have tokens for protect");
|
||||
}
|
||||
}
|
||||
@@ -71,15 +88,15 @@ public class TariffInfoService {
|
||||
public TariffInfo createTariffInfo(TariffType tariffType) {
|
||||
Tariff tariff = tariffRepository.findByType(tariffType.toString()).orElseThrow();
|
||||
|
||||
TariffInfo tariffInfo = createTariffInfo(LocalDateTime.now(), LocalDateTime.now().plusDays(30),
|
||||
TariffStatus.ACTIVE, tariff.getTokens());
|
||||
TariffInfo tariffInfo = createTariffInfo(LocalDateTime.now(),
|
||||
LocalDateTime.now().plusDays(tariff.getTariffTerm().getDays()), TariffStatus.ACTIVE, tariff.getTokens());
|
||||
tariffInfo.setTariff(tariff);
|
||||
|
||||
return tariffInfoRepository.save(tariffInfo);
|
||||
}
|
||||
|
||||
public void addTariffInfo(TariffInfo tariffInfo) {
|
||||
tariffInfoRepository.save(tariffInfo);
|
||||
public TariffInfo addTariffInfo(TariffInfo tariffInfo) {
|
||||
return tariffInfoRepository.save(tariffInfo);
|
||||
}
|
||||
|
||||
public List<TariffInfoDTO> getAllTariffInfos() {
|
||||
@@ -130,6 +147,13 @@ public class TariffInfoService {
|
||||
tariffInfo.getTariff().getMaxFilesCount());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void linkUserTariffInfo(User user, TariffInfo tariffInfo) {
|
||||
user.setPersonalTariffInfo(tariffInfo);
|
||||
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
private void updateTariffInfoFromDTO(TariffInfo tariffInfo, TariffInfoDTO dto) {
|
||||
tariffInfo.setStatus(TariffStatus.valueOf(dto.getStatus()));
|
||||
tariffInfo.setStartTariff(dto.getStartTariff());
|
||||
|
||||
@@ -7,13 +7,12 @@ import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.nocopy.entity.tarif.Tariff;
|
||||
import ru.soune.nocopy.entity.tarif.TariffTimeTerm;
|
||||
import ru.soune.nocopy.entity.tarif.TariffType;
|
||||
import ru.soune.nocopy.repository.TariffRepository;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -109,6 +108,18 @@ public class TariffInitializationService {
|
||||
tariff.setDiskSize(1024L * 1024 * 100);
|
||||
tariff.setMaxUsers(1L);
|
||||
tariff.setTariffAccountType("b2c");
|
||||
tariff.setTariffTerm(TariffTimeTerm.MONTHLY);
|
||||
break;
|
||||
|
||||
case DEMO:
|
||||
tariff.setName("demo");
|
||||
tariff.setPrice(0);
|
||||
tariff.setTokens(30);
|
||||
tariff.setMaxFilesCount(10);
|
||||
tariff.setDiskSize(1024L * 1024 * 100);
|
||||
tariff.setMaxUsers(1L);
|
||||
tariff.setTariffAccountType("b2c");
|
||||
tariff.setTariffTerm(TariffTimeTerm.MONTHLY);
|
||||
break;
|
||||
|
||||
case START:
|
||||
@@ -119,6 +130,18 @@ public class TariffInitializationService {
|
||||
tariff.setDiskSize(1024L * 1024 * 500);
|
||||
tariff.setMaxUsers(1L);
|
||||
tariff.setTariffAccountType("b2c");
|
||||
tariff.setTariffTerm(TariffTimeTerm.MONTHLY);
|
||||
break;
|
||||
|
||||
case START_YEAR:
|
||||
tariff.setName("start_year");
|
||||
tariff.setPrice(1100);
|
||||
tariff.setTokens(300);
|
||||
tariff.setMaxFilesCount(80);
|
||||
tariff.setDiskSize(1024L * 1024 * 500);
|
||||
tariff.setMaxUsers(1L);
|
||||
tariff.setTariffAccountType("b2c");
|
||||
tariff.setTariffTerm(TariffTimeTerm.YEAR);
|
||||
break;
|
||||
|
||||
case PRO:
|
||||
@@ -129,8 +152,21 @@ public class TariffInitializationService {
|
||||
tariff.setDiskSize(1024L * 1024 * 1024 * 10L);
|
||||
tariff.setMaxUsers(1L);
|
||||
tariff.setTariffAccountType("b2c");
|
||||
tariff.setTariffTerm(TariffTimeTerm.MONTHLY);
|
||||
break;
|
||||
|
||||
case PRO_YEAR:
|
||||
tariff.setName("pro_year");
|
||||
tariff.setPrice(4100);
|
||||
tariff.setTokens(900);
|
||||
tariff.setMaxFilesCount(300);
|
||||
tariff.setDiskSize(1024L * 1024 * 1024 * 10L);
|
||||
tariff.setMaxUsers(1L);
|
||||
tariff.setTariffAccountType("b2c");
|
||||
tariff.setTariffTerm(TariffTimeTerm.YEAR);
|
||||
break;
|
||||
|
||||
|
||||
case STUDIO:
|
||||
tariff.setName("studio");
|
||||
tariff.setPrice(9750);
|
||||
@@ -139,6 +175,18 @@ public class TariffInitializationService {
|
||||
tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
|
||||
tariff.setMaxUsers(2L);
|
||||
tariff.setTariffAccountType("b2c");
|
||||
tariff.setTariffTerm(TariffTimeTerm.MONTHLY);
|
||||
break;
|
||||
|
||||
case STUDIO_YEAR:
|
||||
tariff.setName("studio_year");
|
||||
tariff.setPrice(9500);
|
||||
tariff.setTokens(3000);
|
||||
tariff.setMaxFilesCount(1000);
|
||||
tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
|
||||
tariff.setMaxUsers(2L);
|
||||
tariff.setTariffAccountType("b2c");
|
||||
tariff.setTariffTerm(TariffTimeTerm.YEAR);
|
||||
break;
|
||||
|
||||
case BASIC:
|
||||
@@ -149,6 +197,18 @@ public class TariffInitializationService {
|
||||
tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
|
||||
tariff.setMaxUsers(basicUserCount);
|
||||
tariff.setTariffAccountType("b2c");
|
||||
tariff.setTariffTerm(TariffTimeTerm.MONTHLY);
|
||||
break;
|
||||
|
||||
case BASIC_YEAR:
|
||||
tariff.setName("basic_year");
|
||||
tariff.setPrice(basicPrice);
|
||||
tariff.setTokens(basicTokens);
|
||||
tariff.setMaxFilesCount(enterpriceMaxFileCount);
|
||||
tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
|
||||
tariff.setMaxUsers(basicUserCount);
|
||||
tariff.setTariffAccountType("b2c");
|
||||
tariff.setTariffTerm(TariffTimeTerm.YEAR);
|
||||
break;
|
||||
|
||||
case ENTERPRISE:
|
||||
@@ -159,6 +219,18 @@ public class TariffInitializationService {
|
||||
tariff.setDiskSize(1024L * 1024 * 1024 * 10L);
|
||||
tariff.setMaxUsers(10L);
|
||||
tariff.setTariffAccountType("b2b");
|
||||
tariff.setTariffTerm(TariffTimeTerm.MONTHLY);
|
||||
break;
|
||||
|
||||
case ENTERPRISE_YEAR:
|
||||
tariff.setName("enterprise_year");
|
||||
tariff.setPrice(200000);
|
||||
tariff.setTokens(30000);
|
||||
tariff.setMaxFilesCount(100000);
|
||||
tariff.setDiskSize(1024L * 1024 * 1024 * 10L);
|
||||
tariff.setMaxUsers(10L);
|
||||
tariff.setTariffAccountType("b2b");
|
||||
tariff.setTariffTerm(TariffTimeTerm.YEAR);
|
||||
break;
|
||||
|
||||
case BUISNES:
|
||||
@@ -169,8 +241,21 @@ public class TariffInitializationService {
|
||||
tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
|
||||
tariff.setMaxUsers(5L);
|
||||
tariff.setTariffAccountType("b2b");
|
||||
tariff.setTariffTerm(TariffTimeTerm.MONTHLY);
|
||||
break;
|
||||
|
||||
case BUISNES_YEAR:
|
||||
tariff.setName("buisnes_year");
|
||||
tariff.setPrice(29000);
|
||||
tariff.setTokens(5000);
|
||||
tariff.setMaxFilesCount(5000);
|
||||
tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
|
||||
tariff.setMaxUsers(5L);
|
||||
tariff.setTariffAccountType("b2b");
|
||||
tariff.setTariffTerm(TariffTimeTerm.YEAR);
|
||||
break;
|
||||
|
||||
|
||||
case CORPORAT:
|
||||
tariff.setName("corporat");
|
||||
tariff.setPrice(99000);
|
||||
@@ -179,6 +264,90 @@ public class TariffInitializationService {
|
||||
tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
|
||||
tariff.setMaxUsers(10L);
|
||||
tariff.setTariffAccountType("b2b");
|
||||
tariff.setTariffTerm(TariffTimeTerm.MONTHLY);
|
||||
break;
|
||||
|
||||
case CORPORAT_YEAR:
|
||||
tariff.setName("corporat_year");
|
||||
tariff.setPrice(99000);
|
||||
tariff.setTokens(20000);
|
||||
tariff.setMaxFilesCount(50000);
|
||||
tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
|
||||
tariff.setMaxUsers(10L);
|
||||
tariff.setTariffAccountType("b2b");
|
||||
tariff.setTariffTerm(TariffTimeTerm.YEAR);
|
||||
break;
|
||||
|
||||
|
||||
case TOKEN_100:
|
||||
tariff.setName("token_100");
|
||||
tariff.setPrice(299);
|
||||
tariff.setTokens(100);
|
||||
tariff.setMaxFilesCount(0);
|
||||
tariff.setDiskSize(0L);
|
||||
tariff.setMaxUsers(0L);
|
||||
tariff.setTariffAccountType("token");
|
||||
break;
|
||||
|
||||
case TOKEN_500:
|
||||
tariff.setName("token_500");
|
||||
tariff.setPrice(1299);
|
||||
tariff.setTokens(500);
|
||||
tariff.setMaxFilesCount(0);
|
||||
tariff.setDiskSize(0L);
|
||||
tariff.setMaxUsers(0L);
|
||||
tariff.setTariffAccountType("token");
|
||||
break;
|
||||
|
||||
case TOKEN_1000:
|
||||
tariff.setName("token_1000");
|
||||
tariff.setPrice(2399);
|
||||
tariff.setTokens(1000);
|
||||
tariff.setMaxFilesCount(0);
|
||||
tariff.setDiskSize(0L);
|
||||
tariff.setMaxUsers(0L);
|
||||
tariff.setTariffAccountType("token");
|
||||
break;
|
||||
|
||||
case TOKEN_5000:
|
||||
tariff.setName("token_5000");
|
||||
tariff.setPrice(9999);
|
||||
tariff.setTokens(5000);
|
||||
tariff.setMaxFilesCount(0);
|
||||
tariff.setDiskSize(0L);
|
||||
tariff.setMaxUsers(0L);
|
||||
tariff.setTariffAccountType("token");
|
||||
break;
|
||||
|
||||
|
||||
case MONITORING_DAILY:
|
||||
tariff.setName("monitoring_daily");
|
||||
tariff.setPrice(0);
|
||||
tariff.setTokens(10);
|
||||
tariff.setMaxFilesCount(0);
|
||||
tariff.setDiskSize(0L);
|
||||
tariff.setMaxUsers(0L);
|
||||
tariff.setTariffAccountType("monitoring");
|
||||
break;
|
||||
|
||||
case MONITORING_WEEKLY:
|
||||
tariff.setName("monitoring_weekly");
|
||||
tariff.setPrice(0);
|
||||
tariff.setTokens(25);
|
||||
tariff.setMaxFilesCount(0);
|
||||
tariff.setDiskSize(0L);
|
||||
tariff.setMaxUsers(0L);
|
||||
tariff.setTariffAccountType("monitoring");
|
||||
break;
|
||||
|
||||
case MONITORING_MONTHLY:
|
||||
tariff.setName("monitoring_monthly");
|
||||
tariff.setPrice(0);
|
||||
tariff.setTokens(50);
|
||||
tariff.setMaxFilesCount(0);
|
||||
tariff.setDiskSize(0L);
|
||||
tariff.setMaxUsers(0L);
|
||||
tariff.setTariffAccountType("monitoring");
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.nocopy.dto.tarriff.TariffDTO;
|
||||
import ru.soune.nocopy.entity.tarif.Tariff;
|
||||
import ru.soune.nocopy.entity.tarif.TariffTimeTerm;
|
||||
import ru.soune.nocopy.entity.tarif.TariffType;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
import ru.soune.nocopy.exception.TariffNotFoundException;
|
||||
@@ -20,7 +21,7 @@ public class TariffService {
|
||||
private final TariffRepository tariffRepository;
|
||||
|
||||
public void addTariff(String tariffName, double price, String type, int tokens, long diskSize, long maxUsers,
|
||||
int maxFilesCount) {
|
||||
int maxFilesCount, String tariffTerm) {
|
||||
Tariff tariff = new Tariff();
|
||||
tariff.setName(tariffName);
|
||||
tariff.setPrice(price);
|
||||
@@ -29,6 +30,7 @@ public class TariffService {
|
||||
tariff.setDiskSize(diskSize);
|
||||
tariff.setMaxUsers(maxUsers);
|
||||
tariff.setMaxFilesCount(maxFilesCount);
|
||||
tariff.setTariffTerm(TariffTimeTerm.valueOf(tariffTerm.toUpperCase()));
|
||||
|
||||
tariffRepository.save(tariff);
|
||||
}
|
||||
@@ -42,6 +44,22 @@ public class TariffService {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<TariffDTO> getTariffByAccountTypeAndTariffTerm(User user, TariffTimeTerm tariffTerm) {
|
||||
String typeAccount = user.canManageCompanySettings() ? "b2b": "b2c";
|
||||
|
||||
return tariffRepository.findByTariffAccountTypeAndTariffTerm(typeAccount, tariffTerm)
|
||||
.stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<TariffDTO> getTariffByType(String type) {
|
||||
return tariffRepository.findByTariffAccountType(type)
|
||||
.stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<TariffDTO> getAllTariffs() {
|
||||
return tariffRepository.findAllByOrderByPriceAsc()
|
||||
.stream()
|
||||
@@ -84,6 +102,8 @@ public class TariffService {
|
||||
}
|
||||
|
||||
private TariffDTO convertToDTO(Tariff tariff) {
|
||||
TariffTimeTerm tariffTerm = tariff.getTariffTerm();
|
||||
|
||||
return new TariffDTO(
|
||||
tariff.getId(),
|
||||
tariff.getType(),
|
||||
@@ -92,7 +112,8 @@ public class TariffService {
|
||||
tariff.getTokens(),
|
||||
tariff.getMaxFilesCount(),
|
||||
tariff.getDiskSize(),
|
||||
tariff.getMaxUsers()
|
||||
tariff.getMaxUsers(),
|
||||
tariffTerm == null ? "" : tariffTerm.name()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,5 +125,6 @@ public class TariffService {
|
||||
tariff.setMaxFilesCount(dto.getMaxFilesCount());
|
||||
tariff.setDiskSize(dto.getDiskSize());
|
||||
tariff.setMaxUsers(dto.getMaxUsers());
|
||||
tariff.setTariffTerm(TariffTimeTerm.valueOf(dto.getTariffTerm().toUpperCase()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package ru.soune.nocopy.service.user;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
import ru.soune.nocopy.entity.user.UserAdditionalInfo;
|
||||
import ru.soune.nocopy.exception.RegistrationAdditionalInfoNullException;
|
||||
import ru.soune.nocopy.repository.UserAdditionalInfoRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AdditionalInfoService {
|
||||
|
||||
private final UserAdditionalInfoRepository additionalInfoRepository;
|
||||
|
||||
@Transactional
|
||||
public void additionalInfo(String ip, String userAgent, User user) {
|
||||
if (ip == null || userAgent == null) {
|
||||
throw new RegistrationAdditionalInfoNullException("Ip address or user agent is null");
|
||||
}
|
||||
|
||||
UserAdditionalInfo userAdditionalInfo = UserAdditionalInfo.builder()
|
||||
.ipAddress(ip)
|
||||
.userAgent(userAgent)
|
||||
.user(user)
|
||||
.build();
|
||||
|
||||
additionalInfoRepository.save(userAdditionalInfo);
|
||||
}
|
||||
|
||||
public UserAdditionalInfo getAdditionalInfoByUser(Long userId) {
|
||||
return additionalInfoRepository.findById(userId).orElse(null);
|
||||
}
|
||||
|
||||
public List<UserAdditionalInfo> getAllAdditionalInfos() {
|
||||
return additionalInfoRepository.findAll();
|
||||
}
|
||||
|
||||
public void deleteAdditionalInfo(Long userId) {
|
||||
additionalInfoRepository.deleteById(userId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Optional<UserAdditionalInfo> updateAdditionalInfo(Long userId, String ip, String userAgent) {
|
||||
return additionalInfoRepository.findById(userId)
|
||||
.map(info -> {
|
||||
info.setIpAddress(ip);
|
||||
info.setUserAgent(userAgent);
|
||||
return additionalInfoRepository.save(info);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package ru.soune.nocopy.service.violation;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.nocopy.dto.violation.ViolationNotionResponse;
|
||||
import ru.soune.nocopy.dto.violation.ViolationNotionsListResponse;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
import ru.soune.nocopy.entity.violation.Violation;
|
||||
import ru.soune.nocopy.entity.violation.ViolationNotion;
|
||||
import ru.soune.nocopy.repository.UserRepository;
|
||||
import ru.soune.nocopy.repository.ViolationNotionRepository;
|
||||
import ru.soune.nocopy.repository.ViolationRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ViolationNotionService {
|
||||
|
||||
private final ViolationNotionRepository notionRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final ViolationRepository violationRepository;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ViolationNotionsListResponse getNotionsByViolationId(Long violationId, Long userId) {
|
||||
Violation violation = violationRepository.findById(violationId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Violation not found: " + violationId));
|
||||
|
||||
List<ViolationNotion> notions = notionRepository.findByViolationId(violationId);
|
||||
long totalCount = notionRepository.countByViolationId(violationId);
|
||||
|
||||
List<ViolationNotionResponse> notionResponses = notions.stream()
|
||||
.map(this::mapToResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ViolationNotionsListResponse.builder()
|
||||
.violationId(violationId)
|
||||
.url(violation.getUrl())
|
||||
.host(violation.getHost())
|
||||
.totalCount((int) totalCount)
|
||||
.notions(notionResponses)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ViolationNotionResponse getNotionById(Long notionId, Long userId) {
|
||||
ViolationNotion notion = notionRepository.findById(notionId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Notion not found: " + notionId));
|
||||
|
||||
if (!notion.getUser().getId().equals(userId)) {
|
||||
throw new SecurityException("User does not have access to this notion");
|
||||
}
|
||||
|
||||
return mapToResponse(notion);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ViolationNotionResponse createNotion(Long violationId, Long userId, String message) {
|
||||
User user = userRepository.findById(userId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("User not found: " + userId));
|
||||
|
||||
Violation violation = violationRepository.findById(violationId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Violation not found: " + violationId));
|
||||
|
||||
ViolationNotion notion = new ViolationNotion();
|
||||
notion.setUser(user);
|
||||
notion.setViolation(violation);
|
||||
notion.setMessage(message);
|
||||
|
||||
ViolationNotion saved = notionRepository.save(notion);
|
||||
log.info("Created notion for violation: {} by user: {}", violationId, userId);
|
||||
|
||||
return mapToResponse(saved);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ViolationNotionResponse updateNotion(Long notionId, Long userId, String message) {
|
||||
ViolationNotion notion = notionRepository.findById(notionId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Notion not found: " + notionId));
|
||||
|
||||
if (!notion.getUser().getId().equals(userId)) {
|
||||
throw new SecurityException("Only author can update this notion");
|
||||
}
|
||||
|
||||
notion.setMessage(message);
|
||||
ViolationNotion updated = notionRepository.save(notion);
|
||||
log.info("Updated notion: {} by user: {}", notionId, userId);
|
||||
|
||||
return mapToResponse(updated);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteNotion(Long notionId, Long userId) {
|
||||
ViolationNotion notion = notionRepository.findById(notionId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Notion not found: " + notionId));
|
||||
|
||||
if (!notion.getUser().getId().equals(userId)) {
|
||||
throw new SecurityException("Only author can delete this notion");
|
||||
}
|
||||
|
||||
notionRepository.delete(notion);
|
||||
log.info("Deleted notion: {} by user: {}", notionId, userId);
|
||||
}
|
||||
|
||||
private ViolationNotionResponse mapToResponse(ViolationNotion notion) {
|
||||
return ViolationNotionResponse.builder()
|
||||
.id(notion.getId())
|
||||
.message(notion.getMessage())
|
||||
.createdAt(notion.getCreatedAt())
|
||||
.updatedAt(notion.getUpdatedAt())
|
||||
.user(ViolationNotionResponse.UserInfo.builder()
|
||||
.id(notion.getUser().getId())
|
||||
.fullName(notion.getUser().getFullName())
|
||||
.email(notion.getUser().getEmail())
|
||||
.build())
|
||||
.violation(ViolationNotionResponse.ViolationInfo.builder()
|
||||
.id(notion.getViolation().getId())
|
||||
.url(notion.getViolation().getPageUrl())
|
||||
.host(notion.getViolation().getHost())
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package ru.soune.nocopy.service.violation;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
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.dto.file.YandexSearchResponse;
|
||||
import ru.soune.nocopy.dto.violation.FileViolationSummaryDTO;
|
||||
import ru.soune.nocopy.dto.violation.ViolationStatisticsResponse;
|
||||
import ru.soune.nocopy.entity.file.FileEntity;
|
||||
import ru.soune.nocopy.entity.search.GlobalSearchResult;
|
||||
import ru.soune.nocopy.entity.violation.Violation;
|
||||
import ru.soune.nocopy.repository.FileEntityRepository;
|
||||
import ru.soune.nocopy.repository.ViolationRepository;
|
||||
import ru.soune.nocopy.service.file.FileEntityService;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class ViolationService {
|
||||
|
||||
private final ViolationRepository violationRepository;
|
||||
|
||||
private final FileEntityRepository fileEntityRepository;
|
||||
|
||||
private final FileEntityService fileEntityService;
|
||||
|
||||
public void processViolation(YandexSearchResponse.ImageResult imageResult, FileEntity file,
|
||||
GlobalSearchResult result) {
|
||||
String url = imageResult.getUrl();
|
||||
String urlHash = DigestUtils.sha256Hex(url);
|
||||
|
||||
if (!violationRepository.existsByUrlHash(urlHash)) {
|
||||
Violation violation = new Violation();
|
||||
|
||||
if (result != null) {
|
||||
violation.setGlobalSearchResult(result);
|
||||
}
|
||||
|
||||
violation.setHost(imageResult.getHost());
|
||||
violation.setUrl(url);
|
||||
violation.setUrlHash(urlHash);
|
||||
violation.setPageUrl(imageResult.getPageUrl());
|
||||
violation.setPageTitle(imageResult.getPageTitle());
|
||||
violation.setFileEntity(file);
|
||||
violation.setStatus(ViolationStatus.NEW.name());
|
||||
|
||||
violationRepository.save(violation);
|
||||
}
|
||||
}
|
||||
|
||||
public List<FileViolationSummaryDTO> getFileViolationsSummary(Long userId) {
|
||||
List<FileEntity> userFiles = fileEntityService.getAllUserFiles(userId);
|
||||
List<Violation> violations = violationRepository.findByFileEntityIn(userFiles);
|
||||
|
||||
Map<String, List<Violation>> violationsByFileId = violations.stream()
|
||||
.collect(Collectors.groupingBy(v -> v.getFileEntity().getId()));
|
||||
|
||||
return userFiles.stream()
|
||||
.map(file -> {
|
||||
List<Violation> fileViolations = violationsByFileId.getOrDefault(file.getId(),
|
||||
Collections.emptyList());
|
||||
|
||||
if (fileViolations.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LocalDateTime latestDate = fileViolations.stream()
|
||||
.map(Violation::getCreatedDate)
|
||||
.max(LocalDateTime::compareTo)
|
||||
.orElse(null);
|
||||
|
||||
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();
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void changeStatus(ViolationStatus status, Long violationId) {
|
||||
Violation violation = violationRepository.findById(violationId).orElseThrow();
|
||||
violation.setStatus(status.name());
|
||||
|
||||
violationRepository.save(violation);
|
||||
}
|
||||
|
||||
public Page<Violation> getViolationsByFiles(List<FileEntity> files, int page, int size, String sortDirection) {
|
||||
Sort sort = Sort.by(sortDirection.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC, "createdDate");
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
return violationRepository.findByFileEntityIn(files, pageable);
|
||||
}
|
||||
|
||||
public Page<Violation> getViolationsByFilesAndStatus(List<FileEntity> files, String status, int page, int size, String sortDirection) {
|
||||
Sort sort = Sort.by(sortDirection.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC, "createdDate");
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
return violationRepository.findByFileEntityInAndStatus(files, status, pageable);
|
||||
}
|
||||
|
||||
public Page<Violation> getViolationsByFilesAndDateRange(List<FileEntity> files, LocalDateTime startDate, LocalDateTime endDate,
|
||||
int page, int size, String sortDirection) {
|
||||
Sort sort = Sort.by(sortDirection.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC, "createdDate");
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
return violationRepository.findByFileEntityInAndCreatedDateBetween(files, startDate, endDate, pageable);
|
||||
}
|
||||
|
||||
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) {
|
||||
violations = violationRepository.findByFileEntityInAndStatusAndCreatedDateBetween(files, status, startDate, endDate);
|
||||
} else if (status != null && !status.isEmpty()) {
|
||||
violations = violationRepository.findByFileEntityInAndStatus(files, status);
|
||||
} else if (startDate != null && endDate != null) {
|
||||
violations = violationRepository.findByFileEntityInAndCreatedDateBetween(files, startDate, endDate);
|
||||
} else {
|
||||
violations = violationRepository.findByFileEntityIn(files);
|
||||
}
|
||||
|
||||
Map<String, Long> grouped = violations.stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
v -> extractGroupKey(v.getPageUrl(), groupBy),
|
||||
Collectors.counting()));
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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 {
|
||||
userFiles = fileEntityService.getAllUserFiles(userId);
|
||||
}
|
||||
|
||||
if (userFiles.isEmpty()) {
|
||||
return ViolationStatisticsResponse.builder()
|
||||
.totalViolations(0)
|
||||
.newViolations(0)
|
||||
.inProgressViolations(0)
|
||||
.resolvedViolations(0)
|
||||
.build();
|
||||
}
|
||||
|
||||
List<Violation> violations;
|
||||
|
||||
if (startDate != null && endDate != null) {
|
||||
violations = violationRepository.findByFileEntityInAndCreatedDateBetween(userFiles, startDate, endDate);
|
||||
} else {
|
||||
violations = violationRepository.findByFileEntityIn(userFiles);
|
||||
}
|
||||
|
||||
Set<String> inProgressStatuses = Set.of(
|
||||
ViolationStatus.SHOWED.name(),
|
||||
ViolationStatus.LEGAL_IN_WORK.name(),
|
||||
ViolationStatus.COMPLAINT_IN_WORK.name(),
|
||||
ViolationStatus.COMPLAINT_AND_LEGAL_IN_WORK.name()
|
||||
);
|
||||
|
||||
long newViolations = violations.stream()
|
||||
.filter(v -> v.getStatus().equals(ViolationStatus.NEW.name()))
|
||||
.count();
|
||||
|
||||
long inProgressViolations = violations.stream()
|
||||
.filter(v -> inProgressStatuses.contains(v.getStatus()))
|
||||
.count();
|
||||
|
||||
long resolvedViolations = violations.stream()
|
||||
.filter(v -> v.getStatus().equals(ViolationStatus.AUTHORIZED_USE.name()))
|
||||
.count();
|
||||
|
||||
return ViolationStatisticsResponse.builder()
|
||||
.totalViolations(violations.size())
|
||||
.newViolations(newViolations)
|
||||
.inProgressViolations(inProgressViolations)
|
||||
.resolvedViolations(resolvedViolations)
|
||||
.build();
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package ru.soune.nocopy.service.violation;
|
||||
|
||||
public enum ViolationStatus {
|
||||
NEW , SHOWED, LEGAL_IN_WORK, COMPLAINT_IN_WORK, COMPLAINT_AND_LEGAL_IN_WORK , AUTHORIZED_USE
|
||||
}
|
||||
Reference in New Issue
Block a user