From 12aecff5c941060078ee038de819fdefd77af2d0 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 13 Feb 2026 16:25:04 +0700 Subject: [PATCH 001/274] dev add inn check for company --- .../java/ru/soune/nocopy/dto/register/RegRequest.java | 2 ++ .../java/ru/soune/nocopy/entity/company/Company.java | 3 +++ .../ru/soune/nocopy/repository/CompanyRepository.java | 2 ++ .../ru/soune/nocopy/service/register/AuthService.java | 9 +++++++-- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java b/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java index 15d1c47..af948fa 100644 --- a/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java @@ -13,6 +13,8 @@ public class RegRequest { private String companyName; + private String inn; + @NotEmpty(message = "Need account type") private String accountType; diff --git a/src/main/java/ru/soune/nocopy/entity/company/Company.java b/src/main/java/ru/soune/nocopy/entity/company/Company.java index 09faf32..6135ed8 100644 --- a/src/main/java/ru/soune/nocopy/entity/company/Company.java +++ b/src/main/java/ru/soune/nocopy/entity/company/Company.java @@ -32,6 +32,9 @@ public class Company { @Column(name = "phone") private String phone; + @Column(name = "inn", unique = true) + private String inn; + @CreatedDate @Column(name = "register_date", updatable = false, nullable = false) private LocalDateTime registerDate = LocalDateTime.now(); diff --git a/src/main/java/ru/soune/nocopy/repository/CompanyRepository.java b/src/main/java/ru/soune/nocopy/repository/CompanyRepository.java index b075868..258481f 100644 --- a/src/main/java/ru/soune/nocopy/repository/CompanyRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/CompanyRepository.java @@ -13,6 +13,8 @@ import java.util.Optional; public interface CompanyRepository extends JpaRepository { Optional findByCompanyName(String companyName); + Optional findByInn(String companyName); + boolean existsByCompanyName(String companyName); List findByCompanyNameContainingIgnoreCase(String name); diff --git a/src/main/java/ru/soune/nocopy/service/register/AuthService.java b/src/main/java/ru/soune/nocopy/service/register/AuthService.java index f90a75c..29b0be6 100644 --- a/src/main/java/ru/soune/nocopy/service/register/AuthService.java +++ b/src/main/java/ru/soune/nocopy/service/register/AuthService.java @@ -17,7 +17,6 @@ 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.exception.NotFoundAuthToken; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.AuthTokenRepository; @@ -26,7 +25,6 @@ import ru.soune.nocopy.repository.TariffInfoRepository; import ru.soune.nocopy.repository.UserRepository; 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; @@ -153,8 +151,15 @@ public class AuthService { } private void createCompanyAccount(RegRequest request, User user) { + Optional byInn = companyRepository.findByInn(request.getInn()); + + if (byInn.isPresent()) { + throw new IllegalStateException("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); From 1e14fa3f23a76dd30c8502687187125da12f1940 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 13 Feb 2026 21:40:15 +0700 Subject: [PATCH 002/274] dev add correct time for build --- infrastructure/jenkins/deploy-backend.groovy | 2 +- infrastructure/jenkins/deploy.groovy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/infrastructure/jenkins/deploy-backend.groovy b/infrastructure/jenkins/deploy-backend.groovy index 56cd560..c1f96f9 100644 --- a/infrastructure/jenkins/deploy-backend.groovy +++ b/infrastructure/jenkins/deploy-backend.groovy @@ -39,7 +39,7 @@ pipeline { stage('Deploy with docker-compose') { steps { script { - def buildTime = sh(script: "date '+%d-%m-%Y %H:%M:%S'", returnStdout: true).trim() + def buildTime = sh(script: "TZ='Asia/Novosibirsk' date '+%d-%m-%Y %H:%M:%S'", returnStdout: true).trim() withCredentials([ usernamePassword( credentialsId: 'server-root-password', diff --git a/infrastructure/jenkins/deploy.groovy b/infrastructure/jenkins/deploy.groovy index fbfb736..6600988 100644 --- a/infrastructure/jenkins/deploy.groovy +++ b/infrastructure/jenkins/deploy.groovy @@ -34,7 +34,7 @@ pipeline { stage('Deploy with docker-compose') { steps { script { - def buildTime = sh(script: "date '+%d-%m-%Y %H:%M:%S'", returnStdout: true).trim() + def buildTime = sh(script: "TZ='Asia/Novosibirsk' date '+%d-%m-%Y %H:%M:%S'", returnStdout: true).trim() withCredentials([ usernamePassword( credentialsId: 'server-root-password', From 814d13793718fdb486eff8fb76239346eb770022 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 10:13:42 +0700 Subject: [PATCH 003/274] dev add logs for search --- .../nocopy/handler/ImageFoundRequestHandler.java | 16 ++++++++++++++++ .../nocopy/service/dadata/DaDataService.java | 5 +++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 2d1f5f0..2ddf421 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -87,18 +87,34 @@ public class ImageFoundRequestHandler implements RequestHandler { List allYandexImages = searchImageService.getAllImagesWithoutPagination( searchResponseYandex, "visual_matches"); + log.info("GET ALL YANDEX IMAGES!"); + log.info("GET ALL YANDEX IMAGES!"); + log.info("GET ALL YANDEX IMAGES!"); + log.info("GET ALL YANDEX IMAGES!"); List allGoogleImages = searchImageService.getAllImagesWithoutPagination( searchResponseGoogle, "exact_matches"); List allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); + log.info("GET ALL GOOGLE IMAGES!"); + log.info("GET ALL GOOGLE IMAGES!"); + log.info("GET ALL GOOGLE IMAGES!"); + log.info("GET ALL GOOGLE IMAGES!"); + int page = imageSearchRequest.getPage() != null ? imageSearchRequest.getPage() : 1; int pageSize = 5; List pagedResults = searchImageService.paginateResults(allUniqueImages, page, pageSize * 2); + log.info("GET ALL pagedResults IMAGES!"); + log.info("GET ALL pagedResults IMAGES!"); + log.info("GET ALL pagedResults IMAGES!"); + log.info("GET ALL pagedResults IMAGES!"); + log.info("GET ALL pagedResults IMAGES!"); + log.info("GET ALL pagedResults IMAGES!"); + YandexSearchResponse finalResponse = new YandexSearchResponse(); finalResponse.setImages(pagedResults); finalResponse.setPage(page); diff --git a/src/main/java/ru/soune/nocopy/service/dadata/DaDataService.java b/src/main/java/ru/soune/nocopy/service/dadata/DaDataService.java index a1a6113..6d000af 100644 --- a/src/main/java/ru/soune/nocopy/service/dadata/DaDataService.java +++ b/src/main/java/ru/soune/nocopy/service/dadata/DaDataService.java @@ -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"); } From e555436d546814ddc1597f3cafdbcbc2b7ac3a10 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 10:20:36 +0700 Subject: [PATCH 004/274] dev add logs for search --- .../soune/nocopy/service/search/SearchImageService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index ba6fa2c..af27d76 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -32,10 +32,10 @@ public class SearchImageService { public SearchImageService() { this.httpClient = new OkHttpClient.Builder() - .connectTimeout(5, TimeUnit.SECONDS) - .writeTimeout(5, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .callTimeout(20, TimeUnit.SECONDS) + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(35, TimeUnit.SECONDS) + .callTimeout(40, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .build(); From f15a24acc2118fe91e36794630db22cdc00055c8 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 10:33:13 +0700 Subject: [PATCH 005/274] dev add logs for search --- .../nocopy/handler/ImageFoundRequestHandler.java | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 2ddf421..2d1f5f0 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -87,34 +87,18 @@ public class ImageFoundRequestHandler implements RequestHandler { List allYandexImages = searchImageService.getAllImagesWithoutPagination( searchResponseYandex, "visual_matches"); - log.info("GET ALL YANDEX IMAGES!"); - log.info("GET ALL YANDEX IMAGES!"); - log.info("GET ALL YANDEX IMAGES!"); - log.info("GET ALL YANDEX IMAGES!"); List allGoogleImages = searchImageService.getAllImagesWithoutPagination( searchResponseGoogle, "exact_matches"); List allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); - log.info("GET ALL GOOGLE IMAGES!"); - log.info("GET ALL GOOGLE IMAGES!"); - log.info("GET ALL GOOGLE IMAGES!"); - log.info("GET ALL GOOGLE IMAGES!"); - int page = imageSearchRequest.getPage() != null ? imageSearchRequest.getPage() : 1; int pageSize = 5; List pagedResults = searchImageService.paginateResults(allUniqueImages, page, pageSize * 2); - log.info("GET ALL pagedResults IMAGES!"); - log.info("GET ALL pagedResults IMAGES!"); - log.info("GET ALL pagedResults IMAGES!"); - log.info("GET ALL pagedResults IMAGES!"); - log.info("GET ALL pagedResults IMAGES!"); - log.info("GET ALL pagedResults IMAGES!"); - YandexSearchResponse finalResponse = new YandexSearchResponse(); finalResponse.setImages(pagedResults); finalResponse.setPage(page); From 7a02b94bd07bb722dfdb53b01249c1ee71591dac Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 10:43:20 +0700 Subject: [PATCH 006/274] dev add code respone for download files --- .../java/ru/soune/nocopy/controller/ApiController.java | 10 ++++------ src/main/java/ru/soune/nocopy/dto/MessageCode.java | 2 ++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 0e3c75a..3b35a3c 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -300,12 +300,10 @@ public class ApiController { if (tokenHeader == null) { Map errorData = new HashMap<>(); errorData.put("token", tokenHeader); - errorData.put("fileId", fileId); - errorData.put("version", version); return ResponseEntity.ok().body(new BaseResponse(20004, - MessageCode.FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD.getCode(), - MessageCode.FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD.getDescription(), + MessageCode.TOKEN_IS_NULL.getCode(), + MessageCode.TOKEN_IS_NULL.getDescription(), errorData)); } @@ -317,8 +315,8 @@ public class ApiController { errorData.put("token", tokenHeader); return ResponseEntity.ok().body(new BaseResponse(20004, - MessageCode.FILE_DOWNLOAD_ERROR.getCode(), - MessageCode.FILE_DOWNLOAD_ERROR.getDescription(), + MessageCode.USER_NOT_HAD_PERMISSION.getCode(), + MessageCode.USER_NOT_HAD_PERMISSION.getDescription(), errorData)); } diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index b4c1c01..d6a1379 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -12,10 +12,12 @@ public enum MessageCode { FILE_UPLOAD_ERROR(2, "File upload error"), DUPLICATE_FILE_UPLOAD(2, "Duplicate file upload"), FILE_DOWNLOAD_ERROR(2, "File download error"), + USER_NOT_HAD_PERMISSION(2, "User not have permission for file"), USER_NOT_VERIFIED(2, "User not verified"), PERMISSION_NOT_FOUND(2, "Permission not found"), USER_NOT_FOUND(2, "User not found"), FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD(2, "Not correct field"), + TOKEN_IS_NULL(2, "Token field is null"), IMAGE_FOUND_ERROR(2, "Image found error"), INVALID_JSON_BODY(2, "Invalid fields in JSON object"), INCOMPLETE_UPLOAD(2, "Not load all chunks"), From 27705c5739915f0c7366d68bfbab91e239332939 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 16:09:00 +0700 Subject: [PATCH 007/274] dev add check hash for all types,exclude video --- .../java/ru/soune/nocopy/service/file/FileEntityService.java | 2 +- .../soune/nocopy/service/file/impl/FileUploadServiceImpl.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index b91eca9..8cff8f2 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -56,7 +56,7 @@ public class FileEntityService { Map imageHash = Map.of(); - if (session.getFileType().startsWith("image")) { + if (!session.getFileType().startsWith("video")) { imageHash = imageHashService.calculateHash(filePath); List duplicatedByHash = fileSimilarityService.findDuplicatedByHash( imageHash.get("hi"), imageHash.get("low")); diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index db52626..2ed6f93 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -240,7 +240,7 @@ public class FileUploadServiceImpl implements FileUploadService { FileEntity saved = fileEntityRepository.save(fileEntity); - if (session.getFileType().equals("image")) { + if (!session.getFileType().equals("video")) { Map hash = imageHashService.calculateHash(filePath); imageHashService.create(saved, hash); @@ -318,7 +318,7 @@ public class FileUploadServiceImpl implements FileUploadService { String finalFilePath = assembleFileSynchronously(session); - if (session.getFileType().startsWith("image") && findSimilar == 0) { + if (!session.getFileType().startsWith("video") && findSimilar == 0) { checkForDuplicatesSynchronously(finalFilePath); } From d74571b7cb050b8d35b7694c992845d487d57d05 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 16:15:15 +0700 Subject: [PATCH 008/274] dev add check hash for all types,exclude video --- .../java/ru/soune/nocopy/service/file/FileEntityService.java | 2 +- .../soune/nocopy/service/file/impl/FileUploadServiceImpl.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index 8cff8f2..b91eca9 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -56,7 +56,7 @@ public class FileEntityService { Map imageHash = Map.of(); - if (!session.getFileType().startsWith("video")) { + if (session.getFileType().startsWith("image")) { imageHash = imageHashService.calculateHash(filePath); List duplicatedByHash = fileSimilarityService.findDuplicatedByHash( imageHash.get("hi"), imageHash.get("low")); diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index 2ed6f93..9395adc 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -240,7 +240,7 @@ public class FileUploadServiceImpl implements FileUploadService { FileEntity saved = fileEntityRepository.save(fileEntity); - if (!session.getFileType().equals("video")) { + if (session.getFileType().equals("image")) { Map hash = imageHashService.calculateHash(filePath); imageHashService.create(saved, hash); From 31544084caa60e6783c7caf9ef561bdbdcd9a3e6 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 16:41:29 +0700 Subject: [PATCH 009/274] dev add check hash for all types,exclude video --- .../repository/FileEntityRepository.java | 3 + .../nocopy/service/FileSimilarityService.java | 99 +++++++++++++++---- 2 files changed, 83 insertions(+), 19 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java index 2fd8655..9e098db 100644 --- a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java @@ -52,4 +52,7 @@ public interface FileEntityRepository extends JpaRepository String findFileIdByFilePath(@Param("filePath") String filePath); List findFileByUserIdAndStatus(Long userId, FileStatus status); + + @Query("SELECT f FROM FileEntity f WHERE f.userId = :userId AND f.mimeType = :mimeType") + List findByUserIdAndMimeType(Long userId, String mimeType); } diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index b4be9e9..5b4c8f5 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -11,13 +11,14 @@ 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.file.FileEntity; 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.repository.*; import ru.soune.nocopy.util.FileUtil; +import java.io.FileInputStream; +import java.io.InputStream; +import java.security.MessageDigest; import java.util.*; import java.util.stream.Collectors; @@ -137,25 +138,43 @@ public class FileSimilarityService { return duplicates; } - public Page findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable, String authToken) { - var imageHashEntity = hashRepository.findById(fileId) - .orElseThrow(() -> new RuntimeException("Hash not found")); + private final FileEntityRepository fileEntityRepository; - Long hash64Hi = imageHashEntity.getHash64Hi(); - Long hash64Lo = imageHashEntity.getHash64Lo(); + public Page findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable, + String authToken) throws Exception { + FileEntity fileEntity = fileEntityRepository.findByFileId(fileId); + String mimeType = fileEntity.getMimeType(); + List allResults = new ArrayList<>(); - List candidates = authToken.equals("all") ? repository.findCandidates(fileId): - repository.findCandidatesFromUserFiles(fileId, authTokenRepository.findUserIdByToken(authToken)); + if (mimeType.equals("image")) { + var imageHashEntity = hashRepository.findById(fileId) + .orElseThrow(() -> new RuntimeException("Hash not found")); - List similarityLevels = (filter != null && filter.getSimilarityLevels() != null) - ? filter.getSimilarityLevels() - : List.of("DUPLICATE", "SIMILAR", "DIFFERENT"); + Long hash64Hi = imageHashEntity.getHash64Hi(); + Long hash64Lo = imageHashEntity.getHash64Lo(); + List candidates = authToken.equals("all") ? repository.findCandidates(fileId): + repository.findCandidatesFromUserFiles(fileId, authTokenRepository.findUserIdByToken(authToken)); - List allResults = candidates.stream() - .map(c -> createSimilarFileResponse(c, hash64Hi, hash64Lo)) - .filter(response -> similarityLevels.contains(response.getSimilarityLevel())) - .sorted(Comparator.comparingInt(SimilarFileDTO::getHammingDistance)) - .collect(Collectors.toList()); + List similarityLevels = (filter != null && filter.getSimilarityLevels() != null) + ? filter.getSimilarityLevels() + : List.of("DUPLICATE", "SIMILAR", "DIFFERENT"); + + allResults = candidates.stream() + .map(c -> createSimilarFileResponse(c, hash64Hi, hash64Lo)) + .filter(response -> similarityLevels.contains(response.getSimilarityLevel())) + .sorted(Comparator.comparingInt(SimilarFileDTO::getHammingDistance)) + .collect(Collectors.toList()); + } else { + String hash = calculateFileHash(fileEntity); + Long userId = fileEntity.getUserId(); + List fileEntityList = fileEntityRepository.findByUserIdAndMimeType(userId, mimeType); + + for (FileEntity file : fileEntityList) { + if (calculateFileHash(file).equals(hash)) { + allResults.add(buildDTO(file)); + } + } + } int total = allResults.size(); int page = (pageable != null) ? pageable.getPageNumber() : 0; @@ -169,6 +188,35 @@ public class FileSimilarityService { return new PageImpl<>(pageContent, pageable, total); } + + private String calculateFileHash(FileEntity fileEntity) throws Exception { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + +// if (fileEntity.getStatus() == FileStatus.CLOUD) { +// File tempFile = cloudStorageService.readFileFromStorageByPath(fileEntity.getFilePath()); +// try (InputStream is = new FileInputStream(tempFile)) { +// byte[] buffer = new byte[8192]; +// int read; +// while ((read = is.read(buffer)) > 0) { +// digest.update(buffer, 0, read); +// } +// } finally { +// tempFile.delete(); +// } +// } else { + try (InputStream is = new FileInputStream(fileEntity.getFilePath())) { + 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 imageProjectionHash64Hi = similarImageProjection.getHash64Hi(); @@ -198,4 +246,17 @@ public class FileSimilarityService { .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId()) .build(); } + + private SimilarFileDTO buildDTO(FileEntity fileEntity) { + return SimilarFileDTO.builder() + .fileId(fileEntity.getId()) + .ownerId(fileEntity.getUserId()) + .originalFileName(fileEntity.getOriginalFileName()) + .fileSize(fileEntity.getFileSize()) + .supportId(Long.valueOf(fileEntity.getSupportId())) + .uploadDate(fileEntity.getCreatedAt()) + .status(fileEntity.getProtectionStatus()) + .url(baseUrl + "/api/files/protected/" + fileEntity.getId()) + .build(); + } } From e53c17976f8d8544aacdbc86c44af217809571a4 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 16:44:16 +0700 Subject: [PATCH 010/274] dev add check hash for all types,exclude video --- src/main/java/ru/soune/nocopy/controller/ApiController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 3b35a3c..18accc7 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -165,7 +165,7 @@ public class ApiController { @PathVariable String fileId, @RequestParam(value = "auth_token",required = false) String authToken, @RequestParam(required = false) List similarityLevels, - @PageableDefault(size = 20, sort = "hammingDistance") Pageable pageable) { + @PageableDefault(size = 20, sort = "hammingDistance") Pageable pageable) throws Exception { SimilarityFilter filter = SimilarityFilter.builder() .similarityLevels(similarityLevels) .build(); From 1801b7e8fd9b663861037d066b97e5d33a626af3 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 16:51:12 +0700 Subject: [PATCH 011/274] dev add check hash for all types,exclude video --- .../java/ru/soune/nocopy/service/FileSimilarityService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 5b4c8f5..9eb5eaa 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -253,7 +253,7 @@ public class FileSimilarityService { .ownerId(fileEntity.getUserId()) .originalFileName(fileEntity.getOriginalFileName()) .fileSize(fileEntity.getFileSize()) - .supportId(Long.valueOf(fileEntity.getSupportId())) +// .supportId(Long.valueOf(fileEntity.getSupportId())) .uploadDate(fileEntity.getCreatedAt()) .status(fileEntity.getProtectionStatus()) .url(baseUrl + "/api/files/protected/" + fileEntity.getId()) From 1d3bdc506d1dad5a03b38091c1a2c5e52f2abc93 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 18:16:21 +0700 Subject: [PATCH 012/274] dev delete config --- .../configuration/file/FileStorageConfig.java | 25 ------------------- .../service/file/FileStorageService.java | 12 +++++---- .../file/impl/FileUploadServiceImpl.java | 3 --- 3 files changed, 7 insertions(+), 33 deletions(-) delete mode 100644 src/main/java/ru/soune/nocopy/configuration/file/FileStorageConfig.java diff --git a/src/main/java/ru/soune/nocopy/configuration/file/FileStorageConfig.java b/src/main/java/ru/soune/nocopy/configuration/file/FileStorageConfig.java deleted file mode 100644 index c64ba5c..0000000 --- a/src/main/java/ru/soune/nocopy/configuration/file/FileStorageConfig.java +++ /dev/null @@ -1,25 +0,0 @@ -package ru.soune.nocopy.configuration.file; - -import jakarta.annotation.PostConstruct; -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; - -@Configuration -@ConfigurationProperties(prefix = "file.storage") -@Data -public class FileStorageConfig { - private String basePath; - private long maxFileSize; - private int chunkSize; - private int authTokenLifeHours; - - @PostConstruct - public void init() throws IOException { - Files.createDirectories(Paths.get(basePath)); - } -} diff --git a/src/main/java/ru/soune/nocopy/service/file/FileStorageService.java b/src/main/java/ru/soune/nocopy/service/file/FileStorageService.java index a7d0603..421f577 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileStorageService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileStorageService.java @@ -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,14 @@ import java.nio.file.Paths; @Slf4j public class FileStorageService { - @Autowired - private FileStorageConfig storageConfig; +// @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()) { diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index 9395adc..77a0634 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -57,9 +57,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; From 3c00a5bd2c2e03151ec70b9d4284489b51740319 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 18:35:27 +0700 Subject: [PATCH 013/274] dev delete config --- .../java/ru/soune/nocopy/service/FileSimilarityService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 9eb5eaa..c95e6ed 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -256,7 +256,7 @@ public class FileSimilarityService { // .supportId(Long.valueOf(fileEntity.getSupportId())) .uploadDate(fileEntity.getCreatedAt()) .status(fileEntity.getProtectionStatus()) - .url(baseUrl + "/api/files/protected/" + fileEntity.getId()) + .url(baseUrl + "/api/files/public/" + fileEntity.getId()) .build(); } } From e05bf7ef3ae8d58ea6a59b2e4b38eec52d16b081 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 18:57:45 +0700 Subject: [PATCH 014/274] dev delete config --- .../java/ru/soune/nocopy/controller/FileController.java | 8 +++++++- .../ru/soune/nocopy/service/FileSimilarityService.java | 2 -- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/FileController.java b/src/main/java/ru/soune/nocopy/controller/FileController.java index bec838c..9b187b6 100644 --- a/src/main/java/ru/soune/nocopy/controller/FileController.java +++ b/src/main/java/ru/soune/nocopy/controller/FileController.java @@ -17,6 +17,8 @@ import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.file.FileStorageService; import org.springframework.http.ContentDisposition; + +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.io.IOException; @@ -71,10 +73,14 @@ public class FileController { throw new RuntimeException("File is not protected"); } + String fileName = fileEntity.getOriginalFileName(); + String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8) + .replace("+", "%20"); + return ResponseEntity.ok() .contentType(new MediaType(fileEntity.getMimeType(), fileEntity.getFileExtension())) .header(HttpHeaders.CONTENT_DISPOSITION, - "inline; filename=\"" + fileEntity.getOriginalFileName() + "\"") + "inline; filename*=UTF-8''" + encodedFileName) .body(resource); } diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index c95e6ed..69edde1 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -253,10 +253,8 @@ public class FileSimilarityService { .ownerId(fileEntity.getUserId()) .originalFileName(fileEntity.getOriginalFileName()) .fileSize(fileEntity.getFileSize()) -// .supportId(Long.valueOf(fileEntity.getSupportId())) .uploadDate(fileEntity.getCreatedAt()) .status(fileEntity.getProtectionStatus()) - .url(baseUrl + "/api/files/public/" + fileEntity.getId()) .build(); } } From 796f1de0223c3d6337266c658220cced8d63b45b Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 19:08:49 +0700 Subject: [PATCH 015/274] dev delete config --- .../soune/nocopy/service/search/SearchImageService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index af27d76..867d117 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -32,10 +32,10 @@ public class SearchImageService { public SearchImageService() { this.httpClient = new OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .writeTimeout(10, TimeUnit.SECONDS) - .readTimeout(35, TimeUnit.SECONDS) - .callTimeout(40, TimeUnit.SECONDS) + .connectTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .readTimeout(120, TimeUnit.SECONDS) + .callTimeout(180, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .build(); From d4f9397dff1f2820b2d9c60518e8ead090c40ae6 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 19:23:22 +0700 Subject: [PATCH 016/274] dev delete config --- src/main/resources/application.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index e5abeb3..cddeb0d 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -102,7 +102,7 @@ yandex: search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image} searchapi: - api-key: ${SEARCHAPI_API_KEY:rdwNzCr2eybYVbU1KrGTJAgm} + api-key: ${SEARCHAPI_API_KEY:qM4tWwZZkxgNcMQV3AJtmsBY} reverse-image-url: "https://searchapi.io/api/v1/search" app: From ba802ca53f80ae6a9e613f850a5c434df248247f Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 19:43:12 +0700 Subject: [PATCH 017/274] dev delete config --- .../nocopy/handler/ImageFoundRequestHandler.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 2d1f5f0..6707450 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -61,11 +61,19 @@ public class ImageFoundRequestHandler implements RequestHandler { String searchResponseYandex; String searchResponseGoogle; + List allUniqueImages; try { searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", "visual_matches"); + List allYandexImages = searchImageService.getAllImagesWithoutPagination( + searchResponseYandex, "visual_matches"); searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", "exact_matches"); + + List allGoogleImages = searchImageService.getAllImagesWithoutPagination( + searchResponseGoogle, "exact_matches"); + allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, + allGoogleImages); } catch (TimeoutException e) { log.warn("Search timeout for file {}, returning empty results", fileId); return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), @@ -85,14 +93,6 @@ public class ImageFoundRequestHandler implements RequestHandler { checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); - List allYandexImages = searchImageService.getAllImagesWithoutPagination( - searchResponseYandex, "visual_matches"); - List allGoogleImages = searchImageService.getAllImagesWithoutPagination( - searchResponseGoogle, "exact_matches"); - - List allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, - allGoogleImages); - int page = imageSearchRequest.getPage() != null ? imageSearchRequest.getPage() : 1; int pageSize = 5; From a431784baa33f0b06ea2ab32eb9d6fc9e9a23ae7 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 19:51:38 +0700 Subject: [PATCH 018/274] dev delete config --- .../handler/ImageFoundRequestHandler.java | 22 +++++++++++++++++++ .../service/search/SearchImageService.java | 1 - 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 6707450..7f9eaef 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -65,15 +65,37 @@ public class ImageFoundRequestHandler implements RequestHandler { try { searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", "visual_matches"); + log.info("searchResponseYandex OK"); + log.info("searchResponseYandex OK"); + log.info("searchResponseYandex OK"); + log.info("searchResponseYandex OK"); + log.info("searchResponseYandex OK"); + log.info("searchResponseYandex OK"); + log.info("searchResponseYandex OK"); + log.info("searchResponseYandex OK"); + log.info("searchResponseYandex OK"); List allYandexImages = searchImageService.getAllImagesWithoutPagination( searchResponseYandex, "visual_matches"); searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", "exact_matches"); + log.info("searchResponseGoogle OK"); + log.info("searchResponseGoogle OK"); + log.info("searchResponseGoogle OK"); + log.info("searchResponseGoogle OK"); + log.info("searchResponseGoogle OK"); + log.info("searchResponseGoogle OK"); + log.info("searchResponseGoogle OK"); List allGoogleImages = searchImageService.getAllImagesWithoutPagination( searchResponseGoogle, "exact_matches"); allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); + log.info("allUniqueImages OK"); + log.info("allUniqueImages OK"); + log.info("allUniqueImages OK"); + log.info("allUniqueImages OK"); + log.info("allUniqueImages OK"); + log.info("allUniqueImages OK"); } catch (TimeoutException e) { log.warn("Search timeout for file {}, returning empty results", fileId); return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 867d117..925d1e8 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -107,7 +107,6 @@ public class SearchImageService { .addQueryParameter("api_key", searchApiKey) .addQueryParameter("url", imageUrl) .addQueryParameter("search_type", searchType) - .addQueryParameter("wait", "true") .build(); Request request = new Request.Builder() From 5f588fcb30e4f8b9a63b86d235fc425e41e70415 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 20:11:06 +0700 Subject: [PATCH 019/274] dev delete config --- .../handler/ImageFoundRequestHandler.java | 22 +-------- .../nocopy/service/FileSimilarityService.java | 46 +++++++++---------- .../file/impl/FileUploadServiceImpl.java | 5 +- 3 files changed, 29 insertions(+), 44 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 7f9eaef..3c15c4d 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -66,36 +66,18 @@ public class ImageFoundRequestHandler implements RequestHandler { searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", "visual_matches"); log.info("searchResponseYandex OK"); - log.info("searchResponseYandex OK"); - log.info("searchResponseYandex OK"); - log.info("searchResponseYandex OK"); - log.info("searchResponseYandex OK"); - log.info("searchResponseYandex OK"); - log.info("searchResponseYandex OK"); - log.info("searchResponseYandex OK"); - log.info("searchResponseYandex OK"); List allYandexImages = searchImageService.getAllImagesWithoutPagination( searchResponseYandex, "visual_matches"); searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", "exact_matches"); - log.info("searchResponseGoogle OK"); - log.info("searchResponseGoogle OK"); - log.info("searchResponseGoogle OK"); - log.info("searchResponseGoogle OK"); - log.info("searchResponseGoogle OK"); - log.info("searchResponseGoogle OK"); - log.info("searchResponseGoogle OK"); + log.info("searchResponseGoogle OK"); List allGoogleImages = searchImageService.getAllImagesWithoutPagination( searchResponseGoogle, "exact_matches"); allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); log.info("allUniqueImages OK"); - log.info("allUniqueImages OK"); - log.info("allUniqueImages OK"); - log.info("allUniqueImages OK"); - log.info("allUniqueImages OK"); - log.info("allUniqueImages OK"); + } catch (TimeoutException e) { log.warn("Search timeout for file {}, returning empty results", fileId); return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 69edde1..b110bfc 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -35,6 +35,8 @@ public class FileSimilarityService { private final AuthTokenRepository authTokenRepository; + private final FileEntityRepository fileEntityRepository; + @Value("${server.baseurl}") private String baseUrl; @@ -138,7 +140,18 @@ public class FileSimilarityService { return duplicates; } - private final FileEntityRepository fileEntityRepository; + public boolean hasDuplicatesByHash(String path, Long userId,String mimeType) throws Exception { + String hash = calculateFileHash(path); + List fileEntityList = fileEntityRepository.findByUserIdAndMimeType(userId, mimeType); + + for (FileEntity file : fileEntityList) { + if (calculateFileHash(file.getFilePath()).equals(hash)) { + return true; + } + } + + return false; + } public Page findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable, String authToken) throws Exception { @@ -165,12 +178,12 @@ public class FileSimilarityService { .sorted(Comparator.comparingInt(SimilarFileDTO::getHammingDistance)) .collect(Collectors.toList()); } else { - String hash = calculateFileHash(fileEntity); + String hash = calculateFileHash(fileEntity.getFilePath()); Long userId = fileEntity.getUserId(); List fileEntityList = fileEntityRepository.findByUserIdAndMimeType(userId, mimeType); for (FileEntity file : fileEntityList) { - if (calculateFileHash(file).equals(hash)) { + if (calculateFileHash(file.getFilePath()).equals(hash)) { allResults.add(buildDTO(file)); } } @@ -189,28 +202,15 @@ public class FileSimilarityService { } - private String calculateFileHash(FileEntity fileEntity) throws Exception { + private String calculateFileHash(String path) throws Exception { MessageDigest digest = MessageDigest.getInstance("SHA-256"); -// if (fileEntity.getStatus() == FileStatus.CLOUD) { -// File tempFile = cloudStorageService.readFileFromStorageByPath(fileEntity.getFilePath()); -// try (InputStream is = new FileInputStream(tempFile)) { -// byte[] buffer = new byte[8192]; -// int read; -// while ((read = is.read(buffer)) > 0) { -// digest.update(buffer, 0, read); -// } -// } finally { -// tempFile.delete(); -// } -// } else { - try (InputStream is = new FileInputStream(fileEntity.getFilePath())) { - byte[] buffer = new byte[8192]; - int read; - while ((read = is.read(buffer)) > 0) { - digest.update(buffer, 0, read); - } -// } + 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(); diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index 77a0634..5b33c53 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -315,8 +315,11 @@ public class FileUploadServiceImpl implements FileUploadService { String finalFilePath = assembleFileSynchronously(session); - if (!session.getFileType().startsWith("video") && findSimilar == 0) { + if (session.getFileType().startsWith("image") && findSimilar == 0) { checkForDuplicatesSynchronously(finalFilePath); + } else if (session.getFileType().startsWith("audio") || session.getFileType().startsWith("document") + && findSimilar == 0) { + fileSimilarityService.hasDuplicatesByHash(finalFilePath, session.getUserId(), session.getFileType()); } session.setStatus(UploadStatus.COMPLETED); From 919baa418eb64c38aa8f2415f01d170e5b42b7cd Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 20:16:50 +0700 Subject: [PATCH 020/274] dev delete config --- .../java/ru/soune/nocopy/service/FileSimilarityService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index b110bfc..5bb9ea5 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -13,6 +13,7 @@ import ru.soune.nocopy.dto.file.SimilarFileDTO; import ru.soune.nocopy.dto.file.SimilarityFilter; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.ImageHashEntity; +import ru.soune.nocopy.exception.DuplicateImageException; import ru.soune.nocopy.repository.*; import ru.soune.nocopy.util.FileUtil; @@ -146,7 +147,8 @@ public class FileSimilarityService { for (FileEntity file : fileEntityList) { if (calculateFileHash(file.getFilePath()).equals(hash)) { - return true; + throw new DuplicateImageException("Duplicate", file.getId(), + file.getUserId()); } } From fe710c824d9b087706601b459e6769e48c7c7e4c Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 20:17:57 +0700 Subject: [PATCH 021/274] dev delete config --- .../java/ru/soune/nocopy/service/FileSimilarityService.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 5bb9ea5..c5a4bde 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -141,7 +141,7 @@ public class FileSimilarityService { return duplicates; } - public boolean hasDuplicatesByHash(String path, Long userId,String mimeType) throws Exception { + public void hasDuplicatesByHash(String path, Long userId, String mimeType) throws Exception { String hash = calculateFileHash(path); List fileEntityList = fileEntityRepository.findByUserIdAndMimeType(userId, mimeType); @@ -151,8 +151,6 @@ public class FileSimilarityService { file.getUserId()); } } - - return false; } public Page findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable, From 45267158d8e058ea9e036f467a31863dd48a8556 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 20:31:51 +0700 Subject: [PATCH 022/274] dev delete config --- .../service/file/impl/FileUploadServiceImpl.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index 5b33c53..9f81d11 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -315,11 +315,13 @@ public class FileUploadServiceImpl implements FileUploadService { String finalFilePath = assembleFileSynchronously(session); - if (session.getFileType().startsWith("image") && findSimilar == 0) { - checkForDuplicatesSynchronously(finalFilePath); - } else if (session.getFileType().startsWith("audio") || session.getFileType().startsWith("document") - && findSimilar == 0) { - fileSimilarityService.hasDuplicatesByHash(finalFilePath, session.getUserId(), session.getFileType()); + if (findSimilar == 0) { + if (session.getFileType().startsWith("image")) { + checkForDuplicatesSynchronously(finalFilePath); + } else if (session.getFileType().startsWith("audio") || session.getFileType().startsWith("document")) { + fileSimilarityService.hasDuplicatesByHash(finalFilePath, session.getUserId(), + session.getFileType()); + } } session.setStatus(UploadStatus.COMPLETED); From 01b0c12eca156684f9da6a0a6131ba3aafb016a3 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Feb 2026 20:44:14 +0700 Subject: [PATCH 023/274] dev delete config --- .../java/ru/soune/nocopy/service/FileSimilarityService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index c5a4bde..14f55ef 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -12,6 +12,7 @@ import org.springframework.stereotype.Service; import ru.soune.nocopy.dto.file.SimilarFileDTO; import ru.soune.nocopy.dto.file.SimilarityFilter; 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.exception.DuplicateImageException; import ru.soune.nocopy.repository.*; @@ -183,7 +184,7 @@ public class FileSimilarityService { List fileEntityList = fileEntityRepository.findByUserIdAndMimeType(userId, mimeType); for (FileEntity file : fileEntityList) { - if (calculateFileHash(file.getFilePath()).equals(hash)) { + if (calculateFileHash(file.getFilePath()).equals(hash) && !file.getStatus().equals(FileStatus.TEMP)) { allResults.add(buildDTO(file)); } } From e416835c51633cfc71179f95c3f7946708ac1b1c Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 10:54:51 +0700 Subject: [PATCH 024/274] dev delete config --- src/main/java/ru/soune/nocopy/controller/FileController.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/controller/FileController.java b/src/main/java/ru/soune/nocopy/controller/FileController.java index 9b187b6..4ebdb10 100644 --- a/src/main/java/ru/soune/nocopy/controller/FileController.java +++ b/src/main/java/ru/soune/nocopy/controller/FileController.java @@ -73,7 +73,9 @@ public class FileController { throw new RuntimeException("File is not protected"); } - String fileName = fileEntity.getOriginalFileName(); + String fileName = fileEntity.getOriginalFileName().replace("." + + fileEntity.getFileExtension(), "") + "_nocopy_protected" + + "." + fileEntity.getFileExtension(); String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8) .replace("+", "%20"); From 60f78599fa21afa02a907d6662975fd4ad589d97 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 11:11:51 +0700 Subject: [PATCH 025/274] dev delete config --- .../nocopy/repository/ImageSimilarityRepository.java | 3 +++ .../soune/nocopy/repository/SimilarImageProjection.java | 1 + .../ru/soune/nocopy/service/FileSimilarityService.java | 8 ++++++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java index add9315..0ae7006 100644 --- a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java @@ -20,6 +20,7 @@ public interface ImageSimilarityRepository f.support_id AS supportId, f.created_at AS uploadDate, f.protection_status AS protectionStatus, + f.file_extension AS extension, h.hash64_hi AS hash64Hi, h.hash64_lo AS hash64Lo FROM image_hashes ref @@ -43,6 +44,7 @@ public interface ImageSimilarityRepository f.support_id AS supportId, f.created_at AS uploadDate, f.protection_status AS protectionStatus, + f.file_extension AS extension, h.hash64_hi AS hash64Hi, h.hash64_lo AS hash64Lo FROM image_hashes ref @@ -70,6 +72,7 @@ public interface ImageSimilarityRepository f.stored_file_name AS similarFileId, f.support_id AS supportId, f.created_at AS uploadDate, + f.file_extension AS extension, f.protection_status AS protectionStatus FROM image_hashes h JOIN file_entities f ON f.id = h.file_id diff --git a/src/main/java/ru/soune/nocopy/repository/SimilarImageProjection.java b/src/main/java/ru/soune/nocopy/repository/SimilarImageProjection.java index 9b9c8e7..4d3f47f 100644 --- a/src/main/java/ru/soune/nocopy/repository/SimilarImageProjection.java +++ b/src/main/java/ru/soune/nocopy/repository/SimilarImageProjection.java @@ -14,4 +14,5 @@ public interface SimilarImageProjection { Long getSupportId(); LocalDateTime getUploadDate(); ProtectionStatus getProtectionStatus(); + String getExtension(); } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 14f55ef..c479ffd 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -237,7 +237,9 @@ public class FileSimilarityService { 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) @@ -252,7 +254,9 @@ public class FileSimilarityService { return SimilarFileDTO.builder() .fileId(fileEntity.getId()) .ownerId(fileEntity.getUserId()) - .originalFileName(fileEntity.getOriginalFileName()) + .originalFileName(fileEntity.getOriginalFileName().replace("." + + fileEntity.getFileExtension(), "") + "_nocopy_protected" + + "." + fileEntity.getFileExtension()) .fileSize(fileEntity.getFileSize()) .uploadDate(fileEntity.getCreatedAt()) .status(fileEntity.getProtectionStatus()) From 27b0aaaf70563e7fa67265d8a6a4ed8389fb2a6c Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 12:46:54 +0700 Subject: [PATCH 026/274] dev delete config --- .../nocopy/service/FileSimilarityService.java | 76 ++++++++++++++++++- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index c479ffd..e27d477 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -1,5 +1,10 @@ package ru.soune.nocopy.service; +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 com.vrt.fileprotection.image.phash.PHash; import com.vrt.fileprotection.image.phash.PerceptualHashHelper; import lombok.RequiredArgsConstructor; @@ -12,7 +17,6 @@ import org.springframework.stereotype.Service; import ru.soune.nocopy.dto.file.SimilarFileDTO; import ru.soune.nocopy.dto.file.SimilarityFilter; 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.exception.DuplicateImageException; import ru.soune.nocopy.repository.*; @@ -20,6 +24,8 @@ import ru.soune.nocopy.util.FileUtil; import java.io.FileInputStream; import java.io.InputStream; +import java.nio.file.Path; +import java.nio.file.Paths; import java.security.MessageDigest; import java.util.*; import java.util.stream.Collectors; @@ -154,6 +160,8 @@ public class FileSimilarityService { } } + private NoCopyFileService noCopyFileService; + public Page findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable, String authToken) throws Exception { FileEntity fileEntity = fileEntityRepository.findByFileId(fileId); @@ -179,13 +187,73 @@ public class FileSimilarityService { .sorted(Comparator.comparingInt(SimilarFileDTO::getHammingDistance)) .collect(Collectors.toList()); } else { - String hash = calculateFileHash(fileEntity.getFilePath()); +// Long userId = fileEntity.getUserId(); +// List fileEntityList = fileEntityRepository.findByUserIdAndMimeType(userId, mimeType); +// NoCopyCheckResult noCopyCheckResult; +// +// for (FileEntity file : fileEntityList) { +// Path path = Paths.get(file.getFilePath()); +// if (mimeType.equals("document")) { +// noCopyCheckResult = (DocumentCheckResult) noCopyFileService.checkFile( +// path.toFile(), FileProtector.Type.valueOf(file.getMimeType().toUpperCase())); +// } else { +// noCopyCheckResult = (AudioCheckResult) noCopyFileService.checkFile( +// path.toFile(), FileProtector.Type.valueOf(file.getMimeType().toUpperCase())); +// } +// +// switch (noCopyCheckResult) { +// case DocumentCheckResult.Success success -> { +// FileProtector.FileInfo info = success.getInfo(); +// allResults.add(buildDTO(file)); +// } +// case DocumentCheckResult.Failed failed -> { +// String message = failed.getMessage(); +// } +// default -> throw new IllegalStateException("Unexpected result"); +// } +// } + Long userId = fileEntity.getUserId(); List fileEntityList = fileEntityRepository.findByUserIdAndMimeType(userId, mimeType); + List errors = new ArrayList<>(); for (FileEntity file : fileEntityList) { - if (calculateFileHash(file.getFilePath()).equals(hash) && !file.getStatus().equals(FileStatus.TEMP)) { - allResults.add(buildDTO(file)); + try { + Path path = Paths.get(file.getFilePath()); + FileProtector.Type fileType = FileProtector.Type.valueOf(file.getMimeType().toUpperCase()); + + NoCopyCheckResult noCopyCheckResult = noCopyFileService.checkFile(path.toFile(), fileType); + + switch (noCopyCheckResult) { + case DocumentCheckResult.Success success -> { + FileProtector.FileInfo info = success.getInfo(); + allResults.add(buildDTO(file)); + log.debug("File processed successfully: {}", file.getFilePath()); + } + case AudioCheckResult.Success success -> { + FileProtector.FileInfo info = success.getInfo(); + allResults.add(buildDTO(file)); + log.debug("File processed successfully: {}", file.getFilePath()); + } + case DocumentCheckResult.Failed failed -> { + String errorMessage = String.format("Document check failed for %s: %s", + file.getFilePath(), failed.getMessage()); + log.warn(errorMessage); + errors.add(errorMessage); + } + case AudioCheckResult.Failed failed -> { + String errorMessage = String.format("Audio check failed for %s: %s", + file.getFilePath(), failed.getMessage()); + log.warn(errorMessage); + errors.add(errorMessage); + } + default -> throw new IllegalStateException("Unexpected result type: " + noCopyCheckResult.getClass().getSimpleName()); + } + } catch (Exception e) { + String errorMessage = String.format("Error processing file %s: %s", + file.getFilePath(), e.getMessage()); + log.error(errorMessage, e); + errors.add(errorMessage); } } } From fba974f91b89d6ea82cde404ddb375184c297f32 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 12:55:25 +0700 Subject: [PATCH 027/274] dev delete config --- .../java/ru/soune/nocopy/service/FileSimilarityService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index e27d477..39b03eb 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -9,6 +9,7 @@ import com.vrt.fileprotection.image.phash.PHash; import com.vrt.fileprotection.image.phash.PerceptualHashHelper; 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.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -160,6 +161,7 @@ public class FileSimilarityService { } } + @Autowired private NoCopyFileService noCopyFileService; public Page findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable, @@ -220,7 +222,9 @@ public class FileSimilarityService { for (FileEntity file : fileEntityList) { try { Path path = Paths.get(file.getFilePath()); - FileProtector.Type fileType = FileProtector.Type.valueOf(file.getMimeType().toUpperCase()); + + FileProtector.Type fileType = file.getMimeType().equals("document") ? FileProtector.Type.DOC: + FileProtector.Type.AUDIO; NoCopyCheckResult noCopyCheckResult = noCopyFileService.checkFile(path.toFile(), fileType); From 73d010b426e97be2831491316213d605f81dabca Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 13:00:32 +0700 Subject: [PATCH 028/274] dev delete config --- .../java/ru/soune/nocopy/service/FileSimilarityService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 39b03eb..e12dfb6 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -9,6 +9,7 @@ import com.vrt.fileprotection.image.phash.PHash; import com.vrt.fileprotection.image.phash.PerceptualHashHelper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.internal.util.stereotypes.Lazy; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; @@ -162,6 +163,7 @@ public class FileSimilarityService { } @Autowired + @Lazy private NoCopyFileService noCopyFileService; public Page findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable, From 31b41dc3d75c3212f45ca6ba2fb5a47ad5663afd Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 13:03:47 +0700 Subject: [PATCH 029/274] dev delete config --- .../soune/nocopy/service/FileSimilarityService.java | 6 +----- src/main/java/ru/soune/nocopy/util/FileUtil.java | 13 +++++++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index e12dfb6..a8be0cb 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -162,10 +162,6 @@ public class FileSimilarityService { } } - @Autowired - @Lazy - private NoCopyFileService noCopyFileService; - public Page findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable, String authToken) throws Exception { FileEntity fileEntity = fileEntityRepository.findByFileId(fileId); @@ -228,7 +224,7 @@ public class FileSimilarityService { FileProtector.Type fileType = file.getMimeType().equals("document") ? FileProtector.Type.DOC: FileProtector.Type.AUDIO; - NoCopyCheckResult noCopyCheckResult = noCopyFileService.checkFile(path.toFile(), fileType); + NoCopyCheckResult noCopyCheckResult = fileUtil.checkResult(path, fileType); switch (noCopyCheckResult) { case DocumentCheckResult.Success success -> { diff --git a/src/main/java/ru/soune/nocopy/util/FileUtil.java b/src/main/java/ru/soune/nocopy/util/FileUtil.java index 7bfb07b..46c3924 100644 --- a/src/main/java/ru/soune/nocopy/util/FileUtil.java +++ b/src/main/java/ru/soune/nocopy/util/FileUtil.java @@ -1,14 +1,23 @@ package ru.soune.nocopy.util; +import com.vrt.NoCopyFileService; import com.vrt.fileprotection.FileProtector; +import com.vrt.fileprotection.NoCopyCheckResult; import com.vrt.fileprotection.image.ImageLocalSearch; import com.vrt.fileprotection.image.ImageScore; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import ru.soune.nocopy.dto.file.SimilarFileDTO; import ru.soune.nocopy.entity.file.FileEntity; +import java.nio.file.Path; + @Service public class FileUtil { + + @Autowired + private NoCopyFileService noCopyFileService; + public ImageLocalSearch.Result convertToResult(SimilarFileDTO response) { FileProtector.FileInfo fileInfo = new FileProtector.FileInfo(FileProtector.Type.IMAGE, response.getFileId(), @@ -30,6 +39,10 @@ public class FileUtil { + Long.bitCount(aLo ^ bLo); } + public NoCopyCheckResult checkResult(Path path, FileProtector.Type fileType) { + return noCopyFileService.checkFile(path.toFile(), fileType); + } + private FileProtector.Type determineFileType(String mimeType) { if (mimeType == null) { return FileProtector.Type.IMAGE; From a132ac97f20b2ca542fc8e4949314d2a1e4b5c3e Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 13:09:56 +0700 Subject: [PATCH 030/274] dev delete config --- src/main/java/ru/soune/nocopy/util/FileUtil.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/ru/soune/nocopy/util/FileUtil.java b/src/main/java/ru/soune/nocopy/util/FileUtil.java index 46c3924..6c3d064 100644 --- a/src/main/java/ru/soune/nocopy/util/FileUtil.java +++ b/src/main/java/ru/soune/nocopy/util/FileUtil.java @@ -6,6 +6,7 @@ import com.vrt.fileprotection.NoCopyCheckResult; import com.vrt.fileprotection.image.ImageLocalSearch; import com.vrt.fileprotection.image.ImageScore; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import ru.soune.nocopy.dto.file.SimilarFileDTO; import ru.soune.nocopy.entity.file.FileEntity; @@ -16,6 +17,7 @@ import java.nio.file.Path; public class FileUtil { @Autowired + @Lazy private NoCopyFileService noCopyFileService; public ImageLocalSearch.Result convertToResult(SimilarFileDTO response) { From 6abfb4aa37df6ce6bb7dda6d094f2c78dbb35d84 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 13:15:35 +0700 Subject: [PATCH 031/274] dev delete config --- .../nocopy/service/FileSimilarityService.java | 4 +++- src/main/java/ru/soune/nocopy/util/FileUtil.java | 14 -------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index a8be0cb..53262e1 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -47,6 +47,8 @@ public class FileSimilarityService { private final FileEntityRepository fileEntityRepository; + private final NoCopyFileService noCopyFileService; + @Value("${server.baseurl}") private String baseUrl; @@ -224,7 +226,7 @@ public class FileSimilarityService { FileProtector.Type fileType = file.getMimeType().equals("document") ? FileProtector.Type.DOC: FileProtector.Type.AUDIO; - NoCopyCheckResult noCopyCheckResult = fileUtil.checkResult(path, fileType); + NoCopyCheckResult noCopyCheckResult = noCopyFileService.checkFile(path.toFile(), fileType); switch (noCopyCheckResult) { case DocumentCheckResult.Success success -> { diff --git a/src/main/java/ru/soune/nocopy/util/FileUtil.java b/src/main/java/ru/soune/nocopy/util/FileUtil.java index 6c3d064..5e7b2bd 100644 --- a/src/main/java/ru/soune/nocopy/util/FileUtil.java +++ b/src/main/java/ru/soune/nocopy/util/FileUtil.java @@ -1,25 +1,15 @@ package ru.soune.nocopy.util; -import com.vrt.NoCopyFileService; import com.vrt.fileprotection.FileProtector; -import com.vrt.fileprotection.NoCopyCheckResult; import com.vrt.fileprotection.image.ImageLocalSearch; import com.vrt.fileprotection.image.ImageScore; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import ru.soune.nocopy.dto.file.SimilarFileDTO; import ru.soune.nocopy.entity.file.FileEntity; -import java.nio.file.Path; - @Service public class FileUtil { - @Autowired - @Lazy - private NoCopyFileService noCopyFileService; - public ImageLocalSearch.Result convertToResult(SimilarFileDTO response) { FileProtector.FileInfo fileInfo = new FileProtector.FileInfo(FileProtector.Type.IMAGE, response.getFileId(), @@ -41,10 +31,6 @@ public class FileUtil { + Long.bitCount(aLo ^ bLo); } - public NoCopyCheckResult checkResult(Path path, FileProtector.Type fileType) { - return noCopyFileService.checkFile(path.toFile(), fileType); - } - private FileProtector.Type determineFileType(String mimeType) { if (mimeType == null) { return FileProtector.Type.IMAGE; From 633f76cd73ac65120503aaf0fb0135470575b686 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 13:20:37 +0700 Subject: [PATCH 032/274] dev delete config --- src/main/java/ru/soune/nocopy/service/FileSimilarityService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 53262e1..911753d 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -47,6 +47,7 @@ public class FileSimilarityService { private final FileEntityRepository fileEntityRepository; + @Lazy private final NoCopyFileService noCopyFileService; @Value("${server.baseurl}") From db02f23c6759f2e208bd20a042f67b85bc429644 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 13:24:11 +0700 Subject: [PATCH 033/274] dev delete config --- src/main/java/ru/soune/nocopy/controller/ApiController.java | 2 ++ .../soune/nocopy/service/file/FileProcessingOrchestrator.java | 2 ++ .../soune/nocopy/service/file/impl/FileUploadServiceImpl.java | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 18accc7..09b6243 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -5,6 +5,7 @@ import com.vrt.fileprotection.FileProtector; import com.vrt.fileprotection.NoCopyCheckResult; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; import org.springframework.core.io.FileSystemResource; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -65,6 +66,7 @@ public class ApiController { private final FileEntityRepository fileEntityRepository; + @Lazy private final NoCopyFileService noCopyFileService; private final FileUtil fileUtil; diff --git a/src/main/java/ru/soune/nocopy/service/file/FileProcessingOrchestrator.java b/src/main/java/ru/soune/nocopy/service/file/FileProcessingOrchestrator.java index 5c4df44..7df7418 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileProcessingOrchestrator.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileProcessingOrchestrator.java @@ -4,6 +4,7 @@ import com.vrt.NoCopyFileService; import com.vrt.fileprotection.FileProtector; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import ru.soune.nocopy.entity.file.FileEntity; @@ -18,6 +19,7 @@ import java.util.List; @RequiredArgsConstructor public class FileProcessingOrchestrator { + @Lazy private final NoCopyFileService noCopyFileService; private final FileEntityRepository fileRepository; diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index 9f81d11..1b31859 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -7,6 +7,7 @@ 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; @@ -74,6 +75,7 @@ public class FileUploadServiceImpl implements FileUploadService { @Autowired private FileEntityService fileEntityService; + @Lazy private final NoCopyFileService noCopyFileService; private final FileUtil fileUtil; From 8147a6ce49a6ebcb1d33461b281a73fb84ee93a1 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 13:28:04 +0700 Subject: [PATCH 034/274] dev delete config --- .../nocopy/controller/ApiController.java | 85 +++++++++---------- .../nocopy/service/FileSimilarityService.java | 1 - 2 files changed, 38 insertions(+), 48 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 09b6243..29435a6 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -1,11 +1,7 @@ package ru.soune.nocopy.controller; -import com.vrt.NoCopyFileService; -import com.vrt.fileprotection.FileProtector; -import com.vrt.fileprotection.NoCopyCheckResult; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Lazy; import org.springframework.core.io.FileSystemResource; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -24,7 +20,6 @@ import ru.soune.nocopy.dto.file.*; import ru.soune.nocopy.dto.register.RegAnswer; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileStatus; -import ru.soune.nocopy.entity.file.ProtectionStatus; import ru.soune.nocopy.entity.file.UploadStatus; import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.exception.*; @@ -38,7 +33,6 @@ import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileUploadService; import ru.soune.nocopy.util.FileUtil; -import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -66,9 +60,6 @@ public class ApiController { private final FileEntityRepository fileEntityRepository; - @Lazy - private final NoCopyFileService noCopyFileService; - private final FileUtil fileUtil; private final ProtectionsLimitService protectionsLimitService; @@ -386,44 +377,44 @@ public class ApiController { } } - @GetMapping("/protect/{fileId}") - public ResponseEntity protect( @PathVariable(required = false) String fileId) { - Optional optionalFileEntity = fileEntityRepository.findById(fileId); - - if (!optionalFileEntity.isPresent()) { - return ResponseEntity.notFound().build(); - } - FileEntity fileEntity = optionalFileEntity.get(); - FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity); - - noCopyFileService.addFile(fileInfo); - - fileEntity.setProtectionStatus(ProtectionStatus.PROCESSING); - fileEntityRepository.save(fileEntity); - - return ResponseEntity.ok().build(); - } - - @GetMapping("/check/{fileId}/{type}") - public ResponseEntity check(@PathVariable(required = false) String fileId, - @PathVariable(required = false) String type) { - Optional optionalFileEntity = fileEntityRepository.findById(fileId); - - if (!optionalFileEntity.isPresent()) { - return ResponseEntity.notFound().build(); - } - - FileEntity fileEntity = optionalFileEntity.get(); - - Path path = Paths.get(fileEntity.getProtectedFilePath()); - - File file = path.toFile(); - - NoCopyCheckResult noCopyCheckResult = noCopyFileService.checkFile(file, - FileProtector.Type.valueOf(type.toUpperCase())); - - return ResponseEntity.ok().body(noCopyCheckResult); - } +// @GetMapping("/protect/{fileId}") +// public ResponseEntity protect( @PathVariable(required = false) String fileId) { +// Optional optionalFileEntity = fileEntityRepository.findById(fileId); +// +// if (!optionalFileEntity.isPresent()) { +// return ResponseEntity.notFound().build(); +// } +// FileEntity fileEntity = optionalFileEntity.get(); +// FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity); +// +// noCopyFileService.addFile(fileInfo); +// +// fileEntity.setProtectionStatus(ProtectionStatus.PROCESSING); +// fileEntityRepository.save(fileEntity); +// +// return ResponseEntity.ok().build(); +// } +// +// @GetMapping("/check/{fileId}/{type}") +// public ResponseEntity check(@PathVariable(required = false) String fileId, +// @PathVariable(required = false) String type) { +// Optional optionalFileEntity = fileEntityRepository.findById(fileId); +// +// if (!optionalFileEntity.isPresent()) { +// return ResponseEntity.notFound().build(); +// } +// +// FileEntity fileEntity = optionalFileEntity.get(); +// +// Path path = Paths.get(fileEntity.getProtectedFilePath()); +// +// File file = path.toFile(); +// +// NoCopyCheckResult noCopyCheckResult = noCopyFileService.checkFile(file, +// FileProtector.Type.valueOf(type.toUpperCase())); +// +// return ResponseEntity.ok().body(noCopyCheckResult); +// } @GetMapping("/check/file_stats") public ResponseEntity checkFileProtectStats(@RequestHeader("Authorization") String tokenHeader) { diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 911753d..2cf7a5c 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -10,7 +10,6 @@ import com.vrt.fileprotection.image.phash.PerceptualHashHelper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hibernate.validator.internal.util.stereotypes.Lazy; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; From 78698d28733ad7768482319f944456526ca53b28 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 13:57:38 +0700 Subject: [PATCH 035/274] dev delete config --- .../nocopy/controller/ApiController.java | 198 +++++++++++++++--- .../nocopy/service/FileSimilarityService.java | 164 ++++++--------- 2 files changed, 235 insertions(+), 127 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 29435a6..45a0721 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -1,5 +1,10 @@ package ru.soune.nocopy.controller; +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 lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.FileSystemResource; @@ -33,14 +38,12 @@ import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileUploadService; import ru.soune.nocopy.util.FileUtil; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; @Slf4j @@ -152,40 +155,179 @@ public class ApiController { } } + private final NoCopyFileService noCopyFileService; + +// @GetMapping("/v{version}/files/{fileId}/similar") +// public ResponseEntity findSimilarFiles( +// @PathVariable("version") int version, +// @PathVariable String fileId, +// @RequestParam(value = "auth_token",required = false) String authToken, +// @RequestParam(required = false) List similarityLevels, +// @PageableDefault(size = 20, sort = "hammingDistance") Pageable pageable) throws Exception { +// SimilarityFilter filter = SimilarityFilter.builder() +// .similarityLevels(similarityLevels) +// .build(); +// FileEntity fileEntity = fileEntityRepository.findByFileId(fileId); +// Page similarFiles = null; +// List allResults = null; +// List errors = new ArrayList<>(); +// +// if (fileEntity.getMimeType().equals("image")) { +// similarFiles = fileSimilarityService.findSimilarFiles(fileId, filter, pageable, authToken); +// } else { +// File file = new File(fileEntity.getFilePath()); +// FileProtector.Type type = fileEntity.getMimeType().equals("document") ? FileProtector.Type.DOC : +// FileProtector.Type.AUDIO; +// +// NoCopyCheckResult noCopyCheckResult = noCopyFileService.checkFile(file, type); +// FileEntity entity = fileEntityRepository.findByFileId(fileId); +// +// switch (noCopyCheckResult) { +// case DocumentCheckResult.Success success -> { +// FileProtector.FileInfo info = success.getInfo(); +// allResults.add(fileSimilarityService.buildDTO(fileEntityRepository.findByFileId(info.getId()))); +// log.debug("File processed successfully: {}", entity.getFilePath()); +// } +// case AudioCheckResult.Success success -> { +// FileProtector.FileInfo info = success.getInfo(); +// +// allResults.add(fileSimilarityService.buildDTO(fileEntityRepository.findByFileId(info.getId()))); +// log.debug("File processed successfully: {}", entity.getFilePath()); +// } +// case DocumentCheckResult.Failed failed -> { +// String errorMessage = String.format("Document check failed for %s: %s", +// entity.getFilePath(), failed.getMessage()); +// log.warn(errorMessage); +// errors.add(errorMessage); +// } +// case AudioCheckResult.Failed failed -> { +// String errorMessage = String.format("Audio check failed for %s: %s", +// entity.getFilePath(), failed.getMessage()); +// log.warn(errorMessage); +// errors.add(errorMessage); +// } +// default -> throw new IllegalStateException("Unexpected result type: " + +// noCopyCheckResult.getClass().getSimpleName()); +// } +// +// } +// +// +// String messageDesc; +// MessageCode success; +// Map responseData = new HashMap<>(); +// +// assert similarFiles != null; +// if (similarFiles.isEmpty()) { +// messageDesc = MessageCode.FILE_NOT_FOUND.getDescription(); +// success = MessageCode.SUCCESS; +// responseData.put("content", similarFiles.getContent()); +// responseData.put("page", similarFiles.getNumber()); +// responseData.put("size", similarFiles.getSize()); +// responseData.put("totalElements", similarFiles.getTotalElements()); +// responseData.put("totalPages", similarFiles.getTotalPages()); +// responseData.put("hasNext", similarFiles.hasNext()); +// responseData.put("hasPrevious", similarFiles.hasPrevious()); +// } else { +// messageDesc = MessageCode.SIMILAR_FILES_FOUND.getDescription(); +// success = MessageCode.SUCCESS; +// responseData.put("content", allResults); +// } +// +// +// return ResponseEntity.ok() +// .body(new BaseResponse(20004, success.getCode(), messageDesc, responseData)); +// } + @GetMapping("/v{version}/files/{fileId}/similar") public ResponseEntity findSimilarFiles( @PathVariable("version") int version, @PathVariable String fileId, - @RequestParam(value = "auth_token",required = false) String authToken, + @RequestParam(value = "auth_token", required = false) String authToken, @RequestParam(required = false) List similarityLevels, - @PageableDefault(size = 20, sort = "hammingDistance") Pageable pageable) throws Exception { - SimilarityFilter filter = SimilarityFilter.builder() - .similarityLevels(similarityLevels) - .build(); - Page similarFiles = fileSimilarityService.findSimilarFiles(fileId, filter, pageable, authToken); + @PageableDefault(size = 20, sort = "hammingDistance") Pageable pageable) { - String messageDesc; - MessageCode success; + try { + FileEntity fileEntity = fileEntityRepository.findByFileId(fileId); + if (fileEntity == null) { + return ResponseEntity.ok().body(new BaseResponse(0, MessageCode.INVALID_FIELD.getCode(), + MessageCode.INVALID_FIELD.getDescription(), null)); + } - if (similarFiles.isEmpty()) { - messageDesc = MessageCode.FILE_NOT_FOUND.getDescription(); - success = MessageCode.SUCCESS; - } else { - messageDesc = MessageCode.SIMILAR_FILES_FOUND.getDescription(); - success = MessageCode.SUCCESS; + BaseResponse response; + if ("image".equals(fileEntity.getMimeType())) { + Page similarFilesPage = fileSimilarityService.findSimilarFiles( + fileId, similarityLevels, pageable, authToken); + response = buildSuccessResponse(similarFilesPage, MessageCode.SIMILAR_FILES_FOUND); + } else { + List results = processNonImageFile(fileEntity, authToken); + response = buildSuccessResponse(results, MessageCode.SIMILAR_FILES_FOUND); + } + + return ResponseEntity.ok(response); + + } catch (Exception e) { + log.error("Error finding similar files: {}", e.getMessage(), e); + return ResponseEntity.ok().body(new BaseResponse(0, MessageCode.INVALID_FIELD.getCode(), + MessageCode.INVALID_FIELD.getDescription(), null)); + } + } + + private List processNonImageFile(FileEntity fileEntity, String authToken) { + List results = new ArrayList<>(); + + File file = new File(fileEntity.getFilePath()); + FileProtector.Type type = "document".equals(fileEntity.getMimeType()) ? + FileProtector.Type.DOC : FileProtector.Type.AUDIO; + + NoCopyCheckResult checkResult = noCopyFileService.checkFile(file, type); + + switch (checkResult) { + case DocumentCheckResult.Success success -> { + FileProtector.FileInfo info = success.getInfo(); + FileEntity entity = fileEntityRepository.findByFileId(info.getId()); + if (entity != null) { + results.add(fileSimilarityService.buildDTO(entity)); + } + } + case AudioCheckResult.Success success -> { + FileProtector.FileInfo info = success.getInfo(); + FileEntity entity = fileEntityRepository.findByFileId(info.getId()); + if (entity != null) { + results.add(fileSimilarityService.buildDTO(entity)); + } + } + 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()); } - Map responseData = new HashMap<>(); - responseData.put("content", similarFiles.getContent()); - responseData.put("page", similarFiles.getNumber()); - responseData.put("size", similarFiles.getSize()); - responseData.put("totalElements", similarFiles.getTotalElements()); - responseData.put("totalPages", similarFiles.getTotalPages()); - responseData.put("hasNext", similarFiles.hasNext()); - responseData.put("hasPrevious", similarFiles.hasPrevious()); + return results; + } - return ResponseEntity.ok() - .body(new BaseResponse(20004, success.getCode(), messageDesc, responseData)); + private BaseResponse buildSuccessResponse(Page page, MessageCode messageCode) { + Map responseData = new HashMap<>(); + responseData.put("content", page.getContent()); + responseData.put("page", page.getNumber()); + responseData.put("size", page.getSize()); + responseData.put("totalElements", page.getTotalElements()); + responseData.put("totalPages", page.getTotalPages()); + responseData.put("hasNext", page.hasNext()); + responseData.put("hasPrevious", page.hasPrevious()); + + return new BaseResponse(20004, messageCode.getCode(), + messageCode.getDescription(), responseData); + } + + private BaseResponse buildSuccessResponse(List list, MessageCode messageCode) { + Map responseData = new HashMap<>(); + responseData.put("content", list); + responseData.put("totalElements", list.size()); + + return new BaseResponse(20004, messageCode.getCode(), + messageCode.getDescription(), responseData); } @GetMapping("/v{version}/files/progress/{uploadId}") diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 2cf7a5c..8d809f7 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -46,8 +46,8 @@ public class FileSimilarityService { private final FileEntityRepository fileEntityRepository; - @Lazy - private final NoCopyFileService noCopyFileService; +// @Lazy +// private final NoCopyFileService noCopyFileService; @Value("${server.baseurl}") private String baseUrl; @@ -164,114 +164,80 @@ public class FileSimilarityService { } } - public Page findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable, - String authToken) throws Exception { - FileEntity fileEntity = fileEntityRepository.findByFileId(fileId); - String mimeType = fileEntity.getMimeType(); - List allResults = new ArrayList<>(); +// public Page findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable, +// String authToken) throws Exception { +// +// var imageHashEntity = hashRepository.findById(fileId) +// .orElseThrow(() -> new RuntimeException("Hash not found")); +// +// Long hash64Hi = imageHashEntity.getHash64Hi(); +// Long hash64Lo = imageHashEntity.getHash64Lo(); +// List candidates = authToken.equals("all") ? repository.findCandidates(fileId): +// repository.findCandidatesFromUserFiles(fileId, authTokenRepository.findUserIdByToken(authToken)); +// +// List similarityLevels = (filter != null && filter.getSimilarityLevels() != null) +// ? filter.getSimilarityLevels() +// : List.of("DUPLICATE", "SIMILAR", "DIFFERENT"); +// +// List allResults = candidates.stream() +// .map(c -> createSimilarFileResponse(c, hash64Hi, hash64Lo)) +// .filter(response -> similarityLevels.contains(response.getSimilarityLevel())) +// .sorted(Comparator.comparingInt(SimilarFileDTO::getHammingDistance)) +// .collect(Collectors.toList()); +// +// int total = allResults.size(); +// int page = (pageable != null) ? pageable.getPageNumber() : 0; +// int size = (pageable != null) ? pageable.getPageSize() : 20; +// +// int fromIndex = Math.min(page * size, total); +// int toIndex = Math.min(fromIndex + size, total); +// +// List pageContent = allResults.subList(fromIndex, toIndex); +// +// return new PageImpl<>(pageContent, pageable, total); +// } - if (mimeType.equals("image")) { + public Page findSimilarFiles( + String fileId, + List similarityLevels, + Pageable pageable, + String authToken) { + + try { var imageHashEntity = hashRepository.findById(fileId) - .orElseThrow(() -> new RuntimeException("Hash not found")); + .orElseThrow(() -> new RuntimeException("Hash not found for file: " + fileId)); - Long hash64Hi = imageHashEntity.getHash64Hi(); - Long hash64Lo = imageHashEntity.getHash64Lo(); - List candidates = authToken.equals("all") ? repository.findCandidates(fileId): - repository.findCandidatesFromUserFiles(fileId, authTokenRepository.findUserIdByToken(authToken)); + List candidates; + if (authToken == null || "all".equals(authToken)) { + candidates = repository.findCandidates(fileId); + } else { + Long userId = authTokenRepository.findUserIdByToken(authToken); + candidates = repository.findCandidatesFromUserFiles(fileId, userId); + } - List similarityLevels = (filter != null && filter.getSimilarityLevels() != null) - ? filter.getSimilarityLevels() + List levels = (similarityLevels != null && !similarityLevels.isEmpty()) + ? similarityLevels : List.of("DUPLICATE", "SIMILAR", "DIFFERENT"); - allResults = candidates.stream() - .map(c -> createSimilarFileResponse(c, hash64Hi, hash64Lo)) - .filter(response -> similarityLevels.contains(response.getSimilarityLevel())) + List allResults = candidates.stream() + .map(c -> createSimilarFileResponse(c, + imageHashEntity.getHash64Hi(), + imageHashEntity.getHash64Lo())) + .filter(dto -> levels.contains(dto.getSimilarityLevel())) .sorted(Comparator.comparingInt(SimilarFileDTO::getHammingDistance)) .collect(Collectors.toList()); - } else { -// Long userId = fileEntity.getUserId(); -// List fileEntityList = fileEntityRepository.findByUserIdAndMimeType(userId, mimeType); -// NoCopyCheckResult noCopyCheckResult; -// -// for (FileEntity file : fileEntityList) { -// Path path = Paths.get(file.getFilePath()); -// if (mimeType.equals("document")) { -// noCopyCheckResult = (DocumentCheckResult) noCopyFileService.checkFile( -// path.toFile(), FileProtector.Type.valueOf(file.getMimeType().toUpperCase())); -// } else { -// noCopyCheckResult = (AudioCheckResult) noCopyFileService.checkFile( -// path.toFile(), FileProtector.Type.valueOf(file.getMimeType().toUpperCase())); -// } -// -// switch (noCopyCheckResult) { -// case DocumentCheckResult.Success success -> { -// FileProtector.FileInfo info = success.getInfo(); -// allResults.add(buildDTO(file)); -// } -// case DocumentCheckResult.Failed failed -> { -// String message = failed.getMessage(); -// } -// default -> throw new IllegalStateException("Unexpected result"); -// } -// } - Long userId = fileEntity.getUserId(); - List fileEntityList = fileEntityRepository.findByUserIdAndMimeType(userId, mimeType); - List errors = new ArrayList<>(); + int start = (int) pageable.getOffset(); + int end = Math.min((start + pageable.getPageSize()), allResults.size()); + List pageContent = start < allResults.size() ? + allResults.subList(start, end) : Collections.emptyList(); - for (FileEntity file : fileEntityList) { - try { - Path path = Paths.get(file.getFilePath()); + return new PageImpl<>(pageContent, pageable, allResults.size()); - FileProtector.Type fileType = file.getMimeType().equals("document") ? FileProtector.Type.DOC: - FileProtector.Type.AUDIO; - - NoCopyCheckResult noCopyCheckResult = noCopyFileService.checkFile(path.toFile(), fileType); - - switch (noCopyCheckResult) { - case DocumentCheckResult.Success success -> { - FileProtector.FileInfo info = success.getInfo(); - allResults.add(buildDTO(file)); - log.debug("File processed successfully: {}", file.getFilePath()); - } - case AudioCheckResult.Success success -> { - FileProtector.FileInfo info = success.getInfo(); - allResults.add(buildDTO(file)); - log.debug("File processed successfully: {}", file.getFilePath()); - } - case DocumentCheckResult.Failed failed -> { - String errorMessage = String.format("Document check failed for %s: %s", - file.getFilePath(), failed.getMessage()); - log.warn(errorMessage); - errors.add(errorMessage); - } - case AudioCheckResult.Failed failed -> { - String errorMessage = String.format("Audio check failed for %s: %s", - file.getFilePath(), failed.getMessage()); - log.warn(errorMessage); - errors.add(errorMessage); - } - default -> throw new IllegalStateException("Unexpected result type: " + noCopyCheckResult.getClass().getSimpleName()); - } - } catch (Exception e) { - String errorMessage = String.format("Error processing file %s: %s", - file.getFilePath(), e.getMessage()); - log.error(errorMessage, e); - errors.add(errorMessage); - } - } + } catch (Exception e) { + log.error("Error finding similar files for fileId: {}", fileId, e); + return new PageImpl<>(Collections.emptyList(), pageable, 0); } - - int total = allResults.size(); - int page = (pageable != null) ? pageable.getPageNumber() : 0; - int size = (pageable != null) ? pageable.getPageSize() : 20; - - int fromIndex = Math.min(page * size, total); - int toIndex = Math.min(fromIndex + size, total); - - List pageContent = allResults.subList(fromIndex, toIndex); - - return new PageImpl<>(pageContent, pageable, total); } @@ -322,7 +288,7 @@ public class FileSimilarityService { .build(); } - private SimilarFileDTO buildDTO(FileEntity fileEntity) { + public SimilarFileDTO buildDTO(FileEntity fileEntity) { return SimilarFileDTO.builder() .fileId(fileEntity.getId()) .ownerId(fileEntity.getUserId()) From 77bf7ea886040f2323efb2f28f57e4146a17c2b2 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 14:27:16 +0700 Subject: [PATCH 036/274] dev delete config --- libs/testlib-fat-0.3.0-all.jar | Bin 6369312 -> 6369314 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/libs/testlib-fat-0.3.0-all.jar b/libs/testlib-fat-0.3.0-all.jar index c9f3df5cc2d094b6ed0e04c66c26bf90632f2523..6d617f85fb29feca004a7727d73051965a1a84d1 100644 GIT binary patch delta 69564 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 pvb72uxQy@XkTNg^bGDIhmugowz989Dw%5;K_I zIsUbaJsIm)II|C5XUwx_)-3Q}9OhUb=E#_QgL!X(e|>`_Q7b5ozsQ@LevOV08!@88Le zDs&U?Cbo%QZ)wZBn9)+8w`POeM$1&!G}S3i>nxsnGw0qts-vFFxtaT>($jT^hls%J zsb{10cI}#{`_9$bAu^BCRDa1&^~?0U3A5hVEHq2`FQIhGZj)Oob~1CPJfAtfB{8ye z|B=m^W;ZI6H)TJ%wAqYU72Ot?p0GUaYl4k#s!!d_yLGmXcUIPI`tM$d$&oOvwId_j zq)s>e%DEP#{OV%CCGEc8d4at$O>dFDiNcP|E2_&=bmcQ&UY9TP z7P3E5pROQsBxvB$v7SkDSS627vdYVRCgx_ieVtlb^&-gi+JUHN{RduGD*5$VzNWILPVtXef|7@=-&(|j!90Gm`{g`>M-L37W^O}csQ=}3lLdQzp{pvkht60q%FSdo6ZO8rxI|1G%Q>4PNIH zv(p?4R^!~RAFuMJA2{3?e}3hRj`XRYbX&tekjdwM)~()kujoZeWkun1 z|IM51mPO>9zxH!NQS1Fa$xr(&)~@q(6I*xK@kiI9*S))6^-Ou%TTyq{bW_u(nP=YK zc`O_rwytWw*XPN871K!_UDuQ^ZycL9x6LFEb8J9ciO|-`{~Yb)IelOBxWl#t$O5|~N%P(oe=UYLZP}|< zCihSDW&ar8ez7OwG#pbRbaE$Hb8jYVZU|j|WZZEd*O2x%eKDJq9vX&>Pn<9&VPEIETMKM9L+zpXSr2`THD!sW28e*MpGwYi3AIZ7+?b)%Xvuj-W`lJZO z_JZd>@-s#V2h?in*0onYl3#75{b}07(#fY?G_4QjYDg77vC!DPY0sA4e8ZDvH4L?8 z#b*sWZ0=i|-n(0R&-wWAgDXC~w{q8bo+W-k`QEzPIpo;$GQF8Cn*LvuG~n(xtHkz$ zpO)C3T?TvYx48A46L{5`gAIC(ZkuTndO|ID`S}1LXV8jU;DPZ zY2W$}``Wire1G9lchf$oPTz zio!Vp%1ZLq^KMTzn4KnlAbaBmrAfaQf7enhOKB7R(x01R)jHu^ulG&PeEHRycaW3 z1mWK)F!Hltc*(BZ6yeB{T5kKHCr^$wYh6}OS)TPgFsgcXs!4}Vn)BSM1|5RN1QS!2Bev|qZ)mt<7F3t6LEhOwy_3nV$ z{v&&56|X6l6;l7RYtsyvXt3$?^SVhgNBiyxajH_oRTXV-j&dG#_u{3lmxtu1zjG>% z*f-95+C8O;iOVD|za0B2xh3~k#b^6cR@#p3#X+5RHk~_??2=uro-ZrQKIC&*>Cl!p z2R5%R{?j;_m>RXG>ikdp>5Gp|%gu}29CNFzYh=I=*nDG}-H{HH-EnytX}ipP5+v<5 z?M}U;oN6)e+>uVdKVSWXz5NC*dhT~_DJk4_#!4^u$j0kE@vjmu>+iIGU1qG_fBkq! ztBA>KZ|iT>t}CLB%=&4$=V*@miwC0~FU{z7S$leyKLVRd%NySjXZ_VYQA-*ZzLwx_^i0-iLJwo;;^8eiLSvs;HG`#x0 z;o~}=qHP~m?dVt7*Sb7w-*nayGU)i_+OiJ8`OzWGoR8w?S_4LH-T$3jys1Isa0YYh ziEsB8-*H%y>nIs8)@oww7l~|lMJM(6`zuOUJS0@-Ma__|Qq*h`Hl9>A`&nsi;r!Ue z;SEa{#5}qAWa7>FqqpQq_3z)AY18q;NTJ(KWO=gBv#3+*voC6Tw0^i&rV>|qbE#Wk z$(xnq9p6~=zTKyK-f-dKh8~gL#)E0ZVpaXT&(99#=qwo1H!W96*vB=#(D~Dypi>V# zHKTr!oU__$GHW#@A4_{4JGk!1DADp|+a+JcX3cZ#GyPf6$o^8$Y!rW9eeR+4@6`gY zD}Kqmdy8zhiQaDisPCG}i>1Xif7qv<`_}1^A8yw+K3=!?bxh1$rH=oW-dOyt`u^Co zWcQ2dX6d$p+lnBwXwlYOquU3&`%ces-r&D#-BeBWRJ*Mr@{OC%+4kRQxv=xhLET;T z-Qq51dW_vVItyaX==_qsA^8fr!ehOZbNspj`gI=k`>xyhB(!W*kKgVy$~UshZQX0b zz7=_Yo<6D3!{=B-OVznQwKFre6@<2Jw%N$aOw@U&{e8bwnR@4`L)tB|CK2u{V@G^_ z{xrWMU;4A4<+-i(2B&q)Gp2{RD9%4~`KH(1XI{S~a`*h6co^nt5(X+~)z7Z7{N=Rl zQMZ1{w^Z@dXWyUD9{G!r`?jV2xF(_KEQ0G-(tUctZMYLnxaHo-7tGW%Jfzl>rIl~V z%AC|wWcp8lKyuhNp3`+}l^a~TY0mNC1lwClvDimwYE%?>EE z*S%9R@^X>)pKhsB9VgO1y=b&ge`uS2v*TA!zab+f!CHBKsIIh%f7CX0-%`u$j8z)9 zbfTyG6;78?TD&J#JvYod)Ao+)-8&Bs`k1l)6Y6oC8&+>;vHRw1kwta8E_kaSpH}r= zsrZ)nI-&8)d|ZwfiDx#SuyS9s!To2lU*WV$nW(99Hv-28fB7)0?wQ=`xnA#Xc)k0z zAU^ET)|+>K)V=gDex~j_Va*sDTlsgU{?)rDAD5o~{QcP0^Q$|#M}7pI$Zg#yJu)<}g_mX{(WKLS8|+cGVrtqZ`)7bOm_|nj{)+^i=uu%;AQhe|Ch+ z8)L;Op*e44)t;Pr8=BHy&fORgIcw1zZeP*S*9p64#k-w75V`F$8F?}8U{7?(Hs58; zBYJZt$%l5g$k?asog6tw=<5DBgZ3xClB^FnKMk2^A@-TPWVNi*=4o_uihyIf_q3D8 zav8hh%qM0%?36p{qZ5-S72kgET*!>geGwL#ZDs$-pM7n8Sx``{cG;@P%QbNmyaJ_C z?LQ^VGx+w|^vL!9ob02GzGS=g84LDg^c=i!O#Ft+dLa>Ie{s=M+{_!VEyGy(K}HRC zXOI3;*E{vz6^nyAl=LgFuKala_7Xdx$_UwHiz~U6v3Bh;wqcv&be^=`){*TO6b~OA zwswL>o$QR@nCx>mM$ZoXY;Q$|JPQ4Eg>=tsZoc|SVvYWxV>ebL9XPG7Zndd;#xhxF zB^A-SEsO3f=86Zj*xp+2-Lg>OH9Pvh=_Pk}WF3+6e>ur0c(txld7GJTl-rY+){{6l z5=K^U8jw7jtK=sY6vkO;+3V=AcgvlGDXo=rK)(HGWq)J=l;go%lfk-0USTMeyN*DYD5p=|l7OJ>&u z57ykGkCg#$@{=6za6RdF7lYCP^p-Md96&8jyWcx%}o5=gt>Rk?a~=NIu9PhzZc zrqcCA3J;bn`8_kTGfTYnht4{Kjd8h~O@a!891qW%SvN5AVfju0Ba8S}Kkc@Vq6n5! zyZI{*ZRv>a5udxepmDzB~&POh0=s zQLSa%^$Q7K>ne|;UO9oay?p{?K`boih&iH2?y}8Wb{PF`KioFSuZf34}_lu`Vf4k~+<+INY z^R2OEqeXk0ej5w1~_;{U3lu(Jh`D!t#H!D z64_Q5JKK!&tJ3)S3Yk%B?=Vk^r0#B7A*UR?An56t%$ULzcb3YE7Ccev*k`DeYx1%9 zVkJZH{H5H`ttD?K2n!pRzIrli-Ru3iGpx3(Ah&U@o_Z^g{w?L_UHP&z7w%`PXzuB@ z471!);1g;%^~L&c8v@;a%p7abc1*`Fd)XPU$dKm44b^H}&-Yy1o$5O&WSop<@kW{b zi*i>lCw~dfzdCN#n*i}O*PfU3$=}`T7%l9}JimVOq^PhywNnO9_kVNpZjkFp8YR5F zR{u@z`r6&_qkbnmoGp^Ge4u1?Pk(CG#LC&iF(#)1xM#DOHHVkSPyT)CjQP}P2#zNd za$iiHd?wx2@YNOn+NvMVwi$4Aoey5ypPllNj9Gr`=F6#d^R|>9nse0G z&@;bcRYhq(8^&kIV{xK7Dq7<$m=#nMY$H5`C}E z-_Yr(seip=O#YT|3fpl^7>!guvdlYLtb&e}82UQ;VPFvsd@y#3CWYbuim{F2_7 z&#}tOdf{d)taD_?4zsKpJJQ)W^XiT*k(;M~f45@1{KHLC=O$J)EaT0B2$+s>(e(~aXvDuq5Por0EDc`oQ&hO*C;})#6sqL#9p0!!c zBBUGbE2Ce@&B@4#aGLh&db8p3nd9FVBv(DvENg8`^J=Fshm7BiU1^;8>gLqwb_d66zrx48$UJ^0 zDdMhketY>x@v~M{8=N+Nzdl(fu-;C%INko_uaBcr4y;z3^J|GC%gOjLSD^9NHS^m7 z_eWk=?4F_!gTAgm?$*A%VMJN{Ob4vXDw4ax<%SGn{O)2?#?_(Of^n2JG9`)lNp5- z^8^-{$$z;~d`dL^kEQsJgxK#Q%Q9qspKZwfAf1-hVxMXJB2HeRs7Xa8a+Tf4mWf%d zlGo#QjMryN`8?mc()MJ9J4>dJ1(nmYm8$Dc-{H--u zS7T&u;OgwDx6iE*Ucz0St>kcSh4bma)eV=oOgg7-mM=Z~ps8w>%zD-`Zq(M2f%5$1 zOm53pn?o%5*NYz7&qxv~d}6jmLO1(@*+%7T@d%;9r>=H3_oc?1Jv(|z()?J_lu>cD zOU{g_%x0%d^R|?o+*BP`POEBaUC){`B}eK+a{m-Iw>9{@ZA`mjHLF3R{>Ab= zx8Yn%U86+(S?#*UvW<@O#&!OT>9TWJ^s}IQ@mEi$Wn(l9W~q*SAg$VBdt`AqOYr+G z1;eFd}7Oyp{r@i0kuYUYu;+lGRHsfiyaAV=BbKi~ZwdtZDH%%glaJPWc}UFX>m8!f$Uqvci4xs$kM? zQOALd`})@-gKmC1xu$wSukN=GBBw0V6j(=>mX2A_BVSa%>eIb(Id60dM`r~-)4XRzco=XdB`ykN4#Zgu#& zrLk^b10278&upIjc+Z6WIY)D)zWn#=r26Tp7e@7)F8VghIcM>sUZ!=w^RFDucB{-a z`R*GtR(-89n6g>T@$%@#{5uAV3|?%Gj6XAG%z^yx2Aju?w5e?wy@|Ca-!xFoBC{%= z6G)~%v6IcU$=s8_!(brcNkjggz%q;eihR|;Ur`Am`sa=GB@fSV-MDzpp`sa0;8u zq>sq}lOZM}Ovac@FqvW+iOCF;Ii^vVEHGJOvchDI$p(`xCOb^_m>e)UVsgUdjL8L) zD<(Hg?wC9l!{ zfEIO$0d40vCt18Z3|M!bGlMChML@F&VFKf{2vz9TiV+{o1>6>n3h_|~7GA=W*Qyqd z1(TycC}|4^^$7_`Y2*w^T3z6%Ft?iwGM%Aq9Crv$HY3=u?*d1Z;FwWr0^wZ)N0!#E zd=a&KSW~bAv>)Qg(C}gkuJxv1N4R~FGnOb1poy5b3`JN|i)H4vax|FZL;qPV1|?0X zpohXmCj7(Y*LysWfSM~DHrPfGY`n6e|J#|T54o2(af3ywQoH0>@ev~?>x>wo`Bw!0 z?#w$6186ieK8b+Z3XU>tN+P_V$(y9Br?x9=%f@_S#0Y1B5hKioWo$?TjW&)Zd`?3L z40^O;H%93MND;wE)0DzZs&M0=0Qq3$CvRH4!3ih3H`P3 zxPzkvA{`tPf-^(}c5pn1+3P8eHXQ8WSTL~pJ2%3lg}7FP)vuzyx~sH`=~p>+3^&+zmBS&_@6j0hAt40%T^tAE>H`{C*u}9U z=02kcCFtxzQ)ffXHI5coU&Ff8QNImgDfRms^?MRw_68nb=Zpl8>zHxuJDS*eo#V`y z2;J8?(+IaeG`8XoAr7m%FhUl-Mr*g;;7AhkEZFvlqgf))WWlW)RONYqG*q&f;^2N0 z(OqJH#T4PY7?Ta7?xKd~n^@noa(`Lse9Aj}2muKGfuSvHFvy1x1Ew$}-$G>(d`O8g zQwYx9Ld#DY{gXpfjV@x`Z7f2@Y8Xo#N^f&4h~w@w>UM`C3?FZ!;(>pNH^KJeu_fT( z9gYie&U-i_3|e(Kx0%-fsH6A*^C$xJyAxsUnCS$uHCEL0bG9b2ih6Z=GR zHj}?&a-irww)y)7d^uDfVCA$|@FBSm(4mFJG?KoXDFP24U^&+sE{pRJTVk9(MHoW$3mnmo zLH7$z6d~k9W0QI~mc-f7(BFeC)7ZnYVhBKA4+=ztLGw$F6)bp(qlqCjz2r_y)S>Y;x+rsq zc(_486xeS#V~8CaXf`5jm|BJUEc!8)OWt6M7eVbCw5e`8E%JW5fD|~qMKwt$Xe93~ z29Vfknz-{81L*4ony`JxaboB|#ybv|(7sJ$Z{J~mj2j|KZwn|wNG~dzeUD}%A`Sa` z(J{S`d59PYzQ+#Y@RCLzyhfSi_gK*O*E~cK9==CCI=wVv*oVc$4-sX(0upfBICf|t z-O`8SgB=S(`f;9U@55ZP1_?dT=*LOpGz9l!g?DJt0=XYJLh$luoDA)go_=ifT#N-O z^B~XU1ILO;HR2)e3?`iZz=~$4P?nz=;qPgY>T)6XuzuK}eU9 zHz6j>1h;P-H7J-#^Vy*D4O^cHuHQJ}3_n=^4J(lj7r${_8TBCao#Tmfo8NbiH#4JI z5DJ?Gm0{0!tXng5edo9_eu4B4jvvDw4*bCApYsbXT6IZ~e`K6Tv0@n{i2vjSFg8DzImOGlp>m-cx@Tfc+ni zEpyj%L9nzY<-yl2Mhad&=X2i9yxYjk{3xgfYusX3uqlXCfH!||>0{70h#M4}lNOAy z;#e}8dE(n3BoFVu35pfpCry}^KZlS`F(F|{`6(z14h+(SXhc{X3+xjVGJvfVQYJAlX1j3bqE3Y)Cd2 z;-AoPR30o8r4%8^0R;{)NfqM01r1AC3bElG6K&D5!cQwR)5PSdWBA8zQyfAK)oW+7UiDreV1yLRfa;I&?vmFsgr5`WF>~ z&%&e|@u^}EfB+HFgE+dACSLCpQimoHln>g?LqtKGMOqN|Youm;pF*G3N3?F-$7`9?Hj+~T=mluRo zpic}<%D+M*4&tOeqaQNFNe(gd4uxqz%^e|G7!XG(!$-7~uLQci3P(;#$sUSGdP)-_ zlB5$Mg5#w)sR-s$Bny0B3+YkSX2V8F^zpfO!+FxM)*hYtlj3dd(h7hWJw+1$YS0aSSyQx_1+$emQ=)MGSB7_LXe?|wabNVirCXj7E%lq zXkRESOSjhLWy1Vj`ouC?AXN#iO;|}0YEZsXSOwCxaQMt#Bg{YQPAyR;#}SN;bRM@d zVFk!lMjM67c!(OTkVKCNs9?JUl~b@1IH-_b#G49=z`m_Y3d1H9(ve8q$wNfog9=7i zNYy{2Ty*u86+ELp_PGP9DL6!cGjb(nIKWi`iU3 zn68Fy@4n6_pngM`1yyRO zRt#y#(kqlhZ>;otz-I#|S82Og{q?<_G)?@}-eCze68DQN>; z9Ber_oeUu&Fh`fPCNyzQp^%hd5pn3y#n5#O<{@SfqK8#<##x1uNedH^hN-z|{2@J5 zzXm553T_HVIN2Rx0eysnB5BxGpLAlJhYWpkA`uu*VTxd3K#D+*KK3m8$vnh^u?aE_ za2$R$6}A~-on9J{L4;WfMd(3#iik3-9D~}IB9HMLDhzRu($1!{OB&(0@4kRX2t%3? zcBIb>p~wXLnQ5NLP%nRGgr+T7PQ%BSi-^)Asg*IpPxEPbiZL1{SVR*w#-tA+wvi^J zOwf5sTPQ*WLbhQ0vSyLe@Sh2(%xHqOCTP*vy%bjsn5LvZ5xJixl1)il#&OtYii3n& zGp)H&IP6L6>ElBzM&Z&* z?h_9YhWt_J!_WTkm|FaGj>4Fo5<(N3LRhNwA}YuNjmn-t!?_mZ7-C8!O+288I4(^% zTcUCy+?r9UR$Ah6BRGvF9$DfL-;+rbDp@QU2(ZF9^`1c^tF5q#^JdXR^(>YeHPB<^ z&SgpQ9Y#dVgDs z6z&Tt_^=JGUse=QgaSy}Vh~;{q}9dRlJ1065ltMiMbjfT(1es760yJ|M4=2=60pz? zEAxC4AAH#k7c(EXLY@;UU+X{$!UKC!5}YbnL*X4@kEU$i@xKDNfal}bG;Ua~o z!`6!|6`nji%hj=ljLy-#^NKF1pV*#$Dc!?`^-WT6_fD3W& z7fqDA;xh9$1GL?6dG*T>_^W$j#w+1Xw880D8_f=nSY`T*W?@i_tKd10SM8G3SR1QoQVRof7QT_%J zN#m0mmxhJxi^3V{SoScf%4|wX58h;Box4Y4o$t-35OZ+y!7(6lJx$d5pr@|`O=xWt zRi$c-AydjjWTCATRrG$vU~BRvHHe_CG*5C2=}g?*&Lc!&@faM@^mp?R7R34C!hN+r z&P)=1=!2%p;XEfoy^0UH=7$5)!5Thf^jJh#bu@B-B9_+Egq%MT9mgrc7R*kg7gqUW zc;7w6L$o1aDta&JtSJA2G6FmT$P}Wvg;pbRL6i+|1F*^#mng&x?k3?7fOm2#M9vHd z9fyI--iR^RIzvngauQJCb&7|F=Kpx=<8V3Q4k6>n$;A3q6vF`Sjl2aZO0or6x{X5Gr4Y;r?WR+Fi`W-NmOcv^4lF zpMc|d93)PD;zI;Ji?MNqgATkp9@X{#;Nx8$5W^|vr5TEPmLz8pw{d8QCLO??i^9A9qX>Jb;bOem%%zF^ zxv1Sd2A$lvfJS6;#AV?$7wxQz!SYh_XjnKFmllFcX<~jXdi7%=O}wT!6E@OBY#b_Q zm(fIGnK*`594fzpV+2*UcRYH{bO%i=+##+2`O#R??RYF{J`M(yjOipaRvgCxiYS|e z^X48L0x05F4H~OC8TFk#Mk7kc(bsN~sNzKPutwb9JSb3d2Kk#)ke_^h7@u>VR#0CT1m~iGkN?_OQ_NCxrkv_)kT11GIm*i4$d}bi5Nx5pN9E_ zIW&A}8m^JEVrXJi#+a0W$~LeLC}p($5B89T>2ut>%wPf#{( z0e7i{2-GE`Bfrk$AyS}{f{~)Vl!xd+%_MA^eJNN<$_gGV2|j^1=8B{u{Ir0EH47y) zATAZ%F=IWA99%CU4JRnXb0dw6I*L<$P8w=dO~a1fQaT(CBC@s)N5tUtRtZ(8aK+B` zdfPBp-QD*8h~idrI~Kt*#R1c#a+thI<#2gXxR;J&{Kq3am<`SuXsp6<8d;iwQ7+pE z&6&8Cy-RTd2MIm6?1<`}GO_XngNQNYW|AI^#n6;VW)ri|Q&}uvdKRjeXr~A~1dLIK9lK(^cq9)jEN8MrkPfc_a|7BT8QkAdA|CW`*~O(T6XafMt(z`|_O7%ZeD zB_MegvXex3hzu_7B~^eUiB^7>lpGfIk~}GWNcj)zR4c0;xH8bf)V-vAm#j4jICj3EFc>AmThJ z%o^U!rEFY46F$Nixzp!iF_w80G756?Bt@Wc9@^uwghGr#WvS7Rq z>vN)DSbg>v&;suYB*o$3e9SLVL?gjPl6V#I0(X7vjTk(!3rI<(*#=y)Zj@4n`~`Sy zp7@vQSU~zRJ7I{2lsLHNkQU5=Ekn?$ZCHF+4(Y;Nw(TFJ3=+Ah=0W8U)Lktp1qphR;nNh(6$TKqM=ZCIx0w*L_m1?Pom`~GW^ zZ17)1iZV;D{gd#ZF6kDL6PO3@4}$7&GZ^oy2DdWKi?Iql4~Otsi*bTff`g06P@Ll! zOUSv*3<)X9?b=WyA!Pv_OR#2p+5aFTU`tEk4qz$bCzbx;25@>QY0bQ@$`83LBQ2Tx zb@-w8J-FcMT!uzG(&xvgzQjdrz;eVlnepQXm!sA;3w~(i3e+0!zz^+Nfl|fJ{E%Kg zN(FfjLEXMmhES1DS}`kp|3PN(YX09np7lyB+bf7)1+1jBhVnyltEjT0_@Tm8C{;)D zL*lDZYHmE{q|{*H8njh##{b|t;BgS6L$-kO z-<ae-TMC zEe{XoFoKR_QiAa4sFWl$o);IUm;Bq;p=s-n{*Qz_Tp5Rh$i`z*{QYOev43J>)U^&C zB{dC`5N#Ti(1pMzv={FoObDk|=q|=_N_GRz66sfH;y;R*)Ik$>C}LL^O}G@}iAU!R znt0nKB?6i^G3e(~6VSwJbS z-q9WdBAjXfa3e_?g6RqQh zS#fL}Eih0BBsmqi`ZC_)FCYT06-GMz04&gE#)m{c0>-bPBm zn+&!V)lHP3y&NZ}@{Hj;1G=jOZ$;knIea{e1!&b^zBY>YbpCLjHl1&51@dZ_4(AEM z<_bJ(e7xd+VXYx>auPN5M6?=cbe)EC_dH zrP;I+?*quI#Vt6GS8xEMuTzzW2ta{0w(r*ic=q9?Jseh`m8`2mC1rZUc`V4U#fjo4 z#j7`=d4Ei#hj!ma=F*JYGIA2t0ye`*jHq!NbR;(mJ%d`$tfn z$co`S723e@N0Fz6#{xqZh`{Efcoe*J9S;$Pwsq3t_}vm3rcjTA^GUF*$H8>YR(O1j zw1iXjz?h9>-&ow2OaZc<%``An-B` z|2jch;~~GrNpcFY_9ms$7;L7anYT`&)aa)aq6@B{rNveu_Jdt>uev4u%jV zSZFGPcl6Yx{P8rFnySR(`4L5$H1S9iWd={FKI_Qv_mk&3w19duZp-{2q?ycR$U$c_ znMe4V!_{Uo2qK!O*gJ!&Gi@nhdk{T~He7X}iJ27P=|T}Bq2e4V0EI3x!tn4cR-@RR z2MdDpIoySv^Wq`4M4lf-n8GDL8F7$2j~N;QXe9AGI_elcW}w&?&*MSDuTYxs36qhb zS1L;3!)hWDP7C~L!TrqyNj&aoXhECfB6+Yov6jOlM8M}~3?4{bKpFdZ8t%M+L6wz2 z6AR<97A_a5noi{*xS6_$x2K;|`4FvXD414-tHr_d4fj^mI3be~FoC75*h_1dPy{X) zF5zn6SrsN0gU zw_`XW3L~$e5nJ}t@WLy2>~p)CCnF4+2W5odgbBtE(S{lypN66?yeLRND}#qw)B+}t zf_2aGU|o6^7@*)Uczrq~Edn9!7_AQNe8}o{85R|rCd8fV6l??UZ^@{EJ4+V5Ma|YR z9jIi>Egnyw?#E9lxce>-R)>&I94F-=uajIzJnEqsAu#SGMs?c&zLXr?JSJboOuO-> zmEzsHieu4bykDgVZWl)PVp!XSVIBH|!h+${4;e8?y@saE`b{G*ui-?shzXhZvHCTg zq!6@S!>hDDCN3>pe*WEwExwMWwBxe`o|F~~U!f1yhIIpZ8}VTrKTi`Z?%}Soh~hnw z{2yM)O$;z~d_BjX11}xyvCP3mQ|V32_EwcoAWlXG8~VJGVYU0 zh=cfk;yxKlOK~5dltUDS1oP^NkBDRFe2p=(#&D_}ofQ8N1v~KJ1Z4@r+I*|dCloJb z8jmLq4v#RhPp9x9)sHY(_Ga-AVfdLPJG24z@5cP;_=aLgH@*q##$MMlZ#W_f>W{H! z{?4P}j3=ZhEPsrvqKM@@1V5lzE-OK=NerIg_bIZM@28Q4hh=Txzh@XY9*22|6Mc51@EmpD1k2}m5n0^C<5~l=7so53 z7g!6^4S`8?4=gKYYA*PqEU~2s1T=>myT{ zS3L(o2N>IrBKCemRDqwIFr4bg!;PuIL(u79ITnokfKXi65LEO5^PG(wqW*k9wZFmg zBRPYaG-VL9hi)BI^!y{90PRnegW7gkbNH4jr$={+d7rR;(=z_yYJ9!o8^zr9-w@OL zGwH-^STIN#!oUJKB{=<=I(u0-2pIr#7Is>rFPLZ9S~>ctH0BHG#ynR%M4ehICj_Uy z;IN(t@4w)E!nfVf_XQg){VVCfJXbj+_Nx+A-}{QHe^md2Sm1n6PO>-#ckgrR-3!ToR8500Fm2wXa!kduJd4u1#qJE!INSB0&R{vErK)0six zk{`I=7igh=8_`GFbADj7xPGSf=6;qFfgnM&qVo4JD>nV61@8Tp69v_u7@)39*!B}w ziaAVqG1$wB#TQpUF?41@#ZP>P_Cu87;v4;6xO(vrlOOzW4euSKbdl*bT)gQoJD;QR+`WMn-YR)UH@IGc4i42PwPnOqCvn&%)|9Kv-Vgh$g7 z9;4;eKueX&F22fDA+C(!q2jQaIzPtO@+yQ9$%92;8iQ*^FcbKYCI;7r5KX6%r37wu zqtfLS!HwXW5T#i>*q69BmnL2k+;N0k4v&xp)vs9AJSLWPehCGu!0#of!d!!^h93*z z*B7EIKw6NiOW)OSRt#&BfI*%<6s(X};Jf=P5I}VgR#EwE!D6*M?((T4^v?+65Y86l zP9@%z(p--+`N0sub0|T~)Km_hEUq~$5aNn}su0#!c^40HC)oRG;;j%mwX1<9G>*xO zP;chgkRXgIs+(!}{bhM+I4z7S{4eq$doE%$44&f$i=gz5%RHVU)QF(p!n=5gAV{&e z7L2KQz`|WYEPhNe>`FutdJLAL++^Zt-=J*CPmGE;qF9vK*MC5Y7}uVt`|%ghr_Ue9 z5(@l5?MT2jajq*!{gju4ba7OT51_=kX@nmJxH#7Y)=DerL5u_%hKu^Y2p)Axpa+b# zCc!2m*~xE`P`#kIn{XowUylhC%NFgLLF#UQxt zKFqRBQNXKSSuA5uGEYzr z#N{ySCERfgDH8*&ECnH$FNcEv&8Ogz&@>;ByA<+d5#=#mFqY@~6Tg=a5+$G@L4PT7 zLLSQ_SN%n$pg;k?CvI4U-nCG`+^g3PW4RNjOa21#^xJerMbvN|zx=~OP_F<)D{{RF z(TYI;+;=F5L7O5Mw>aBrWRw!unK-@+t}0iryKMk%4oR^-=X;X+16zX74 zUv5MbZAOYRFlr=LUqTn_cGQ6fivmZNYs0t-YjwGkh=8#aLmO6(Rg|J%G~4K*{F*=t z)`AQ@>}4TgJVF(I=y4s1wuuy?38E2-lHja^rDW-2uGA=C7+|hyeQqf6hop!EB5@A& z*9J-r(7e<&bmiU|aD#}BLLR|_yX#O@mLWHoC|<`0zc%E$5fQs+q|ylAU_}{WomVyT z5O2b-nI^=IxlY8yOEfXXnCnBlybMLAXy+AU?igZE7mvUf{FWG?(I#lL;Zr_%qa*I2 z8Yy@h?q{j2-%PN+dtXt6HuStwl!lQ`G58MJl&j3pg0-d?0W5qUY|6C&!;x6S`TaDp zY9!a4kivJu6o>msQIz^-3y<8)aI(SY9A?}&;))nPQ^l6nGDll9#PMo$@FCa>O2iHr z=G+Vd7hHcu>Pmq0_orb0&jyWHw1fxCL9i_%IJOSv-)xJcV#{`l(1Jg~Y513r+Ui zEMnhr3e$kX%P9KxII0=^7%KYgu)_S$@}$Jzqyx_5<1h0NIrt%k0cP)rC2#5E!Q!yM z5$8qaYdk~E8xX*J}ILYSm(i@o#2EzV_x#{Dqbpy!B!{qo##6m zUhqyyn{TpLamFmC-qXAv@0An=hl!FXoVb4fz~$T3FW~8dUXB`rR73F5Cb@S^~CFV%_n|_@TNh_V3wjK4gt6PFlvYe8_++dS$Q0a6}j~+_<*H zR9imyjvIES?eRQB1@==PGlsjPDFu+>j=kl>bjWn)8p1Dk90D%ja+b0r!ULzq_Dq@( z&r+6yUU`h;+a6e7pP4i)=gAFZ+<|yc9RAwyyM4@8vQ$|Bq;iz;RFC%&4lGpW52fV` z|4Dj!Vep;7l`oxDf(jpe4(Nr2e8PnQWk{bF_Vj&gp~#zS46)ugfyBdFZ!Sq}+6{TW zI7x|(#^$*`L`dyX)__X{I(yM*RN}IamO+>W3-_T}e_s6Ed)=i3j@I%CsQKV9vjktu z`{K&)$`R$EcV}CCP%!ow4GZVuTJ1Z9NSvY&bBH>HZ8rEwdGI7~&pBoOu<=d_Zw_ zJ*0`Z4>4na7xvZ_&xW};`x#}LF79TPCE>y|Woi1ur04I4N!@tQCne?2^&+?*`H%x2 zmF=m$3>z-{V|C5H@_2Hf5rEd>KN?aSj19oHAQvtel{(&7IqG7LsFNdfhYL2rCe_Ty- zrlo4e;Uw7ZP802cSpL=^t{998!a*<1cR1`!tPkWNI#jjTU>b}TnN6f%6PPtoMUH-& zTZ}Lx5o&_D9O7mi#Z`c&1Qj7L4#C>7<9P^v#2JDecy$5~QJ^_juaj&4)QZ& z^ah`F5x>)DSZj@n0OW?EM%#2A!h%boH~^Gn@gYWG*sssbqLGX+ZVd5#36G77m8B}` zaC|m)ChhT9>f6;ko*JZ&NAEM z#DyW^N52YAS=6_!ha$0!M?hO7cN{|yRHL}mzpXPaifhLV`>6t%KUMVL;Z9rwSDlEF zWOT!gD6TU@5~QQKDY&uDiN;1aEd;sIT>OV6qPal~5%A-1eHgY-z`?%%#taHMID4~5 zt_Aa^lPZ`ytEzzMdkncLWXV;(<9@$c#Y}`bMaTxlOxlv5<2SfQ2 zQ@Hlb-iJd}eG1o;DTjx9gSrD=sH)Q1+)`0ndH)c8qkjk=Tp(#}VT-0@Bg@~bW0;GB1kMPNLi>znizX`F`lj1jopaN zvBhUM<}<-~HX{1$0_;Zg+4cL(on>9VzkgUhpL6ESnYeT3PX1Aw+ze{lP?lb8T{o4nnCj^Rg>gpk8T$&|3x~xq7Cxsobo#1I3x*&)r=} zn`0&dp1`s~@ZOvnV^LdsSzTWXwTV27+SqlH2|FX||E6^5OQs@7R@p#J)s3iaE!mOM zn70~Etc&m#=}I^FqrsrB&TN$gHH4W4k`e28j;&@bQ2fPq0|xZ`@nE099hhP(+DX1K`A5P zfA0;PdkRkYx`SB=B;^`-B&}l~wYPS)*Y#emuyZ6VSW)>%MDehYbKfQ-y*Z=c)uU6K z8E^n*`b~ke4n@_>7#4Ab*wHZ4i3W{^naejBSC_tzMrFzPq0aneKWgI?B{opm!_7(i z!E8t4$0#}oj=q#RK@m=wzKBZNiBWef3J052#wz*WLUJEX$LXile$ z!@KZvQx4`0L3`+v=%k%oheR$LnCPTWepx7?9&OaxSqOTy69PH|H;kz}x?1{dXy614!VDSDR2s1IvnB6|2()23XU0uL z-hHB-6OqpTOE`x@w52|ipk=s*oBmyrS3$FhlS!}|wv9_`je`EB$-uwU=uDU?3gzGi11F*x2q3c<*o>QnaxrYendP%kq;* zdtGM3SOLzaVnP0y4TWad0L_3`HjHiR&B06tnhoUOB?Cs-?#$GZ<^Tv|IM@lG<3#0i z@J8uAopVESpin%A0Z+P+gAUy0U9@XHGR1E$z?Xd-%$kZ`^&ki*K&*-iFq94=DA?g;&O$#^q#r*S7%dnMUO2FYKL%eOfzLV+gvg4yEQ{Y{5AXab;3U z9=c$Wc9g#w-M-U#ia4P^;$&chgR4D7twH9W#1j9%;6N4wpCnM5POm}Hjbyzm+n&Yk)Px|;G$k2Y&jo5oV$T6CN$l}zeL%hqD*Z*K+%6>ITL+44^g zg8Fo^lX6h}g&dqkL)W3pbhM`{-R#d`ZbI;Y8Cy&_WQJvCjgXd^Qo>Pq#8x_VOK zdiZyABt>t)D234mq^|=-ZBVqqIdtjoH^L!KL92NkqBTzXytxSjG3VD){$`Yj-)20!SVjThLRkPhCA4$1 zqP1}MAm{84xng7G7Q``~I&M)U%DxmS_)Qlnou)GlsuWDQc) zQ5vE~Ms7tUI|`_L2l9IMR;1;D1l;Ks1MY=P5<^*qu11u+4a|)boH?*f(Ez8ARc=#E z5Eh*0+?(^R*iWz>T8}Ss=GSFceUk4$MPG|ULYXBnwdhV4ynj#aKnZ11*$%}>;pa^z zjit`FT+Jxc2#?Z%ov3(cFy6re-7j<1CA)l>nN+4>ENFQ?qDsI~qD*JQBUcMTytSFCK_dMjV_sa#N@E&|X-_GbIJsVdiE@ z*(cHD)!zrYM=ef|-iM%zZ8&(o4>JJ?4jgpZk5L(YXAWBZt+1rtMwp2Fz8|4?yJ_eq z!Z=S3iap(|x$DpVhR*SNoDM&L80z_QFv{1>iVht>4!#ZGjQv4;W&aVyL2`nd3C%r- zXJl!t7V{|sThc$V@DOSIx}+% zP%z287!^xfdm#zZD4=IQH|-a^bw6&cPd_&^?oG2oRGA-G+KP|5!8m7%#dT%~r2acm zOtvGcr3tV3W0)L1Jeup7k8v~NnJpYgno_bj-R?N5#hZy7oMxc+R1ONJy5XGKT`=6{ z1OniALn*!uC!ljEn}IMgo#UoUwNE0Sw=ZK%F#TMHEJm&I5%%GbDaO1!sR$PSL+7ag zE#HoR6nfNfy&Kl&{sWz@TQziJELyDm2R-w!gBnhcCY(Y$be9gFLZA28Q6{jXdFK>{ z6m}YI$RCt(8gn)VC$t1{1#l_l1Ty958Tf8^2EOk)`M(OuXHbf(PX8~x{S2yJ{MrAd zv(F+coX*1GkPH7S8+`$lSc)q5ETZ~}Gp{sGO1|7ai^1t&oPMRIO)2^uTC-bMG)$aM zE?qu{Z-GX)HJl-t+(m(9-^QN-=MnQo9C)QM3|TF@#Av-zE!ydVB0@H?+zscqy49ue z7a-Spz~oq_^1w}x&N3$IH)Br<12kg6MW|LQD7i?{UTFK6Qi|}}%6W_&gawTEJgHVI z+dp9nc9i|Z&6Fu%k?KXfLN`V~X+6d2O6i@6B3^bx?oM~)?)7NS6(~f8 zyK6TfK4kJQ-li3l@vkC9$AESL$s%IiDLvNRi~O(RDrtUPhDNr#G zW){C(MNe%GMO{PYETDpAJgemNe)Nw65exd|irsh;sM`6u!ty?^_tT+AlZdziv{O8xICV(=-k?H+RI)NXegbif_wt=z{Ko9$i+*;C41cN{Nv zAH~!9fJFK8!oGDM@#Yn{Q(=L-54n~iS8GwLQbj!K#)4AR<4tt46vIC|FSt|j1$XRR z{}--}F0NKZ`p|$fjE7oWK>=KGuT4?Q@L`uaK~a;Ql_~6Ht*;}->nIo;Om9y0%8{&3 z)TCU|SmyfNonoK62k?SE1BtBSr9`>Vnb&w;>>q&o^$t@4@7qb+@Yh&q3oq8=eD+tchv zAiq=bBSjp>R?Hp))uq;t@g&!wWslKw9Ei%05(Vl=}hvd&Fvln zrL&W+Jw+AH!%=pHN)K0l#7q1$NN|JIGer~G+e{BK8{~nLOP`^LuF}hA=pSDh;X#!n zsvPY69O`Xo_;WbuN&BCphCHVlFVH5;S?)pGmU}qxCu90*5AElB1kHP)XoNa-T%w1gk5|*7P|0Qy23RS*D_O+zAS6K1z=rT2WrLdyquh4)MQ}HWg z;y|)^jcS>OW0X-XC(zEXqc@0Z^cxRa`UZ(;5QjeL zJ|;1tk~c7Z<2~h9z&OU{vAFj?2ApYF1wJ}lXlDgJIxK#21&6ozg8A@^gUq*zNZBQn z;jRi4&9S$5Z#ZwM^~BmFeRO0{c_pe6y4{!}u7n#nL#z_5 zN0KFF{D=9q^mpiR&!PG6FxvjF3+GDSqmtKht%JjB*!*Acdo=2e>S)OZy@zX9a4K1V zc9c-VUju4Tz<>CV2qe{i_$+N2LvA1NjW#o;js@?6*Zcr4+{pCPZM9p47j(SAL>9KaO@e@5qM8V-#6 zhKLG=Ad4hV9)5<|D;PdubY4~+tS%f2FQrE;<_l!KvNSRy8vg}R*1`6$Z-`R)7t&PB zn0ad%<3h<>!MJ=yhsR>8hB0M2=Hz*#np@UKm_jhQ`k+hu$NEEZkQitva;_mv*bPp@jn0t zV`CVTY-6AUc7lDw>WsW+aM}DP5|JB788t*O(VmXv^agG%{Rw+RLOEUb6P3mtK!;EEbl z#Z!}ss#<(A%`+X*L9kBKk|hWt2Ey7gCZ3L2C~fJs0G+$(oQdn`iIJOcXe`^vK%eNs z>3uSU+N9z@MPYhW|2uS=)eza*2UQJtFtit!y4Hkh$9^0f?FVytHQ{({CTD*8=BJH`fY+Zyh9m#+Lz0>o$`VFc7eY8_~Bw z9NpJ(5WA_KKCNC?PnR9m=_BmiprH+E_)nZHf58Gi8gJ6jF0AM0BlOv=VXP?AQp8zY z|8Qo&KlLo>0@HbXjx)x!#5iH&MGY{a%v$iL^A*nIT&bt76nAPNuAFOXMJrUCe{NEm zjc7+-Z$f9Q9PQC$E9g|-<+QOi3cTrloHU$&4R4TJR-(Nypj0Enn%ZX!I8rZbRFeiT z7;vRS*2n<;N(S7>!A1<#AxgCo+Y0vX6zGU--f1o3+#YZ5n$*$V+lZEhBg5Nyduz80 z&Ah9ts@M8N*uuMv=3Lgt7V(58a!_C^HWTJ2Yk)Nc*rB*=(m4}gkNa_^*@>~jAcYo_ zukhBTH+IO(?%gzW8{up(4X`4!-q7jgj<)tu|7t#7#fn9ioKy$Vkbc^u-e(QgFpI~xnCpC16u;>B@au-ytlHb69jEk_iJz^+wXpaj*y}HB| zC%B1L^w~vhB5b)?oiWS3g*+W+i*ldsiV*VGTJjQCF;du2p~bwe@HV7aH)PP;Pa3+Z zY@ewQ=>+>Y(Q6k}(+W2+NY>m=qUuqOosW9qXkT~HL3Xybn$LF^BW3SBexn)`Q^&`U zAG5TvzK`})F4b47-LLOsz~VQ@`d%1&5G3)TbXJB3<~^gb!qut`>WC4tz$giM(egT? zT-HpfMzfVZCNyREwAWJ%lZ6hJkQ3dliwfpE3~oBt zh5I*${tvJ2J#EdZ=Kk`m-)hZe_7i_26H4_G<>>QH^b)(t{+q3q$g_R0Fq$3fT&Esd zzf}}lPwcF7n9A#+7Vobo`e0BS|96o+UFAcUFCpU<-lC7pW3_~^vD6zzS5UdPm?Q6o5RvL^}h-VS@OBNt;mN55x)!IAr|~N(;uLSrLe9GLFf`WFY|) zUm7OwM|~45-_O?a)ov^g)Y7Or(%L{VR1lptOp?&rgM+CazF5u`gxETHaV9@Vj2F7r z*8n|QQQy~s>EI@`VEAiEsljNe#$skHSoENya$j4Pd+4J@(wa~_-mW14{nbDfB8CfY z*gh4Clq>%}XsZHHN#V8C0<%Nn#!r$$Ckf`*!&POsZv^gQ z?mS!~XwGn7Y%Py~dagEO6)83r(y?G5TC7EVBO#ut23sQWY7QLFCEp@p>fQtnOegwc zcfLLz(cY7*Re_F^H40`VtvbEY5Lq&1nig#xg^Jl|CTF@tiDAOUSseVF?Z8gW&if;g%HQ!hJV|+BlMT@NjXB^HOjqcCJXq0=&A|B{{Gz#`9$z#M=OjKsZ zh~0%d%V|)I=u1K@GP=VWS`#b!lhK7p_4m}*wbhKjpuAX7sk1jX4mGda-%Q&k_k=GN zR@FvHXUD<8GY6|!+)j1KSG{iRBSaWkuxKP=7Xh}@@NkZFz7MMN-bM(w9lL3&ti5f7 zd`&<1o4{5KJL=mQY86)`mRxW8n$fn#D2!J(xLy-<2j|??NNwn`Db_F=lvOK>FW5+f zSa|TLG8DdaS;x8ZYFY=&HCm>$DFaDs-V|}CV!w?#uqjP%ib5)TPJuCa)c$RX3b_&Q zMF#wv;kEj2B?r@+i3!4`|2TNs486e-*i6gRqT^9A`+oxyqIj{n@Ii+vTS4q4_^F2$ zk`7?``5uAME^PTZUbSlAC-bwXql`({W8(T$Hxh~SX#r>d(Ps>n9hv%}C++8_K8EZl zlbtrv$Q&uICBomgWXw7;Pr!GO+@6D{3@mctpr94L_Hc4vEAhIp-IsAq$kflzo1bph zGzn%Ea!wb>LBp&$-DR|Q2&1DYBpGc?zc>cMX=O4#HpWv%ifB*IlkxuA$NqQcyCdUM zP+R_E#GgX%WGZis-r0*3F$^y}w^XsGFtt7Bwx%LKuXW+TG!5DQRpg+ktDgzE*`WeZ z8eEvuRYP0T^E4#*YCq0w8H3%l9a}@^5teGS7DH%rYeZB*Wv#^?>?=)*yvt}mS6Z2Z z6fJ23)sYxrU_OC1q|!F%v6)WhOl(_pkgniPGA0Hvqz!HHWDD7x`NqV?IUJPDMWHC$ zi4BA`b2SW(%5I0;aHG%dpqjjhF1N>1oZKE+*B=uh3@mGpe(F^!X^-YvSjoA_mGH#z z6JESa*ZOI{e`c=b3Q-&UENOWM*sQTZ!`P8QI`U$}77FNydee%5+iGCC6?ID*&EKDn z(#WIB>0%emSo?PrI}5>kSctkbVvnCWy);Fecex|-Wz{}LyOUWbWJ&S?N>ifwEQ-Ku zGqV$HWYO$SVs{LmzU?HY;KL)Xv)D$qs=$wq7Wmbs2)nA0h*8JB78ehtdVsP znqJia8(P^NX}`&Ibnj@^_$x^}fH&3;RmcZ3`l%AzVbZ5ra_T9nbc`vh zr`Q3rBG-G0DYAw*BdAlne;utkoG!gk8es`^xfi_bpWtsrzk1{03rntMtuB@HLXRz` zEz|R-S#ABTX-o_Xd|7V<5AVq7N4?QD$0*f0N`FfXmqBMtALy*kV6-3ES)mHtX3V`_ zjIk%PzG8&%PhSS?sB2$j;c!~mS4>7Ve9%{HD!VbtpDNeEvFLt4t0t?_l73>WtnL~$ zdecvgkzH9Qp{d-NSfnsZ! z-9vv0f9UVROZO3zWYe#Kc+_N+l8M*W@Q)NY2;If}Ow_l{5~xj|Gf|InNj^yIBsl(J z3iym3gyv~DW;upHJ8rOu{rye>M)YeC!nt^8=mx?MoTbKeNch6XBOIG&(xls#N*t5ZPb z!x4Xn!RQ&HC){C#7=pR|>=7{ZZA<_;j1BN0>fTPfOc&T zu%eYC@x3@>h8i875nw@ISeQ>a5>=ZTj}l{WC*QCqT3^XUFad_DWQpz}Sl(6IlID9Zq zv6QnoaCe-gTs`iST1V-i)0?Jbi4n-zf-F%KzD7}D7Di7Rjz@_ZP`~l$=>#{Sv*TlL%+PJ!|%@|+?z!^~LUDR}3^ z)BY)9BKCvHri#t+QfoC;Y@>6VRs+d&M$5@{w7f2vO@qUuC~_JCETO5>#FiMZzBCPe z==BsZ9nHyY%9<|5qhobyI#i;`dO`-N3c(BZy8rGS7zL?b|pvB^zcO z@;SJ{z+Zb9aG}yYuqmH|C|B;|%$PaoEX5tIJA{1`EXdFtgfBV4fyqgP zznFvYC>lu|a|(>hTrf?~aHh{(1Xy;CgCh(W6>(rX55CT$xOwO}_+IARZg8^m|H^6N z(?A>gK2P+P{bcAZDoxCM6zsZN5^|t>x8&*}|Izp4c;hS*4P|}qsf9o1qiqT;SEB*t za`lMes0Dbs|9GtC^B15MD4~i4Vn^BPR}$$+w_d@TbUnwkg{X44t$Cr?R#x|eBz7l< zMQ9QBe^R5rFt*W&gF8-D#%eEt&Js6HCojQ3%W4k}Z2SXFDIz>b z{Z;yaDL(Mi$gFrA4VHpF63^+k@h~fGyO!2j)NCE7-7Wr*#KS1Q`1p`(+CaFr(fd&4ahzpK}N5H`IFe!s|gPo7HO~v=Nn9R6+3xXp`&B83Lgk# zaJI)Pbb(Z>FifzJ@>Yo*gh4Yow`pde71`gYy1FcEHR>qtY*~$GC}AnLpS3g)SKX|J zYZ1$7O&*%40m}n%Tt_>k(<={=VaOab_6Y2#svJ{6<8+F=JoF8*l4}jR7C&*bcOE<% zwgyAsueVUfIvBdR2IB%QI~Z^xzrRp*k7G7#9r}J!G7tU*>p%B!MzH|_9M*!_`?tj8 z9)K4E*TSz|hq=m+!+|)xlQF|F-Nnod+=y31vvu%hJgL^fo6dB69Y#qUFYvUbTtKrR z)#vpk)e3oW30F8*5{PxqtVoZqN7Q9F+k@qK=Dk1*?xVv7&{6j^v>h3)h4$jIYUZYv zX%sM_REDyK<5)oQsG3&IM;a{)s!EJG`EEoMU2#jeCT1inRCxA6i00e2&_TpD+A5>F#HlGYy4hIR=f#y^VNSE#)AAeBS#m1RGZz5)~4}i zjkpGl{)}f?x;o+MW*B?(MN8(i1%oO$wp4OmihAG{R`q{qF~fhLeo5iOe^tv4uU}eP z%3oEnglqG8@LrUTZBXqePJnBzK&34Cp)2IcR8Vk{OY zX=wAk@bf;_lW3S)C)rDGFJc|nLyIojD+cSl&fSMDV!fWEvkxso&wc2EqTSvnMzeEo z_MvaTr*{zM)T)N9rHzDL`UX*c-yj=G*pI&6e2%-Q82`BzWb-IrOM#W^=A2~tfk`q*iuLwM}I!L>pwuQzWKnu0zFUmNG z@w{6HP(%KefHRq`4YH($d(iJ~b`a)PlIkF8)C@Fp2a%4O2T^*eojjcLAvDR0Fi+2n z3^*h<#vv}d4v7k34sLv7T;UO<<m<%Gk?q;mzF{#FoVE78)ti?q+VFC957Hbjs0 z^I?2_ucFi=VlN@|9Hkw>qKCG}Q8B+9L0>{F3Zl|t)X*hGRWIDMqu`GfOS~J+J&KH* zd^L#X+zxW0!mCwUUzpb8>uTQR2%4CL0!*Y0xGhnhG`j!+_udbpOXUdo_I?l!I`*ns z3iGi5Ie~6sAwEE#Q$`_rTSXrvRa{?Mh^}BA98HYEai*AKcq5H7#!<$2I^Z{!da^Aa z*tvX6jFYvh6-<3<1qV~7<0#Q6ntB{*?nB3q;~TD*Loijk1v^vJ34F|4!LeOHzP!J+ z^#r`X=P6OONPZH%+_Dpx(p!lW>Yyc2p|UUXU{b~f;~LPD_$;>%myiQpJ1K_Be4{1g zMV{v|A=BX>*o%vkC_i$(hyn^}60F@NtJ6enDqN#pOJ7|!1eR`)U=7C}D=LxO6V z5i3L=S))h6qBCXk$@OVE5@MT*H4nzijEee z;)LU}jeH@*jt1z2V0{rwkH;k_&!?D6VtXv7U3^JQ!^h#%OZcwn=MqA*T|#_tR4wYa zK8-2Ccjqg*RDzWJQIASWkS}NMVL9os#aK={);C1^L#?kbjP)etaEoczzk<)Q=Mfw@M~0YURTSE;O;I7*d5%HDD;S`~6$V$t z4l*GwgxumnaA+Kx61&|fM7!pnZzKur>D<3)>cpm$aUJ93E1QOBAE_B}6`pmC=dyD* zQQxu|vk>R-G3|3#@$Oiez(Ma@aI@hxh`q5AjfqEI!x%v`+I0=D?{8Rvb{)+`!`9Uz zXwbHr|Mf_99m;=npo|;n29;dL`)@JUr7=*P9-=;Pv)K(4%oS4Iz_-qECC$H$?Eaz* zF(W_Is=sv^n z47a7SIFQ0X;dDO^e)dBd{62j2`haQ?o*tmq8W5sKx3A&Zaf1cn#2`jH(zij?a$y)` zNYK37VxsKb@DQ>X5mJvAn(ZA_R3H=>qzQFVt*`Ar$p#=9_Bdrk;7oD<^0SM?g)L%`ViAtcTZapS`@9T-K6 zL+IC%5Kp?ZI0Wly?jhQ;B@$Jif5q6|ht}&gA#{0d2xh+j3Q_NC=*sxrYbD;9R^CUz zxD9Hp`}g5eoqRQVQ;H8gMLvp1>S}c=MZi9|%Ud!i3D@op!GR&AFtKajZ$1n|Kb~b` z7h%yks#}JNCtX=~s0^7}kFJ%WLM2?IK@ZUvi7sbFuLfhwF|xVq7L#Dc@B!v4%F0pN zGwy4cSfSMe2Jof*AOy3uY)9SB2PnrjIJ;O)qx1C?wdU;uSgw4{>AG)1Ecq~TyN7V% zJ7qq^hfFuR{19WF)4wvMTGa3nD))sS9K3mi^Wj!K!Xl~k8aTWe4plr7<1qdwe~ffp zH)32}axe~6FK)W}7+$QkWV9!>{D=p${u40iwv4Goho8V)-zPA)!AZk360W&%5bgy# z_Qs*=nFkv{ts9?py);@jJQeg5whMhY{njTGXZ*3O_z0SH7oH)98-`HEbJT3f7kUPL z(=g5yKf~u^VH^iO&oOZ}id4_h!@$Du=i+!wh{#`vT?HYPc0I>p&m%AJ{qdwop(cZspo%qd349FWEMFXOS=6ZqNph4Ok^3%Np}>t{{VA1 z3;lzr!1(^94;Tfw(3(m<;PV3oE0=xk7?#^F%!UFwg{dEh!yoaGeiSvdKCc1BpFpSd zV6+FtybV#;wEm1P@5^X!^*l;unDz_Ii0*$9`v^3QNMQy)|jbtlk zh0)%LU0DjDBuHMoG)+U;jW< zcehEDJ+0q_UV=2L7V#6gQ~WpW`iXw0@qsXP&uPSwYL(Kcs;6P9TPXunJ>RxFc17lx0nl zP<@*95QBma)5Ep9YlhP$p&hNzL$nyn(^D!jPwA(x>?9=SQn8-Wm5FSqP#?vWvof3- zZ3{>La%EL&Obx(${-x$)43te}OV|BI*s{7jtT-|F5=Q8M$sj{2QDninA zp|)nqP}#Z+31JJc8Oq{HcL}xNNoryahqm>SC^!023)xJJYML)m_-L?H>eE$onAv{# zH)Tlv7RnUahC*%XX<-BP`uiIeFfr|Kp0c|7XJm^r4GuL|2 zbSq^H#v)EzDLZ14&ed8Oflf*%YvrFhXX&`LvV+jK4~?@?)}a_1Wi%ErjI~j=lYJW- zK@Q_0JSf=>A4z(wb=W;If-X;taOclWdr&&Kdcs!O3O5lQwMBvD&WNCWGa_(*ANvxC zw}XNWW!Ndxgx+&0&;j|ma&CnBX?_8z5V4HYwe9i54L`=gmSYhZKDNgbJNz_f*0W1v zJC#OgKmG}&OooHj5EOG)atCFS(BcW>YE#nl2;{GWGFqsGWn*f_o-eTvazwmXbyP*0 zQp<`6Yu*)I$z;2+o=hFYlzbmY=+jF_Jl|`{rZ!6X$hU}G>qzw5zEve;MQsE+TZ7g( zDXpEw@bH&5-tQ!X|-6Nftj*l>YfQB)k!U2)G zG^(`f18lb&;(1LM-INN!Y$(&Sqi5DA=uYmaFw2HV(!t@dbAj#ST?a}hfC}BAdl(lc zyDM@3iU&&1k_LI8hBje8R`ibtn$foNX_tqx7EPTWiOT}(AenWjZyjZXEOl`trqYni ziy&p0OCo9Rl1NW-@q`La()C1o!x>3Cgb2U+mNNP2r7ClR6@mFpa!o?gl*+0!eL z6mu0$6?wra-+v_(MD^S&e+)zVKZVdXs5GjP04vRn{P4{F|Q=?@j@~BK7E= z4+=3rp!tz_dGr?=8j@jFm3ny1hT0F67w$Y`TI1_0LxeH<99*lf3=+cJIXG6oVGZ*0 zMJ+e;)MDC?$LoBVFDla>oHEW#NQ$?8mFOVX*DyvjyE~k3;RnS6e+^wDH=v;&&Gmzy zEkk~@RO+V;#0fDL{>t{kv{)^{9Dmq7-k39G{>o6He!P~re>}{YOhQbp0}zvlgUp#r zIRQx35*%O7z#|57Q#go}1BAEXU>*YkjxlFyaqW>`Z{_f;VMoqH1u8=@0y-#A*-mJU zVF{)+PYErDHi)o$*J>HswkubN2|^WWM}vY8X!=CPxzmdvWw3B~3J2e&G&GRd>3Vw=Z_-Z6^t}RWpi0YFpMml%Rzhyz}|%%tOUR|-^(G$ zyz9$2*D4eWZC7#d-v~VOheKiP+8WM$VdAh&9HfS!*k5erU_%%r<-0lf0e}y}z;IYL zD&*YOa46v9IVojrN5isX16baKQ`T8pJ2rr^9+x=S$-ogDxy~ftuQb#rlL%z+mFt}8 z90Bn&TqefER+VTHq-39qfW7DUIIS1S?3Ht%0AS-PkxGmynNfL^(uJbUq72z%;Um1X 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 From 8de13b410b255b2e4faef003f5cfd4e0ec7b5e62 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 14:42:58 +0700 Subject: [PATCH 037/274] dev delete config --- .../nocopy/service/FileSimilarityService.java | 22 +++++++-------- .../file/impl/FileUploadServiceImpl.java | 28 +++++++++++++++++-- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 8d809f7..8ad1d12 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -152,17 +152,17 @@ public class FileSimilarityService { return duplicates; } - public void hasDuplicatesByHash(String path, Long userId, String mimeType) throws Exception { - String hash = calculateFileHash(path); - List fileEntityList = fileEntityRepository.findByUserIdAndMimeType(userId, mimeType); - - for (FileEntity file : fileEntityList) { - if (calculateFileHash(file.getFilePath()).equals(hash)) { - throw new DuplicateImageException("Duplicate", file.getId(), - file.getUserId()); - } - } - } +// public void hasDuplicatesByHash(String path, Long userId, String mimeType) throws Exception { +// String hash = calculateFileHash(path); +// List fileEntityList = fileEntityRepository.findByUserIdAndMimeType(userId, mimeType); +// +// for (FileEntity file : fileEntityList) { +// if (calculateFileHash(file.getFilePath()).equals(hash)) { +// throw new DuplicateImageException("Duplicate", file.getId(), +// file.getUserId()); +// } +// } +// } // public Page findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable, // String authToken) throws Exception { diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index 1b31859..6c029a1 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -2,6 +2,9 @@ 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 lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -321,8 +324,29 @@ public class FileUploadServiceImpl implements FileUploadService { if (session.getFileType().startsWith("image")) { checkForDuplicatesSynchronously(finalFilePath); } else if (session.getFileType().startsWith("audio") || session.getFileType().startsWith("document")) { - fileSimilarityService.hasDuplicatesByHash(finalFilePath, session.getUserId(), - session.getFileType()); + File file = new File(finalFilePath); + FileProtector.Type type = "document".equals(session.getFileType()) ? + FileProtector.Type.DOC : FileProtector.Type.AUDIO; + + 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()); + } } } From 74ba9709a9916131cecea5a128a926efe0005322 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 15:39:26 +0700 Subject: [PATCH 038/274] dev delete config --- .../nocopy/controller/ApiController.java | 82 ---------------- .../nocopy/service/FileSimilarityService.java | 95 ++----------------- .../service/file/FileStorageService.java | 3 - .../file/impl/AudioLocalSearchImpl.java | 2 + .../file/impl/FileUploadServiceImpl.java | 6 +- 5 files changed, 15 insertions(+), 173 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 45a0721..9211860 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -157,88 +157,6 @@ public class ApiController { private final NoCopyFileService noCopyFileService; -// @GetMapping("/v{version}/files/{fileId}/similar") -// public ResponseEntity findSimilarFiles( -// @PathVariable("version") int version, -// @PathVariable String fileId, -// @RequestParam(value = "auth_token",required = false) String authToken, -// @RequestParam(required = false) List similarityLevels, -// @PageableDefault(size = 20, sort = "hammingDistance") Pageable pageable) throws Exception { -// SimilarityFilter filter = SimilarityFilter.builder() -// .similarityLevels(similarityLevels) -// .build(); -// FileEntity fileEntity = fileEntityRepository.findByFileId(fileId); -// Page similarFiles = null; -// List allResults = null; -// List errors = new ArrayList<>(); -// -// if (fileEntity.getMimeType().equals("image")) { -// similarFiles = fileSimilarityService.findSimilarFiles(fileId, filter, pageable, authToken); -// } else { -// File file = new File(fileEntity.getFilePath()); -// FileProtector.Type type = fileEntity.getMimeType().equals("document") ? FileProtector.Type.DOC : -// FileProtector.Type.AUDIO; -// -// NoCopyCheckResult noCopyCheckResult = noCopyFileService.checkFile(file, type); -// FileEntity entity = fileEntityRepository.findByFileId(fileId); -// -// switch (noCopyCheckResult) { -// case DocumentCheckResult.Success success -> { -// FileProtector.FileInfo info = success.getInfo(); -// allResults.add(fileSimilarityService.buildDTO(fileEntityRepository.findByFileId(info.getId()))); -// log.debug("File processed successfully: {}", entity.getFilePath()); -// } -// case AudioCheckResult.Success success -> { -// FileProtector.FileInfo info = success.getInfo(); -// -// allResults.add(fileSimilarityService.buildDTO(fileEntityRepository.findByFileId(info.getId()))); -// log.debug("File processed successfully: {}", entity.getFilePath()); -// } -// case DocumentCheckResult.Failed failed -> { -// String errorMessage = String.format("Document check failed for %s: %s", -// entity.getFilePath(), failed.getMessage()); -// log.warn(errorMessage); -// errors.add(errorMessage); -// } -// case AudioCheckResult.Failed failed -> { -// String errorMessage = String.format("Audio check failed for %s: %s", -// entity.getFilePath(), failed.getMessage()); -// log.warn(errorMessage); -// errors.add(errorMessage); -// } -// default -> throw new IllegalStateException("Unexpected result type: " + -// noCopyCheckResult.getClass().getSimpleName()); -// } -// -// } -// -// -// String messageDesc; -// MessageCode success; -// Map responseData = new HashMap<>(); -// -// assert similarFiles != null; -// if (similarFiles.isEmpty()) { -// messageDesc = MessageCode.FILE_NOT_FOUND.getDescription(); -// success = MessageCode.SUCCESS; -// responseData.put("content", similarFiles.getContent()); -// responseData.put("page", similarFiles.getNumber()); -// responseData.put("size", similarFiles.getSize()); -// responseData.put("totalElements", similarFiles.getTotalElements()); -// responseData.put("totalPages", similarFiles.getTotalPages()); -// responseData.put("hasNext", similarFiles.hasNext()); -// responseData.put("hasPrevious", similarFiles.hasPrevious()); -// } else { -// messageDesc = MessageCode.SIMILAR_FILES_FOUND.getDescription(); -// success = MessageCode.SUCCESS; -// responseData.put("content", allResults); -// } -// -// -// return ResponseEntity.ok() -// .body(new BaseResponse(20004, success.getCode(), messageDesc, responseData)); -// } - @GetMapping("/v{version}/files/{fileId}/similar") public ResponseEntity findSimilarFiles( @PathVariable("version") int version, diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 8ad1d12..4219c9e 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -46,51 +46,9 @@ public class FileSimilarityService { private final FileEntityRepository fileEntityRepository; -// @Lazy -// private final NoCopyFileService noCopyFileService; - @Value("${server.baseurl}") private String baseUrl; - public List findSimilarFiles(String fileId) { - var imageHashEntity = hashRepository.findById(fileId) - .orElseThrow(() -> new RuntimeException("Hash not found")); - - Long hash64Hi = imageHashEntity.getHash64Hi(); - Long hash64Lo = imageHashEntity.getHash64Lo(); - - List 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 findDuplicateByHammingDistance(String fileId, int hammingDistance, int duplicate, int similar) { List candidates = repository.findCandidates(fileId); @@ -152,50 +110,17 @@ public class FileSimilarityService { return duplicates; } -// public void hasDuplicatesByHash(String path, Long userId, String mimeType) throws Exception { -// String hash = calculateFileHash(path); -// List fileEntityList = fileEntityRepository.findByUserIdAndMimeType(userId, mimeType); -// -// for (FileEntity file : fileEntityList) { -// if (calculateFileHash(file.getFilePath()).equals(hash)) { -// throw new DuplicateImageException("Duplicate", file.getId(), -// file.getUserId()); -// } -// } -// } + public void hasDuplicatesByHash(String path, Long userId, String mimeType) throws Exception { + String hash = calculateFileHash(path); + List fileEntityList = fileEntityRepository.findByUserIdAndMimeType(userId, mimeType); -// public Page findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable, -// String authToken) throws Exception { -// -// var imageHashEntity = hashRepository.findById(fileId) -// .orElseThrow(() -> new RuntimeException("Hash not found")); -// -// Long hash64Hi = imageHashEntity.getHash64Hi(); -// Long hash64Lo = imageHashEntity.getHash64Lo(); -// List candidates = authToken.equals("all") ? repository.findCandidates(fileId): -// repository.findCandidatesFromUserFiles(fileId, authTokenRepository.findUserIdByToken(authToken)); -// -// List similarityLevels = (filter != null && filter.getSimilarityLevels() != null) -// ? filter.getSimilarityLevels() -// : List.of("DUPLICATE", "SIMILAR", "DIFFERENT"); -// -// List allResults = candidates.stream() -// .map(c -> createSimilarFileResponse(c, hash64Hi, hash64Lo)) -// .filter(response -> similarityLevels.contains(response.getSimilarityLevel())) -// .sorted(Comparator.comparingInt(SimilarFileDTO::getHammingDistance)) -// .collect(Collectors.toList()); -// -// int total = allResults.size(); -// int page = (pageable != null) ? pageable.getPageNumber() : 0; -// int size = (pageable != null) ? pageable.getPageSize() : 20; -// -// int fromIndex = Math.min(page * size, total); -// int toIndex = Math.min(fromIndex + size, total); -// -// List pageContent = allResults.subList(fromIndex, toIndex); -// -// return new PageImpl<>(pageContent, pageable, total); -// } + for (FileEntity file : fileEntityList) { + if (calculateFileHash(file.getFilePath()).equals(hash)) { + throw new DuplicateImageException("Duplicate", file.getId(), + file.getUserId()); + } + } + } public Page findSimilarFiles( String fileId, diff --git a/src/main/java/ru/soune/nocopy/service/file/FileStorageService.java b/src/main/java/ru/soune/nocopy/service/file/FileStorageService.java index 421f577..1a7a2fb 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileStorageService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileStorageService.java @@ -15,9 +15,6 @@ import java.nio.file.Paths; @Slf4j public class FileStorageService { -// @Autowired -// private FileStorageConfig storageConfig; - @Value("${file.storage.base-path}") private String basePath; diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java index 6ac96bb..dd47c08 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java @@ -26,6 +26,8 @@ public class AudioLocalSearchImpl implements AudioLocalSearch { public @Nullable FileProtector.FileInfo findBySignature(@NotNull String signature) { FileEntity fileEntity = fileEntityService.findBySignature(signature); + if (fileEntity == null) return null; + return fileUtil.createFileInfo(fileEntity); } diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index 6c029a1..bc080c0 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -315,15 +315,15 @@ public class FileUploadServiceImpl implements FileUploadService { boolean isLastChunk = session.getChunksUploaded().equals(session.getTotalChunks()); if (isLastChunk) { - log.info("All chunks uploaded for session {}. Starting assembly...", - session.getUploadId()); - String finalFilePath = assembleFileSynchronously(session); if (findSimilar == 0) { if (session.getFileType().startsWith("image")) { checkForDuplicatesSynchronously(finalFilePath); } else if (session.getFileType().startsWith("audio") || session.getFileType().startsWith("document")) { + fileSimilarityService.hasDuplicatesByHash(finalFilePath, session.getUserId(), + session.getFileType()); + File file = new File(finalFilePath); FileProtector.Type type = "document".equals(session.getFileType()) ? FileProtector.Type.DOC : FileProtector.Type.AUDIO; From b982cb96c1b5b0871553f9fb7427734ca890c54d Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 15:52:55 +0700 Subject: [PATCH 039/274] dev delete config --- .../file/impl/FileUploadServiceImpl.java | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index bc080c0..1967dc7 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -5,6 +5,7 @@ import com.vrt.fileprotection.FileProtector; import com.vrt.fileprotection.NoCopyCheckResult; import com.vrt.fileprotection.audio.AudioCheckResult; import com.vrt.fileprotection.documents.DocumentCheckResult; +import com.vrt.fileprotection.image.ImageCheckResult; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -318,15 +319,19 @@ public class FileUploadServiceImpl implements FileUploadService { String finalFilePath = assembleFileSynchronously(session); if (findSimilar == 0) { - if (session.getFileType().startsWith("image")) { - checkForDuplicatesSynchronously(finalFilePath); - } else if (session.getFileType().startsWith("audio") || session.getFileType().startsWith("document")) { - fileSimilarityService.hasDuplicatesByHash(finalFilePath, session.getUserId(), - session.getFileType()); +// if (session.getFileType().startsWith("image")) { +// checkForDuplicatesSynchronously(finalFilePath); +// } else if (session.getFileType().startsWith("audio") || session.getFileType().startsWith("document")) { + if (session.getFileType().startsWith("image")) { + checkForDuplicatesSynchronously(finalFilePath); + } else { + fileSimilarityService.hasDuplicatesByHash(finalFilePath, session.getUserId(), + session.getFileType()); + } File file = new File(finalFilePath); FileProtector.Type type = "document".equals(session.getFileType()) ? - FileProtector.Type.DOC : FileProtector.Type.AUDIO; + FileProtector.Type.DOC : FileProtector.Type.valueOf(session.getFileType()); NoCopyCheckResult checkResult = noCopyFileService.checkFile(file, type); @@ -336,6 +341,11 @@ public class FileUploadServiceImpl implements FileUploadService { throw new DuplicateImageException("Duplicate", info.getId(), Long.valueOf(info.getOwnerId())); } + case ImageCheckResult.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(), @@ -345,9 +355,11 @@ public class FileUploadServiceImpl implements FileUploadService { log.warn("Document check failed: {}", failed.getMessage()); case AudioCheckResult.Failed failed -> log.warn("Audio check failed: {}", failed.getMessage()); + case ImageCheckResult.Failed failed -> + log.warn("ImageCheckResult check failed: {}", failed.getMessage()); default -> log.warn("Unexpected result type: {}", checkResult.getClass().getSimpleName()); } - } +// } } session.setStatus(UploadStatus.COMPLETED); From 67690db7c0c3b2f502ef6a8f50eb63a86683b54f Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 15:57:01 +0700 Subject: [PATCH 040/274] dev delete config --- .../soune/nocopy/service/file/impl/FileUploadServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index 1967dc7..314946e 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -331,7 +331,7 @@ public class FileUploadServiceImpl implements FileUploadService { File file = new File(finalFilePath); FileProtector.Type type = "document".equals(session.getFileType()) ? - FileProtector.Type.DOC : FileProtector.Type.valueOf(session.getFileType()); + FileProtector.Type.DOC : FileProtector.Type.valueOf(session.getFileType().toUpperCase()); NoCopyCheckResult checkResult = noCopyFileService.checkFile(file, type); From 18de1ab91b2cac3b9c5a64908706f5284ffb5ca7 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 16:01:47 +0700 Subject: [PATCH 041/274] dev delete config --- .../soune/nocopy/service/file/impl/ImageLocalSearchImpl.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/ImageLocalSearchImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/ImageLocalSearchImpl.java index 79b8963..5dbb20a 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/ImageLocalSearchImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/ImageLocalSearchImpl.java @@ -46,6 +46,9 @@ public class ImageLocalSearchImpl implements ImageLocalSearch { ImageHashEntity imageHashEntity = 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(), From ec93afa70519a47ac51e0c61c22a359afd34b302 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 16:15:55 +0700 Subject: [PATCH 042/274] dev delete config --- .../file/impl/FileUploadServiceImpl.java | 24 ++++++------------- .../file/impl/ImageLocalSearchImpl.java | 3 +-- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index 314946e..1a09541 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -6,6 +6,7 @@ import com.vrt.fileprotection.NoCopyCheckResult; import com.vrt.fileprotection.audio.AudioCheckResult; import com.vrt.fileprotection.documents.DocumentCheckResult; import com.vrt.fileprotection.image.ImageCheckResult; +import com.vrt.fileprotection.image.ImageLocalSearch; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -319,15 +320,11 @@ public class FileUploadServiceImpl implements FileUploadService { String finalFilePath = assembleFileSynchronously(session); if (findSimilar == 0) { -// if (session.getFileType().startsWith("image")) { -// checkForDuplicatesSynchronously(finalFilePath); -// } else if (session.getFileType().startsWith("audio") || session.getFileType().startsWith("document")) { - if (session.getFileType().startsWith("image")) { - checkForDuplicatesSynchronously(finalFilePath); - } else { - fileSimilarityService.hasDuplicatesByHash(finalFilePath, session.getUserId(), - session.getFileType()); - } + if (session.getFileType().startsWith("image")) { + checkForDuplicatesSynchronously(finalFilePath); + } else if (session.getFileType().startsWith("audio") || session.getFileType().startsWith("document")) { + fileSimilarityService.hasDuplicatesByHash(finalFilePath, session.getUserId(), + session.getFileType()); File file = new File(finalFilePath); FileProtector.Type type = "document".equals(session.getFileType()) ? @@ -341,11 +338,6 @@ public class FileUploadServiceImpl implements FileUploadService { throw new DuplicateImageException("Duplicate", info.getId(), Long.valueOf(info.getOwnerId())); } - case ImageCheckResult.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(), @@ -355,11 +347,9 @@ public class FileUploadServiceImpl implements FileUploadService { log.warn("Document check failed: {}", failed.getMessage()); case AudioCheckResult.Failed failed -> log.warn("Audio check failed: {}", failed.getMessage()); - case ImageCheckResult.Failed failed -> - log.warn("ImageCheckResult check failed: {}", failed.getMessage()); default -> log.warn("Unexpected result type: {}", checkResult.getClass().getSimpleName()); } -// } + } } session.setStatus(UploadStatus.COMPLETED); diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/ImageLocalSearchImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/ImageLocalSearchImpl.java index 5dbb20a..a6ea133 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/ImageLocalSearchImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/ImageLocalSearchImpl.java @@ -44,8 +44,7 @@ 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; From a4e3d62f1458f9e6162dd4be4776202687974a91 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 17:07:28 +0700 Subject: [PATCH 043/274] dev delete config --- .../nocopy/handler/RegRequestHandler.java | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java index 6f69ac2..f6a988d 100644 --- a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java @@ -12,6 +12,7 @@ import ru.soune.nocopy.dto.register.RegAnswer; import ru.soune.nocopy.dto.register.RegRequest; import ru.soune.nocopy.entity.referral.Referral; import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.entity.user.EmailVerificationToken; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.ValidationException; import ru.soune.nocopy.handler.validator.RegRequestValidator; @@ -78,17 +79,17 @@ public class RegRequestHandler implements RequestHandler { } AuthToken authToken = authService.register(regRequest); -// EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken); + EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken); -// try { -// emailService.sendVerificationEmail(authToken.getUser(), emailToken.getToken()); -// } catch (Exception e) { -// return new BaseResponse(request.getMsgId(), MessageCode.SEND_EMAIL_EXCEPTION.getCode(), -// MessageCode.SEND_EMAIL_EXCEPTION.getDescription(), Map.of( -// "message", "not send", -// "email", regRequest.getEmail() -// )); -// } + try { + emailService.sendVerificationEmail(authToken.getUser(), emailToken.getToken()); + } catch (Exception e) { + return new BaseResponse(request.getMsgId(), MessageCode.SEND_EMAIL_EXCEPTION.getCode(), + MessageCode.SEND_EMAIL_EXCEPTION.getDescription(), Map.of( + "message", "not send", + "email", regRequest.getEmail() + )); + } String token = authToken.getToken(); @@ -96,10 +97,13 @@ public class RegRequestHandler implements RequestHandler { RegAnswer regAnswer = new RegAnswer(); +// regAnswer.setUserId(authToken.getUser().getId()); +// regAnswer.setToken(token); +// regAnswer.setVerified(true); +// regAnswer.setActive(true); + regAnswer.setVerified(false); + regAnswer.setActive(false); regAnswer.setUserId(authToken.getUser().getId()); - regAnswer.setToken(token); - regAnswer.setVerified(true); - regAnswer.setActive(true); if (regRequest.getAccountType().equals("b2c")) { referralService.onRegister(authToken.getUser().getId(), regRequest.getReferralLink()); From c54dfc994b38aec898068eb3e64e64d5d3de7576 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 18:11:32 +0700 Subject: [PATCH 044/274] dev delete config --- src/main/java/ru/soune/nocopy/service/mail/EmailService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java index b610f3e..5544bf2 100644 --- a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java +++ b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java @@ -42,7 +42,7 @@ public class EmailService { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); - helper.setFrom(standartAdressFrom); + helper.setFrom("noreply@no-copy.ru"); helper.setTo(user.getEmail()); helper.setSubject(standartSubject); From 9f67f0c06dfebfe68f79d56205ff3f7dd138993a Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 18:48:34 +0700 Subject: [PATCH 045/274] dev add check verified email --- .../soune/nocopy/dto/register/RegRequest.java | 4 +++ .../nocopy/handler/RegRequestHandler.java | 35 ++++++++++--------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java b/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java index af948fa..ef19795 100644 --- a/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java @@ -1,6 +1,7 @@ package ru.soune.nocopy.dto.register; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.*; import lombok.Data; @@ -33,4 +34,7 @@ public class RegRequest { private String authToken; private String referralLink; + + @JsonProperty("mail_verified") + private String mailVerified; } diff --git a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java index f6a988d..dc06ffa 100644 --- a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java @@ -80,29 +80,32 @@ public class RegRequestHandler implements RequestHandler { AuthToken authToken = authService.register(regRequest); EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken); + RegAnswer regAnswer = new RegAnswer(); - try { - emailService.sendVerificationEmail(authToken.getUser(), emailToken.getToken()); - } catch (Exception e) { - return new BaseResponse(request.getMsgId(), MessageCode.SEND_EMAIL_EXCEPTION.getCode(), - MessageCode.SEND_EMAIL_EXCEPTION.getDescription(), Map.of( + if (regRequest.getMailVerified().equals("true")) { + try { + emailService.sendVerificationEmail(authToken.getUser(), emailToken.getToken()); + } catch (Exception e) { + return new BaseResponse(request.getMsgId(), MessageCode.SEND_EMAIL_EXCEPTION.getCode(), + MessageCode.SEND_EMAIL_EXCEPTION.getDescription(), Map.of( "message", "not send", - "email", regRequest.getEmail() - )); - } + "email", regRequest.getEmail() + )); + } + regAnswer.setVerified(false); + regAnswer.setActive(false); + } else { + regAnswer.setUserId(authToken.getUser().getId()); + + regAnswer.setVerified(true); + regAnswer.setActive(true); + } String token = authToken.getToken(); authService.useUserAuthToken(token); - RegAnswer regAnswer = new RegAnswer(); - -// regAnswer.setUserId(authToken.getUser().getId()); -// regAnswer.setToken(token); -// regAnswer.setVerified(true); -// regAnswer.setActive(true); - regAnswer.setVerified(false); - regAnswer.setActive(false); + regAnswer.setToken(token); regAnswer.setUserId(authToken.getUser().getId()); if (regRequest.getAccountType().equals("b2c")) { From ac11de2abb9b95d294621bf15cd20112866c1200 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Feb 2026 20:17:34 +0700 Subject: [PATCH 046/274] dev add reset password --- .../nocopy/configuration/HandlerConfig.java | 2 + .../java/ru/soune/nocopy/dto/MessageCode.java | 3 +- .../dto/register/ResetPasswordRequest.java | 24 +++ .../dto/register/ResetPasswordResponse.java | 13 ++ .../ru/soune/nocopy/entity/user/User.java | 10 + .../nocopy/entity/user/UserNotActive.java | 32 +++ .../nocopy/handler/ResetPasswordHandler.java | 112 ++++++++++ .../handler/VerifyRegisterUserHandler.java | 5 +- .../repository/AuthTokenRepository.java | 2 +- .../repository/UserNotActiveRepository.java | 16 ++ .../service/auth/CleanUpNotActiveUser.java | 48 +++++ .../nocopy/service/mail/EmailService.java | 32 ++- .../nocopy/service/register/AuthService.java | 47 +++- src/main/resources/application.yaml | 1 + .../resources/templates/reset-password.html | 201 ++++++++++++++++++ 15 files changed, 534 insertions(+), 14 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/register/ResetPasswordRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/register/ResetPasswordResponse.java create mode 100644 src/main/java/ru/soune/nocopy/entity/user/UserNotActive.java create mode 100644 src/main/java/ru/soune/nocopy/handler/ResetPasswordHandler.java create mode 100644 src/main/java/ru/soune/nocopy/repository/UserNotActiveRepository.java create mode 100644 src/main/java/ru/soune/nocopy/service/auth/CleanUpNotActiveUser.java create mode 100644 src/main/resources/templates/reset-password.html diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index f4465ef..87e703b 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -24,6 +24,7 @@ public class HandlerConfig { TariffHandler tariffHandler, TariffInfoHandler tariffInfoHandler, ReferralHandler referralHandler, + ResetPasswordHandler resetPasswordHandler, DaDataHandler daDataHandler ) { Map map = new HashMap<>(); @@ -35,6 +36,7 @@ public class HandlerConfig { map.put(20007, imageFoundRequestHandler); map.put(20008, authRequestHandler); map.put(20009, verifyRegisterUser); + map.put(20010, resetPasswordHandler); map.put(30000, companyHandler); map.put(30001, tariffHandler); map.put(30002, tariffInfoHandler); diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index d6a1379..1a53d2d 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -44,7 +44,8 @@ public enum MessageCode { COMPANY_NOT_FOUND(4, "Company not found"), COMPANY_ALREADY_EXISTS(2, "Company already exists"), USER_LIMIT_IS_OVER(2, "Over user limits"), - ERROR_TARIFF_INFO(2, "Erorr with tariff info"); + ERROR_TARIFF_INFO(2, "Erorr with tariff info"), + USER_NOT_ACTIVE(2, "User not active"); private final Integer code; diff --git a/src/main/java/ru/soune/nocopy/dto/register/ResetPasswordRequest.java b/src/main/java/ru/soune/nocopy/dto/register/ResetPasswordRequest.java new file mode 100644 index 0000000..60ab799 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/register/ResetPasswordRequest.java @@ -0,0 +1,24 @@ +package ru.soune.nocopy.dto.register; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ResetPasswordRequest { + + @JsonProperty("email") + private String email; + + @JsonProperty("verifyToken") + private String verifyToken; + + @JsonProperty("action") + private String action; + + @JsonProperty("password") + private String password; +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/dto/register/ResetPasswordResponse.java b/src/main/java/ru/soune/nocopy/dto/register/ResetPasswordResponse.java new file mode 100644 index 0000000..b3d7122 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/register/ResetPasswordResponse.java @@ -0,0 +1,13 @@ +package ru.soune.nocopy.dto.register; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class ResetPasswordResponse { + @JsonProperty("user_id") + private Long userId; + + @JsonProperty("authToken") + private String authToken; +} diff --git a/src/main/java/ru/soune/nocopy/entity/user/User.java b/src/main/java/ru/soune/nocopy/entity/user/User.java index b3bd024..cd8febd 100644 --- a/src/main/java/ru/soune/nocopy/entity/user/User.java +++ b/src/main/java/ru/soune/nocopy/entity/user/User.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import jakarta.validation.constraints.Size; import lombok.*; +import org.hibernate.annotations.ColumnDefault; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import ru.soune.nocopy.entity.company.Company; @@ -102,6 +103,15 @@ public class User { @ToString.Exclude private TariffInfo personalTariffInfo; + @Column(name = "failed_login_attempts") + @ColumnDefault("0") + private Integer failedLoginAttempts = 0; + + @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonIgnore + @ToString.Exclude + private UserNotActive userNotActive; + public TariffInfo getActiveTariffInfo() { if (company != null && company.getTariffInfo() != null) { return company.getTariffInfo(); diff --git a/src/main/java/ru/soune/nocopy/entity/user/UserNotActive.java b/src/main/java/ru/soune/nocopy/entity/user/UserNotActive.java new file mode 100644 index 0000000..e586ade --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/user/UserNotActive.java @@ -0,0 +1,32 @@ +package ru.soune.nocopy.entity.user; + +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "user_not_active") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +@Builder +public class UserNotActive { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + private User user; + + @CreatedDate + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @Column(name = "blocked_until") + private LocalDateTime blockedUntil; +} diff --git a/src/main/java/ru/soune/nocopy/handler/ResetPasswordHandler.java b/src/main/java/ru/soune/nocopy/handler/ResetPasswordHandler.java new file mode 100644 index 0000000..717416b --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/ResetPasswordHandler.java @@ -0,0 +1,112 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.file.ActionResponse; +import ru.soune.nocopy.dto.register.ResetPasswordRequest; +import ru.soune.nocopy.dto.register.ResetPasswordResponse; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.entity.user.EmailVerificationToken; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.exception.NotValidFieldException; +import ru.soune.nocopy.exception.UserNotFoundException; +import ru.soune.nocopy.repository.AuthTokenRepository; +import ru.soune.nocopy.repository.EmailVerificationTokenRepository; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.service.mail.EmailService; +import ru.soune.nocopy.service.register.AuthService; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@Component +@Slf4j +@RequiredArgsConstructor +public class ResetPasswordHandler implements RequestHandler{ + + private final ObjectMapper objectMapper; + + private final EmailService emailService; + + private final AuthService authService; + + private final UserRepository userRepository; + + private final EmailVerificationTokenRepository emailVerificationTokenRepository; + + private final AuthTokenRepository authTokenRepository; + + private final PasswordEncoder passwordEncoder; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + ResetPasswordRequest resetPasswordRequest = objectMapper.convertValue(request.getMessageBody(), + ResetPasswordRequest.class); + String action = resetPasswordRequest.getAction(); + User user = userRepository.findByEmail(resetPasswordRequest.getEmail()); + AuthToken authToken; + + if (user == null) { + throw new UserNotFoundException("User not found with email: " + resetPasswordRequest.getEmail()); + } + + switch (action) { + case "resetPassword": + authToken = authService.generateAuthToken(user); + + EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken); + + emailService.sendResetPasswordEmail(authToken.getUser(), emailToken.getToken()); + + return successResponse(request, authToken, user, MessageCode.SUCCESS.getDescription()); + case "confirmVerification": + String verifyToken = resetPasswordRequest.getVerifyToken(); + + EmailVerificationToken verificationToken = + emailVerificationTokenRepository.findByUserIdAndToken(user.getId(), verifyToken); + + if (verificationToken == null) { + throw new NotValidFieldException("Verification token not found", + new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(), + MessageCode.INVALID_TOKEN.getDescription(), Map.of("verifyToken", verifyToken))); + } + List authTokens = authTokenRepository.findByUserId(user.getId()); + + authToken = authTokens.isEmpty() ? authService.generateAuthToken(user): authTokens.getFirst(); + + user.setPassword(passwordEncoder.encode(resetPasswordRequest.getPassword())); + user.setActive(true); + + emailVerificationTokenRepository.delete(verificationToken); + + User saveUser = userRepository.save(user); + + return successResponse(request, authToken, saveUser, MessageCode.SUCCESS.getDescription()); + default: + ActionResponse response = ActionResponse.builder() + .action(action) + .availableActions(Arrays.asList("confirmVerification", "resetPassword")) + .build(); + + return new BaseResponse(request.getMsgId(), MessageCode.INVALID_ACTION.getCode(), + "Invalid action: " + action, response); + + } + } + + private BaseResponse successResponse(BaseRequest request, AuthToken authToken, User user, String message) { + ResetPasswordResponse resetPasswordResponse = new ResetPasswordResponse(); + resetPasswordResponse.setAuthToken(authToken.getToken()); + resetPasswordResponse.setUserId(user.getId()); + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), message, resetPasswordResponse); + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java b/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java index 373b953..35efc32 100644 --- a/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java @@ -23,6 +23,7 @@ import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.user.UserService; import java.io.IOException; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -53,9 +54,9 @@ public class VerifyRegisterUserHandler implements RequestHandler { User user = userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId)); - Optional userAuthTokens = authTokenRepository.findByUserId(userId); + List userAuthTokens = authTokenRepository.findByUserId(userId); - AuthToken authToken = userAuthTokens.isEmpty() ? authService.generateAuthToken(user): userAuthTokens.get(); + AuthToken authToken = userAuthTokens.isEmpty() ? authService.generateAuthToken(user): userAuthTokens.getFirst(); if (verifyUserRequest.getResend() != null && verifyUserRequest.getResend() == 1) { return handleResend(request, user, authToken); diff --git a/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java b/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java index c1b65e2..48efb76 100644 --- a/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java @@ -20,5 +20,5 @@ public interface AuthTokenRepository extends JpaRepository { nativeQuery = true) Long findUserIdByToken(String token); Optional findByUserIdAndIsActive(Long userId, boolean isActive); - Optional findByUserId(Long userId); + List findByUserId(Long userId); } diff --git a/src/main/java/ru/soune/nocopy/repository/UserNotActiveRepository.java b/src/main/java/ru/soune/nocopy/repository/UserNotActiveRepository.java new file mode 100644 index 0000000..e3fad73 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/UserNotActiveRepository.java @@ -0,0 +1,16 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.user.UserNotActive; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository +public interface UserNotActiveRepository extends JpaRepository { + Optional findByUserId(Long userId); + List findByBlockedUntilBefore(LocalDateTime now); + +} diff --git a/src/main/java/ru/soune/nocopy/service/auth/CleanUpNotActiveUser.java b/src/main/java/ru/soune/nocopy/service/auth/CleanUpNotActiveUser.java new file mode 100644 index 0000000..6ea818e --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/auth/CleanUpNotActiveUser.java @@ -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 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); + + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java index 5544bf2..68fef77 100644 --- a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java +++ b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java @@ -38,6 +38,9 @@ public class EmailService { @Value("${app.email.verification.user}") private String user; + @Value("${app.email.verification.drop-password-subject}") + private String dropPasswordSubject; + public void sendVerificationEmail(User user, String code) throws MessagingException, IOException { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); @@ -45,8 +48,8 @@ public class EmailService { helper.setFrom("noreply@no-copy.ru"); helper.setTo(user.getEmail()); helper.setSubject(standartSubject); - - String htmlContent = loadAndProcessTemplate(user.getFullName(), code); + String htmlContent = loadAndProcessTemplate(user.getFullName(), code, + "templates/email-verification.html"); helper.setText(htmlContent, true); mailSender.send(message); @@ -69,8 +72,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); @@ -89,4 +92,25 @@ 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"); + 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()); + } + } } diff --git a/src/main/java/ru/soune/nocopy/service/register/AuthService.java b/src/main/java/ru/soune/nocopy/service/register/AuthService.java index 29b0be6..efed941 100644 --- a/src/main/java/ru/soune/nocopy/service/register/AuthService.java +++ b/src/main/java/ru/soune/nocopy/service/register/AuthService.java @@ -17,12 +17,10 @@ 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.entity.user.UserNotActive; 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; @@ -48,6 +46,8 @@ public class AuthService { private final TariffInfoService tariffInfoService; + private final UserNotActiveRepository userNotActiveRepository; + private final SecureRandom secureRandom = new SecureRandom(); @Transactional @@ -102,14 +102,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(), @@ -117,6 +133,9 @@ public class AuthService { } user.setLastLoginAt(LocalDateTime.now()); + user.setFailedLoginAttempts(0); + user.setActive(true); + User savedUser = userRepository.save(user); AuthToken authToken = new AuthToken(); @@ -132,6 +151,7 @@ public class AuthService { .ifPresent(authTokenRepository::delete); } + @Transactional public AuthToken generateAuthToken(User user) { return authTokenRepository.save(genereateAuthToken(user)); } @@ -211,4 +231,19 @@ public class AuthService { user.setCompany(company); user.grantPermission(Permission.DEFAULT_USER_PERMISSIONS); } + + public void blockUser(User user) { + Optional 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); + } + } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index cddeb0d..e04c46d 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -110,6 +110,7 @@ app: verification: standart-subject: NO COPY - Подтверждение email адреса standart-address-from: vlad.popovtsev@gmail.com + drop-password-subject: NO COPY - сброс пароля user: Пользователь tarif: diff --git a/src/main/resources/templates/reset-password.html b/src/main/resources/templates/reset-password.html new file mode 100644 index 0000000..abc80ec --- /dev/null +++ b/src/main/resources/templates/reset-password.html @@ -0,0 +1,201 @@ + + + + + + Восстановление пароля + + + +
+
+ +

Восстановление пароля

+

Безопасный доступ к вашему аккаунту

+
+ +
+

+ Здравствуйте, {{username}}! +

+ +

Мы получили запрос на восстановление пароля для вашего аккаунта NO COPY.

+

Для продолжения восстановления доступа, пожалуйста, используйте следующий код подтверждения:

+ +
+

{{code}}

+

Код для восстановления пароля

+
+ +
+

Код действителен в течение 15 минут

+
+ +

Как использовать этот код:

+
    +
  1. Вернитесь на страницу восстановления пароля
  2. +
  3. Введите полученный код в поле подтверждения
  4. +
  5. Создайте новый надежный пароль
  6. +
  7. Войдите в аккаунт с новым паролем
  8. +
+ +
+ + + +
+ Важная информация:
+ 1. Если вы не запрашивали восстановление пароля, проигнорируйте это письмо.
+ 2. Никому не сообщайте этот код, включая сотрудников поддержки.
+ 3. После успешного восстановления старый пароль будет недействителен. +
+ +

+ Совет по безопасности: Используйте пароль длиной не менее 8 символов, + содержащий буквы (заглавные и строчные), цифры и специальные символы. +

+
+ + +
+ + \ No newline at end of file From 8811f3ce0edf80c280ef329e0b2e5cfed45e4872 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 18 Feb 2026 11:30:15 +0700 Subject: [PATCH 047/274] dev add reset password --- .../nocopy/controller/ApiController.java | 23 ++++++++++++------- .../java/ru/soune/nocopy/dto/MessageCode.java | 2 ++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 9211860..f9b6fe9 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -350,7 +350,7 @@ public class ApiController { @PathVariable(required = false) Integer version, @RequestHeader(value = "Authorization", required = false) String tokenHeader) { try { - if (tokenHeader == null) { + if (tokenHeader == null || tokenHeader.isBlank()) { Map errorData = new HashMap<>(); errorData.put("token", tokenHeader); @@ -378,8 +378,8 @@ public class ApiController { errorData.put("file_status", entityResponse.getStatus()); return ResponseEntity.ok().body(new BaseResponse(20004, - MessageCode.FILE_DOWNLOAD_ERROR.getCode(), - MessageCode.FILE_DOWNLOAD_ERROR.getDescription(), + MessageCode.FILE_DELETE.getCode(), + MessageCode.FILE_DELETE.getDescription(), errorData)); } @@ -388,14 +388,13 @@ public class ApiController { errorData.put("onDisk", entityResponse.isExistsOnDisk()); return ResponseEntity.ok().body(new BaseResponse(20004, - MessageCode.FILE_DOWNLOAD_ERROR.getCode(), - MessageCode.FILE_DOWNLOAD_ERROR.getDescription(), + MessageCode.FILE_NOT_EXIST.getCode(), + MessageCode.FILE_NOT_EXIST.getDescription(), errorData)); } Path filePath = Paths.get(entityResponse.getProtectedFilePath()); -// Resource resource = new UrlResource(filePath.toUri()); FileSystemResource resource = new FileSystemResource(filePath); long fileSize = Files.size(filePath); @@ -408,7 +407,7 @@ public class ApiController { MessageCode.FILE_DOWNLOAD_ERROR.getDescription(), errorData)); } - String contentType = determineContentType(filePath); + String contentType = determineContentType(filePath); return ResponseEntity.ok() .contentLength(fileSize) @@ -424,7 +423,15 @@ public class ApiController { MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(), errorData)); - } catch (NotFoundAuthToken | IOException e) { + } catch (NotFoundAuthToken e) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(new BaseResponse(20004, + MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), + MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), + errorData)); + } catch (IOException e) { Map errorData = new HashMap<>(); errorData.put("token", tokenHeader); errorData.put("fileId", fileId); diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index 1a53d2d..5484191 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -12,6 +12,8 @@ public enum MessageCode { FILE_UPLOAD_ERROR(2, "File upload error"), DUPLICATE_FILE_UPLOAD(2, "Duplicate file upload"), FILE_DOWNLOAD_ERROR(2, "File download error"), + FILE_NOT_EXIST(2, "File not exist on disk"), + FILE_DELETE(2, "File was deleted"), USER_NOT_HAD_PERMISSION(2, "User not have permission for file"), USER_NOT_VERIFIED(2, "User not verified"), PERMISSION_NOT_FOUND(2, "Permission not found"), From 3d5856d302f5322ca2ae6002492c1cfb8fd7d75c Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 18 Feb 2026 16:14:17 +0700 Subject: [PATCH 048/274] dev add reset password --- .../nocopy/controller/PaymentController.java | 33 ++++++ .../soune/nocopy/entity/payment/Payment.java | 61 +++++++++++ .../entity/payment/PaymentOperationType.java | 6 ++ .../nocopy/entity/payment/PaymentStatus.java | 10 ++ .../nocopy/repository/PaymentRepository.java | 12 +++ .../service/payment/PaymentService.java | 101 ++++++++++++++++++ .../service/tariff/TariffInfoService.java | 11 +- 7 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/controller/PaymentController.java create mode 100644 src/main/java/ru/soune/nocopy/entity/payment/Payment.java create mode 100644 src/main/java/ru/soune/nocopy/entity/payment/PaymentOperationType.java create mode 100644 src/main/java/ru/soune/nocopy/entity/payment/PaymentStatus.java create mode 100644 src/main/java/ru/soune/nocopy/repository/PaymentRepository.java create mode 100644 src/main/java/ru/soune/nocopy/service/payment/PaymentService.java diff --git a/src/main/java/ru/soune/nocopy/controller/PaymentController.java b/src/main/java/ru/soune/nocopy/controller/PaymentController.java new file mode 100644 index 0000000..7200379 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/controller/PaymentController.java @@ -0,0 +1,33 @@ +package ru.soune.nocopy.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import ru.soune.nocopy.entity.payment.Payment; +import ru.soune.nocopy.service.payment.PaymentService; + +import java.util.Map; + +@RestController +@RequestMapping("/api/payments") +@RequiredArgsConstructor +@Slf4j +public class PaymentController { + + private final PaymentService paymentService; + + @PostMapping("/create") + public Payment createPayment(@RequestParam String email, @RequestParam Long tariffId, + @RequestParam String operationType, @RequestParam String operationUuid) { + return paymentService.createPayment(email, tariffId, operationType, operationUuid); + } + + @PostMapping("webhook/yookassa") + public void handleYooKassaNotification(@RequestBody Map notification) { + try { + paymentService.handlePaymentNotification(notification); + } catch (Exception e) { + log.error("Error processing payment notification", e); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/payment/Payment.java b/src/main/java/ru/soune/nocopy/entity/payment/Payment.java new file mode 100644 index 0000000..720037c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payment/Payment.java @@ -0,0 +1,61 @@ +package ru.soune.nocopy.entity.payment; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import ru.soune.nocopy.entity.tarif.Tariff; +import ru.soune.nocopy.entity.user.User; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "payment") +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +@EntityListeners(AuditingEntityListener.class) +public class Payment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + private PaymentStatus status; + + @Column(name = "payment_uuid", unique = true) + private String paymentUuid; + + @Column(name = "amount") + private Double amount; + + @CreatedDate + @Column(name = "created_at", updatable = false, nullable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @Enumerated(EnumType.STRING) + @Column(name = "operation_type", nullable = false) + private PaymentOperationType operationType; + + @Column(name = "cancellation_reason") + private String cancellationReason; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne + @JoinColumn(name = "tariff_id") + private Tariff tariff; +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/entity/payment/PaymentOperationType.java b/src/main/java/ru/soune/nocopy/entity/payment/PaymentOperationType.java new file mode 100644 index 0000000..2119238 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payment/PaymentOperationType.java @@ -0,0 +1,6 @@ +package ru.soune.nocopy.entity.payment; + +public enum PaymentOperationType { + TARIFF, + TOKEN_REFILL +} diff --git a/src/main/java/ru/soune/nocopy/entity/payment/PaymentStatus.java b/src/main/java/ru/soune/nocopy/entity/payment/PaymentStatus.java new file mode 100644 index 0000000..0e9478e --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payment/PaymentStatus.java @@ -0,0 +1,10 @@ +package ru.soune.nocopy.entity.payment; + + +public enum PaymentStatus { + PENDING, + WAITING, + SUCCEEDED, + CANCELED, + FAILED +} diff --git a/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java b/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java new file mode 100644 index 0000000..2873fda --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java @@ -0,0 +1,12 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.payment.Payment; + +import java.util.Optional; + +@Repository +public interface PaymentRepository extends JpaRepository { + Optional findByPaymentUuid(String paymentUuid); +} diff --git a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java new file mode 100644 index 0000000..b637512 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java @@ -0,0 +1,101 @@ +package ru.soune.nocopy.service.payment; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.entity.payment.Payment; +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.repository.PaymentRepository; +import ru.soune.nocopy.repository.TariffInfoRepository; +import ru.soune.nocopy.repository.TariffRepository; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.service.tariff.TariffInfoService; + +import java.time.LocalDateTime; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class PaymentService { + private final PaymentRepository paymentRepository; + + private final UserRepository userRepository; + + private final TariffRepository tariffRepository; + + private final TariffInfoRepository tariffInfoRepository; + + private final TariffInfoService tariffInfoService; + + @Transactional + public Payment createPayment(String email, Long tariffId, String operationType, String paymentUuid) { + User user = userRepository.findByEmail(email); + Tariff tariff = tariffRepository.findById(tariffId).orElseThrow(); + + 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); + } + + @Transactional + public void handlePaymentNotification(Map notification) { + String eventType = (String) notification.get("event"); + Map object = (Map) notification.get("object"); + String paymentUuid = (String) object.get("id"); + + Payment payment = paymentRepository.findByPaymentUuid(paymentUuid) + .orElseThrow(() -> new RuntimeException("Payment not found")); + + if ("payment.succeeded".equals(eventType)) { + payment.setStatus(PaymentStatus.SUCCEEDED); + payment.setCancellationReason(null); + activateTariffForUser(payment.getUser(), payment.getTariff()); + + } else if ("payment.canceled".equals(eventType)) { + payment.setStatus(PaymentStatus.CANCELED); + + Map cancellationDetails = (Map) 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 cancellationDetails = (Map) object.get("cancellation_details"); + if (cancellationDetails != null) { + String reason = (String) cancellationDetails.get("reason"); + payment.setCancellationReason(reason); + } + } + + paymentRepository.save(payment); + } + + private void activateTariffForUser(User user, Tariff tariff) { + TariffInfo tariffInfo = new TariffInfo(); + tariffInfo.setStatus(TariffStatus.ACTIVE); + tariffInfo.setStartTariff(LocalDateTime.now()); + tariffInfo.setEndTariff(LocalDateTime.now().plusMonths(1)); + tariffInfo.setTokens(tariff.getTokens()); + tariffInfo.setTariff(tariff); + + tariffInfoService.linkUserTariffInfo(user, tariffInfoService.addTariffInfo(tariffInfo)); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java b/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java index 951f7c3..ac28249 100644 --- a/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java +++ b/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java @@ -78,8 +78,8 @@ public class TariffInfoService { return tariffInfoRepository.save(tariffInfo); } - public void addTariffInfo(TariffInfo tariffInfo) { - tariffInfoRepository.save(tariffInfo); + public TariffInfo addTariffInfo(TariffInfo tariffInfo) { + return tariffInfoRepository.save(tariffInfo); } public List getAllTariffInfos() { @@ -130,6 +130,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()); From d7e3afb4dc552828094e4cfbe00eb50f573a3a0a Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 18 Feb 2026 18:26:11 +0700 Subject: [PATCH 049/274] dev add api for payment --- .../nocopy/configuration/HandlerConfig.java | 4 +- .../nocopy/dto/payment/PaymentRequest.java | 16 +++++++ .../nocopy/dto/payment/PaymentResponse.java | 17 +++++++ .../soune/nocopy/entity/payment/Payment.java | 3 ++ .../exception/PaymentNotFoundException.java | 7 +++ .../soune/nocopy/handler/PaymentHandler.java | 45 +++++++++++++++++++ .../nocopy/repository/PaymentRepository.java | 2 + .../service/payment/PaymentService.java | 21 +++++++-- 8 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/payment/PaymentRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/payment/PaymentResponse.java create mode 100644 src/main/java/ru/soune/nocopy/exception/PaymentNotFoundException.java create mode 100644 src/main/java/ru/soune/nocopy/handler/PaymentHandler.java diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index 87e703b..c000833 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -25,7 +25,8 @@ public class HandlerConfig { TariffInfoHandler tariffInfoHandler, ReferralHandler referralHandler, ResetPasswordHandler resetPasswordHandler, - DaDataHandler daDataHandler + DaDataHandler daDataHandler, + PaymentHandler paymentHandler ) { Map map = new HashMap<>(); map.put(20001, login); @@ -42,6 +43,7 @@ public class HandlerConfig { map.put(30002, tariffInfoHandler); map.put(30003, referralHandler); map.put(30004, daDataHandler); + map.put(30005, paymentHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/dto/payment/PaymentRequest.java b/src/main/java/ru/soune/nocopy/dto/payment/PaymentRequest.java new file mode 100644 index 0000000..7755dcf --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payment/PaymentRequest.java @@ -0,0 +1,16 @@ +package ru.soune.nocopy.dto.payment; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class PaymentRequest { + private String email; + private String paymentUuid; + private String action; +} diff --git a/src/main/java/ru/soune/nocopy/dto/payment/PaymentResponse.java b/src/main/java/ru/soune/nocopy/dto/payment/PaymentResponse.java new file mode 100644 index 0000000..86544e3 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payment/PaymentResponse.java @@ -0,0 +1,17 @@ +package ru.soune.nocopy.dto.payment; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.soune.nocopy.entity.payment.Payment; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class PaymentResponse { + List payments; +} diff --git a/src/main/java/ru/soune/nocopy/entity/payment/Payment.java b/src/main/java/ru/soune/nocopy/entity/payment/Payment.java index 720037c..3646fbc 100644 --- a/src/main/java/ru/soune/nocopy/entity/payment/Payment.java +++ b/src/main/java/ru/soune/nocopy/entity/payment/Payment.java @@ -1,5 +1,6 @@ package ru.soune.nocopy.entity.payment; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; @@ -53,9 +54,11 @@ public class Payment { @ManyToOne @JoinColumn(name = "user_id", nullable = false) + @JsonIgnore private User user; @ManyToOne @JoinColumn(name = "tariff_id") + @JsonIgnore private Tariff tariff; } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/exception/PaymentNotFoundException.java b/src/main/java/ru/soune/nocopy/exception/PaymentNotFoundException.java new file mode 100644 index 0000000..3d7f072 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/PaymentNotFoundException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class PaymentNotFoundException extends RuntimeException { + public PaymentNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/PaymentHandler.java b/src/main/java/ru/soune/nocopy/handler/PaymentHandler.java new file mode 100644 index 0000000..7842fff --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/PaymentHandler.java @@ -0,0 +1,45 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.payment.PaymentRequest; +import ru.soune.nocopy.dto.payment.PaymentResponse; +import ru.soune.nocopy.entity.payment.Payment; +import ru.soune.nocopy.service.payment.PaymentService; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class PaymentHandler implements RequestHandler{ + + private final PaymentService paymentService; + + private final ObjectMapper objectMapper; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + PaymentRequest paymentRequest = objectMapper.convertValue(request.getMessageBody(), PaymentRequest.class); + String action = paymentRequest.getAction(); + + switch (action) { + case "user_payments": + List payments = paymentService.userPayments(paymentRequest.getEmail()); + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), new PaymentResponse(payments)); + case "payment_info": + Payment payment = paymentService.paymentInfo(paymentRequest.getPaymentUuid()); + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), new PaymentResponse(List.of(payment))); + + } + + return null; + } +} diff --git a/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java b/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java index 2873fda..d599c4b 100644 --- a/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java @@ -4,9 +4,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import ru.soune.nocopy.entity.payment.Payment; +import java.util.List; import java.util.Optional; @Repository public interface PaymentRepository extends JpaRepository { Optional findByPaymentUuid(String paymentUuid); + List findByUserId(Long userId); } diff --git a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java index b637512..50f00ab 100644 --- a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java +++ b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java @@ -10,13 +10,15 @@ 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.UserNotFoundException; import ru.soune.nocopy.repository.PaymentRepository; -import ru.soune.nocopy.repository.TariffInfoRepository; import ru.soune.nocopy.repository.TariffRepository; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.tariff.TariffInfoService; import java.time.LocalDateTime; +import java.util.List; import java.util.Map; @Service @@ -28,8 +30,6 @@ public class PaymentService { private final TariffRepository tariffRepository; - private final TariffInfoRepository tariffInfoRepository; - private final TariffInfoService tariffInfoService; @Transactional @@ -88,6 +88,21 @@ public class PaymentService { paymentRepository.save(payment); } + public List 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) { TariffInfo tariffInfo = new TariffInfo(); tariffInfo.setStatus(TariffStatus.ACTIVE); From e2f22bb27b11b4a701be2a7a316ffa616f83efac Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 18 Feb 2026 18:42:55 +0700 Subject: [PATCH 050/274] dev add api for payment --- .../soune/nocopy/controller/PaymentController.java | 14 +++++++++++--- .../nocopy/service/payment/PaymentService.java | 9 ++++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/PaymentController.java b/src/main/java/ru/soune/nocopy/controller/PaymentController.java index 7200379..0330b80 100644 --- a/src/main/java/ru/soune/nocopy/controller/PaymentController.java +++ b/src/main/java/ru/soune/nocopy/controller/PaymentController.java @@ -2,8 +2,11 @@ package ru.soune.nocopy.controller; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import ru.soune.nocopy.entity.payment.Payment; +import ru.soune.nocopy.exception.TariffNotFoundException; +import ru.soune.nocopy.exception.UserNotFoundException; import ru.soune.nocopy.service.payment.PaymentService; import java.util.Map; @@ -17,9 +20,14 @@ public class PaymentController { private final PaymentService paymentService; @PostMapping("/create") - public Payment createPayment(@RequestParam String email, @RequestParam Long tariffId, - @RequestParam String operationType, @RequestParam String operationUuid) { - return paymentService.createPayment(email, tariffId, operationType, operationUuid); + public ResponseEntity createPayment(@RequestParam String email, @RequestParam Long tariffId, + @RequestParam String operationType, @RequestParam String operationUuid) { + try { + Payment payment = paymentService.createPayment(email, tariffId, operationType, operationUuid); + return ResponseEntity.ok(payment); + } catch (TariffNotFoundException | UserNotFoundException e) { + return ResponseEntity.notFound().build(); + } } @PostMapping("webhook/yookassa") diff --git a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java index 50f00ab..c53eaa8 100644 --- a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java +++ b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java @@ -11,6 +11,7 @@ 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.PaymentRepository; import ru.soune.nocopy.repository.TariffRepository; @@ -35,7 +36,13 @@ public class PaymentService { @Transactional public Payment createPayment(String email, Long tariffId, String operationType, String paymentUuid) { User user = userRepository.findByEmail(email); - Tariff tariff = tariffRepository.findById(tariffId).orElseThrow(); + + 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); From 22a44d833e6c5d126842c81a3ec73a448c8f4ed3 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 18 Feb 2026 19:00:44 +0700 Subject: [PATCH 051/274] dev add api for payment --- .../java/ru/soune/nocopy/dto/MessageCode.java | 1 + .../ru/soune/nocopy/handler/PaymentHandler.java | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index 5484191..00e2597 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -44,6 +44,7 @@ public enum MessageCode { RESOURCE_NOT_FOUND(4, "Resource not found"), INTERNAL_ERROR(4, "Internal server error"), COMPANY_NOT_FOUND(4, "Company not found"), + PAYMENT_NOT_FOUND(4, "Payment not found"), COMPANY_ALREADY_EXISTS(2, "Company already exists"), USER_LIMIT_IS_OVER(2, "Over user limits"), ERROR_TARIFF_INFO(2, "Erorr with tariff info"), diff --git a/src/main/java/ru/soune/nocopy/handler/PaymentHandler.java b/src/main/java/ru/soune/nocopy/handler/PaymentHandler.java index 7842fff..57167af 100644 --- a/src/main/java/ru/soune/nocopy/handler/PaymentHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/PaymentHandler.java @@ -9,9 +9,11 @@ import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.payment.PaymentRequest; import ru.soune.nocopy.dto.payment.PaymentResponse; import ru.soune.nocopy.entity.payment.Payment; +import ru.soune.nocopy.exception.PaymentNotFoundException; import ru.soune.nocopy.service.payment.PaymentService; import java.util.List; +import java.util.Map; @Component @RequiredArgsConstructor @@ -33,11 +35,15 @@ public class PaymentHandler implements RequestHandler{ return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), MessageCode.SUCCESS.getDescription(), new PaymentResponse(payments)); case "payment_info": - Payment payment = paymentService.paymentInfo(paymentRequest.getPaymentUuid()); - - return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), - MessageCode.SUCCESS.getDescription(), new PaymentResponse(List.of(payment))); - + try { + Payment payment = paymentService.paymentInfo(paymentRequest.getPaymentUuid()); + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), new PaymentResponse(List.of(payment))); + } catch (PaymentNotFoundException e) { + return new BaseResponse(request.getMsgId(), MessageCode.PAYMENT_NOT_FOUND.getCode(), + MessageCode.PAYMENT_NOT_FOUND.getDescription(), Map.of("paymentUuid", + paymentRequest.getPaymentUuid())); + } } return null; From 27df74891baf4df5dda76d9a08f80c45d6157e54 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 19 Feb 2026 10:41:45 +0700 Subject: [PATCH 052/274] dev add api for payment --- .../java/ru/soune/nocopy/handler/RegRequestHandler.java | 5 +++-- .../java/ru/soune/nocopy/service/register/AuthService.java | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java index dc06ffa..39a100e 100644 --- a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java @@ -78,11 +78,12 @@ public class RegRequestHandler implements RequestHandler { throw new ValidationException(bindingResult, request.getMsgId()); } - AuthToken authToken = authService.register(regRequest); + boolean verified = regRequest.getMailVerified().equals("true"); + AuthToken authToken = authService.register(regRequest, verified); EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken); RegAnswer regAnswer = new RegAnswer(); - if (regRequest.getMailVerified().equals("true")) { + if (verified) { try { emailService.sendVerificationEmail(authToken.getUser(), emailToken.getToken()); } catch (Exception e) { diff --git a/src/main/java/ru/soune/nocopy/service/register/AuthService.java b/src/main/java/ru/soune/nocopy/service/register/AuthService.java index efed941..88db04f 100644 --- a/src/main/java/ru/soune/nocopy/service/register/AuthService.java +++ b/src/main/java/ru/soune/nocopy/service/register/AuthService.java @@ -51,13 +51,13 @@ public class AuthService { 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()); From 4ccf39f8110c5c9801e9568872a2a9df9673c72d Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 19 Feb 2026 10:53:03 +0700 Subject: [PATCH 053/274] dev add api for payment --- src/main/java/ru/soune/nocopy/dto/MessageCode.java | 1 + .../ru/soune/nocopy/handler/RegRequestHandler.java | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index 00e2597..61b92aa 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -6,6 +6,7 @@ public enum MessageCode { REG_EMAIL_OR_PHONE_EXISTS(1, "Email or phone already registered"), REFERRAL_LINK_IS_NOT_EXIST(1, "Refferal link is not exist"), INVALID_FIELD(2, "Invalid field"), + MAIL_VERIFIED_NULL(2, "Mail verified null"), INVALID_TOKEN(2, "Invalid token"), TOKEN_IS_ALIVE(2, "Token is alive"), INVALID_ACTION(2, "Invalid action"), diff --git a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java index 39a100e..e3aad4e 100644 --- a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java @@ -58,6 +58,7 @@ public class RegRequestHandler implements RequestHandler { } String referralLink = regRequest.getReferralLink(); + String mailVerified = regRequest.getMailVerified(); if (referralLink != null) { Referral referral = referralJpaRepository.findByReferralLink(referralLink); @@ -68,6 +69,14 @@ public class RegRequestHandler implements RequestHandler { MessageCode.REFERRAL_LINK_IS_NOT_EXIST.getDescription(), Map.of( "referralLink", referralLink))); } + + if (mailVerified == null) { + throw new NotValidFieldException("Mailverified is null:" + referralLink + + referralLink, new BaseResponse(request.getMsgId(), + MessageCode.MAIL_VERIFIED_NULL.getCode(), + MessageCode.MAIL_VERIFIED_NULL.getDescription(), Map.of( + "mailVerified", mailVerified))); + } } BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest"); @@ -78,7 +87,7 @@ public class RegRequestHandler implements RequestHandler { throw new ValidationException(bindingResult, request.getMsgId()); } - boolean verified = regRequest.getMailVerified().equals("true"); + boolean verified = mailVerified.equals("true"); AuthToken authToken = authService.register(regRequest, verified); EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken); RegAnswer regAnswer = new RegAnswer(); From 9112854feace0edc84dfd61b3564ca9da4ef316b Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 19 Feb 2026 11:27:01 +0700 Subject: [PATCH 054/274] dev add api for payment --- .../ru/soune/nocopy/service/payment/PaymentService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java index c53eaa8..d12af9a 100644 --- a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java +++ b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java @@ -1,6 +1,7 @@ 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.nocopy.entity.payment.Payment; @@ -24,6 +25,7 @@ import java.util.Map; @Service @RequiredArgsConstructor +@Slf4j public class PaymentService { private final PaymentRepository paymentRepository; @@ -57,10 +59,13 @@ public class PaymentService { @Transactional public void handlePaymentNotification(Map notification) { + log.info("INFO FROM YOKASSA:" + notification.toString()); + + String eventType = (String) notification.get("event"); Map object = (Map) notification.get("object"); String paymentUuid = (String) object.get("id"); - + log.info("METADATA:" + (String) object.get("metadata")); Payment payment = paymentRepository.findByPaymentUuid(paymentUuid) .orElseThrow(() -> new RuntimeException("Payment not found")); From e9cdc66f725f6c6595a4a9273bf928c0ed79015a Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 19 Feb 2026 14:15:59 +0700 Subject: [PATCH 055/274] dev add api for payment --- .../nocopy/controller/PaymentController.java | 20 ++++++++-- .../soune/nocopy/entity/tarif/TariffType.java | 2 +- .../handler/ImageFoundRequestHandler.java | 1 + .../soune/nocopy/handler/TariffHandler.java | 10 +++++ .../service/payment/PaymentService.java | 33 +++++++++------ .../service/search/SearchImageService.java | 4 +- .../tariff/TariffInitializationService.java | 40 +++++++++++++++++++ .../nocopy/service/tariff/TariffService.java | 7 ++++ 8 files changed, 98 insertions(+), 19 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/PaymentController.java b/src/main/java/ru/soune/nocopy/controller/PaymentController.java index 0330b80..1c29804 100644 --- a/src/main/java/ru/soune/nocopy/controller/PaymentController.java +++ b/src/main/java/ru/soune/nocopy/controller/PaymentController.java @@ -2,6 +2,7 @@ package ru.soune.nocopy.controller; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import ru.soune.nocopy.entity.payment.Payment; @@ -20,13 +21,24 @@ public class PaymentController { private final PaymentService paymentService; @PostMapping("/create") - public ResponseEntity createPayment(@RequestParam String email, @RequestParam Long tariffId, - @RequestParam String operationType, @RequestParam String operationUuid) { + public ResponseEntity createPayment(@RequestParam String email, @RequestParam Long tariffId, + @RequestParam String operationType, @RequestParam String operationUuid) { try { Payment payment = paymentService.createPayment(email, tariffId, operationType, operationUuid); + return ResponseEntity.ok(payment); - } catch (TariffNotFoundException | UserNotFoundException e) { - return ResponseEntity.notFound().build(); + } catch (TariffNotFoundException e) { + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(Map.of("error", "Tariff not found", "message", e.getMessage())); + } catch (UserNotFoundException e) { + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(Map.of("error", "User not found", "message", e.getMessage())); + } catch (Exception e) { + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "Internal server error", "message", e.getMessage())); } } diff --git a/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java b/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java index 43ae949..6507b51 100644 --- a/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java +++ b/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java @@ -2,5 +2,5 @@ package ru.soune.nocopy.entity.tarif; public enum TariffType { - START, BASIC, PRO, ENTERPRISE, DEMO, BUISNES, CORPORAT, STUDIO, FREE; + START, BASIC, PRO, ENTERPRISE, DEMO, BUISNES, CORPORAT, STUDIO, FREE, TOKEN_100, TOKEN_500, TOKEN_1000, TOKEN_5000; } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 3c15c4d..e8e4c7d 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -62,6 +62,7 @@ public class ImageFoundRequestHandler implements RequestHandler { String searchResponseYandex; String searchResponseGoogle; List allUniqueImages; + //TODO отдавать любой полученный результат,даже если по таймауту падают try { searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", "visual_matches"); diff --git a/src/main/java/ru/soune/nocopy/handler/TariffHandler.java b/src/main/java/ru/soune/nocopy/handler/TariffHandler.java index 50ea527..89d24f0 100644 --- a/src/main/java/ru/soune/nocopy/handler/TariffHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/TariffHandler.java @@ -44,6 +44,8 @@ public class TariffHandler implements RequestHandler { return handleUpdateTariff(request, tariffRequest); case "get" : return handleGetTariffs(request, tariffRequest); + case "tariffs_by_type" : + return handleGetTariffsByType(request, tariffRequest); } } catch (Exception e) { @@ -126,4 +128,12 @@ public class TariffHandler implements RequestHandler { MessageCode.SUCCESS.getDescription(), Map.of("tariffs", allTariffs)); } + + private BaseResponse handleGetTariffsByType(BaseRequest request, TariffRequest tariffRequest) { + List allTariffs = tariffService.getTariffByType(tariffRequest.getType()); + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + Map.of("token_tariffs", allTariffs)); + } } diff --git a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java index d12af9a..163a7c8 100644 --- a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java +++ b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java @@ -15,6 +15,7 @@ import ru.soune.nocopy.exception.PaymentNotFoundException; import ru.soune.nocopy.exception.TariffNotFoundException; import ru.soune.nocopy.exception.UserNotFoundException; import ru.soune.nocopy.repository.PaymentRepository; +import ru.soune.nocopy.repository.TariffInfoRepository; import ru.soune.nocopy.repository.TariffRepository; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.tariff.TariffInfoService; @@ -33,6 +34,8 @@ public class PaymentService { private final TariffRepository tariffRepository; + private final TariffInfoRepository tariffInfoRepository; + private final TariffInfoService tariffInfoService; @Transactional @@ -59,13 +62,10 @@ public class PaymentService { @Transactional public void handlePaymentNotification(Map notification) { - log.info("INFO FROM YOKASSA:" + notification.toString()); - - String eventType = (String) notification.get("event"); Map object = (Map) notification.get("object"); String paymentUuid = (String) object.get("id"); - log.info("METADATA:" + (String) object.get("metadata")); + Payment payment = paymentRepository.findByPaymentUuid(paymentUuid) .orElseThrow(() -> new RuntimeException("Payment not found")); @@ -73,7 +73,6 @@ public class PaymentService { payment.setStatus(PaymentStatus.SUCCEEDED); payment.setCancellationReason(null); activateTariffForUser(payment.getUser(), payment.getTariff()); - } else if ("payment.canceled".equals(eventType)) { payment.setStatus(PaymentStatus.CANCELED); @@ -116,13 +115,23 @@ public class PaymentService { } private void activateTariffForUser(User user, Tariff tariff) { - TariffInfo tariffInfo = new TariffInfo(); - tariffInfo.setStatus(TariffStatus.ACTIVE); - tariffInfo.setStartTariff(LocalDateTime.now()); - tariffInfo.setEndTariff(LocalDateTime.now().plusMonths(1)); - tariffInfo.setTokens(tariff.getTokens()); - tariffInfo.setTariff(tariff); + String type = tariff.getType(); - tariffInfoService.linkUserTariffInfo(user, tariffInfoService.addTariffInfo(tariffInfo)); + if (type.equals("token")) { + TariffInfo activeTariffInfo = user.getActiveTariffInfo(); + Integer tokens = activeTariffInfo.getTokens() + tariff.getTokens(); + activeTariffInfo.setTokens(tokens); + + 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.setTariff(tariff); + + tariffInfoService.linkUserTariffInfo(user, tariffInfoService.addTariffInfo(tariffInfo)); + } } } diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 925d1e8..aaf510d 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -34,8 +34,8 @@ public class SearchImageService { this.httpClient = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) - .readTimeout(120, TimeUnit.SECONDS) - .callTimeout(180, TimeUnit.SECONDS) + .readTimeout(45, TimeUnit.SECONDS) + .callTimeout(90, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .build(); diff --git a/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java b/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java index 0e037fa..1f5464c 100644 --- a/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java +++ b/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java @@ -180,6 +180,46 @@ public class TariffInitializationService { tariff.setMaxUsers(10L); tariff.setTariffAccountType("b2b"); 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; } tariffRepository.save(tariff); diff --git a/src/main/java/ru/soune/nocopy/service/tariff/TariffService.java b/src/main/java/ru/soune/nocopy/service/tariff/TariffService.java index 5c6561e..2149b08 100644 --- a/src/main/java/ru/soune/nocopy/service/tariff/TariffService.java +++ b/src/main/java/ru/soune/nocopy/service/tariff/TariffService.java @@ -42,6 +42,13 @@ public class TariffService { .collect(Collectors.toList()); } + public List getTariffByType(String type) { + return tariffRepository.findByTariffAccountType(type) + .stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + public List getAllTariffs() { return tariffRepository.findAllByOrderByPriceAsc() .stream() From d4f26db9ce345a24bc9a8628d8b2083d16979dcf Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 19 Feb 2026 15:06:48 +0700 Subject: [PATCH 056/274] dev add api for payment --- .../java/ru/soune/nocopy/controller/PaymentController.java | 3 ++- .../java/ru/soune/nocopy/service/payment/PaymentService.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/PaymentController.java b/src/main/java/ru/soune/nocopy/controller/PaymentController.java index 1c29804..42faa55 100644 --- a/src/main/java/ru/soune/nocopy/controller/PaymentController.java +++ b/src/main/java/ru/soune/nocopy/controller/PaymentController.java @@ -6,6 +6,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import ru.soune.nocopy.entity.payment.Payment; +import ru.soune.nocopy.exception.PaymentNotFoundException; import ru.soune.nocopy.exception.TariffNotFoundException; import ru.soune.nocopy.exception.UserNotFoundException; import ru.soune.nocopy.service.payment.PaymentService; @@ -46,7 +47,7 @@ public class PaymentController { public void handleYooKassaNotification(@RequestBody Map notification) { try { paymentService.handlePaymentNotification(notification); - } catch (Exception e) { + } catch (PaymentNotFoundException e) { log.error("Error processing payment notification", e); } } diff --git a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java index 163a7c8..7030802 100644 --- a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java +++ b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java @@ -67,7 +67,7 @@ public class PaymentService { String paymentUuid = (String) object.get("id"); Payment payment = paymentRepository.findByPaymentUuid(paymentUuid) - .orElseThrow(() -> new RuntimeException("Payment not found")); + .orElseThrow(() -> new PaymentNotFoundException("Payment not found")); if ("payment.succeeded".equals(eventType)) { payment.setStatus(PaymentStatus.SUCCEEDED); From 65bd103c74b62cc3b41046c34152d4e18dc50b99 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 19 Feb 2026 15:36:22 +0700 Subject: [PATCH 057/274] dev set only yandex search --- .../nocopy/handler/ImageFoundRequestHandler.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index e8e4c7d..23f8a46 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -69,14 +69,16 @@ public class ImageFoundRequestHandler implements RequestHandler { log.info("searchResponseYandex OK"); List allYandexImages = searchImageService.getAllImagesWithoutPagination( searchResponseYandex, "visual_matches"); - searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", - "exact_matches"); +// searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", +// "exact_matches"); log.info("searchResponseGoogle OK"); - List allGoogleImages = searchImageService.getAllImagesWithoutPagination( - searchResponseGoogle, "exact_matches"); +// List allGoogleImages = searchImageService.getAllImagesWithoutPagination( +// searchResponseGoogle, "exact_matches"); +// allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, +// allGoogleImages); allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, - allGoogleImages); + new ArrayList<>()); log.info("allUniqueImages OK"); } catch (TimeoutException e) { From e5da30400a3bb5abef3db70d6ba9f8c058b19465 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 19 Feb 2026 16:01:10 +0700 Subject: [PATCH 058/274] dev check timeout for google --- .../handler/ImageFoundRequestHandler.java | 142 ++++++++++++++---- 1 file changed, 110 insertions(+), 32 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 23f8a46..1e12bed 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -54,51 +54,52 @@ public class ImageFoundRequestHandler implements RequestHandler { Map.of("fileId",fileId))); }); -// YandexSearchResponse response = yandexSearchService.searchByFileEntity(fileEntity); +// YandexSearchResponse response = yandexSearchService.searchByFileEntity(fileId); //TODO uncommited when add billing // GoogleVisionSearchResponse googleVisionSearchResponse = googleVisionSearchService.searchByFileEntity(fileId); - - String searchResponseYandex; - String searchResponseGoogle; - List allUniqueImages; //TODO отдавать любой полученный результат,даже если по таймауту падают + + String searchResponseYandex = null; + String searchResponseGoogle = null; + List allYandexImages = new ArrayList<>(); + List allGoogleImages = new ArrayList<>(); + List allUniqueImages; + boolean hasTimeout = false; + try { searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", "visual_matches"); log.info("searchResponseYandex OK"); - List allYandexImages = searchImageService.getAllImagesWithoutPagination( + allYandexImages = searchImageService.getAllImagesWithoutPagination( searchResponseYandex, "visual_matches"); -// searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", -// "exact_matches"); - - log.info("searchResponseGoogle OK"); -// List allGoogleImages = searchImageService.getAllImagesWithoutPagination( -// searchResponseGoogle, "exact_matches"); -// allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, -// allGoogleImages); - allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, - new ArrayList<>()); - log.info("allUniqueImages OK"); - } catch (TimeoutException e) { - log.warn("Search timeout for file {}, returning empty results", fileId); - return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), - "Search completed (partial)", Map.of( - "results", List.of(), - "total", 0, - "searchStatus", "timeout" - )); + log.warn("Yandex search timeout for file {}", fileId); + hasTimeout = true; } catch (IOException e) { - log.error("Search failed for file {}", fileId, e); - return new BaseResponse(request.getMsgId(), 20007, - "Search service temporarily unavailable", - Map.of("fileId", fileId)); + log.error("Yandex search failed for file {}", fileId, e); } - tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); + try { + searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", + "exact_matches"); + log.info("searchResponseGoogle OK"); + allGoogleImages = searchImageService.getAllImagesWithoutPagination( + searchResponseGoogle, "exact_matches"); + } catch (TimeoutException e) { + log.warn("Google search timeout for file {}", fileId); + hasTimeout = true; + } catch (IOException e) { + log.error("Google search failed for file {}", fileId, e); + } - checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); + allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); + log.info("allUniqueImages OK: {} images", allUniqueImages.size()); + + if (!allUniqueImages.isEmpty()) { + tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); + checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); + } int page = imageSearchRequest.getPage() != null ? imageSearchRequest.getPage() : 1; int pageSize = 5; @@ -113,7 +114,84 @@ public class ImageFoundRequestHandler implements RequestHandler { finalResponse.setTotalResults(allUniqueImages.size()); finalResponse.setTotalPages((int) Math.ceil((double) allUniqueImages.size() / (pageSize * 2))); + String message = hasTimeout ? "Search completed (partial)" : MessageCode.SUCCESS.getDescription(); + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), - MessageCode.SUCCESS.getDescription(), finalResponse); + message, finalResponse); } + +// @Override +// public BaseResponse handle(BaseRequest request) throws Exception { +// ImageSearchRequest imageSearchRequest = objectMapper.convertValue(request.getMessageBody(), +// ImageSearchRequest.class); +// +// String fileId = imageSearchRequest.getFileId(); +// +// FileEntity fileEntity = fileEntityRepository.findById(fileId) +// .orElseThrow(() -> { +// throw new NotValidFieldException("File not found", new BaseResponse(20007, +// MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(), +// Map.of("fileId",fileId))); +// }); +// +//// YandexSearchResponse response = yandexSearchService.searchByFileEntity(fileEntity); +// +// //TODO uncommited when add billing +//// GoogleVisionSearchResponse googleVisionSearchResponse = googleVisionSearchService.searchByFileEntity(fileId); +// //TODO отдавать любой полученный результат,даже если по таймауту падают +// +// String searchResponseYandex; +// String searchResponseGoogle; +// List allUniqueImages; +// try { +// searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", +// "visual_matches"); +// log.info("searchResponseYandex OK"); +// List allYandexImages = searchImageService.getAllImagesWithoutPagination( +// searchResponseYandex, "visual_matches"); +// searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", +// "exact_matches"); +// +// log.info("searchResponseGoogle OK"); +// List allGoogleImages = searchImageService.getAllImagesWithoutPagination( +// searchResponseGoogle, "exact_matches"); +// allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, +// allGoogleImages); +// log.info("allUniqueImages OK"); +// +// } catch (TimeoutException e) { +// log.warn("Search timeout for file {}, returning empty results", fileId); +// return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), +// "Search completed (partial)", Map.of( +// "results", List.of(), +// "total", 0, +// "searchStatus", "timeout" +// )); +// } catch (IOException e) { +// log.error("Search failed for file {}", fileId, e); +// return new BaseResponse(request.getMsgId(), 20007, +// "Search service temporarily unavailable", +// Map.of("fileId", fileId)); +// } +// +// tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); +// +// checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); +// +// int page = imageSearchRequest.getPage() != null ? imageSearchRequest.getPage() : 1; +// int pageSize = 5; +// +// List pagedResults = searchImageService.paginateResults(allUniqueImages, page, +// pageSize * 2); +// +// YandexSearchResponse finalResponse = new YandexSearchResponse(); +// finalResponse.setImages(pagedResults); +// finalResponse.setPage(page); +// finalResponse.setPageSize(pageSize * 2); +// finalResponse.setTotalResults(allUniqueImages.size()); +// finalResponse.setTotalPages((int) Math.ceil((double) allUniqueImages.size() / (pageSize * 2))); +// +// return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), +// MessageCode.SUCCESS.getDescription(), finalResponse); +// } } From 34231d9f6f0a52ed2f5fa863a25d093ab798f622 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 19 Feb 2026 16:06:49 +0700 Subject: [PATCH 059/274] dev set timeout 30 sec --- .../ru/soune/nocopy/service/search/SearchImageService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index aaf510d..a7dfb3b 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -34,8 +34,8 @@ public class SearchImageService { this.httpClient = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) - .readTimeout(45, TimeUnit.SECONDS) - .callTimeout(90, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .callTimeout(30, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .build(); From db4cf418006b6427fbec99d527549c6ce12c3f58 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 19 Feb 2026 17:28:01 +0700 Subject: [PATCH 060/274] dev set timeout 30 sec --- .../ru/soune/nocopy/entity/payment/PaymentOperationType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/entity/payment/PaymentOperationType.java b/src/main/java/ru/soune/nocopy/entity/payment/PaymentOperationType.java index 2119238..69c1b00 100644 --- a/src/main/java/ru/soune/nocopy/entity/payment/PaymentOperationType.java +++ b/src/main/java/ru/soune/nocopy/entity/payment/PaymentOperationType.java @@ -2,5 +2,5 @@ package ru.soune.nocopy.entity.payment; public enum PaymentOperationType { TARIFF, - TOKEN_REFILL + TOKEN } From 0767088fc47b299e539249bcbb2bb24ff9a75a87 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Feb 2026 10:50:44 +0700 Subject: [PATCH 061/274] dev set timeout 30 sec --- .../ru/soune/nocopy/service/mail/EmailService.java | 10 ++++++++++ src/main/resources/application.yaml | 2 ++ 2 files changed, 12 insertions(+) diff --git a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java index 68fef77..622e44a 100644 --- a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java +++ b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java @@ -41,6 +41,12 @@ public class EmailService { @Value("${app.email.verification.drop-password-subject}") private String dropPasswordSubject; + @Value("${server.baseurl}") + private String baseUrl; + + @Value("${server.front.port}") + private String frontPort; + public void sendVerificationEmail(User user, String code) throws MessagingException, IOException { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); @@ -104,6 +110,10 @@ public class EmailService { String htmlContent = loadAndProcessTemplate(user.getFullName(), code, "templates/reset-password.html"); + + String resetLink = escapeHtml(baseUrl + "/reset-password?email=" + user.getEmail()); + htmlContent = htmlContent.replace("{{resetLink}}", resetLink); + helper.setText(htmlContent, true); mailSender.send(message); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index e04c46d..cbf977c 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -76,6 +76,8 @@ security: server: port: ${SERVER_PORT:8080} baseurl: "http://92.242.61.23" + front: + port: ":2998" tomcat: uri-encoding: UTF-8 servlet: From fbe80ff6d7ba20ee8b3c277170abb5c702203ea7 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Feb 2026 11:11:24 +0700 Subject: [PATCH 062/274] dev set timeout 30 sec --- .../java/ru/soune/nocopy/service/mail/EmailService.java | 7 +++++++ .../soune/nocopy/service/register/CleanUpVerifyTokens.java | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java index 622e44a..50eda40 100644 --- a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java +++ b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java @@ -63,6 +63,13 @@ public class EmailService { @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()); diff --git a/src/main/java/ru/soune/nocopy/service/register/CleanUpVerifyTokens.java b/src/main/java/ru/soune/nocopy/service/register/CleanUpVerifyTokens.java index 455d8e8..2a16142 100644 --- a/src/main/java/ru/soune/nocopy/service/register/CleanUpVerifyTokens.java +++ b/src/main/java/ru/soune/nocopy/service/register/CleanUpVerifyTokens.java @@ -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 expiredTokens = emailVerificationTokenRepository.findByExpiresAtBefore(now); if (!expiredTokens.isEmpty()) { From f34e9df0c84ee5f3a69f5bf49f0738e9e91341d7 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Feb 2026 11:19:54 +0700 Subject: [PATCH 063/274] dev set timeout 30 sec --- src/main/java/ru/soune/nocopy/service/mail/EmailService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java index 50eda40..763a009 100644 --- a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java +++ b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java @@ -117,8 +117,8 @@ public class EmailService { String htmlContent = loadAndProcessTemplate(user.getFullName(), code, "templates/reset-password.html"); - - String resetLink = escapeHtml(baseUrl + "/reset-password?email=" + user.getEmail()); + //TODO frontPort убрать после настройки редиректа nginx + String resetLink = escapeHtml(baseUrl + frontPort + "/reset-password?email=" + user.getEmail()); htmlContent = htmlContent.replace("{{resetLink}}", resetLink); helper.setText(htmlContent, true); From 86e0e0619f83122bb6ec8732536f166dfa738112 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Feb 2026 13:06:53 +0700 Subject: [PATCH 064/274] dev comment logic for check max users --- .../nocopy/service/register/AuthService.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/register/AuthService.java b/src/main/java/ru/soune/nocopy/service/register/AuthService.java index 88db04f..7baa057 100644 --- a/src/main/java/ru/soune/nocopy/service/register/AuthService.java +++ b/src/main/java/ru/soune/nocopy/service/register/AuthService.java @@ -217,16 +217,16 @@ 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); From 5551d84bf7de28f4f44f4a3dc1af4506e0ee5fdb Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Feb 2026 20:51:14 +0700 Subject: [PATCH 065/274] dev comment logic for check max users --- .../nocopy/controller/PayoutController.java | 133 +++++++++++++++ .../dto/payout/AddBankTransferRequest.java | 44 +++++ .../nocopy/dto/payout/AddCardRequest.java | 19 +++ .../nocopy/dto/payout/PayoutMethodDTO.java | 20 +++ .../dto/payout/PayoutMethodResponse.java | 23 +++ .../dto/payout/PayoutMethodTypeDTO.java | 14 ++ .../nocopy/dto/payout/PayoutRequestDTO.java | 23 +++ .../nocopy/dto/payout/PayoutResponse.java | 54 ++++++ .../payout/BankTransferPayoutMethod.java | 50 ++++++ .../entity/payout/CardPayoutMethod.java | 37 ++++ .../entity/payout/InternalPayoutMethod.java | 19 +++ .../nocopy/entity/payout/PayoutMethod.java | 48 ++++++ .../nocopy/entity/payout/PayoutRequest.java | 64 +++++++ .../entity/payout/PayoutRequestStatus.java | 9 + .../nocopy/entity/payout/PayoutType.java | 17 ++ .../payout/convertor/CardNumberEncryptor.java | 74 ++++++++ .../nocopy/entity/referral/Referral.java | 3 + .../exception/DuplicateMethodException.java | 7 + .../exception/InsufficientFundsException.java | 7 + .../InvalidPayoutMethodException.java | 7 + .../PendingPayoutExistsException.java | 7 + .../repository/PayoutMethodRepository.java | 37 ++++ .../repository/PayoutRequestRepository.java | 39 +++++ .../repository/ReferralJpaRepository.java | 5 +- .../service/payment/PaymentService.java | 11 +- .../nocopy/service/payout/BalanceService.java | 94 ++++++++++ .../service/payout/PayoutMethodService.java | 143 ++++++++++++++++ .../service/payout/PayoutRequestService.java | 160 ++++++++++++++++++ .../service/referral/ReferralRepoImpl.java | 1 + .../nocopy/service/register/AuthService.java | 1 - 30 files changed, 1167 insertions(+), 3 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/controller/PayoutController.java create mode 100644 src/main/java/ru/soune/nocopy/dto/payout/AddBankTransferRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/payout/AddCardRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodDTO.java create mode 100644 src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodResponse.java create mode 100644 src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodTypeDTO.java create mode 100644 src/main/java/ru/soune/nocopy/dto/payout/PayoutRequestDTO.java create mode 100644 src/main/java/ru/soune/nocopy/dto/payout/PayoutResponse.java create mode 100644 src/main/java/ru/soune/nocopy/entity/payout/BankTransferPayoutMethod.java create mode 100644 src/main/java/ru/soune/nocopy/entity/payout/CardPayoutMethod.java create mode 100644 src/main/java/ru/soune/nocopy/entity/payout/InternalPayoutMethod.java create mode 100644 src/main/java/ru/soune/nocopy/entity/payout/PayoutMethod.java create mode 100644 src/main/java/ru/soune/nocopy/entity/payout/PayoutRequest.java create mode 100644 src/main/java/ru/soune/nocopy/entity/payout/PayoutRequestStatus.java create mode 100644 src/main/java/ru/soune/nocopy/entity/payout/PayoutType.java create mode 100644 src/main/java/ru/soune/nocopy/entity/payout/convertor/CardNumberEncryptor.java create mode 100644 src/main/java/ru/soune/nocopy/exception/DuplicateMethodException.java create mode 100644 src/main/java/ru/soune/nocopy/exception/InsufficientFundsException.java create mode 100644 src/main/java/ru/soune/nocopy/exception/InvalidPayoutMethodException.java create mode 100644 src/main/java/ru/soune/nocopy/exception/PendingPayoutExistsException.java create mode 100644 src/main/java/ru/soune/nocopy/repository/PayoutMethodRepository.java create mode 100644 src/main/java/ru/soune/nocopy/repository/PayoutRequestRepository.java create mode 100644 src/main/java/ru/soune/nocopy/service/payout/BalanceService.java create mode 100644 src/main/java/ru/soune/nocopy/service/payout/PayoutMethodService.java create mode 100644 src/main/java/ru/soune/nocopy/service/payout/PayoutRequestService.java diff --git a/src/main/java/ru/soune/nocopy/controller/PayoutController.java b/src/main/java/ru/soune/nocopy/controller/PayoutController.java new file mode 100644 index 0000000..bad323d --- /dev/null +++ b/src/main/java/ru/soune/nocopy/controller/PayoutController.java @@ -0,0 +1,133 @@ +package ru.soune.nocopy.controller; + + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.soune.nocopy.dto.payout.*; +import ru.soune.nocopy.entity.payout.BankTransferPayoutMethod; +import ru.soune.nocopy.entity.payout.CardPayoutMethod; +import ru.soune.nocopy.entity.payout.PayoutRequest; +import ru.soune.nocopy.entity.payout.PayoutType; +import ru.soune.nocopy.service.payout.PayoutMethodService; +import ru.soune.nocopy.service.payout.PayoutRequestService; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/payouts") +@RequiredArgsConstructor +@Slf4j +public class PayoutController { + + private final PayoutRequestService payoutRequestService; + + private final PayoutMethodService payoutMethodService; + + /* + Create payout + */ + @PostMapping("/create-request") + public ResponseEntity createPayoutRequest(@RequestParam Long userId, + @Valid @RequestBody PayoutRequestDTO request) { + PayoutRequest payoutRequest = payoutRequestService.createPayoutRequest(userId, request); + + return ResponseEntity.status(HttpStatus.CREATED).body(payoutRequest); + } + + /* + Completed payout + */ + @PostMapping("/requests/{requestId}/process") + public ResponseEntity processPayoutRequest(@PathVariable Long requestId) { + PayoutRequest processed = payoutRequestService.processRequest(requestId); + + return ResponseEntity.ok(processed); + } + + /* + Cancel payout + */ + @PostMapping("/requests/{requestId}/{userId}/cancel") + public ResponseEntity cancelPayoutRequest(@PathVariable Long userId, @PathVariable Long requestId) { + PayoutRequest cancelled = payoutRequestService.cancelPayoutRequest(userId, requestId); + + return ResponseEntity.ok(cancelled); + } + + /* + Get user payouts + */ + @GetMapping("/requests/{userId}") + public ResponseEntity> getUserRequests(@PathVariable Long userId) { + return ResponseEntity.ok(payoutRequestService.getUserPayoutRequests(userId)); + } + + /* + Get payout by id + */ + @GetMapping("/requests/{requestId}/{userId}") + public ResponseEntity getPayoutRequest(@PathVariable Long userId, @PathVariable Long requestId) { + return ResponseEntity.ok(payoutRequestService.getPayoutRequest(userId, requestId)); + } + + /* + Get payout by id + */ + @GetMapping("/payout-method/{userId}") + public ResponseEntity> getUserMethods(@PathVariable Long userId) { + return ResponseEntity.ok(payoutMethodService.getUserMethods(userId)); + } + + /* + Get payout-methods + */ + @GetMapping("/payout-method") + public ResponseEntity> getAvailableMethodTypes() { + List types = Arrays.stream(PayoutType.values()) + .map(type -> PayoutMethodTypeDTO.builder() + .code(type.name()) + .name(type.getDisplayName()) + .build()) + .collect(Collectors.toList()); + + return ResponseEntity.ok(types); + } + /* + add card method by userId + */ + @PostMapping("/payout-method/card/{userId}") + public ResponseEntity addCardMethod(@PathVariable Long userId, + @Valid @RequestBody AddCardRequest request) { + CardPayoutMethod method = payoutMethodService.addCardMethod(userId, request); + return ResponseEntity.status(HttpStatus.CREATED) + .body(payoutMethodService.convertToDto(method)); + } + + /* + add bank payout-method + */ + @PostMapping("/payout-method/bank-transfer/{userId}") + public ResponseEntity addBankTransferMethod(@PathVariable Long userId, + @Valid @RequestBody AddBankTransferRequest request) { + BankTransferPayoutMethod method = payoutMethodService.addBankTransferMethod(userId, request); + return ResponseEntity.status(HttpStatus.CREATED) + .body(payoutMethodService.convertToDto(method)); + } + + + /* + set default bank payout-method + */ + @PutMapping("/payout-method/{userId}/{methodId}/default") + public ResponseEntity setDefaultMethod(@PathVariable Long userId, @PathVariable Long methodId) { + + payoutMethodService.setDefaultMethod(userId, methodId); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/payout/AddBankTransferRequest.java b/src/main/java/ru/soune/nocopy/dto/payout/AddBankTransferRequest.java new file mode 100644 index 0000000..39a5db3 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payout/AddBankTransferRequest.java @@ -0,0 +1,44 @@ +package ru.soune.nocopy.dto.payout; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AddBankTransferRequest { + + @NotBlank(message = "Bank name is required") + @Size(min = 2, max = 200, message = "Bank name must be between 2 and 200 characters") + private String bankName; + + @NotBlank(message = "BIC is required") + @Pattern(regexp = "^[0-9]{9}$", message = "BIC must contain 9 digits") + private String bic; + + @Pattern(regexp = "^[0-9]{20}$", message = "Correspondent account must contain 20 digits") + private String correspondentAccount; + + @NotBlank(message = "Account number is required") + @Pattern(regexp = "^[0-9]{20}$", message = "Account number must contain 20 digits") + private String accountNumber; + + @NotBlank(message = "Account holder name is required") + @Size(min = 3, max = 200, message = "Account holder name must be between 3 and 200 characters") + private String accountHolder; + + @Pattern(regexp = "^[0-9]{10}$|^[0-9]{12}$", message = "INN must contain 10 or 12 digits") + private String inn; + + @Pattern(regexp = "^[0-9]{9}$", message = "KPP must contain 9 digits") + private String kpp; + + @Builder.Default + private boolean saveAsDefault = false; +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/dto/payout/AddCardRequest.java b/src/main/java/ru/soune/nocopy/dto/payout/AddCardRequest.java new file mode 100644 index 0000000..0f9f355 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payout/AddCardRequest.java @@ -0,0 +1,19 @@ +package ru.soune.nocopy.dto.payout; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AddCardRequest { + + @NotBlank(message = "Card number required") + @Pattern(regexp = "^[0-9]{16}$", message = "Card number 16 cipher") + private String cardNumber; +} diff --git a/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodDTO.java b/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodDTO.java new file mode 100644 index 0000000..8eadf89 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodDTO.java @@ -0,0 +1,20 @@ +package ru.soune.nocopy.dto.payout; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayoutMethodDTO { + private Long id; + private String type; + private String displayName; + private String maskedDetails; + private boolean isDefault; + private boolean isVerified; +} diff --git a/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodResponse.java b/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodResponse.java new file mode 100644 index 0000000..df72994 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodResponse.java @@ -0,0 +1,23 @@ +package ru.soune.nocopy.dto.payout; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayoutMethodResponse { + private Long id; + private String type; + private String displayName; + private String maskedDetails; + private boolean isDefault; + private boolean isVerified; + private LocalDateTime createdAt; + private String bankName; + private String cardBrand; +} diff --git a/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodTypeDTO.java b/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodTypeDTO.java new file mode 100644 index 0000000..d1f0884 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodTypeDTO.java @@ -0,0 +1,14 @@ +package ru.soune.nocopy.dto.payout; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayoutMethodTypeDTO { + private String code; + private String name; +} diff --git a/src/main/java/ru/soune/nocopy/dto/payout/PayoutRequestDTO.java b/src/main/java/ru/soune/nocopy/dto/payout/PayoutRequestDTO.java new file mode 100644 index 0000000..609a3ae --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payout/PayoutRequestDTO.java @@ -0,0 +1,23 @@ +package ru.soune.nocopy.dto.payout; + +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayoutRequestDTO { + + @NotNull(message = "Sum of pay is required") + @DecimalMin(value = "1000.00", message = "Min sum for pay 1000") + private BigDecimal amount; + + @NotNull(message = "Способ выплаты обязателен") + private Long payoutMethodId; +} diff --git a/src/main/java/ru/soune/nocopy/dto/payout/PayoutResponse.java b/src/main/java/ru/soune/nocopy/dto/payout/PayoutResponse.java new file mode 100644 index 0000000..243db58 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payout/PayoutResponse.java @@ -0,0 +1,54 @@ +package ru.soune.nocopy.dto.payout; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.soune.nocopy.entity.payout.PayoutRequest; +import ru.soune.nocopy.entity.payout.PayoutRequestStatus; +import ru.soune.nocopy.entity.payout.PayoutType; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor + +public class PayoutResponse { + private Long id; + private BigDecimal amount; + private String formattedAmount; + private PayoutRequestStatus status; + private PayoutType payoutType; + private String payoutMethodDisplay; + private LocalDateTime createdAt; + private String formattedCreatedAt; + private LocalDateTime processedAt; + private LocalDateTime completedAt; + private String transactionId; + private String rejectionReason; + private boolean canCancel; + + public static PayoutResponse fromEntity(PayoutRequest request) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"); + + return PayoutResponse.builder() + .id(request.getId()) + .amount(request.getAmount()) + .formattedAmount(String.format("%.2f ₽", request.getAmount())) + .status(request.getStatus()) + .payoutType(request.getPayoutType()) + .payoutMethodDisplay(request.getPayoutMethod() != null ? + request.getPayoutMethod().getDisplayName() : null) + .createdAt(request.getCreatedAt()) + .formattedCreatedAt(request.getCreatedAt() != null ? + request.getCreatedAt().format(formatter) : null) + .processedAt(request.getProcessAt()) + .completedAt(request.getCompletedAt()) + .rejectionReason(request.getRejectionReason()) + .canCancel(request.getStatus() == PayoutRequestStatus.PENDING) + .build(); + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/payout/BankTransferPayoutMethod.java b/src/main/java/ru/soune/nocopy/entity/payout/BankTransferPayoutMethod.java new file mode 100644 index 0000000..fca64ee --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payout/BankTransferPayoutMethod.java @@ -0,0 +1,50 @@ +package ru.soune.nocopy.entity.payout; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import lombok.*; + +@Entity +@DiscriminatorValue("BANK_TRANSFER") +@NoArgsConstructor +@AllArgsConstructor +@Getter @Setter +public class BankTransferPayoutMethod extends PayoutMethod { + + @Column(name = "bank_name") + private String bankName; + + @Column(name = "bic") + private String bic; + + @Column(name = "correspondent_account") + private String correspondentAccount; + + @Column(name = "account_number") + private String accountNumber; + + @Column(name = "account_holder") + private String accountHolder; + + @Column(name = "inn") + private String inn; + + @Column(name = "kpp") + private String kpp; + + @Override + public String getDisplayName() { + return String.format("Tax %s in %s", getMaskedAccount(), bankName); + } + + @Override + public String getMaskedDetails() { + return getMaskedAccount(); + } + + private String getMaskedAccount() { + if (accountNumber == null || accountNumber.length() < 4) return ""; + return "..." + accountNumber.substring(accountNumber.length() - 4); + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/payout/CardPayoutMethod.java b/src/main/java/ru/soune/nocopy/entity/payout/CardPayoutMethod.java new file mode 100644 index 0000000..053fc91 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payout/CardPayoutMethod.java @@ -0,0 +1,37 @@ +package ru.soune.nocopy.entity.payout; + +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import lombok.*; +import ru.soune.nocopy.entity.payout.convertor.CardNumberEncryptor; + +@Entity +@DiscriminatorValue("CARD") +@NoArgsConstructor +@AllArgsConstructor +@Setter @Getter +public class CardPayoutMethod extends PayoutMethod { + @Convert(converter = CardNumberEncryptor.class) + @Column(name = "card_number", nullable = false) + private String cardNumber; + + @Column(name = "card_hash", unique = true) + private String cardHash; + + @Override + public String getDisplayName() { + return "Card: " + getMaskedCardNumber(); + } + + @Override + public String getMaskedDetails() { + return getMaskedCardNumber(); + } + + private String getMaskedCardNumber() { + if (cardNumber == null || cardNumber.length() < 4) return ""; + return "****" + cardNumber.substring(cardNumber.length() - 4); + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/payout/InternalPayoutMethod.java b/src/main/java/ru/soune/nocopy/entity/payout/InternalPayoutMethod.java new file mode 100644 index 0000000..9b3c8ff --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payout/InternalPayoutMethod.java @@ -0,0 +1,19 @@ +package ru.soune.nocopy.entity.payout; + +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; + +@Entity +@DiscriminatorValue("INTERNAL") +public class InternalPayoutMethod extends PayoutMethod { + + @Override + public String getDisplayName() { + return "Internal balance"; + } + + @Override + public String getMaskedDetails() { + return "Account balance"; + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/payout/PayoutMethod.java b/src/main/java/ru/soune/nocopy/entity/payout/PayoutMethod.java new file mode 100644 index 0000000..77c91d3 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payout/PayoutMethod.java @@ -0,0 +1,48 @@ +package ru.soune.nocopy.entity.payout; + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import ru.soune.nocopy.entity.user.User; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "payout_methods") +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +@DiscriminatorColumn(name = "method_type") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PayoutMethod { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "method_type", insertable = false, updatable = false) + private String methodType; + + @Column(name = "is_default") + private boolean isDefault; + + @CreationTimestamp + private LocalDateTime createdAt; + + @UpdateTimestamp + private LocalDateTime updatedAt; + + + public String getDisplayName() { + return null; + } + + public String getMaskedDetails() { + return null; + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/payout/PayoutRequest.java b/src/main/java/ru/soune/nocopy/entity/payout/PayoutRequest.java new file mode 100644 index 0000000..231f8d0 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payout/PayoutRequest.java @@ -0,0 +1,64 @@ +package ru.soune.nocopy.entity.payout; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import ru.soune.nocopy.entity.user.User; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + + +@Entity +@Table(name = "payout_requests") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class PayoutRequest { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "payout_method_id") + @JsonIgnore + private PayoutMethod payoutMethod; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + @JsonIgnore + private User user; + + @Column(nullable = false) + private BigDecimal amount; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private PayoutRequestStatus status; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, name = "payout_type") + @JsonIgnore + private PayoutType payoutType; + + @Column(name = "process_at") + private LocalDateTime processAt; + + @Column(name = "completed_at") + private LocalDateTime completedAt; + + @Column(name = "rejection_reason") + private String rejectionReason; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; +} diff --git a/src/main/java/ru/soune/nocopy/entity/payout/PayoutRequestStatus.java b/src/main/java/ru/soune/nocopy/entity/payout/PayoutRequestStatus.java new file mode 100644 index 0000000..bf5f013 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payout/PayoutRequestStatus.java @@ -0,0 +1,9 @@ +package ru.soune.nocopy.entity.payout; + +public enum PayoutRequestStatus { + PENDING, + PROCESSING, + COMPLETED, + REJECTED, + CANCELLED +} diff --git a/src/main/java/ru/soune/nocopy/entity/payout/PayoutType.java b/src/main/java/ru/soune/nocopy/entity/payout/PayoutType.java new file mode 100644 index 0000000..b7095bc --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payout/PayoutType.java @@ -0,0 +1,17 @@ +package ru.soune.nocopy.entity.payout; + +public enum PayoutType { + CARD("card"), + BANK_TRANSFER("bank_transfer"), + INTERNAL("internal"); + + private final String displayName; + + PayoutType(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/payout/convertor/CardNumberEncryptor.java b/src/main/java/ru/soune/nocopy/entity/payout/convertor/CardNumberEncryptor.java new file mode 100644 index 0000000..46b95b1 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payout/convertor/CardNumberEncryptor.java @@ -0,0 +1,74 @@ +package ru.soune.nocopy.entity.payout.convertor; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import javax.crypto.Cipher; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Base64; + +@Converter +public class CardNumberEncryptor implements AttributeConverter { + + private static final String ALGORITHM = "AES/GCM/NoPadding"; + private static final int GCM_TAG_LENGTH = 128; + private static final int GCM_IV_LENGTH = 12; + + private static final byte[] FIXED_KEY = "12345678901234567890123456789012".getBytes(); + + @Override + public String convertToDatabaseColumn(String cardNumber) { + if (cardNumber == null) return null; + + try { + SecretKeySpec key = new SecretKeySpec(FIXED_KEY, "AES"); + + byte[] iv = new byte[GCM_IV_LENGTH]; + new SecureRandom().nextBytes(iv); + + Cipher cipher = Cipher.getInstance(ALGORITHM); + GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); + cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec); + + byte[] encrypted = cipher.doFinal(cardNumber.getBytes(StandardCharsets.UTF_8)); + + byte[] result = new byte[iv.length + encrypted.length]; + System.arraycopy(iv, 0, result, 0, iv.length); + System.arraycopy(encrypted, 0, result, iv.length, encrypted.length); + + return Base64.getEncoder().encodeToString(result); + + } catch (Exception e) { + throw new RuntimeException("Failed to encrypt card number", e); + } + } + + @Override + public String convertToEntityAttribute(String dbData) { + if (dbData == null) return null; + + try { + byte[] decoded = Base64.getDecoder().decode(dbData); + + byte[] iv = new byte[GCM_IV_LENGTH]; + byte[] encrypted = new byte[decoded.length - GCM_IV_LENGTH]; + System.arraycopy(decoded, 0, iv, 0, iv.length); + System.arraycopy(decoded, iv.length, encrypted, 0, encrypted.length); + + SecretKeySpec key = new SecretKeySpec(FIXED_KEY, "AES"); + + Cipher cipher = Cipher.getInstance(ALGORITHM); + GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); + cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec); + + byte[] decrypted = cipher.doFinal(encrypted); + return new String(decrypted, StandardCharsets.UTF_8); + + } catch (Exception e) { + throw new RuntimeException("Failed to decrypt card number", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/entity/referral/Referral.java b/src/main/java/ru/soune/nocopy/entity/referral/Referral.java index b03dc4a..a978652 100644 --- a/src/main/java/ru/soune/nocopy/entity/referral/Referral.java +++ b/src/main/java/ru/soune/nocopy/entity/referral/Referral.java @@ -25,6 +25,9 @@ public class Referral { @Column(name = "total_income") private int totalIncome = 0; + @Column(name = "available_income") + private int availableIncome = 0; + @Column(name = "is_active") private boolean isActive = false; diff --git a/src/main/java/ru/soune/nocopy/exception/DuplicateMethodException.java b/src/main/java/ru/soune/nocopy/exception/DuplicateMethodException.java new file mode 100644 index 0000000..3897426 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/DuplicateMethodException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class DuplicateMethodException extends RuntimeException { + public DuplicateMethodException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/InsufficientFundsException.java b/src/main/java/ru/soune/nocopy/exception/InsufficientFundsException.java new file mode 100644 index 0000000..6d43734 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/InsufficientFundsException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class InsufficientFundsException extends RuntimeException { + public InsufficientFundsException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/InvalidPayoutMethodException.java b/src/main/java/ru/soune/nocopy/exception/InvalidPayoutMethodException.java new file mode 100644 index 0000000..8be2b6d --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/InvalidPayoutMethodException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class InvalidPayoutMethodException extends RuntimeException { + public InvalidPayoutMethodException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/PendingPayoutExistsException.java b/src/main/java/ru/soune/nocopy/exception/PendingPayoutExistsException.java new file mode 100644 index 0000000..cba5885 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/PendingPayoutExistsException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class PendingPayoutExistsException extends RuntimeException { + public PendingPayoutExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/repository/PayoutMethodRepository.java b/src/main/java/ru/soune/nocopy/repository/PayoutMethodRepository.java new file mode 100644 index 0000000..1b8effd --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/PayoutMethodRepository.java @@ -0,0 +1,37 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.payout.PayoutMethod; +import ru.soune.nocopy.entity.user.User; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface PayoutMethodRepository extends JpaRepository { + + Optional findByIdAndUserId(Long id, Long userId); + + @Query("SELECT p FROM PayoutMethod p WHERE p.user.Id = :userId AND TYPE(p) = :type") + Optional findByUserAndType(@Param("userId") Long userId, + @Param("type") Class type); + + List findByUserId(Long userId); + + Optional findByUserIdAndIsDefaultTrue(Long userId); + + long countByUserId(Long userId); + + @Query("SELECT COUNT(c) > 0 FROM CardPayoutMethod c WHERE c.cardHash = :cardHash") + boolean existsByCardHash(@Param("cardHash") String cardHash); + + @Modifying + @Query("UPDATE PayoutMethod p SET p.isDefault = false WHERE p.user.id = :userId AND p.id != :methodId") + void clearOtherDefault(@Param("userId") Long userId, @Param("methodId") Long methodId); + + Long user(User user); +} diff --git a/src/main/java/ru/soune/nocopy/repository/PayoutRequestRepository.java b/src/main/java/ru/soune/nocopy/repository/PayoutRequestRepository.java new file mode 100644 index 0000000..e6d90b0 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/PayoutRequestRepository.java @@ -0,0 +1,39 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.payout.PayoutRequest; +import ru.soune.nocopy.entity.payout.PayoutRequestStatus; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository +public interface PayoutRequestRepository extends JpaRepository { + + List findByUserIdOrderByCreatedAtDesc(Long userId); + + Page findByUserId(Long userId, Pageable pageable); + + Optional findByIdAndUserId(Long id, Long userId); + + @Query("SELECT COALESCE(SUM(pr.amount), 0) FROM PayoutRequest pr " + + "WHERE pr.user.Id = :userId AND pr.status = 'COMPLETED'") + BigDecimal getTotalPayoutAmountByUser(@Param("userId") Long userId); + + boolean existsByUserIdAndStatusIn(Long userId, List statuses); + + List findByStatusOrderByCreatedAtAsc(PayoutRequestStatus status); + + Page findByStatus(PayoutRequestStatus status, Pageable pageable); + + @Query("SELECT pr FROM PayoutRequest pr WHERE pr.status = 'PENDING' " + + "AND pr.createdAt < :threshold") + List findPendingOlderThan(@Param("threshold") LocalDateTime threshold); +} diff --git a/src/main/java/ru/soune/nocopy/repository/ReferralJpaRepository.java b/src/main/java/ru/soune/nocopy/repository/ReferralJpaRepository.java index edbdd48..c90eba9 100644 --- a/src/main/java/ru/soune/nocopy/repository/ReferralJpaRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ReferralJpaRepository.java @@ -24,7 +24,10 @@ public interface ReferralJpaRepository extends JpaRepository { List findByInviterId(Long inviterId, Pageable pageable); @Modifying - @Query("UPDATE Referral r SET r.totalIncome = r.totalIncome + :amount WHERE r.userId = :userId") + @Query("UPDATE Referral r SET " + + "r.availableIncome = r.availableIncome + :amount ," + + "r.totalIncome = r.totalIncome + :amount " + + "WHERE r.userId = :userId") void increaseIncome(@Param("userId") Long userId, @Param("amount") int amount); @Modifying diff --git a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java index 7030802..8a05b74 100644 --- a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java +++ b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java @@ -4,6 +4,7 @@ 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.entity.payment.Payment; import ru.soune.nocopy.entity.payment.PaymentOperationType; import ru.soune.nocopy.entity.payment.PaymentStatus; @@ -38,6 +39,8 @@ public class PaymentService { private final TariffInfoService tariffInfoService; + private final ReferralService referralService; + @Transactional public Payment createPayment(String email, Long tariffId, String operationType, String paymentUuid) { User user = userRepository.findByEmail(email); @@ -70,9 +73,15 @@ public class PaymentService { .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); - activateTariffForUser(payment.getUser(), payment.getTariff()); + activateTariffForUser(payment.getUser(), tariff); + + referralService.onUserAccountRefill(payment.getUser().getId(), intPrice); } else if ("payment.canceled".equals(eventType)) { payment.setStatus(PaymentStatus.CANCELED); diff --git a/src/main/java/ru/soune/nocopy/service/payout/BalanceService.java b/src/main/java/ru/soune/nocopy/service/payout/BalanceService.java new file mode 100644 index 0000000..62b357c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/payout/BalanceService.java @@ -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()); + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/payout/PayoutMethodService.java b/src/main/java/ru/soune/nocopy/service/payout/PayoutMethodService.java new file mode 100644 index 0000000..023f9b9 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/payout/PayoutMethodService.java @@ -0,0 +1,143 @@ +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.BankTransferPayoutMethod; +import ru.soune.nocopy.entity.payout.CardPayoutMethod; +import ru.soune.nocopy.entity.payout.InternalPayoutMethod; +import ru.soune.nocopy.entity.payout.PayoutMethod; +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 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) { + 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 methods = payoutMethodRepository.findByUserId(user.getId()); + if (methods.isEmpty()) { + return createInternalMethod(user); + } + return methods.get(0); + }); + } + + public PayoutMethodDTO convertToDto(PayoutMethod method) { + return PayoutMethodDTO.builder() + .id(method.getId()) + .type(method.getClass().getSimpleName().replace("PayoutMethod", "")) + .displayName(method.getDisplayName()) + .maskedDetails(method.getMaskedDetails()) + .isDefault(method.isDefault()) + .build(); + } + + 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); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/service/payout/PayoutRequestService.java b/src/main/java/ru/soune/nocopy/service/payout/PayoutRequestService.java new file mode 100644 index 0000000..1db44cf --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/payout/PayoutRequestService.java @@ -0,0 +1,160 @@ +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 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()); + + log.info("Payout requset created: ID={}, user={}, sum={}, status={}", + savedRequest.getId(), user.getId(), request.getAmount(), savedRequest.getStatus()); + + 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 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); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/referral/ReferralRepoImpl.java b/src/main/java/ru/soune/nocopy/service/referral/ReferralRepoImpl.java index 59f67db..c210ed3 100644 --- a/src/main/java/ru/soune/nocopy/service/referral/ReferralRepoImpl.java +++ b/src/main/java/ru/soune/nocopy/service/referral/ReferralRepoImpl.java @@ -151,6 +151,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()); diff --git a/src/main/java/ru/soune/nocopy/service/register/AuthService.java b/src/main/java/ru/soune/nocopy/service/register/AuthService.java index 7baa057..576ced2 100644 --- a/src/main/java/ru/soune/nocopy/service/register/AuthService.java +++ b/src/main/java/ru/soune/nocopy/service/register/AuthService.java @@ -11,7 +11,6 @@ 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; From a42e3c72fcbb6b87c005e69d2592a7b235312284 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Feb 2026 21:53:49 +0700 Subject: [PATCH 066/274] dev comment logic for check max users --- .../nocopy/exception/CompanyAlreadyExist.java | 7 + .../nocopy/handler/RegRequestHandler.java | 144 +++++++++--------- .../nocopy/service/register/AuthService.java | 9 +- 3 files changed, 90 insertions(+), 70 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/exception/CompanyAlreadyExist.java diff --git a/src/main/java/ru/soune/nocopy/exception/CompanyAlreadyExist.java b/src/main/java/ru/soune/nocopy/exception/CompanyAlreadyExist.java new file mode 100644 index 0000000..80f3775 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/CompanyAlreadyExist.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class CompanyAlreadyExist extends RuntimeException { + public CompanyAlreadyExist(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java index e3aad4e..6e60a96 100644 --- a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java @@ -13,6 +13,7 @@ import ru.soune.nocopy.dto.register.RegRequest; import ru.soune.nocopy.entity.referral.Referral; import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.user.EmailVerificationToken; +import ru.soune.nocopy.exception.CompanyAlreadyExist; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.ValidationException; import ru.soune.nocopy.handler.validator.RegRequestValidator; @@ -45,84 +46,91 @@ public class RegRequestHandler implements RequestHandler { @Override public BaseResponse handle(BaseRequest request) throws ValidationException { RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class); + try { + if (userRepository.existsByEmail(regRequest.getEmail()) || userRepository.existsByPhone(regRequest.getPhone())) { + RegAnswer regAnswer = new RegAnswer(); + regAnswer.setFieldErrors(Arrays.asList(Map.of("email", regRequest.getEmail()))); + regAnswer.setFieldErrors(Arrays.asList(Map.of("phone", regRequest.getPhone()))); - if (userRepository.existsByEmail(regRequest.getEmail()) || userRepository.existsByPhone(regRequest.getPhone())) { + throw new NotValidFieldException("User already exists with email:" + regRequest.getEmail() + " or phone: " + + regRequest.getPhone(), new BaseResponse(request.getMsgId(), + MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getCode(), + MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getDescription(), regAnswer)); + } + + String referralLink = regRequest.getReferralLink(); + String mailVerified = regRequest.getMailVerified(); + + if (referralLink != null) { + Referral referral = referralJpaRepository.findByReferralLink(referralLink); + if (referral == null) { + throw new NotValidFieldException("Referral link is not found:" + referralLink + + referralLink, new BaseResponse(request.getMsgId(), + MessageCode.REFERRAL_LINK_IS_NOT_EXIST.getCode(), + MessageCode.REFERRAL_LINK_IS_NOT_EXIST.getDescription(), Map.of( + "referralLink", referralLink))); + } + + if (mailVerified == null) { + throw new NotValidFieldException("Mailverified is null:" + referralLink + + referralLink, new BaseResponse(request.getMsgId(), + MessageCode.MAIL_VERIFIED_NULL.getCode(), + MessageCode.MAIL_VERIFIED_NULL.getDescription(), Map.of( + "mailVerified", mailVerified))); + } + } + + BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest"); + + regRequestValidator.validate(regRequest, bindingResult); + + if (bindingResult.hasErrors()) { + throw new ValidationException(bindingResult, request.getMsgId()); + } + + boolean verified = mailVerified.equals("true"); + + AuthToken authToken = authService.register(regRequest, verified); + EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken); RegAnswer regAnswer = new RegAnswer(); - regAnswer.setFieldErrors(Arrays.asList(Map.of("email", regRequest.getEmail()))); - regAnswer.setFieldErrors(Arrays.asList(Map.of("phone", regRequest.getPhone()))); - throw new NotValidFieldException("User already exists with email:" + regRequest.getEmail() + " or phone: " + - regRequest.getPhone(), new BaseResponse(request.getMsgId(), - MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getCode(), - MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getDescription(), regAnswer)); - } + if (verified) { + try { + emailService.sendVerificationEmail(authToken.getUser(), emailToken.getToken()); + } catch (Exception e) { + return new BaseResponse(request.getMsgId(), MessageCode.SEND_EMAIL_EXCEPTION.getCode(), + MessageCode.SEND_EMAIL_EXCEPTION.getDescription(), Map.of( + "message", "not send", + "email", regRequest.getEmail() + )); + } - String referralLink = regRequest.getReferralLink(); - String mailVerified = regRequest.getMailVerified(); + regAnswer.setVerified(false); + regAnswer.setActive(false); + } else { + regAnswer.setUserId(authToken.getUser().getId()); - if (referralLink != null) { - Referral referral = referralJpaRepository.findByReferralLink(referralLink); - if (referral == null) { - throw new NotValidFieldException("Referral link is not found:" + referralLink + - referralLink, new BaseResponse(request.getMsgId(), - MessageCode.REFERRAL_LINK_IS_NOT_EXIST.getCode(), - MessageCode.REFERRAL_LINK_IS_NOT_EXIST.getDescription(), Map.of( - "referralLink", referralLink))); + regAnswer.setVerified(true); + regAnswer.setActive(true); } - if (mailVerified == null) { - throw new NotValidFieldException("Mailverified is null:" + referralLink + - referralLink, new BaseResponse(request.getMsgId(), - MessageCode.MAIL_VERIFIED_NULL.getCode(), - MessageCode.MAIL_VERIFIED_NULL.getDescription(), Map.of( - "mailVerified", mailVerified))); - } - } + String token = authToken.getToken(); + authService.useUserAuthToken(token); - BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest"); - - regRequestValidator.validate(regRequest, bindingResult); - - if (bindingResult.hasErrors()) { - throw new ValidationException(bindingResult, request.getMsgId()); - } - - boolean verified = mailVerified.equals("true"); - AuthToken authToken = authService.register(regRequest, verified); - EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken); - RegAnswer regAnswer = new RegAnswer(); - - if (verified) { - try { - emailService.sendVerificationEmail(authToken.getUser(), emailToken.getToken()); - } catch (Exception e) { - return new BaseResponse(request.getMsgId(), MessageCode.SEND_EMAIL_EXCEPTION.getCode(), - MessageCode.SEND_EMAIL_EXCEPTION.getDescription(), Map.of( - "message", "not send", - "email", regRequest.getEmail() - )); - } - - regAnswer.setVerified(false); - regAnswer.setActive(false); - } else { + regAnswer.setToken(token); regAnswer.setUserId(authToken.getUser().getId()); - regAnswer.setVerified(true); - regAnswer.setActive(true); + if (regRequest.getAccountType().equals("b2c")) { + referralService.onRegister(authToken.getUser().getId(), regRequest.getReferralLink()); + } + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), regAnswer); + + } catch (CompanyAlreadyExist e) { + return new BaseResponse(request.getMsgId(), MessageCode.COMPANY_ALREADY_EXISTS.getCode(), + MessageCode.COMPANY_ALREADY_EXISTS.getDescription(), Map.of( + "inn", regRequest.getInn())); } - - String token = authToken.getToken(); - authService.useUserAuthToken(token); - - regAnswer.setToken(token); - regAnswer.setUserId(authToken.getUser().getId()); - - if (regRequest.getAccountType().equals("b2c")) { - referralService.onRegister(authToken.getUser().getId(), regRequest.getReferralLink()); - } - - return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), - MessageCode.SUCCESS.getDescription(), regAnswer); } } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/register/AuthService.java b/src/main/java/ru/soune/nocopy/service/register/AuthService.java index 576ced2..a8dc32c 100644 --- a/src/main/java/ru/soune/nocopy/service/register/AuthService.java +++ b/src/main/java/ru/soune/nocopy/service/register/AuthService.java @@ -17,6 +17,7 @@ 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.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.*; @@ -61,7 +62,11 @@ public class AuthService { 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 { @@ -173,7 +178,7 @@ public class AuthService { Optional byInn = companyRepository.findByInn(request.getInn()); if (byInn.isPresent()) { - throw new IllegalStateException("Company was already created"); + throw new CompanyAlreadyExist("Company was already created"); } Company company = new Company(); From 339a3f61cdbb61c77cd1550d213151cde670b580 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 21 Feb 2026 16:16:58 +0700 Subject: [PATCH 067/274] dev comment logic for check max users --- src/main/resources/application.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index cbf977c..d2fb1bb 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -104,7 +104,7 @@ yandex: search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image} searchapi: - api-key: ${SEARCHAPI_API_KEY:qM4tWwZZkxgNcMQV3AJtmsBY} + api-key: ${SEARCHAPI_API_KEY:Hz3J2xCCU1qhaK4TdZ6dMyNM} reverse-image-url: "https://searchapi.io/api/v1/search" app: From ca10d4d13094ceeb8215e4b00bc43cf2b0e8e54d Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 21 Feb 2026 21:01:23 +0700 Subject: [PATCH 068/274] dev comment logic for check max users --- .../java/ru/soune/nocopy/repository/FileEntityRepository.java | 3 +++ .../java/ru/soune/nocopy/service/FileSimilarityService.java | 4 ++-- .../soune/nocopy/service/file/impl/FileUploadServiceImpl.java | 3 +-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java index 9e098db..c201496 100644 --- a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java @@ -55,4 +55,7 @@ public interface FileEntityRepository extends JpaRepository @Query("SELECT f FROM FileEntity f WHERE f.userId = :userId AND f.mimeType = :mimeType") List findByUserIdAndMimeType(Long userId, String mimeType); + + @Query("SELECT f FROM FileEntity f WHERE f.mimeType = :mimeType") + List findByMimeType(String mimeType); } diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 4219c9e..c261a20 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -110,9 +110,9 @@ public class FileSimilarityService { return duplicates; } - public void hasDuplicatesByHash(String path, Long userId, String mimeType) throws Exception { + public void hasDuplicatesByHash(String path, String mimeType) throws Exception { String hash = calculateFileHash(path); - List fileEntityList = fileEntityRepository.findByUserIdAndMimeType(userId, mimeType); + List fileEntityList = fileEntityRepository.findByMimeType(mimeType); for (FileEntity file : fileEntityList) { if (calculateFileHash(file.getFilePath()).equals(hash)) { diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index 1a09541..2cb7d7d 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -323,8 +323,7 @@ public class FileUploadServiceImpl implements FileUploadService { if (session.getFileType().startsWith("image")) { checkForDuplicatesSynchronously(finalFilePath); } else if (session.getFileType().startsWith("audio") || session.getFileType().startsWith("document")) { - fileSimilarityService.hasDuplicatesByHash(finalFilePath, session.getUserId(), - session.getFileType()); + fileSimilarityService.hasDuplicatesByHash(finalFilePath ,session.getFileType()); File file = new File(finalFilePath); FileProtector.Type type = "document".equals(session.getFileType()) ? From f932bb47f0fdf6d6688901f6c54a829ad7183a45 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 21 Feb 2026 21:35:13 +0700 Subject: [PATCH 069/274] dev comment logic for check max users --- .../file/impl/FileUploadServiceImpl.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index 2cb7d7d..ed895cc 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -326,6 +326,16 @@ public class FileUploadServiceImpl implements FileUploadService { fileSimilarityService.hasDuplicatesByHash(finalFilePath ,session.getFileType()); 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 FileUploadException("Failed to upload chunk: .wav file not have RIFF header"); + } + FileProtector.Type type = "document".equals(session.getFileType()) ? FileProtector.Type.DOC : FileProtector.Type.valueOf(session.getFileType().toUpperCase()); @@ -403,6 +413,29 @@ public class FileUploadServiceImpl implements FileUploadService { } } + //TODO убрать когда научимся защищать другие типы + public boolean isWavFile(File file) { + if (file == null || !file.exists() || file.length() < 12) { + return false; + } + + 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; + } + } + private int protectCost(String fileType) { int cost = 0; From 7631ea8c8c6f5c3b42b257f412bc6007f43bd71c Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 21 Feb 2026 21:41:05 +0700 Subject: [PATCH 070/274] dev comment logic for check max users --- .../java/ru/soune/nocopy/controller/ApiController.java | 2 ++ .../ru/soune/nocopy/exception/FileFormatException.java | 7 +++++++ .../nocopy/service/file/impl/FileUploadServiceImpl.java | 9 ++------- 3 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/exception/FileFormatException.java diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index f9b6fe9..33c4976 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -149,6 +149,8 @@ public class ApiController { MessageCode.DUPLICATE_FILE_UPLOAD.getDescription(), duplicateData )); + }catch (FileFormatException e){ + return buildErrorResponse(uploadId, chunkNumber, e.getMessage()); } catch (Exception e) { log.error("Error uploading chunk", e); return buildErrorResponse(uploadId, chunkNumber, "Failed to upload chunk: " + e.getMessage()); diff --git a/src/main/java/ru/soune/nocopy/exception/FileFormatException.java b/src/main/java/ru/soune/nocopy/exception/FileFormatException.java new file mode 100644 index 0000000..6480320 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/FileFormatException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class FileFormatException extends RuntimeException { + public FileFormatException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index ed895cc..013f9bc 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -5,8 +5,6 @@ import com.vrt.fileprotection.FileProtector; import com.vrt.fileprotection.NoCopyCheckResult; import com.vrt.fileprotection.audio.AudioCheckResult; import com.vrt.fileprotection.documents.DocumentCheckResult; -import com.vrt.fileprotection.image.ImageCheckResult; -import com.vrt.fileprotection.image.ImageLocalSearch; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -19,10 +17,7 @@ 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; @@ -333,7 +328,7 @@ public class FileUploadServiceImpl implements FileUploadService { sessionRepository.save(session); - throw new FileUploadException("Failed to upload chunk: .wav file not have RIFF header"); + throw new FileFormatException("Failed to upload chunk: .wav file not have RIFF header"); } FileProtector.Type type = "document".equals(session.getFileType()) ? From f103aca8daabcadc77754c0e0537da12ca46cee6 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 21 Feb 2026 22:41:48 +0700 Subject: [PATCH 071/274] dev comment logic for check max users --- .../file/impl/FileUploadServiceImpl.java | 66 ++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index 013f9bc..98cc244 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -322,13 +322,14 @@ public class FileUploadServiceImpl implements FileUploadService { File file = new File(finalFilePath); - if (session.getFileType().startsWith("audio") && !isWavFile(file)) { + if (session.getFileType().startsWith("audio") && !isValidWav(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"); + 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()) ? @@ -431,6 +432,67 @@ public class FileUploadServiceImpl implements FileUploadService { } } + 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 int protectCost(String fileType) { int cost = 0; From c289032671abc76ae7dc14e5db41e65a6053c744 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 21 Feb 2026 23:21:44 +0700 Subject: [PATCH 072/274] dev comment logic for check max users --- .../nocopy/controller/FileController.java | 91 +++++++++++++++---- 1 file changed, 75 insertions(+), 16 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/FileController.java b/src/main/java/ru/soune/nocopy/controller/FileController.java index 4ebdb10..3ffd0cd 100644 --- a/src/main/java/ru/soune/nocopy/controller/FileController.java +++ b/src/main/java/ru/soune/nocopy/controller/FileController.java @@ -3,9 +3,7 @@ package ru.soune.nocopy.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import org.springframework.web.bind.annotation.*; import ru.soune.nocopy.dto.file.CheckStatus; import ru.soune.nocopy.entity.file.FileEntity; @@ -16,12 +14,15 @@ import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.file.FileStorageService; -import org.springframework.http.ContentDisposition; +import java.io.FileNotFoundException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; @RestController @RequestMapping("/api/files") @@ -40,23 +41,81 @@ public class FileController { @Autowired private UserRepository userRepository; +// @GetMapping("/public/{fileId}") +// public ResponseEntity getPublicFile( +// @PathVariable String fileId) throws IOException { +// +// FileEntity fileEntity = fileRepository.findById(fileId) +// .orElseThrow(() -> new RuntimeException("File not found")); +// +// Resource resource = fileStorageService.loadFileAsResource(fileEntity.getFilePath()); +// +// ContentDisposition contentDisposition = ContentDisposition.inline() +// .filename(fileEntity.getOriginalFileName(), StandardCharsets.UTF_8) +// .build(); +// +// return ResponseEntity.ok() +// .contentType(new MediaType(fileEntity.getMimeType(), fileEntity.getFileExtension())) +// .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString()) +// .body(resource); +// } + @GetMapping("/public/{fileId}") - public ResponseEntity getPublicFile( - @PathVariable String fileId) throws IOException { + public ResponseEntity getPublicFile(@PathVariable String fileId) { + try { + FileEntity fileEntity = fileRepository.findById(fileId) + .orElseThrow(() -> new FileNotFoundException("Not found: " + fileId)); - FileEntity fileEntity = fileRepository.findById(fileId) - .orElseThrow(() -> new RuntimeException("File not found")); + Path filePath = Paths.get(fileEntity.getFilePath()); + if (!Files.exists(filePath)) { + log.error("Not on disk: {}", fileEntity.getFilePath()); + return ResponseEntity.notFound().build(); + } - Resource resource = fileStorageService.loadFileAsResource(fileEntity.getFilePath()); + Resource resource = fileStorageService.loadFileAsResource(fileEntity.getFilePath()); - ContentDisposition contentDisposition = ContentDisposition.inline() - .filename(fileEntity.getOriginalFileName(), StandardCharsets.UTF_8) - .build(); + MediaType mediaType = getMediaType(fileEntity); - return ResponseEntity.ok() - .contentType(new MediaType(fileEntity.getMimeType(), fileEntity.getFileExtension())) - .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString()) - .body(resource); + ContentDisposition contentDisposition = ContentDisposition.inline() + .filename(fileEntity.getOriginalFileName(), StandardCharsets.UTF_8) + .build(); + + return ResponseEntity.ok() + .contentType(mediaType) + .contentLength(Files.size(filePath)) + .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString()) + .header(HttpHeaders.CACHE_CONTROL, "public, max-age=3600") + .body(resource); + + } catch (FileNotFoundException e) { + log.warn("Not on disk: {}", fileId); + return ResponseEntity.notFound().build(); + } catch (IOException e) { + log.error("Read file exception: {}", fileId, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + private MediaType getMediaType(FileEntity fileEntity) { + try { + return MediaType.parseMediaType(fileEntity.getMimeType()); + } catch (InvalidMediaTypeException e) { + // Если MIME тип некорректен, определяем по расширению + String extension = fileEntity.getFileExtension().toLowerCase(); + switch (extension) { + case "jpg": + case "jpeg": + return MediaType.IMAGE_JPEG; + case "png": + return MediaType.IMAGE_PNG; + case "gif": + return MediaType.IMAGE_GIF; + case "pdf": + return MediaType.APPLICATION_PDF; + default: + return MediaType.APPLICATION_OCTET_STREAM; + } + } } @GetMapping("/protected/{fileId}") From 76b07db96405e2fe5eaa1c398c72f8f3b9ac3490 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 21 Feb 2026 23:45:54 +0700 Subject: [PATCH 073/274] dev add retry --- .../service/search/SearchImageService.java | 100 ++++++++++++++---- 1 file changed, 82 insertions(+), 18 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index a7dfb3b..d38b935 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -32,10 +32,10 @@ public class SearchImageService { public SearchImageService() { this.httpClient = new OkHttpClient.Builder() - .connectTimeout(30, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .callTimeout(30, TimeUnit.SECONDS) + .connectTimeout(18, TimeUnit.SECONDS) + .writeTimeout(18, TimeUnit.SECONDS) + .readTimeout(18, TimeUnit.SECONDS) + .callTimeout(18, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .build(); @@ -96,7 +96,6 @@ public class SearchImageService { } private String callReverseImageApiByUrl(String imageUrl, String engine, String searchType) throws IOException { - if (searchApiKey == null || searchApiKey.isBlank()) { throw new IllegalStateException("SearchAPI key not configured"); } @@ -115,26 +114,91 @@ public class SearchImageService { .header("User-Agent", "Mozilla/5.0") .build(); - long start = System.currentTimeMillis(); + int maxAttempts = 3; + IOException lastException = null; - try (Response response = httpClient.newCall(request).execute()) { + for (int attempt = 1; attempt <= maxAttempts; attempt++) { + long start = System.currentTimeMillis(); - long duration = System.currentTimeMillis() - start; - log.info("Yandex response code={}, duration={}ms", response.code(), duration); + try (Response response = httpClient.newCall(request).execute()) { + long duration = System.currentTimeMillis() - start; + log.info("Yandex response code={}, duration={}ms, attempt={}", + response.code(), duration, attempt); - if (!response.isSuccessful()) { - throw new IOException("API error: " + response.code()); + if (!response.isSuccessful()) { + throw new IOException("API error: " + response.code()); + } + + ResponseBody body = response.body(); + if (body == null) { + throw new IOException("Empty response body"); + } + + return body.string(); + + } catch (IOException e) { + lastException = e; + log.warn("Attempt {} failed for image: {}, error: {}", + attempt, imageUrl, e.getMessage()); + + if (attempt == maxAttempts) { + log.error("All {} attempts failed for image: {}", maxAttempts, imageUrl); + throw lastException; + } + + try { + Thread.sleep(500L * attempt); // 500ms, 1000ms, 1500ms + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new IOException("Retry interrupted", ie); + } } - - ResponseBody body = response.body(); - if (body == null) { - throw new IOException("Empty response body"); - } - - return body.string(); } + + throw lastException; } + +// 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") +// .newBuilder() +// .addQueryParameter("engine", engine) +// .addQueryParameter("api_key", searchApiKey) +// .addQueryParameter("url", imageUrl) +// .addQueryParameter("search_type", searchType) +// .build(); +// +// Request request = new Request.Builder() +// .url(url) +// .header("Accept", "application/json") +// .header("User-Agent", "Mozilla/5.0") +// .build(); +// +// long start = System.currentTimeMillis(); +// +// try (Response response = httpClient.newCall(request).execute()) { +// +// long duration = System.currentTimeMillis() - start; +// log.info("Yandex response code={}, duration={}ms", response.code(), duration); +// +// if (!response.isSuccessful()) { +// throw new IOException("API error: " + response.code()); +// } +// +// ResponseBody body = response.body(); +// if (body == null) { +// throw new IOException("Empty response body"); +// } +// +// return body.string(); +// } +// } + public List getAllImagesWithoutPagination(String searchApiJson, String findType) throws IOException { JsonNode root = objectMapper.readTree(searchApiJson); From 3e39a3bf47120af7306efbdaee1c3014a69052ef Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 21 Feb 2026 23:58:24 +0700 Subject: [PATCH 074/274] dev comment google search --- .../handler/ImageFoundRequestHandler.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 1e12bed..50fd35e 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -61,7 +61,7 @@ public class ImageFoundRequestHandler implements RequestHandler { //TODO отдавать любой полученный результат,даже если по таймауту падают String searchResponseYandex = null; - String searchResponseGoogle = null; +// String searchResponseGoogle = null; List allYandexImages = new ArrayList<>(); List allGoogleImages = new ArrayList<>(); List allUniqueImages; @@ -80,18 +80,18 @@ public class ImageFoundRequestHandler implements RequestHandler { log.error("Yandex search failed for file {}", fileId, e); } - try { - searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", - "exact_matches"); - log.info("searchResponseGoogle OK"); - allGoogleImages = searchImageService.getAllImagesWithoutPagination( - searchResponseGoogle, "exact_matches"); - } catch (TimeoutException e) { - log.warn("Google search timeout for file {}", fileId); - hasTimeout = true; - } catch (IOException e) { - log.error("Google search failed for file {}", fileId, e); - } +// try { +// searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", +// "exact_matches"); +// log.info("searchResponseGoogle OK"); +// allGoogleImages = searchImageService.getAllImagesWithoutPagination( +// searchResponseGoogle, "exact_matches"); +// } catch (TimeoutException e) { +// log.warn("Google search timeout for file {}", fileId); +// hasTimeout = true; +// } catch (IOException e) { +// log.error("Google search failed for file {}", fileId, e); +// } allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); log.info("allUniqueImages OK: {} images", allUniqueImages.size()); From f926de161dae8f5b41452c2712108fc7b5c15816 Mon Sep 17 00:00:00 2001 From: vladp Date: Sun, 22 Feb 2026 00:08:05 +0700 Subject: [PATCH 075/274] dev comment google search --- src/main/resources/application.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index d2fb1bb..c75c810 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -104,7 +104,7 @@ yandex: search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image} searchapi: - api-key: ${SEARCHAPI_API_KEY:Hz3J2xCCU1qhaK4TdZ6dMyNM} + api-key: ${SEARCHAPI_API_KEY:M3vjBpaKatdyrYmBdVaAnDmg} reverse-image-url: "https://searchapi.io/api/v1/search" app: From 5d02e2e0f243f6b67917fb2d2749c88f069d97c4 Mon Sep 17 00:00:00 2001 From: vladp Date: Sun, 22 Feb 2026 00:25:05 +0700 Subject: [PATCH 076/274] dev comment google search --- .../ru/soune/nocopy/service/search/SearchImageService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index d38b935..bfa4355 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -36,6 +36,8 @@ public class SearchImageService { .writeTimeout(18, TimeUnit.SECONDS) .readTimeout(18, TimeUnit.SECONDS) .callTimeout(18, TimeUnit.SECONDS) + .followRedirects(true) + .followSslRedirects(true) .retryOnConnectionFailure(true) .build(); @@ -100,7 +102,7 @@ public class SearchImageService { throw new IllegalStateException("SearchAPI key not configured"); } - HttpUrl url = HttpUrl.parse("https://searchapi.io/api/v1/search") + HttpUrl url = HttpUrl.parse("https://www.searchapi.io/api/v1/search") .newBuilder() .addQueryParameter("engine", engine) .addQueryParameter("api_key", searchApiKey) From 7ae1c817ad523070b56b22567d31694db88655e2 Mon Sep 17 00:00:00 2001 From: vladp Date: Sun, 22 Feb 2026 00:42:13 +0700 Subject: [PATCH 077/274] dev comment google search --- .../handler/ImageFoundRequestHandler.java | 26 +++++++++---------- .../service/search/SearchImageService.java | 3 ++- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 50fd35e..1e12bed 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -61,7 +61,7 @@ public class ImageFoundRequestHandler implements RequestHandler { //TODO отдавать любой полученный результат,даже если по таймауту падают String searchResponseYandex = null; -// String searchResponseGoogle = null; + String searchResponseGoogle = null; List allYandexImages = new ArrayList<>(); List allGoogleImages = new ArrayList<>(); List allUniqueImages; @@ -80,18 +80,18 @@ public class ImageFoundRequestHandler implements RequestHandler { log.error("Yandex search failed for file {}", fileId, e); } -// try { -// searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", -// "exact_matches"); -// log.info("searchResponseGoogle OK"); -// allGoogleImages = searchImageService.getAllImagesWithoutPagination( -// searchResponseGoogle, "exact_matches"); -// } catch (TimeoutException e) { -// log.warn("Google search timeout for file {}", fileId); -// hasTimeout = true; -// } catch (IOException e) { -// log.error("Google search failed for file {}", fileId, e); -// } + try { + searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", + "exact_matches"); + log.info("searchResponseGoogle OK"); + allGoogleImages = searchImageService.getAllImagesWithoutPagination( + searchResponseGoogle, "exact_matches"); + } catch (TimeoutException e) { + log.warn("Google search timeout for file {}", fileId); + hasTimeout = true; + } catch (IOException e) { + log.error("Google search failed for file {}", fileId, e); + } allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); log.info("allUniqueImages OK: {} images", allUniqueImages.size()); diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index bfa4355..eafcc1e 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -39,6 +39,7 @@ public class SearchImageService { .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) + .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) .build(); this.objectMapper = new ObjectMapper(); @@ -149,7 +150,7 @@ public class SearchImageService { } try { - Thread.sleep(500L * attempt); // 500ms, 1000ms, 1500ms + Thread.sleep(500L * attempt); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new IOException("Retry interrupted", ie); From 247e97cd7a57fcf551edd6d3b28f112d5451979a Mon Sep 17 00:00:00 2001 From: vladp Date: Sun, 22 Feb 2026 01:03:22 +0700 Subject: [PATCH 078/274] dev add another client for google and yandex --- .../service/search/SearchImageService.java | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index eafcc1e..b263bd0 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -28,10 +28,38 @@ public class SearchImageService { private final ObjectMapper objectMapper; - private final OkHttpClient httpClient; +// private final OkHttpClient httpClient; + private final OkHttpClient yandexHttpClient; + private final OkHttpClient googleHttpClient; + +// public SearchImageService() { +// this.httpClient = new OkHttpClient.Builder() +// .connectTimeout(18, TimeUnit.SECONDS) +// .writeTimeout(18, TimeUnit.SECONDS) +// .readTimeout(18, TimeUnit.SECONDS) +// .callTimeout(18, TimeUnit.SECONDS) +// .followRedirects(true) +// .followSslRedirects(true) +// .retryOnConnectionFailure(true) +// .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) +// .build(); +// +// this.objectMapper = new ObjectMapper(); +// } public SearchImageService() { - this.httpClient = new OkHttpClient.Builder() + this.yandexHttpClient = new OkHttpClient.Builder() + .connectTimeout(18, TimeUnit.SECONDS) + .writeTimeout(18, TimeUnit.SECONDS) + .readTimeout(18, TimeUnit.SECONDS) + .callTimeout(18, TimeUnit.SECONDS) + .followRedirects(true) + .followSslRedirects(true) + .retryOnConnectionFailure(true) + .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) + .build(); + + this.googleHttpClient = new OkHttpClient.Builder() .connectTimeout(18, TimeUnit.SECONDS) .writeTimeout(18, TimeUnit.SECONDS) .readTimeout(18, TimeUnit.SECONDS) @@ -103,6 +131,8 @@ public class SearchImageService { throw new IllegalStateException("SearchAPI key not configured"); } + OkHttpClient client = engine.contains("yandex_reverse_image") ? yandexHttpClient : googleHttpClient; + HttpUrl url = HttpUrl.parse("https://www.searchapi.io/api/v1/search") .newBuilder() .addQueryParameter("engine", engine) @@ -123,7 +153,7 @@ public class SearchImageService { for (int attempt = 1; attempt <= maxAttempts; attempt++) { long start = System.currentTimeMillis(); - try (Response response = httpClient.newCall(request).execute()) { + try (Response response = client.newCall(request).execute()) { long duration = System.currentTimeMillis() - start; log.info("Yandex response code={}, duration={}ms, attempt={}", response.code(), duration, attempt); From 47f3cda7b0220703d88439792ad19323485a1ee8 Mon Sep 17 00:00:00 2001 From: vladp Date: Sun, 22 Feb 2026 01:21:09 +0700 Subject: [PATCH 079/274] dev add another client for google and yandex --- .../java/ru/soune/nocopy/service/search/SearchImageService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index b263bd0..b90a79b 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -57,6 +57,7 @@ public class SearchImageService { .followSslRedirects(true) .retryOnConnectionFailure(true) .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) + .build(); this.googleHttpClient = new OkHttpClient.Builder() @@ -139,6 +140,7 @@ public class SearchImageService { .addQueryParameter("api_key", searchApiKey) .addQueryParameter("url", imageUrl) .addQueryParameter("search_type", searchType) + .addQueryParameter("t_", String.valueOf(System.currentTimeMillis())) .build(); Request request = new Request.Builder() From ab495eada74fc7f18887d1f609519bcaef291061 Mon Sep 17 00:00:00 2001 From: vladp Date: Sun, 22 Feb 2026 01:48:49 +0700 Subject: [PATCH 080/274] dev add another client for google and yandex --- .../soune/nocopy/service/file/impl/FileUploadServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index 98cc244..6d3131e 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -322,7 +322,7 @@ public class FileUploadServiceImpl implements FileUploadService { File file = new File(finalFilePath); - if (session.getFileType().startsWith("audio") && !isValidWav(file)) { + if (session.getFileType().startsWith("audio") && !isWavFile(file)) { session.setStatus(UploadStatus.FAILED); session.setLastError("wav file not have RIFF header"); From a5c8d09fc4178e39c84e4af8c5a3f31a914ac72a Mon Sep 17 00:00:00 2001 From: vladp Date: Sun, 22 Feb 2026 02:18:23 +0700 Subject: [PATCH 081/274] dev add another client for google and yandex --- src/main/resources/application.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index c75c810..babac22 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -104,7 +104,7 @@ yandex: search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image} searchapi: - api-key: ${SEARCHAPI_API_KEY:M3vjBpaKatdyrYmBdVaAnDmg} + api-key: ${SEARCHAPI_API_KEY:7pu81qZdoih2JaTBDaoNfrRS} reverse-image-url: "https://searchapi.io/api/v1/search" app: From b3479587726cce417eb02231c6b9e1d1498a52cc Mon Sep 17 00:00:00 2001 From: vladp Date: Sun, 22 Feb 2026 02:40:28 +0700 Subject: [PATCH 082/274] dev add another client for google and yandex --- .../service/search/SearchImageService.java | 176 +++++++++--------- 1 file changed, 89 insertions(+), 87 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index b90a79b..c96b9c0 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -49,10 +49,10 @@ public class SearchImageService { public SearchImageService() { this.yandexHttpClient = new OkHttpClient.Builder() - .connectTimeout(18, TimeUnit.SECONDS) - .writeTimeout(18, TimeUnit.SECONDS) - .readTimeout(18, TimeUnit.SECONDS) - .callTimeout(18, TimeUnit.SECONDS) + .connectTimeout(15, TimeUnit.SECONDS) + .writeTimeout(15, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) + .callTimeout(15, TimeUnit.SECONDS) .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) @@ -61,10 +61,10 @@ public class SearchImageService { .build(); this.googleHttpClient = new OkHttpClient.Builder() - .connectTimeout(18, TimeUnit.SECONDS) - .writeTimeout(18, TimeUnit.SECONDS) - .readTimeout(18, TimeUnit.SECONDS) - .callTimeout(18, TimeUnit.SECONDS) + .connectTimeout(15, TimeUnit.SECONDS) + .writeTimeout(15, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) + .callTimeout(15, TimeUnit.SECONDS) .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) @@ -127,6 +127,72 @@ public class SearchImageService { return mimeType != null && mimeType.startsWith("image"); } +// private String callReverseImageApiByUrl(String imageUrl, String engine, String searchType) throws IOException { +// if (searchApiKey == null || searchApiKey.isBlank()) { +// throw new IllegalStateException("SearchAPI key not configured"); +// } +// +// OkHttpClient client = engine.contains("yandex_reverse_image") ? yandexHttpClient : googleHttpClient; +// +// 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("t_", String.valueOf(System.currentTimeMillis())) +// .build(); +// +// Request request = new Request.Builder() +// .url(url) +// .header("Accept", "application/json") +// .header("User-Agent", "Mozilla/5.0") +// .build(); +// +// int maxAttempts = 2; +// IOException lastException = null; +// +// for (int attempt = 1; attempt <= maxAttempts; attempt++) { +// long start = System.currentTimeMillis(); +// +// try (Response response = client.newCall(request).execute()) { +// long duration = System.currentTimeMillis() - start; +// log.info("Yandex response code={}, duration={}ms, attempt={}", +// response.code(), duration, attempt); +// +// if (!response.isSuccessful()) { +// throw new IOException("API error: " + response.code()); +// } +// +// ResponseBody body = response.body(); +// if (body == null) { +// throw new IOException("Empty response body"); +// } +// +// return body.string(); +// +// } catch (IOException e) { +// lastException = e; +// log.warn("Attempt {} failed for image: {}, error: {}", +// attempt, imageUrl, e.getMessage()); +// +// if (attempt == maxAttempts) { +// log.error("All {} attempts failed for image: {}", maxAttempts, imageUrl); +// throw lastException; +// } +// +// try { +// Thread.sleep(500L * attempt); +// } catch (InterruptedException ie) { +// Thread.currentThread().interrupt(); +// throw new IOException("Retry interrupted", ie); +// } +// } +// } +// +// throw lastException; +// } + private String callReverseImageApiByUrl(String imageUrl, String engine, String searchType) throws IOException { if (searchApiKey == null || searchApiKey.isBlank()) { throw new IllegalStateException("SearchAPI key not configured"); @@ -149,91 +215,27 @@ public class SearchImageService { .header("User-Agent", "Mozilla/5.0") .build(); - int maxAttempts = 3; - IOException lastException = null; + long start = System.currentTimeMillis(); - for (int attempt = 1; attempt <= maxAttempts; attempt++) { - long start = System.currentTimeMillis(); + try (Response response = client.newCall(request).execute()) { + long duration = System.currentTimeMillis() - start; + log.info("SearchAPI response code={}, duration={}ms, engine={}", + response.code(), duration, engine); - try (Response response = client.newCall(request).execute()) { - long duration = System.currentTimeMillis() - start; - log.info("Yandex response code={}, duration={}ms, attempt={}", - response.code(), duration, attempt); - - if (!response.isSuccessful()) { - throw new IOException("API error: " + response.code()); - } - - ResponseBody body = response.body(); - if (body == null) { - throw new IOException("Empty response body"); - } - - return body.string(); - - } catch (IOException e) { - lastException = e; - log.warn("Attempt {} failed for image: {}, error: {}", - attempt, imageUrl, e.getMessage()); - - if (attempt == maxAttempts) { - log.error("All {} attempts failed for image: {}", maxAttempts, imageUrl); - throw lastException; - } - - try { - Thread.sleep(500L * attempt); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - throw new IOException("Retry interrupted", ie); - } + if (!response.isSuccessful()) { + 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"); + } + + return body.string(); } - - throw lastException; } - -// 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") -// .newBuilder() -// .addQueryParameter("engine", engine) -// .addQueryParameter("api_key", searchApiKey) -// .addQueryParameter("url", imageUrl) -// .addQueryParameter("search_type", searchType) -// .build(); -// -// Request request = new Request.Builder() -// .url(url) -// .header("Accept", "application/json") -// .header("User-Agent", "Mozilla/5.0") -// .build(); -// -// long start = System.currentTimeMillis(); -// -// try (Response response = httpClient.newCall(request).execute()) { -// -// long duration = System.currentTimeMillis() - start; -// log.info("Yandex response code={}, duration={}ms", response.code(), duration); -// -// if (!response.isSuccessful()) { -// throw new IOException("API error: " + response.code()); -// } -// -// ResponseBody body = response.body(); -// if (body == null) { -// throw new IOException("Empty response body"); -// } -// -// return body.string(); -// } -// } - public List getAllImagesWithoutPagination(String searchApiJson, String findType) throws IOException { JsonNode root = objectMapper.readTree(searchApiJson); From 69be949f22a09bd44b3a0aaa3a1c320c6b1a53ff Mon Sep 17 00:00:00 2001 From: vladp Date: Sun, 22 Feb 2026 02:49:21 +0700 Subject: [PATCH 083/274] dev add another client for google and yandex --- .../handler/ImageFoundRequestHandler.java | 26 +++++++++---------- .../service/search/SearchImageService.java | 8 +++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 1e12bed..7c29743 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -67,19 +67,6 @@ public class ImageFoundRequestHandler implements RequestHandler { List allUniqueImages; boolean hasTimeout = false; - try { - searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", - "visual_matches"); - log.info("searchResponseYandex OK"); - allYandexImages = searchImageService.getAllImagesWithoutPagination( - searchResponseYandex, "visual_matches"); - } catch (TimeoutException e) { - log.warn("Yandex search timeout for file {}", fileId); - hasTimeout = true; - } catch (IOException e) { - log.error("Yandex search failed for file {}", fileId, e); - } - try { searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", "exact_matches"); @@ -93,6 +80,19 @@ public class ImageFoundRequestHandler implements RequestHandler { log.error("Google search failed for file {}", fileId, e); } + try { + searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", + "visual_matches"); + log.info("searchResponseYandex OK"); + allYandexImages = searchImageService.getAllImagesWithoutPagination( + searchResponseYandex, "visual_matches"); + } catch (TimeoutException e) { + log.warn("Yandex search timeout for file {}", fileId); + hasTimeout = true; + } catch (IOException e) { + log.error("Yandex search failed for file {}", fileId, e); + } + allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); log.info("allUniqueImages OK: {} images", allUniqueImages.size()); diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index c96b9c0..3ee4151 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -49,10 +49,10 @@ public class SearchImageService { public SearchImageService() { this.yandexHttpClient = new OkHttpClient.Builder() - .connectTimeout(15, TimeUnit.SECONDS) - .writeTimeout(15, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .callTimeout(15, TimeUnit.SECONDS) + .connectTimeout(25, TimeUnit.SECONDS) + .writeTimeout(25, TimeUnit.SECONDS) + .readTimeout(25, TimeUnit.SECONDS) + .callTimeout(25, TimeUnit.SECONDS) .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) From abe98fb6094039d42c69c63230c318f85b058cf1 Mon Sep 17 00:00:00 2001 From: vladp Date: Sun, 22 Feb 2026 02:54:39 +0700 Subject: [PATCH 084/274] dev add another client for google and yandex --- .../soune/nocopy/service/search/SearchImageService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 3ee4151..1a35f71 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -49,10 +49,10 @@ public class SearchImageService { public SearchImageService() { this.yandexHttpClient = new OkHttpClient.Builder() - .connectTimeout(25, TimeUnit.SECONDS) - .writeTimeout(25, TimeUnit.SECONDS) - .readTimeout(25, TimeUnit.SECONDS) - .callTimeout(25, TimeUnit.SECONDS) + .connectTimeout(35, TimeUnit.SECONDS) + .writeTimeout(35, TimeUnit.SECONDS) + .readTimeout(35, TimeUnit.SECONDS) + .callTimeout(35, TimeUnit.SECONDS) .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) From 5460d91a6f5032521ffc78f54e6cdb9235a7f182 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 24 Feb 2026 11:27:56 +0700 Subject: [PATCH 085/274] dev use authtoken --- .../nocopy/controller/PayoutController.java | 114 ++++++++++++++---- 1 file changed, 91 insertions(+), 23 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/PayoutController.java b/src/main/java/ru/soune/nocopy/controller/PayoutController.java index bad323d..2e7ec24 100644 --- a/src/main/java/ru/soune/nocopy/controller/PayoutController.java +++ b/src/main/java/ru/soune/nocopy/controller/PayoutController.java @@ -12,11 +12,16 @@ import ru.soune.nocopy.entity.payout.BankTransferPayoutMethod; import ru.soune.nocopy.entity.payout.CardPayoutMethod; import ru.soune.nocopy.entity.payout.PayoutRequest; import ru.soune.nocopy.entity.payout.PayoutType; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.exception.NotFoundAuthToken; +import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.service.payout.PayoutMethodService; import ru.soune.nocopy.service.payout.PayoutRequestService; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; @RestController @@ -29,6 +34,8 @@ public class PayoutController { private final PayoutMethodService payoutMethodService; + private final AuthTokenRepository authTokenRepository; + /* Create payout */ @@ -53,9 +60,18 @@ public class PayoutController { /* Cancel payout */ - @PostMapping("/requests/{requestId}/{userId}/cancel") - public ResponseEntity cancelPayoutRequest(@PathVariable Long userId, @PathVariable Long requestId) { - PayoutRequest cancelled = payoutRequestService.cancelPayoutRequest(userId, requestId); + @PostMapping("/requests/{requestId}/cancel") + public ResponseEntity cancelPayoutRequest(@PathVariable Long requestId, + @RequestHeader("Authorization") String tokenHeader) { + String token = tokenHeader.replace("Bearer ", ""); + + Optional tokenOptional = authTokenRepository.findByToken(token); + + AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); + + User user = authToken.getUser(); + + PayoutRequest cancelled = payoutRequestService.cancelPayoutRequest(user.getId(), requestId); return ResponseEntity.ok(cancelled); } @@ -63,25 +79,50 @@ public class PayoutController { /* Get user payouts */ - @GetMapping("/requests/{userId}") - public ResponseEntity> getUserRequests(@PathVariable Long userId) { - return ResponseEntity.ok(payoutRequestService.getUserPayoutRequests(userId)); + @GetMapping("/requests") + public ResponseEntity> getUserRequests(@RequestHeader("Authorization") String tokenHeader) { + String token = tokenHeader.replace("Bearer ", ""); + + Optional tokenOptional = authTokenRepository.findByToken(token); + + AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); + + User user = authToken.getUser(); + + return ResponseEntity.ok(payoutRequestService.getUserPayoutRequests(user.getId())); } /* Get payout by id */ - @GetMapping("/requests/{requestId}/{userId}") - public ResponseEntity getPayoutRequest(@PathVariable Long userId, @PathVariable Long requestId) { - return ResponseEntity.ok(payoutRequestService.getPayoutRequest(userId, requestId)); + @GetMapping("/requests/{requestId}") + public ResponseEntity getPayoutRequest(@PathVariable Long requestId, + @RequestHeader("Authorization") String tokenHeader) { + String token = tokenHeader.replace("Bearer ", ""); + + Optional tokenOptional = authTokenRepository.findByToken(token); + + AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); + + User user = authToken.getUser(); + + return ResponseEntity.ok(payoutRequestService.getPayoutRequest(user.getId(), requestId)); } /* Get payout by id */ - @GetMapping("/payout-method/{userId}") - public ResponseEntity> getUserMethods(@PathVariable Long userId) { - return ResponseEntity.ok(payoutMethodService.getUserMethods(userId)); + @GetMapping("/payout-method") + public ResponseEntity> getUserMethods(@RequestHeader("Authorization") String tokenHeader) { + String token = tokenHeader.replace("Bearer ", ""); + + Optional tokenOptional = authTokenRepository.findByToken(token); + + AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); + + User user = authToken.getUser(); + + return ResponseEntity.ok(payoutMethodService.getUserMethods(user.getId())); } /* @@ -101,10 +142,19 @@ public class PayoutController { /* add card method by userId */ - @PostMapping("/payout-method/card/{userId}") - public ResponseEntity addCardMethod(@PathVariable Long userId, - @Valid @RequestBody AddCardRequest request) { - CardPayoutMethod method = payoutMethodService.addCardMethod(userId, request); + @PostMapping("/payout-method/card") + public ResponseEntity addCardMethod(@Valid @RequestBody AddCardRequest request, + @RequestHeader("Authorization") String tokenHeader) { + String token = tokenHeader.replace("Bearer ", ""); + + Optional tokenOptional = authTokenRepository.findByToken(token); + + AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); + + User user = authToken.getUser(); + + CardPayoutMethod method = payoutMethodService.addCardMethod(user.getId(), request); + return ResponseEntity.status(HttpStatus.CREATED) .body(payoutMethodService.convertToDto(method)); } @@ -112,10 +162,19 @@ public class PayoutController { /* add bank payout-method */ - @PostMapping("/payout-method/bank-transfer/{userId}") - public ResponseEntity addBankTransferMethod(@PathVariable Long userId, - @Valid @RequestBody AddBankTransferRequest request) { - BankTransferPayoutMethod method = payoutMethodService.addBankTransferMethod(userId, request); + @PostMapping("/payout-method/bank-transfer") + public ResponseEntity addBankTransferMethod(@Valid @RequestBody AddBankTransferRequest request, + @RequestHeader("Authorization") String tokenHeader) { + String token = tokenHeader.replace("Bearer ", ""); + + Optional tokenOptional = authTokenRepository.findByToken(token); + + AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); + + User user = authToken.getUser(); + + BankTransferPayoutMethod method = payoutMethodService.addBankTransferMethod(user.getId(), request); + return ResponseEntity.status(HttpStatus.CREATED) .body(payoutMethodService.convertToDto(method)); } @@ -124,10 +183,19 @@ public class PayoutController { /* set default bank payout-method */ - @PutMapping("/payout-method/{userId}/{methodId}/default") - public ResponseEntity setDefaultMethod(@PathVariable Long userId, @PathVariable Long methodId) { + @PutMapping("/payout-method/{methodId}/default") + public ResponseEntity setDefaultMethod(@PathVariable Long methodId, + @RequestHeader("Authorization") String tokenHeader) { + String token = tokenHeader.replace("Bearer ", ""); + + Optional tokenOptional = authTokenRepository.findByToken(token); + + AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); + + User user = authToken.getUser(); + + payoutMethodService.setDefaultMethod(user.getId(), methodId); - payoutMethodService.setDefaultMethod(userId, methodId); return ResponseEntity.ok().build(); } } From c81a8c8d65308692a99b81a3e767261af3e3759c Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 24 Feb 2026 11:41:02 +0700 Subject: [PATCH 086/274] dev use authtoken --- .../java/ru/soune/nocopy/controller/PayoutController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/PayoutController.java b/src/main/java/ru/soune/nocopy/controller/PayoutController.java index 2e7ec24..c8d5887 100644 --- a/src/main/java/ru/soune/nocopy/controller/PayoutController.java +++ b/src/main/java/ru/soune/nocopy/controller/PayoutController.java @@ -112,7 +112,7 @@ public class PayoutController { /* Get payout by id */ - @GetMapping("/payout-method") + @GetMapping("user/payout-methods") public ResponseEntity> getUserMethods(@RequestHeader("Authorization") String tokenHeader) { String token = tokenHeader.replace("Bearer ", ""); @@ -128,7 +128,7 @@ public class PayoutController { /* Get payout-methods */ - @GetMapping("/payout-method") + @GetMapping("/payout-methods") public ResponseEntity> getAvailableMethodTypes() { List types = Arrays.stream(PayoutType.values()) .map(type -> PayoutMethodTypeDTO.builder() From 81342ae05e6d426550f95b0c252f9ad67b26c5ff Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 24 Feb 2026 12:57:48 +0700 Subject: [PATCH 087/274] dev use authtoken --- .../soune/nocopy/controller/PayoutController.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/PayoutController.java b/src/main/java/ru/soune/nocopy/controller/PayoutController.java index c8d5887..ba8b827 100644 --- a/src/main/java/ru/soune/nocopy/controller/PayoutController.java +++ b/src/main/java/ru/soune/nocopy/controller/PayoutController.java @@ -40,9 +40,17 @@ public class PayoutController { Create payout */ @PostMapping("/create-request") - public ResponseEntity createPayoutRequest(@RequestParam Long userId, - @Valid @RequestBody PayoutRequestDTO request) { - PayoutRequest payoutRequest = payoutRequestService.createPayoutRequest(userId, request); + public ResponseEntity createPayoutRequest(@RequestHeader("Authorization") String tokenHeader, + @Valid @RequestBody PayoutRequestDTO request) { + String token = tokenHeader.replace("Bearer ", ""); + + Optional tokenOptional = authTokenRepository.findByToken(token); + + AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); + + User user = authToken.getUser(); + + PayoutRequest payoutRequest = payoutRequestService.createPayoutRequest(user.getId(), request); return ResponseEntity.status(HttpStatus.CREATED).body(payoutRequest); } From c5edc9a5fc8e5f74336d6c48055181af6bf5f1dc Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 24 Feb 2026 14:19:15 +0700 Subject: [PATCH 088/274] dev use authtoken --- .../nocopy/controller/ApiController.java | 19 ++++++++++++++++++- .../nocopy/controller/FileController.java | 1 - 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 33c4976..5aeb8c4 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -23,14 +23,17 @@ import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.file.*; import ru.soune.nocopy.dto.register.RegAnswer; +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.UploadStatus; import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.*; import ru.soune.nocopy.handler.*; import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.FileSimilarityService; import ru.soune.nocopy.service.file.ProtectionsLimitService; import ru.soune.nocopy.service.register.AuthService; @@ -69,6 +72,8 @@ public class ApiController { private final AuthTokenRepository authTokenRepository; + private final UserRepository userRepository; + @PostMapping("/v{version}/data") public ResponseEntity handlePostRequest(@RequestBody BaseRequest request, @PathVariable("version") int version) { @@ -365,7 +370,19 @@ public class ApiController { Long userId = authService.useUserAuthToken(tokenHeader); FileEntityResponse entityResponse = fileEntityService.getById(fileId, version); - if (!entityResponse.getUserId().equals(userId)) { + User fileUser = userRepository.findById(entityResponse.getUserId()).orElseThrow(); + User user = userRepository.findById(userId).orElseThrow(); + Company companyUser = user.getCompany(); + + if (companyUser != null && !fileUser.getCompany().getId().equals(companyUser.getId())) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(new BaseResponse(20004, + MessageCode.USER_NOT_HAD_PERMISSION.getCode(), + MessageCode.USER_NOT_HAD_PERMISSION.getDescription(), + errorData)); + } else if (!entityResponse.getUserId().equals(userId)) { Map errorData = new HashMap<>(); errorData.put("token", tokenHeader); diff --git a/src/main/java/ru/soune/nocopy/controller/FileController.java b/src/main/java/ru/soune/nocopy/controller/FileController.java index 3ffd0cd..f814982 100644 --- a/src/main/java/ru/soune/nocopy/controller/FileController.java +++ b/src/main/java/ru/soune/nocopy/controller/FileController.java @@ -100,7 +100,6 @@ public class FileController { try { return MediaType.parseMediaType(fileEntity.getMimeType()); } catch (InvalidMediaTypeException e) { - // Если MIME тип некорректен, определяем по расширению String extension = fileEntity.getFileExtension().toLowerCase(); switch (extension) { case "jpg": From 274861c300612329494055c5ae0c3f770c10da94 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 24 Feb 2026 14:24:42 +0700 Subject: [PATCH 089/274] dev use authtoken --- src/main/java/ru/soune/nocopy/controller/ApiController.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 5aeb8c4..2244b3b 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -382,7 +382,9 @@ public class ApiController { MessageCode.USER_NOT_HAD_PERMISSION.getCode(), MessageCode.USER_NOT_HAD_PERMISSION.getDescription(), errorData)); - } else if (!entityResponse.getUserId().equals(userId)) { + } + + if (companyUser == null && !entityResponse.getUserId().equals(userId)) { Map errorData = new HashMap<>(); errorData.put("token", tokenHeader); From 28ce99ec1c8c99629cfd058a8fcc26c739a73dc2 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 24 Feb 2026 14:56:41 +0700 Subject: [PATCH 090/274] dev use authtoken --- .../handler/ImageFoundRequestHandler.java | 85 +------------------ .../repository/ImageSimilarityRepository.java | 26 ++++++ .../nocopy/service/FileSimilarityService.java | 22 ++--- 3 files changed, 40 insertions(+), 93 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 7c29743..bf0194b 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -54,14 +54,8 @@ public class ImageFoundRequestHandler implements RequestHandler { Map.of("fileId",fileId))); }); -// YandexSearchResponse response = yandexSearchService.searchByFileEntity(fileId); - - //TODO uncommited when add billing -// GoogleVisionSearchResponse googleVisionSearchResponse = googleVisionSearchService.searchByFileEntity(fileId); - //TODO отдавать любой полученный результат,даже если по таймауту падают - - String searchResponseYandex = null; - String searchResponseGoogle = null; + String searchResponseYandex; + String searchResponseGoogle; List allYandexImages = new ArrayList<>(); List allGoogleImages = new ArrayList<>(); List allUniqueImages; @@ -119,79 +113,4 @@ public class ImageFoundRequestHandler implements RequestHandler { return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), message, finalResponse); } - -// @Override -// public BaseResponse handle(BaseRequest request) throws Exception { -// ImageSearchRequest imageSearchRequest = objectMapper.convertValue(request.getMessageBody(), -// ImageSearchRequest.class); -// -// String fileId = imageSearchRequest.getFileId(); -// -// FileEntity fileEntity = fileEntityRepository.findById(fileId) -// .orElseThrow(() -> { -// throw new NotValidFieldException("File not found", new BaseResponse(20007, -// MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(), -// Map.of("fileId",fileId))); -// }); -// -//// YandexSearchResponse response = yandexSearchService.searchByFileEntity(fileEntity); -// -// //TODO uncommited when add billing -//// GoogleVisionSearchResponse googleVisionSearchResponse = googleVisionSearchService.searchByFileEntity(fileId); -// //TODO отдавать любой полученный результат,даже если по таймауту падают -// -// String searchResponseYandex; -// String searchResponseGoogle; -// List allUniqueImages; -// try { -// searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", -// "visual_matches"); -// log.info("searchResponseYandex OK"); -// List allYandexImages = searchImageService.getAllImagesWithoutPagination( -// searchResponseYandex, "visual_matches"); -// searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", -// "exact_matches"); -// -// log.info("searchResponseGoogle OK"); -// List allGoogleImages = searchImageService.getAllImagesWithoutPagination( -// searchResponseGoogle, "exact_matches"); -// allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, -// allGoogleImages); -// log.info("allUniqueImages OK"); -// -// } catch (TimeoutException e) { -// log.warn("Search timeout for file {}, returning empty results", fileId); -// return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), -// "Search completed (partial)", Map.of( -// "results", List.of(), -// "total", 0, -// "searchStatus", "timeout" -// )); -// } catch (IOException e) { -// log.error("Search failed for file {}", fileId, e); -// return new BaseResponse(request.getMsgId(), 20007, -// "Search service temporarily unavailable", -// Map.of("fileId", fileId)); -// } -// -// tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); -// -// checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); -// -// int page = imageSearchRequest.getPage() != null ? imageSearchRequest.getPage() : 1; -// int pageSize = 5; -// -// List pagedResults = searchImageService.paginateResults(allUniqueImages, page, -// pageSize * 2); -// -// YandexSearchResponse finalResponse = new YandexSearchResponse(); -// finalResponse.setImages(pagedResults); -// finalResponse.setPage(page); -// finalResponse.setPageSize(pageSize * 2); -// finalResponse.setTotalResults(allUniqueImages.size()); -// finalResponse.setTotalPages((int) Math.ceil((double) allUniqueImages.size() / (pageSize * 2))); -// -// return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), -// MessageCode.SUCCESS.getDescription(), finalResponse); -// } } diff --git a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java index 0ae7006..4e32614 100644 --- a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java @@ -7,6 +7,7 @@ import org.springframework.stereotype.Repository; import ru.soune.nocopy.entity.file.FileEntity; import java.util.List; +import java.util.Set; @Repository public interface ImageSimilarityRepository @@ -60,6 +61,31 @@ public interface ImageSimilarityRepository @Param("userId") Long userId ); + @Query(value = """ + SELECT + f.id AS id, + f.original_file_name AS originalFileName, + f.file_size AS fileSize, + f.user_id AS userId, + f.support_id AS supportId, + f.created_at AS uploadDate, + f.protection_status AS protectionStatus, + f.file_extension AS extension, + h.hash64_hi AS hash64Hi, + h.hash64_lo AS hash64Lo + FROM image_hashes ref + JOIN image_hashes h + ON ref.file_id <> h.file_id + JOIN file_entities f + ON f.id = h.file_id + WHERE ref.file_id = :fileId AND f.user_id IN :userIds + """, + nativeQuery = true) + List findCandidatesFromUserFiles( + @Param("fileId") String fileId, + @Param("userId") Set userIds + ); + @Query(value = """ SELECT diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index c261a20..5cb6d14 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -1,32 +1,25 @@ package ru.soune.nocopy.service; -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 com.vrt.fileprotection.image.phash.PHash; import com.vrt.fileprotection.image.phash.PerceptualHashHelper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.hibernate.validator.internal.util.stereotypes.Lazy; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; 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.ImageHashEntity; +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.FileInputStream; import java.io.InputStream; -import java.nio.file.Path; -import java.nio.file.Paths; import java.security.MessageDigest; import java.util.*; import java.util.stream.Collectors; @@ -122,6 +115,8 @@ public class FileSimilarityService { } } + private final UserRepository userRepository; + public Page findSimilarFiles( String fileId, List similarityLevels, @@ -137,7 +132,14 @@ public class FileSimilarityService { candidates = repository.findCandidates(fileId); } else { Long userId = authTokenRepository.findUserIdByToken(authToken); - candidates = repository.findCandidatesFromUserFiles(fileId, userId); + User user = userRepository.findById(userId).orElseThrow(() -> new RuntimeException("User not found")); + Company company = user.getCompany(); + if (company != null) { + Set userIds = company.getUsers().stream().map(User::getId).collect(Collectors.toSet()); + candidates = repository.findCandidatesFromUserFiles(fileId, userIds); + } else { + candidates = repository.findCandidatesFromUserFiles(fileId, userId); + } } List levels = (similarityLevels != null && !similarityLevels.isEmpty()) From 51d1d78f4054f0a356d8e1855f8ed9a1d013409f Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 24 Feb 2026 15:02:01 +0700 Subject: [PATCH 091/274] dev use authtoken --- .../ru/soune/nocopy/repository/ImageSimilarityRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java index 4e32614..075da11 100644 --- a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java @@ -83,7 +83,7 @@ public interface ImageSimilarityRepository nativeQuery = true) List findCandidatesFromUserFiles( @Param("fileId") String fileId, - @Param("userId") Set userIds + @Param("userIds") Set userIds ); From ba6464147a217d65f1d466bbe975d658668d5acc Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 24 Feb 2026 16:53:33 +0700 Subject: [PATCH 092/274] dev use authtoken --- src/main/java/ru/soune/nocopy/dto/MessageCode.java | 2 +- .../java/ru/soune/nocopy/handler/ResetPasswordHandler.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index 61b92aa..74d1df9 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -7,7 +7,7 @@ public enum MessageCode { REFERRAL_LINK_IS_NOT_EXIST(1, "Refferal link is not exist"), INVALID_FIELD(2, "Invalid field"), MAIL_VERIFIED_NULL(2, "Mail verified null"), - INVALID_TOKEN(2, "Invalid token"), + INVALID_TOKEN(2, "Token not found or time expired"), TOKEN_IS_ALIVE(2, "Token is alive"), INVALID_ACTION(2, "Invalid action"), FILE_UPLOAD_ERROR(2, "File upload error"), diff --git a/src/main/java/ru/soune/nocopy/handler/ResetPasswordHandler.java b/src/main/java/ru/soune/nocopy/handler/ResetPasswordHandler.java index 717416b..e51a806 100644 --- a/src/main/java/ru/soune/nocopy/handler/ResetPasswordHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ResetPasswordHandler.java @@ -74,10 +74,10 @@ public class ResetPasswordHandler implements RequestHandler{ emailVerificationTokenRepository.findByUserIdAndToken(user.getId(), verifyToken); if (verificationToken == null) { - throw new NotValidFieldException("Verification token not found", - new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(), - MessageCode.INVALID_TOKEN.getDescription(), Map.of("verifyToken", verifyToken))); + return new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(), + MessageCode.INVALID_TOKEN.getDescription(), Map.of("verifyToken", verifyToken)); } + List authTokens = authTokenRepository.findByUserId(user.getId()); authToken = authTokens.isEmpty() ? authService.generateAuthToken(user): authTokens.getFirst(); From 87b97c39eaf29eacf58355d71ceff766aea035a6 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 24 Feb 2026 18:35:35 +0700 Subject: [PATCH 093/274] dev use authtoken --- README.md | 20 +++++++++++++++++- .../soune/nocopy/entity/file/FileEntity.java | 12 +++++++++++ .../entity/file/SupportIdInitializer.java | 21 +++++++++++++++++++ .../repository/FileEntityRepository.java | 3 +++ 4 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ru/soune/nocopy/entity/file/SupportIdInitializer.java diff --git a/README.md b/README.md index 602cdd0..529ee1d 100644 --- a/README.md +++ b/README.md @@ -150,4 +150,22 @@ INSERT INTO referral_levels (id, name, min_invitees, reward_percentage) VALUES ('bronze', 'BRONZE', 0, 15), ('silver', 'SILVER', 6, 18), ('gold', 'GOLD', 16, 22), -('platinum', 'PLATINUM', 50, 25); \ No newline at end of file +('platinum', 'PLATINUM', 50, 25); + +----------------- +Скрипт,для нумерации для уже сохданных файлов +WITH max_id AS ( +SELECT COALESCE(MAX(support_id), 0) as max_val FROM file_entities +), +-- Нумеруем только NULL записи, начиная с max_val + 1 +numbered AS ( +SELECT +f.id, +(SELECT max_val FROM max_id) + ROW_NUMBER() OVER (ORDER BY f.created_at, f.id) as new_support_id +FROM file_entities f +WHERE f.support_id IS NULL +) +UPDATE file_entities +SET support_id = numbered.new_support_id +FROM numbered +WHERE file_entities.id = numbered.id; \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java b/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java index 2d8d781..a7b74b6 100644 --- a/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java +++ b/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java @@ -8,6 +8,7 @@ import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; +import java.util.concurrent.atomic.AtomicInteger; @Data @Builder @@ -17,6 +18,9 @@ import java.time.LocalDateTime; @Table(name = "file_entities") @EntityListeners(AuditingEntityListener.class) public class FileEntity { + + private static AtomicInteger supportIdCounter = new AtomicInteger(0); + @Id @GeneratedValue(strategy = GenerationType.UUID) private String id; @@ -83,6 +87,10 @@ public class FileEntity { @PrePersist public void prePersist() { + if (this.supportId == null) { + this.supportId = supportIdCounter.incrementAndGet(); + } + if (this.status == null) { this.status = FileStatus.ACTIVE; } @@ -101,4 +109,8 @@ public class FileEntity { public void preUpdate() { this.updatedAt = LocalDateTime.now(); } + + public static void initializeCounter(int maxId) { + supportIdCounter = new AtomicInteger(maxId); + } } diff --git a/src/main/java/ru/soune/nocopy/entity/file/SupportIdInitializer.java b/src/main/java/ru/soune/nocopy/entity/file/SupportIdInitializer.java new file mode 100644 index 0000000..8e8d8d7 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/file/SupportIdInitializer.java @@ -0,0 +1,21 @@ +package ru.soune.nocopy.entity.file; + +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.repository.FileEntityRepository; + +@Component +public class SupportIdInitializer { + + @Autowired + private FileEntityRepository fileRepository; + + @PostConstruct + public void init() { + Integer maxId = fileRepository.findMaxSupportId(); + if (maxId != null) { + FileEntity.initializeCounter(maxId); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java index c201496..4398a51 100644 --- a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java @@ -58,4 +58,7 @@ public interface FileEntityRepository extends JpaRepository @Query("SELECT f FROM FileEntity f WHERE f.mimeType = :mimeType") List findByMimeType(String mimeType); + + @Query("SELECT MAX(f.supportId) FROM FileEntity f") + Integer findMaxSupportId(); } From 0185e7cd54d7d730c45c06429049439eeffabfa1 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 24 Feb 2026 20:27:58 +0700 Subject: [PATCH 094/274] dev use authtoken --- .../service/payout/PayoutMethodService.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/payout/PayoutMethodService.java b/src/main/java/ru/soune/nocopy/service/payout/PayoutMethodService.java index 023f9b9..fce8c5e 100644 --- a/src/main/java/ru/soune/nocopy/service/payout/PayoutMethodService.java +++ b/src/main/java/ru/soune/nocopy/service/payout/PayoutMethodService.java @@ -7,10 +7,7 @@ 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.BankTransferPayoutMethod; -import ru.soune.nocopy.entity.payout.CardPayoutMethod; -import ru.soune.nocopy.entity.payout.InternalPayoutMethod; -import ru.soune.nocopy.entity.payout.PayoutMethod; +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; @@ -90,6 +87,16 @@ public class PayoutMethodService { @Transactional public CardPayoutMethod addCardMethod(Long userId, AddCardRequest request) { + List byUserId = payoutMethodRepository.findByUserId(userId); + + if (!byUserId.isEmpty()) { + for (PayoutMethod payoutMethod : byUserId) { + if (payoutMethod.getMethodType().equals(PayoutType.CARD.getDisplayName())) { + payoutMethodRepository.delete(payoutMethod); + } + } + } + String cardHash = generateCardHash(request.getCardNumber()); if (payoutMethodRepository.existsByCardHash(cardHash)) { From bfe2367715d87578afc8ddcee3536641469b3020 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 25 Feb 2026 11:31:08 +0700 Subject: [PATCH 095/274] dev use authtoken --- .../nocopy/handler/VerifyRegisterUserHandler.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java b/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java index 35efc32..5dac630 100644 --- a/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java @@ -69,13 +69,13 @@ public class VerifyRegisterUserHandler implements RequestHandler { EmailVerificationToken token = emailVerificationTokenRepository.findByUserId(user.getId()); - if (token != null && !token.isExpired()) { - emailService.sendVerificationEmail(user, token.getToken()); - - log.info("Verification token resent for user {}", user.getEmail()); - return successResponse(request, authToken, "Verification code has been resent", false, - false, user.getEmail()); - } +// if (token != null && !token.isExpired()) { +// emailService.sendVerificationEmail(user, token.getToken()); +// +// log.info("Verification token resent for user {}", user.getEmail()); +// return successResponse(request, authToken, "Verification code has been resent", false, +// false, user.getEmail()); +// } if (token != null) { emailVerificationTokenRepository.delete(token); From cb46ac6c8883c649cfaccb76cf03f4ddec8f64ac Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 25 Feb 2026 12:58:39 +0700 Subject: [PATCH 096/274] dev use authtoken --- .../java/ru/soune/nocopy/dto/payout/PayoutMethodDTO.java | 1 + .../soune/nocopy/service/payout/PayoutMethodService.java | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodDTO.java b/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodDTO.java index 8eadf89..51bde8c 100644 --- a/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodDTO.java +++ b/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodDTO.java @@ -15,6 +15,7 @@ public class PayoutMethodDTO { private String type; private String displayName; private String maskedDetails; + private String cardNumber; private boolean isDefault; private boolean isVerified; } diff --git a/src/main/java/ru/soune/nocopy/service/payout/PayoutMethodService.java b/src/main/java/ru/soune/nocopy/service/payout/PayoutMethodService.java index fce8c5e..6e2b0dd 100644 --- a/src/main/java/ru/soune/nocopy/service/payout/PayoutMethodService.java +++ b/src/main/java/ru/soune/nocopy/service/payout/PayoutMethodService.java @@ -129,13 +129,19 @@ public class PayoutMethodService { } public PayoutMethodDTO convertToDto(PayoutMethod method) { - return PayoutMethodDTO.builder() + 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) { From 694681fccc791d45a263f93973abde3fcb248a27 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 25 Feb 2026 13:06:27 +0700 Subject: [PATCH 097/274] dev use authtoken --- .../ru/soune/nocopy/service/payout/PayoutMethodService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/payout/PayoutMethodService.java b/src/main/java/ru/soune/nocopy/service/payout/PayoutMethodService.java index 6e2b0dd..60dfd25 100644 --- a/src/main/java/ru/soune/nocopy/service/payout/PayoutMethodService.java +++ b/src/main/java/ru/soune/nocopy/service/payout/PayoutMethodService.java @@ -91,7 +91,7 @@ public class PayoutMethodService { if (!byUserId.isEmpty()) { for (PayoutMethod payoutMethod : byUserId) { - if (payoutMethod.getMethodType().equals(PayoutType.CARD.getDisplayName())) { + if (payoutMethod.getMethodType().equals(PayoutType.CARD.getDisplayName().toUpperCase())) { payoutMethodRepository.delete(payoutMethod); } } From 3bb6667c2f46f888e5ee890e88c5edd985f4a16f Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 25 Feb 2026 19:29:19 +0700 Subject: [PATCH 098/274] dev conver files --- build.gradle | 2 +- ....3.0-all.jar => testlib-fat-0.3.1-all.jar} | Bin 6369314 -> 6369461 bytes .../configuration/file/NoCopyInitializer.java | 4 - .../nocopy/dto/file/FileUploadRequest.java | 3 + .../nocopy/entity/file/FileUploadSession.java | 3 + .../nocopy/handler/FileUploadHandler.java | 1 + .../FileUploadSessionRepository.java | 7 ++ .../file/FileProcessingOrchestrator.java | 55 ------------- .../service/file/FileUploadService.java | 2 +- .../file/impl/AudioLocalSearchImpl.java | 2 +- .../file/impl/DocumentLocalSearchImpl.java | 2 +- .../file/impl/FileUploadServiceImpl.java | 75 +----------------- .../file/impl/ImageLocalSearchImpl.java | 2 +- .../file/impl/ProtectionFileProviderImpl.java | 8 ++ .../java/ru/soune/nocopy/util/FileUtil.java | 6 +- 15 files changed, 33 insertions(+), 139 deletions(-) rename libs/{testlib-fat-0.3.0-all.jar => testlib-fat-0.3.1-all.jar} (95%) delete mode 100644 src/main/java/ru/soune/nocopy/service/file/FileProcessingOrchestrator.java diff --git a/build.gradle b/build.gradle index d8aaad8..0a81b99 100644 --- a/build.gradle +++ b/build.gradle @@ -59,7 +59,7 @@ dependencies { testImplementation 'org.mockito:mockito-core:5.3.1' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - implementation name: 'testlib-fat-0.3.0-all' + implementation name: 'testlib-fat-0.3.1-all' implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.12.0' diff --git a/libs/testlib-fat-0.3.0-all.jar b/libs/testlib-fat-0.3.1-all.jar similarity index 95% rename from libs/testlib-fat-0.3.0-all.jar rename to libs/testlib-fat-0.3.1-all.jar index 6d617f85fb29feca004a7727d73051965a1a84d1..9b0a93c4a1688bfe7b11c05c8f0d0948c0305e0d 100644 GIT binary patch delta 77897 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 findExpiredSessions( @Param("now") LocalDateTime now, @Param("statuses") Set statuses); + + + @Query("SELECT s FROM FileUploadSession s WHERE s.filePath = :filePath AND s.status = :status AND s.userId = :userId") + FileUploadSession findByFilePathAndStatus( + @Param("filePath") String filePath, + @Param("status") String status, + @Param("userId") Long userId); } diff --git a/src/main/java/ru/soune/nocopy/service/file/FileProcessingOrchestrator.java b/src/main/java/ru/soune/nocopy/service/file/FileProcessingOrchestrator.java deleted file mode 100644 index 7df7418..0000000 --- a/src/main/java/ru/soune/nocopy/service/file/FileProcessingOrchestrator.java +++ /dev/null @@ -1,55 +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.context.annotation.Lazy; -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 { - - @Lazy - private final NoCopyFileService noCopyFileService; - - private final FileEntityRepository fileRepository; - - private final FileUtil fileUtil; - - public void initializeProcessingQueue() { - List 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 newFiles = fileRepository.findAllActiveFilesAndNotProtected(); - - for (FileEntity fileEntity : newFiles) { - FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity); - - noCopyFileService.addFile(fileInfo); - } - } -} diff --git a/src/main/java/ru/soune/nocopy/service/file/FileUploadService.java b/src/main/java/ru/soune/nocopy/service/file/FileUploadService.java index 1ae1a28..f1d94d3 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileUploadService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileUploadService.java @@ -9,7 +9,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); diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java index dd47c08..7d2e137 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java @@ -28,7 +28,7 @@ public class AudioLocalSearchImpl implements AudioLocalSearch { if (fileEntity == null) return null; - return fileUtil.createFileInfo(fileEntity); + return fileUtil.createFileInfo(fileEntity, null); } @Override diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/DocumentLocalSearchImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/DocumentLocalSearchImpl.java index dea41ef..b7151d1 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/DocumentLocalSearchImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/DocumentLocalSearchImpl.java @@ -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; diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index 6d3131e..e601f8c 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -107,7 +107,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); @@ -138,6 +138,7 @@ public class FileUploadServiceImpl implements FileUploadService { .status(UploadStatus.INITIATED) .expiresAt(LocalDateTime.now().plusHours(sessionExpiryHours)) .retryCount(0) + .convertTo(convertTo) .build(); FileUploadSession savedSession = sessionRepository.save(session); @@ -248,7 +249,7 @@ public class FileUploadServiceImpl implements FileUploadService { cleanupSessionFiles(session); if (status != FileStatus.TEMP) { - FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity); + FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity, session.getConvertTo()); noCopyFileService.addFile(fileInfo); } @@ -615,76 +616,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())) diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/ImageLocalSearchImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/ImageLocalSearchImpl.java index a6ea133..ea65c88 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/ImageLocalSearchImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/ImageLocalSearchImpl.java @@ -51,6 +51,6 @@ public class ImageLocalSearchImpl implements ImageLocalSearch { FileEntity file = fileEntityRepository.findByFileId(imageHashEntity.getFileId()); return new FileProtector.FileInfo(FileProtector.Type.IMAGE, imageHashEntity.getFileId(), - String.valueOf(file.getUserId())); + String.valueOf(file.getUserId()), null); } } diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/ProtectionFileProviderImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/ProtectionFileProviderImpl.java index 9661001..304db80 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/ProtectionFileProviderImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/ProtectionFileProviderImpl.java @@ -7,7 +7,11 @@ import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; 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.service.file.FileEntityService; +import ru.soune.nocopy.service.file.FileUploadService; import java.io.File; import java.io.IOException; @@ -24,6 +28,10 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider { private final FileEntityService fileEntityService; + private final FileUploadSessionRepository fileUploadSessionRepository; + + private final FileEntityRepository fileEntityRepository; + @Nullable @Override public File getImageFile(@NotNull String id) { diff --git a/src/main/java/ru/soune/nocopy/util/FileUtil.java b/src/main/java/ru/soune/nocopy/util/FileUtil.java index 5e7b2bd..f53642b 100644 --- a/src/main/java/ru/soune/nocopy/util/FileUtil.java +++ b/src/main/java/ru/soune/nocopy/util/FileUtil.java @@ -13,17 +13,17 @@ public class FileUtil { public ImageLocalSearch.Result convertToResult(SimilarFileDTO response) { FileProtector.FileInfo fileInfo = new FileProtector.FileInfo(FileProtector.Type.IMAGE, response.getFileId(), - String.valueOf(response.getOwnerId())); + String.valueOf(response.getOwnerId()), null); ImageScore imageScore = new ImageScore(ImageScore.Rate.valueOf(response.getSimilarityLevel()), response.getHammingDistance()); return new ImageLocalSearch.Result(fileInfo, imageScore); } - public FileProtector.FileInfo createFileInfo(FileEntity fileEntity) { + public FileProtector.FileInfo createFileInfo(FileEntity fileEntity, String convertTo) { FileProtector.Type type = determineFileType(fileEntity.getMimeType()); - return new FileProtector.FileInfo(type, fileEntity.getId(), String.valueOf(fileEntity.getUserId())); + return new FileProtector.FileInfo(type, fileEntity.getId(), String.valueOf(fileEntity.getUserId()), convertTo); } public int hamming64(long aHi, long aLo, long bHi, long bLo) { From 3cf3037120a2583296389db71b8077eac6a25b98 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 25 Feb 2026 20:26:22 +0700 Subject: [PATCH 099/274] dev conver files --- .../java/ru/soune/nocopy/service/file/FileEntityService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index b91eca9..ac4214c 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -373,8 +373,8 @@ public class FileEntityService { return FileEntityResponse.builder() .id(fileEntity.getId()) .userId(fileEntity.getUserId()) - .originalFileName(fileEntity.getOriginalFileName()) - .storedFileName(fileEntity.getStoredFileName()) + .originalFileName("1.1") + .storedFileName("2.2") .filePath(fileEntity.getProtectedFilePath()) .fileSize(fileEntity.getFileSize()) .mimeType(fileEntity.getMimeType()) From 45010bb79afe144d6f1a90970d0052b8856c9a16 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 25 Feb 2026 20:29:58 +0700 Subject: [PATCH 100/274] dev conver files --- .../java/ru/soune/nocopy/service/file/FileEntityService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index ac4214c..007738c 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -373,8 +373,9 @@ public class FileEntityService { return FileEntityResponse.builder() .id(fileEntity.getId()) .userId(fileEntity.getUserId()) - .originalFileName("1.1") - .storedFileName("2.2") + .originalFileName(fileEntity.getOriginalFileName().replace("." + + fileEntity.getFileExtension(), "") + "." + fileEntity.getFileExtension()) + .storedFileName(fileEntity.getStoredFileName()) .filePath(fileEntity.getProtectedFilePath()) .fileSize(fileEntity.getFileSize()) .mimeType(fileEntity.getMimeType()) From 983b5eddda01c1ca2808d02dd45ae1a80bac7dcb Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 26 Feb 2026 00:27:31 +0700 Subject: [PATCH 101/274] dev conver files --- .../nocopy/configuration/ImageMigration.java | 44 ++++++++ .../nocopy/controller/FileController.java | 29 ++--- .../soune/nocopy/entity/file/FileEntity.java | 6 + .../repository/FileEntityRepository.java | 2 + .../nocopy/service/FileSimilarityService.java | 3 +- .../service/file/FileEntityService.java | 7 +- .../service/file/ImageResizeService.java | 104 ++++++++++++++++++ .../service/search/SearchImageService.java | 66 ----------- 8 files changed, 172 insertions(+), 89 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/configuration/ImageMigration.java create mode 100644 src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java diff --git a/src/main/java/ru/soune/nocopy/configuration/ImageMigration.java b/src/main/java/ru/soune/nocopy/configuration/ImageMigration.java new file mode 100644 index 0000000..0638556 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/configuration/ImageMigration.java @@ -0,0 +1,44 @@ +package ru.soune.nocopy.configuration; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.service.file.ImageResizeService; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +@Component +@Slf4j +public class ImageMigration { + + @Autowired + private FileEntityRepository fileRepository; + + @Autowired + private ImageResizeService imageResizeService; + + @EventListener(ApplicationReadyEvent.class) + public void migrate() { + List images = fileRepository.findByThumbnailPathIsNull(); + + for (FileEntity file : images) { + try { + Path path = Paths.get(file.getProtectedFilePath()); + if (Files.exists(path)) { + byte[] data = Files.readAllBytes(path); + imageResizeService.generateSizes(file, data); + fileRepository.save(file); + } + } catch (Exception e) { + log.error("Error migrating {}: {}", file.getId(), e.getMessage()); + } + } + } +} diff --git a/src/main/java/ru/soune/nocopy/controller/FileController.java b/src/main/java/ru/soune/nocopy/controller/FileController.java index f814982..2b100bc 100644 --- a/src/main/java/ru/soune/nocopy/controller/FileController.java +++ b/src/main/java/ru/soune/nocopy/controller/FileController.java @@ -14,6 +14,7 @@ import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.file.FileStorageService; +import ru.soune.nocopy.service.file.ImageResizeService; import java.io.FileNotFoundException; import java.net.URLEncoder; @@ -41,24 +42,8 @@ public class FileController { @Autowired private UserRepository userRepository; -// @GetMapping("/public/{fileId}") -// public ResponseEntity getPublicFile( -// @PathVariable String fileId) throws IOException { -// -// FileEntity fileEntity = fileRepository.findById(fileId) -// .orElseThrow(() -> new RuntimeException("File not found")); -// -// Resource resource = fileStorageService.loadFileAsResource(fileEntity.getFilePath()); -// -// ContentDisposition contentDisposition = ContentDisposition.inline() -// .filename(fileEntity.getOriginalFileName(), StandardCharsets.UTF_8) -// .build(); -// -// return ResponseEntity.ok() -// .contentType(new MediaType(fileEntity.getMimeType(), fileEntity.getFileExtension())) -// .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString()) -// .body(resource); -// } + @Autowired + private ImageResizeService imageResizeService; @GetMapping("/public/{fileId}") public ResponseEntity getPublicFile(@PathVariable String fileId) { @@ -117,14 +102,16 @@ public class FileController { } } - @GetMapping("/protected/{fileId}") + @GetMapping("/protected/{fileId}/{type}") public ResponseEntity getProtectedFile( - @PathVariable String fileId) throws IOException { + @PathVariable String fileId, @PathVariable String type) throws IOException { FileEntity fileEntity = fileRepository.findById(fileId) .orElseThrow(() -> new RuntimeException("File not found")); - Resource resource = fileStorageService.loadFileAsResource(fileEntity.getProtectedFilePath()); + String path = imageResizeService.getPath(fileEntity, type); + + Resource resource = fileStorageService.loadFileAsResource(path); if (fileEntity.getProtectionStatus() == ProtectionStatus.NOT_PROTECTED || fileEntity.getProtectionStatus() == ProtectionStatus.PROCESSING) { diff --git a/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java b/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java index a7b74b6..02104c7 100644 --- a/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java +++ b/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java @@ -81,6 +81,12 @@ public class FileEntity { @JsonIgnore private String signature; + @Column(name = "thumbnail_path") + private String thumbnailPath; + + @Column(name = "medium_path") + private String mediumPath; + @OneToOne(mappedBy = "file", cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnore private ImageHashEntity imageHash; diff --git a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java index 4398a51..17cf09e 100644 --- a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java @@ -61,4 +61,6 @@ public interface FileEntityRepository extends JpaRepository @Query("SELECT MAX(f.supportId) FROM FileEntity f") Integer findMaxSupportId(); + + List findByThumbnailPathIsNull(); } diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 5cb6d14..da1b0e0 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -211,7 +211,8 @@ public class FileSimilarityService { .supportId(similarImageProjection.getSupportId()) .uploadDate(similarImageProjection.getUploadDate()) .status(similarImageProjection.getProtectionStatus()) - .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId()) +// .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId()) + .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId() + "/thumbnail") .build(); } diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index 007738c..1325f54 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -255,13 +255,14 @@ public class FileEntityService { fileEntityRepository.save(fileEntity); } + private ImageResizeService imageResizeService; + @Transactional public void writeProtectedFile(String id, byte[] data, String fileExt) throws IOException { FileEntity fileEntity = fileEntityRepository.findById(id) .orElseThrow(() -> new RuntimeException("File not found: " + id)); String extension = determineFileExtension(fileExt, fileEntity); - log.info("EXTENSION: {}", extension); Path protectedFilePath = prepareProtectedPath(fileEntity, extension); if (Files.exists(protectedFilePath)) { @@ -270,6 +271,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()); diff --git a/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java b/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java new file mode 100644 index 0000000..7e9383c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java @@ -0,0 +1,104 @@ +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.path}") + private String storagePath; + + /** + * Проверяет, является ли файл изображением + */ + public boolean isImage(String extension) { + return extension != null && List.of("jpg", "jpeg", "png", "gif", "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(); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 1a35f71..d25a633 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -127,72 +127,6 @@ public class SearchImageService { return mimeType != null && mimeType.startsWith("image"); } -// private String callReverseImageApiByUrl(String imageUrl, String engine, String searchType) throws IOException { -// if (searchApiKey == null || searchApiKey.isBlank()) { -// throw new IllegalStateException("SearchAPI key not configured"); -// } -// -// OkHttpClient client = engine.contains("yandex_reverse_image") ? yandexHttpClient : googleHttpClient; -// -// 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("t_", String.valueOf(System.currentTimeMillis())) -// .build(); -// -// Request request = new Request.Builder() -// .url(url) -// .header("Accept", "application/json") -// .header("User-Agent", "Mozilla/5.0") -// .build(); -// -// int maxAttempts = 2; -// IOException lastException = null; -// -// for (int attempt = 1; attempt <= maxAttempts; attempt++) { -// long start = System.currentTimeMillis(); -// -// try (Response response = client.newCall(request).execute()) { -// long duration = System.currentTimeMillis() - start; -// log.info("Yandex response code={}, duration={}ms, attempt={}", -// response.code(), duration, attempt); -// -// if (!response.isSuccessful()) { -// throw new IOException("API error: " + response.code()); -// } -// -// ResponseBody body = response.body(); -// if (body == null) { -// throw new IOException("Empty response body"); -// } -// -// return body.string(); -// -// } catch (IOException e) { -// lastException = e; -// log.warn("Attempt {} failed for image: {}, error: {}", -// attempt, imageUrl, e.getMessage()); -// -// if (attempt == maxAttempts) { -// log.error("All {} attempts failed for image: {}", maxAttempts, imageUrl); -// throw lastException; -// } -// -// try { -// Thread.sleep(500L * attempt); -// } catch (InterruptedException ie) { -// Thread.currentThread().interrupt(); -// throw new IOException("Retry interrupted", ie); -// } -// } -// } -// -// throw lastException; -// } - private String callReverseImageApiByUrl(String imageUrl, String engine, String searchType) throws IOException { if (searchApiKey == null || searchApiKey.isBlank()) { throw new IllegalStateException("SearchAPI key not configured"); From 2b5f17f170179fef78b384a83dcb6b35240e51f1 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 26 Feb 2026 00:34:26 +0700 Subject: [PATCH 102/274] dev conver files --- .../java/ru/soune/nocopy/service/file/ImageResizeService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java b/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java index 7e9383c..fdc09a9 100644 --- a/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java +++ b/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java @@ -23,7 +23,7 @@ public class ImageResizeService { private static final int THUMBNAIL_SIZE = 150; private static final int MEDIUM_SIZE = 600; - @Value("${file.storage.path}") + @Value("${file.storage.base-path}") private String storagePath; /** From 4f2d97d205f9a5b8f5cd4d42761a5fb6b7138f57 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 26 Feb 2026 00:42:47 +0700 Subject: [PATCH 103/274] dev conver files --- .../java/ru/soune/nocopy/service/file/FileEntityService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index 1325f54..bb5a9c8 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -43,6 +43,8 @@ public class FileEntityService { private final UserRepository userRepository; + private final ImageResizeService imageResizeService; + @Transactional(noRollbackFor = DuplicateImageException.class) public FileEntity createFromUploadSession(FileUploadSession session, String checksum) { log.info("Creating FileEntity for upload session: {}", session.getUploadId()); @@ -255,8 +257,6 @@ public class FileEntityService { fileEntityRepository.save(fileEntity); } - private ImageResizeService imageResizeService; - @Transactional public void writeProtectedFile(String id, byte[] data, String fileExt) throws IOException { FileEntity fileEntity = fileEntityRepository.findById(id) From b8003c11e2d5ac0aa8da4a85525da0adf740a9c7 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 26 Feb 2026 00:49:13 +0700 Subject: [PATCH 104/274] dev conver files --- .../nocopy/service/file/ImageResizeService.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java b/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java index fdc09a9..78993b8 100644 --- a/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java +++ b/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java @@ -26,17 +26,11 @@ public class ImageResizeService { @Value("${file.storage.base-path}") private String storagePath; - /** - * Проверяет, является ли файл изображением - */ public boolean isImage(String extension) { return extension != null && List.of("jpg", "jpeg", "png", "gif", "webp") .contains(extension.toLowerCase()); } - /** - * Генерирует два размера для изображения - */ @Transactional public void generateSizes(FileEntity file, byte[] data) throws IOException { if (!isImage(file.getFileExtension())) { @@ -65,9 +59,6 @@ public class ImageResizeService { } } - /** - * Ресайзит и сохраняет - */ private String resizeAndSave(BufferedImage original, String fileName, int targetWidth) throws IOException { int targetHeight = (int) ((double) original.getHeight() / original.getWidth() * targetWidth); @@ -86,9 +77,6 @@ public class ImageResizeService { return filePath.toString(); } - /** - * Получить путь к нужному размеру - */ public String getPath(FileEntity file, String size) { if ("thumbnail".equals(size) && file.getThumbnailPath() != null) { From a2c680d10ac50ba6e89e93fc3e79bf613d1ccfbd Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 27 Feb 2026 01:18:41 +0700 Subject: [PATCH 105/274] dev conver files --- .../ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java | 2 +- .../soune/nocopy/service/file/impl/DocumentLocalSearchImpl.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java index 7d2e137..76f2499 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java @@ -28,7 +28,7 @@ public class AudioLocalSearchImpl implements AudioLocalSearch { if (fileEntity == null) return null; - return fileUtil.createFileInfo(fileEntity, null); + return fileUtil.createFileInfo(fileEntity, fileEntity.getFileExtension()); } @Override diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/DocumentLocalSearchImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/DocumentLocalSearchImpl.java index b7151d1..7e3f657 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/DocumentLocalSearchImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/DocumentLocalSearchImpl.java @@ -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, null); + FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity, fileEntity.getFileExtension()); log.info("fileInfo: {}", fileInfo); return fileInfo; From f3a0ab8181067eeea9654bfc5becd3f0232ac29a Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 27 Feb 2026 01:47:50 +0700 Subject: [PATCH 106/274] dev conver files --- .../nocopy/controller/ApiController.java | 50 ++++--------------- .../repository/FileEntityRepository.java | 3 ++ .../nocopy/service/FileSimilarityService.java | 27 ++++++++-- .../file/impl/FileUploadServiceImpl.java | 3 +- 4 files changed, 38 insertions(+), 45 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 2244b3b..db879d8 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -198,9 +198,18 @@ public class ApiController { } } - private List processNonImageFile(FileEntity fileEntity, String authToken) { + private List processNonImageFile(FileEntity fileEntity, String authToken) throws Exception { List results = new ArrayList<>(); + FileEntity duplicateByHash = fileSimilarityService.findDuplicateByHash(fileEntity.getProtectedFilePath(), + fileEntity.getMimeType(), fileEntity.getUserId()); + + if (duplicateByHash != null) { + results.add(fileSimilarityService.buildDTO(duplicateByHash)); + + return results; + } + File file = new File(fileEntity.getFilePath()); FileProtector.Type type = "document".equals(fileEntity.getMimeType()) ? FileProtector.Type.DOC : FileProtector.Type.AUDIO; @@ -465,45 +474,6 @@ public class ApiController { } } -// @GetMapping("/protect/{fileId}") -// public ResponseEntity protect( @PathVariable(required = false) String fileId) { -// Optional optionalFileEntity = fileEntityRepository.findById(fileId); -// -// if (!optionalFileEntity.isPresent()) { -// return ResponseEntity.notFound().build(); -// } -// FileEntity fileEntity = optionalFileEntity.get(); -// FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity); -// -// noCopyFileService.addFile(fileInfo); -// -// fileEntity.setProtectionStatus(ProtectionStatus.PROCESSING); -// fileEntityRepository.save(fileEntity); -// -// return ResponseEntity.ok().build(); -// } -// -// @GetMapping("/check/{fileId}/{type}") -// public ResponseEntity check(@PathVariable(required = false) String fileId, -// @PathVariable(required = false) String type) { -// Optional optionalFileEntity = fileEntityRepository.findById(fileId); -// -// if (!optionalFileEntity.isPresent()) { -// return ResponseEntity.notFound().build(); -// } -// -// FileEntity fileEntity = optionalFileEntity.get(); -// -// Path path = Paths.get(fileEntity.getProtectedFilePath()); -// -// File file = path.toFile(); -// -// NoCopyCheckResult noCopyCheckResult = noCopyFileService.checkFile(file, -// FileProtector.Type.valueOf(type.toUpperCase())); -// -// return ResponseEntity.ok().body(noCopyCheckResult); -// } - @GetMapping("/check/file_stats") public ResponseEntity checkFileProtectStats(@RequestHeader("Authorization") String tokenHeader) { String token = tokenHeader.replace("Bearer ", ""); diff --git a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java index 17cf09e..234cbc4 100644 --- a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java @@ -59,6 +59,9 @@ public interface FileEntityRepository extends JpaRepository @Query("SELECT f FROM FileEntity f WHERE f.mimeType = :mimeType") List findByMimeType(String mimeType); + @Query("SELECT f FROM FileEntity f WHERE f.mimeType = :mimeType AND f.userId IN :userIds") + List findByMimeTypeAndUserIds(String mimeType, List userIds); + @Query("SELECT MAX(f.supportId) FROM FileEntity f") Integer findMaxSupportId(); diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index da1b0e0..4401cad 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -103,16 +103,35 @@ public class FileSimilarityService { return duplicates; } - public void hasDuplicatesByHash(String path, String mimeType) throws Exception { + 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()); + } + } + + public FileEntity findDuplicateByHash(String path, String mimeType, Long userId) throws Exception { String hash = calculateFileHash(path); - List fileEntityList = fileEntityRepository.findByMimeType(mimeType); + User user = userRepository.findById(userId).orElseThrow(); + Company company = user.getCompany(); + List userIds = new ArrayList<>(); + + if (company != null) { + userIds.addAll(company.getUsers().stream().map(User::getId).toList()); + } else { + userIds.add(userId); + } + + List fileEntityList = fileEntityRepository.findByMimeTypeAndUserIds(mimeType, userIds); for (FileEntity file : fileEntityList) { if (calculateFileHash(file.getFilePath()).equals(hash)) { - throw new DuplicateImageException("Duplicate", file.getId(), - file.getUserId()); + return file; } } + + return null; } private final UserRepository userRepository; diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index e601f8c..cfcba2c 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -319,7 +319,8 @@ public class FileUploadServiceImpl implements FileUploadService { if (session.getFileType().startsWith("image")) { checkForDuplicatesSynchronously(finalFilePath); } else if (session.getFileType().startsWith("audio") || session.getFileType().startsWith("document")) { - fileSimilarityService.hasDuplicatesByHash(finalFilePath ,session.getFileType()); + fileSimilarityService.hasDuplicatesByHash(finalFilePath ,session.getFileType(), + session.getUserId()); File file = new File(finalFilePath); From ff441a6ea4ba9ad8a95283b3a39614aef8ac63b6 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 27 Feb 2026 01:52:56 +0700 Subject: [PATCH 107/274] dev conver files --- src/main/java/ru/soune/nocopy/controller/ApiController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index db879d8..9face2a 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -201,7 +201,7 @@ public class ApiController { private List processNonImageFile(FileEntity fileEntity, String authToken) throws Exception { List results = new ArrayList<>(); - FileEntity duplicateByHash = fileSimilarityService.findDuplicateByHash(fileEntity.getProtectedFilePath(), + FileEntity duplicateByHash = fileSimilarityService.findDuplicateByHash(fileEntity.getFilePath(), fileEntity.getMimeType(), fileEntity.getUserId()); if (duplicateByHash != null) { From 76aa04e17413cade1b2e137bfab97ee784518115 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 27 Feb 2026 17:55:11 +0700 Subject: [PATCH 108/274] dev conver files --- src/main/resources/application.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index babac22..9d480a2 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -104,7 +104,7 @@ yandex: search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image} searchapi: - api-key: ${SEARCHAPI_API_KEY:7pu81qZdoih2JaTBDaoNfrRS} + api-key: ${SEARCHAPI_API_KEY:qQ19qn3JeRtqm9aasEgUFvGc} reverse-image-url: "https://searchapi.io/api/v1/search" app: From c09a3a3a1839a47c6faf3d3795d762c22ba40e95 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 27 Feb 2026 17:57:55 +0700 Subject: [PATCH 109/274] dev add monitoring basic --- .../nocopy/configuration/HandlerConfig.java | 6 +- .../nocopy/dto/file/YandexSearchResponse.java | 5 - .../dto/monitoring/GetViolationsRequest.java | 23 ++++ .../monitoring/MonitoringStatusResponse.java | 40 ++++++ .../dto/monitoring/SetMonitoringRequest.java | 20 +++ .../dto/monitoring/ViolationResponse.java | 43 ++++++ .../monitoring/FileMonitoringEntity.java | 60 ++++++++ .../monitoring/FoundViolationEntity.java | 76 ++++++++++ .../entity/monitoring/MonitoringType.java | 6 + .../entity/monitoring/SearchEngine.java | 8 ++ .../soune/nocopy/entity/tarif/TariffType.java | 3 +- .../exception/UserNotHavePermission.java | 7 + .../nocopy/handler/GetViolationsHandler.java | 85 ++++++++++++ .../nocopy/handler/MonitoringHandler.java | 72 ++++++++++ .../repository/FileMonitoringRepository.java | 25 ++++ .../repository/FoundViolationRepository.java | 30 ++++ .../nocopy/scheduler/MonitoringScheduler.java | 47 +++++++ .../monitoring/FileMonitoringService.java | 101 ++++++++++++++ .../monitoring/MonitoringSearchService.java | 130 ++++++++++++++++++ .../nocopy/service/register/AuthService.java | 4 + .../tariff/TariffInitializationService.java | 31 +++++ 21 files changed, 815 insertions(+), 7 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/monitoring/GetViolationsRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/monitoring/MonitoringStatusResponse.java create mode 100644 src/main/java/ru/soune/nocopy/dto/monitoring/SetMonitoringRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/monitoring/ViolationResponse.java create mode 100644 src/main/java/ru/soune/nocopy/entity/monitoring/FileMonitoringEntity.java create mode 100644 src/main/java/ru/soune/nocopy/entity/monitoring/FoundViolationEntity.java create mode 100644 src/main/java/ru/soune/nocopy/entity/monitoring/MonitoringType.java create mode 100644 src/main/java/ru/soune/nocopy/entity/monitoring/SearchEngine.java create mode 100644 src/main/java/ru/soune/nocopy/exception/UserNotHavePermission.java create mode 100644 src/main/java/ru/soune/nocopy/handler/GetViolationsHandler.java create mode 100644 src/main/java/ru/soune/nocopy/handler/MonitoringHandler.java create mode 100644 src/main/java/ru/soune/nocopy/repository/FileMonitoringRepository.java create mode 100644 src/main/java/ru/soune/nocopy/repository/FoundViolationRepository.java create mode 100644 src/main/java/ru/soune/nocopy/scheduler/MonitoringScheduler.java create mode 100644 src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java create mode 100644 src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index c000833..40361d4 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -26,7 +26,9 @@ public class HandlerConfig { ReferralHandler referralHandler, ResetPasswordHandler resetPasswordHandler, DaDataHandler daDataHandler, - PaymentHandler paymentHandler + PaymentHandler paymentHandler, + GetViolationsHandler getViolationsHandler, + MonitoringHandler monitoringHandler ) { Map map = new HashMap<>(); map.put(20001, login); @@ -44,6 +46,8 @@ public class HandlerConfig { map.put(30003, referralHandler); map.put(30004, daDataHandler); map.put(30005, paymentHandler); + map.put(30006, getViolationsHandler); + map.put(30007, monitoringHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/dto/file/YandexSearchResponse.java b/src/main/java/ru/soune/nocopy/dto/file/YandexSearchResponse.java index f064ad3..3542068 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/YandexSearchResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/file/YandexSearchResponse.java @@ -12,11 +12,6 @@ public class YandexSearchResponse { @JsonProperty("images") private List images; -// @JsonProperty("images") -// public void setImages(List images) { -// this.images = images; -// } - @Data @JsonIgnoreProperties(ignoreUnknown = true) public static class ImageResult { diff --git a/src/main/java/ru/soune/nocopy/dto/monitoring/GetViolationsRequest.java b/src/main/java/ru/soune/nocopy/dto/monitoring/GetViolationsRequest.java new file mode 100644 index 0000000..9c9dda1 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/monitoring/GetViolationsRequest.java @@ -0,0 +1,23 @@ +package ru.soune.nocopy.dto.monitoring; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class GetViolationsRequest { + + @JsonProperty("file_id") + private String fileId; + + @JsonProperty("page") + private Integer page; + + @JsonProperty("page_size") + private Integer pageSize; + + @JsonProperty("mark_as_read") + private boolean markAsRead; + + @JsonProperty("auth_token") + private String authToken; +} diff --git a/src/main/java/ru/soune/nocopy/dto/monitoring/MonitoringStatusResponse.java b/src/main/java/ru/soune/nocopy/dto/monitoring/MonitoringStatusResponse.java new file mode 100644 index 0000000..b9b832a --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/monitoring/MonitoringStatusResponse.java @@ -0,0 +1,40 @@ +package ru.soune.nocopy.dto.monitoring; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import ru.soune.nocopy.entity.monitoring.MonitoringType; + +import java.time.LocalDateTime; + +/** + * Response for status monitoring + */ +@Data +@Builder +public class MonitoringStatusResponse { + + @JsonProperty("file_id") + private String fileId; + + @JsonProperty("file_name") + private String fileName; + + @JsonProperty("monitoring_type") + private MonitoringType monitoringType; + + @JsonProperty("next_run") + private LocalDateTime nextRun; + + @JsonProperty("last_run") + private LocalDateTime lastRun; + + @JsonProperty("last_run_status") + private String lastRunStatus; + + @JsonProperty("is_active") + private boolean isActive; + + @JsonProperty("new_violations_count") + private long newViolationsCount; +} diff --git a/src/main/java/ru/soune/nocopy/dto/monitoring/SetMonitoringRequest.java b/src/main/java/ru/soune/nocopy/dto/monitoring/SetMonitoringRequest.java new file mode 100644 index 0000000..4694f65 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/monitoring/SetMonitoringRequest.java @@ -0,0 +1,20 @@ +package ru.soune.nocopy.dto.monitoring; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import ru.soune.nocopy.entity.monitoring.MonitoringType; +/** + Set monitoring request + */ +@Data +public class SetMonitoringRequest { + + @JsonProperty("file_id") + private String fileId; + + @JsonProperty("monitoring_type") + private MonitoringType monitoringType; + + @JsonProperty("auth_token") + private String authToken; +} diff --git a/src/main/java/ru/soune/nocopy/dto/monitoring/ViolationResponse.java b/src/main/java/ru/soune/nocopy/dto/monitoring/ViolationResponse.java new file mode 100644 index 0000000..188b4b8 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/monitoring/ViolationResponse.java @@ -0,0 +1,43 @@ +package ru.soune.nocopy.dto.monitoring; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import ru.soune.nocopy.entity.monitoring.SearchEngine; + +import java.time.LocalDateTime; + +@Data +@Builder +public class ViolationResponse { + + @JsonProperty("id") + private String id; + + @JsonProperty("file_id") + private String fileId; + + @JsonProperty("file_name") + private String fileName; + + @JsonProperty("found_url") + private String foundUrl; + + @JsonProperty("page_url") + private String pageUrl; + + @JsonProperty("page_title") + private String pageTitle; + + @JsonProperty("host") + private String host; + + @JsonProperty("search_engine") + private SearchEngine searchEngine; + + @JsonProperty("found_at") + private LocalDateTime foundAt; + + @JsonProperty("is_new") + private boolean isNew; +} diff --git a/src/main/java/ru/soune/nocopy/entity/monitoring/FileMonitoringEntity.java b/src/main/java/ru/soune/nocopy/entity/monitoring/FileMonitoringEntity.java new file mode 100644 index 0000000..87fea5c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/monitoring/FileMonitoringEntity.java @@ -0,0 +1,60 @@ +package ru.soune.nocopy.entity.monitoring; + +import jakarta.persistence.*; +import lombok.*; +import ru.soune.nocopy.entity.file.FileEntity; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "file_monitoring") +public class FileMonitoringEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "file_id", nullable = false) + private FileEntity file; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Enumerated(EnumType.STRING) + @Column(name = "monitoring_type", nullable = false) + private MonitoringType monitoringType; + + @Column(name = "next_run", nullable = false) + private LocalDateTime nextRun; + + @Column(name = "last_run") + private LocalDateTime lastRun; + + @Column(name = "last_run_status") + private String lastRunStatus; + + @Column(name = "is_active", nullable = false) + private boolean isActive = true; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @PrePersist + public void prePersist() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + public void preUpdate() { + this.updatedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/entity/monitoring/FoundViolationEntity.java b/src/main/java/ru/soune/nocopy/entity/monitoring/FoundViolationEntity.java new file mode 100644 index 0000000..d91ed8f --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/monitoring/FoundViolationEntity.java @@ -0,0 +1,76 @@ +package ru.soune.nocopy.entity.monitoring; + +import jakarta.persistence.*; +import lombok.*; +import ru.soune.nocopy.entity.file.FileEntity; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "found_violations", + indexes = { + @Index(name = "idx_file_id", columnList = "file_id"), + @Index(name = "idx_found_at", columnList = "found_at") + }) +public class FoundViolationEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "file_id", nullable = false) + private FileEntity file; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "file_id_copy", nullable = false) + private String fileId; + + @Column(name = "file_name_copy") + private String fileName; + + @Column(name = "found_url", nullable = false, length = 2048) + private String foundUrl; + + @Column(name = "page_url", length = 2048) + private String pageUrl; + + @Column(name = "page_title") + private String pageTitle; + + @Column(name = "host") + private String host; + + @Column(name = "image_width") + private Integer imageWidth; + + @Column(name = "image_height") + private Integer imageHeight; + + @Enumerated(EnumType.STRING) + @Column(name = "search_engine", nullable = false) + private SearchEngine searchEngine; + + @Column(name = "search_session_id") + private String searchSessionId; + + @Column(name = "found_at", nullable = false) + private LocalDateTime foundAt; + + @Column(name = "is_new", nullable = false) + private boolean isNew = true; + + @Column(name = "is_notified", nullable = false) + private boolean isNotified = false; + + @PrePersist + public void prePersist() { + this.foundAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/monitoring/MonitoringType.java b/src/main/java/ru/soune/nocopy/entity/monitoring/MonitoringType.java new file mode 100644 index 0000000..21a8b84 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/monitoring/MonitoringType.java @@ -0,0 +1,6 @@ +package ru.soune.nocopy.entity.monitoring; + + +public enum MonitoringType { + MONITORING_DAILY, MONITORING_WEEKLY, MONITORING_MONTHLY, NONE +} diff --git a/src/main/java/ru/soune/nocopy/entity/monitoring/SearchEngine.java b/src/main/java/ru/soune/nocopy/entity/monitoring/SearchEngine.java new file mode 100644 index 0000000..97a663c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/monitoring/SearchEngine.java @@ -0,0 +1,8 @@ +package ru.soune.nocopy.entity.monitoring; + + +public enum SearchEngine { + YANDEX, + GOOGLE, + BOTH +} diff --git a/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java b/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java index 6507b51..aa3eb2b 100644 --- a/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java +++ b/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java @@ -2,5 +2,6 @@ package ru.soune.nocopy.entity.tarif; public enum TariffType { - START, BASIC, PRO, ENTERPRISE, DEMO, BUISNES, CORPORAT, STUDIO, FREE, TOKEN_100, TOKEN_500, TOKEN_1000, TOKEN_5000; + START, BASIC, PRO, ENTERPRISE, DEMO, BUISNES, CORPORAT, STUDIO, FREE, TOKEN_100, TOKEN_500, TOKEN_1000, TOKEN_5000, + MONITORING_DAILY, MONITORING_WEEKLY, MONITORING_MONTHLY; } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/exception/UserNotHavePermission.java b/src/main/java/ru/soune/nocopy/exception/UserNotHavePermission.java new file mode 100644 index 0000000..7704209 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/UserNotHavePermission.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class UserNotHavePermission extends RuntimeException { + public UserNotHavePermission(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/GetViolationsHandler.java b/src/main/java/ru/soune/nocopy/handler/GetViolationsHandler.java new file mode 100644 index 0000000..edd261e --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/GetViolationsHandler.java @@ -0,0 +1,85 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.monitoring.GetViolationsRequest; +import ru.soune.nocopy.dto.monitoring.ViolationResponse; +import ru.soune.nocopy.entity.monitoring.FoundViolationEntity; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.repository.FoundViolationRepository; +import ru.soune.nocopy.service.register.AuthService; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Component +@RequiredArgsConstructor +public class GetViolationsHandler implements RequestHandler { + private final ObjectMapper objectMapper; + + private final FoundViolationRepository violationRepository; + + private final AuthService authService; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + GetViolationsRequest violationsRequest = objectMapper.convertValue( + request.getMessageBody(), GetViolationsRequest.class); + + String authToken = violationsRequest.getAuthToken(); + + //TODO add return for null token + if (authToken == null) { + return new BaseResponse(0, 0, "", new Object()); + } + + User user = authService.getAuthToken(authToken).getUser(); + List violations; + + if (violationsRequest.getFileId() != null) { + violations = violationRepository.findByFileIdOrderByFoundAtDesc( + violationsRequest.getFileId()); + } else { + violations = violationRepository.findByUserIdOrderByFoundAtDesc(user.getId()); + } + + if (violationsRequest.isMarkAsRead()) { + violations.stream() + .filter(FoundViolationEntity::isNew) + .forEach(v -> v.setNew(false)); + violationRepository.saveAll(violations); + } + + List response = violations.stream() + .map(this::mapToResponse) + .collect(Collectors.toList()); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + "Violations retrieved", + response + ); + } + + private ViolationResponse mapToResponse(FoundViolationEntity entity) { + return ViolationResponse.builder() + .id(entity.getId()) + .fileId(entity.getFileId()) + .fileName(entity.getFileName()) + .foundUrl(entity.getFoundUrl()) + .pageUrl(entity.getPageUrl()) + .pageTitle(entity.getPageTitle()) + .host(entity.getHost()) + .searchEngine(entity.getSearchEngine()) + .foundAt(entity.getFoundAt()) + .isNew(entity.isNew()) + .build(); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/MonitoringHandler.java b/src/main/java/ru/soune/nocopy/handler/MonitoringHandler.java new file mode 100644 index 0000000..8a19270 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/MonitoringHandler.java @@ -0,0 +1,72 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.monitoring.MonitoringStatusResponse; +import ru.soune.nocopy.dto.monitoring.SetMonitoringRequest; +import ru.soune.nocopy.entity.monitoring.MonitoringType; +import ru.soune.nocopy.entity.tarif.TariffType; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.repository.TariffRepository; +import ru.soune.nocopy.service.monitoring.FileMonitoringService; +import ru.soune.nocopy.service.register.AuthService; +import ru.soune.nocopy.service.tariff.TariffService; + +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MonitoringHandler implements RequestHandler { + + private final ObjectMapper objectMapper; + + private final FileMonitoringService monitoringService; + + private final AuthService authService; + + private final TariffService tariffService; + + private final TariffRepository tariffRepository; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + SetMonitoringRequest setRequest = objectMapper.convertValue( + request.getMessageBody(), SetMonitoringRequest.class); + + String authToken = setRequest.getAuthToken(); + AuthToken userAuthToken = authService.getAuthToken(authToken); + + if (authToken == null || userAuthToken == null) { + return new BaseResponse(30007, MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), + MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), + Map.of("token", authToken)); + } + + User user = userAuthToken.getUser(); + Long userId = user.getId(); + MonitoringType monitoringType = setRequest.getMonitoringType(); + + monitoringService.setMonitoring(userId, setRequest.getFileId(), monitoringType); + + MonitoringStatusResponse status = monitoringService.getMonitoringStatus( + setRequest.getFileId(), userId); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + "Monitoring settings updated", + status); + } + //TODO check tokens for apply + private boolean checkTokensForMonitoring(User user, MonitoringType monitoringType) { + tariffService.getTariffByType(TariffType.valueOf(monitoringType.name())); + return true; + } +} diff --git a/src/main/java/ru/soune/nocopy/repository/FileMonitoringRepository.java b/src/main/java/ru/soune/nocopy/repository/FileMonitoringRepository.java new file mode 100644 index 0000000..126aa7a --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/FileMonitoringRepository.java @@ -0,0 +1,25 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; +import ru.soune.nocopy.entity.monitoring.MonitoringType; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository +public interface FileMonitoringRepository extends JpaRepository { + + Optional findByFileId(String fileId); + + List findByUserIdAndIsActiveTrue(Long userId); + + @Query("SELECT fm FROM FileMonitoringEntity fm WHERE fm.isActive = true AND fm.monitoringType != 'NONE' AND fm.nextRun <= :now") + List findReadyForRun(@Param("now") LocalDateTime now); + + List findByMonitoringTypeAndIsActiveTrue(MonitoringType type); +} diff --git a/src/main/java/ru/soune/nocopy/repository/FoundViolationRepository.java b/src/main/java/ru/soune/nocopy/repository/FoundViolationRepository.java new file mode 100644 index 0000000..c6ed9a5 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/FoundViolationRepository.java @@ -0,0 +1,30 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.monitoring.FoundViolationEntity; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public interface FoundViolationRepository extends JpaRepository { + + List findByFileIdOrderByFoundAtDesc(String fileId); + + List findByUserIdOrderByFoundAtDesc(Long userId); + + @Query("SELECT fv FROM FoundViolationEntity fv WHERE fv.userId = :userId AND fv.isNew = true") + List findNewViolationsByUser(@Param("userId") Long userId); + + @Query("SELECT fv FROM FoundViolationEntity fv WHERE fv.file.id = :fileId AND fv.foundUrl = :foundUrl") + List findDuplicate(@Param("fileId") String fileId, @Param("foundUrl") String foundUrl); + + List findByFoundAtBetween(LocalDateTime start, LocalDateTime end); + + @Query("SELECT COUNT(fv) FROM FoundViolationEntity fv WHERE fv.userId = :userId AND fv.isNew = true") + long countNewByUserId(@Param("userId") Long userId); +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/scheduler/MonitoringScheduler.java b/src/main/java/ru/soune/nocopy/scheduler/MonitoringScheduler.java new file mode 100644 index 0000000..442bcbe --- /dev/null +++ b/src/main/java/ru/soune/nocopy/scheduler/MonitoringScheduler.java @@ -0,0 +1,47 @@ +package ru.soune.nocopy.scheduler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; +import ru.soune.nocopy.repository.FileMonitoringRepository; +import ru.soune.nocopy.service.monitoring.MonitoringSearchService; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MonitoringScheduler { + + private final FileMonitoringRepository monitoringRepository; + + private final MonitoringSearchService monitoringSearchService; + + @Scheduled(cron = "0 1 2 * * *", zone = "Europe/Moscow") + @Transactional + public void processScheduledMonitoring() { + log.info("Starting scheduled monitoring check at 02:01 MSK"); + + LocalDateTime now = LocalDateTime.now(); + List readyForRun = monitoringRepository.findReadyForRun(now); + + log.info("Found {} files ready for monitoring", readyForRun.size()); + + //TODO check tokens for apply + for (FileMonitoringEntity monitoring : readyForRun) { + try { + monitoringSearchService.processFileSearch(monitoring); + log.info("Successfully queued search for file: {}", monitoring.getFile().getId()); + } catch (Exception e) { + log.error("Failed to process monitoring for file: {}", monitoring.getFile().getId(), e); + monitoring.setLastRunStatus("ERROR: " + e.getMessage()); + monitoringRepository.save(monitoring); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java b/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java new file mode 100644 index 0000000..85b6e44 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java @@ -0,0 +1,101 @@ +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.BaseResponse; +import ru.soune.nocopy.dto.monitoring.MonitoringStatusResponse; +import ru.soune.nocopy.entity.file.*; +import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; +import ru.soune.nocopy.entity.monitoring.MonitoringType; +import ru.soune.nocopy.exception.NotValidFieldException; +import ru.soune.nocopy.exception.UserNotHavePermission; +import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.repository.FileMonitoringRepository; +import ru.soune.nocopy.repository.FoundViolationRepository; + +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 FoundViolationRepository violationRepository; + + @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(); + + long newViolations = violationRepository.countNewByUserId(userId); + + 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()) + .newViolationsCount(newViolations) + .build(); + } + +} diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java new file mode 100644 index 0000000..3068959 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java @@ -0,0 +1,130 @@ +package ru.soune.nocopy.service.monitoring; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.dto.file.YandexSearchResponse; +import ru.soune.nocopy.entity.file.*; +import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; +import ru.soune.nocopy.entity.monitoring.FoundViolationEntity; +import ru.soune.nocopy.entity.monitoring.MonitoringType; +import ru.soune.nocopy.entity.monitoring.SearchEngine; +import ru.soune.nocopy.repository.FileMonitoringRepository; +import ru.soune.nocopy.repository.FoundViolationRepository; +import ru.soune.nocopy.service.search.SearchImageService; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeoutException; + +@Slf4j +@Service +@RequiredArgsConstructor +public class MonitoringSearchService { + + private final SearchImageService searchImageService; + private final FoundViolationRepository violationRepository; + private final FileMonitoringRepository monitoringRepository; + + @Transactional + public void processFileSearch(FileMonitoringEntity monitoring) { + String searchSessionId = UUID.randomUUID().toString(); + log.info("Starting search session: {} for file: {}", searchSessionId, monitoring.getFile().getId()); + + monitoring.setLastRun(LocalDateTime.now()); + monitoring.setLastRunStatus("IN_PROGRESS"); + monitoringRepository.save(monitoring); + + try { + String yandexResponse = searchImageService.searchReverseByPublicUrl( + monitoring.getFile(), "yandex_reverse_image", "visual_matches"); + + List yandexImages = + searchImageService.getAllImagesWithoutPagination(yandexResponse, "visual_matches"); + + int savedCount = saveFoundImages(monitoring, yandexImages, SearchEngine.YANDEX, searchSessionId); + + try { + String googleResponse = searchImageService.searchReverseByPublicUrl( + monitoring.getFile(), "google_lens", "exact_matches"); + + List googleImages = + searchImageService.getAllImagesWithoutPagination(googleResponse, "exact_matches"); + + savedCount += saveFoundImages(monitoring, googleImages, SearchEngine.GOOGLE, searchSessionId); + + } catch (TimeoutException | IOException e) { + log.warn("Google search failed for session: {}", searchSessionId, e); + } + + monitoring.setLastRunStatus("SUCCESS - Found " + savedCount + " violations"); + + } catch (Exception e) { + log.error("Search failed for session: {}", searchSessionId, e); + monitoring.setLastRunStatus("ERROR: " + e.getMessage()); + } finally { + updateNextRun(monitoring); + monitoringRepository.save(monitoring); + } + } + + private int saveFoundImages(FileMonitoringEntity monitoring, + List images, + SearchEngine engine, + String sessionId) { + int savedCount = 0; + + for (YandexSearchResponse.ImageResult image : images) { + List existing = violationRepository.findDuplicate( + monitoring.getFile().getId(), image.getUrl()); + + if (existing.isEmpty()) { + FoundViolationEntity violation = FoundViolationEntity.builder() + .file(monitoring.getFile()) + .fileId(monitoring.getFile().getId()) + .fileName(monitoring.getFile().getOriginalFileName()) + .userId(monitoring.getUserId()) + .foundUrl(image.getUrl()) + .pageUrl(image.getPageUrl()) + .pageTitle(image.getPageTitle()) + .host(image.getHost()) + .imageWidth(image.getWidth()) + .imageHeight(image.getHeight()) + .searchEngine(engine) + .searchSessionId(sessionId) + .isNew(true) + .isNotified(false) + .build(); + + violationRepository.save(violation); + savedCount++; + } + } + + return savedCount; + } + + 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; + }; + } +} diff --git a/src/main/java/ru/soune/nocopy/service/register/AuthService.java b/src/main/java/ru/soune/nocopy/service/register/AuthService.java index a8dc32c..652d45b 100644 --- a/src/main/java/ru/soune/nocopy/service/register/AuthService.java +++ b/src/main/java/ru/soune/nocopy/service/register/AuthService.java @@ -160,6 +160,10 @@ public class AuthService { 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()); diff --git a/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java b/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java index 1f5464c..72244d5 100644 --- a/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java +++ b/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java @@ -220,6 +220,37 @@ public class TariffInitializationService { 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; } tariffRepository.save(tariff); From c86290c2cb589e54ce2e09bfe62f31f0cec929ea Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 28 Feb 2026 11:25:41 +0700 Subject: [PATCH 110/274] dev add monitoring basic --- src/main/java/ru/soune/nocopy/handler/MonitoringHandler.java | 3 +-- .../nocopy/service/monitoring/FileMonitoringService.java | 5 ++--- .../nocopy/service/monitoring/MonitoringSearchService.java | 3 ++- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/MonitoringHandler.java b/src/main/java/ru/soune/nocopy/handler/MonitoringHandler.java index 8a19270..77dee1b 100644 --- a/src/main/java/ru/soune/nocopy/handler/MonitoringHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/MonitoringHandler.java @@ -55,8 +55,7 @@ public class MonitoringHandler implements RequestHandler { monitoringService.setMonitoring(userId, setRequest.getFileId(), monitoringType); - MonitoringStatusResponse status = monitoringService.getMonitoringStatus( - setRequest.getFileId(), userId); + MonitoringStatusResponse status = monitoringService.getMonitoringStatus(setRequest.getFileId(), userId); return new BaseResponse( request.getMsgId(), diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java b/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java index 85b6e44..4a2d1e7 100644 --- a/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java +++ b/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java @@ -4,12 +4,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.monitoring.MonitoringStatusResponse; import ru.soune.nocopy.entity.file.*; import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; import ru.soune.nocopy.entity.monitoring.MonitoringType; -import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.UserNotHavePermission; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileMonitoringRepository; @@ -23,9 +21,10 @@ import java.time.temporal.TemporalAdjusters; @Service @RequiredArgsConstructor public class FileMonitoringService { - private final FileMonitoringRepository monitoringRepository; + private final FileEntityRepository fileRepository; + private final FoundViolationRepository violationRepository; @Transactional diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java index 3068959..8796bce 100644 --- a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java @@ -24,9 +24,10 @@ import java.util.concurrent.TimeoutException; @Service @RequiredArgsConstructor public class MonitoringSearchService { - private final SearchImageService searchImageService; + private final FoundViolationRepository violationRepository; + private final FileMonitoringRepository monitoringRepository; @Transactional From 24228609bb03d8f6da7078971f5a33a22920afbc Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 2 Mar 2026 13:40:07 +0700 Subject: [PATCH 111/274] dev add monitoring basic --- src/main/java/ru/soune/nocopy/controller/ApiController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 9face2a..752c4f7 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -210,7 +210,7 @@ public class ApiController { return results; } - File file = new File(fileEntity.getFilePath()); + File file = new File(fileEntity.getProtectedFilePath()); FileProtector.Type type = "document".equals(fileEntity.getMimeType()) ? FileProtector.Type.DOC : FileProtector.Type.AUDIO; From 8bcbc44528e5ba7dc01ccc6d27eeb3b6f2e1354e Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 2 Mar 2026 13:55:45 +0700 Subject: [PATCH 112/274] dev add monitoring basic --- .../soune/nocopy/service/FileSimilarityService.java | 11 +++++++++-- .../service/file/impl/AudioLocalSearchImpl.java | 2 +- .../service/file/impl/DocumentLocalSearchImpl.java | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 4401cad..4030844 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -18,6 +18,7 @@ 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; @@ -113,6 +114,7 @@ public class FileSimilarityService { public FileEntity findDuplicateByHash(String path, String mimeType, Long userId) throws Exception { String hash = calculateFileHash(path); + User user = userRepository.findById(userId).orElseThrow(); Company company = user.getCompany(); List userIds = new ArrayList<>(); @@ -126,8 +128,13 @@ public class FileSimilarityService { List fileEntityList = fileEntityRepository.findByMimeTypeAndUserIds(mimeType, userIds); for (FileEntity file : fileEntityList) { - if (calculateFileHash(file.getFilePath()).equals(hash)) { - return file; + File existingFile = new File(file.getProtectedFilePath()); + File newFile = new File(path); + + if (existingFile.length() == newFile.length()) { + if (calculateFileHash(file.getProtectedFilePath()).equals(hash)) { + return file; + } } } diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java index 76f2499..7d2e137 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java @@ -28,7 +28,7 @@ public class AudioLocalSearchImpl implements AudioLocalSearch { if (fileEntity == null) return null; - return fileUtil.createFileInfo(fileEntity, fileEntity.getFileExtension()); + return fileUtil.createFileInfo(fileEntity, null); } @Override diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/DocumentLocalSearchImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/DocumentLocalSearchImpl.java index 7e3f657..b7151d1 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/DocumentLocalSearchImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/DocumentLocalSearchImpl.java @@ -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, fileEntity.getFileExtension()); + FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity, null); log.info("fileInfo: {}", fileInfo); return fileInfo; From ce7906d2a8102f8b47f49f9cec3063caae8f0d4c Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 2 Mar 2026 14:00:10 +0700 Subject: [PATCH 113/274] dev add monitoring basic --- .../java/ru/soune/nocopy/controller/ApiController.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 752c4f7..899287a 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -186,6 +186,10 @@ public class ApiController { response = buildSuccessResponse(similarFilesPage, MessageCode.SIMILAR_FILES_FOUND); } else { List results = processNonImageFile(fileEntity, authToken); + log.info("results: {}", results); + log.info("results: {}", results); + log.info("results: {}", results); + log.info("results: {}", results); response = buildSuccessResponse(results, MessageCode.SIMILAR_FILES_FOUND); } @@ -203,7 +207,7 @@ public class ApiController { FileEntity duplicateByHash = fileSimilarityService.findDuplicateByHash(fileEntity.getFilePath(), fileEntity.getMimeType(), fileEntity.getUserId()); - + log.info("duplicateByHash: {}", duplicateByHash); if (duplicateByHash != null) { results.add(fileSimilarityService.buildDTO(duplicateByHash)); @@ -215,6 +219,7 @@ public class ApiController { FileProtector.Type.DOC : FileProtector.Type.AUDIO; NoCopyCheckResult checkResult = noCopyFileService.checkFile(file, type); + log.info("checkResult: {}", checkResult); switch (checkResult) { case DocumentCheckResult.Success success -> { From 077fe0c254b2fcd719bcb92483c6176acf42aaf1 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 2 Mar 2026 14:05:27 +0700 Subject: [PATCH 114/274] dev add monitoring basic --- src/main/java/ru/soune/nocopy/service/FileSimilarityService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 4030844..8a1261a 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -128,6 +128,7 @@ public class FileSimilarityService { List fileEntityList = fileEntityRepository.findByMimeTypeAndUserIds(mimeType, userIds); for (FileEntity file : fileEntityList) { + if (file.getProtectedFilePath() == null) continue; File existingFile = new File(file.getProtectedFilePath()); File newFile = new File(path); From fa7f5a731ec45bacf4311fe2b03c6bdf59c8b1c7 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 2 Mar 2026 14:12:40 +0700 Subject: [PATCH 115/274] dev add monitoring basic --- src/main/java/ru/soune/nocopy/controller/ApiController.java | 3 --- .../java/ru/soune/nocopy/service/FileSimilarityService.java | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 899287a..6a0e273 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -187,9 +187,6 @@ public class ApiController { } else { List results = processNonImageFile(fileEntity, authToken); log.info("results: {}", results); - log.info("results: {}", results); - log.info("results: {}", results); - log.info("results: {}", results); response = buildSuccessResponse(results, MessageCode.SIMILAR_FILES_FOUND); } diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 8a1261a..3b036ee 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -253,6 +253,7 @@ public class FileSimilarityService { .fileSize(fileEntity.getFileSize()) .uploadDate(fileEntity.getCreatedAt()) .status(fileEntity.getProtectionStatus()) + .supportId(Long.valueOf(fileEntity.getSupportId())) .build(); } } From 2966d9f3a97e7b73d6bd49af7db7024b6c4091e2 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 2 Mar 2026 15:44:46 +0700 Subject: [PATCH 116/274] dev add monitoring basic --- .../ru/soune/nocopy/service/file/FileEntityService.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index bb5a9c8..5120647 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -374,12 +374,12 @@ public class FileEntityService { private FileEntityResponse convertToResponse(FileEntity fileEntity, int version) { boolean existsOnDisk = checkFileExistsOnDisk(fileEntity.getFilePath()); User user = userRepository.findById(fileEntity.getUserId()).get(); + String[] splitName = fileEntity.getOriginalFileName().split("."); return FileEntityResponse.builder() .id(fileEntity.getId()) .userId(fileEntity.getUserId()) - .originalFileName(fileEntity.getOriginalFileName().replace("." + - fileEntity.getFileExtension(), "") + "." + fileEntity.getFileExtension()) + .originalFileName(splitName[0] + "_nocopy_protected" + "." + fileEntity.getFileExtension()) .storedFileName(fileEntity.getStoredFileName()) .filePath(fileEntity.getProtectedFilePath()) .fileSize(fileEntity.getFileSize()) @@ -395,9 +395,7 @@ public class FileEntityService { .existsOnDisk(existsOnDisk) .supportId(fileEntity.getSupportId()) .protectStatus(fileEntity.getProtectionStatus().toString()) - .fileName(fileEntity.getOriginalFileName().replace("." + - fileEntity.getFileExtension(), "") + "_nocopy_protected" + - "." + fileEntity.getFileExtension()) + .fileName(splitName[0] + "_nocopy_protected" + "." + fileEntity.getFileExtension()) .ownerName(user.getFullName()) .ownerEmail(user.getEmail()) .ownerCompany(user.getCompanyName()) From 597b46ccffe646dd132c32970c253b8194960cf1 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 2 Mar 2026 15:52:17 +0700 Subject: [PATCH 117/274] dev add monitoring basic --- .../soune/nocopy/service/file/FileEntityService.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index 5120647..8cf481c 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -374,12 +374,18 @@ public class FileEntityService { private FileEntityResponse convertToResponse(FileEntity fileEntity, int version) { boolean existsOnDisk = checkFileExistsOnDisk(fileEntity.getFilePath()); User user = userRepository.findById(fileEntity.getUserId()).get(); - String[] splitName = fileEntity.getOriginalFileName().split("."); + String fileName = fileEntity.getOriginalFileName(); + String name = fileName; + + int lastDotIndex = fileName.lastIndexOf("."); + if (lastDotIndex > 0) { + name = fileName.substring(0, lastDotIndex); + } return FileEntityResponse.builder() .id(fileEntity.getId()) .userId(fileEntity.getUserId()) - .originalFileName(splitName[0] + "_nocopy_protected" + "." + fileEntity.getFileExtension()) + .originalFileName(name + "_nocopy_protected" + "." + fileEntity.getFileExtension()) .storedFileName(fileEntity.getStoredFileName()) .filePath(fileEntity.getProtectedFilePath()) .fileSize(fileEntity.getFileSize()) @@ -395,7 +401,7 @@ public class FileEntityService { .existsOnDisk(existsOnDisk) .supportId(fileEntity.getSupportId()) .protectStatus(fileEntity.getProtectionStatus().toString()) - .fileName(splitName[0] + "_nocopy_protected" + "." + fileEntity.getFileExtension()) + .fileName(name + "_nocopy_protected" + "." + fileEntity.getFileExtension()) .ownerName(user.getFullName()) .ownerEmail(user.getEmail()) .ownerCompany(user.getCompanyName()) From b0804938bd5ebe0006f303f740bba14e332440e6 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 2 Mar 2026 15:57:58 +0700 Subject: [PATCH 118/274] dev add monitoring basic --- .../java/ru/soune/nocopy/service/file/FileEntityService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index 8cf481c..92562da 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -385,7 +385,7 @@ public class FileEntityService { return FileEntityResponse.builder() .id(fileEntity.getId()) .userId(fileEntity.getUserId()) - .originalFileName(name + "_nocopy_protected" + "." + fileEntity.getFileExtension()) + .originalFileName(name + "." + fileEntity.getFileExtension()) .storedFileName(fileEntity.getStoredFileName()) .filePath(fileEntity.getProtectedFilePath()) .fileSize(fileEntity.getFileSize()) From 1124cddb5a8cdc72e592755112e3f2af9cdfa7fa Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 2 Mar 2026 16:15:32 +0700 Subject: [PATCH 119/274] dev add monitoring basic --- src/main/java/ru/soune/nocopy/controller/ApiController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 6a0e273..415da10 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -211,7 +211,7 @@ public class ApiController { return results; } - File file = new File(fileEntity.getProtectedFilePath()); + File file = new File(fileEntity.getFilePath()); FileProtector.Type type = "document".equals(fileEntity.getMimeType()) ? FileProtector.Type.DOC : FileProtector.Type.AUDIO; From 404e1a6bee07d0b340dae953a550e6782427a2a1 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 3 Mar 2026 11:38:22 +0700 Subject: [PATCH 120/274] dev add monitoring basic --- .../java/ru/soune/nocopy/controller/ApiController.java | 8 +++++++- src/main/java/ru/soune/nocopy/dto/MessageCode.java | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 415da10..8c47397 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -192,9 +192,15 @@ public class ApiController { return ResponseEntity.ok(response); + } catch(IllegalArgumentException e) { + log.error("Error with file extensions : {}", e.getMessage(), e); + return ResponseEntity.ok().body(new BaseResponse(MessageCode.FILE_FOR_SEARCH_NOT_VALID.getCode(), + MessageCode.FILE_FOR_SEARCH_NOT_VALID.getCode(), + MessageCode.FILE_FOR_SEARCH_NOT_VALID.getDescription(), null)); } catch (Exception e) { log.error("Error finding similar files: {}", e.getMessage(), e); - return ResponseEntity.ok().body(new BaseResponse(0, MessageCode.INVALID_FIELD.getCode(), + return ResponseEntity.ok().body(new BaseResponse(MessageCode.INVALID_FIELD.getCode(), + MessageCode.INVALID_FIELD.getCode(), MessageCode.INVALID_FIELD.getDescription(), null)); } } diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index 74d1df9..af4360c 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -49,6 +49,7 @@ public enum MessageCode { COMPANY_ALREADY_EXISTS(2, "Company already exists"), USER_LIMIT_IS_OVER(2, "Over user limits"), ERROR_TARIFF_INFO(2, "Erorr with tariff info"), + FILE_FOR_SEARCH_NOT_VALID(2, "File for search unsupported"), USER_NOT_ACTIVE(2, "User not active"); private final Integer code; From 38e7a382c7dd2270fbfab3d0e3d99046debf6d8d Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 3 Mar 2026 15:43:47 +0700 Subject: [PATCH 121/274] dev add monitoring basic --- .../nocopy/configuration/HandlerConfig.java | 2 + .../ru/soune/nocopy/dto/cost/CostRequest.java | 18 ++++ .../soune/nocopy/dto/cost/CostResponse.java | 16 ++++ .../ru/soune/nocopy/handler/CostHandler.java | 86 +++++++++++++++++++ .../nocopy/service/cost/CostService.java | 29 +++++++ .../file/impl/FileUploadServiceImpl.java | 24 +----- 6 files changed, 155 insertions(+), 20 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/cost/CostRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/cost/CostResponse.java create mode 100644 src/main/java/ru/soune/nocopy/handler/CostHandler.java create mode 100644 src/main/java/ru/soune/nocopy/service/cost/CostService.java diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index 40361d4..386bb20 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -28,6 +28,7 @@ public class HandlerConfig { DaDataHandler daDataHandler, PaymentHandler paymentHandler, GetViolationsHandler getViolationsHandler, + CostHandler costHandler, MonitoringHandler monitoringHandler ) { Map map = new HashMap<>(); @@ -48,6 +49,7 @@ public class HandlerConfig { map.put(30005, paymentHandler); map.put(30006, getViolationsHandler); map.put(30007, monitoringHandler); + map.put(30008, costHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/dto/cost/CostRequest.java b/src/main/java/ru/soune/nocopy/dto/cost/CostRequest.java new file mode 100644 index 0000000..66a3b17 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/cost/CostRequest.java @@ -0,0 +1,18 @@ +package ru.soune.nocopy.dto.cost; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class CostRequest { + String action; + + @JsonProperty("file_type") + String fileType; + + @JsonProperty("count_files") + Integer countFilesForProtect; + + @JsonProperty("auth_token") + String authToken; +} diff --git a/src/main/java/ru/soune/nocopy/dto/cost/CostResponse.java b/src/main/java/ru/soune/nocopy/dto/cost/CostResponse.java new file mode 100644 index 0000000..68b3058 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/cost/CostResponse.java @@ -0,0 +1,16 @@ +package ru.soune.nocopy.dto.cost; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class CostResponse { + private int cost; + + private Boolean success; + + @JsonProperty("tokens_count") + private int needTokensCount; +} diff --git a/src/main/java/ru/soune/nocopy/handler/CostHandler.java b/src/main/java/ru/soune/nocopy/handler/CostHandler.java new file mode 100644 index 0000000..b6b130f --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/CostHandler.java @@ -0,0 +1,86 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.cost.CostRequest; +import ru.soune.nocopy.dto.cost.CostResponse; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.repository.AuthTokenRepository; +import ru.soune.nocopy.service.cost.CostService; + +import java.util.Optional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CostHandler implements RequestHandler { + + private final CostService costService; + + private final AuthTokenRepository authTokenRepository; + + private final ObjectMapper objectMapper; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + CostRequest costRequest = objectMapper.convertValue(request.getMessageBody(), CostRequest.class); + String action = costRequest.getAction(); + + return switch (action) { + case "costFileProtect" -> handleFileProtectCost(costRequest, request); + case "checkTokensForProtect" -> handleCheckTokensForFilesProtect(costRequest, request); + default -> new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_ACTION.getCode(), + "Invalid action: " + action, + null); + }; + } + + private BaseResponse handleFileProtectCost(CostRequest costRequest, BaseRequest request) { + int cost = costService.protectFileCost(costRequest.getFileType()); + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + CostResponse.builder() + .cost(cost) + .build()); + } + + private BaseResponse handleCheckTokensForFilesProtect(CostRequest costRequest, BaseRequest request) { + String authToken = costRequest.getAuthToken(); + Optional token = authTokenRepository.findByToken(authToken); + + if (token.isEmpty()) { + return new BaseResponse( + request.getMsgId(), + MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), + MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), + null); + } + + User user = token.get().getUser(); + Integer tokens = user.getActiveTariffInfo().getTokens(); + + int cost = costService.protectFileCost(costRequest.getFileType()); + int count = costRequest.getCountFilesForProtect(); + int totalCost = count * cost; + boolean success = totalCost <= tokens; + int needToken = success ? 0: tokens - totalCost; + + CostResponse costResponse = CostResponse.builder() + .cost(totalCost) + .success(totalCost <= tokens) + .needTokensCount(Math.abs(needToken)) + .build(); + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), costResponse); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/cost/CostService.java b/src/main/java/ru/soune/nocopy/service/cost/CostService.java new file mode 100644 index 0000000..782f89f --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/cost/CostService.java @@ -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; + } + + return cost; + } +} diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index cfcba2c..f0a8050 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -23,9 +23,9 @@ 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.tariff.TariffConstants; import ru.soune.nocopy.service.tariff.TariffInfoService; import ru.soune.nocopy.util.FileUtil; @@ -75,6 +75,8 @@ public class FileUploadServiceImpl implements FileUploadService { @Autowired private FileEntityService fileEntityService; + private final CostService costService; + @Lazy private final NoCopyFileService noCopyFileService; @@ -374,7 +376,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); @@ -494,24 +496,6 @@ 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; - } - - return cost; - } - private String assembleFileSynchronously(FileUploadSession session) throws IOException { Path finalFilePath = prepareFinalFile(session); log.info("file path: " + finalFilePath); From c7ded41c1605a1fbe49615731a6b236342c5d1e0 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 3 Mar 2026 17:51:51 +0700 Subject: [PATCH 122/274] dev add monitoring basic --- src/main/java/ru/soune/nocopy/controller/PaymentController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/ru/soune/nocopy/controller/PaymentController.java b/src/main/java/ru/soune/nocopy/controller/PaymentController.java index 42faa55..97658da 100644 --- a/src/main/java/ru/soune/nocopy/controller/PaymentController.java +++ b/src/main/java/ru/soune/nocopy/controller/PaymentController.java @@ -47,6 +47,7 @@ public class PaymentController { public void handleYooKassaNotification(@RequestBody Map notification) { try { paymentService.handlePaymentNotification(notification); + log.info("Notification from yookassa: " + notification); } catch (PaymentNotFoundException e) { log.error("Error processing payment notification", e); } From b8a0e6cce794053c63e670bc5dc5d2f5158049d5 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 4 Mar 2026 12:10:27 +0700 Subject: [PATCH 123/274] dev add monitoring basic --- .../ru/soune/nocopy/repository/ImageSimilarityRepository.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java index 075da11..5237602 100644 --- a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java @@ -30,6 +30,7 @@ public interface ImageSimilarityRepository JOIN file_entities f ON f.id = h.file_id WHERE ref.file_id = :fileId + AND f.protection_status = "PROTECTED" """, nativeQuery = true) List findCandidates( From f5fee52cb45ea8df3e077cd53fa719338b01781c Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 4 Mar 2026 12:16:53 +0700 Subject: [PATCH 124/274] dev add monitoring basic --- .../ru/soune/nocopy/repository/ImageSimilarityRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java index 5237602..9ba14ba 100644 --- a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java @@ -30,7 +30,7 @@ public interface ImageSimilarityRepository JOIN file_entities f ON f.id = h.file_id WHERE ref.file_id = :fileId - AND f.protection_status = "PROTECTED" + AND f.status = 'ACTIVE' """, nativeQuery = true) List findCandidates( From a89c29f76381323fb83722829fc99e919a5e8dbc Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 4 Mar 2026 12:20:19 +0700 Subject: [PATCH 125/274] dev add monitoring basic --- .../ru/soune/nocopy/repository/ImageSimilarityRepository.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java index 9ba14ba..4144760 100644 --- a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java @@ -55,6 +55,7 @@ public interface ImageSimilarityRepository JOIN file_entities f ON f.id = h.file_id WHERE ref.file_id = :fileId AND f.user_id = :userId + AND f.status = 'ACTIVE' """, nativeQuery = true) List findCandidatesFromUserFiles( @@ -80,6 +81,7 @@ public interface ImageSimilarityRepository JOIN file_entities f ON f.id = h.file_id WHERE ref.file_id = :fileId AND f.user_id IN :userIds + AND f.status = 'ACTIVE' """, nativeQuery = true) List findCandidatesFromUserFiles( @@ -105,6 +107,7 @@ public interface ImageSimilarityRepository JOIN file_entities f ON f.id = h.file_id WHERE h.hash64_hi = :hash64Hi AND h.hash64_lo = :hash64Lo + AND f.status = 'ACTIVE' """, nativeQuery = true) List findExactDuplicates( From 34256c072eef6fbab290ab820215f906d105a525 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 4 Mar 2026 21:16:21 +0700 Subject: [PATCH 126/274] dev fix pipline --- infrastructure/jenkins/deploy-backend.groovy | 81 ++++++++++++++++++-- infrastructure/jenkins/deploy-front.groovy | 69 +++++++++++++++++ 2 files changed, 142 insertions(+), 8 deletions(-) diff --git a/infrastructure/jenkins/deploy-backend.groovy b/infrastructure/jenkins/deploy-backend.groovy index c1f96f9..30fc29f 100644 --- a/infrastructure/jenkins/deploy-backend.groovy +++ b/infrastructure/jenkins/deploy-backend.groovy @@ -9,14 +9,24 @@ pipeline { ) string( name: 'PORT', - defaultValue: '3000', + defaultValue: '3001', description: 'Порт для запуска экземпляра' ) + string( + name: 'PORT_DB', + defaultValue: '', + description: 'Порт для запуска БД' + ) string( name: 'SERVER', defaultValue: '92.242.61.23', description: 'Сервер для деплоя' ) + string( + name: 'NETWORK', + defaultValue: 'app-network-dev', + description: 'Имя сети (будет создана если не существует)' + ) } stages { @@ -36,6 +46,29 @@ pipeline { } } + stage('Copy to server') { + steps { + script { + withCredentials([ + usernamePassword( + credentialsId: 'server-root-password', + usernameVariable: 'SSH_USER', + passwordVariable: 'SSH_PASS' + ) + ]) { + sh """ + sshpass -p '${SSH_PASS}' ssh -o StrictHostKeyChecking=no ${SSH_USER}@${params.SERVER} " + rm -rf /opt/deployments/${params.BRANCH}-${params.PORT}/* + mkdir -p /opt/deployments/${params.BRANCH}-${params.PORT} + " + + sshpass -p '${SSH_PASS}' scp -r ./* ${SSH_USER}@${params.SERVER}:/opt/deployments/${params.BRANCH}-${params.PORT}/ + """ + } + } + } + } + stage('Deploy with docker-compose') { steps { script { @@ -61,10 +94,16 @@ pipeline { # Основная команда sshpass -p '${SSH_PASS}' ssh -o StrictHostKeyChecking=no ${SSH_USER}@${params.SERVER} " + mkdir -p /opt/deployments/${params.BRANCH}-${params.PORT} cd /opt/deployments/${params.BRANCH}-${params.PORT} - echo '1. Создаем изолированную сеть для порта ${params.PORT}...' - docker network create app-network-${params.PORT} 2>/dev/null || echo 'Сеть уже существует' + echo '1. Проверяем/создаем сеть ${params.NETWORK}...' + if ! docker network ls --format '{{.Name}}' | grep -q '^${params.NETWORK}\$'; then + docker network create ${params.NETWORK} + echo 'Сеть ${params.NETWORK} создана' + else + echo 'Сеть ${params.NETWORK} уже существует' + fi echo '2. Останавливаем старые контейнеры порта ${params.PORT}...' docker stop app-backend-${params.PORT} 2>/dev/null || echo 'Контейнер не найден' @@ -77,12 +116,12 @@ pipeline { echo '3. Запускаем БД...' docker run -d \\ --name postgres-${params.PORT} \\ - --network app-network-${params.PORT} \\ + --network ${params.NETWORK} \\ --network-alias no_copy_${params.PORT} \\ --network-alias database-${params.PORT} \\ --network-alias db-${params.PORT} \\ -p ${params.PORT}0:5432 \\ - -e POSTGRES_DB=no_copy_${params.PORT} \\ + -e POSTGRES_DB=no_copy${params.PORT_DB} \\ -e POSTGRES_USER=${DB_USER} \\ -e POSTGRES_PASSWORD=${DB_PASSWORD} \\ -v pgdata_${params.PORT}:/var/lib/postgresql/data \\ @@ -92,7 +131,7 @@ pipeline { echo '4. Запускаем storage...' docker run -d \\ --name file-storage-${params.PORT} \\ - --network app-network-${params.PORT} \\ + --network ${params.NETWORK} \\ -v uploads_data_${params.PORT}:/storage:rw \\ alpine:latest tail -f /dev/null @@ -105,10 +144,12 @@ pipeline { echo '7. Запускаем приложение...' docker run -d \\ --name app-backend-${params.PORT} \\ - --network app-network-${params.PORT} \\ + --network ${params.NETWORK} \\ + --network-alias app \\ + --network-alias app-${params.PORT} \\ -p ${params.PORT}:8080 \\ -v uploads_data_${params.PORT}:/data/uploads:rw \\ - -e POSTGRES_DB=no_copy_${params.PORT} \\ + -e POSTGRES_DB=no_copy${params.PORT_DB} \\ -e POSTGRES_USER=${DB_USER} \\ -e POSTGRES_PASSWORD=${DB_PASSWORD} \\ -e POSTGRES_PORT=5432 \\ @@ -131,6 +172,30 @@ pipeline { } } } + + stage('Verify network') { + steps { + script { + withCredentials([ + usernamePassword( + credentialsId: 'server-root-password', + usernameVariable: 'SSH_USER', + passwordVariable: 'SSH_PASS' + ) + ]) { + sh """ + sshpass -p '${SSH_PASS}' ssh -o StrictHostKeyChecking=no ${SSH_USER}@${params.SERVER} " + echo 'Проверяем подключение к сети ${params.NETWORK}' + docker network inspect ${params.NETWORK} --format='{{.Name}}: {{range .Containers}}{{.Name}} {{end}}' + + echo 'Проверяем алиас app для контейнера app-backend-${params.PORT}' + docker inspect app-backend-${params.PORT} | grep -A 5 'Aliases' + " + """ + } + } + } + } } post { diff --git a/infrastructure/jenkins/deploy-front.groovy b/infrastructure/jenkins/deploy-front.groovy index 5758335..2f4f52b 100644 --- a/infrastructure/jenkins/deploy-front.groovy +++ b/infrastructure/jenkins/deploy-front.groovy @@ -12,6 +12,11 @@ pipeline { defaultValue: '2998', description: 'Порт для запуска экземпляра' ) + string( + name: 'NETWORK', + defaultValue: 'app-network-dev', + description: 'Docker сеть для подключения' + ) } stages { @@ -189,6 +194,70 @@ pipeline { } } } + +// stage('Connect to network') { +// steps { +// script { +// withCredentials([ +// usernamePassword( +// credentialsId: 'server-root-password', +// usernameVariable: 'SSH_USER', +// passwordVariable: 'SSH_PASS' +// ) +// ]) { +// sh """ +// sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 " +// echo 'Отключаем от всех сетей и подключаем к ${params.NETWORK}' +// +// # Отключаем от всех сетей (кроме bridge, host, none) +// current_networks=\$(docker inspect no-copy-frontend-${params.PORT} --format='{{range \$net, \$v := .NetworkSettings.Networks}}{{\$net}}{{\"\\n\"}}{{end}}' | grep -v -E '^(bridge|host|none)\$' || true) +// +// for net in \$current_networks; do +// echo \"Отключаем от сети: \$net\" +// docker network disconnect \$net no-copy-frontend-${params.PORT} 2>/dev/null || true +// done +// +// # Подключаем к нужной сети +// docker network connect ${params.NETWORK} no-copy-frontend-${params.PORT} +// +// echo 'Готово! Контейнер теперь в сети:' +// docker inspect no-copy-frontend-${params.PORT} --format='{{range \$net, \$v := .NetworkSettings.Networks}}{{\$net}}{{\"\\n\"}}{{end}}' +// " +// """ +// } +// } +// } +// } + + stage('Connect to network') { + steps { + script { + withCredentials([ + usernamePassword( + credentialsId: 'server-root-password', + usernameVariable: 'SSH_USER', + passwordVariable: 'SSH_PASS' + ) + ]) { + sh """ + sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 " + echo 'Подключаем контейнер к сети ${params.NETWORK}' + + if ! docker network ls --format '{{.Name}}' | grep -q '^${params.NETWORK}\$'; then + docker network create ${params.NETWORK} + fi + + if docker ps -a --format '{{.Names}}' | grep -q '^no-copy-frontend-${params.PORT}\$'; then + docker network disconnect ${params.NETWORK} no-copy-frontend-${params.PORT} 2>/dev/null || true + docker network connect ${params.NETWORK} no-copy-frontend-${params.PORT} + echo 'Контейнер подключен к ${params.NETWORK}' + fi + " + """ + } + } + } + } } post { From 89478c1c9af980b580d2aa5e9c5a1d0d944bcc22 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 5 Mar 2026 09:37:35 +0700 Subject: [PATCH 127/274] dev fix pipline --- README.md | 156 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 155 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 529ee1d..70ba5f0 100644 --- a/README.md +++ b/README.md @@ -168,4 +168,158 @@ WHERE f.support_id IS NULL UPDATE file_entities SET support_id = numbered.new_support_id FROM numbered -WHERE file_entities.id = numbered.id; \ No newline at end of file +WHERE file_entities.id = numbered.id; + +--------------------------------- +НАСТРОЙКА NGINX +-------------------------------- + + +server { +listen 80; +listen [::]:80; +server_name admin.not-copy.ru; + + location /api/ { + proxy_pass http://localhost:3003; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://localhost:2995; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +server { +listen 80; +listen [::]:80; +server_name workspace.not-copy.ru; + + location /api/ { + proxy_pass http://localhost:3003; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://localhost:2998; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +server { +listen 80; +listen [::]:80; +server_name lp.not-copy.ru; + + location /api/ { + proxy_pass http://localhost:3003; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://localhost:2993; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +server { +listen 80; +listen [::]:80; +server_name dev-admin.not-copy.ru; + + location /api/ { + proxy_pass http://localhost:3001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://localhost:2996; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +server { +listen 80; +listen [::]:80; +server_name dev-workspace.not-copy.ru 92.242.61.23; + + location /api/ { + proxy_pass http://localhost:3001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://localhost:3002; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +server { +listen 80; +listen [::]:80; +server_name dev-lp.not-copy.ru; + + location /api/ { + proxy_pass http://localhost:3001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://localhost:2994; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +# Проверяем конфиг +ls -la /etc/nginx/sites-available/ +ls -la /etc/nginx/sites-enabled/ + +# Создание конфига + +sudo nano /etc/nginx/sites-available/not-copy + +# Создание симлинк +sudo ln -s /etc/nginx/sites-available/not-copy /etc/nginx/sites-enabled/ + +# Чек +sudo nginx -t + +# Рестарт +sudo systemctl reload nginx \ No newline at end of file From c9921c6faca28597ce3f2002920ede5cb5345a24 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 5 Mar 2026 10:05:18 +0700 Subject: [PATCH 128/274] dev add check for file check --- .../java/ru/soune/nocopy/dto/MessageCode.java | 2 ++ .../soune/nocopy/dto/cost/CostResponse.java | 6 ++++++ .../ru/soune/nocopy/handler/CostHandler.java | 21 +++++++++++++++++++ .../nocopy/service/cost/CostService.java | 4 ++-- 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index af4360c..0261972 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -50,6 +50,8 @@ public enum MessageCode { USER_LIMIT_IS_OVER(2, "Over user limits"), ERROR_TARIFF_INFO(2, "Erorr with tariff info"), FILE_FOR_SEARCH_NOT_VALID(2, "File for search unsupported"), + NOT_VALID_FILE_TYPE_OR_COUNT_FILE(2, "Cost for file type not found, count is negative or files count" + + "more than max valid"), USER_NOT_ACTIVE(2, "User not active"); private final Integer code; diff --git a/src/main/java/ru/soune/nocopy/dto/cost/CostResponse.java b/src/main/java/ru/soune/nocopy/dto/cost/CostResponse.java index 68b3058..7285870 100644 --- a/src/main/java/ru/soune/nocopy/dto/cost/CostResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/cost/CostResponse.java @@ -11,6 +11,12 @@ public class CostResponse { private Boolean success; + @JsonProperty("max_files_for_check") + private Integer maxFilesForCheck; + + @JsonProperty("count_file") + private Integer countFile; + @JsonProperty("tokens_count") private int needTokensCount; } diff --git a/src/main/java/ru/soune/nocopy/handler/CostHandler.java b/src/main/java/ru/soune/nocopy/handler/CostHandler.java index b6b130f..80234c6 100644 --- a/src/main/java/ru/soune/nocopy/handler/CostHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/CostHandler.java @@ -27,6 +27,8 @@ public class CostHandler implements RequestHandler { private final ObjectMapper objectMapper; + private final Integer MAX_FILES_FOR_CHECK = 1000; + @Override public BaseResponse handle(BaseRequest request) throws Exception { CostRequest costRequest = objectMapper.convertValue(request.getMessageBody(), CostRequest.class); @@ -46,6 +48,14 @@ public class CostHandler implements RequestHandler { private BaseResponse handleFileProtectCost(CostRequest costRequest, BaseRequest request) { int cost = costService.protectFileCost(costRequest.getFileType()); + if (cost == 0) { + return new BaseResponse(request.getMsgId(), MessageCode.NOT_VALID_FILE_TYPE_OR_COUNT_FILE.getCode(), + MessageCode.NOT_VALID_FILE_TYPE_OR_COUNT_FILE.getDescription(), + CostResponse.builder() + .cost(cost) + .build()); + } + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), MessageCode.SUCCESS.getDescription(), CostResponse.builder() @@ -70,6 +80,17 @@ public class CostHandler implements RequestHandler { int cost = costService.protectFileCost(costRequest.getFileType()); int count = costRequest.getCountFilesForProtect(); + + if (cost == 0 || count <= 0 || count > MAX_FILES_FOR_CHECK) { + return new BaseResponse(request.getMsgId(), MessageCode.NOT_VALID_FILE_TYPE_OR_COUNT_FILE.getCode(), + MessageCode.NOT_VALID_FILE_TYPE_OR_COUNT_FILE.getDescription(), + CostResponse.builder() + .cost(cost) + .countFile(count) + .maxFilesForCheck(MAX_FILES_FOR_CHECK) + .build()); + } + int totalCost = count * cost; boolean success = totalCost <= tokens; int needToken = success ? 0: tokens - totalCost; diff --git a/src/main/java/ru/soune/nocopy/service/cost/CostService.java b/src/main/java/ru/soune/nocopy/service/cost/CostService.java index 782f89f..536aef9 100644 --- a/src/main/java/ru/soune/nocopy/service/cost/CostService.java +++ b/src/main/java/ru/soune/nocopy/service/cost/CostService.java @@ -22,8 +22,8 @@ public class CostService { return TariffConstants.AUDIO_TOKEN_VALUE; case "document": return TariffConstants.PDF_TOKEN_VALUE; + default: + return cost; } - - return cost; } } From 5c27fd52bafcb3b8c06c727afe5f3bdba71bfcd5 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 5 Mar 2026 10:26:53 +0700 Subject: [PATCH 129/274] dev add check for file check --- src/main/java/ru/soune/nocopy/service/mail/EmailService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java index 763a009..eee6e16 100644 --- a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java +++ b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java @@ -118,7 +118,7 @@ public class EmailService { String htmlContent = loadAndProcessTemplate(user.getFullName(), code, "templates/reset-password.html"); //TODO frontPort убрать после настройки редиректа nginx - String resetLink = escapeHtml(baseUrl + frontPort + "/reset-password?email=" + user.getEmail()); + String resetLink = escapeHtml(baseUrl + "/reset-password?email=" + user.getEmail()); htmlContent = htmlContent.replace("{{resetLink}}", resetLink); helper.setText(htmlContent, true); From 88e71212130abbfbf13817169feb98af86d90f32 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 5 Mar 2026 12:11:09 +0700 Subject: [PATCH 130/274] dev add check tokens for monitoring --- .../java/ru/soune/nocopy/dto/MessageCode.java | 1 + .../monitoring/MonitoringStatusResponse.java | 3 ++ .../dto/monitoring/SetMonitoringRequest.java | 2 +- .../nocopy/handler/MonitoringHandler.java | 46 +++++++++++-------- .../monitoring/FileMonitoringService.java | 20 +++++++- 5 files changed, 51 insertions(+), 21 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index 0261972..4384659 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -48,6 +48,7 @@ public enum MessageCode { PAYMENT_NOT_FOUND(4, "Payment not found"), COMPANY_ALREADY_EXISTS(2, "Company already exists"), USER_LIMIT_IS_OVER(2, "Over user limits"), + MONITORING_TYPE_NOT_FOUND(4, "Monitoring type not found"), ERROR_TARIFF_INFO(2, "Erorr with tariff info"), FILE_FOR_SEARCH_NOT_VALID(2, "File for search unsupported"), NOT_VALID_FILE_TYPE_OR_COUNT_FILE(2, "Cost for file type not found, count is negative or files count" + diff --git a/src/main/java/ru/soune/nocopy/dto/monitoring/MonitoringStatusResponse.java b/src/main/java/ru/soune/nocopy/dto/monitoring/MonitoringStatusResponse.java index b9b832a..e8d8a87 100644 --- a/src/main/java/ru/soune/nocopy/dto/monitoring/MonitoringStatusResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/monitoring/MonitoringStatusResponse.java @@ -37,4 +37,7 @@ public class MonitoringStatusResponse { @JsonProperty("new_violations_count") private long newViolationsCount; + + @JsonProperty("need_add_tokens") + private boolean needAddTokens; } diff --git a/src/main/java/ru/soune/nocopy/dto/monitoring/SetMonitoringRequest.java b/src/main/java/ru/soune/nocopy/dto/monitoring/SetMonitoringRequest.java index 4694f65..bf49b47 100644 --- a/src/main/java/ru/soune/nocopy/dto/monitoring/SetMonitoringRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/monitoring/SetMonitoringRequest.java @@ -13,7 +13,7 @@ public class SetMonitoringRequest { private String fileId; @JsonProperty("monitoring_type") - private MonitoringType monitoringType; + private String monitoringType; @JsonProperty("auth_token") private String authToken; diff --git a/src/main/java/ru/soune/nocopy/handler/MonitoringHandler.java b/src/main/java/ru/soune/nocopy/handler/MonitoringHandler.java index 77dee1b..876c71e 100644 --- a/src/main/java/ru/soune/nocopy/handler/MonitoringHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/MonitoringHandler.java @@ -10,14 +10,14 @@ import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.monitoring.MonitoringStatusResponse; import ru.soune.nocopy.dto.monitoring.SetMonitoringRequest; import ru.soune.nocopy.entity.monitoring.MonitoringType; -import ru.soune.nocopy.entity.tarif.TariffType; import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.user.User; -import ru.soune.nocopy.repository.TariffRepository; +import ru.soune.nocopy.exception.UserNotHavePermission; import ru.soune.nocopy.service.monitoring.FileMonitoringService; import ru.soune.nocopy.service.register.AuthService; -import ru.soune.nocopy.service.tariff.TariffService; +import java.io.FileNotFoundException; +import java.util.Arrays; import java.util.Map; @Slf4j @@ -31,10 +31,6 @@ public class MonitoringHandler implements RequestHandler { private final AuthService authService; - private final TariffService tariffService; - - private final TariffRepository tariffRepository; - @Override public BaseResponse handle(BaseRequest request) throws Exception { SetMonitoringRequest setRequest = objectMapper.convertValue( @@ -51,21 +47,33 @@ public class MonitoringHandler implements RequestHandler { User user = userAuthToken.getUser(); Long userId = user.getId(); - MonitoringType monitoringType = setRequest.getMonitoringType(); + String typeStr = setRequest.getMonitoringType(); + MonitoringType monitoringType; - monitoringService.setMonitoring(userId, setRequest.getFileId(), monitoringType); + try { + monitoringType = MonitoringType.valueOf(typeStr); + } catch (IllegalArgumentException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.MONITORING_TYPE_NOT_FOUND.getCode(), + MessageCode.MONITORING_TYPE_NOT_FOUND.getDescription(), + Map.of("monitoring", Arrays.asList(MonitoringType.values()))); + } + + try { + monitoringService.setMonitoring(userId, setRequest.getFileId(), monitoringType); + } catch (UserNotHavePermission e) { + return new BaseResponse(request.getMsgId(), + MessageCode.USER_NOT_HAD_PERMISSION.getCode(), + MessageCode.USER_NOT_HAD_PERMISSION.getDescription(), Map.of("token", authToken)); + } catch (FileNotFoundException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.FILE_NOT_FOUND.getCode(), + MessageCode.FILE_NOT_FOUND.getDescription(), Map.of("fileId", setRequest.getFileId())); + } MonitoringStatusResponse status = monitoringService.getMonitoringStatus(setRequest.getFileId(), userId); - return new BaseResponse( - request.getMsgId(), - MessageCode.SUCCESS.getCode(), - "Monitoring settings updated", - status); - } - //TODO check tokens for apply - private boolean checkTokensForMonitoring(User user, MonitoringType monitoringType) { - tariffService.getTariffByType(TariffType.valueOf(monitoringType.name())); - return true; + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + "Monitoring settings updated", status); } } diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java b/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java index 4a2d1e7..45453cf 100644 --- a/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java +++ b/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java @@ -5,13 +5,19 @@ 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.FoundViolationRepository; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.service.tariff.TariffService; import java.io.FileNotFoundException; import java.time.LocalDateTime; @@ -27,6 +33,10 @@ public class FileMonitoringService { private final FoundViolationRepository violationRepository; + 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) @@ -82,7 +92,7 @@ public class FileMonitoringService { 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(); long newViolations = violationRepository.countNewByUserId(userId); return MonitoringStatusResponse.builder() @@ -94,7 +104,15 @@ public class FileMonitoringService { .lastRunStatus(monitoring != null ? monitoring.getLastRunStatus() : null) .isActive(monitoring != null && monitoring.isActive()) .newViolationsCount(newViolations) + .needAddTokens(checkTokensForMonitoring(user, monitoring.getMonitoringType())) .build(); } + public boolean checkTokensForMonitoring(User user, MonitoringType monitoringType) { + TariffDTO tariff = tariffService.getTariffByType(TariffType.valueOf(monitoringType.name())); + TariffInfo activeTariffInfo = user.getActiveTariffInfo(); + int currentUserCountTokens = activeTariffInfo.getTokens() + activeTariffInfo.getBoughtTokens(); + + return currentUserCountTokens > tariff.getTokens(); + } } From 3ed38dacd18090604f6f6352ba584996531368c9 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 5 Mar 2026 14:27:14 +0700 Subject: [PATCH 131/274] dev add check tokens for monitoring --- .../handler/ImageFoundRequestHandler.java | 8 +- .../nocopy/scheduler/MonitoringScheduler.java | 1 + .../nocopy/service/mail/EmailService.java | 41 ++++- .../monitoring/MonitoringSearchService.java | 35 +++- .../notification/NotificationService.java | 12 ++ .../service/tariff/TariffInfoService.java | 6 +- src/main/resources/application.yaml | 2 +- .../resources/templates/token-not-found.html | 171 ++++++++++++++++++ 8 files changed, 262 insertions(+), 14 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/service/notification/NotificationService.java create mode 100644 src/main/resources/templates/token-not-found.html diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index bf0194b..81182da 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -90,10 +90,10 @@ public class ImageFoundRequestHandler implements RequestHandler { allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); log.info("allUniqueImages OK: {} images", allUniqueImages.size()); - if (!allUniqueImages.isEmpty()) { - tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); - checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); - } +// if (!allUniqueImages.isEmpty()) { + tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); + checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); +// } int page = imageSearchRequest.getPage() != null ? imageSearchRequest.getPage() : 1; int pageSize = 5; diff --git a/src/main/java/ru/soune/nocopy/scheduler/MonitoringScheduler.java b/src/main/java/ru/soune/nocopy/scheduler/MonitoringScheduler.java index 442bcbe..4a92b57 100644 --- a/src/main/java/ru/soune/nocopy/scheduler/MonitoringScheduler.java +++ b/src/main/java/ru/soune/nocopy/scheduler/MonitoringScheduler.java @@ -9,6 +9,7 @@ import org.springframework.transaction.annotation.Transactional; import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; import ru.soune.nocopy.repository.FileMonitoringRepository; import ru.soune.nocopy.service.monitoring.MonitoringSearchService; +import ru.soune.nocopy.service.tariff.TariffInfoService; import java.time.LocalDateTime; import java.util.List; diff --git a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java index eee6e16..c34212d 100644 --- a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java +++ b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.time.LocalDateTime; +import java.util.Map; @Service @RequiredArgsConstructor @@ -47,20 +48,34 @@ public class EmailService { @Value("${server.front.port}") private String frontPort; - public void sendVerificationEmail(User user, String code) throws MessagingException, IOException { + 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("noreply@no-copy.ru"); - helper.setTo(user.getEmail()); - helper.setSubject(standartSubject); - String htmlContent = loadAndProcessTemplate(user.getFullName(), code, - "templates/email-verification.html"); - 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); + } + @Transactional public EmailVerificationToken createEmailVerificationToken(AuthToken authToken) { EmailVerificationToken existingToken = emailVerificationTokenRepository @@ -96,6 +111,18 @@ public class EmailService { .replace("{{code}}", safeCode); } + private String loadAndProcessTemplate(Map fieldsForTemplate, String templatePath) throws IOException { + ClassPathResource resource = new ClassPathResource(templatePath); + String template = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + + for (Map.Entry entry : fieldsForTemplate.entrySet()) { + template = template.replace("{{" + entry.getKey() + "}}", escapeHtml(entry.getValue())); + } + + return template; + } + + private String escapeHtml(String text) { if (text == null) return ""; return text diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java index 8796bce..9fa44d8 100644 --- a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java @@ -1,19 +1,30 @@ package ru.soune.nocopy.service.monitoring; +import jakarta.mail.MessagingException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.soune.nocopy.dto.file.YandexSearchResponse; +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.FoundViolationEntity; import ru.soune.nocopy.entity.monitoring.MonitoringType; import ru.soune.nocopy.entity.monitoring.SearchEngine; +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.FoundViolationRepository; +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 java.io.IOException; import java.time.LocalDateTime; import java.util.List; @@ -30,8 +41,16 @@ public class MonitoringSearchService { private final FileMonitoringRepository monitoringRepository; + private final TariffInfoService tariffInfoService; + + private final TariffService tariffService; + + private final EmailService emailService; + + private final UserRepository userRepository; + @Transactional - public void processFileSearch(FileMonitoringEntity monitoring) { + public void processFileSearch(FileMonitoringEntity monitoring) throws MessagingException, IOException { String searchSessionId = UUID.randomUUID().toString(); log.info("Starting search session: {} for file: {}", searchSessionId, monitoring.getFile().getId()); @@ -39,7 +58,12 @@ public class MonitoringSearchService { 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()); + String yandexResponse = searchImageService.searchReverseByPublicUrl( monitoring.getFile(), "yandex_reverse_image", "visual_matches"); @@ -62,7 +86,16 @@ public class MonitoringSearchService { } monitoring.setLastRunStatus("SUCCESS - Found " + savedCount + " violations"); + } 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("Search failed for session: {}", searchSessionId, e); monitoring.setLastRunStatus("ERROR: " + e.getMessage()); diff --git a/src/main/java/ru/soune/nocopy/service/notification/NotificationService.java b/src/main/java/ru/soune/nocopy/service/notification/NotificationService.java new file mode 100644 index 0000000..5bb807a --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/notification/NotificationService.java @@ -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 { + +} diff --git a/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java b/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java index ac28249..8e2054c 100644 --- a/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java +++ b/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java @@ -39,7 +39,7 @@ public class TariffInfoService { return tariffInfoRepository.save(tariffInfo); } - + @Transactional public void writeOffTokens(long userId, Integer value) { User user = userRepository.findById(userId).orElseThrow(); @@ -52,6 +52,10 @@ public class TariffInfoService { activeTariffInfo = user.getCompany().getTariffInfo(); } + writeOffTokensForTariff(activeTariffInfo, value); + } + + public void writeOffTokensForTariff(TariffInfo activeTariffInfo, Integer value) { Integer tokens = activeTariffInfo.getTokens(); Integer boughtTokens = activeTariffInfo.getBoughtTokens(); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 9d480a2..9b1d602 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -104,7 +104,7 @@ yandex: search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image} searchapi: - api-key: ${SEARCHAPI_API_KEY:qQ19qn3JeRtqm9aasEgUFvGc} + api-key: ${SEARCHAPI_API_KEY:5jyYZC8jSaxhZTwjMUhwtAXi} reverse-image-url: "https://searchapi.io/api/v1/search" app: diff --git a/src/main/resources/templates/token-not-found.html b/src/main/resources/templates/token-not-found.html new file mode 100644 index 0000000..8cb9674 --- /dev/null +++ b/src/main/resources/templates/token-not-found.html @@ -0,0 +1,171 @@ + + + + + + Недостаточно токенов + + + + + + \ No newline at end of file From 120408e4883c8f088419bb4e4ee3313828201d7f Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 6 Mar 2026 12:03:07 +0700 Subject: [PATCH 132/274] dev add check tokens for monitoring --- .../ru/soune/nocopy/entity/monitoring/FileMonitoringEntity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/entity/monitoring/FileMonitoringEntity.java b/src/main/java/ru/soune/nocopy/entity/monitoring/FileMonitoringEntity.java index 87fea5c..1c94abf 100644 --- a/src/main/java/ru/soune/nocopy/entity/monitoring/FileMonitoringEntity.java +++ b/src/main/java/ru/soune/nocopy/entity/monitoring/FileMonitoringEntity.java @@ -29,7 +29,7 @@ public class FileMonitoringEntity { @Column(name = "monitoring_type", nullable = false) private MonitoringType monitoringType; - @Column(name = "next_run", nullable = false) + @Column(name = "next_run") private LocalDateTime nextRun; @Column(name = "last_run") From 4c3558dcfafc055e0c7c94f20f4b534b3e3360fa Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 6 Mar 2026 12:15:39 +0700 Subject: [PATCH 133/274] dev add check tokens for monitoring --- .../soune/nocopy/dto/file/FileEntityResponse.java | 1 + .../nocopy/service/file/FileEntityService.java | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/main/java/ru/soune/nocopy/dto/file/FileEntityResponse.java b/src/main/java/ru/soune/nocopy/dto/file/FileEntityResponse.java index e57164c..504e8b3 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/FileEntityResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/file/FileEntityResponse.java @@ -40,4 +40,5 @@ public class FileEntityResponse { private String fileFormat; private Integer checksCount; private LocalDateTime fileUploadDate; + private String monitoring; } diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index 92562da..f0425fd 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -10,10 +10,14 @@ 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.handler.MonitoringHandler; import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.repository.FileMonitoringRepository; import ru.soune.nocopy.repository.SimilarImageProjection; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.FileSimilarityService; @@ -28,6 +32,7 @@ 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 @@ -371,11 +376,19 @@ 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 fileMonitoringRepositoryByFileId = + fileMonitoringRepository.findByFileId(fileEntity.getId()); + + String monitoring = fileMonitoringRepositoryByFileId.map(fileMonitoringEntity -> + fileMonitoringEntity.getMonitoringType().name()) + .orElseGet(MonitoringType.NONE::name); int lastDotIndex = fileName.lastIndexOf("."); if (lastDotIndex > 0) { @@ -405,6 +418,7 @@ public class FileEntityService { .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()) From 52bc0e254c2bfa150aebec78ab759f27b3c8813a Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 6 Mar 2026 12:48:15 +0700 Subject: [PATCH 134/274] dev add check tokens for monitoring --- .../nocopy/service/monitoring/FileMonitoringService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java b/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java index 45453cf..6682377 100644 --- a/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java +++ b/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java @@ -109,6 +109,10 @@ public class FileMonitoringService { } 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(); From 31c5d9aaabbc3fba88e1e29b7d0713e4fad723bb Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 6 Mar 2026 17:28:52 +0700 Subject: [PATCH 135/274] dev async global search --- .../configuration/file/AsyncConfig.java | 11 ++ .../controller/GlobalSearchController.java | 140 +++++++++++++++++ .../dto/search/GlobalSearchFileResult.java | 19 +++ .../dto/search/GlobalSearchStartRequest.java | 14 ++ .../dto/search/GlobalSearchStartResponse.java | 12 ++ .../search/GlobalSearchStatusResponse.java | 18 +++ .../entity/search/GlobalSearchResult.java | 32 ++++ .../entity/search/GlobalSearchTask.java | 34 ++++ .../nocopy/entity/search/SearchStatus.java | 5 + .../handler/ImageFoundRequestHandler.java | 2 - .../GlobalSearchResultRepository.java | 13 ++ .../GlobalSearchTaskRepository.java | 13 ++ .../search/GlobalSearchAsyncProcessor.java | 147 ++++++++++++++++++ .../service/search/GlobalSearchService.java | 62 ++++++++ 14 files changed, 520 insertions(+), 2 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java create mode 100644 src/main/java/ru/soune/nocopy/dto/search/GlobalSearchFileResult.java create mode 100644 src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStartRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStartResponse.java create mode 100644 src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatusResponse.java create mode 100644 src/main/java/ru/soune/nocopy/entity/search/GlobalSearchResult.java create mode 100644 src/main/java/ru/soune/nocopy/entity/search/GlobalSearchTask.java create mode 100644 src/main/java/ru/soune/nocopy/entity/search/SearchStatus.java create mode 100644 src/main/java/ru/soune/nocopy/repository/GlobalSearchResultRepository.java create mode 100644 src/main/java/ru/soune/nocopy/repository/GlobalSearchTaskRepository.java create mode 100644 src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java create mode 100644 src/main/java/ru/soune/nocopy/service/search/GlobalSearchService.java diff --git a/src/main/java/ru/soune/nocopy/configuration/file/AsyncConfig.java b/src/main/java/ru/soune/nocopy/configuration/file/AsyncConfig.java index 6882014..d90475e 100644 --- a/src/main/java/ru/soune/nocopy/configuration/file/AsyncConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/file/AsyncConfig.java @@ -21,4 +21,15 @@ public class AsyncConfig { executor.initialize(); return executor; } + + @Bean(name = "taskExecutor") + public Executor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(3); + executor.setMaxPoolSize(5); + executor.setQueueCapacity(50); + executor.setThreadNamePrefix("global-search-"); + executor.initialize(); + return executor; + } } diff --git a/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java b/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java new file mode 100644 index 0000000..cab9462 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java @@ -0,0 +1,140 @@ +package ru.soune.nocopy.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.soune.nocopy.dto.file.YandexSearchResponse; +import ru.soune.nocopy.dto.search.GlobalSearchFileResult; +import ru.soune.nocopy.dto.search.GlobalSearchStartRequest; +import ru.soune.nocopy.dto.search.GlobalSearchStartResponse; +import ru.soune.nocopy.dto.search.GlobalSearchStatusResponse; +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.register.AuthService; +import ru.soune.nocopy.service.search.GlobalSearchService; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@RestController +@RequestMapping("/api/v1/global-search") +@RequiredArgsConstructor +public class GlobalSearchController { + + private final AuthService authService; + + private final GlobalSearchService globalSearchService; + + private final GlobalSearchTaskRepository globalSearchTaskRepository; + + private final GlobalSearchTaskRepository searchTaskRepository; + + private final GlobalSearchResultRepository globalSearchResultRepository; + + private final ObjectMapper objectMapper; + + @PostMapping("/start") + public ResponseEntity startSearch( + @RequestBody GlobalSearchStartRequest request, + @RequestHeader(value = "Authorization", required = false) String tokenHeader) { + + if (tokenHeader == null || tokenHeader.isBlank()) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } + + Long userId = authService.useUserAuthToken(tokenHeader); + + List filesToProcess = globalSearchService.getFilesToProcess(request, userId); + + if (filesToProcess.isEmpty()) return ResponseEntity.ok().body("Files for search not found"); + + String taskId = globalSearchService.startSearch(request, userId, filesToProcess); + + Optional taskOptional = globalSearchTaskRepository.findById(taskId); + + if (taskOptional.isEmpty()) return ResponseEntity.ok().body("Task not found"); + + GlobalSearchTask task = taskOptional.orElseThrow(); + GlobalSearchStartResponse response = new GlobalSearchStartResponse(); + response.setTaskId(taskId); + response.setStatus(SearchStatus.ACCEPTED.name()); + response.setTotalFiles(task.getTotalFiles()); + + return ResponseEntity.ok(response); + } + + @GetMapping("/status/{taskId}") + public ResponseEntity getStatus( + @PathVariable String taskId, + @RequestHeader(value = "Authorization", required = false) String tokenHeader) { + + if (tokenHeader == null || tokenHeader.isBlank()) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } + + Long userId = authService.useUserAuthToken(tokenHeader); + + Optional taskOptional = searchTaskRepository.findByTaskIdAndUserId(taskId, userId); + + if (taskOptional.isEmpty()) return ResponseEntity.ok().body("Task not found"); + + GlobalSearchTask task = taskOptional.orElseThrow(); + + List results = globalSearchResultRepository.findByTaskIdOrderByIdAsc(taskId); + + List fileResults = results.stream() + .map(result -> { + GlobalSearchFileResult dto = new GlobalSearchFileResult(); + dto.setFileId(result.getFileId()); + dto.setFileName(result.getFileName()); + dto.setThumbnail(result.getThumbnail()); + dto.setFileStatus(result.getFileStatus()); + + try { + YandexSearchResponse searchResponse = objectMapper.readValue( + result.getSearchResults(), + YandexSearchResponse.class + ); + + dto.setImages(searchResponse.getImages()); + dto.setPage(searchResponse.getPage()); + dto.setPageSize(searchResponse.getPageSize()); + dto.setTotalResults(searchResponse.getTotalResults()); + dto.setTotalPages(searchResponse.getTotalPages()); + + } catch (Exception e) { + log.error("Failed to parse search results for file: {}", result.getFileId(), e); + dto.setImages(new ArrayList<>()); + dto.setTotalResults(0); + } + + return dto; + }) + .collect(Collectors.toList()); + + int progress = task.getTotalFiles() > 0 + ? task.getProcessedFiles() * 100 / task.getTotalFiles() + : 0; + + GlobalSearchStatusResponse response = new GlobalSearchStatusResponse(); + response.setTaskId(taskId); + response.setStatus(task.getStatus()); + response.setProgress(progress); + response.setResults(fileResults); + + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchFileResult.java b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchFileResult.java new file mode 100644 index 0000000..31d1358 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchFileResult.java @@ -0,0 +1,19 @@ +package ru.soune.nocopy.dto.search; + +import lombok.Data; +import ru.soune.nocopy.dto.file.YandexSearchResponse; + +import java.util.List; + +@Data +public class GlobalSearchFileResult { + private String fileId; + private String fileName; + private String thumbnail; + private String fileStatus; + private List images; + private int page; + private int pageSize; + private int totalResults; + private int totalPages; +} diff --git a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStartRequest.java b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStartRequest.java new file mode 100644 index 0000000..9e1021a --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStartRequest.java @@ -0,0 +1,14 @@ +package ru.soune.nocopy.dto.search; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Builder +@Data +public class GlobalSearchStartRequest { + private String searchType; + + private List fileIds; +} diff --git a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStartResponse.java b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStartResponse.java new file mode 100644 index 0000000..9b55562 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStartResponse.java @@ -0,0 +1,12 @@ +package ru.soune.nocopy.dto.search; + +import lombok.Data; + +@Data +public class GlobalSearchStartResponse { + private String taskId; + + private String status; + + private Integer totalFiles; +} diff --git a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatusResponse.java b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatusResponse.java new file mode 100644 index 0000000..0634aa1 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatusResponse.java @@ -0,0 +1,18 @@ +package ru.soune.nocopy.dto.search; + +import lombok.Data; + +import java.util.List; + +@Data +public class GlobalSearchStatusResponse { + private String taskId; + + private String status; + + private String searchType; + + private Integer progress; + + private List results; +} diff --git a/src/main/java/ru/soune/nocopy/entity/search/GlobalSearchResult.java b/src/main/java/ru/soune/nocopy/entity/search/GlobalSearchResult.java new file mode 100644 index 0000000..ab8661f --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/search/GlobalSearchResult.java @@ -0,0 +1,32 @@ +package ru.soune.nocopy.entity.search; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "global_search_results") +@Data +@Getter @Setter +@RequiredArgsConstructor +public class GlobalSearchResult { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String taskId; + + private String fileId; + + private String fileName; + + private String thumbnail; + + private String fileStatus; + + @Column(length = 5000) + private String searchResults; +} diff --git a/src/main/java/ru/soune/nocopy/entity/search/GlobalSearchTask.java b/src/main/java/ru/soune/nocopy/entity/search/GlobalSearchTask.java new file mode 100644 index 0000000..2a7b4c7 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/search/GlobalSearchTask.java @@ -0,0 +1,34 @@ +package ru.soune.nocopy.entity.search; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "global_search_tasks") +@Data +@Getter @Setter +@RequiredArgsConstructor +public class GlobalSearchTask { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String taskId; + + private Long userId; + + private String searchType; + + private String status; + + private Integer totalFiles; + + private Integer processedFiles; + + private LocalDateTime createdAt; + + private LocalDateTime updatedAt; +} diff --git a/src/main/java/ru/soune/nocopy/entity/search/SearchStatus.java b/src/main/java/ru/soune/nocopy/entity/search/SearchStatus.java new file mode 100644 index 0000000..867abb8 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/search/SearchStatus.java @@ -0,0 +1,5 @@ +package ru.soune.nocopy.entity.search; + +public enum SearchStatus { + PROCESSING, COMPLETED, ACCEPTED, FAILED, SUCCESS, TIMEOUT +} diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 81182da..01e8a27 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -90,10 +90,8 @@ public class ImageFoundRequestHandler implements RequestHandler { allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); log.info("allUniqueImages OK: {} images", allUniqueImages.size()); -// if (!allUniqueImages.isEmpty()) { tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); -// } int page = imageSearchRequest.getPage() != null ? imageSearchRequest.getPage() : 1; int pageSize = 5; diff --git a/src/main/java/ru/soune/nocopy/repository/GlobalSearchResultRepository.java b/src/main/java/ru/soune/nocopy/repository/GlobalSearchResultRepository.java new file mode 100644 index 0000000..dc41337 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/GlobalSearchResultRepository.java @@ -0,0 +1,13 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.search.GlobalSearchResult; + +import java.util.List; + +@Repository +public interface GlobalSearchResultRepository extends JpaRepository { + List findByTaskIdOrderByIdAsc(String taskId); + void deleteByTaskId(String taskId); +} diff --git a/src/main/java/ru/soune/nocopy/repository/GlobalSearchTaskRepository.java b/src/main/java/ru/soune/nocopy/repository/GlobalSearchTaskRepository.java new file mode 100644 index 0000000..908a659 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/GlobalSearchTaskRepository.java @@ -0,0 +1,13 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.search.GlobalSearchTask; + +import java.util.Optional; + +@Repository +public interface GlobalSearchTaskRepository extends JpaRepository { + Optional findByTaskIdAndUserId(String taskId, Long userId); +} + diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java new file mode 100644 index 0000000..cd55272 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -0,0 +1,147 @@ +package ru.soune.nocopy.service.search; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.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 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 ObjectMapper objectMapper; + + private final TariffInfoService tariffInfoService; + + private final GlobalSearchTaskRepository globalSearchTaskRepository; + + @Async + @Transactional + public void processFilesAsync(String taskId, List filesToProcess, Long userId) { + try { + for (int i = 0; i < filesToProcess.size(); i++) { + FileEntity file = filesToProcess.get(i); + + 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.setThumbnail(file.getThumbnailPath()); + + List allUniqueImages = new ArrayList<>(); + boolean hasTimeout = false; + + try { + String searchResponseGoogle = searchImageService.searchReverseByPublicUrl( + file, "google_lens", "exact_matches"); + + List googleImages = + searchImageService.getAllImagesWithoutPagination(searchResponseGoogle, "exact_matches"); + + allUniqueImages.addAll(googleImages); + log.info("Google search OK for file {}", file.getId()); + + } 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); + } + + try { + String searchResponseYandex = searchImageService.searchReverseByPublicUrl( + file, "yandex_reverse_image", "visual_matches"); + + List yandexImages = + searchImageService.getAllImagesWithoutPagination(searchResponseYandex, "visual_matches"); + + allUniqueImages.addAll(yandexImages); + log.info("Yandex search OK for file {}", file.getId()); + + } 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); + } + + allUniqueImages = searchImageService.removeDuplicateUrls( + allUniqueImages.stream() + .filter(img -> img.getUrl() != null) + .collect(Collectors.toList()), + new ArrayList<>() + ); + + YandexSearchResponse searchResponse = new YandexSearchResponse(); + searchResponse.setImages(allUniqueImages); + searchResponse.setPage(1); + searchResponse.setPageSize(allUniqueImages.size()); + searchResponse.setTotalResults(allUniqueImages.size()); + searchResponse.setTotalPages(1); + + try { + result.setSearchResults(objectMapper.writeValueAsString(searchResponse)); + } catch (Exception e) { + log.error("Failed to serialize search results", e); + result.setSearchResults("{}"); + } + + if (allUniqueImages.isEmpty() && hasTimeout) { + result.setFileStatus(SearchStatus.TIMEOUT.name()); + } else { + result.setFileStatus(SearchStatus.SUCCESS.name()); + + tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH); + } + + return result; + } +} diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchService.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchService.java new file mode 100644 index 0000000..4b5ae40 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchService.java @@ -0,0 +1,62 @@ +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.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 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; + + @Transactional + public String startSearch(GlobalSearchStartRequest request, Long userId, List 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 getFilesToProcess(GlobalSearchStartRequest request, Long userId) { + if ("monitoring".equals(request.getSearchType())) { + List monitoringFiles = + fileMonitoringRepository.findByUserIdAndIsActiveTrue(userId); + + return monitoringFiles.isEmpty() ? new ArrayList<>(): monitoringFiles.stream() + .map(FileMonitoringEntity::getFile) + .toList(); + } else { + return fileEntityRepository.findAllById(request.getFileIds()); + } + } +} From cfaf8fcf32169b7077f6982e700423097c426bc0 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 6 Mar 2026 22:42:09 +0700 Subject: [PATCH 136/274] dev async global search --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 70ba5f0..4bf59ae 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ WHERE file_entities.id = numbered.id; server { listen 80; listen [::]:80; -server_name admin.not-copy.ru; +server_name admin.not-copy.сom; location /api/ { proxy_pass http://localhost:3003; @@ -200,7 +200,7 @@ server_name admin.not-copy.ru; server { listen 80; listen [::]:80; -server_name workspace.not-copy.ru; +server_name workspace.not-copy.сom; location /api/ { proxy_pass http://localhost:3003; @@ -222,7 +222,7 @@ server_name workspace.not-copy.ru; server { listen 80; listen [::]:80; -server_name lp.not-copy.ru; +server_name lp.not-copy.сom; location /api/ { proxy_pass http://localhost:3003; @@ -244,7 +244,7 @@ server_name lp.not-copy.ru; server { listen 80; listen [::]:80; -server_name dev-admin.not-copy.ru; +server_name dev-admin.not-copy.сom; location /api/ { proxy_pass http://localhost:3001; @@ -266,7 +266,7 @@ server_name dev-admin.not-copy.ru; server { listen 80; listen [::]:80; -server_name dev-workspace.not-copy.ru 92.242.61.23; +server_name dev-workspace.not-copy.сom; location /api/ { proxy_pass http://localhost:3001; @@ -288,7 +288,7 @@ server_name dev-workspace.not-copy.ru 92.242.61.23; server { listen 80; listen [::]:80; -server_name dev-lp.not-copy.ru; +server_name dev-lp.not-copy.сom; location /api/ { proxy_pass http://localhost:3001; From 7f601fcfb3e5cdc9ffc66b72383d83ef6f79e1d6 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 6 Mar 2026 23:52:06 +0700 Subject: [PATCH 137/274] dev async global search --- infrastructure/jenkins/deploy-backend.groovy | 7 +++++++ .../nocopy/dto/search/GlobalSearchStatusResponse.java | 2 +- src/main/resources/application-dev.yaml | 2 +- src/main/resources/application-prod.yaml | 2 +- src/main/resources/application.yaml | 2 +- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/infrastructure/jenkins/deploy-backend.groovy b/infrastructure/jenkins/deploy-backend.groovy index 30fc29f..07105f2 100644 --- a/infrastructure/jenkins/deploy-backend.groovy +++ b/infrastructure/jenkins/deploy-backend.groovy @@ -27,6 +27,11 @@ pipeline { defaultValue: 'app-network-dev', description: 'Имя сети (будет создана если не существует)' ) + choice( + name: 'SPRING_PROFILE', + choices: ['dev', 'prod'], + description: 'Профиль Spring для запуска' + ) } stages { @@ -73,6 +78,7 @@ pipeline { steps { script { def buildTime = sh(script: "TZ='Asia/Novosibirsk' date '+%d-%m-%Y %H:%M:%S'", returnStdout: true).trim() + def springProfile = params.SPRING_PROFILE withCredentials([ usernamePassword( credentialsId: 'server-root-password', @@ -155,6 +161,7 @@ pipeline { -e POSTGRES_PORT=5432 \\ -e POSTGRES_HOST=no_copy_${params.PORT} \\ -e FILE_STORAGE_PATH=/data/uploads \\ + -e SPRING_PROFILES_ACTIVE=${springProfile} \\ app-backend-${params.PORT}:latest echo '8. Проверка...' diff --git a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatusResponse.java b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatusResponse.java index 0634aa1..3983c19 100644 --- a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatusResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatusResponse.java @@ -14,5 +14,5 @@ public class GlobalSearchStatusResponse { private Integer progress; - private List results; +// private List results; } diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml index bd1102b..597f72a 100644 --- a/src/main/resources/application-dev.yaml +++ b/src/main/resources/application-dev.yaml @@ -1,2 +1,2 @@ server: - baseurl: "http://localhost" \ No newline at end of file + baseurl: "dev-workspace.not-copy.com" \ No newline at end of file diff --git a/src/main/resources/application-prod.yaml b/src/main/resources/application-prod.yaml index 2a35858..d048416 100644 --- a/src/main/resources/application-prod.yaml +++ b/src/main/resources/application-prod.yaml @@ -1,2 +1,2 @@ server: - baseurl: "http://92.242.61.23" \ No newline at end of file + baseurl: "workspace.not-copy.com" \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 9b1d602..65731c1 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -75,7 +75,7 @@ security: server: port: ${SERVER_PORT:8080} - baseurl: "http://92.242.61.23" +# baseurl: "http://92.242.61.23" front: port: ":2998" tomcat: From 66fe68342a84721b178536d57b1ec567b76c403d Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 7 Mar 2026 00:24:31 +0700 Subject: [PATCH 138/274] dev async global search --- src/main/resources/application-dev.yaml | 2 +- src/main/resources/application.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml index 597f72a..8879f2d 100644 --- a/src/main/resources/application-dev.yaml +++ b/src/main/resources/application-dev.yaml @@ -1,2 +1,2 @@ server: - baseurl: "dev-workspace.not-copy.com" \ No newline at end of file + baseurl: "https://dev-workspace.not-copy.com" \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 65731c1..48404f1 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -75,7 +75,7 @@ security: server: port: ${SERVER_PORT:8080} -# baseurl: "http://92.242.61.23" + baseurl: ${BASE_URL:http://localhost:8080} front: port: ":2998" tomcat: From 167002643010510b000da90f16997291d75e0050 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 7 Mar 2026 00:33:37 +0700 Subject: [PATCH 139/274] dev async global search --- src/main/resources/application-dev.yaml | 2 -- src/main/resources/application-prod.yaml | 2 -- src/main/resources/application.yaml | 25 +++++++++++++++++++++++- 3 files changed, 24 insertions(+), 5 deletions(-) delete mode 100644 src/main/resources/application-dev.yaml delete mode 100644 src/main/resources/application-prod.yaml diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml deleted file mode 100644 index 8879f2d..0000000 --- a/src/main/resources/application-dev.yaml +++ /dev/null @@ -1,2 +0,0 @@ -server: - baseurl: "https://dev-workspace.not-copy.com" \ No newline at end of file diff --git a/src/main/resources/application-prod.yaml b/src/main/resources/application-prod.yaml deleted file mode 100644 index d048416..0000000 --- a/src/main/resources/application-prod.yaml +++ /dev/null @@ -1,2 +0,0 @@ -server: - baseurl: "workspace.not-copy.com" \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 48404f1..2e0570b 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -75,7 +75,6 @@ security: server: port: ${SERVER_PORT:8080} - baseurl: ${BASE_URL:http://localhost:8080} front: port: ":2998" tomcat: @@ -146,3 +145,27 @@ tarif: enterprice-disk-size: 500 enterprice-user-count: 80 +--- +spring: + config: + activate: + on-profile: dev +server: + baseurl: "https://dev-workspace.not-copy.com" + +--- +spring: + config: + activate: + on-profile: prod +server: + baseurl: "https://workspace.not-copy.com" + +--- +spring: + config: + activate: + on-profile: local +server: + baseurl: "http://localhost:8080" + From 2bf0468f6118aa72a425c9d191ae7331c210c7a2 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 7 Mar 2026 00:47:43 +0700 Subject: [PATCH 140/274] dev async global search --- .../java/ru/soune/nocopy/service/FileSimilarityService.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 3b036ee..8afdf25 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -225,6 +225,12 @@ public class FileSimilarityService { } else { level = "DIFFERENT"; } + log.info("baseUrl: {}", baseUrl); + log.info("baseUrl: {}", baseUrl); + log.info("baseUrl: {}", baseUrl); + log.info("baseUrl: {}", baseUrl); + log.info("baseUrl: {}", baseUrl); + log.info("baseUrl: {}", baseUrl); return SimilarFileDTO.builder() .fileId(similarImageProjection.getId()) From 4e9a3e90084e28cf371d21f39c94fcc56fcef476 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 7 Mar 2026 00:59:17 +0700 Subject: [PATCH 141/274] dev async global search --- src/main/resources/application.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 2e0570b..a696470 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -75,6 +75,7 @@ security: server: port: ${SERVER_PORT:8080} + baseurl: ${BASE_URL:} front: port: ":2998" tomcat: From cd469edb7ff1cb69951f5478cdc2a89a44bde623 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 7 Mar 2026 01:11:22 +0700 Subject: [PATCH 142/274] dev async global search --- infrastructure/jenkins/deploy-backend.groovy | 4 +-- src/main/resources/application.yaml | 26 +------------------- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/infrastructure/jenkins/deploy-backend.groovy b/infrastructure/jenkins/deploy-backend.groovy index 07105f2..dfe82ec 100644 --- a/infrastructure/jenkins/deploy-backend.groovy +++ b/infrastructure/jenkins/deploy-backend.groovy @@ -78,7 +78,7 @@ pipeline { steps { script { def buildTime = sh(script: "TZ='Asia/Novosibirsk' date '+%d-%m-%Y %H:%M:%S'", returnStdout: true).trim() - def springProfile = params.SPRING_PROFILE + def baseUrl = params.SPRING_PROFILE == 'prod' ? 'https://workspace.not-copy.com' : 'https://dev-workspace.not-copy.com' withCredentials([ usernamePassword( credentialsId: 'server-root-password', @@ -161,7 +161,7 @@ pipeline { -e POSTGRES_PORT=5432 \\ -e POSTGRES_HOST=no_copy_${params.PORT} \\ -e FILE_STORAGE_PATH=/data/uploads \\ - -e SPRING_PROFILES_ACTIVE=${springProfile} \\ + -e BASE_URL=${baseUrl} \\ app-backend-${params.PORT}:latest echo '8. Проверка...' diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index a696470..8d4a90f 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -75,7 +75,7 @@ security: server: port: ${SERVER_PORT:8080} - baseurl: ${BASE_URL:} + baseurl: ${BASE_URL} front: port: ":2998" tomcat: @@ -146,27 +146,3 @@ tarif: enterprice-disk-size: 500 enterprice-user-count: 80 ---- -spring: - config: - activate: - on-profile: dev -server: - baseurl: "https://dev-workspace.not-copy.com" - ---- -spring: - config: - activate: - on-profile: prod -server: - baseurl: "https://workspace.not-copy.com" - ---- -spring: - config: - activate: - on-profile: local -server: - baseurl: "http://localhost:8080" - From a46dd54c6622fa6ad42c0913ace8e90049024720 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 7 Mar 2026 01:22:36 +0700 Subject: [PATCH 143/274] dev async global search --- src/main/resources/application.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 8d4a90f..4fa9b77 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -75,7 +75,7 @@ security: server: port: ${SERVER_PORT:8080} - baseurl: ${BASE_URL} + baseurl: "https://dev-workspace.not-copy.com" front: port: ":2998" tomcat: @@ -145,4 +145,3 @@ tarif: enterprice-max-files-count: 50 enterprice-disk-size: 500 enterprice-user-count: 80 - From 8b7f580b0e83ecd58287afee8edd857aba0739f4 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 7 Mar 2026 01:36:31 +0700 Subject: [PATCH 144/274] dev async global search --- infrastructure/jenkins/deploy-backend.groovy | 3 ++- .../java/ru/soune/nocopy/service/FileSimilarityService.java | 5 ++++- src/main/java/ru/soune/nocopy/service/mail/EmailService.java | 5 ++++- .../ru/soune/nocopy/service/search/SearchImageService.java | 5 ++++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/infrastructure/jenkins/deploy-backend.groovy b/infrastructure/jenkins/deploy-backend.groovy index dfe82ec..f7d3677 100644 --- a/infrastructure/jenkins/deploy-backend.groovy +++ b/infrastructure/jenkins/deploy-backend.groovy @@ -78,7 +78,8 @@ pipeline { steps { script { def buildTime = sh(script: "TZ='Asia/Novosibirsk' date '+%d-%m-%Y %H:%M:%S'", returnStdout: true).trim() - def baseUrl = params.SPRING_PROFILE == 'prod' ? 'https://workspace.not-copy.com' : 'https://dev-workspace.not-copy.com' + def baseUrl = params.SPRING_PROFILE == 'prod' ? 'https://workspace.not-copy.com' : + 'https://dev-workspace.not-copy.com' withCredentials([ usernamePassword( credentialsId: 'server-root-password', diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 8afdf25..ce1e281 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -40,7 +40,10 @@ public class FileSimilarityService { private final FileEntityRepository fileEntityRepository; - @Value("${server.baseurl}") +// @Value("${server.baseurl}") +// private String baseUrl; + + @Value("${BASE_URL}") private String baseUrl; public List findDuplicateByHammingDistance(String fileId, int hammingDistance, diff --git a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java index c34212d..1bb4054 100644 --- a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java +++ b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java @@ -42,7 +42,10 @@ public class EmailService { @Value("${app.email.verification.drop-password-subject}") private String dropPasswordSubject; - @Value("${server.baseurl}") + // @Value("${server.baseurl}") + // private String baseUrl; + + @Value("${BASE_URL}") private String baseUrl; @Value("${server.front.port}") diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index d25a633..6f4d7fc 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -86,7 +86,10 @@ public class SearchImageService { @Value("${searchapi.api-key:}") private String searchApiKey; - @Value("${server.baseurl}") +// @Value("${server.baseurl}") +// private String appBaseUrl; + + @Value("${BASE_URL}") private String appBaseUrl; @PostConstruct From 60857d407747869f8c26f4098d6a5b17172724d0 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 7 Mar 2026 01:54:33 +0700 Subject: [PATCH 145/274] dev temp set urk --- .../ru/soune/nocopy/service/FileSimilarityService.java | 9 ++------- .../java/ru/soune/nocopy/service/mail/EmailService.java | 6 ++++-- .../soune/nocopy/service/search/SearchImageService.java | 3 ++- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index ce1e281..5c86988 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -228,12 +228,6 @@ public class FileSimilarityService { } else { level = "DIFFERENT"; } - log.info("baseUrl: {}", baseUrl); - log.info("baseUrl: {}", baseUrl); - log.info("baseUrl: {}", baseUrl); - log.info("baseUrl: {}", baseUrl); - log.info("baseUrl: {}", baseUrl); - log.info("baseUrl: {}", baseUrl); return SimilarFileDTO.builder() .fileId(similarImageProjection.getId()) @@ -248,7 +242,8 @@ public class FileSimilarityService { .uploadDate(similarImageProjection.getUploadDate()) .status(similarImageProjection.getProtectionStatus()) // .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId()) - .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId() + "/thumbnail") + .url("https://workspace.not-copy.com" + "/api/files/protected/" + similarImageProjection.getId() + "/thumbnail") +// .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId() + "/thumbnail") .build(); } diff --git a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java index 1bb4054..5d90cf3 100644 --- a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java +++ b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java @@ -73,7 +73,8 @@ public class EmailService { 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"), +// String.valueOf(missingTokens), "link_to_token_board", baseUrl + "/pages/payment"), + String.valueOf(missingTokens), "link_to_token_board", "https://workspace.not-copy.com" + "/pages/payment"), "templates/token-not-found.html"); sendEmail("noreply@no-copy.ru", user.getEmail(), standartAdressFrom, htmlContent); @@ -148,7 +149,8 @@ public class EmailService { String htmlContent = loadAndProcessTemplate(user.getFullName(), code, "templates/reset-password.html"); //TODO frontPort убрать после настройки редиректа nginx - String resetLink = escapeHtml(baseUrl + "/reset-password?email=" + user.getEmail()); +// String resetLink = escapeHtml(baseUrl + "/reset-password?email=" + user.getEmail()); + String resetLink = escapeHtml("https://workspace.not-copy.com" + "/reset-password?email=" + user.getEmail()); htmlContent = htmlContent.replace("{{resetLink}}", resetLink); helper.setText(htmlContent, true); diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 6f4d7fc..f3a9104 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -113,7 +113,8 @@ public class SearchImageService { ); } - String publicUrl = String.format("%s/api/files/public/%s", appBaseUrl, fileEntity.getId()); +// String publicUrl = String.format("%s/api/files/public/%s", appBaseUrl, fileEntity.getId()); + String publicUrl = String.format("%s/api/files/public/%s", "https://workspace.not-copy.com", fileEntity.getId()); log.info("Searching reverse for image: {}", publicUrl); From 94061381c796291892aee6d4256be2bcb088c8b8 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 7 Mar 2026 02:18:56 +0700 Subject: [PATCH 146/274] dev temp set urk --- infrastructure/jenkins/deploy-backend.groovy | 7 +++-- .../nocopy/service/FileSimilarityService.java | 8 ++---- .../nocopy/service/mail/EmailService.java | 11 +++----- .../service/search/SearchImageService.java | 8 ++---- src/main/resources/application.yaml | 26 ++++++++++++++++++- 5 files changed, 35 insertions(+), 25 deletions(-) diff --git a/infrastructure/jenkins/deploy-backend.groovy b/infrastructure/jenkins/deploy-backend.groovy index f7d3677..a1cedce 100644 --- a/infrastructure/jenkins/deploy-backend.groovy +++ b/infrastructure/jenkins/deploy-backend.groovy @@ -78,8 +78,7 @@ pipeline { steps { script { def buildTime = sh(script: "TZ='Asia/Novosibirsk' date '+%d-%m-%Y %H:%M:%S'", returnStdout: true).trim() - def baseUrl = params.SPRING_PROFILE == 'prod' ? 'https://workspace.not-copy.com' : - 'https://dev-workspace.not-copy.com' + def springProfile = params.SPRING_PROFILE withCredentials([ usernamePassword( credentialsId: 'server-root-password', @@ -161,8 +160,8 @@ pipeline { -e POSTGRES_PASSWORD=${DB_PASSWORD} \\ -e POSTGRES_PORT=5432 \\ -e POSTGRES_HOST=no_copy_${params.PORT} \\ - -e FILE_STORAGE_PATH=/data/uploads \\ - -e BASE_URL=${baseUrl} \\ + -e FILE_STORAGE_PATH=/data/uploads \\ + -e SPRING_PROFILES_ACTIVE=${springProfile} \\ app-backend-${params.PORT}:latest echo '8. Проверка...' diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 5c86988..3b036ee 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -40,10 +40,7 @@ public class FileSimilarityService { private final FileEntityRepository fileEntityRepository; -// @Value("${server.baseurl}") -// private String baseUrl; - - @Value("${BASE_URL}") + @Value("${server.baseurl}") private String baseUrl; public List findDuplicateByHammingDistance(String fileId, int hammingDistance, @@ -242,8 +239,7 @@ public class FileSimilarityService { .uploadDate(similarImageProjection.getUploadDate()) .status(similarImageProjection.getProtectionStatus()) // .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId()) - .url("https://workspace.not-copy.com" + "/api/files/protected/" + similarImageProjection.getId() + "/thumbnail") -// .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId() + "/thumbnail") + .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId() + "/thumbnail") .build(); } diff --git a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java index 5d90cf3..c34212d 100644 --- a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java +++ b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java @@ -42,10 +42,7 @@ public class EmailService { @Value("${app.email.verification.drop-password-subject}") private String dropPasswordSubject; - // @Value("${server.baseurl}") - // private String baseUrl; - - @Value("${BASE_URL}") + @Value("${server.baseurl}") private String baseUrl; @Value("${server.front.port}") @@ -73,8 +70,7 @@ public class EmailService { 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"), - String.valueOf(missingTokens), "link_to_token_board", "https://workspace.not-copy.com" + "/pages/payment"), + String.valueOf(missingTokens), "link_to_token_board", baseUrl + "/pages/payment"), "templates/token-not-found.html"); sendEmail("noreply@no-copy.ru", user.getEmail(), standartAdressFrom, htmlContent); @@ -149,8 +145,7 @@ public class EmailService { String htmlContent = loadAndProcessTemplate(user.getFullName(), code, "templates/reset-password.html"); //TODO frontPort убрать после настройки редиректа nginx -// String resetLink = escapeHtml(baseUrl + "/reset-password?email=" + user.getEmail()); - String resetLink = escapeHtml("https://workspace.not-copy.com" + "/reset-password?email=" + user.getEmail()); + String resetLink = escapeHtml(baseUrl + "/reset-password?email=" + user.getEmail()); htmlContent = htmlContent.replace("{{resetLink}}", resetLink); helper.setText(htmlContent, true); diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index f3a9104..d25a633 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -86,10 +86,7 @@ public class SearchImageService { @Value("${searchapi.api-key:}") private String searchApiKey; -// @Value("${server.baseurl}") -// private String appBaseUrl; - - @Value("${BASE_URL}") + @Value("${server.baseurl}") private String appBaseUrl; @PostConstruct @@ -113,8 +110,7 @@ public class SearchImageService { ); } -// String publicUrl = String.format("%s/api/files/public/%s", appBaseUrl, fileEntity.getId()); - String publicUrl = String.format("%s/api/files/public/%s", "https://workspace.not-copy.com", fileEntity.getId()); + String publicUrl = String.format("%s/api/files/public/%s", appBaseUrl, fileEntity.getId()); log.info("Searching reverse for image: {}", publicUrl); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 4fa9b77..2aa392f 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -75,7 +75,7 @@ security: server: port: ${SERVER_PORT:8080} - baseurl: "https://dev-workspace.not-copy.com" + baseurl: ${BASE_URL:https://dev-workspace.not-copy.com} front: port: ":2998" tomcat: @@ -145,3 +145,27 @@ tarif: enterprice-max-files-count: 50 enterprice-disk-size: 500 enterprice-user-count: 80 + +--- +spring: + config: + activate: + on-profile: dev +server: + baseurl: https://dev-workspace.not-copy.com + +--- +spring: + config: + activate: + on-profile: prod +server: + baseurl: https://workspace.not-copy.com + +--- +spring: + config: + activate: + on-profile: local +server: + baseurl: http://localhost:8080 \ No newline at end of file From 5c8610397ae3856f16d3317379390419f6312e69 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 7 Mar 2026 02:25:43 +0700 Subject: [PATCH 147/274] dev temp set urk --- .../ru/soune/nocopy/dto/search/GlobalSearchStatusResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatusResponse.java b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatusResponse.java index 3983c19..0634aa1 100644 --- a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatusResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatusResponse.java @@ -14,5 +14,5 @@ public class GlobalSearchStatusResponse { private Integer progress; -// private List results; + private List results; } From edec60bf19c808e3e811437ae58d3c597abd537e Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 7 Mar 2026 02:35:01 +0700 Subject: [PATCH 148/274] dev temp set urk --- infrastructure/jenkins/deploy-backend.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/jenkins/deploy-backend.groovy b/infrastructure/jenkins/deploy-backend.groovy index a1cedce..07105f2 100644 --- a/infrastructure/jenkins/deploy-backend.groovy +++ b/infrastructure/jenkins/deploy-backend.groovy @@ -160,7 +160,7 @@ pipeline { -e POSTGRES_PASSWORD=${DB_PASSWORD} \\ -e POSTGRES_PORT=5432 \\ -e POSTGRES_HOST=no_copy_${params.PORT} \\ - -e FILE_STORAGE_PATH=/data/uploads \\ + -e FILE_STORAGE_PATH=/data/uploads \\ -e SPRING_PROFILES_ACTIVE=${springProfile} \\ app-backend-${params.PORT}:latest From 2f57a5413749958e7372907554297de2b2b30f97 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 7 Mar 2026 02:43:35 +0700 Subject: [PATCH 149/274] dev temp set urk --- infrastructure/jenkins/deploy-backend.groovy | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/infrastructure/jenkins/deploy-backend.groovy b/infrastructure/jenkins/deploy-backend.groovy index 07105f2..78e2105 100644 --- a/infrastructure/jenkins/deploy-backend.groovy +++ b/infrastructure/jenkins/deploy-backend.groovy @@ -207,8 +207,11 @@ pipeline { post { success { - echo "Deployment successful" - echo "Application URL: http://${params.SERVER}:${params.PORT}" + script { + def appUrl = params.SPRING_PROFILE == 'prod' ? 'https://workspace.not-copy.com' : 'https://dev-workspace.not-copy.com' + echo "Deployment successful" + echo "Application URL: ${appUrl}" + } } failure { echo "Deployment failed for branch ${params.BRANCH} on port ${params.PORT}" From c87b5af628e87a0d6d2f7b4187772764b396cf74 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 9 Mar 2026 12:12:06 +0700 Subject: [PATCH 150/274] dev temp set urk --- .../nocopy/configuration/CorsConfig.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/main/java/ru/soune/nocopy/configuration/CorsConfig.java diff --git a/src/main/java/ru/soune/nocopy/configuration/CorsConfig.java b/src/main/java/ru/soune/nocopy/configuration/CorsConfig.java new file mode 100644 index 0000000..da8a0f5 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/configuration/CorsConfig.java @@ -0,0 +1,23 @@ +package ru.soune.nocopy.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +@Configuration +public class CorsConfig { + + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedOrigin("*"); + config.addAllowedMethod("*"); + config.addAllowedHeader("*"); + source.registerCorsConfiguration("/**", config); + + return new CorsFilter(source); + } +} From b65a39b67275fe3f044a52c2f9620af4285e45a7 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 10 Mar 2026 12:26:43 +0700 Subject: [PATCH 151/274] dev temp set urk --- .../ru/soune/nocopy/service/payment/PaymentService.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java index 8a05b74..81f4233 100644 --- a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java +++ b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java @@ -124,12 +124,12 @@ public class PaymentService { } private void activateTariffForUser(User user, Tariff tariff) { - String type = tariff.getType(); + String type = tariff.getTariffAccountType(); + TariffInfo activeTariffInfo = user.getActiveTariffInfo(); if (type.equals("token")) { - TariffInfo activeTariffInfo = user.getActiveTariffInfo(); - Integer tokens = activeTariffInfo.getTokens() + tariff.getTokens(); - activeTariffInfo.setTokens(tokens); + Integer tokens = activeTariffInfo.getBoughtTokens(); + activeTariffInfo.setBoughtTokens(tokens + tariff.getTokens()); tariffInfoRepository.save(activeTariffInfo); } else { @@ -138,6 +138,7 @@ public class PaymentService { tariffInfo.setStartTariff(LocalDateTime.now()); tariffInfo.setEndTariff(LocalDateTime.now().plusMonths(1)); tariffInfo.setTokens(tariff.getTokens()); + tariffInfo.setBoughtTokens(activeTariffInfo.getBoughtTokens()); tariffInfo.setTariff(tariff); tariffInfoService.linkUserTariffInfo(user, tariffInfoService.addTariffInfo(tariffInfo)); From 590d42690b73d99a0118a3c5ccd8a40901632203 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 10 Mar 2026 21:19:27 +0700 Subject: [PATCH 152/274] dev add config for nginx --- README.md | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4bf59ae..2e80afb 100644 --- a/README.md +++ b/README.md @@ -174,11 +174,21 @@ WHERE file_entities.id = numbered.id; НАСТРОЙКА NGINX -------------------------------- - +# admin.not-copy.com server { listen 80; listen [::]:80; -server_name admin.not-copy.сom; +server_name admin.not-copy.com; +return 301 https://$server_name$request_uri; +} + +server { +listen 443 ssl; +listen [::]:443 ssl; +server_name admin.not-copy.com; + + ssl_certificate /etc/letsencrypt/live/admin.not-copy.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/admin.not-copy.com/privkey.pem; location /api/ { proxy_pass http://localhost:3003; @@ -197,10 +207,21 @@ server_name admin.not-copy.сom; } } +# workspace.not-copy.com server { listen 80; listen [::]:80; -server_name workspace.not-copy.сom; +server_name workspace.not-copy.com; +return 301 https://$server_name$request_uri; +} + +server { +listen 443 ssl; +listen [::]:443 ssl; +server_name workspace.not-copy.com; + + ssl_certificate /etc/letsencrypt/live/workspace.not-copy.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/workspace.not-copy.com/privkey.pem; location /api/ { proxy_pass http://localhost:3003; @@ -219,10 +240,21 @@ server_name workspace.not-copy.сom; } } +# lp.not-copy.com server { listen 80; listen [::]:80; -server_name lp.not-copy.сom; +server_name lp.not-copy.com; +return 301 https://$server_name$request_uri; +} + +server { +listen 443 ssl; +listen [::]:443 ssl; +server_name lp.not-copy.com; + + ssl_certificate /etc/letsencrypt/live/lp.not-copy.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/lp.not-copy.com/privkey.pem; location /api/ { proxy_pass http://localhost:3003; @@ -241,10 +273,21 @@ server_name lp.not-copy.сom; } } +# dev-admin.not-copy.com server { listen 80; listen [::]:80; -server_name dev-admin.not-copy.сom; +server_name dev-admin.not-copy.com; +return 301 https://$server_name$request_uri; +} + +server { +listen 443 ssl; +listen [::]:443 ssl; +server_name dev-admin.not-copy.com; + + ssl_certificate /etc/letsencrypt/live/dev-admin.not-copy.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/dev-admin.not-copy.com/privkey.pem; location /api/ { proxy_pass http://localhost:3001; @@ -263,10 +306,21 @@ server_name dev-admin.not-copy.сom; } } +# dev-workspace.not-copy.com server { listen 80; listen [::]:80; -server_name dev-workspace.not-copy.сom; +server_name dev-workspace.not-copy.com; +return 301 https://$server_name$request_uri; +} + +server { +listen 443 ssl; +listen [::]:443 ssl; +server_name dev-workspace.not-copy.com; + + ssl_certificate /etc/letsencrypt/live/dev-workspace.not-copy.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/dev-workspace.not-copy.com/privkey.pem; location /api/ { proxy_pass http://localhost:3001; @@ -285,10 +339,21 @@ server_name dev-workspace.not-copy.сom; } } +# dev-lp.not-copy.com server { listen 80; listen [::]:80; -server_name dev-lp.not-copy.сom; +server_name dev-lp.not-copy.com; +return 301 https://$server_name$request_uri; +} + +server { +listen 443 ssl; +listen [::]:443 ssl; +server_name dev-lp.not-copy.com; + + ssl_certificate /etc/letsencrypt/live/dev-lp.not-copy.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/dev-lp.not-copy.com/privkey.pem; location /api/ { proxy_pass http://localhost:3001; @@ -307,6 +372,7 @@ server_name dev-lp.not-copy.сom; } } + # Проверяем конфиг ls -la /etc/nginx/sites-available/ ls -la /etc/nginx/sites-enabled/ From c591ab40df499fb31e9315545e07a42760456d3b Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 10 Mar 2026 22:47:43 +0700 Subject: [PATCH 153/274] dev add config for nginx --- .../nocopy/configuration/HandlerConfig.java | 6 +- .../controller/GlobalSearchController.java | 40 +---- .../ru/soune/nocopy/dto/BaseResponse.java | 2 + .../dto/monitoring/ViolationResponse.java | 43 ------ .../dto/violation/ViolationRequest.java | 36 +++++ .../dto/violation/ViolationResponse.java | 77 ++++++++++ .../soune/nocopy/entity/file/Violation.java | 57 ------- .../entity/search/GlobalSearchResult.java | 5 - .../ru/soune/nocopy/entity/user/User.java | 6 - .../nocopy/entity/violation/Violation.java | 54 +++++++ .../exception/ViolationNotFoundException.java | 7 + .../nocopy/handler/GetViolationsHandler.java | 85 ----------- .../nocopy/handler/ViolationHandler.java | 139 ++++++++++++++++++ .../repository/ViolationRepository.java | 39 +++++ .../monitoring/MonitoringSearchService.java | 66 ++------- .../search/GlobalSearchAsyncProcessor.java | 26 ++-- .../service/violation/ViolationService.java | 113 ++++++++++++++ 17 files changed, 495 insertions(+), 306 deletions(-) delete mode 100644 src/main/java/ru/soune/nocopy/dto/monitoring/ViolationResponse.java create mode 100644 src/main/java/ru/soune/nocopy/dto/violation/ViolationRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/violation/ViolationResponse.java delete mode 100644 src/main/java/ru/soune/nocopy/entity/file/Violation.java create mode 100644 src/main/java/ru/soune/nocopy/entity/violation/Violation.java create mode 100644 src/main/java/ru/soune/nocopy/exception/ViolationNotFoundException.java delete mode 100644 src/main/java/ru/soune/nocopy/handler/GetViolationsHandler.java create mode 100644 src/main/java/ru/soune/nocopy/handler/ViolationHandler.java create mode 100644 src/main/java/ru/soune/nocopy/repository/ViolationRepository.java create mode 100644 src/main/java/ru/soune/nocopy/service/violation/ViolationService.java diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index 386bb20..e159080 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -27,9 +27,9 @@ public class HandlerConfig { ResetPasswordHandler resetPasswordHandler, DaDataHandler daDataHandler, PaymentHandler paymentHandler, - GetViolationsHandler getViolationsHandler, CostHandler costHandler, - MonitoringHandler monitoringHandler + MonitoringHandler monitoringHandler, + ViolationHandler violationHandler ) { Map map = new HashMap<>(); map.put(20001, login); @@ -47,9 +47,9 @@ public class HandlerConfig { map.put(30003, referralHandler); map.put(30004, daDataHandler); map.put(30005, paymentHandler); - map.put(30006, getViolationsHandler); map.put(30007, monitoringHandler); map.put(30008, costHandler); + map.put(30009, violationHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java b/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java index cab9462..c388962 100644 --- a/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java +++ b/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java @@ -36,10 +36,6 @@ public class GlobalSearchController { private final GlobalSearchTaskRepository searchTaskRepository; - private final GlobalSearchResultRepository globalSearchResultRepository; - - private final ObjectMapper objectMapper; - @PostMapping("/start") public ResponseEntity startSearch( @RequestBody GlobalSearchStartRequest request, @@ -74,8 +70,7 @@ public class GlobalSearchController { } @GetMapping("/status/{taskId}") - public ResponseEntity getStatus( - @PathVariable String taskId, + public ResponseEntity getStatus(@PathVariable String taskId, @RequestHeader(value = "Authorization", required = false) String tokenHeader) { if (tokenHeader == null || tokenHeader.isBlank()) { @@ -93,38 +88,6 @@ public class GlobalSearchController { GlobalSearchTask task = taskOptional.orElseThrow(); - List results = globalSearchResultRepository.findByTaskIdOrderByIdAsc(taskId); - - List fileResults = results.stream() - .map(result -> { - GlobalSearchFileResult dto = new GlobalSearchFileResult(); - dto.setFileId(result.getFileId()); - dto.setFileName(result.getFileName()); - dto.setThumbnail(result.getThumbnail()); - dto.setFileStatus(result.getFileStatus()); - - try { - YandexSearchResponse searchResponse = objectMapper.readValue( - result.getSearchResults(), - YandexSearchResponse.class - ); - - dto.setImages(searchResponse.getImages()); - dto.setPage(searchResponse.getPage()); - dto.setPageSize(searchResponse.getPageSize()); - dto.setTotalResults(searchResponse.getTotalResults()); - dto.setTotalPages(searchResponse.getTotalPages()); - - } catch (Exception e) { - log.error("Failed to parse search results for file: {}", result.getFileId(), e); - dto.setImages(new ArrayList<>()); - dto.setTotalResults(0); - } - - return dto; - }) - .collect(Collectors.toList()); - int progress = task.getTotalFiles() > 0 ? task.getProcessedFiles() * 100 / task.getTotalFiles() : 0; @@ -133,7 +96,6 @@ public class GlobalSearchController { response.setTaskId(taskId); response.setStatus(task.getStatus()); response.setProgress(progress); - response.setResults(fileResults); return ResponseEntity.ok(response); } diff --git a/src/main/java/ru/soune/nocopy/dto/BaseResponse.java b/src/main/java/ru/soune/nocopy/dto/BaseResponse.java index 75f1543..6a4e3b0 100644 --- a/src/main/java/ru/soune/nocopy/dto/BaseResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/BaseResponse.java @@ -3,10 +3,12 @@ package ru.soune.nocopy.dto; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; @Data @AllArgsConstructor +@Builder @JsonFormat(with = JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) public class BaseResponse { @JsonProperty("msg_id") diff --git a/src/main/java/ru/soune/nocopy/dto/monitoring/ViolationResponse.java b/src/main/java/ru/soune/nocopy/dto/monitoring/ViolationResponse.java deleted file mode 100644 index 188b4b8..0000000 --- a/src/main/java/ru/soune/nocopy/dto/monitoring/ViolationResponse.java +++ /dev/null @@ -1,43 +0,0 @@ -package ru.soune.nocopy.dto.monitoring; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; -import lombok.Data; -import ru.soune.nocopy.entity.monitoring.SearchEngine; - -import java.time.LocalDateTime; - -@Data -@Builder -public class ViolationResponse { - - @JsonProperty("id") - private String id; - - @JsonProperty("file_id") - private String fileId; - - @JsonProperty("file_name") - private String fileName; - - @JsonProperty("found_url") - private String foundUrl; - - @JsonProperty("page_url") - private String pageUrl; - - @JsonProperty("page_title") - private String pageTitle; - - @JsonProperty("host") - private String host; - - @JsonProperty("search_engine") - private SearchEngine searchEngine; - - @JsonProperty("found_at") - private LocalDateTime foundAt; - - @JsonProperty("is_new") - private boolean isNew; -} diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationRequest.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationRequest.java new file mode 100644 index 0000000..97cdbd2 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationRequest.java @@ -0,0 +1,36 @@ +package ru.soune.nocopy.dto.violation; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class ViolationRequest { + + @JsonProperty("file_id") + private String fileId; + + @JsonProperty("page") + private Integer page = 0; + + @JsonProperty("size") + private Integer size = 10; + + @JsonProperty("sort_direction") + private String sortDirection = "desc"; + + @JsonProperty("status") + private String status; + + @JsonProperty("start_date") + private String startDate; + + @JsonProperty("end_date") + private String endDate; + + @JsonProperty("group_by") + private String groupBy; + + @JsonProperty("action") + private String action; +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationResponse.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationResponse.java new file mode 100644 index 0000000..94f1321 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationResponse.java @@ -0,0 +1,77 @@ +package ru.soune.nocopy.dto.violation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import ru.soune.nocopy.entity.violation.Violation; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@Builder +public class ViolationResponse { + + @JsonProperty("violations") + private List violations; + + @JsonProperty("total_elements") + private long totalElements; + + @JsonProperty("total_pages") + private int totalPages; + + @JsonProperty("current_page") + private int currentPage; + + @JsonProperty("page_size") + private int pageSize; + + @JsonProperty("has_next") + private boolean hasNext; + + @JsonProperty("has_previous") + private boolean hasPrevious; + + @Data + @Builder + public static class ViolationDto { + + @JsonProperty("id") + private Long id; + + @JsonProperty("url") + private String url; + + @JsonProperty("page_url") + private String pageUrl; + + @JsonProperty("page_title") + private String pageTitle; + + @JsonProperty("host") + private String host; + + @JsonProperty("status") + private String status; + + @JsonProperty("created_date") + private LocalDateTime createdDate; + + @JsonProperty("file_id") + private String fileId; + + public static ViolationDto fromEntity(Violation violation) { + return ViolationDto.builder() + .id(violation.getId()) + .url(violation.getUrl()) + .pageUrl(violation.getPageUrl()) + .pageTitle(violation.getPageTitle()) + .host(violation.getHost()) + .status(violation.getStatus()) + .createdDate(violation.getCreatedDate()) + .fileId(violation.getFileEntity() != null ? violation.getFileEntity().getId() : null) + .build(); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/file/Violation.java b/src/main/java/ru/soune/nocopy/entity/file/Violation.java deleted file mode 100644 index c5317bc..0000000 --- a/src/main/java/ru/soune/nocopy/entity/file/Violation.java +++ /dev/null @@ -1,57 +0,0 @@ -package ru.soune.nocopy.entity.file; - -import jakarta.persistence.*; -import lombok.*; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import ru.soune.nocopy.entity.user.User; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "violations") -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@ToString -@EntityListeners(AuditingEntityListener.class) -public class Violation { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long violationId; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - @ToString.Exclude - private User user; - - @Column(name = "violation_type", nullable = false, length = 50) - private String violationType; - - @Column(name = "description", columnDefinition = "TEXT") - private String description; - - @Column(name = "content_id") - private Long contentId; - - @Column(name = "status", nullable = false, length = 20) - private String status = "new"; - - @Column(name = "severity", length = 20) - private String severity; - - @Column(name = "resolved_at") - private LocalDateTime resolvedAt; - - @Column(name = "resolved_by") - private Long resolvedBy; - - @Column(name = "resolution_notes", columnDefinition = "TEXT") - private String resolutionNotes; - - @CreatedDate - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; -} diff --git a/src/main/java/ru/soune/nocopy/entity/search/GlobalSearchResult.java b/src/main/java/ru/soune/nocopy/entity/search/GlobalSearchResult.java index ab8661f..bd0af95 100644 --- a/src/main/java/ru/soune/nocopy/entity/search/GlobalSearchResult.java +++ b/src/main/java/ru/soune/nocopy/entity/search/GlobalSearchResult.java @@ -23,10 +23,5 @@ public class GlobalSearchResult { private String fileName; - private String thumbnail; - private String fileStatus; - - @Column(length = 5000) - private String searchResults; } diff --git a/src/main/java/ru/soune/nocopy/entity/user/User.java b/src/main/java/ru/soune/nocopy/entity/user/User.java index cd8febd..0c58638 100644 --- a/src/main/java/ru/soune/nocopy/entity/user/User.java +++ b/src/main/java/ru/soune/nocopy/entity/user/User.java @@ -8,7 +8,6 @@ import org.hibernate.annotations.ColumnDefault; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import ru.soune.nocopy.entity.company.Company; -import ru.soune.nocopy.entity.file.Violation; import ru.soune.nocopy.entity.tarif.TariffInfo; import ru.soune.nocopy.entity.tarif.TariffStatus; @@ -81,11 +80,6 @@ public class User { @ToString.Exclude private List userContents = new ArrayList<>(); - @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonIgnore - @ToString.Exclude - private List violations = new ArrayList<>(); - @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnore private ProtectedFileCheck protectedFileCheck; diff --git a/src/main/java/ru/soune/nocopy/entity/violation/Violation.java b/src/main/java/ru/soune/nocopy/entity/violation/Violation.java new file mode 100644 index 0000000..2c65045 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/violation/Violation.java @@ -0,0 +1,54 @@ +package ru.soune.nocopy.entity.violation; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.search.GlobalSearchResult; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "violation") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class Violation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "url", nullable = false, unique = true) + private String url; + + @Column(name = "page_url") + private String pageUrl; + + @Column(name = "title") + private String pageTitle; + + @Column(name = "host") + private String host; + + @Column(name = "status") + private String status; + + @CreatedDate + @Column(name = "created_at", updatable = false) + private LocalDateTime createdDate; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "file") + @ToString.Exclude + @JsonIgnore + private FileEntity fileEntity; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "global_search_result") + @ToString.Exclude + @JsonIgnore + private GlobalSearchResult globalSearchResult; +} diff --git a/src/main/java/ru/soune/nocopy/exception/ViolationNotFoundException.java b/src/main/java/ru/soune/nocopy/exception/ViolationNotFoundException.java new file mode 100644 index 0000000..b088ed1 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/ViolationNotFoundException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class ViolationNotFoundException extends RuntimeException { + public ViolationNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/GetViolationsHandler.java b/src/main/java/ru/soune/nocopy/handler/GetViolationsHandler.java deleted file mode 100644 index edd261e..0000000 --- a/src/main/java/ru/soune/nocopy/handler/GetViolationsHandler.java +++ /dev/null @@ -1,85 +0,0 @@ -package ru.soune.nocopy.handler; - -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import ru.soune.nocopy.dto.BaseRequest; -import ru.soune.nocopy.dto.BaseResponse; -import ru.soune.nocopy.dto.MessageCode; -import ru.soune.nocopy.dto.monitoring.GetViolationsRequest; -import ru.soune.nocopy.dto.monitoring.ViolationResponse; -import ru.soune.nocopy.entity.monitoring.FoundViolationEntity; -import ru.soune.nocopy.entity.user.User; -import ru.soune.nocopy.repository.FoundViolationRepository; -import ru.soune.nocopy.service.register.AuthService; - -import java.util.List; -import java.util.stream.Collectors; - -@Slf4j -@Component -@RequiredArgsConstructor -public class GetViolationsHandler implements RequestHandler { - private final ObjectMapper objectMapper; - - private final FoundViolationRepository violationRepository; - - private final AuthService authService; - - @Override - public BaseResponse handle(BaseRequest request) throws Exception { - GetViolationsRequest violationsRequest = objectMapper.convertValue( - request.getMessageBody(), GetViolationsRequest.class); - - String authToken = violationsRequest.getAuthToken(); - - //TODO add return for null token - if (authToken == null) { - return new BaseResponse(0, 0, "", new Object()); - } - - User user = authService.getAuthToken(authToken).getUser(); - List violations; - - if (violationsRequest.getFileId() != null) { - violations = violationRepository.findByFileIdOrderByFoundAtDesc( - violationsRequest.getFileId()); - } else { - violations = violationRepository.findByUserIdOrderByFoundAtDesc(user.getId()); - } - - if (violationsRequest.isMarkAsRead()) { - violations.stream() - .filter(FoundViolationEntity::isNew) - .forEach(v -> v.setNew(false)); - violationRepository.saveAll(violations); - } - - List response = violations.stream() - .map(this::mapToResponse) - .collect(Collectors.toList()); - - return new BaseResponse( - request.getMsgId(), - MessageCode.SUCCESS.getCode(), - "Violations retrieved", - response - ); - } - - private ViolationResponse mapToResponse(FoundViolationEntity entity) { - return ViolationResponse.builder() - .id(entity.getId()) - .fileId(entity.getFileId()) - .fileName(entity.getFileName()) - .foundUrl(entity.getFoundUrl()) - .pageUrl(entity.getPageUrl()) - .pageTitle(entity.getPageTitle()) - .host(entity.getHost()) - .searchEngine(entity.getSearchEngine()) - .foundAt(entity.getFoundAt()) - .isNew(entity.isNew()) - .build(); - } -} diff --git a/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java b/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java new file mode 100644 index 0000000..9989e50 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java @@ -0,0 +1,139 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.violation.ViolationRequest; +import ru.soune.nocopy.dto.violation.ViolationResponse; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.violation.Violation; +import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.service.violation.ViolationService; + +import java.io.FileNotFoundException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.List; +import java.util.Map; + +@Component +@Slf4j +@RequiredArgsConstructor +public class ViolationHandler implements RequestHandler { + private final ObjectMapper objectMapper; + + private final ViolationService violationService; + + private final FileEntityRepository fileRepository; + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + ViolationRequest violationRequest = objectMapper.convertValue(request.getMessageBody(), ViolationRequest.class); + + validateRequest(violationRequest); + //TODO add response + FileEntity file = fileRepository.findById(violationRequest.getFileId()) + .orElseThrow(() -> new FileNotFoundException("File not found with id: " + violationRequest.getFileId())); + + LocalDateTime startDate = null; + LocalDateTime endDate = null; + + if (violationRequest.getStartDate() != null && violationRequest.getEndDate() != null) { + startDate = parseDate(violationRequest.getStartDate()); + endDate = parseDate(violationRequest.getEndDate()); + } + + if ("group".equalsIgnoreCase(violationRequest.getAction())) { + Map groupedData = violationService.getGroupedViolations( + file, + violationRequest.getGroupBy(), + violationRequest.getStatus(), + startDate, + endDate + ); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(groupedData) + .build(); + } + // Пагинация + else { + Page violationPage; + + if (startDate != null && endDate != null) { + violationPage = violationService.getViolationsByFileAndDateRange( + file, startDate, endDate, + violationRequest.getPage(), violationRequest.getSize(), violationRequest.getSortDirection() + ); + } else if (violationRequest.getStatus() != null && !violationRequest.getStatus().isEmpty()) { + violationPage = violationService.getViolationsByFileAndStatus( + file, violationRequest.getStatus(), + violationRequest.getPage(), violationRequest.getSize(), violationRequest.getSortDirection() + ); + } else { + violationPage = violationService.getViolationsByFile( + file, + violationRequest.getPage(), violationRequest.getSize(), violationRequest.getSortDirection() + ); + } + + List violationDtos = violationPage.getContent() + .stream() + .map(ViolationResponse.ViolationDto::fromEntity) + .toList(); + + ViolationResponse response = ViolationResponse.builder() + .violations(violationDtos) + .totalElements(violationPage.getTotalElements()) + .totalPages(violationPage.getTotalPages()) + .currentPage(violationPage.getNumber()) + .pageSize(violationPage.getSize()) + .hasNext(violationPage.hasNext()) + .hasPrevious(violationPage.hasPrevious()) + .build(); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(response) + .build(); + } + } + + //TODO add response exception + private void validateRequest(ViolationRequest request) { + if (request.getFileId() == null) { + return; + } + + if (request.getPage() < 0) { + request.setPage(0); + } + + if (request.getSize() <= 0 || request.getSize() > 100) { + request.setSize(10); + } + + if (!request.getSortDirection().equalsIgnoreCase("asc") && + !request.getSortDirection().equalsIgnoreCase("desc")) { + request.setSortDirection("desc"); + } + } + + //TODO add response exception + private LocalDateTime parseDate(String dateStr) { + try { + return LocalDateTime.parse(dateStr, DATE_FORMATTER); + } catch (DateTimeParseException e) { + log.error(e.getMessage()); + return null; + } + } +} diff --git a/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java b/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java new file mode 100644 index 0000000..b73ec54 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java @@ -0,0 +1,39 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.violation.Violation; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository +public interface ViolationRepository extends JpaRepository { + + Page findByFileEntity(FileEntity file, Pageable pageable); + + Page findByFileEntityAndStatus(FileEntity file, String status, Pageable pageable); + + Page findByFileEntityAndCreatedDateBetween(FileEntity file, LocalDateTime startDate, + LocalDateTime endDate, Pageable pageable); + + List findByFileEntity(FileEntity file); + + List findByFileEntityAndStatus(FileEntity file, String status); + + List findByFileEntityAndCreatedDateBetween(FileEntity file, LocalDateTime startDate, + LocalDateTime endDate); + + List findByFileEntityAndStatusAndCreatedDateBetween(FileEntity file, String status, + LocalDateTime startDate, LocalDateTime endDate); + + boolean existsByUrl(String url); + + Optional findById(Long id); + + void deleteByFileEntity(FileEntity file); +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java index 9fa44d8..5eedfa5 100644 --- a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java @@ -3,32 +3,28 @@ package ru.soune.nocopy.service.monitoring; import jakarta.mail.MessagingException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.soune.nocopy.dto.file.YandexSearchResponse; 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.FoundViolationEntity; import ru.soune.nocopy.entity.monitoring.MonitoringType; -import ru.soune.nocopy.entity.monitoring.SearchEngine; +import ru.soune.nocopy.entity.search.GlobalSearchResult; 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.FoundViolationRepository; 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.List; -import java.util.UUID; import java.util.concurrent.TimeoutException; @Slf4j @@ -37,8 +33,6 @@ import java.util.concurrent.TimeoutException; public class MonitoringSearchService { private final SearchImageService searchImageService; - private final FoundViolationRepository violationRepository; - private final FileMonitoringRepository monitoringRepository; private final TariffInfoService tariffInfoService; @@ -49,18 +43,16 @@ public class MonitoringSearchService { private final UserRepository userRepository; + private final ViolationService violationService; + @Transactional public void processFileSearch(FileMonitoringEntity monitoring) throws MessagingException, IOException { - String searchSessionId = UUID.randomUUID().toString(); - log.info("Starting search session: {} for file: {}", searchSessionId, monitoring.getFile().getId()); - 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()); @@ -70,7 +62,9 @@ public class MonitoringSearchService { List yandexImages = searchImageService.getAllImagesWithoutPagination(yandexResponse, "visual_matches"); - int savedCount = saveFoundImages(monitoring, yandexImages, SearchEngine.YANDEX, searchSessionId); + for (YandexSearchResponse.ImageResult imageResult : yandexImages) { + violationService.processViolation(imageResult, monitoring.getFile(), null); + } try { String googleResponse = searchImageService.searchReverseByPublicUrl( @@ -79,13 +73,16 @@ public class MonitoringSearchService { List googleImages = searchImageService.getAllImagesWithoutPagination(googleResponse, "exact_matches"); - savedCount += saveFoundImages(monitoring, googleImages, SearchEngine.GOOGLE, searchSessionId); + + for (YandexSearchResponse.ImageResult imageResult : googleImages) { + violationService.processViolation(imageResult, monitoring.getFile(), null); + } } catch (TimeoutException | IOException e) { - log.warn("Google search failed for session: {}", searchSessionId, e); + log.warn("Google search failed"); } - monitoring.setLastRunStatus("SUCCESS - Found " + savedCount + " violations"); + monitoring.setLastRunStatus("SUCCESS"); } catch (TariffNotFoundException e) { User user = userRepository.findById(monitoring.getUserId()).orElseThrow(); TariffInfo activeTariffInfo = user.getActiveTariffInfo(); @@ -97,7 +94,6 @@ public class MonitoringSearchService { monitoringRepository.save(monitoring); } catch (Exception e) { - log.error("Search failed for session: {}", searchSessionId, e); monitoring.setLastRunStatus("ERROR: " + e.getMessage()); } finally { updateNextRun(monitoring); @@ -105,42 +101,6 @@ public class MonitoringSearchService { } } - private int saveFoundImages(FileMonitoringEntity monitoring, - List images, - SearchEngine engine, - String sessionId) { - int savedCount = 0; - - for (YandexSearchResponse.ImageResult image : images) { - List existing = violationRepository.findDuplicate( - monitoring.getFile().getId(), image.getUrl()); - - if (existing.isEmpty()) { - FoundViolationEntity violation = FoundViolationEntity.builder() - .file(monitoring.getFile()) - .fileId(monitoring.getFile().getId()) - .fileName(monitoring.getFile().getOriginalFileName()) - .userId(monitoring.getUserId()) - .foundUrl(image.getUrl()) - .pageUrl(image.getPageUrl()) - .pageTitle(image.getPageTitle()) - .host(image.getHost()) - .imageWidth(image.getWidth()) - .imageHeight(image.getHeight()) - .searchEngine(engine) - .searchSessionId(sessionId) - .isNew(true) - .isNotified(false) - .build(); - - violationRepository.save(violation); - savedCount++; - } - } - - return savedCount; - } - private void updateNextRun(FileMonitoringEntity monitoring) { LocalDateTime nextRun = calculateNextRun(monitoring.getMonitoringType()); monitoring.setNextRun(nextRun); diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java index cd55272..ab85231 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -1,6 +1,7 @@ package ru.soune.nocopy.service.search; import com.fasterxml.jackson.databind.ObjectMapper; +import com.vrt.fileprotection.image.ImageCheckResult; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; @@ -11,10 +12,13 @@ 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.entity.violation.Violation; import ru.soune.nocopy.repository.GlobalSearchResultRepository; import ru.soune.nocopy.repository.GlobalSearchTaskRepository; +import ru.soune.nocopy.repository.ViolationRepository; 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; @@ -38,6 +42,10 @@ public class GlobalSearchAsyncProcessor { private final GlobalSearchTaskRepository globalSearchTaskRepository; + private final ViolationRepository violationRepository; + + private final ViolationService violationService; + @Async @Transactional public void processFilesAsync(String taskId, List filesToProcess, Long userId) { @@ -74,7 +82,6 @@ public class GlobalSearchAsyncProcessor { result.setTaskId(taskId); result.setFileId(file.getId()); result.setFileName(file.getOriginalFileName()); - result.setThumbnail(file.getThumbnailPath()); List allUniqueImages = new ArrayList<>(); boolean hasTimeout = false; @@ -117,21 +124,10 @@ public class GlobalSearchAsyncProcessor { allUniqueImages.stream() .filter(img -> img.getUrl() != null) .collect(Collectors.toList()), - new ArrayList<>() - ); + new ArrayList<>()); - YandexSearchResponse searchResponse = new YandexSearchResponse(); - searchResponse.setImages(allUniqueImages); - searchResponse.setPage(1); - searchResponse.setPageSize(allUniqueImages.size()); - searchResponse.setTotalResults(allUniqueImages.size()); - searchResponse.setTotalPages(1); - - try { - result.setSearchResults(objectMapper.writeValueAsString(searchResponse)); - } catch (Exception e) { - log.error("Failed to serialize search results", e); - result.setSearchResults("{}"); + for (YandexSearchResponse.ImageResult imageResult : allUniqueImages) { + violationService.processViolation(imageResult, file, result); } if (allUniqueImages.isEmpty() && hasTimeout) { diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java new file mode 100644 index 0000000..9d8660d --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java @@ -0,0 +1,113 @@ +package ru.soune.nocopy.service.violation; + +import lombok.AllArgsConstructor; +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 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.violation.Violation; +import ru.soune.nocopy.exception.ViolationNotFoundException; +import ru.soune.nocopy.repository.ViolationRepository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@AllArgsConstructor +public class ViolationService { + + private final ViolationRepository violationRepository; + + public void processViolation(YandexSearchResponse.ImageResult imageResult, FileEntity file, + GlobalSearchResult result) { + String url = imageResult.getUrl(); + + if (!violationRepository.existsByUrl(url)) { + Violation violation = new Violation(); + + if (result != null) { + violation.setGlobalSearchResult(result); + } + + violation.setHost(imageResult.getHost()); + violation.setUrl(url); + violation.setPageUrl(imageResult.getPageUrl()); + violation.setPageTitle(imageResult.getPageTitle()); + violation.setFileEntity(file); + violation.setStatus("CREATED"); + + violationRepository.save(violation); + } + } + + public List violationsByFileId(FileEntity file) { + return violationRepository.findByFileEntity(file); + } + + public void changeStatusViolation(Long violationId, String status) { + Violation violation = violationRepository.findById(violationId) + .orElseThrow(() -> new ViolationNotFoundException("Violation not found with id: " + violationId)); + violation.setStatus(status); + violationRepository.save(violation); + } + + public Page getViolationsByFile(FileEntity file, 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.findByFileEntity(file, pageable); + } + + public Page getViolationsByFileAndStatus(FileEntity file, 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.findByFileEntityAndStatus(file, status, pageable); + } + + public Page getViolationsByFileAndDateRange(FileEntity file, 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.findByFileEntityAndCreatedDateBetween(file, startDate, endDate, pageable); + } + + public Map getGroupedViolations(FileEntity file, String groupBy, String status, + LocalDateTime startDate, LocalDateTime endDate) { + + List violations; + + if (status != null && !status.isEmpty() && startDate != null && endDate != null) { + violations = violationRepository.findByFileEntityAndStatusAndCreatedDateBetween(file, status, startDate, endDate); + } else if (status != null && !status.isEmpty()) { + violations = violationRepository.findByFileEntityAndStatus(file, status); + } else if (startDate != null && endDate != null) { + violations = violationRepository.findByFileEntityAndCreatedDateBetween(file, startDate, endDate); + } else { + violations = violationRepository.findByFileEntity(file); + } + + return violations.stream() + .collect(Collectors.groupingBy( + v -> extractGroupKey(v.getUrl(), groupBy), + Collectors.counting() + )); + } + + 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"; + } + } +} \ No newline at end of file From 1b54badd006bc35b605ff40fdb9aefca9556c29f Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 11 Mar 2026 12:48:54 +0700 Subject: [PATCH 154/274] dev add config for nginx --- .../ru/soune/nocopy/service/search/SearchImageService.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index d25a633..5b64553 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -56,8 +56,7 @@ public class SearchImageService { .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) - .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) - +// .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) .build(); this.googleHttpClient = new OkHttpClient.Builder() @@ -68,7 +67,7 @@ public class SearchImageService { .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) - .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) +// .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) .build(); this.objectMapper = new ObjectMapper(); From 9d61f8c88fde65fe6e4ee3e1230fc37ad30ae510 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 11 Mar 2026 13:04:56 +0700 Subject: [PATCH 155/274] dev add config for nginx --- .../nocopy/service/search/SearchImageService.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 5b64553..48a4aa2 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -142,10 +142,17 @@ public class SearchImageService { .addQueryParameter("t_", String.valueOf(System.currentTimeMillis())) .build(); +// Request request = new Request.Builder() +// .url(url) +// .header("Accept", "application/json") +// .header("User-Agent", "Mozilla/5.0") +// .build(); + Request request = new Request.Builder() .url(url) - .header("Accept", "application/json") - .header("User-Agent", "Mozilla/5.0") + .header("Accept", "*/*") + .header("User-Agent", "curl/8.5.0") + .header("Host", "www.searchapi.io") .build(); long start = System.currentTimeMillis(); From 832b13e088c3d50541ad3c528ef1400b7e9827c7 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 11 Mar 2026 13:15:21 +0700 Subject: [PATCH 156/274] dev add config for nginx --- .../nocopy/service/search/SearchImageService.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 48a4aa2..ccd1bea 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -16,10 +16,7 @@ import ru.soune.nocopy.exception.NotValidFieldException; import java.io.*; 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 @@ -56,6 +53,7 @@ public class SearchImageService { .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) + .protocols(Arrays.asList(Protocol.HTTP_1_1)) // .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) .build(); @@ -67,6 +65,7 @@ public class SearchImageService { .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) + .protocols(Arrays.asList(Protocol.HTTP_1_1)) // .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) .build(); @@ -151,8 +150,8 @@ public class SearchImageService { Request request = new Request.Builder() .url(url) .header("Accept", "*/*") - .header("User-Agent", "curl/8.5.0") - .header("Host", "www.searchapi.io") +// .header("User-Agent", "curl/8.5.0") +// .header("Host", "www.searchapi.io") .build(); long start = System.currentTimeMillis(); From b5ca777722145cd2402b63a229df0d6b2ecb2b49 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 11 Mar 2026 15:02:34 +0700 Subject: [PATCH 157/274] dev add config for nginx --- .../nocopy/service/search/SearchImageService.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index ccd1bea..fd18efd 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -15,6 +15,8 @@ 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.*; import java.util.concurrent.*; @@ -45,7 +47,11 @@ public class SearchImageService { // } public SearchImageService() { + Proxy proxy = new Proxy(Proxy.Type.HTTP, + new InetSocketAddress("193.46.217.94", 3128)); + this.yandexHttpClient = new OkHttpClient.Builder() + .proxy(proxy) .connectTimeout(35, TimeUnit.SECONDS) .writeTimeout(35, TimeUnit.SECONDS) .readTimeout(35, TimeUnit.SECONDS) @@ -58,6 +64,7 @@ public class SearchImageService { .build(); this.googleHttpClient = new OkHttpClient.Builder() + .proxy(proxy) .connectTimeout(15, TimeUnit.SECONDS) .writeTimeout(15, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) @@ -150,8 +157,9 @@ public class SearchImageService { Request request = new Request.Builder() .url(url) .header("Accept", "*/*") -// .header("User-Agent", "curl/8.5.0") -// .header("Host", "www.searchapi.io") + .header("User-Agent", "PostmanRuntime/7.29.2") + .header("Accept-Encoding", "gzip, deflate") + .header("Connection", "keep-alive") .build(); long start = System.currentTimeMillis(); From 54db3d78b41e81189ba6d6c6cc9c5f7ba6024a68 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 11 Mar 2026 15:17:35 +0700 Subject: [PATCH 158/274] dev add config for nginx --- README.md | 103 +++++++++++++++++- .../service/search/SearchImageService.java | 28 ++--- 2 files changed, 116 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 2e80afb..7e64986 100644 --- a/README.md +++ b/README.md @@ -388,4 +388,105 @@ sudo ln -s /etc/nginx/sites-available/not-copy /etc/nginx/sites-enabled/ sudo nginx -t # Рестарт -sudo systemctl reload nginx \ No newline at end of file +sudo systemctl reload nginx + + +--------------------------------------------- + +# ПОЛНАЯ ИНСТРУКЦИЯ: НАСТРОЙКА ПРОКСИ ДЛЯ ОБХОДА БЛОКИРОВОК SEARCHAPI.IO + +## Данные для подключения к прокси-серверу +IP: 193.46.217.94 +Порт HTTP: 3128 +Порт SOCKS: 1080 +Логин SSH: root +Пароль SSH: u7nRtsD6 + +## 1. Настройка прокси-сервера +```bash +# Подключаемся к серверу +ssh root@193.46.217.94 +# Вводим пароль: u7nRtsD6 + +# Обновляем систему и устанавливаем пакеты +yum update -y +yum install -y epel-release net-tools nano +yum install -y 3proxy + +# Создаем конфигурацию +mkdir -p /etc/3proxy +cat > /etc/3proxy/3proxy.cfg << 'EOF' +nserver 8.8.8.8 +nserver 8.8.4.4 +timeouts 1 5 30 60 180 1800 15 60 +daemon +log /var/log/3proxy/3proxy.log +logformat "- +_L%t.%. %N.%p %E %U %C:%c %R:%r %O %I %h %T" +rotate 30 +auth none +allow * * * * * +proxy -p3128 +socks -p1080 +EOF + +# Создаем папку для логов и запускаем прокси +mkdir -p /var/log/3proxy +/usr/bin/3proxy /etc/3proxy/3proxy.cfg + +# Проверяем, что запустилось +ps aux | grep 3proxy +netstat -tulpn | grep -E '3128|1080' + +# Настраиваем файрвол +firewall-cmd --permanent --add-port=3128/tcp +firewall-cmd --permanent --add-port=1080/tcp +firewall-cmd --reload +firewall-cmd --list-ports + +# Проверяем локальную работу прокси +curl -x http://127.0.0.1:3128 https://api.ipify.org +# Должен вернуть: 193.46.217.94 + +# Настраиваем автозапуск +echo "/usr/bin/3proxy /etc/3proxy/3proxy.cfg" >> /etc/rc.local +chmod +x /etc/rc.local + + +# Проверяем доступность прокси +ping -c 4 193.46.217.94 +curl -x http://193.46.217.94:3128 https://api.ipify.org +# Должен вернуть: 193.46.217.94 + +# Проверяем работу с SearchAPI через прокси +curl -x http://193.46.217.94:3128 "https://www.searchapi.io/api/v1/search?engine=google_lens&api_key=5jyYZC8jSaxhZTwjMUhwtAXi&url=https://dev-workspace.not-copy.com/api/files/public/76d06557-6df1-4fcd-a273-050ec6a35faf&search_type=exact_matches" -o test.json +ls -la test.json +tail -20 test.json +# Файл должен быть полным (заканчиваться на '}') + + + +ps aux | grep 3proxy # Проверка статуса +netstat -tulpn | grep -E '3128|1080' # Проверка портов +tail -f /var/log/3proxy/3proxy.log # Просмотр логов +killall 3proxy # Остановка прокси +/usr/bin/3proxy /etc/3proxy/3proxy.cfg # Запуск прокси + + +firewall-cmd --permanent --remove-port=3128/tcp +firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="IP_ТВОЕГО_СЕРВЕРА" port port="3128" protocol="tcp" accept' +firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="IP_ТВОЕГО_СЕРВЕРА" port port="1080" protocol="tcp" accept' +firewall-cmd --reload + + +На прокси-сервере: +bash +ps aux | grep 3proxy # Проверка статуса +netstat -tulpn | grep -E '3128|1080' # Проверка портов +tail -f /var/log/3proxy/3proxy.log # Просмотр логов +killall 3proxy # Остановка прокси +/usr/bin/3proxy /etc/3proxy/3proxy.cfg # Запуск прокси + +На основном сервере: +bash +curl -x http://193.46.217.94:3128 https://api.ipify.org # Проверка прокси +telnet 193.46.217.94 3128 # Проверка порта \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index fd18efd..5d59ff3 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -59,8 +59,8 @@ public class SearchImageService { .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) - .protocols(Arrays.asList(Protocol.HTTP_1_1)) -// .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) +// .protocols(Arrays.asList(Protocol.HTTP_1_1)) + .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) .build(); this.googleHttpClient = new OkHttpClient.Builder() @@ -72,8 +72,8 @@ public class SearchImageService { .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) - .protocols(Arrays.asList(Protocol.HTTP_1_1)) -// .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) +// .protocols(Arrays.asList(Protocol.HTTP_1_1)) + .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) .build(); this.objectMapper = new ObjectMapper(); @@ -148,20 +148,20 @@ public class SearchImageService { .addQueryParameter("t_", String.valueOf(System.currentTimeMillis())) .build(); -// Request request = new Request.Builder() -// .url(url) -// .header("Accept", "application/json") -// .header("User-Agent", "Mozilla/5.0") -// .build(); - Request request = new Request.Builder() .url(url) - .header("Accept", "*/*") - .header("User-Agent", "PostmanRuntime/7.29.2") - .header("Accept-Encoding", "gzip, deflate") - .header("Connection", "keep-alive") + .header("Accept", "application/json") + .header("User-Agent", "Mozilla/5.0") .build(); +// Request request = new Request.Builder() +// .url(url) +// .header("Accept", "*/*") +// .header("User-Agent", "PostmanRuntime/7.29.2") +// .header("Accept-Encoding", "gzip, deflate") +// .header("Connection", "keep-alive") +// .build(); + long start = System.currentTimeMillis(); try (Response response = client.newCall(request).execute()) { From 7fb96d4b51c077f8ed8d705ae79dadc84c40f0aa Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 11 Mar 2026 15:29:10 +0700 Subject: [PATCH 159/274] dev add config for nginx --- .../service/search/SearchImageService.java | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 5d59ff3..c397e55 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -27,24 +27,9 @@ public class SearchImageService { private final ObjectMapper objectMapper; -// private final OkHttpClient httpClient; private final OkHttpClient yandexHttpClient; - private final OkHttpClient googleHttpClient; -// public SearchImageService() { -// this.httpClient = new OkHttpClient.Builder() -// .connectTimeout(18, TimeUnit.SECONDS) -// .writeTimeout(18, TimeUnit.SECONDS) -// .readTimeout(18, TimeUnit.SECONDS) -// .callTimeout(18, TimeUnit.SECONDS) -// .followRedirects(true) -// .followSslRedirects(true) -// .retryOnConnectionFailure(true) -// .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) -// .build(); -// -// this.objectMapper = new ObjectMapper(); -// } + private final OkHttpClient googleHttpClient; public SearchImageService() { Proxy proxy = new Proxy(Proxy.Type.HTTP, @@ -59,7 +44,6 @@ public class SearchImageService { .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) -// .protocols(Arrays.asList(Protocol.HTTP_1_1)) .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) .build(); @@ -72,7 +56,6 @@ public class SearchImageService { .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) -// .protocols(Arrays.asList(Protocol.HTTP_1_1)) .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) .build(); @@ -154,14 +137,6 @@ public class SearchImageService { .header("User-Agent", "Mozilla/5.0") .build(); -// Request request = new Request.Builder() -// .url(url) -// .header("Accept", "*/*") -// .header("User-Agent", "PostmanRuntime/7.29.2") -// .header("Accept-Encoding", "gzip, deflate") -// .header("Connection", "keep-alive") -// .build(); - long start = System.currentTimeMillis(); try (Response response = client.newCall(request).execute()) { From b04a357ccaf5d2c023b75e14bc793440dcb6fc6e Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 11 Mar 2026 15:42:06 +0700 Subject: [PATCH 160/274] dev add config for nginx --- .../java/ru/soune/nocopy/entity/violation/Violation.java | 5 ++++- .../java/ru/soune/nocopy/repository/ViolationRepository.java | 2 +- .../ru/soune/nocopy/service/violation/ViolationService.java | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/entity/violation/Violation.java b/src/main/java/ru/soune/nocopy/entity/violation/Violation.java index 2c65045..803b43a 100644 --- a/src/main/java/ru/soune/nocopy/entity/violation/Violation.java +++ b/src/main/java/ru/soune/nocopy/entity/violation/Violation.java @@ -21,9 +21,12 @@ public class Violation { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(name = "url", nullable = false, unique = true) + @Column(name = "url", nullable = false, columnDefinition = "TEXT") private String url; + @Column(name = "url_hash", unique = true) + private String urlHash; + @Column(name = "page_url") private String pageUrl; diff --git a/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java b/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java index b73ec54..ccc6f14 100644 --- a/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java @@ -31,7 +31,7 @@ public interface ViolationRepository extends JpaRepository { List findByFileEntityAndStatusAndCreatedDateBetween(FileEntity file, String status, LocalDateTime startDate, LocalDateTime endDate); - boolean existsByUrl(String url); + boolean existsByUrlHash(String urlHash); Optional findById(Long id); diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java index 9d8660d..40aa0c4 100644 --- a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java @@ -1,6 +1,7 @@ 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; @@ -27,8 +28,9 @@ public class ViolationService { public void processViolation(YandexSearchResponse.ImageResult imageResult, FileEntity file, GlobalSearchResult result) { String url = imageResult.getUrl(); + String urlHash = DigestUtils.sha256Hex(url); - if (!violationRepository.existsByUrl(url)) { + if (!violationRepository.existsByUrlHash(urlHash)) { Violation violation = new Violation(); if (result != null) { @@ -37,6 +39,7 @@ public class ViolationService { violation.setHost(imageResult.getHost()); violation.setUrl(url); + violation.setUrlHash(urlHash); violation.setPageUrl(imageResult.getPageUrl()); violation.setPageTitle(imageResult.getPageTitle()); violation.setFileEntity(file); From 5b0c6ce5bd90e78196c564bbb3bd15c2ae8240f5 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 11 Mar 2026 15:49:39 +0700 Subject: [PATCH 161/274] dev add config for nginx --- .../soune/nocopy/service/search/GlobalSearchAsyncProcessor.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java index ab85231..fb39b84 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -83,6 +83,8 @@ public class GlobalSearchAsyncProcessor { result.setFileId(file.getId()); result.setFileName(file.getOriginalFileName()); + result = globalSearchResultRepository.save(result); + List allUniqueImages = new ArrayList<>(); boolean hasTimeout = false; From edbbeab9be66654545cc0826e7d5be6d95fd8393 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 11 Mar 2026 16:02:47 +0700 Subject: [PATCH 162/274] dev add config for nginx --- src/main/java/ru/soune/nocopy/entity/violation/Violation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/entity/violation/Violation.java b/src/main/java/ru/soune/nocopy/entity/violation/Violation.java index 803b43a..8762528 100644 --- a/src/main/java/ru/soune/nocopy/entity/violation/Violation.java +++ b/src/main/java/ru/soune/nocopy/entity/violation/Violation.java @@ -24,7 +24,7 @@ public class Violation { @Column(name = "url", nullable = false, columnDefinition = "TEXT") private String url; - @Column(name = "url_hash", unique = true) + @Column(name = "url_hash", nullable = false, unique = true) private String urlHash; @Column(name = "page_url") From a39363a417234286270a4dda5e7e41e43904b39b Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 11 Mar 2026 16:16:29 +0700 Subject: [PATCH 163/274] dev add config for nginx --- .../java/ru/soune/nocopy/entity/violation/Violation.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/entity/violation/Violation.java b/src/main/java/ru/soune/nocopy/entity/violation/Violation.java index 8762528..3b329f2 100644 --- a/src/main/java/ru/soune/nocopy/entity/violation/Violation.java +++ b/src/main/java/ru/soune/nocopy/entity/violation/Violation.java @@ -24,16 +24,16 @@ public class Violation { @Column(name = "url", nullable = false, columnDefinition = "TEXT") private String url; - @Column(name = "url_hash", nullable = false, unique = true) + @Column(name = "url_hash", nullable = false, unique = true, columnDefinition = "TEXT") private String urlHash; - @Column(name = "page_url") + @Column(name = "page_url", columnDefinition = "TEXT") private String pageUrl; - @Column(name = "title") + @Column(name = "title", columnDefinition = "TEXT") private String pageTitle; - @Column(name = "host") + @Column(name = "host", columnDefinition = "TEXT") private String host; @Column(name = "status") From 087b9f7b33197bd37d355d37ecbeec4d06f330d8 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 11 Mar 2026 16:20:56 +0700 Subject: [PATCH 164/274] dev delete --- .../monitoring/MonitoringStatusResponse.java | 3 - .../monitoring/FoundViolationEntity.java | 76 ------------------- .../repository/FoundViolationRepository.java | 30 -------- .../monitoring/FileMonitoringService.java | 5 -- 4 files changed, 114 deletions(-) delete mode 100644 src/main/java/ru/soune/nocopy/entity/monitoring/FoundViolationEntity.java delete mode 100644 src/main/java/ru/soune/nocopy/repository/FoundViolationRepository.java diff --git a/src/main/java/ru/soune/nocopy/dto/monitoring/MonitoringStatusResponse.java b/src/main/java/ru/soune/nocopy/dto/monitoring/MonitoringStatusResponse.java index e8d8a87..818849d 100644 --- a/src/main/java/ru/soune/nocopy/dto/monitoring/MonitoringStatusResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/monitoring/MonitoringStatusResponse.java @@ -35,9 +35,6 @@ public class MonitoringStatusResponse { @JsonProperty("is_active") private boolean isActive; - @JsonProperty("new_violations_count") - private long newViolationsCount; - @JsonProperty("need_add_tokens") private boolean needAddTokens; } diff --git a/src/main/java/ru/soune/nocopy/entity/monitoring/FoundViolationEntity.java b/src/main/java/ru/soune/nocopy/entity/monitoring/FoundViolationEntity.java deleted file mode 100644 index d91ed8f..0000000 --- a/src/main/java/ru/soune/nocopy/entity/monitoring/FoundViolationEntity.java +++ /dev/null @@ -1,76 +0,0 @@ -package ru.soune.nocopy.entity.monitoring; - -import jakarta.persistence.*; -import lombok.*; -import ru.soune.nocopy.entity.file.FileEntity; - -import java.time.LocalDateTime; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Entity -@Table(name = "found_violations", - indexes = { - @Index(name = "idx_file_id", columnList = "file_id"), - @Index(name = "idx_found_at", columnList = "found_at") - }) -public class FoundViolationEntity { - - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private String id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "file_id", nullable = false) - private FileEntity file; - - @Column(name = "user_id", nullable = false) - private Long userId; - - @Column(name = "file_id_copy", nullable = false) - private String fileId; - - @Column(name = "file_name_copy") - private String fileName; - - @Column(name = "found_url", nullable = false, length = 2048) - private String foundUrl; - - @Column(name = "page_url", length = 2048) - private String pageUrl; - - @Column(name = "page_title") - private String pageTitle; - - @Column(name = "host") - private String host; - - @Column(name = "image_width") - private Integer imageWidth; - - @Column(name = "image_height") - private Integer imageHeight; - - @Enumerated(EnumType.STRING) - @Column(name = "search_engine", nullable = false) - private SearchEngine searchEngine; - - @Column(name = "search_session_id") - private String searchSessionId; - - @Column(name = "found_at", nullable = false) - private LocalDateTime foundAt; - - @Column(name = "is_new", nullable = false) - private boolean isNew = true; - - @Column(name = "is_notified", nullable = false) - private boolean isNotified = false; - - @PrePersist - public void prePersist() { - this.foundAt = LocalDateTime.now(); - } -} diff --git a/src/main/java/ru/soune/nocopy/repository/FoundViolationRepository.java b/src/main/java/ru/soune/nocopy/repository/FoundViolationRepository.java deleted file mode 100644 index c6ed9a5..0000000 --- a/src/main/java/ru/soune/nocopy/repository/FoundViolationRepository.java +++ /dev/null @@ -1,30 +0,0 @@ -package ru.soune.nocopy.repository; - -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; -import ru.soune.nocopy.entity.monitoring.FoundViolationEntity; - -import java.time.LocalDateTime; -import java.util.List; - -@Repository -public interface FoundViolationRepository extends JpaRepository { - - List findByFileIdOrderByFoundAtDesc(String fileId); - - List findByUserIdOrderByFoundAtDesc(Long userId); - - @Query("SELECT fv FROM FoundViolationEntity fv WHERE fv.userId = :userId AND fv.isNew = true") - List findNewViolationsByUser(@Param("userId") Long userId); - - @Query("SELECT fv FROM FoundViolationEntity fv WHERE fv.file.id = :fileId AND fv.foundUrl = :foundUrl") - List findDuplicate(@Param("fileId") String fileId, @Param("foundUrl") String foundUrl); - - List findByFoundAtBetween(LocalDateTime start, LocalDateTime end); - - @Query("SELECT COUNT(fv) FROM FoundViolationEntity fv WHERE fv.userId = :userId AND fv.isNew = true") - long countNewByUserId(@Param("userId") Long userId); -} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java b/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java index 6682377..a098f03 100644 --- a/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java +++ b/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java @@ -15,7 +15,6 @@ 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.FoundViolationRepository; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.tariff.TariffService; @@ -31,8 +30,6 @@ public class FileMonitoringService { private final FileEntityRepository fileRepository; - private final FoundViolationRepository violationRepository; - private final TariffService tariffService; private final UserRepository userRepository; @@ -93,7 +90,6 @@ public class FileMonitoringService { FileMonitoringEntity monitoring = monitoringRepository.findByFileId(fileId).orElse(null); FileEntity file = fileRepository.findById(fileId).orElseThrow(); User user = userRepository.findById(userId).orElseThrow(); - long newViolations = violationRepository.countNewByUserId(userId); return MonitoringStatusResponse.builder() .fileId(fileId) @@ -103,7 +99,6 @@ public class FileMonitoringService { .lastRun(monitoring != null ? monitoring.getLastRun() : null) .lastRunStatus(monitoring != null ? monitoring.getLastRunStatus() : null) .isActive(monitoring != null && monitoring.isActive()) - .newViolationsCount(newViolations) .needAddTokens(checkTokensForMonitoring(user, monitoring.getMonitoringType())) .build(); } From 88bfe51297cb39e6fc2796a7b94913c2d2300be2 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 11 Mar 2026 16:30:26 +0700 Subject: [PATCH 165/274] dev delete --- src/main/java/ru/soune/nocopy/entity/violation/Violation.java | 2 +- src/main/java/ru/soune/nocopy/handler/ViolationHandler.java | 4 +--- .../ru/soune/nocopy/service/violation/ViolationService.java | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/entity/violation/Violation.java b/src/main/java/ru/soune/nocopy/entity/violation/Violation.java index 3b329f2..ad0e4d2 100644 --- a/src/main/java/ru/soune/nocopy/entity/violation/Violation.java +++ b/src/main/java/ru/soune/nocopy/entity/violation/Violation.java @@ -41,7 +41,7 @@ public class Violation { @CreatedDate @Column(name = "created_at", updatable = false) - private LocalDateTime createdDate; + private LocalDateTime createdDate = LocalDateTime.now(); @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "file") diff --git a/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java b/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java index 9989e50..8728e89 100644 --- a/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java @@ -63,9 +63,7 @@ public class ViolationHandler implements RequestHandler { .msgId(request.getMsgId()) .messageBody(groupedData) .build(); - } - // Пагинация - else { + } else { Page violationPage; if (startDate != null && endDate != null) { diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java index 40aa0c4..ab1aa42 100644 --- a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java @@ -96,7 +96,7 @@ public class ViolationService { return violations.stream() .collect(Collectors.groupingBy( - v -> extractGroupKey(v.getUrl(), groupBy), + v -> extractGroupKey(v.getPageUrl(), groupBy), Collectors.counting() )); } From ffa977f06e1f401e5112efa494c9f0f5b0f6971d Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 12 Mar 2026 00:21:19 +0700 Subject: [PATCH 166/274] dev add vio --- .../nocopy/configuration/HandlerConfig.java | 4 +- .../controller/GlobalSearchController.java | 6 -- .../violation/ViolationStatisticsRequest.java | 20 +++++ .../ViolationStatisticsResponse.java | 22 ++++++ .../handler/ViolationStatisticsHandler.java | 77 +++++++++++++++++++ .../repository/ViolationRepository.java | 6 ++ .../service/violation/ViolationService.java | 65 ++++++++++++++++ 7 files changed, 193 insertions(+), 7 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsResponse.java create mode 100644 src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index e159080..223c94d 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -29,7 +29,8 @@ public class HandlerConfig { PaymentHandler paymentHandler, CostHandler costHandler, MonitoringHandler monitoringHandler, - ViolationHandler violationHandler + ViolationHandler violationHandler, + ViolationStatisticsHandler violationStatisticsHandler ) { Map map = new HashMap<>(); map.put(20001, login); @@ -50,6 +51,7 @@ public class HandlerConfig { map.put(30007, monitoringHandler); map.put(30008, costHandler); map.put(30009, violationHandler); + map.put(30010, violationStatisticsHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java b/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java index c388962..2d8078c 100644 --- a/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java +++ b/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java @@ -1,26 +1,20 @@ package ru.soune.nocopy.controller; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import ru.soune.nocopy.dto.file.YandexSearchResponse; -import ru.soune.nocopy.dto.search.GlobalSearchFileResult; import ru.soune.nocopy.dto.search.GlobalSearchStartRequest; import ru.soune.nocopy.dto.search.GlobalSearchStartResponse; import ru.soune.nocopy.dto.search.GlobalSearchStatusResponse; 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.register.AuthService; import ru.soune.nocopy.service.search.GlobalSearchService; import java.util.*; -import java.util.stream.Collectors; @Slf4j @RestController diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsRequest.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsRequest.java new file mode 100644 index 0000000..c9246e7 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsRequest.java @@ -0,0 +1,20 @@ +package ru.soune.nocopy.dto.violation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class ViolationStatisticsRequest { + + @JsonProperty("user_id") + private Long userId; + + @JsonProperty("file_id") + private String fileId; + + @JsonProperty("start_date") + private String startDate; + + @JsonProperty("end_date") + private String endDate; +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsResponse.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsResponse.java new file mode 100644 index 0000000..64d143c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsResponse.java @@ -0,0 +1,22 @@ +package ru.soune.nocopy.dto.violation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ViolationStatisticsResponse { + + @JsonProperty("total_violations") + private long totalViolations; + + @JsonProperty("new_violations") + private long newViolations; + + @JsonProperty("in_progress_violations") + private long inProgressViolations; + + @JsonProperty("resolved_violations") + private long resolvedViolations; +} diff --git a/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java b/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java new file mode 100644 index 0000000..a12b6c5 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java @@ -0,0 +1,77 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.violation.ViolationStatisticsRequest; +import ru.soune.nocopy.dto.violation.ViolationStatisticsResponse; +import ru.soune.nocopy.service.violation.ViolationService; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ViolationStatisticsHandler implements RequestHandler { + private final ObjectMapper objectMapper; + + private final ViolationService violationService; + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + ViolationStatisticsRequest statRequest = objectMapper.convertValue( + request.getMessageBody(), ViolationStatisticsRequest.class); + + validateRequest(statRequest); + + LocalDateTime startDate = null; + LocalDateTime endDate = null; + + if (statRequest.getStartDate() != null && statRequest.getEndDate() != null) { + startDate = parseDate(statRequest.getStartDate()); + endDate = parseDate(statRequest.getEndDate()); + + if (endDate != null && !statRequest.getEndDate().contains(" ")) { + endDate = endDate.withHour(23).withMinute(59).withSecond(59); + } + } + + ViolationStatisticsResponse statistics = violationService.getViolationStatistics( + statRequest.getUserId(), + statRequest.getFileId(), + startDate, + endDate + ); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(statistics) + .build(); + } + + private void validateRequest(ViolationStatisticsRequest request) { + if (request.getUserId() == null) { + throw new IllegalArgumentException("User ID is required"); + } + } + + private LocalDateTime parseDate(String dateStr) { + try { + return LocalDateTime.parse(dateStr, DATE_FORMATTER); + } catch (DateTimeParseException e) { + try { + return LocalDateTime.parse(dateStr + " 00:00:00", DATE_FORMATTER); + } catch (DateTimeParseException ex) { + log.error("Error parsing date: {}", dateStr, ex); + return null; + } + } + } +} diff --git a/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java b/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java index ccc6f14..1679f9f 100644 --- a/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java @@ -36,4 +36,10 @@ public interface ViolationRepository extends JpaRepository { Optional findById(Long id); void deleteByFileEntity(FileEntity file); + + List findByFileEntityIn(List files); + + List findByFileEntityInAndCreatedDateBetween(List files, + LocalDateTime startDate, + LocalDateTime endDate); } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java index ab1aa42..d226eb6 100644 --- a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java @@ -8,10 +8,12 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import ru.soune.nocopy.dto.file.YandexSearchResponse; +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.exception.ViolationNotFoundException; +import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.ViolationRepository; import java.time.LocalDateTime; @@ -25,6 +27,8 @@ public class ViolationService { private final ViolationRepository violationRepository; + private final FileEntityRepository fileEntityRepository; + public void processViolation(YandexSearchResponse.ImageResult imageResult, FileEntity file, GlobalSearchResult result) { String url = imageResult.getUrl(); @@ -113,4 +117,65 @@ public class ViolationService { return "unknown"; } } + + public ViolationStatisticsResponse getViolationStatistics(Long userId, String fileId, + LocalDateTime startDate, LocalDateTime endDate) { + + List 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 = fileEntityRepository.findByUserId(userId); + } + + if (userFiles.isEmpty()) { + return ViolationStatisticsResponse.builder() + .totalViolations(0) + .newViolations(0) + .inProgressViolations(0) + .resolvedViolations(0) + .build(); + } + + List violations; + + if (startDate != null && endDate != null) { + violations = violationRepository.findByFileEntityInAndCreatedDateBetween(userFiles, startDate, endDate); + } else { + violations = violationRepository.findByFileEntityIn(userFiles); + } + + long newViolations = violations.stream() + .filter(v -> "CREATED".equalsIgnoreCase(v.getStatus()) || + "NEW".equalsIgnoreCase(v.getStatus())) + .count(); + + long inProgressViolations = violations.stream() + .filter(v -> "IN_PROGRESS".equalsIgnoreCase(v.getStatus()) || + "PROCESSING".equalsIgnoreCase(v.getStatus()) || + "IN_WORK".equalsIgnoreCase(v.getStatus())) + .count(); + + long resolvedViolations = violations.stream() + .filter(v -> "RESOLVED".equalsIgnoreCase(v.getStatus()) || + "COMPLETED".equalsIgnoreCase(v.getStatus()) || + "SOLVED".equalsIgnoreCase(v.getStatus()) || + "DONE".equalsIgnoreCase(v.getStatus())) + .count(); + + return ViolationStatisticsResponse.builder() + .totalViolations(violations.size()) + .newViolations(newViolations) + .inProgressViolations(inProgressViolations) + .resolvedViolations(resolvedViolations) + .build(); + } } \ No newline at end of file From 869a25043a0a9639ea17c26f45355f79a422d922 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 12 Mar 2026 00:41:04 +0700 Subject: [PATCH 167/274] dev add vio --- .../service/file/FileEntityService.java | 32 ++++++++----------- .../service/violation/ViolationService.java | 5 ++- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index f0425fd..3547eec 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -132,28 +132,22 @@ public class FileEntityService { return convertToResponse(fileEntity, version); } - @Transactional(readOnly = true) - public FileResponse getAllUserFiles(Long userId, int version) { - List fileEntities = fileEntityRepository.findByUserIdAndStatus( - userId, FileStatus.ACTIVE); + public List getAllUserFiles(Long userId, int version) { + User user = userRepository.findById(userId).orElseThrow(); + List allFiles = new ArrayList<>(); - List files = fileEntities.stream() - .map(file -> convertToResponse(file, version)) - .collect(Collectors.toList()); + if (user.getCompany() != null) { + for (Long uId : userRepository.findByCompanyId(user.getCompany().getId()).stream() + .map(User::getId) + .toList()) { + allFiles.addAll(fileEntityRepository.findByUserIdAndStatus(uId, FileStatus.ACTIVE)); + } + } else { + allFiles = fileEntityRepository.findByUserIdAndStatus(userId, FileStatus.ACTIVE); + } - long totalSize = fileEntities.stream() - .mapToLong(FileEntity::getFileSize) - .sum(); - - return FileResponse.builder() - .files(files) - .totalCount(files.size()) - .totalSize(totalSize) - .formattedTotalSize(formatFileSize(totalSize)) - .page(1) - .pageSize(files.size()) - .build(); + return allFiles; } @Transactional(readOnly = true) diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java index d226eb6..dca5b8d 100644 --- a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java @@ -15,6 +15,7 @@ import ru.soune.nocopy.entity.violation.Violation; import ru.soune.nocopy.exception.ViolationNotFoundException; 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.List; @@ -29,6 +30,8 @@ public class ViolationService { private final FileEntityRepository fileEntityRepository; + private final FileEntityService fileEntityService; + public void processViolation(YandexSearchResponse.ImageResult imageResult, FileEntity file, GlobalSearchResult result) { String url = imageResult.getUrl(); @@ -133,7 +136,7 @@ public class ViolationService { userFiles = List.of(file); } else { - userFiles = fileEntityRepository.findByUserId(userId); + userFiles = fileEntityService.getAllUserFiles(userId, 1); } if (userFiles.isEmpty()) { From 65bbb00abf0962ed3c13d58c6606f2af190720dc Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 12 Mar 2026 01:32:11 +0700 Subject: [PATCH 168/274] dev add vio --- .../nocopy/configuration/HandlerConfig.java | 4 +- .../search/GlobalSearchStatisticsRequest.java | 17 +++ .../GlobalSearchStatisticsResponse.java | 83 +++++++++++++ .../violation/ViolationStatisticsRequest.java | 4 +- .../nocopy/handler/GlobalSearchHandler.java | 54 +++++++++ .../handler/ViolationStatisticsHandler.java | 20 +++- .../GlobalSearchResultRepository.java | 10 ++ .../GlobalSearchTaskRepository.java | 18 +++ .../repository/ViolationRepository.java | 8 ++ .../search/GlobalSearchStatisticsService.java | 109 ++++++++++++++++++ 10 files changed, 321 insertions(+), 6 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsResponse.java create mode 100644 src/main/java/ru/soune/nocopy/handler/GlobalSearchHandler.java create mode 100644 src/main/java/ru/soune/nocopy/service/search/GlobalSearchStatisticsService.java diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index 223c94d..2b02251 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -30,7 +30,8 @@ public class HandlerConfig { CostHandler costHandler, MonitoringHandler monitoringHandler, ViolationHandler violationHandler, - ViolationStatisticsHandler violationStatisticsHandler + ViolationStatisticsHandler violationStatisticsHandler, + GlobalSearchHandler globalSearchHandler ) { Map map = new HashMap<>(); map.put(20001, login); @@ -52,6 +53,7 @@ public class HandlerConfig { map.put(30008, costHandler); map.put(30009, violationHandler); map.put(30010, violationStatisticsHandler); + map.put(30011, globalSearchHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsRequest.java b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsRequest.java new file mode 100644 index 0000000..ba11c41 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsRequest.java @@ -0,0 +1,17 @@ +package ru.soune.nocopy.dto.search; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class GlobalSearchStatisticsRequest { + + @JsonProperty("token") + private String token; + + @JsonProperty("limit") + private Integer limit = 10; + + @JsonProperty("days") + private Integer days; +} diff --git a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsResponse.java b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsResponse.java new file mode 100644 index 0000000..69444ef --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsResponse.java @@ -0,0 +1,83 @@ +package ru.soune.nocopy.dto.search; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import ru.soune.nocopy.entity.search.GlobalSearchTask; + +import java.time.format.DateTimeFormatter; +import java.util.List; + +@Data +@Builder +public class GlobalSearchStatisticsResponse { + + @JsonProperty("total_searches") + private long totalSearches; + + @JsonProperty("searches_by_status") + private StatusCountDto searchesByStatus; + + @JsonProperty("recent_searches") + private List recentSearches; + + @Data + @Builder + public static class StatusCountDto { + @JsonProperty("processing") + private long processing; + + @JsonProperty("completed") + private long completed; + + @JsonProperty("failed") + private long failed; + + @JsonProperty("total") + private long total; + } + + @Data + @Builder + public static class SearchStatDto { + @JsonProperty("task_id") + private String taskId; + + @JsonProperty("search_type") + private String searchType; + + @JsonProperty("status") + private String status; + + @JsonProperty("total_files") + private Integer totalFiles; + + @JsonProperty("violations_found") + private long violationsFound; + + @JsonProperty("created_at") + private String createdAt; + + @JsonProperty("duration_seconds") + private Long durationSeconds; + + public static SearchStatDto fromEntity(GlobalSearchTask task, long violationsCount) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + Long duration = null; + if (task.getUpdatedAt() != null && task.getCreatedAt() != null) { + duration = java.time.Duration.between(task.getCreatedAt(), task.getUpdatedAt()).getSeconds(); + } + + return SearchStatDto.builder() + .taskId(task.getTaskId()) + .searchType(task.getSearchType()) + .status(task.getStatus()) + .totalFiles(task.getTotalFiles()) + .violationsFound(violationsCount) + .createdAt(task.getCreatedAt() != null ? task.getCreatedAt().format(formatter) : null) + .durationSeconds(duration) + .build(); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsRequest.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsRequest.java index c9246e7..04cbac6 100644 --- a/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsRequest.java @@ -6,8 +6,8 @@ import lombok.Data; @Data public class ViolationStatisticsRequest { - @JsonProperty("user_id") - private Long userId; + @JsonProperty("token") + private String token; @JsonProperty("file_id") private String fileId; diff --git a/src/main/java/ru/soune/nocopy/handler/GlobalSearchHandler.java b/src/main/java/ru/soune/nocopy/handler/GlobalSearchHandler.java new file mode 100644 index 0000000..a332c2e --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/GlobalSearchHandler.java @@ -0,0 +1,54 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.search.GlobalSearchStatisticsRequest; +import ru.soune.nocopy.dto.search.GlobalSearchStatisticsResponse; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.repository.AuthTokenRepository; +import ru.soune.nocopy.service.search.GlobalSearchStatisticsService; + +import java.util.Optional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class GlobalSearchHandler implements RequestHandler { + + private final ObjectMapper objectMapper; + + private final GlobalSearchStatisticsService statisticsService; + + private final AuthTokenRepository authTokenRepository; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + GlobalSearchStatisticsRequest statRequest = objectMapper.convertValue( + request.getMessageBody(), GlobalSearchStatisticsRequest.class); + + if (statRequest.getToken() == null) { + throw new IllegalArgumentException("User token is required"); + } + + Optional tokenOptional = + authTokenRepository.findByToken(statRequest.getToken()); + + if (tokenOptional.isPresent()) { + throw new IllegalArgumentException("User token is required"); + } + + Long userId = tokenOptional.orElseThrow().getUser().getId(); + + GlobalSearchStatisticsResponse statistics = statisticsService.getStatistics( + userId, statRequest.getLimit(), statRequest.getDays()); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(statistics) + .build(); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java b/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java index a12b6c5..60dfb88 100644 --- a/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java @@ -8,11 +8,14 @@ import ru.soune.nocopy.dto.BaseRequest; import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.violation.ViolationStatisticsRequest; import ru.soune.nocopy.dto.violation.ViolationStatisticsResponse; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.service.violation.ViolationService; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; +import java.util.Optional; @Slf4j @Component @@ -22,6 +25,8 @@ public class ViolationStatisticsHandler implements RequestHandler { private final ViolationService violationService; + private final AuthTokenRepository authTokenRepository; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @Override @@ -43,8 +48,17 @@ public class ViolationStatisticsHandler implements RequestHandler { } } + Optional tokenOptional = + authTokenRepository.findByToken(statRequest.getToken()); + + if (tokenOptional.isPresent()) { + throw new IllegalArgumentException("User token is required"); + } + + Long userId = tokenOptional.orElseThrow().getUser().getId(); + ViolationStatisticsResponse statistics = violationService.getViolationStatistics( - statRequest.getUserId(), + userId, statRequest.getFileId(), startDate, endDate @@ -57,8 +71,8 @@ public class ViolationStatisticsHandler implements RequestHandler { } private void validateRequest(ViolationStatisticsRequest request) { - if (request.getUserId() == null) { - throw new IllegalArgumentException("User ID is required"); + if (request.getToken() == null) { + throw new IllegalArgumentException("Token is required"); } } diff --git a/src/main/java/ru/soune/nocopy/repository/GlobalSearchResultRepository.java b/src/main/java/ru/soune/nocopy/repository/GlobalSearchResultRepository.java index dc41337..42eec8a 100644 --- a/src/main/java/ru/soune/nocopy/repository/GlobalSearchResultRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/GlobalSearchResultRepository.java @@ -1,6 +1,8 @@ package ru.soune.nocopy.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import ru.soune.nocopy.entity.search.GlobalSearchResult; @@ -9,5 +11,13 @@ import java.util.List; @Repository public interface GlobalSearchResultRepository extends JpaRepository { List findByTaskIdOrderByIdAsc(String taskId); + void deleteByTaskId(String taskId); + + List findByTaskId(String taskId); + + @Query("SELECT g.fileId FROM GlobalSearchResult g WHERE g.taskId = :taskId") + List findFileIdsByTaskId(@Param("taskId") String taskId); + + long countByTaskId(String taskId); } diff --git a/src/main/java/ru/soune/nocopy/repository/GlobalSearchTaskRepository.java b/src/main/java/ru/soune/nocopy/repository/GlobalSearchTaskRepository.java index 908a659..e969384 100644 --- a/src/main/java/ru/soune/nocopy/repository/GlobalSearchTaskRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/GlobalSearchTaskRepository.java @@ -1,13 +1,31 @@ package ru.soune.nocopy.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import ru.soune.nocopy.entity.search.GlobalSearchTask; +import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; @Repository public interface GlobalSearchTaskRepository extends JpaRepository { Optional findByTaskIdAndUserId(String taskId, Long userId); + + List findByUserIdOrderByCreatedAtDesc(Long userId); + + List findByUserIdAndCreatedAtAfterOrderByCreatedAtDesc(Long userId, LocalDateTime date); + + @Query("SELECT COUNT(t) FROM GlobalSearchTask t WHERE t.userId IN :userIds") + long countByUserId(@Param("userIds") List userIds); + + @Query("SELECT t.status, COUNT(t) FROM GlobalSearchTask t WHERE t.userId IN :userIds GROUP BY t.status") + List countByStatusAndUserId(@Param("userIds") List userIds); + + @Query("SELECT t FROM GlobalSearchTask t WHERE t.userId = :userId ORDER BY t.createdAt DESC") + List findTopByUserIdOrderByCreatedAtDesc(@Param("userId") Long userId, + org.springframework.data.domain.Pageable pageable); } diff --git a/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java b/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java index 1679f9f..942b31c 100644 --- a/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java @@ -3,6 +3,8 @@ package ru.soune.nocopy.repository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.violation.Violation; @@ -42,4 +44,10 @@ public interface ViolationRepository extends JpaRepository { List findByFileEntityInAndCreatedDateBetween(List files, LocalDateTime startDate, LocalDateTime endDate); + + @Query("SELECT COUNT(v) FROM Violation v WHERE v.fileEntity.id IN :fileIds") + long countByFileEntityIdIn(@Param("fileIds") List fileIds); + + @Query("SELECT COUNT(v) FROM Violation v WHERE v.globalSearchResult.taskId = :taskId") + long countByGlobalSearchResultTaskId(@Param("taskId") String taskId); } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchStatisticsService.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchStatisticsService.java new file mode 100644 index 0000000..383f8b4 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchStatisticsService.java @@ -0,0 +1,109 @@ +package ru.soune.nocopy.service.search; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.dto.search.GlobalSearchStatisticsResponse; +import ru.soune.nocopy.entity.company.Company; +import ru.soune.nocopy.entity.search.GlobalSearchTask; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.repository.GlobalSearchResultRepository; +import ru.soune.nocopy.repository.GlobalSearchTaskRepository; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.repository.ViolationRepository; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class GlobalSearchStatisticsService { + + private final GlobalSearchTaskRepository taskRepository; + + private final GlobalSearchResultRepository resultRepository; + + private final ViolationRepository violationRepository; + + private final UserRepository userRepository; + + @Transactional(readOnly = true) + public GlobalSearchStatisticsResponse getStatistics(Long userId, Integer limit, Integer days) { + User user = userRepository.findById(userId).orElseThrow(); + Company company = user.getCompany(); + List userIds = company == null ? Arrays.asList(userId): + user.getCompany() + .getUsers() + .stream() + .map(User::getId).toList(); + + long totalSearches = taskRepository.countByUserId(userIds); + List statusCounts = taskRepository.countByStatusAndUserId(userIds); + + GlobalSearchStatisticsResponse.StatusCountDto statusCountDto = + GlobalSearchStatisticsResponse.StatusCountDto.builder() + .processing(0) + .completed(0) + .failed(0) + .total(totalSearches) + .build(); + + for (Object[] row : statusCounts) { + String status = (String) row[0]; + Long count = (Long) row[1]; + + if ("PROCESSING".equalsIgnoreCase(status)) { + statusCountDto.setProcessing(count); + } else if ("COMPLETED".equalsIgnoreCase(status) || "SUCCESS".equalsIgnoreCase(status)) { + statusCountDto.setCompleted(statusCountDto.getCompleted() + count); + } else if ("FAILED".equalsIgnoreCase(status) || "TIMEOUT".equalsIgnoreCase(status)) { + statusCountDto.setFailed(statusCountDto.getFailed() + count); + } + } + + List recentTasks; + + if (days != null && days > 0) { + LocalDateTime fromDate = LocalDateTime.now().minusDays(days); + recentTasks = taskRepository.findByUserIdAndCreatedAtAfterOrderByCreatedAtDesc(userId, fromDate); + + if (limit != null && limit > 0 && recentTasks.size() > limit) { + recentTasks = recentTasks.subList(0, limit); + } + } else { + org.springframework.data.domain.Pageable pageable = + org.springframework.data.domain.PageRequest.of(0, limit != null ? limit : 10); + recentTasks = taskRepository.findTopByUserIdOrderByCreatedAtDesc(userId, pageable); + } + + List recentSearchDtos = new ArrayList<>(); + + for (GlobalSearchTask task : recentTasks) { + long violationsCount = getViolationsCountForTask(task.getTaskId()); + + recentSearchDtos.add(GlobalSearchStatisticsResponse.SearchStatDto.fromEntity(task, violationsCount)); + } + + return GlobalSearchStatisticsResponse.builder() + .totalSearches(totalSearches) + .searchesByStatus(statusCountDto) + .recentSearches(recentSearchDtos) + .build(); + } + + @Transactional(readOnly = true) + public long getViolationsCountForTask(String taskId) { + List fileIds = resultRepository.findByTaskId(taskId).stream() + .map(result -> result.getFileId()) + .toList(); + + if (fileIds.isEmpty()) { + return 0; + } + return violationRepository.countByFileEntityIdIn(fileIds); + } +} From 37b1f370e5d38451dcfae8d7bfc2abc9b07e3624 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 12 Mar 2026 01:45:57 +0700 Subject: [PATCH 169/274] dev add vio --- src/main/java/ru/soune/nocopy/handler/GlobalSearchHandler.java | 2 +- .../ru/soune/nocopy/handler/ViolationStatisticsHandler.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/GlobalSearchHandler.java b/src/main/java/ru/soune/nocopy/handler/GlobalSearchHandler.java index a332c2e..a6e7820 100644 --- a/src/main/java/ru/soune/nocopy/handler/GlobalSearchHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/GlobalSearchHandler.java @@ -37,7 +37,7 @@ public class GlobalSearchHandler implements RequestHandler { Optional tokenOptional = authTokenRepository.findByToken(statRequest.getToken()); - if (tokenOptional.isPresent()) { + if (tokenOptional.isEmpty()) { throw new IllegalArgumentException("User token is required"); } diff --git a/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java b/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java index 60dfb88..585dd38 100644 --- a/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java @@ -51,7 +51,7 @@ public class ViolationStatisticsHandler implements RequestHandler { Optional tokenOptional = authTokenRepository.findByToken(statRequest.getToken()); - if (tokenOptional.isPresent()) { + if (tokenOptional.isEmpty()) { throw new IllegalArgumentException("User token is required"); } From 2ca73e4417daeff1d27d7478de550e0aa66c2220 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 12 Mar 2026 02:52:38 +0700 Subject: [PATCH 170/274] dev add vio --- .../GlobalSearchStatisticsResponse.java | 6 +++- .../service/file/FileEntityService.java | 2 +- .../search/GlobalSearchStatisticsService.java | 32 +++++++------------ .../service/violation/ViolationService.java | 2 +- 4 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsResponse.java b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsResponse.java index 69444ef..0a199b1 100644 --- a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsResponse.java @@ -61,7 +61,10 @@ public class GlobalSearchStatisticsResponse { @JsonProperty("duration_seconds") private Long durationSeconds; - public static SearchStatDto fromEntity(GlobalSearchTask task, long violationsCount) { + @JsonProperty("file_ids") + private List fileIds; + + public static SearchStatDto fromEntity(GlobalSearchTask task, long violationsCount, List fileIds ) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); Long duration = null; @@ -75,6 +78,7 @@ public class GlobalSearchStatisticsResponse { .status(task.getStatus()) .totalFiles(task.getTotalFiles()) .violationsFound(violationsCount) + .fileIds(fileIds) .createdAt(task.getCreatedAt() != null ? task.getCreatedAt().format(formatter) : null) .durationSeconds(duration) .build(); diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index 3547eec..58cd956 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -133,7 +133,7 @@ public class FileEntityService { } @Transactional(readOnly = true) - public List getAllUserFiles(Long userId, int version) { + public List getAllUserFiles(Long userId) { User user = userRepository.findById(userId).orElseThrow(); List allFiles = new ArrayList<>(); diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchStatisticsService.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchStatisticsService.java index 383f8b4..1fc8c5c 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchStatisticsService.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchStatisticsService.java @@ -6,16 +6,13 @@ 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.GlobalSearchResultRepository; -import ru.soune.nocopy.repository.GlobalSearchTaskRepository; -import ru.soune.nocopy.repository.UserRepository; -import ru.soune.nocopy.repository.ViolationRepository; +import ru.soune.nocopy.repository.*; import java.time.LocalDateTime; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; @Slf4j @@ -35,7 +32,7 @@ public class GlobalSearchStatisticsService { public GlobalSearchStatisticsResponse getStatistics(Long userId, Integer limit, Integer days) { User user = userRepository.findById(userId).orElseThrow(); Company company = user.getCompany(); - List userIds = company == null ? Arrays.asList(userId): + List userIds = company == null ? List.of(userId) : user.getCompany() .getUsers() .stream() @@ -83,9 +80,16 @@ public class GlobalSearchStatisticsService { List recentSearchDtos = new ArrayList<>(); for (GlobalSearchTask task : recentTasks) { - long violationsCount = getViolationsCountForTask(task.getTaskId()); + List globalSearchResults = resultRepository.findByTaskId(task.getTaskId()); + List fileIds = globalSearchResults.stream() + .map(GlobalSearchResult::getFileId) + .toList(); - recentSearchDtos.add(GlobalSearchStatisticsResponse.SearchStatDto.fromEntity(task, violationsCount)); + GlobalSearchStatisticsResponse.SearchStatDto searchStatDto = + GlobalSearchStatisticsResponse.SearchStatDto.fromEntity(task, + violationRepository.countByFileEntityIdIn(fileIds), fileIds); + + recentSearchDtos.add(searchStatDto); } return GlobalSearchStatisticsResponse.builder() @@ -94,16 +98,4 @@ public class GlobalSearchStatisticsService { .recentSearches(recentSearchDtos) .build(); } - - @Transactional(readOnly = true) - public long getViolationsCountForTask(String taskId) { - List fileIds = resultRepository.findByTaskId(taskId).stream() - .map(result -> result.getFileId()) - .toList(); - - if (fileIds.isEmpty()) { - return 0; - } - return violationRepository.countByFileEntityIdIn(fileIds); - } } diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java index dca5b8d..6cff26f 100644 --- a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java @@ -136,7 +136,7 @@ public class ViolationService { userFiles = List.of(file); } else { - userFiles = fileEntityService.getAllUserFiles(userId, 1); + userFiles = fileEntityService.getAllUserFiles(userId); } if (userFiles.isEmpty()) { From 3a477bc2ae875916f73c3cc6033bf43f450981ee Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 12 Mar 2026 15:46:37 +0700 Subject: [PATCH 171/274] dev add vio --- .../controller/GlobalSearchController.java | 20 +++++++++++++++++++ .../GlobalSearchTaskRepository.java | 2 ++ 2 files changed, 22 insertions(+) diff --git a/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java b/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java index 2d8078c..ee9301f 100644 --- a/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java +++ b/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java @@ -63,6 +63,26 @@ public class GlobalSearchController { return ResponseEntity.ok(response); } + @GetMapping("/actual-task") + public ResponseEntity getStatus(@RequestHeader(value = "Authorization", required = false) String tokenHeader) { + + if (tokenHeader == null || tokenHeader.isBlank()) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } + + Long userId = authService.useUserAuthToken(tokenHeader); + + Optional taskOptional = searchTaskRepository.findByUserIdAndStatus(userId, + "PROCESSING"); + + if (taskOptional.isEmpty()) return ResponseEntity.ok().body("Task not found"); + + return ResponseEntity.ok(taskOptional.get()); + } + @GetMapping("/status/{taskId}") public ResponseEntity getStatus(@PathVariable String taskId, @RequestHeader(value = "Authorization", required = false) String tokenHeader) { diff --git a/src/main/java/ru/soune/nocopy/repository/GlobalSearchTaskRepository.java b/src/main/java/ru/soune/nocopy/repository/GlobalSearchTaskRepository.java index e969384..d95ff45 100644 --- a/src/main/java/ru/soune/nocopy/repository/GlobalSearchTaskRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/GlobalSearchTaskRepository.java @@ -14,6 +14,8 @@ import java.util.Optional; public interface GlobalSearchTaskRepository extends JpaRepository { Optional findByTaskIdAndUserId(String taskId, Long userId); + Optional findByUserIdAndStatus(Long userId, String status); + List findByUserIdOrderByCreatedAtDesc(Long userId); List findByUserIdAndCreatedAtAfterOrderByCreatedAtDesc(Long userId, LocalDateTime date); From 835a9bf18df0356d8ea85a7aa8fab7140d0720d3 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 12 Mar 2026 17:27:22 +0700 Subject: [PATCH 172/274] dev add vio --- .../controller/GlobalSearchController.java | 88 ++++++++++++------- 1 file changed, 56 insertions(+), 32 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java b/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java index ee9301f..5f7cf1d 100644 --- a/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java +++ b/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java @@ -10,6 +10,7 @@ import ru.soune.nocopy.dto.search.GlobalSearchStatusResponse; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.search.GlobalSearchTask; import ru.soune.nocopy.entity.search.SearchStatus; +import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.repository.GlobalSearchTaskRepository; import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.search.GlobalSearchService; @@ -34,7 +35,6 @@ public class GlobalSearchController { public ResponseEntity startSearch( @RequestBody GlobalSearchStartRequest request, @RequestHeader(value = "Authorization", required = false) String tokenHeader) { - if (tokenHeader == null || tokenHeader.isBlank()) { Map errorData = new HashMap<>(); errorData.put("token", tokenHeader); @@ -42,23 +42,32 @@ public class GlobalSearchController { return ResponseEntity.ok().body(Map.of("error", errorData)); } - Long userId = authService.useUserAuthToken(tokenHeader); - - List filesToProcess = globalSearchService.getFilesToProcess(request, userId); - - if (filesToProcess.isEmpty()) return ResponseEntity.ok().body("Files for search not found"); - - String taskId = globalSearchService.startSearch(request, userId, filesToProcess); - - Optional taskOptional = globalSearchTaskRepository.findById(taskId); - - if (taskOptional.isEmpty()) return ResponseEntity.ok().body("Task not found"); - - GlobalSearchTask task = taskOptional.orElseThrow(); GlobalSearchStartResponse response = new GlobalSearchStartResponse(); - response.setTaskId(taskId); - response.setStatus(SearchStatus.ACCEPTED.name()); - response.setTotalFiles(task.getTotalFiles()); + + try { + Long userId = authService.useUserAuthToken(tokenHeader); + + List filesToProcess = globalSearchService.getFilesToProcess(request, userId); + + if (filesToProcess.isEmpty()) return ResponseEntity.ok().body("Files for search not found"); + + String taskId = globalSearchService.startSearch(request, userId, filesToProcess); + + Optional taskOptional = globalSearchTaskRepository.findById(taskId); + + if (taskOptional.isEmpty()) return ResponseEntity.ok().body("Task not found"); + + GlobalSearchTask task = taskOptional.orElseThrow(); + + response.setTaskId(taskId); + response.setStatus(SearchStatus.ACCEPTED.name()); + response.setTotalFiles(task.getTotalFiles()); + } catch (NotFoundAuthToken e) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } return ResponseEntity.ok(response); } @@ -73,12 +82,21 @@ public class GlobalSearchController { return ResponseEntity.ok().body(Map.of("error", errorData)); } - Long userId = authService.useUserAuthToken(tokenHeader); + Optional taskOptional; - Optional taskOptional = searchTaskRepository.findByUserIdAndStatus(userId, - "PROCESSING"); + try { + Long userId = authService.useUserAuthToken(tokenHeader); - if (taskOptional.isEmpty()) return ResponseEntity.ok().body("Task not found"); + taskOptional = searchTaskRepository.findByUserIdAndStatus(userId, + "PROCESSING"); + + if (taskOptional.isEmpty()) return ResponseEntity.ok().body("Task not found"); + } catch (NotFoundAuthToken e) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } return ResponseEntity.ok(taskOptional.get()); } @@ -93,23 +111,29 @@ public class GlobalSearchController { return ResponseEntity.ok().body(Map.of("error", errorData)); } + GlobalSearchStatusResponse response; - Long userId = authService.useUserAuthToken(tokenHeader); + try { + Long userId = authService.useUserAuthToken(tokenHeader); - Optional taskOptional = searchTaskRepository.findByTaskIdAndUserId(taskId, userId); + Optional taskOptional = searchTaskRepository.findByTaskIdAndUserId(taskId, userId); - if (taskOptional.isEmpty()) return ResponseEntity.ok().body("Task not found"); + if (taskOptional.isEmpty()) return ResponseEntity.ok().body("Task not found"); - GlobalSearchTask task = taskOptional.orElseThrow(); + GlobalSearchTask task = taskOptional.orElseThrow(); - int progress = task.getTotalFiles() > 0 - ? task.getProcessedFiles() * 100 / task.getTotalFiles() - : 0; + int progress = task.getTotalFiles() > 0 ? task.getProcessedFiles() * 100 / task.getTotalFiles() : 0; - GlobalSearchStatusResponse response = new GlobalSearchStatusResponse(); - response.setTaskId(taskId); - response.setStatus(task.getStatus()); - response.setProgress(progress); + response = new GlobalSearchStatusResponse(); + response.setTaskId(taskId); + response.setStatus(task.getStatus()); + response.setProgress(progress); + } catch (NotFoundAuthToken e) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } return ResponseEntity.ok(response); } From 7af4b6c78170ded6a8023ea27f85a2d2f7acecac Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 12 Mar 2026 17:31:40 +0700 Subject: [PATCH 173/274] dev add vio --- .../nocopy/service/search/GlobalSearchService.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchService.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchService.java index 4b5ae40..87a2230 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchService.java @@ -12,6 +12,7 @@ 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; @@ -30,6 +31,8 @@ public class GlobalSearchService { private final GlobalSearchAsyncProcessor asyncProcessor; + private final FileEntityService fileEntityService; + @Transactional public String startSearch(GlobalSearchStartRequest request, Long userId, List filesToProcess) { GlobalSearchTask task = new GlobalSearchTask(); @@ -56,7 +59,12 @@ public class GlobalSearchService { .map(FileMonitoringEntity::getFile) .toList(); } else { - return fileEntityRepository.findAllById(request.getFileIds()); + List fileIds = request.getFileIds().isEmpty() ? fileEntityService.getAllUserFiles(userId) + .stream() + .map(FileEntity::getId) + .toList(): request.getFileIds(); + + return fileEntityRepository.findAllById(fileIds); } } } From 507947d6a6ef6c74cc51d798075c0c66799b9f45 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 13 Mar 2026 13:48:03 +0700 Subject: [PATCH 174/274] dev add vio --- .../controller/GlobalSearchController.java | 32 +++++++++++ .../handler/ViolationStatisticsHandler.java | 3 +- .../service/violation/ViolationService.java | 53 ++++++++++++++----- 3 files changed, 73 insertions(+), 15 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java b/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java index 5f7cf1d..9f09b72 100644 --- a/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java +++ b/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java @@ -14,6 +14,7 @@ import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.repository.GlobalSearchTaskRepository; import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.search.GlobalSearchService; +import ru.soune.nocopy.service.violation.ViolationService; import java.util.*; @@ -31,6 +32,8 @@ public class GlobalSearchController { private final GlobalSearchTaskRepository searchTaskRepository; + private final ViolationService violationService; + @PostMapping("/start") public ResponseEntity startSearch( @RequestBody GlobalSearchStartRequest request, @@ -101,6 +104,35 @@ public class GlobalSearchController { return ResponseEntity.ok(taskOptional.get()); } + @GetMapping("/violation-summary") + public ResponseEntity getSummary(@RequestHeader(value = "Authorization", required = false) String tokenHeader) { + + if (tokenHeader == null || tokenHeader.isBlank()) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } + + Map> fileViolationsSummary; + + try { + Long userId = authService.useUserAuthToken(tokenHeader); + + fileViolationsSummary = + violationService.getFileViolationsSummary(userId); + + if (fileViolationsSummary.isEmpty()) return ResponseEntity.ok().body("Violations summary not found"); + } catch (NotFoundAuthToken e) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } + + return ResponseEntity.ok(fileViolationsSummary); + } + @GetMapping("/status/{taskId}") public ResponseEntity getStatus(@PathVariable String taskId, @RequestHeader(value = "Authorization", required = false) String tokenHeader) { diff --git a/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java b/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java index 585dd38..6f2de2c 100644 --- a/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java @@ -61,8 +61,7 @@ public class ViolationStatisticsHandler implements RequestHandler { userId, statRequest.getFileId(), startDate, - endDate - ); + endDate); return BaseResponse.builder() .msgId(request.getMsgId()) diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java index 6cff26f..67edda1 100644 --- a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java @@ -18,6 +18,7 @@ import ru.soune.nocopy.repository.ViolationRepository; import ru.soune.nocopy.service.file.FileEntityService; import java.time.LocalDateTime; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -56,6 +57,31 @@ public class ViolationService { } } + public Map> getFileViolationsSummary(Long userId) { + List userFiles = fileEntityService.getAllUserFiles(userId); + List violations = violationRepository.findByFileEntityIn(userFiles); + + Map> violationsByFileId = violations.stream() + .collect(Collectors.groupingBy(v -> v.getFileEntity().getId())); + + Map> result = new HashMap<>(); + + for (Map.Entry> entry : violationsByFileId.entrySet()) { + List fileViolations = entry.getValue(); + + Map info = new HashMap<>(); + info.put("count", fileViolations.size()); + info.put("latestDate", fileViolations.stream() + .map(Violation::getCreatedDate) + .max(LocalDateTime::compareTo) + .orElse(null)); + + result.put(entry.getKey(), info); + } + + return result; + } + public List violationsByFileId(FileEntity file) { return violationRepository.findByFileEntity(file); } @@ -108,19 +134,6 @@ public class ViolationService { )); } - 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"; - } - } - public ViolationStatisticsResponse getViolationStatistics(Long userId, String fileId, LocalDateTime startDate, LocalDateTime endDate) { @@ -181,4 +194,18 @@ public class ViolationService { .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"; + } + } } \ No newline at end of file From bffdf177170805de34862c93171567485b6d0015 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 13 Mar 2026 13:55:10 +0700 Subject: [PATCH 175/274] dev add vio --- .../controller/GlobalSearchController.java | 3 +- .../violation/FileViolationSummaryDTO.java | 25 +++++++++ .../dto/violation/ViolationInfoDTO.java | 16 ++++++ .../service/violation/ViolationService.java | 51 ++++++++++++++----- 4 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/violation/FileViolationSummaryDTO.java create mode 100644 src/main/java/ru/soune/nocopy/dto/violation/ViolationInfoDTO.java diff --git a/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java b/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java index 9f09b72..2765f28 100644 --- a/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java +++ b/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.*; import ru.soune.nocopy.dto.search.GlobalSearchStartRequest; import ru.soune.nocopy.dto.search.GlobalSearchStartResponse; import ru.soune.nocopy.dto.search.GlobalSearchStatusResponse; +import ru.soune.nocopy.dto.violation.FileViolationSummaryDTO; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.search.GlobalSearchTask; import ru.soune.nocopy.entity.search.SearchStatus; @@ -114,7 +115,7 @@ public class GlobalSearchController { return ResponseEntity.ok().body(Map.of("error", errorData)); } - Map> fileViolationsSummary; + List fileViolationsSummary; try { Long userId = authService.useUserAuthToken(tokenHeader); diff --git a/src/main/java/ru/soune/nocopy/dto/violation/FileViolationSummaryDTO.java b/src/main/java/ru/soune/nocopy/dto/violation/FileViolationSummaryDTO.java new file mode 100644 index 0000000..77d713d --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/FileViolationSummaryDTO.java @@ -0,0 +1,25 @@ +package ru.soune.nocopy.dto.violation; + +import lombok.Builder; +import lombok.Data; +import ru.soune.nocopy.entity.file.FileStatus; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@Builder +public class FileViolationSummaryDTO { + private String fileId; + private Integer supportId; + private String fileName; + private String storedFileName; + private String filePath; + private Long fileSize; + private String mimeType; + private FileStatus status; + private LocalDateTime createdAt; + private long violationCount; + private LocalDateTime latestViolationDate; + private List violations; +} diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationInfoDTO.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationInfoDTO.java new file mode 100644 index 0000000..fa6ee64 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationInfoDTO.java @@ -0,0 +1,16 @@ +package ru.soune.nocopy.dto.violation; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@Builder +public class ViolationInfoDTO { + private Long violationId; + private String url; + private String pageUrl; + private String host; + private LocalDateTime createdDate; +} diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java index 67edda1..ddf3b2d 100644 --- a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java @@ -8,6 +8,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import ru.soune.nocopy.dto.file.YandexSearchResponse; +import ru.soune.nocopy.dto.violation.FileViolationSummaryDTO; +import ru.soune.nocopy.dto.violation.ViolationInfoDTO; import ru.soune.nocopy.dto.violation.ViolationStatisticsResponse; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.search.GlobalSearchResult; @@ -18,6 +20,7 @@ import ru.soune.nocopy.repository.ViolationRepository; import ru.soune.nocopy.service.file.FileEntityService; import java.time.LocalDateTime; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -57,29 +60,49 @@ public class ViolationService { } } - public Map> getFileViolationsSummary(Long userId) { + public List getFileViolationsSummary(Long userId) { List userFiles = fileEntityService.getAllUserFiles(userId); List violations = violationRepository.findByFileEntityIn(userFiles); Map> violationsByFileId = violations.stream() .collect(Collectors.groupingBy(v -> v.getFileEntity().getId())); - Map> result = new HashMap<>(); + return userFiles.stream() + .map(file -> { + List fileViolations = violationsByFileId.getOrDefault(file.getId(), + Collections.emptyList()); - for (Map.Entry> entry : violationsByFileId.entrySet()) { - List fileViolations = entry.getValue(); + List violationDtos = fileViolations.stream() + .map(v -> ViolationInfoDTO.builder() + .violationId(v.getId()) + .url(v.getUrl()) + .pageUrl(v.getPageUrl()) + .host(v.getHost()) + .createdDate(v.getCreatedDate()) + .build()) + .collect(Collectors.toList()); - Map info = new HashMap<>(); - info.put("count", fileViolations.size()); - info.put("latestDate", fileViolations.stream() - .map(Violation::getCreatedDate) - .max(LocalDateTime::compareTo) - .orElse(null)); + LocalDateTime latestDate = fileViolations.stream() + .map(Violation::getCreatedDate) + .max(LocalDateTime::compareTo) + .orElse(null); - result.put(entry.getKey(), info); - } - - return result; + return FileViolationSummaryDTO.builder() + .fileId(file.getId()) + .supportId(file.getSupportId()) + .fileName(file.getOriginalFileName()) + .storedFileName(file.getStoredFileName()) + .filePath(file.getFilePath()) + .fileSize(file.getFileSize()) + .mimeType(file.getMimeType()) + .status(file.getStatus()) + .createdAt(file.getCreatedAt()) + .violationCount(fileViolations.size()) + .latestViolationDate(latestDate) + .violations(violationDtos) + .build(); + }) + .collect(Collectors.toList()); } public List violationsByFileId(FileEntity file) { From 83f29e33829da69eda7d0b8a31c730628cdfc338 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 13 Mar 2026 14:06:54 +0700 Subject: [PATCH 176/274] dev add vio --- .../nocopy/service/violation/ViolationService.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java index ddf3b2d..da4df11 100644 --- a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java @@ -72,16 +72,6 @@ public class ViolationService { List fileViolations = violationsByFileId.getOrDefault(file.getId(), Collections.emptyList()); - List violationDtos = fileViolations.stream() - .map(v -> ViolationInfoDTO.builder() - .violationId(v.getId()) - .url(v.getUrl()) - .pageUrl(v.getPageUrl()) - .host(v.getHost()) - .createdDate(v.getCreatedDate()) - .build()) - .collect(Collectors.toList()); - LocalDateTime latestDate = fileViolations.stream() .map(Violation::getCreatedDate) .max(LocalDateTime::compareTo) @@ -91,15 +81,12 @@ public class ViolationService { .fileId(file.getId()) .supportId(file.getSupportId()) .fileName(file.getOriginalFileName()) - .storedFileName(file.getStoredFileName()) - .filePath(file.getFilePath()) .fileSize(file.getFileSize()) .mimeType(file.getMimeType()) .status(file.getStatus()) .createdAt(file.getCreatedAt()) .violationCount(fileViolations.size()) .latestViolationDate(latestDate) - .violations(violationDtos) .build(); }) .collect(Collectors.toList()); From e2d4312e0fc6ade70957009ea83d7c661bf4f21d Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 13 Mar 2026 14:14:37 +0700 Subject: [PATCH 177/274] dev add vio --- .../nocopy/dto/violation/FileViolationSummaryDTO.java | 3 --- .../nocopy/service/violation/ViolationService.java | 10 ++++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/dto/violation/FileViolationSummaryDTO.java b/src/main/java/ru/soune/nocopy/dto/violation/FileViolationSummaryDTO.java index 77d713d..e4a5129 100644 --- a/src/main/java/ru/soune/nocopy/dto/violation/FileViolationSummaryDTO.java +++ b/src/main/java/ru/soune/nocopy/dto/violation/FileViolationSummaryDTO.java @@ -13,13 +13,10 @@ public class FileViolationSummaryDTO { private String fileId; private Integer supportId; private String fileName; - private String storedFileName; - private String filePath; private Long fileSize; private String mimeType; private FileStatus status; private LocalDateTime createdAt; private long violationCount; private LocalDateTime latestViolationDate; - private List violations; } diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java index da4df11..faa80ff 100644 --- a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java @@ -20,10 +20,7 @@ import ru.soune.nocopy.repository.ViolationRepository; import ru.soune.nocopy.service.file.FileEntityService; import java.time.LocalDateTime; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; @Service @@ -72,6 +69,10 @@ public class ViolationService { List fileViolations = violationsByFileId.getOrDefault(file.getId(), Collections.emptyList()); + if (fileViolations.isEmpty()) { + return null; + } + LocalDateTime latestDate = fileViolations.stream() .map(Violation::getCreatedDate) .max(LocalDateTime::compareTo) @@ -89,6 +90,7 @@ public class ViolationService { .latestViolationDate(latestDate) .build(); }) + .filter(Objects::nonNull) .collect(Collectors.toList()); } From f4675eb73958bbadc837b6e8345835b54ac66c74 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 14 Mar 2026 20:39:09 +0700 Subject: [PATCH 178/274] dev add vio --- .../ru/soune/nocopy/service/search/SearchImageService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index c397e55..29fc5bb 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -44,7 +44,7 @@ public class SearchImageService { .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) - .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) +// .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) .build(); this.googleHttpClient = new OkHttpClient.Builder() @@ -56,7 +56,7 @@ public class SearchImageService { .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) - .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) +// .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) .build(); this.objectMapper = new ObjectMapper(); From b5b71ddfe65bbbe5bfe9db1a396955add806ac67 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 14 Mar 2026 20:57:39 +0700 Subject: [PATCH 179/274] dev add vio --- .../soune/nocopy/service/search/SearchImageService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 29fc5bb..1c35f4a 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -49,10 +49,10 @@ public class SearchImageService { this.googleHttpClient = new OkHttpClient.Builder() .proxy(proxy) - .connectTimeout(15, TimeUnit.SECONDS) - .writeTimeout(15, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .callTimeout(15, TimeUnit.SECONDS) + .connectTimeout(35, TimeUnit.SECONDS) + .writeTimeout(35, TimeUnit.SECONDS) + .readTimeout(35, TimeUnit.SECONDS) + .callTimeout(35, TimeUnit.SECONDS) .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) From aa665acbad49d9f085a2e0f989bb0258268997b9 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 14 Mar 2026 21:16:29 +0700 Subject: [PATCH 180/274] dev add vio --- .../search/GlobalSearchAsyncProcessor.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java index fb39b84..2b41844 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -87,23 +87,23 @@ public class GlobalSearchAsyncProcessor { List allUniqueImages = new ArrayList<>(); boolean hasTimeout = false; - - try { - String searchResponseGoogle = searchImageService.searchReverseByPublicUrl( - file, "google_lens", "exact_matches"); - - List googleImages = - searchImageService.getAllImagesWithoutPagination(searchResponseGoogle, "exact_matches"); - - allUniqueImages.addAll(googleImages); - log.info("Google search OK for file {}", file.getId()); - - } 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); - } +// +// try { +// String searchResponseGoogle = searchImageService.searchReverseByPublicUrl( +// file, "google_lens", "exact_matches"); +// +// List googleImages = +// searchImageService.getAllImagesWithoutPagination(searchResponseGoogle, "exact_matches"); +// +// allUniqueImages.addAll(googleImages); +// log.info("Google search OK for file {}", file.getId()); +// +// } 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); +// } try { String searchResponseYandex = searchImageService.searchReverseByPublicUrl( From f291287b4cb1194bd0c58ecaca370659fc029855 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 14 Mar 2026 21:28:24 +0700 Subject: [PATCH 181/274] dev add vio --- .../search/GlobalSearchAsyncProcessor.java | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java index 2b41844..7325e81 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -87,41 +87,41 @@ public class GlobalSearchAsyncProcessor { List allUniqueImages = new ArrayList<>(); boolean hasTimeout = false; -// -// try { -// String searchResponseGoogle = searchImageService.searchReverseByPublicUrl( -// file, "google_lens", "exact_matches"); -// -// List googleImages = -// searchImageService.getAllImagesWithoutPagination(searchResponseGoogle, "exact_matches"); -// -// allUniqueImages.addAll(googleImages); -// log.info("Google search OK for file {}", file.getId()); -// -// } 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); -// } try { - String searchResponseYandex = searchImageService.searchReverseByPublicUrl( - file, "yandex_reverse_image", "visual_matches"); + String searchResponseGoogle = searchImageService.searchReverseByPublicUrl( + file, "google_lens", "exact_matches"); - List yandexImages = - searchImageService.getAllImagesWithoutPagination(searchResponseYandex, "visual_matches"); + List googleImages = + searchImageService.getAllImagesWithoutPagination(searchResponseGoogle, "exact_matches"); - allUniqueImages.addAll(yandexImages); - log.info("Yandex search OK for file {}", file.getId()); + allUniqueImages.addAll(googleImages); + log.info("Google search OK for file {}", file.getId()); } catch (TimeoutException e) { - log.warn("Yandex search timeout for file {}", file.getId()); + log.warn("Google search timeout for file {}", file.getId()); hasTimeout = true; } catch (IOException e) { - log.error("Yandex search failed for file {}", file.getId(), e); + log.error("Google search failed for file {}", file.getId(), e); } +// try { +// String searchResponseYandex = searchImageService.searchReverseByPublicUrl( +// file, "yandex_reverse_image", "visual_matches"); +// +// List yandexImages = +// searchImageService.getAllImagesWithoutPagination(searchResponseYandex, "visual_matches"); +// +// allUniqueImages.addAll(yandexImages); +// log.info("Yandex search OK for file {}", file.getId()); +// +// } 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); +// } + allUniqueImages = searchImageService.removeDuplicateUrls( allUniqueImages.stream() .filter(img -> img.getUrl() != null) From 168aa3812c034b3c1d4395689dcd3301b1f661bb Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Mar 2026 13:30:24 +0700 Subject: [PATCH 182/274] dev add vio --- src/main/java/ru/soune/nocopy/controller/FileController.java | 2 -- src/main/java/ru/soune/nocopy/entity/file/FileType.java | 2 +- .../java/ru/soune/nocopy/service/file/ImageResizeService.java | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/FileController.java b/src/main/java/ru/soune/nocopy/controller/FileController.java index 2b100bc..b044427 100644 --- a/src/main/java/ru/soune/nocopy/controller/FileController.java +++ b/src/main/java/ru/soune/nocopy/controller/FileController.java @@ -92,8 +92,6 @@ public class FileController { return MediaType.IMAGE_JPEG; case "png": return MediaType.IMAGE_PNG; - case "gif": - return MediaType.IMAGE_GIF; case "pdf": return MediaType.APPLICATION_PDF; default: diff --git a/src/main/java/ru/soune/nocopy/entity/file/FileType.java b/src/main/java/ru/soune/nocopy/entity/file/FileType.java index c79f61d..987deec 100644 --- a/src/main/java/ru/soune/nocopy/entity/file/FileType.java +++ b/src/main/java/ru/soune/nocopy/entity/file/FileType.java @@ -7,7 +7,7 @@ import java.util.List; @Getter public enum FileType { - IMAGE("image", Arrays.asList("jpg", "jpeg", "png", "gif", "bmp")), + IMAGE("image", Arrays.asList("jpg", "jpeg", "png", "bmp")), VIDEO("video", Arrays.asList("mp4", "avi", "mov", "wmv", "flv", "mkv", "webm", "m4v", "mpg", "mpeg", "3gp", "3g2", "f4v", "m2ts", "mts", "vob", "ogv", "divx")), AUDIO("audio", List.of("wav")), diff --git a/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java b/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java index 78993b8..57d6216 100644 --- a/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java +++ b/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java @@ -27,7 +27,7 @@ public class ImageResizeService { private String storagePath; public boolean isImage(String extension) { - return extension != null && List.of("jpg", "jpeg", "png", "gif", "webp") + return extension != null && List.of("jpg", "jpeg", "png", "webp") .contains(extension.toLowerCase()); } From 68e102b6cea0fd6efa824cd12db4beb1d897474a Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Mar 2026 14:12:34 +0700 Subject: [PATCH 183/274] dev add check count for all type --- .../nocopy/controller/ApiController.java | 5 ++++ .../handler/ImageFoundRequestHandler.java | 30 ++++++++----------- .../monitoring/MonitoringSearchService.java | 14 ++++----- .../service/search/SearchImageService.java | 2 -- 4 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 8c47397..4026dfe 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -35,6 +35,7 @@ import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.FileSimilarityService; +import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.file.ProtectionsLimitService; import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.file.FileEntityService; @@ -74,6 +75,8 @@ public class ApiController { private final UserRepository userRepository; + private final CheckCounterService checkCounterService; + @PostMapping("/v{version}/data") public ResponseEntity handlePostRequest(@RequestBody BaseRequest request, @PathVariable("version") int version) { @@ -174,6 +177,7 @@ public class ApiController { try { FileEntity fileEntity = fileEntityRepository.findByFileId(fileId); + if (fileEntity == null) { return ResponseEntity.ok().body(new BaseResponse(0, MessageCode.INVALID_FIELD.getCode(), MessageCode.INVALID_FIELD.getDescription(), null)); @@ -188,6 +192,7 @@ public class ApiController { List results = processNonImageFile(fileEntity, authToken); log.info("results: {}", results); response = buildSuccessResponse(results, MessageCode.SIMILAR_FILES_FOUND); + checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); } return ResponseEntity.ok(response); diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 01e8a27..a3d1517 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -1,6 +1,5 @@ package ru.soune.nocopy.handler; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -15,7 +14,6 @@ import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.search.SearchImageService; -import ru.soune.nocopy.service.search.GoogleVisionSearchService; import ru.soune.nocopy.service.tariff.TariffConstants; import ru.soune.nocopy.service.tariff.TariffInfoService; @@ -32,8 +30,6 @@ public class ImageFoundRequestHandler implements RequestHandler { private final SearchImageService searchImageService; - private final GoogleVisionSearchService googleVisionSearchService; - private final CheckCounterService checkCounterService; private final FileEntityRepository fileEntityRepository; @@ -54,7 +50,7 @@ public class ImageFoundRequestHandler implements RequestHandler { Map.of("fileId",fileId))); }); - String searchResponseYandex; +// String searchResponseYandex; String searchResponseGoogle; List allYandexImages = new ArrayList<>(); List allGoogleImages = new ArrayList<>(); @@ -74,18 +70,18 @@ public class ImageFoundRequestHandler implements RequestHandler { log.error("Google search failed for file {}", fileId, e); } - try { - searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", - "visual_matches"); - log.info("searchResponseYandex OK"); - allYandexImages = searchImageService.getAllImagesWithoutPagination( - searchResponseYandex, "visual_matches"); - } catch (TimeoutException e) { - log.warn("Yandex search timeout for file {}", fileId); - hasTimeout = true; - } catch (IOException e) { - log.error("Yandex search failed for file {}", fileId, e); - } +// try { +// searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", +// "visual_matches"); +// log.info("searchResponseYandex OK"); +// allYandexImages = searchImageService.getAllImagesWithoutPagination( +// searchResponseYandex, "visual_matches"); +// } catch (TimeoutException e) { +// log.warn("Yandex search timeout for file {}", fileId); +// hasTimeout = true; +// } catch (IOException e) { +// log.error("Yandex search failed for file {}", fileId, e); +// } allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); log.info("allUniqueImages OK: {} images", allUniqueImages.size()); diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java index 5eedfa5..1ad021c 100644 --- a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java @@ -56,15 +56,15 @@ public class MonitoringSearchService { try { tariffInfoService.writeOffTokens(monitoring.getUserId(), tariffMonitoring.getTokens()); - String yandexResponse = searchImageService.searchReverseByPublicUrl( - monitoring.getFile(), "yandex_reverse_image", "visual_matches"); +// String yandexResponse = searchImageService.searchReverseByPublicUrl( +// monitoring.getFile(), "yandex_reverse_image", "visual_matches"); - List yandexImages = - searchImageService.getAllImagesWithoutPagination(yandexResponse, "visual_matches"); +// List yandexImages = +// searchImageService.getAllImagesWithoutPagination(yandexResponse, "visual_matches"); - for (YandexSearchResponse.ImageResult imageResult : yandexImages) { - violationService.processViolation(imageResult, monitoring.getFile(), null); - } +// for (YandexSearchResponse.ImageResult imageResult : yandexImages) { +// violationService.processViolation(imageResult, monitoring.getFile(), null); +// } try { String googleResponse = searchImageService.searchReverseByPublicUrl( diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 1c35f4a..1c89488 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -44,7 +44,6 @@ public class SearchImageService { .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) -// .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) .build(); this.googleHttpClient = new OkHttpClient.Builder() @@ -56,7 +55,6 @@ public class SearchImageService { .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) -// .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)) .build(); this.objectMapper = new ObjectMapper(); From 9186a6d9b0072f94c4b73f619feb72403c3605b9 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Mar 2026 14:18:56 +0700 Subject: [PATCH 184/274] dev add check count for all type --- .../handler/ImageFoundRequestHandler.java | 26 +++++++++---------- .../monitoring/MonitoringSearchService.java | 14 +++++----- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index a3d1517..22eff35 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -50,7 +50,7 @@ public class ImageFoundRequestHandler implements RequestHandler { Map.of("fileId",fileId))); }); -// String searchResponseYandex; + String searchResponseYandex; String searchResponseGoogle; List allYandexImages = new ArrayList<>(); List allGoogleImages = new ArrayList<>(); @@ -70,18 +70,18 @@ public class ImageFoundRequestHandler implements RequestHandler { log.error("Google search failed for file {}", fileId, e); } -// try { -// searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", -// "visual_matches"); -// log.info("searchResponseYandex OK"); -// allYandexImages = searchImageService.getAllImagesWithoutPagination( -// searchResponseYandex, "visual_matches"); -// } catch (TimeoutException e) { -// log.warn("Yandex search timeout for file {}", fileId); -// hasTimeout = true; -// } catch (IOException e) { -// log.error("Yandex search failed for file {}", fileId, e); -// } + try { + searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", + "visual_matches"); + log.info("searchResponseYandex OK"); + allYandexImages = searchImageService.getAllImagesWithoutPagination( + searchResponseYandex, "visual_matches"); + } catch (TimeoutException e) { + log.warn("Yandex search timeout for file {}", fileId); + hasTimeout = true; + } catch (IOException e) { + log.error("Yandex search failed for file {}", fileId, e); + } allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); log.info("allUniqueImages OK: {} images", allUniqueImages.size()); diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java index 1ad021c..5eedfa5 100644 --- a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java @@ -56,15 +56,15 @@ public class MonitoringSearchService { try { tariffInfoService.writeOffTokens(monitoring.getUserId(), tariffMonitoring.getTokens()); -// String yandexResponse = searchImageService.searchReverseByPublicUrl( -// monitoring.getFile(), "yandex_reverse_image", "visual_matches"); + String yandexResponse = searchImageService.searchReverseByPublicUrl( + monitoring.getFile(), "yandex_reverse_image", "visual_matches"); -// List yandexImages = -// searchImageService.getAllImagesWithoutPagination(yandexResponse, "visual_matches"); + List yandexImages = + searchImageService.getAllImagesWithoutPagination(yandexResponse, "visual_matches"); -// for (YandexSearchResponse.ImageResult imageResult : yandexImages) { -// violationService.processViolation(imageResult, monitoring.getFile(), null); -// } + for (YandexSearchResponse.ImageResult imageResult : yandexImages) { + violationService.processViolation(imageResult, monitoring.getFile(), null); + } try { String googleResponse = searchImageService.searchReverseByPublicUrl( From c56b2ba4e9c7a671892df65e6bd8245168162fc0 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Mar 2026 14:28:17 +0700 Subject: [PATCH 185/274] dev add check count for all type --- .../handler/ImageFoundRequestHandler.java | 26 +++++++++---------- .../service/search/SearchImageService.java | 8 +++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 22eff35..a3d1517 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -50,7 +50,7 @@ public class ImageFoundRequestHandler implements RequestHandler { Map.of("fileId",fileId))); }); - String searchResponseYandex; +// String searchResponseYandex; String searchResponseGoogle; List allYandexImages = new ArrayList<>(); List allGoogleImages = new ArrayList<>(); @@ -70,18 +70,18 @@ public class ImageFoundRequestHandler implements RequestHandler { log.error("Google search failed for file {}", fileId, e); } - try { - searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", - "visual_matches"); - log.info("searchResponseYandex OK"); - allYandexImages = searchImageService.getAllImagesWithoutPagination( - searchResponseYandex, "visual_matches"); - } catch (TimeoutException e) { - log.warn("Yandex search timeout for file {}", fileId); - hasTimeout = true; - } catch (IOException e) { - log.error("Yandex search failed for file {}", fileId, e); - } +// try { +// searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", +// "visual_matches"); +// log.info("searchResponseYandex OK"); +// allYandexImages = searchImageService.getAllImagesWithoutPagination( +// searchResponseYandex, "visual_matches"); +// } catch (TimeoutException e) { +// log.warn("Yandex search timeout for file {}", fileId); +// hasTimeout = true; +// } catch (IOException e) { +// log.error("Yandex search failed for file {}", fileId, e); +// } allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); log.info("allUniqueImages OK: {} images", allUniqueImages.size()); diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 1c89488..c70f14e 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -202,10 +202,10 @@ public class SearchImageService { uniqueByUrl.putIfAbsent(normalizedUrl, image); } - for (YandexSearchResponse.ImageResult image : yandexImages) { - String normalizedUrl = normalizeUrl(image.getUrl()); - uniqueByUrl.putIfAbsent(normalizedUrl, image); - } +// for (YandexSearchResponse.ImageResult image : yandexImages) { +// String normalizedUrl = normalizeUrl(image.getUrl()); +// uniqueByUrl.putIfAbsent(normalizedUrl, image); +// } return new ArrayList<>(uniqueByUrl.values()); } From 73fd5e9456b78b8cd6529cfd2267ba8ce0780fc6 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Mar 2026 14:40:34 +0700 Subject: [PATCH 186/274] dev add check count for all type --- .../handler/ImageFoundRequestHandler.java | 26 +++++++-------- .../search/GlobalSearchAsyncProcessor.java | 32 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index a3d1517..22eff35 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -50,7 +50,7 @@ public class ImageFoundRequestHandler implements RequestHandler { Map.of("fileId",fileId))); }); -// String searchResponseYandex; + String searchResponseYandex; String searchResponseGoogle; List allYandexImages = new ArrayList<>(); List allGoogleImages = new ArrayList<>(); @@ -70,18 +70,18 @@ public class ImageFoundRequestHandler implements RequestHandler { log.error("Google search failed for file {}", fileId, e); } -// try { -// searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", -// "visual_matches"); -// log.info("searchResponseYandex OK"); -// allYandexImages = searchImageService.getAllImagesWithoutPagination( -// searchResponseYandex, "visual_matches"); -// } catch (TimeoutException e) { -// log.warn("Yandex search timeout for file {}", fileId); -// hasTimeout = true; -// } catch (IOException e) { -// log.error("Yandex search failed for file {}", fileId, e); -// } + try { + searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", + "visual_matches"); + log.info("searchResponseYandex OK"); + allYandexImages = searchImageService.getAllImagesWithoutPagination( + searchResponseYandex, "visual_matches"); + } catch (TimeoutException e) { + log.warn("Yandex search timeout for file {}", fileId); + hasTimeout = true; + } catch (IOException e) { + log.error("Yandex search failed for file {}", fileId, e); + } allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); log.info("allUniqueImages OK: {} images", allUniqueImages.size()); diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java index 7325e81..fb39b84 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -105,22 +105,22 @@ public class GlobalSearchAsyncProcessor { log.error("Google search failed for file {}", file.getId(), e); } -// try { -// String searchResponseYandex = searchImageService.searchReverseByPublicUrl( -// file, "yandex_reverse_image", "visual_matches"); -// -// List yandexImages = -// searchImageService.getAllImagesWithoutPagination(searchResponseYandex, "visual_matches"); -// -// allUniqueImages.addAll(yandexImages); -// log.info("Yandex search OK for file {}", file.getId()); -// -// } 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); -// } + try { + String searchResponseYandex = searchImageService.searchReverseByPublicUrl( + file, "yandex_reverse_image", "visual_matches"); + + List yandexImages = + searchImageService.getAllImagesWithoutPagination(searchResponseYandex, "visual_matches"); + + allUniqueImages.addAll(yandexImages); + log.info("Yandex search OK for file {}", file.getId()); + + } 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); + } allUniqueImages = searchImageService.removeDuplicateUrls( allUniqueImages.stream() From 3a478f919555afb236fcc5a280ff45b2f56dc594 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Mar 2026 14:48:39 +0700 Subject: [PATCH 187/274] dev add check count for all type --- .../handler/ImageFoundRequestHandler.java | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 22eff35..f6fdbd6 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -50,9 +50,9 @@ public class ImageFoundRequestHandler implements RequestHandler { Map.of("fileId",fileId))); }); - String searchResponseYandex; +// String searchResponseYandex; String searchResponseGoogle; - List allYandexImages = new ArrayList<>(); +// List allYandexImages = new ArrayList<>(); List allGoogleImages = new ArrayList<>(); List allUniqueImages; boolean hasTimeout = false; @@ -70,21 +70,21 @@ public class ImageFoundRequestHandler implements RequestHandler { log.error("Google search failed for file {}", fileId, e); } - try { - searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", - "visual_matches"); - log.info("searchResponseYandex OK"); - allYandexImages = searchImageService.getAllImagesWithoutPagination( - searchResponseYandex, "visual_matches"); - } catch (TimeoutException e) { - log.warn("Yandex search timeout for file {}", fileId); - hasTimeout = true; - } catch (IOException e) { - log.error("Yandex search failed for file {}", fileId, e); - } +// try { +// searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", +// "visual_matches"); +// log.info("searchResponseYandex OK"); +// allYandexImages = searchImageService.getAllImagesWithoutPagination( +// searchResponseYandex, "visual_matches"); +// } catch (TimeoutException e) { +// log.warn("Yandex search timeout for file {}", fileId); +// hasTimeout = true; +// } catch (IOException e) { +// log.error("Yandex search failed for file {}", fileId, e); +// } - allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); - log.info("allUniqueImages OK: {} images", allUniqueImages.size()); +// allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); +// log.info("allUniqueImages OK: {} images", allUniqueImages.size()); tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); @@ -92,15 +92,17 @@ public class ImageFoundRequestHandler implements RequestHandler { int page = imageSearchRequest.getPage() != null ? imageSearchRequest.getPage() : 1; int pageSize = 5; - List pagedResults = searchImageService.paginateResults(allUniqueImages, page, +// List pagedResults = searchImageService.paginateResults(allUniqueImages, page, +// pageSize * 2); + List pagedResults = searchImageService.paginateResults(allGoogleImages, page, pageSize * 2); YandexSearchResponse finalResponse = new YandexSearchResponse(); finalResponse.setImages(pagedResults); finalResponse.setPage(page); finalResponse.setPageSize(pageSize * 2); - finalResponse.setTotalResults(allUniqueImages.size()); - finalResponse.setTotalPages((int) Math.ceil((double) allUniqueImages.size() / (pageSize * 2))); + finalResponse.setTotalResults(allGoogleImages.size()); + finalResponse.setTotalPages((int) Math.ceil((double) allGoogleImages.size() / (pageSize * 2))); String message = hasTimeout ? "Search completed (partial)" : MessageCode.SUCCESS.getDescription(); From 0f7079d118ab56df83f276514b4203623c44fc51 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Mar 2026 15:03:02 +0700 Subject: [PATCH 188/274] dev add check count for all type --- .../handler/ImageFoundRequestHandler.java | 43 ++++++++++--------- .../service/search/SearchImageService.java | 8 ++-- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index f6fdbd6..80fcb85 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -50,9 +50,9 @@ public class ImageFoundRequestHandler implements RequestHandler { Map.of("fileId",fileId))); }); -// String searchResponseYandex; + // String searchResponseYandex; String searchResponseGoogle; -// List allYandexImages = new ArrayList<>(); + // List allYandexImages = new ArrayList<>(); List allGoogleImages = new ArrayList<>(); List allUniqueImages; boolean hasTimeout = false; @@ -70,21 +70,24 @@ public class ImageFoundRequestHandler implements RequestHandler { log.error("Google search failed for file {}", fileId, e); } -// try { -// searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", -// "visual_matches"); -// log.info("searchResponseYandex OK"); -// allYandexImages = searchImageService.getAllImagesWithoutPagination( -// searchResponseYandex, "visual_matches"); -// } catch (TimeoutException e) { -// log.warn("Yandex search timeout for file {}", fileId); -// hasTimeout = true; -// } catch (IOException e) { -// log.error("Yandex search failed for file {}", fileId, e); -// } + /* + try { + searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", + "visual_matches"); + log.info("searchResponseYandex OK"); + allYandexImages = searchImageService.getAllImagesWithoutPagination( + searchResponseYandex, "visual_matches"); + } catch (TimeoutException e) { + log.warn("Yandex search timeout for file {}", fileId); + hasTimeout = true; + } catch (IOException e) { + log.error("Yandex search failed for file {}", fileId, e); + } + */ -// allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); -// log.info("allUniqueImages OK: {} images", allUniqueImages.size()); + // allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); + allUniqueImages = allGoogleImages; + log.info("allUniqueImages OK: {} images", allUniqueImages.size()); tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); @@ -92,17 +95,15 @@ public class ImageFoundRequestHandler implements RequestHandler { int page = imageSearchRequest.getPage() != null ? imageSearchRequest.getPage() : 1; int pageSize = 5; -// List pagedResults = searchImageService.paginateResults(allUniqueImages, page, -// pageSize * 2); - List pagedResults = searchImageService.paginateResults(allGoogleImages, page, + List pagedResults = searchImageService.paginateResults(allUniqueImages, page, pageSize * 2); YandexSearchResponse finalResponse = new YandexSearchResponse(); finalResponse.setImages(pagedResults); finalResponse.setPage(page); finalResponse.setPageSize(pageSize * 2); - finalResponse.setTotalResults(allGoogleImages.size()); - finalResponse.setTotalPages((int) Math.ceil((double) allGoogleImages.size() / (pageSize * 2))); + finalResponse.setTotalResults(allUniqueImages.size()); + finalResponse.setTotalPages((int) Math.ceil((double) allUniqueImages.size() / (pageSize * 2))); String message = hasTimeout ? "Search completed (partial)" : MessageCode.SUCCESS.getDescription(); diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index c70f14e..1c89488 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -202,10 +202,10 @@ public class SearchImageService { uniqueByUrl.putIfAbsent(normalizedUrl, image); } -// for (YandexSearchResponse.ImageResult image : yandexImages) { -// String normalizedUrl = normalizeUrl(image.getUrl()); -// uniqueByUrl.putIfAbsent(normalizedUrl, image); -// } + for (YandexSearchResponse.ImageResult image : yandexImages) { + String normalizedUrl = normalizeUrl(image.getUrl()); + uniqueByUrl.putIfAbsent(normalizedUrl, image); + } return new ArrayList<>(uniqueByUrl.values()); } From 7b3000152fbe4ff8e40eb498b6790fe997c3f731 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Mar 2026 15:18:30 +0700 Subject: [PATCH 189/274] dev add check count for all type --- .../ru/soune/nocopy/handler/ImageFoundRequestHandler.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 80fcb85..147698f 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -61,6 +61,9 @@ public class ImageFoundRequestHandler implements RequestHandler { searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", "exact_matches"); log.info("searchResponseGoogle OK"); + log.info("searchResponseGoogle OK"); + log.info("searchResponseGoogle OK"); + log.info("searchResponseGoogle OK,{}", searchResponseGoogle); allGoogleImages = searchImageService.getAllImagesWithoutPagination( searchResponseGoogle, "exact_matches"); } catch (TimeoutException e) { @@ -88,6 +91,11 @@ public class ImageFoundRequestHandler implements RequestHandler { // allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); allUniqueImages = allGoogleImages; log.info("allUniqueImages OK: {} images", allUniqueImages.size()); + log.info("allUniqueImages OK: {} images", allUniqueImages.size()); + log.info("allUniqueImages OK: {} images", allUniqueImages.size()); + log.info("allUniqueImages OK: {} images", allUniqueImages.size()); + log.info("allUniqueImages OK: {} images", allUniqueImages.size()); + log.info("allUniqueImages OK: {} images", allUniqueImages.size()); tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); From a1669826b53d0a31ec88aface24f0cbfb9671290 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Mar 2026 15:35:08 +0700 Subject: [PATCH 190/274] dev add check count for all type --- .../soune/nocopy/handler/ImageFoundRequestHandler.java | 8 -------- .../soune/nocopy/service/search/SearchImageService.java | 9 +++++++-- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 147698f..449af20 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -60,9 +60,6 @@ public class ImageFoundRequestHandler implements RequestHandler { try { searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", "exact_matches"); - log.info("searchResponseGoogle OK"); - log.info("searchResponseGoogle OK"); - log.info("searchResponseGoogle OK"); log.info("searchResponseGoogle OK,{}", searchResponseGoogle); allGoogleImages = searchImageService.getAllImagesWithoutPagination( searchResponseGoogle, "exact_matches"); @@ -91,11 +88,6 @@ public class ImageFoundRequestHandler implements RequestHandler { // allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); allUniqueImages = allGoogleImages; log.info("allUniqueImages OK: {} images", allUniqueImages.size()); - log.info("allUniqueImages OK: {} images", allUniqueImages.size()); - log.info("allUniqueImages OK: {} images", allUniqueImages.size()); - log.info("allUniqueImages OK: {} images", allUniqueImages.size()); - log.info("allUniqueImages OK: {} images", allUniqueImages.size()); - log.info("allUniqueImages OK: {} images", allUniqueImages.size()); tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 1c89488..a6da0dd 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -169,8 +169,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"); From 18c07b427c9d39345e5dcafbb1622932bc7d262b Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 16 Mar 2026 15:46:19 +0700 Subject: [PATCH 191/274] dev comment all yandex search --- .../monitoring/MonitoringSearchService.java | 16 +++---- .../search/GlobalSearchAsyncProcessor.java | 47 ++++++++++--------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java index 5eedfa5..bf5217d 100644 --- a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java @@ -56,15 +56,15 @@ public class MonitoringSearchService { try { tariffInfoService.writeOffTokens(monitoring.getUserId(), tariffMonitoring.getTokens()); - String yandexResponse = searchImageService.searchReverseByPublicUrl( - monitoring.getFile(), "yandex_reverse_image", "visual_matches"); +// String yandexResponse = searchImageService.searchReverseByPublicUrl( +// monitoring.getFile(), "yandex_reverse_image", "visual_matches"); - List yandexImages = - searchImageService.getAllImagesWithoutPagination(yandexResponse, "visual_matches"); - - for (YandexSearchResponse.ImageResult imageResult : yandexImages) { - violationService.processViolation(imageResult, monitoring.getFile(), null); - } +// List yandexImages = +// searchImageService.getAllImagesWithoutPagination(yandexResponse, "visual_matches"); +// +// for (YandexSearchResponse.ImageResult imageResult : yandexImages) { +// violationService.processViolation(imageResult, monitoring.getFile(), null); +// } try { String googleResponse = searchImageService.searchReverseByPublicUrl( diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java index fb39b84..89d6f8f 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -85,15 +85,16 @@ public class GlobalSearchAsyncProcessor { result = globalSearchResultRepository.save(result); - List allUniqueImages = new ArrayList<>(); + List googleImages = new ArrayList<>(); + List allUniqueImages = new ArrayList<>(); boolean hasTimeout = false; try { String searchResponseGoogle = searchImageService.searchReverseByPublicUrl( file, "google_lens", "exact_matches"); - List googleImages = - searchImageService.getAllImagesWithoutPagination(searchResponseGoogle, "exact_matches"); + googleImages = searchImageService.getAllImagesWithoutPagination(searchResponseGoogle, + "exact_matches"); allUniqueImages.addAll(googleImages); log.info("Google search OK for file {}", file.getId()); @@ -105,28 +106,30 @@ public class GlobalSearchAsyncProcessor { log.error("Google search failed for file {}", file.getId(), e); } - try { - String searchResponseYandex = searchImageService.searchReverseByPublicUrl( - file, "yandex_reverse_image", "visual_matches"); +// try { +// String searchResponseYandex = searchImageService.searchReverseByPublicUrl( +// file, "yandex_reverse_image", "visual_matches"); +// +// List yandexImages = +// searchImageService.getAllImagesWithoutPagination(searchResponseYandex, "visual_matches"); - List yandexImages = - searchImageService.getAllImagesWithoutPagination(searchResponseYandex, "visual_matches"); +// allUniqueImages.addAll(yandexImages); +// log.info("Yandex search OK for file {}", file.getId()); +// +// } 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); +// } - allUniqueImages.addAll(yandexImages); - log.info("Yandex search OK for file {}", file.getId()); +// allUniqueImages = searchImageService.removeDuplicateUrls( +// allUniqueImages.stream() +// .filter(img -> img.getUrl() != null) +// .collect(Collectors.toList()), +// new ArrayList<>()); - } 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); - } - - allUniqueImages = searchImageService.removeDuplicateUrls( - allUniqueImages.stream() - .filter(img -> img.getUrl() != null) - .collect(Collectors.toList()), - new ArrayList<>()); + allUniqueImages = googleImages; for (YandexSearchResponse.ImageResult imageResult : allUniqueImages) { violationService.processViolation(imageResult, file, result); From 59125b4d196a726acf4f7d5fcfe5d758807c001e Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Mar 2026 11:08:33 +0700 Subject: [PATCH 192/274] dev comment all yandex search --- .../nocopy/handler/ImageFoundRequestHandler.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 449af20..e833ee6 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -50,9 +50,9 @@ public class ImageFoundRequestHandler implements RequestHandler { Map.of("fileId",fileId))); }); - // String searchResponseYandex; + String searchResponseYandex; String searchResponseGoogle; - // List allYandexImages = new ArrayList<>(); + List allYandexImages = new ArrayList<>(); List allGoogleImages = new ArrayList<>(); List allUniqueImages; boolean hasTimeout = false; @@ -70,7 +70,7 @@ public class ImageFoundRequestHandler implements RequestHandler { log.error("Google search failed for file {}", fileId, e); } - /* +// /* try { searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", "visual_matches"); @@ -83,10 +83,11 @@ public class ImageFoundRequestHandler implements RequestHandler { } catch (IOException e) { log.error("Yandex search failed for file {}", fileId, e); } - */ +// */ // allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); - allUniqueImages = allGoogleImages; +// allUniqueImages = allGoogleImages; + allUniqueImages = allYandexImages; log.info("allUniqueImages OK: {} images", allUniqueImages.size()); tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); From 566c1634a2ca5565a749d15c60d284f39d11ef51 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Mar 2026 11:21:16 +0700 Subject: [PATCH 193/274] dev comment all yandex search --- .../ru/soune/nocopy/handler/ImageFoundRequestHandler.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index e833ee6..3c7357d 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -63,6 +63,7 @@ public class ImageFoundRequestHandler implements RequestHandler { log.info("searchResponseGoogle OK,{}", searchResponseGoogle); allGoogleImages = searchImageService.getAllImagesWithoutPagination( searchResponseGoogle, "exact_matches"); + } catch (TimeoutException e) { log.warn("Google search timeout for file {}", fileId); hasTimeout = true; @@ -70,7 +71,6 @@ public class ImageFoundRequestHandler implements RequestHandler { log.error("Google search failed for file {}", fileId, e); } -// /* try { searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", "visual_matches"); @@ -83,11 +83,10 @@ public class ImageFoundRequestHandler implements RequestHandler { } catch (IOException e) { log.error("Yandex search failed for file {}", fileId, e); } -// */ - // allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); + allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); // allUniqueImages = allGoogleImages; - allUniqueImages = allYandexImages; +// allUniqueImages = allYandexImages; log.info("allUniqueImages OK: {} images", allUniqueImages.size()); tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); From 28bc24647ad7b400cdd8c0a9b0c4dbe2640f18c1 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Mar 2026 11:28:25 +0700 Subject: [PATCH 194/274] dev comment all yandex search --- .../handler/ImageFoundRequestHandler.java | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 3c7357d..f87ff5c 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -50,9 +50,9 @@ public class ImageFoundRequestHandler implements RequestHandler { Map.of("fileId",fileId))); }); - String searchResponseYandex; +// String searchResponseYandex; String searchResponseGoogle; - List allYandexImages = new ArrayList<>(); +// List allYandexImages = new ArrayList<>(); List allGoogleImages = new ArrayList<>(); List allUniqueImages; boolean hasTimeout = false; @@ -71,21 +71,21 @@ public class ImageFoundRequestHandler implements RequestHandler { log.error("Google search failed for file {}", fileId, e); } - try { - searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", - "visual_matches"); - log.info("searchResponseYandex OK"); - allYandexImages = searchImageService.getAllImagesWithoutPagination( - searchResponseYandex, "visual_matches"); - } catch (TimeoutException e) { - log.warn("Yandex search timeout for file {}", fileId); - hasTimeout = true; - } catch (IOException e) { - log.error("Yandex search failed for file {}", fileId, e); - } +// try { +// searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", +// "visual_matches"); +// log.info("searchResponseYandex OK"); +// allYandexImages = searchImageService.getAllImagesWithoutPagination( +// searchResponseYandex, "visual_matches"); +// } catch (TimeoutException e) { +// log.warn("Yandex search timeout for file {}", fileId); +// hasTimeout = true; +// } catch (IOException e) { +// log.error("Yandex search failed for file {}", fileId, e); +// } - allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); -// allUniqueImages = allGoogleImages; +// allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); + allUniqueImages = allGoogleImages; // allUniqueImages = allYandexImages; log.info("allUniqueImages OK: {} images", allUniqueImages.size()); From 95edcdf5276c03349329eb8c2867d9ac4f23f28b Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Mar 2026 12:20:19 +0700 Subject: [PATCH 195/274] dev comment all yandex search --- .../handler/ImageFoundRequestHandler.java | 64 +++++++++---------- .../service/search/SearchImageService.java | 8 +-- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index f87ff5c..2f42428 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -50,43 +50,43 @@ public class ImageFoundRequestHandler implements RequestHandler { Map.of("fileId",fileId))); }); -// String searchResponseYandex; - String searchResponseGoogle; -// List allYandexImages = new ArrayList<>(); - List allGoogleImages = new ArrayList<>(); + String searchResponseYandex; +// String searchResponseGoogle; + List allYandexImages = new ArrayList<>(); +// List allGoogleImages = new ArrayList<>(); List allUniqueImages; boolean hasTimeout = false; +// +// try { +// searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", +// "exact_matches"); +// log.info("searchResponseGoogle OK,{}", searchResponseGoogle); +// allGoogleImages = searchImageService.getAllImagesWithoutPagination( +// searchResponseGoogle, "exact_matches"); +// +// } catch (TimeoutException e) { +// log.warn("Google search timeout for file {}", fileId); +// hasTimeout = true; +// } catch (IOException e) { +// log.error("Google search failed for file {}", fileId, e); +// } - try { - searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", - "exact_matches"); - log.info("searchResponseGoogle OK,{}", searchResponseGoogle); - allGoogleImages = searchImageService.getAllImagesWithoutPagination( - searchResponseGoogle, "exact_matches"); - - } catch (TimeoutException e) { - log.warn("Google search timeout for file {}", fileId); - hasTimeout = true; - } catch (IOException e) { - log.error("Google search failed for file {}", fileId, e); - } - -// try { -// searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", -// "visual_matches"); -// log.info("searchResponseYandex OK"); -// allYandexImages = searchImageService.getAllImagesWithoutPagination( -// searchResponseYandex, "visual_matches"); -// } catch (TimeoutException e) { -// log.warn("Yandex search timeout for file {}", fileId); -// hasTimeout = true; -// } catch (IOException e) { -// log.error("Yandex search failed for file {}", fileId, e); -// } + try { + searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", + "visual_matches"); + log.info("searchResponseYandex OK"); + allYandexImages = searchImageService.getAllImagesWithoutPagination( + searchResponseYandex, "visual_matches"); + } catch (TimeoutException e) { + log.warn("Yandex search timeout for file {}", fileId); + hasTimeout = true; + } catch (IOException e) { + log.error("Yandex search failed for file {}", fileId, e); + } // allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); - allUniqueImages = allGoogleImages; -// allUniqueImages = allYandexImages; +// allUniqueImages = allGoogleImages; + allUniqueImages = allYandexImages; log.info("allUniqueImages OK: {} images", allUniqueImages.size()); tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index a6da0dd..cb203a0 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -32,11 +32,11 @@ public class SearchImageService { private final OkHttpClient googleHttpClient; public SearchImageService() { - Proxy proxy = new Proxy(Proxy.Type.HTTP, - new InetSocketAddress("193.46.217.94", 3128)); +// Proxy proxy = new Proxy(Proxy.Type.HTTP, +// new InetSocketAddress("193.46.217.94", 3128)); this.yandexHttpClient = new OkHttpClient.Builder() - .proxy(proxy) +// .proxy(proxy) .connectTimeout(35, TimeUnit.SECONDS) .writeTimeout(35, TimeUnit.SECONDS) .readTimeout(35, TimeUnit.SECONDS) @@ -47,7 +47,7 @@ public class SearchImageService { .build(); this.googleHttpClient = new OkHttpClient.Builder() - .proxy(proxy) +// .proxy(proxy) .connectTimeout(35, TimeUnit.SECONDS) .writeTimeout(35, TimeUnit.SECONDS) .readTimeout(35, TimeUnit.SECONDS) From e66225cdd1e542f823274c5321e11d2f20313764 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Mar 2026 12:32:48 +0700 Subject: [PATCH 196/274] dev comment all google search --- .../monitoring/MonitoringSearchService.java | 44 +++++++------- .../search/GlobalSearchAsyncProcessor.java | 57 ++++++++++--------- 2 files changed, 51 insertions(+), 50 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java index bf5217d..0b11018 100644 --- a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java @@ -56,32 +56,32 @@ public class MonitoringSearchService { try { tariffInfoService.writeOffTokens(monitoring.getUserId(), tariffMonitoring.getTokens()); -// String yandexResponse = searchImageService.searchReverseByPublicUrl( -// monitoring.getFile(), "yandex_reverse_image", "visual_matches"); + String yandexResponse = searchImageService.searchReverseByPublicUrl( + monitoring.getFile(), "yandex_reverse_image", "visual_matches"); -// List yandexImages = -// searchImageService.getAllImagesWithoutPagination(yandexResponse, "visual_matches"); -// -// for (YandexSearchResponse.ImageResult imageResult : yandexImages) { -// violationService.processViolation(imageResult, monitoring.getFile(), null); -// } + List yandexImages = + searchImageService.getAllImagesWithoutPagination(yandexResponse, "visual_matches"); - try { - String googleResponse = searchImageService.searchReverseByPublicUrl( - monitoring.getFile(), "google_lens", "exact_matches"); - - List googleImages = - searchImageService.getAllImagesWithoutPagination(googleResponse, "exact_matches"); - - - for (YandexSearchResponse.ImageResult imageResult : googleImages) { - violationService.processViolation(imageResult, monitoring.getFile(), null); - } - - } catch (TimeoutException | IOException e) { - log.warn("Google search failed"); + for (YandexSearchResponse.ImageResult imageResult : yandexImages) { + violationService.processViolation(imageResult, monitoring.getFile(), null); } +// try { +// String googleResponse = searchImageService.searchReverseByPublicUrl( +// monitoring.getFile(), "google_lens", "exact_matches"); +// +// List googleImages = +// searchImageService.getAllImagesWithoutPagination(googleResponse, "exact_matches"); +// +// +// for (YandexSearchResponse.ImageResult imageResult : googleImages) { +// violationService.processViolation(imageResult, monitoring.getFile(), null); +// } +// +// } catch (TimeoutException | IOException e) { +// log.warn("Google search failed"); +// } + monitoring.setLastRunStatus("SUCCESS"); } catch (TariffNotFoundException e) { User user = userRepository.findById(monitoring.getUserId()).orElseThrow(); diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java index 89d6f8f..9f202c4 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -85,51 +85,52 @@ public class GlobalSearchAsyncProcessor { result = globalSearchResultRepository.save(result); - List googleImages = new ArrayList<>(); +// List googleImages = new ArrayList<>(); + List yandexImages = new ArrayList<>(); List allUniqueImages = new ArrayList<>(); boolean hasTimeout = false; - try { - String searchResponseGoogle = searchImageService.searchReverseByPublicUrl( - file, "google_lens", "exact_matches"); - - googleImages = searchImageService.getAllImagesWithoutPagination(searchResponseGoogle, - "exact_matches"); - - allUniqueImages.addAll(googleImages); - log.info("Google search OK for file {}", file.getId()); - - } 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); - } - // try { -// String searchResponseYandex = searchImageService.searchReverseByPublicUrl( -// file, "yandex_reverse_image", "visual_matches"); +// String searchResponseGoogle = searchImageService.searchReverseByPublicUrl( +// file, "google_lens", "exact_matches"); // -// List yandexImages = -// searchImageService.getAllImagesWithoutPagination(searchResponseYandex, "visual_matches"); - -// allUniqueImages.addAll(yandexImages); -// log.info("Yandex search OK for file {}", file.getId()); +// googleImages = searchImageService.getAllImagesWithoutPagination(searchResponseGoogle, +// "exact_matches"); +// +// allUniqueImages.addAll(googleImages); +// log.info("Google search OK for file {}", file.getId()); // // } catch (TimeoutException e) { -// log.warn("Yandex search timeout for file {}", file.getId()); +// log.warn("Google search timeout for file {}", file.getId()); // hasTimeout = true; // } catch (IOException e) { -// log.error("Yandex search failed for file {}", file.getId(), e); +// log.error("Google search failed for file {}", file.getId(), e); // } + try { + String searchResponseYandex = searchImageService.searchReverseByPublicUrl( + file, "yandex_reverse_image", "visual_matches"); + + yandexImages = + searchImageService.getAllImagesWithoutPagination(searchResponseYandex, "visual_matches"); + + allUniqueImages.addAll(yandexImages); + log.info("Yandex search OK for file {}", file.getId()); + + } 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); + } + // allUniqueImages = searchImageService.removeDuplicateUrls( // allUniqueImages.stream() // .filter(img -> img.getUrl() != null) // .collect(Collectors.toList()), // new ArrayList<>()); - allUniqueImages = googleImages; + allUniqueImages = yandexImages; for (YandexSearchResponse.ImageResult imageResult : allUniqueImages) { violationService.processViolation(imageResult, file, result); From 15d1824757c1fb18b8f7a101cdf35fa33c82c16a Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Mar 2026 14:50:43 +0700 Subject: [PATCH 197/274] dev comment all google search --- .../java/ru/soune/nocopy/dto/file/FileEntityResponse.java | 1 + .../java/ru/soune/nocopy/service/file/FileEntityService.java | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/ru/soune/nocopy/dto/file/FileEntityResponse.java b/src/main/java/ru/soune/nocopy/dto/file/FileEntityResponse.java index 504e8b3..60c65b6 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/FileEntityResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/file/FileEntityResponse.java @@ -20,6 +20,7 @@ public class FileEntityResponse { private String storedFileName; private String filePath; private String protectedFilePath; + private String thumbnailFileUrl; private Long fileSize; private String mimeType; private String fileExtension; diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index 58cd956..3131eac 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -2,6 +2,7 @@ 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; @@ -50,6 +51,9 @@ public class FileEntityService { private final ImageResizeService imageResizeService; + @Value("${server.baseurl}") + private String baseUrl; + @Transactional(noRollbackFor = DuplicateImageException.class) public FileEntity createFromUploadSession(FileUploadSession session, String checksum) { log.info("Creating FileEntity for upload session: {}", session.getUploadId()); @@ -417,6 +421,7 @@ public class FileEntityService { .checksCount(0) .fileUploadDate(fileEntity.getCreatedAt()) .protectedFilePath(fileEntity.getProtectedFilePath()) + .thumbnailFileUrl(baseUrl + "/api/files/protected/" + fileEntity.getId() + "/thumbnail") .build(); } From f1e0f9594ed2661e812ddc84ecced64ce7ae7191 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Mar 2026 16:35:27 +0700 Subject: [PATCH 198/274] dev add violation notion --- .../nocopy/configuration/HandlerConfig.java | 4 +- .../java/ru/soune/nocopy/dto/MessageCode.java | 4 +- .../dto/violation/DeleteNotionResponse.java | 12 + .../dto/violation/ViolationNotionRequest.java | 12 + .../violation/ViolationNotionResponse.java | 30 ++ .../ViolationNotionsListResponse.java | 15 + .../entity/violation/ViolationNotion.java | 47 ++++ .../handler/ViolationNotionHandler.java | 265 ++++++++++++++++++ .../repository/ViolationNotionRepository.java | 24 ++ .../violation/ViolationNotionService.java | 138 +++++++++ 10 files changed, 549 insertions(+), 2 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/violation/DeleteNotionResponse.java create mode 100644 src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionResponse.java create mode 100644 src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionsListResponse.java create mode 100644 src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java create mode 100644 src/main/java/ru/soune/nocopy/handler/ViolationNotionHandler.java create mode 100644 src/main/java/ru/soune/nocopy/repository/ViolationNotionRepository.java create mode 100644 src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index 2b02251..4c3baff 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -31,7 +31,8 @@ public class HandlerConfig { MonitoringHandler monitoringHandler, ViolationHandler violationHandler, ViolationStatisticsHandler violationStatisticsHandler, - GlobalSearchHandler globalSearchHandler + GlobalSearchHandler globalSearchHandler, + ViolationNotionHandler violationNotionHandler ) { Map map = new HashMap<>(); map.put(20001, login); @@ -54,6 +55,7 @@ public class HandlerConfig { map.put(30009, violationHandler); map.put(30010, violationStatisticsHandler); map.put(30011, globalSearchHandler); + map.put(30012, violationNotionHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index 4384659..ec273ba 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -53,7 +53,9 @@ public enum MessageCode { FILE_FOR_SEARCH_NOT_VALID(2, "File for search unsupported"), NOT_VALID_FILE_TYPE_OR_COUNT_FILE(2, "Cost for file type not found, count is negative or files count" + "more than max valid"), - USER_NOT_ACTIVE(2, "User not active"); + USER_NOT_ACTIVE(2, "User not active"), + NOTION_NOT_FOUND(4, "Notion not found"), + MESSAGE_IS_REQUIRED_FOR_NOTION(4, "Message is required for notion"); private final Integer code; diff --git a/src/main/java/ru/soune/nocopy/dto/violation/DeleteNotionResponse.java b/src/main/java/ru/soune/nocopy/dto/violation/DeleteNotionResponse.java new file mode 100644 index 0000000..c3c06fb --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/DeleteNotionResponse.java @@ -0,0 +1,12 @@ +package ru.soune.nocopy.dto.violation; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class DeleteNotionResponse { + private Long notionId; + private String message; + private boolean deleted; +} diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionRequest.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionRequest.java new file mode 100644 index 0000000..fca54be --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionRequest.java @@ -0,0 +1,12 @@ +package ru.soune.nocopy.dto.violation; + +import lombok.Data; + +@Data +public class ViolationNotionRequest { + private String token; + private String fileId; + private String message; + private String action; + private Long notionId; +} diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionResponse.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionResponse.java new file mode 100644 index 0000000..17da29a --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionResponse.java @@ -0,0 +1,30 @@ +package ru.soune.nocopy.dto.violation; +import lombok.Builder; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@Builder +public class ViolationNotionResponse { + private Long id; + private String message; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private UserInfo user; + private FileInfo file; + + @Data + @Builder + public static class UserInfo { + private Long id; + private String fullName; + private String email; + } + + @Data + @Builder + public static class FileInfo { + private String id; + private String originalFileName; + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionsListResponse.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionsListResponse.java new file mode 100644 index 0000000..884bb32 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionsListResponse.java @@ -0,0 +1,15 @@ +package ru.soune.nocopy.dto.violation; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class ViolationNotionsListResponse { + private String fileId; + private String fileName; + private int totalCount; + private List notions; +} diff --git a/src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java b/src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java new file mode 100644 index 0000000..aa93cbc --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java @@ -0,0 +1,47 @@ +package ru.soune.nocopy.entity.violation; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.user.User; + +import java.time.LocalDateTime; + +@Entity +@Getter @Setter +@Table(name = "violation_notions") +@NoArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class ViolationNotion { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @JsonIgnore + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "file_id", nullable = false) + @JsonIgnore + private FileEntity file; + + @CreatedDate + @Column(name = "created_at", updatable = false, nullable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @Column(name = "message") + private String message; +} diff --git a/src/main/java/ru/soune/nocopy/handler/ViolationNotionHandler.java b/src/main/java/ru/soune/nocopy/handler/ViolationNotionHandler.java new file mode 100644 index 0000000..2c06a01 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/ViolationNotionHandler.java @@ -0,0 +1,265 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.violation.*; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.repository.AuthTokenRepository; +import ru.soune.nocopy.service.violation.ViolationNotionService; + +import java.util.Optional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ViolationNotionHandler implements RequestHandler { + private final ObjectMapper objectMapper; + + private final ViolationNotionService notionService; + + private final AuthTokenRepository authTokenRepository; + + @Override + public BaseResponse handle(BaseRequest request) { + try { + ViolationNotionRequest notionRequest = objectMapper.convertValue( + request.getMessageBody(), ViolationNotionRequest.class); + + if (notionRequest.getToken() == null) { + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_TOKEN.getCode(), + "Token is required", + null); + } + + Optional tokenOptional = authTokenRepository.findByToken(notionRequest.getToken()); + if (tokenOptional.isEmpty()) { + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_TOKEN.getCode(), + "Invalid token", + null); + } + + Long userId = tokenOptional.get().getUser().getId(); + String action = notionRequest.getAction(); + + if (action == null || action.trim().isEmpty()) { + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_ACTION.getCode(), + "Action is required", + null); + } + + switch (action) { + case "get_notions": + return handleGetNotions(request, notionRequest, userId); + + case "get_notion": + return handleGetNotion(request, notionRequest, userId); + + case "add_notion": + return handleAddNotion(request, notionRequest, userId); + + case "update_notion": + return handleUpdateNotion(request, notionRequest, userId); + + case "delete_notion": + return handleDeleteNotion(request, notionRequest, userId); + + default: + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_ACTION.getCode(), + "Invalid action: " + action, + null); + } + + } catch (IllegalArgumentException e) { + log.error("Validation error in ViolationNotionHandler: {}", e.getMessage()); + return new BaseResponse( + request.getMsgId(), + MessageCode.VALIDATION_ERROR.getCode(), + e.getMessage(), + null); + } catch (SecurityException e) { + log.error("Security error in ViolationNotionHandler: {}", e.getMessage()); + return new BaseResponse( + request.getMsgId(), + MessageCode.ACCESS_DENIED.getCode(), + e.getMessage(), + null); + } catch (Exception e) { + log.error("Unexpected error in ViolationNotionHandler", e); + return new BaseResponse( + request.getMsgId(), + MessageCode.INTERNAL_ERROR.getCode(), + "Internal server error", + null); + } + } + + private BaseResponse handleGetNotions(BaseRequest request, ViolationNotionRequest notionRequest, Long userId) { + if (notionRequest.getFileId() == null) { + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_JSON_BODY.getCode(), + "FileId is required for get_notions action", + null); + } + + try { + ViolationNotionsListResponse notions = notionService.getNotionsByFileId( + notionRequest.getFileId(), userId); + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(notions) + .build(); + } catch (IllegalArgumentException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.FILE_NOT_FOUND.getCode(), + e.getMessage(), + null); + } + } + + private BaseResponse handleGetNotion(BaseRequest request, ViolationNotionRequest notionRequest, Long userId) { + if (notionRequest.getNotionId() == null) { + return new BaseResponse( + request.getMsgId(), + MessageCode.NOTION_NOT_FOUND.getCode(), + "NotionId is required for get_notion action", + null); + } + + try { + ViolationNotionResponse notion = notionService.getNotionById( + notionRequest.getNotionId(), userId); + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(notion) + .build(); + } catch (IllegalArgumentException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.NOTION_NOT_FOUND.getCode(), + e.getMessage(), + null); + } + } + + private BaseResponse handleAddNotion(BaseRequest request, ViolationNotionRequest notionRequest, Long userId) { + if (notionRequest.getFileId() == null) { + return new BaseResponse( + request.getMsgId(), + MessageCode.FILE_NOT_FOUND.getCode(), + "FileId is required for add_notion action", + null); + } + + if (notionRequest.getMessage() == null || notionRequest.getMessage().trim().isEmpty()) { + return new BaseResponse( + request.getMsgId(), + MessageCode.MESSAGE_IS_REQUIRED_FOR_NOTION.getCode(), + "Message is required for add_notion action", + null); + } + + try { + ViolationNotionResponse created = notionService.createNotion( + notionRequest.getFileId(), userId, notionRequest.getMessage()); + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(created) + .build(); + } catch (IllegalArgumentException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.NOTION_NOT_FOUND.getCode(), + e.getMessage(), + null); + } + } + + private BaseResponse handleUpdateNotion(BaseRequest request, ViolationNotionRequest notionRequest, Long userId) { + if (notionRequest.getNotionId() == null) { + return new BaseResponse( + request.getMsgId(), + MessageCode.NOTION_NOT_FOUND.getCode(), + "NotionId is required for update_notion action", + null); + } + + if (notionRequest.getMessage() == null || notionRequest.getMessage().trim().isEmpty()) { + return new BaseResponse( + request.getMsgId(), + MessageCode.MESSAGE_IS_REQUIRED_FOR_NOTION.getCode(), + "Message is required for update_notion action", + null); + } + + try { + ViolationNotionResponse updated = notionService.updateNotion( + notionRequest.getNotionId(), userId, notionRequest.getMessage()); + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(updated) + .build(); + } catch (IllegalArgumentException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.NOTION_NOT_FOUND.getCode(), + e.getMessage(), + null); + } catch (SecurityException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.ACCESS_DENIED.getCode(), + e.getMessage(), + null); + } + } + + private BaseResponse handleDeleteNotion(BaseRequest request, ViolationNotionRequest notionRequest, Long userId) { + if (notionRequest.getNotionId() == null) { + return new BaseResponse( + request.getMsgId(), + MessageCode.NOTION_NOT_FOUND.getCode(), + "NotionId is required for delete_notion action", + null); + } + + try { + notionService.deleteNotion(notionRequest.getNotionId(), userId); + DeleteNotionResponse deleteResponse = DeleteNotionResponse.builder() + .notionId(notionRequest.getNotionId()) + .message("Notion deleted successfully") + .deleted(true) + .build(); + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(deleteResponse) + .build(); + } catch (IllegalArgumentException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.NOTION_NOT_FOUND.getCode(), + e.getMessage(), + null); + } catch (SecurityException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.ACCESS_DENIED.getCode(), + e.getMessage(), + MessageCode.ACCESS_DENIED.getDescription()); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/repository/ViolationNotionRepository.java b/src/main/java/ru/soune/nocopy/repository/ViolationNotionRepository.java new file mode 100644 index 0000000..129321d --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/ViolationNotionRepository.java @@ -0,0 +1,24 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.violation.ViolationNotion; + +import java.util.List; + +@Repository +public interface ViolationNotionRepository extends JpaRepository { + + @Query("SELECT vn FROM ViolationNotion vn " + + "JOIN FETCH vn.user " + + "WHERE vn.file.id = :fileId " + + "ORDER BY vn.createdAt DESC") + List findByFileId(@Param("fileId") String fileId); + + @Query("SELECT COUNT(vn) FROM ViolationNotion vn WHERE vn.file.id = :fileId") + long countByFileId(@Param("fileId") String fileId); + + void deleteByFileId(String fileId); +} diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java new file mode 100644 index 0000000..e0e648b --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java @@ -0,0 +1,138 @@ +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.company.Company; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.entity.violation.ViolationNotion; +import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.repository.ViolationNotionRepository; + +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 FileEntityRepository fileRepository; + + @Transactional(readOnly = true) + public ViolationNotionsListResponse getNotionsByFileId(String fileId, Long userId) { + FileEntity file = fileRepository.findById(fileId) + .orElseThrow(() -> new IllegalArgumentException("File not found: " + fileId)); + checkPermission(userId, file); + + List notions = notionRepository.findByFileId(fileId); + long totalCount = notionRepository.countByFileId(fileId); + + List notionResponses = notions.stream() + .map(this::mapToResponse) + .collect(Collectors.toList()); + + return ViolationNotionsListResponse.builder() + .fileId(fileId) + .fileName(file.getOriginalFileName()) + .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.getFile().getUserId().equals(userId) && !notion.getUser().getId().equals(userId)) { + throw new SecurityException("User does not have access to this notion"); + } + + return mapToResponse(notion); + } + + @Transactional + public ViolationNotionResponse createNotion(String fileId, Long userId, String message) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("User not found: " + userId)); + + FileEntity file = fileRepository.findById(fileId) + .orElseThrow(() -> new IllegalArgumentException("File not found: " + fileId)); + + ViolationNotion notion = new ViolationNotion(); + notion.setUser(user); + notion.setFile(file); + notion.setMessage(message); + + ViolationNotion saved = notionRepository.save(notion); + + return mapToResponse(saved); + } + + @Transactional + public void deleteNotion(Long notionId, Long userId) { + ViolationNotion notion = notionRepository.findById(notionId) + .orElseThrow(() -> new IllegalArgumentException("Notion not found: " + notionId)); + + checkPermission(userId, notion.getFile()); + + notionRepository.delete(notion); + } + + @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 with id: {} by user: {}", notionId, userId); + + return mapToResponse(updated); + } + + private ViolationNotionResponse mapToResponse(ViolationNotion notion) { + return ViolationNotionResponse.builder() + .id(notion.getId()) + .message(notion.getMessage()) + .createdAt(notion.getCreatedAt()) + .user(ViolationNotionResponse.UserInfo.builder() + .id(notion.getUser().getId()) + .fullName(notion.getUser().getFullName()) + .email(notion.getUser().getEmail()) + .build()) + .build(); + } + + private void checkPermission(long userId, FileEntity file) { + User user = userRepository.findById(userId).orElseThrow(() -> + new IllegalArgumentException("User not found: " + userId)); + Company company = user.getCompany(); + + if (company != null) { + List companyUsers = company.getUsers() + .stream() + .map(User::getId) + .toList(); + if (companyUsers.contains(userId)) { + throw new SecurityException("User does not have access to this file"); + } + } else if (!file.getUserId().equals(userId)) { + throw new SecurityException("User does not have access to this file"); + } + } +} From 2d8f904bfd76ddf5691777c5bea1389aa4016dfa Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Mar 2026 16:52:35 +0700 Subject: [PATCH 199/274] dev add violation notion --- .../soune/nocopy/dto/violation/ViolationNotionResponse.java | 2 ++ .../java/ru/soune/nocopy/handler/ViolationNotionHandler.java | 5 +++++ .../nocopy/service/violation/ViolationNotionService.java | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionResponse.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionResponse.java index 17da29a..2afaca6 100644 --- a/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionResponse.java @@ -1,4 +1,5 @@ package ru.soune.nocopy.dto.violation; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Builder; import lombok.Data; import java.time.LocalDateTime; @@ -11,6 +12,7 @@ public class ViolationNotionResponse { private LocalDateTime createdAt; private LocalDateTime updatedAt; private UserInfo user; + @JsonIgnore private FileInfo file; @Data diff --git a/src/main/java/ru/soune/nocopy/handler/ViolationNotionHandler.java b/src/main/java/ru/soune/nocopy/handler/ViolationNotionHandler.java index 2c06a01..c6d1f5d 100644 --- a/src/main/java/ru/soune/nocopy/handler/ViolationNotionHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ViolationNotionHandler.java @@ -120,6 +120,7 @@ public class ViolationNotionHandler implements RequestHandler { notionRequest.getFileId(), userId); return BaseResponse.builder() .msgId(request.getMsgId()) + .messageCode(MessageCode.SUCCESS.getCode()) .messageBody(notions) .build(); } catch (IllegalArgumentException e) { @@ -145,6 +146,7 @@ public class ViolationNotionHandler implements RequestHandler { notionRequest.getNotionId(), userId); return BaseResponse.builder() .msgId(request.getMsgId()) + .messageCode(MessageCode.SUCCESS.getCode()) .messageBody(notion) .build(); } catch (IllegalArgumentException e) { @@ -178,6 +180,7 @@ public class ViolationNotionHandler implements RequestHandler { notionRequest.getFileId(), userId, notionRequest.getMessage()); return BaseResponse.builder() .msgId(request.getMsgId()) + .messageCode(MessageCode.SUCCESS.getCode()) .messageBody(created) .build(); } catch (IllegalArgumentException e) { @@ -211,6 +214,7 @@ public class ViolationNotionHandler implements RequestHandler { notionRequest.getNotionId(), userId, notionRequest.getMessage()); return BaseResponse.builder() .msgId(request.getMsgId()) + .messageCode(MessageCode.SUCCESS.getCode()) .messageBody(updated) .build(); } catch (IllegalArgumentException e) { @@ -246,6 +250,7 @@ public class ViolationNotionHandler implements RequestHandler { .build(); return BaseResponse.builder() .msgId(request.getMsgId()) + .messageCode(MessageCode.SUCCESS.getCode()) .messageBody(deleteResponse) .build(); } catch (IllegalArgumentException e) { diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java index e0e648b..d608386 100644 --- a/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java @@ -128,7 +128,7 @@ public class ViolationNotionService { .stream() .map(User::getId) .toList(); - if (companyUsers.contains(userId)) { + if (!companyUsers.contains(userId)) { throw new SecurityException("User does not have access to this file"); } } else if (!file.getUserId().equals(userId)) { From 921d439e0a576209f843e6c72ffdc5f3f88442d6 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Mar 2026 22:23:19 +0700 Subject: [PATCH 200/274] dev add violation notion --- .../dto/violation/ViolationNotionRequest.java | 4 +- .../violation/ViolationNotionResponse.java | 11 ++- .../ViolationNotionsListResponse.java | 5 +- .../entity/violation/ViolationNotion.java | 5 ++ .../handler/ViolationNotionHandler.java | 46 ++++------ .../repository/ViolationNotionRepository.java | 10 +-- .../violation/ViolationNotionService.java | 85 ++++++++----------- 7 files changed, 74 insertions(+), 92 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionRequest.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionRequest.java index fca54be..e431730 100644 --- a/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionRequest.java @@ -5,8 +5,8 @@ import lombok.Data; @Data public class ViolationNotionRequest { private String token; - private String fileId; - private String message; private String action; + private Long violationId; private Long notionId; + private String message; } diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionResponse.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionResponse.java index 2afaca6..eea7e86 100644 --- a/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionResponse.java @@ -1,5 +1,4 @@ package ru.soune.nocopy.dto.violation; -import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Builder; import lombok.Data; import java.time.LocalDateTime; @@ -12,8 +11,7 @@ public class ViolationNotionResponse { private LocalDateTime createdAt; private LocalDateTime updatedAt; private UserInfo user; - @JsonIgnore - private FileInfo file; + private ViolationInfo violation; @Data @Builder @@ -25,8 +23,9 @@ public class ViolationNotionResponse { @Data @Builder - public static class FileInfo { - private String id; - private String originalFileName; + public static class ViolationInfo { + private Long id; + private String url; + private String host; } } diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionsListResponse.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionsListResponse.java index 884bb32..eead976 100644 --- a/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionsListResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionsListResponse.java @@ -8,8 +8,9 @@ import java.util.List; @Data @Builder public class ViolationNotionsListResponse { - private String fileId; - private String fileName; + private Long violationId; + private String url; + private String host; private int totalCount; private List notions; } diff --git a/src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java b/src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java index aa93cbc..4ea82c0 100644 --- a/src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java +++ b/src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java @@ -34,6 +34,11 @@ public class ViolationNotion { @JsonIgnore private FileEntity file; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "violation_id", nullable = false) + @JsonIgnore + private Violation violation; + @CreatedDate @Column(name = "created_at", updatable = false, nullable = false) private LocalDateTime createdAt; diff --git a/src/main/java/ru/soune/nocopy/handler/ViolationNotionHandler.java b/src/main/java/ru/soune/nocopy/handler/ViolationNotionHandler.java index c6d1f5d..9c0a8f9 100644 --- a/src/main/java/ru/soune/nocopy/handler/ViolationNotionHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ViolationNotionHandler.java @@ -61,19 +61,14 @@ public class ViolationNotionHandler implements RequestHandler { switch (action) { case "get_notions": return handleGetNotions(request, notionRequest, userId); - case "get_notion": return handleGetNotion(request, notionRequest, userId); - case "add_notion": return handleAddNotion(request, notionRequest, userId); - case "update_notion": return handleUpdateNotion(request, notionRequest, userId); - case "delete_notion": return handleDeleteNotion(request, notionRequest, userId); - default: return new BaseResponse( request.getMsgId(), @@ -83,21 +78,21 @@ public class ViolationNotionHandler implements RequestHandler { } } catch (IllegalArgumentException e) { - log.error("Validation error in ViolationNotionHandler: {}", e.getMessage()); + log.error("Validation error: {}", e.getMessage()); return new BaseResponse( request.getMsgId(), MessageCode.VALIDATION_ERROR.getCode(), e.getMessage(), null); } catch (SecurityException e) { - log.error("Security error in ViolationNotionHandler: {}", e.getMessage()); + log.error("Security error: {}", e.getMessage()); return new BaseResponse( request.getMsgId(), MessageCode.ACCESS_DENIED.getCode(), e.getMessage(), null); } catch (Exception e) { - log.error("Unexpected error in ViolationNotionHandler", e); + log.error("Unexpected error", e); return new BaseResponse( request.getMsgId(), MessageCode.INTERNAL_ERROR.getCode(), @@ -107,26 +102,25 @@ public class ViolationNotionHandler implements RequestHandler { } private BaseResponse handleGetNotions(BaseRequest request, ViolationNotionRequest notionRequest, Long userId) { - if (notionRequest.getFileId() == null) { + if (notionRequest.getViolationId() == null) { return new BaseResponse( request.getMsgId(), MessageCode.INVALID_JSON_BODY.getCode(), - "FileId is required for get_notions action", + "ViolationId is required for get_notions action", null); } try { - ViolationNotionsListResponse notions = notionService.getNotionsByFileId( - notionRequest.getFileId(), userId); + ViolationNotionsListResponse notions = notionService.getNotionsByViolationId( + notionRequest.getViolationId(), userId); return BaseResponse.builder() .msgId(request.getMsgId()) - .messageCode(MessageCode.SUCCESS.getCode()) .messageBody(notions) .build(); } catch (IllegalArgumentException e) { return new BaseResponse( request.getMsgId(), - MessageCode.FILE_NOT_FOUND.getCode(), + MessageCode.NOTION_NOT_FOUND.getCode(), e.getMessage(), null); } @@ -136,7 +130,7 @@ public class ViolationNotionHandler implements RequestHandler { if (notionRequest.getNotionId() == null) { return new BaseResponse( request.getMsgId(), - MessageCode.NOTION_NOT_FOUND.getCode(), + MessageCode.INVALID_JSON_BODY.getCode(), "NotionId is required for get_notion action", null); } @@ -146,7 +140,6 @@ public class ViolationNotionHandler implements RequestHandler { notionRequest.getNotionId(), userId); return BaseResponse.builder() .msgId(request.getMsgId()) - .messageCode(MessageCode.SUCCESS.getCode()) .messageBody(notion) .build(); } catch (IllegalArgumentException e) { @@ -159,28 +152,27 @@ public class ViolationNotionHandler implements RequestHandler { } private BaseResponse handleAddNotion(BaseRequest request, ViolationNotionRequest notionRequest, Long userId) { - if (notionRequest.getFileId() == null) { + if (notionRequest.getViolationId() == null) { return new BaseResponse( request.getMsgId(), - MessageCode.FILE_NOT_FOUND.getCode(), - "FileId is required for add_notion action", + MessageCode.INVALID_JSON_BODY.getCode(), + "ViolationId is required for add_notion action", null); } if (notionRequest.getMessage() == null || notionRequest.getMessage().trim().isEmpty()) { return new BaseResponse( request.getMsgId(), - MessageCode.MESSAGE_IS_REQUIRED_FOR_NOTION.getCode(), + MessageCode.INVALID_JSON_BODY.getCode(), "Message is required for add_notion action", null); } try { ViolationNotionResponse created = notionService.createNotion( - notionRequest.getFileId(), userId, notionRequest.getMessage()); + notionRequest.getViolationId(), userId, notionRequest.getMessage()); return BaseResponse.builder() .msgId(request.getMsgId()) - .messageCode(MessageCode.SUCCESS.getCode()) .messageBody(created) .build(); } catch (IllegalArgumentException e) { @@ -196,7 +188,7 @@ public class ViolationNotionHandler implements RequestHandler { if (notionRequest.getNotionId() == null) { return new BaseResponse( request.getMsgId(), - MessageCode.NOTION_NOT_FOUND.getCode(), + MessageCode.INVALID_JSON_BODY.getCode(), "NotionId is required for update_notion action", null); } @@ -204,7 +196,7 @@ public class ViolationNotionHandler implements RequestHandler { if (notionRequest.getMessage() == null || notionRequest.getMessage().trim().isEmpty()) { return new BaseResponse( request.getMsgId(), - MessageCode.MESSAGE_IS_REQUIRED_FOR_NOTION.getCode(), + MessageCode.INVALID_JSON_BODY.getCode(), "Message is required for update_notion action", null); } @@ -214,7 +206,6 @@ public class ViolationNotionHandler implements RequestHandler { notionRequest.getNotionId(), userId, notionRequest.getMessage()); return BaseResponse.builder() .msgId(request.getMsgId()) - .messageCode(MessageCode.SUCCESS.getCode()) .messageBody(updated) .build(); } catch (IllegalArgumentException e) { @@ -236,7 +227,7 @@ public class ViolationNotionHandler implements RequestHandler { if (notionRequest.getNotionId() == null) { return new BaseResponse( request.getMsgId(), - MessageCode.NOTION_NOT_FOUND.getCode(), + MessageCode.INVALID_JSON_BODY.getCode(), "NotionId is required for delete_notion action", null); } @@ -250,7 +241,6 @@ public class ViolationNotionHandler implements RequestHandler { .build(); return BaseResponse.builder() .msgId(request.getMsgId()) - .messageCode(MessageCode.SUCCESS.getCode()) .messageBody(deleteResponse) .build(); } catch (IllegalArgumentException e) { @@ -264,7 +254,7 @@ public class ViolationNotionHandler implements RequestHandler { request.getMsgId(), MessageCode.ACCESS_DENIED.getCode(), e.getMessage(), - MessageCode.ACCESS_DENIED.getDescription()); + null); } } } diff --git a/src/main/java/ru/soune/nocopy/repository/ViolationNotionRepository.java b/src/main/java/ru/soune/nocopy/repository/ViolationNotionRepository.java index 129321d..ce6b74b 100644 --- a/src/main/java/ru/soune/nocopy/repository/ViolationNotionRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ViolationNotionRepository.java @@ -10,15 +10,13 @@ import java.util.List; @Repository public interface ViolationNotionRepository extends JpaRepository { - @Query("SELECT vn FROM ViolationNotion vn " + "JOIN FETCH vn.user " + - "WHERE vn.file.id = :fileId " + + "WHERE vn.violation.id = :violationId " + "ORDER BY vn.createdAt DESC") - List findByFileId(@Param("fileId") String fileId); + List findByViolationId(@Param("violationId") Long violationId); - @Query("SELECT COUNT(vn) FROM ViolationNotion vn WHERE vn.file.id = :fileId") - long countByFileId(@Param("fileId") String fileId); + @Query("SELECT COUNT(vn) FROM ViolationNotion vn WHERE vn.violation.id = :violationId") + long countByViolationId(@Param("violationId") Long violationId); - void deleteByFileId(String fileId); } diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java index d608386..5be8b6a 100644 --- a/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java @@ -6,13 +6,12 @@ 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.company.Company; -import ru.soune.nocopy.entity.file.FileEntity; 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.FileEntityRepository; 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; @@ -23,27 +22,25 @@ import java.util.stream.Collectors; public class ViolationNotionService { private final ViolationNotionRepository notionRepository; - private final UserRepository userRepository; - - private final FileEntityRepository fileRepository; + private final ViolationRepository violationRepository; @Transactional(readOnly = true) - public ViolationNotionsListResponse getNotionsByFileId(String fileId, Long userId) { - FileEntity file = fileRepository.findById(fileId) - .orElseThrow(() -> new IllegalArgumentException("File not found: " + fileId)); - checkPermission(userId, file); + public ViolationNotionsListResponse getNotionsByViolationId(Long violationId, Long userId) { + Violation violation = violationRepository.findById(violationId) + .orElseThrow(() -> new IllegalArgumentException("Violation not found: " + violationId)); - List notions = notionRepository.findByFileId(fileId); - long totalCount = notionRepository.countByFileId(fileId); + List notions = notionRepository.findByViolationId(violationId); + long totalCount = notionRepository.countByViolationId(violationId); List notionResponses = notions.stream() .map(this::mapToResponse) .collect(Collectors.toList()); return ViolationNotionsListResponse.builder() - .fileId(fileId) - .fileName(file.getOriginalFileName()) + .violationId(violationId) + .url(violation.getUrl()) + .host(violation.getHost()) .totalCount((int) totalCount) .notions(notionResponses) .build(); @@ -54,7 +51,7 @@ public class ViolationNotionService { ViolationNotion notion = notionRepository.findById(notionId) .orElseThrow(() -> new IllegalArgumentException("Notion not found: " + notionId)); - if (!notion.getFile().getUserId().equals(userId) && !notion.getUser().getId().equals(userId)) { + if (!notion.getUser().getId().equals(userId)) { throw new SecurityException("User does not have access to this notion"); } @@ -62,33 +59,24 @@ public class ViolationNotionService { } @Transactional - public ViolationNotionResponse createNotion(String fileId, Long userId, String message) { + public ViolationNotionResponse createNotion(Long violationId, Long userId, String message) { User user = userRepository.findById(userId) .orElseThrow(() -> new IllegalArgumentException("User not found: " + userId)); - FileEntity file = fileRepository.findById(fileId) - .orElseThrow(() -> new IllegalArgumentException("File not found: " + fileId)); + Violation violation = violationRepository.findById(violationId) + .orElseThrow(() -> new IllegalArgumentException("Violation not found: " + violationId)); ViolationNotion notion = new ViolationNotion(); notion.setUser(user); - notion.setFile(file); + 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 void deleteNotion(Long notionId, Long userId) { - ViolationNotion notion = notionRepository.findById(notionId) - .orElseThrow(() -> new IllegalArgumentException("Notion not found: " + notionId)); - - checkPermission(userId, notion.getFile()); - - notionRepository.delete(notion); - } - @Transactional public ViolationNotionResponse updateNotion(Long notionId, Long userId, String message) { ViolationNotion notion = notionRepository.findById(notionId) @@ -100,39 +88,40 @@ public class ViolationNotionService { notion.setMessage(message); ViolationNotion updated = notionRepository.save(notion); - log.info("Updated notion with id: {} by user: {}", notionId, userId); + 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().getUrl()) + .host(notion.getViolation().getHost()) + .build()) .build(); } - - private void checkPermission(long userId, FileEntity file) { - User user = userRepository.findById(userId).orElseThrow(() -> - new IllegalArgumentException("User not found: " + userId)); - Company company = user.getCompany(); - - if (company != null) { - List companyUsers = company.getUsers() - .stream() - .map(User::getId) - .toList(); - if (!companyUsers.contains(userId)) { - throw new SecurityException("User does not have access to this file"); - } - } else if (!file.getUserId().equals(userId)) { - throw new SecurityException("User does not have access to this file"); - } - } } From 7b58e4979d3a77c25e454bf4d265a21403f20648 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Mar 2026 22:40:06 +0700 Subject: [PATCH 201/274] dev add violation notion --- .../ru/soune/nocopy/entity/violation/ViolationNotion.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java b/src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java index 4ea82c0..0c361bc 100644 --- a/src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java +++ b/src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java @@ -8,7 +8,6 @@ import lombok.Setter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.user.User; import java.time.LocalDateTime; @@ -29,11 +28,6 @@ public class ViolationNotion { @JsonIgnore private User user; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "file_id", nullable = false) - @JsonIgnore - private FileEntity file; - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "violation_id", nullable = false) @JsonIgnore From 41bc26ee94ac229f0bf6de7da2278b6ee757054a Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Mar 2026 22:48:27 +0700 Subject: [PATCH 202/274] dev add violation notion --- .../soune/nocopy/service/violation/ViolationNotionService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java index 5be8b6a..235e1ce 100644 --- a/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java @@ -119,7 +119,7 @@ public class ViolationNotionService { .build()) .violation(ViolationNotionResponse.ViolationInfo.builder() .id(notion.getViolation().getId()) - .url(notion.getViolation().getUrl()) + .url(notion.getViolation().getPageUrl()) .host(notion.getViolation().getHost()) .build()) .build(); From 9f3430adf2c2129aef69e2467f6089ee27ff1c7f Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 18 Mar 2026 15:27:43 +0700 Subject: [PATCH 203/274] dev add violation notion --- .../soune/nocopy/client/YooKassaClient.java | 69 +++++++ .../nocopy/controller/PaymentController.java | 36 ++++ .../nocopy/dto/payment/PaymentMethodDTO.java | 16 ++ .../nocopy/entity/payment/PaymentMethod.java | 45 +++++ .../soune/nocopy/entity/tarif/TariffInfo.java | 6 + .../repository/PaymentMethodRepository.java | 12 ++ .../repository/TariffInfoRepository.java | 7 + .../nocopy/repository/UserRepository.java | 2 + .../nocopy/scheduler/PaymentScheduler.java | 70 ++++++++ .../nocopy/service/mail/EmailService.java | 34 +++- .../service/payment/PaymentService.java | 94 +++++++++- src/main/resources/application.yaml | 5 + .../templates/auto-renewal-success.html | 169 +++++++++++++++++ .../resources/templates/failed-template.html | 170 ++++++++++++++++++ 14 files changed, 726 insertions(+), 9 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/client/YooKassaClient.java create mode 100644 src/main/java/ru/soune/nocopy/dto/payment/PaymentMethodDTO.java create mode 100644 src/main/java/ru/soune/nocopy/entity/payment/PaymentMethod.java create mode 100644 src/main/java/ru/soune/nocopy/repository/PaymentMethodRepository.java create mode 100644 src/main/java/ru/soune/nocopy/scheduler/PaymentScheduler.java create mode 100644 src/main/resources/templates/auto-renewal-success.html create mode 100644 src/main/resources/templates/failed-template.html diff --git a/src/main/java/ru/soune/nocopy/client/YooKassaClient.java b/src/main/java/ru/soune/nocopy/client/YooKassaClient.java new file mode 100644 index 0000000..63307ce --- /dev/null +++ b/src/main/java/ru/soune/nocopy/client/YooKassaClient.java @@ -0,0 +1,69 @@ +package ru.soune.nocopy.client; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import com.fasterxml.jackson.databind.ObjectMapper; + +@Component +@Slf4j +public class YooKassaClient { + + @Value("${yookassa.shop-id}") + private String shopId; + + @Value("${yookassa.secret-key}") + private String secretKey; + + private final HttpClient httpClient = HttpClient.newHttpClient(); + + private final ObjectMapper mapper = new ObjectMapper(); + + public boolean createAutoPayment(String paymentMethodId, double amount, String description, String userId) { + try { + String url = "https://api.yookassa.ru/v3/payments"; + String auth = shopId + ":" + secretKey; + String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); + + Map body = new HashMap<>(); + body.put("amount", Map.of("value", String.format("%.2f", amount), + "currency", "RUB")); + body.put("payment_method_id", paymentMethodId); + body.put("description", description); + body.put("capture", true); + body.put("metadata", Map.of("user_id", userId, "auto_payment", "true")); + + String requestBody = mapper.writeValueAsString(body); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .header("Authorization", "Basic " + encodedAuth) + .header("Idempotence-Key", UUID.randomUUID().toString()) + .POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200 || response.statusCode() == 201) { + log.info("Auto payment created: {}", response.body()); + return true; + } else { + log.error("Auto payment failed: {} - {}", response.statusCode(), response.body()); + return false; + } + } catch (Exception e) { + log.error("Error creating auto payment", e); + return false; + } + } +} diff --git a/src/main/java/ru/soune/nocopy/controller/PaymentController.java b/src/main/java/ru/soune/nocopy/controller/PaymentController.java index 97658da..11fd7e3 100644 --- a/src/main/java/ru/soune/nocopy/controller/PaymentController.java +++ b/src/main/java/ru/soune/nocopy/controller/PaymentController.java @@ -52,4 +52,40 @@ public class PaymentController { log.error("Error processing payment notification", e); } } + + @PostMapping("/auto-renewal/disable") + public ResponseEntity disableAutoRenewal(@RequestParam String email) { + try { + paymentService.disableAutoRenewal(email); + return ResponseEntity.ok(Map.of("message", "Auto-renewal disabled")); + } catch (UserNotFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(Map.of("error", e.getMessage())); + } + } + + @DeleteMapping("/methods/{paymentMethodId}") + public ResponseEntity deletePaymentMethod(@RequestParam String email, + @PathVariable String paymentMethodId) { + try { + paymentService.deletePaymentMethod(email, paymentMethodId); + return ResponseEntity.ok(Map.of("message", "Payment method deleted")); + } catch (UserNotFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(Map.of("error", e.getMessage())); + } catch (RuntimeException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of("error", e.getMessage())); + } + } + + @GetMapping("/methods") + public ResponseEntity getPaymentMethods(@RequestParam String email) { + try { + return ResponseEntity.ok(paymentService.getUserPaymentMethods(email)); + } catch (UserNotFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(Map.of("error", e.getMessage())); + } + } } diff --git a/src/main/java/ru/soune/nocopy/dto/payment/PaymentMethodDTO.java b/src/main/java/ru/soune/nocopy/dto/payment/PaymentMethodDTO.java new file mode 100644 index 0000000..9c25f1f --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payment/PaymentMethodDTO.java @@ -0,0 +1,16 @@ +package ru.soune.nocopy.dto.payment; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PaymentMethodDTO { + private String paymentMethodId; + private String lastFour; + private String cardType; + private String expiryMonth; + private String expiryYear; +} diff --git a/src/main/java/ru/soune/nocopy/entity/payment/PaymentMethod.java b/src/main/java/ru/soune/nocopy/entity/payment/PaymentMethod.java new file mode 100644 index 0000000..174d9a3 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payment/PaymentMethod.java @@ -0,0 +1,45 @@ +package ru.soune.nocopy.entity.payment; + + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.soune.nocopy.entity.user.User; +import java.time.LocalDateTime; + +@Entity +@Table(name = "payment_methods") +@Getter @Setter @NoArgsConstructor @AllArgsConstructor +public class PaymentMethod { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "payment_method_id", nullable = false, unique = true) + private String paymentMethodId; + + @Column(name = "last_four") + private String lastFour; + + @Column(name = "card_type") + private String cardType; + + @Column(name = "expiry_month") + private String expiryMonth; + + @Column(name = "expiry_year") + private String expiryYear; + + @Column(name = "active") + private boolean active = true; + + @Column(name = "created_at") + private LocalDateTime createdAt; +} diff --git a/src/main/java/ru/soune/nocopy/entity/tarif/TariffInfo.java b/src/main/java/ru/soune/nocopy/entity/tarif/TariffInfo.java index c8f647e..9c71022 100644 --- a/src/main/java/ru/soune/nocopy/entity/tarif/TariffInfo.java +++ b/src/main/java/ru/soune/nocopy/entity/tarif/TariffInfo.java @@ -37,4 +37,10 @@ public class TariffInfo { @ManyToOne @JoinColumn(name = "tariff_id") private Tariff tariff; + + @Column(name = "auto_renewal") + private boolean autoRenewal = false; + + @Column(name = "payment_method_id") + private String paymentMethodId; } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/repository/PaymentMethodRepository.java b/src/main/java/ru/soune/nocopy/repository/PaymentMethodRepository.java new file mode 100644 index 0000000..6f772c5 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/PaymentMethodRepository.java @@ -0,0 +1,12 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.soune.nocopy.entity.payment.PaymentMethod; +import ru.soune.nocopy.entity.user.User; +import java.util.List; +import java.util.Optional; + +public interface PaymentMethodRepository extends JpaRepository { + List findByUserAndActiveTrue(User user); + Optional findByPaymentMethodId(String paymentMethodId); +} diff --git a/src/main/java/ru/soune/nocopy/repository/TariffInfoRepository.java b/src/main/java/ru/soune/nocopy/repository/TariffInfoRepository.java index a74bd87..6982080 100644 --- a/src/main/java/ru/soune/nocopy/repository/TariffInfoRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/TariffInfoRepository.java @@ -1,9 +1,16 @@ package ru.soune.nocopy.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import ru.soune.nocopy.entity.tarif.TariffInfo; +import java.time.LocalDateTime; +import java.util.List; + @Repository public interface TariffInfoRepository extends JpaRepository { + @Query("SELECT t FROM TariffInfo t WHERE t.autoRenewal = true AND t.endTariff <= :date") + List findForAutoRenewal(@Param("date") LocalDateTime date); } diff --git a/src/main/java/ru/soune/nocopy/repository/UserRepository.java b/src/main/java/ru/soune/nocopy/repository/UserRepository.java index 88c3bce..8c7e31c 100644 --- a/src/main/java/ru/soune/nocopy/repository/UserRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/UserRepository.java @@ -2,6 +2,7 @@ package ru.soune.nocopy.repository; import jakarta.validation.constraints.Size; import org.springframework.data.jpa.repository.JpaRepository; +import ru.soune.nocopy.entity.tarif.TariffInfo; import ru.soune.nocopy.entity.user.User; import java.util.List; @@ -12,4 +13,5 @@ public interface UserRepository extends JpaRepository { boolean existsByPhone(@Size(min = 11, max = 14) String phone); List findByCompanyId(String companyId); long countByCompanyId(String companyId); + User findByPersonalTariffInfo(TariffInfo tariffInfo); } diff --git a/src/main/java/ru/soune/nocopy/scheduler/PaymentScheduler.java b/src/main/java/ru/soune/nocopy/scheduler/PaymentScheduler.java new file mode 100644 index 0000000..112aacb --- /dev/null +++ b/src/main/java/ru/soune/nocopy/scheduler/PaymentScheduler.java @@ -0,0 +1,70 @@ +package ru.soune.nocopy.scheduler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.client.YooKassaClient; +import ru.soune.nocopy.entity.tarif.Tariff; +import ru.soune.nocopy.entity.tarif.TariffInfo; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.repository.TariffInfoRepository; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.service.mail.EmailService; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class PaymentScheduler { + + private final TariffInfoRepository tariffInfoRepository; + + private final EmailService emailService; + + private final YooKassaClient yooKassaClient; + + private final UserRepository userRepository; + + @Scheduled(cron = "0 0 1 * * *") + @Transactional + public void processAutoRenewals() { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime renewalDate = now.plusDays(1); + List forRenewal = tariffInfoRepository.findForAutoRenewal(renewalDate); + + for (TariffInfo tariffInfo : forRenewal) { + try { + if (tariffInfo.getPaymentMethodId() == null) { + tariffInfo.setAutoRenewal(false); + tariffInfoRepository.save(tariffInfo); + continue; + } + + User user = userRepository.findByPersonalTariffInfo(tariffInfo); + boolean success = yooKassaClient.createAutoPayment( + tariffInfo.getPaymentMethodId(), + tariffInfo.getTariff().getPrice(), + "Auto-renewal " + tariffInfo.getTariff().getName(), + String.valueOf(user.getId())); + + Tariff tariff = tariffInfo.getTariff(); + if (success) { + tariffInfo.setStartTariff(now); + tariffInfo.setEndTariff(now.plusMonths(1)); + + tariffInfoRepository.save(tariffInfo); + + emailService.sendAutoRenewalSuccessEmail(user, tariff.getName(), tariff.getPrice(), renewalDate); + } else { + emailService.sendAutoRenewalFailedEmail(user, tariff.getName(), tariff.getPrice()); + } + } catch (Exception e) { + log.error("Error processing auto-renewal for tariffInfo: " + tariffInfo.getId(), e); + } + } + } +} diff --git a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java index c34212d..fabb47d 100644 --- a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java +++ b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java @@ -19,6 +19,8 @@ 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 @@ -45,9 +47,6 @@ public class EmailService { @Value("${server.baseurl}") private String baseUrl; - @Value("${server.front.port}") - private String frontPort; - 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"); @@ -76,6 +75,35 @@ public class EmailService { sendEmail("noreply@no-copy.ru", user.getEmail(), standartAdressFrom, htmlContent); } + public void sendAutoRenewalFailedEmail(User user, String tariffName, Double price) + throws MessagingException, IOException { + + Map 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 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 diff --git a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java index 81f4233..4c32ce4 100644 --- a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java +++ b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java @@ -5,7 +5,10 @@ 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; @@ -15,15 +18,13 @@ 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.PaymentRepository; -import ru.soune.nocopy.repository.TariffInfoRepository; -import ru.soune.nocopy.repository.TariffRepository; -import ru.soune.nocopy.repository.UserRepository; +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 @@ -41,6 +42,10 @@ public class PaymentService { 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); @@ -63,6 +68,72 @@ public class PaymentService { return paymentRepository.save(payment); } + private void savePaymentMethod(User user, Map paymentMethodData) { + if (paymentMethodData == null) return; + + String paymentMethodId = (String) paymentMethodData.get("id"); + if (paymentMethodRepository.findByPaymentMethodId(paymentMethodId).isPresent()) return; + + Map card = (Map) 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 getUserPaymentMethods(String email) { + User user = userRepository.findByEmail(email); + if (user == null) throw new UserNotFoundException(email); + + 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(String email, String paymentMethodId) { + User user = userRepository.findByEmail(email); + if (user == null) throw new UserNotFoundException(email); + + 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 disableAutoRenewal(String email) { + User user = userRepository.findByEmail(email); + if (user == null) throw new UserNotFoundException(email); + + TariffInfo tariffInfo = user.getActiveTariffInfo(); + if (tariffInfo != null) { + tariffInfo.setAutoRenewal(false); + tariffInfoRepository.save(tariffInfo); + } + } + @Transactional public void handlePaymentNotification(Map notification) { String eventType = (String) notification.get("event"); @@ -79,7 +150,16 @@ public class PaymentService { payment.setStatus(PaymentStatus.SUCCEEDED); payment.setCancellationReason(null); - activateTariffForUser(payment.getUser(), tariff); + + Map metadata = (Map) object.get("metadata"); + Map paymentMethod = (Map) 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)) { @@ -123,7 +203,7 @@ public class PaymentService { new PaymentNotFoundException("Payment not found")); } - private void activateTariffForUser(User user, Tariff tariff) { + private void activateTariffForUser(User user, Tariff tariff, boolean autoRenewal, String paymentMethodId) { String type = tariff.getTariffAccountType(); TariffInfo activeTariffInfo = user.getActiveTariffInfo(); @@ -140,6 +220,8 @@ public class PaymentService { tariffInfo.setTokens(tariff.getTokens()); tariffInfo.setBoughtTokens(activeTariffInfo.getBoughtTokens()); tariffInfo.setTariff(tariff); + activeTariffInfo.setAutoRenewal(autoRenewal); + tariffInfo.setPaymentMethodId(paymentMethodId); tariffInfoService.linkUserTariffInfo(user, tariffInfoService.addTariffInfo(tariffInfo)); } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 2aa392f..a0d6d5a 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -107,6 +107,11 @@ searchapi: api-key: ${SEARCHAPI_API_KEY:5jyYZC8jSaxhZTwjMUhwtAXi} reverse-image-url: "https://searchapi.io/api/v1/search" +yookassa: + secret-key: test_0Yns_0NHV5GJf6ypJ5HC4NSfnLO8SJkw-1PwrVWsDl4 + shop-id: 1276731 + + app: email: verification: diff --git a/src/main/resources/templates/auto-renewal-success.html b/src/main/resources/templates/auto-renewal-success.html new file mode 100644 index 0000000..959fa36 --- /dev/null +++ b/src/main/resources/templates/auto-renewal-success.html @@ -0,0 +1,169 @@ + + + + + + Подписка успешно продлена + + + +
+
+ +

Подписка успешно продлена!

+
+ +
+

+ Здравствуйте, {{username}}! +

+ +

Ваша подписка была автоматически продлена. Спасибо, что остаётесь с нами!

+ +
+
+ Тариф: + {{tariff_name}} +
+
+ Сумма списания: + {{price}} ₽ +
+
+ Дата продления: + {{renewal_date}} +
+
+ Следующее списание: + {{next_payment_date}} +
+
+ +
+ 💡 Детали подписки: +
    +
  • Доступ к сервису активен до {{next_payment_date}}
  • +
  • Вы всегда можете управлять подпиской в личном кабинете
  • +
+
+ +

+ Если у вас есть вопросы, наша служба поддержки всегда готова помочь. +

+
+ + +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/failed-template.html b/src/main/resources/templates/failed-template.html new file mode 100644 index 0000000..33c671f --- /dev/null +++ b/src/main/resources/templates/failed-template.html @@ -0,0 +1,170 @@ + + + + + + Автопродление подписки не удалось + + + +
+
+ +

Автопродление не удалось

+
+ +
+

+ Здравствуйте, {{username}}! +

+ +

К сожалению, автоматическое продление подписки не удалось выполнить.

+ +
+
+ Тариф: + {{tariff_name}} +
+
+ Сумма: + {{price}} ₽ +
+
+ Дата попытки: + {{attempt_date}} +
+
+ +
+ Что делать? +
    +
  • Проверьте достаточно ли средств на карте
  • +
  • Убедитесь что карта не заблокирована
  • +
  • Если проблема сохраняется, обратитесь в поддержку
  • +
+
+ +

+ После успешной оплаты доступ к сервису будет восстановлен автоматически. +

+
+ + +
+ + \ No newline at end of file From 9e5ff7ce978814e987c649673c7625cec4345268 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 18 Mar 2026 16:08:37 +0700 Subject: [PATCH 204/274] dev add violation notion --- .../nocopy/controller/PaymentController.java | 39 ++++++++++++++++--- .../service/payment/PaymentService.java | 16 +++----- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/PaymentController.java b/src/main/java/ru/soune/nocopy/controller/PaymentController.java index 11fd7e3..308ef69 100644 --- a/src/main/java/ru/soune/nocopy/controller/PaymentController.java +++ b/src/main/java/ru/soune/nocopy/controller/PaymentController.java @@ -5,12 +5,15 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import ru.soune.nocopy.dto.search.GlobalSearchStartResponse; import ru.soune.nocopy.entity.payment.Payment; import ru.soune.nocopy.exception.PaymentNotFoundException; import ru.soune.nocopy.exception.TariffNotFoundException; import ru.soune.nocopy.exception.UserNotFoundException; import ru.soune.nocopy.service.payment.PaymentService; +import ru.soune.nocopy.service.register.AuthService; +import java.util.HashMap; import java.util.Map; @RestController @@ -21,6 +24,8 @@ public class PaymentController { private final PaymentService paymentService; + private final AuthService authService; + @PostMapping("/create") public ResponseEntity createPayment(@RequestParam String email, @RequestParam Long tariffId, @RequestParam String operationType, @RequestParam String operationUuid) { @@ -54,9 +59,16 @@ public class PaymentController { } @PostMapping("/auto-renewal/disable") - public ResponseEntity disableAutoRenewal(@RequestParam String email) { + public ResponseEntity disableAutoRenewal(@RequestHeader(value = "Authorization", required = false) String tokenHeader) { try { - paymentService.disableAutoRenewal(email); + if (tokenHeader == null || tokenHeader.isBlank()) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } + + paymentService.disableAutoRenewal(authService.useUserAuthToken(tokenHeader)); return ResponseEntity.ok(Map.of("message", "Auto-renewal disabled")); } catch (UserNotFoundException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND) @@ -65,10 +77,17 @@ public class PaymentController { } @DeleteMapping("/methods/{paymentMethodId}") - public ResponseEntity deletePaymentMethod(@RequestParam String email, + public ResponseEntity deletePaymentMethod(@RequestHeader(value = "Authorization", required = false) String tokenHeader, @PathVariable String paymentMethodId) { try { - paymentService.deletePaymentMethod(email, paymentMethodId); + if (tokenHeader == null || tokenHeader.isBlank()) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } + + paymentService.deletePaymentMethod(authService.useUserAuthToken(tokenHeader), paymentMethodId); return ResponseEntity.ok(Map.of("message", "Payment method deleted")); } catch (UserNotFoundException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND) @@ -80,9 +99,17 @@ public class PaymentController { } @GetMapping("/methods") - public ResponseEntity getPaymentMethods(@RequestParam String email) { + public ResponseEntity getPaymentMethods(@RequestHeader(value = "Authorization", required = false) String tokenHeader) { try { - return ResponseEntity.ok(paymentService.getUserPaymentMethods(email)); + if (tokenHeader == null || tokenHeader.isBlank()) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } + + return ResponseEntity.ok(paymentService.getUserPaymentMethods( + authService.useUserAuthToken(tokenHeader))); } catch (UserNotFoundException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(Map.of("error", e.getMessage())); diff --git a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java index 4c32ce4..92f9f7a 100644 --- a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java +++ b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java @@ -90,9 +90,8 @@ public class PaymentService { } @Transactional(readOnly = true) - public List getUserPaymentMethods(String email) { - User user = userRepository.findByEmail(email); - if (user == null) throw new UserNotFoundException(email); + public List getUserPaymentMethods(Long userId) { + User user = userRepository.findById(userId).orElseThrow(); return paymentMethodRepository.findByUserAndActiveTrue(user) .stream() @@ -107,10 +106,8 @@ public class PaymentService { } @Transactional - public void deletePaymentMethod(String email, String paymentMethodId) { - User user = userRepository.findByEmail(email); - if (user == null) throw new UserNotFoundException(email); - + 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")); @@ -123,9 +120,8 @@ public class PaymentService { } @Transactional - public void disableAutoRenewal(String email) { - User user = userRepository.findByEmail(email); - if (user == null) throw new UserNotFoundException(email); + public void disableAutoRenewal(Long userId) { + User user = userRepository.findById(userId).orElseThrow(); TariffInfo tariffInfo = user.getActiveTariffInfo(); if (tariffInfo != null) { From 9024d3aacad1bc25ef0a3e3d1587c982c1191c28 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 18 Mar 2026 16:26:47 +0700 Subject: [PATCH 205/274] dev add violation notion --- .../java/ru/soune/nocopy/controller/PaymentController.java | 7 ++++--- .../ru/soune/nocopy/service/payment/PaymentService.java | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/PaymentController.java b/src/main/java/ru/soune/nocopy/controller/PaymentController.java index 308ef69..47407b5 100644 --- a/src/main/java/ru/soune/nocopy/controller/PaymentController.java +++ b/src/main/java/ru/soune/nocopy/controller/PaymentController.java @@ -58,8 +58,9 @@ public class PaymentController { } } - @PostMapping("/auto-renewal/disable") - public ResponseEntity disableAutoRenewal(@RequestHeader(value = "Authorization", required = false) String tokenHeader) { + @PostMapping("/auto-renewal") + public ResponseEntity disableAutoRenewal(@RequestParam(value = "renewal") Boolean renewal, + @RequestHeader(value = "Authorization", required = false) String tokenHeader) { try { if (tokenHeader == null || tokenHeader.isBlank()) { Map errorData = new HashMap<>(); @@ -68,7 +69,7 @@ public class PaymentController { return ResponseEntity.ok().body(Map.of("error", errorData)); } - paymentService.disableAutoRenewal(authService.useUserAuthToken(tokenHeader)); + paymentService.changeAutoRenewal(authService.useUserAuthToken(tokenHeader), renewal); return ResponseEntity.ok(Map.of("message", "Auto-renewal disabled")); } catch (UserNotFoundException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND) diff --git a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java index 92f9f7a..96c9f74 100644 --- a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java +++ b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java @@ -120,12 +120,12 @@ public class PaymentService { } @Transactional - public void disableAutoRenewal(Long userId) { + public void changeAutoRenewal(Long userId, boolean autoRenewal) { User user = userRepository.findById(userId).orElseThrow(); TariffInfo tariffInfo = user.getActiveTariffInfo(); if (tariffInfo != null) { - tariffInfo.setAutoRenewal(false); + tariffInfo.setAutoRenewal(autoRenewal); tariffInfoRepository.save(tariffInfo); } } From 02b8d3ee6d275e12c2d191c93bd62e2e0151e56e Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 18 Mar 2026 17:07:44 +0700 Subject: [PATCH 206/274] dev add violation notion --- .../dto/violation/ViolationRequest.java | 3 ++ .../nocopy/handler/ViolationHandler.java | 45 +++++++++++-------- .../repository/ViolationRepository.java | 12 +++++ .../service/violation/ViolationService.java | 41 ++++++++++++++++- 4 files changed, 82 insertions(+), 19 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationRequest.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationRequest.java index 97cdbd2..5c7a2d5 100644 --- a/src/main/java/ru/soune/nocopy/dto/violation/ViolationRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationRequest.java @@ -33,4 +33,7 @@ public class ViolationRequest { @JsonProperty("action") private String action; + + @JsonProperty("token") + private String token; } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java b/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java index 8728e89..02fd0a6 100644 --- a/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java @@ -12,6 +12,8 @@ import ru.soune.nocopy.dto.violation.ViolationResponse; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.violation.Violation; import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.service.file.FileEntityService; +import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.violation.ViolationService; import java.io.FileNotFoundException; @@ -31,6 +33,10 @@ public class ViolationHandler implements RequestHandler { private final FileEntityRepository fileRepository; + private final AuthService authService; + + private final FileEntityService fileEntityService; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @Override @@ -38,9 +44,19 @@ public class ViolationHandler implements RequestHandler { ViolationRequest violationRequest = objectMapper.convertValue(request.getMessageBody(), ViolationRequest.class); validateRequest(violationRequest); - //TODO add response - FileEntity file = fileRepository.findById(violationRequest.getFileId()) - .orElseThrow(() -> new FileNotFoundException("File not found with id: " + violationRequest.getFileId())); + + String token = violationRequest.getToken(); + Long userId = authService.useUserAuthToken(token); + + List targetFiles; + + if (violationRequest.getFileId() != null && !violationRequest.getFileId().isEmpty()) { + FileEntity file = fileRepository.findById(violationRequest.getFileId()) + .orElseThrow(() -> new FileNotFoundException("File not found with id: " + violationRequest.getFileId())); + targetFiles = List.of(file); + } else { + targetFiles = fileEntityService.getAllUserFiles(userId); + } LocalDateTime startDate = null; LocalDateTime endDate = null; @@ -52,12 +68,11 @@ public class ViolationHandler implements RequestHandler { if ("group".equalsIgnoreCase(violationRequest.getAction())) { Map groupedData = violationService.getGroupedViolations( - file, + targetFiles, violationRequest.getGroupBy(), violationRequest.getStatus(), startDate, - endDate - ); + endDate); return BaseResponse.builder() .msgId(request.getMsgId()) @@ -67,18 +82,18 @@ public class ViolationHandler implements RequestHandler { Page violationPage; if (startDate != null && endDate != null) { - violationPage = violationService.getViolationsByFileAndDateRange( - file, startDate, endDate, + violationPage = violationService.getViolationsByFilesAndDateRange( + targetFiles, startDate, endDate, violationRequest.getPage(), violationRequest.getSize(), violationRequest.getSortDirection() ); } else if (violationRequest.getStatus() != null && !violationRequest.getStatus().isEmpty()) { - violationPage = violationService.getViolationsByFileAndStatus( - file, violationRequest.getStatus(), + violationPage = violationService.getViolationsByFilesAndStatus( + targetFiles, violationRequest.getStatus(), violationRequest.getPage(), violationRequest.getSize(), violationRequest.getSortDirection() ); } else { - violationPage = violationService.getViolationsByFile( - file, + violationPage = violationService.getViolationsByFiles( + targetFiles, violationRequest.getPage(), violationRequest.getSize(), violationRequest.getSortDirection() ); } @@ -105,12 +120,7 @@ public class ViolationHandler implements RequestHandler { } } - //TODO add response exception private void validateRequest(ViolationRequest request) { - if (request.getFileId() == null) { - return; - } - if (request.getPage() < 0) { request.setPage(0); } @@ -125,7 +135,6 @@ public class ViolationHandler implements RequestHandler { } } - //TODO add response exception private LocalDateTime parseDate(String dateStr) { try { return LocalDateTime.parse(dateStr, DATE_FORMATTER); diff --git a/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java b/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java index 942b31c..25d7baa 100644 --- a/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java @@ -41,6 +41,18 @@ public interface ViolationRepository extends JpaRepository { List findByFileEntityIn(List files); + Page findByFileEntityIn(List files, Pageable pageable); + + Page findByFileEntityInAndStatus(List files, String status, Pageable pageable); + + List findByFileEntityInAndStatus(List files, String status); + + Page findByFileEntityInAndCreatedDateBetween(List files, LocalDateTime startDate, + LocalDateTime endDate, Pageable pageable); + + List findByFileEntityInAndStatusAndCreatedDateBetween(List files, String status, + LocalDateTime startDate, LocalDateTime endDate); + List findByFileEntityInAndCreatedDateBetween(List files, LocalDateTime startDate, LocalDateTime endDate); diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java index faa80ff..da47a62 100644 --- a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java @@ -146,6 +146,46 @@ public class ViolationService { )); } + public Page getViolationsByFiles(List 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 getViolationsByFilesAndStatus(List 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 getViolationsByFilesAndDateRange(List 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 getGroupedViolations(List files, String groupBy, String status, + LocalDateTime startDate, LocalDateTime endDate) { + List 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); + } + + return violations.stream() + .collect(Collectors.groupingBy( + v -> extractGroupKey(v.getPageUrl(), groupBy), + Collectors.counting() + )); + } + public ViolationStatisticsResponse getViolationStatistics(Long userId, String fileId, LocalDateTime startDate, LocalDateTime endDate) { @@ -207,7 +247,6 @@ public class ViolationService { .build(); } - private String extractGroupKey(String url, String groupBy) { try { String domain = url.replaceAll("https?://(www\\.)?", "").split("/")[0]; From c7660c30de4d18332c265199c3d46e9f3d1926c6 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 18 Mar 2026 17:55:14 +0700 Subject: [PATCH 207/274] dev add violation notion --- .../nocopy/handler/ViolationHandler.java | 3 ++- .../repository/ViolationRepository.java | 12 +++++++++ .../service/violation/ViolationService.java | 26 +++++++++++++++++-- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java b/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java index 02fd0a6..9bff5a2 100644 --- a/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java @@ -72,7 +72,8 @@ public class ViolationHandler implements RequestHandler { violationRequest.getGroupBy(), violationRequest.getStatus(), startDate, - endDate); + endDate, + violationRequest.getSortDirection()); return BaseResponse.builder() .msgId(request.getMsgId()) diff --git a/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java b/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java index 25d7baa..0834914 100644 --- a/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java @@ -2,6 +2,7 @@ package ru.soune.nocopy.repository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -62,4 +63,15 @@ public interface ViolationRepository extends JpaRepository { @Query("SELECT COUNT(v) FROM Violation v WHERE v.globalSearchResult.taskId = :taskId") long countByGlobalSearchResultTaskId(@Param("taskId") String taskId); + + List findByFileEntityIn(List files, Sort sort); + + List findByFileEntityInAndStatus(List files, String status, Sort sort); + + List findByFileEntityInAndCreatedDateBetween(List files, LocalDateTime startDate, + LocalDateTime endDate, Sort sort); + + List findByFileEntityInAndStatusAndCreatedDateBetween(List files, String status, + LocalDateTime startDate, LocalDateTime endDate, + Sort sort); } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java index da47a62..c90b5c1 100644 --- a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java @@ -182,8 +182,30 @@ public class ViolationService { return violations.stream() .collect(Collectors.groupingBy( v -> extractGroupKey(v.getPageUrl(), groupBy), - Collectors.counting() - )); + Collectors.counting())); + } + + public Map getGroupedViolations(List files, String groupBy, String status, + LocalDateTime startDate, LocalDateTime endDate, + String sortDirection) { + List violations; + + Sort sort = Sort.by(sortDirection.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC, "createdDate"); + + if (status != null && !status.isEmpty() && startDate != null && endDate != null) { + violations = violationRepository.findByFileEntityInAndStatusAndCreatedDateBetween(files, status, startDate, endDate, sort); + } else if (status != null && !status.isEmpty()) { + violations = violationRepository.findByFileEntityInAndStatus(files, status, sort); + } else if (startDate != null && endDate != null) { + violations = violationRepository.findByFileEntityInAndCreatedDateBetween(files, startDate, endDate, sort); + } else { + violations = violationRepository.findByFileEntityIn(files, sort); + } + + return violations.stream() + .collect(Collectors.groupingBy( + v -> extractGroupKey(v.getPageUrl(), groupBy), + Collectors.counting())); } public ViolationStatisticsResponse getViolationStatistics(Long userId, String fileId, From f7c2a186cf7b8a498aa5528934be3079547df2df Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 18 Mar 2026 18:21:33 +0700 Subject: [PATCH 208/274] dev add violation notion --- .../service/violation/ViolationService.java | 50 ++++++++++++++++--- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java index c90b5c1..8133dcf 100644 --- a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java @@ -185,27 +185,63 @@ public class ViolationService { Collectors.counting())); } +// public Map getGroupedViolations(List files, String groupBy, String status, +// LocalDateTime startDate, LocalDateTime endDate, +// String sortDirection) { +// List violations; +// +// Sort sort = Sort.by(sortDirection.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC, +// "createdDate"); +// +// if (status != null && !status.isEmpty() && startDate != null && endDate != null) { +// violations = violationRepository.findByFileEntityInAndStatusAndCreatedDateBetween(files, status, startDate, endDate, sort); +// } else if (status != null && !status.isEmpty()) { +// violations = violationRepository.findByFileEntityInAndStatus(files, status, sort); +// } else if (startDate != null && endDate != null) { +// violations = violationRepository.findByFileEntityInAndCreatedDateBetween(files, startDate, endDate, sort); +// } else { +// violations = violationRepository.findByFileEntityIn(files, sort); +// } +// +// return violations.stream() +// .collect(Collectors.groupingBy( +// v -> extractGroupKey(v.getPageUrl(), groupBy), +// Collectors.counting())); +// } + public Map getGroupedViolations(List files, String groupBy, String status, LocalDateTime startDate, LocalDateTime endDate, String sortDirection) { List violations; - Sort sort = Sort.by(sortDirection.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC, "createdDate"); - if (status != null && !status.isEmpty() && startDate != null && endDate != null) { - violations = violationRepository.findByFileEntityInAndStatusAndCreatedDateBetween(files, status, startDate, endDate, sort); + violations = violationRepository.findByFileEntityInAndStatusAndCreatedDateBetween(files, status, startDate, endDate); } else if (status != null && !status.isEmpty()) { - violations = violationRepository.findByFileEntityInAndStatus(files, status, sort); + violations = violationRepository.findByFileEntityInAndStatus(files, status); } else if (startDate != null && endDate != null) { - violations = violationRepository.findByFileEntityInAndCreatedDateBetween(files, startDate, endDate, sort); + violations = violationRepository.findByFileEntityInAndCreatedDateBetween(files, startDate, endDate); } else { - violations = violationRepository.findByFileEntityIn(files, sort); + violations = violationRepository.findByFileEntityIn(files); } - return violations.stream() + Map 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, From 741ca5d5a2c0a74de96a352bc89f17ad70f5d42a Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 18 Mar 2026 19:05:07 +0700 Subject: [PATCH 209/274] dev add violation notion --- .../service/violation/ViolationService.java | 96 ------------------- 1 file changed, 96 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java index 8133dcf..6174d01 100644 --- a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java @@ -94,58 +94,6 @@ public class ViolationService { .collect(Collectors.toList()); } - public List violationsByFileId(FileEntity file) { - return violationRepository.findByFileEntity(file); - } - - public void changeStatusViolation(Long violationId, String status) { - Violation violation = violationRepository.findById(violationId) - .orElseThrow(() -> new ViolationNotFoundException("Violation not found with id: " + violationId)); - violation.setStatus(status); - violationRepository.save(violation); - } - - public Page getViolationsByFile(FileEntity file, 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.findByFileEntity(file, pageable); - } - - public Page getViolationsByFileAndStatus(FileEntity file, 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.findByFileEntityAndStatus(file, status, pageable); - } - - public Page getViolationsByFileAndDateRange(FileEntity file, 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.findByFileEntityAndCreatedDateBetween(file, startDate, endDate, pageable); - } - - public Map getGroupedViolations(FileEntity file, String groupBy, String status, - LocalDateTime startDate, LocalDateTime endDate) { - - List violations; - - if (status != null && !status.isEmpty() && startDate != null && endDate != null) { - violations = violationRepository.findByFileEntityAndStatusAndCreatedDateBetween(file, status, startDate, endDate); - } else if (status != null && !status.isEmpty()) { - violations = violationRepository.findByFileEntityAndStatus(file, status); - } else if (startDate != null && endDate != null) { - violations = violationRepository.findByFileEntityAndCreatedDateBetween(file, startDate, endDate); - } else { - violations = violationRepository.findByFileEntity(file); - } - - return violations.stream() - .collect(Collectors.groupingBy( - v -> extractGroupKey(v.getPageUrl(), groupBy), - Collectors.counting() - )); - } - public Page getViolationsByFiles(List 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); @@ -165,50 +113,6 @@ public class ViolationService { return violationRepository.findByFileEntityInAndCreatedDateBetween(files, startDate, endDate, pageable); } - public Map getGroupedViolations(List files, String groupBy, String status, - LocalDateTime startDate, LocalDateTime endDate) { - List 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); - } - - return violations.stream() - .collect(Collectors.groupingBy( - v -> extractGroupKey(v.getPageUrl(), groupBy), - Collectors.counting())); - } - -// public Map getGroupedViolations(List files, String groupBy, String status, -// LocalDateTime startDate, LocalDateTime endDate, -// String sortDirection) { -// List violations; -// -// Sort sort = Sort.by(sortDirection.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC, -// "createdDate"); -// -// if (status != null && !status.isEmpty() && startDate != null && endDate != null) { -// violations = violationRepository.findByFileEntityInAndStatusAndCreatedDateBetween(files, status, startDate, endDate, sort); -// } else if (status != null && !status.isEmpty()) { -// violations = violationRepository.findByFileEntityInAndStatus(files, status, sort); -// } else if (startDate != null && endDate != null) { -// violations = violationRepository.findByFileEntityInAndCreatedDateBetween(files, startDate, endDate, sort); -// } else { -// violations = violationRepository.findByFileEntityIn(files, sort); -// } -// -// return violations.stream() -// .collect(Collectors.groupingBy( -// v -> extractGroupKey(v.getPageUrl(), groupBy), -// Collectors.counting())); -// } - public Map getGroupedViolations(List files, String groupBy, String status, LocalDateTime startDate, LocalDateTime endDate, String sortDirection) { From 0dd8400229b4e8471860714f3c38b9601b6ea833 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 19 Mar 2026 13:40:34 +0700 Subject: [PATCH 210/274] dev add violation notion --- .../ru/soune/nocopy/service/search/SearchImageService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index cb203a0..c9ad65f 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -32,11 +32,11 @@ public class SearchImageService { private final OkHttpClient googleHttpClient; public SearchImageService() { -// Proxy proxy = new Proxy(Proxy.Type.HTTP, -// new InetSocketAddress("193.46.217.94", 3128)); + Proxy proxy = new Proxy(Proxy.Type.HTTP, + new InetSocketAddress("193.46.217.94", 3128)); this.yandexHttpClient = new OkHttpClient.Builder() -// .proxy(proxy) + .proxy(proxy) .connectTimeout(35, TimeUnit.SECONDS) .writeTimeout(35, TimeUnit.SECONDS) .readTimeout(35, TimeUnit.SECONDS) From 1cdc6ee1c90042b9ec79560cdeb71a16506f4880 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 19 Mar 2026 13:43:58 +0700 Subject: [PATCH 211/274] dev add violation notion --- .../java/ru/soune/nocopy/service/file/FileCleanupService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java b/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java index 06e91eb..18bf37c 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java @@ -34,7 +34,7 @@ public class FileCleanupService { public void cleanupExpiredFiles() { log.info("Starting cleanup of expired temporary files"); - cleanupOldPeriods(); +// cleanupOldPeriods(); Path tempDir = Paths.get(basePath, "temp"); if (!Files.exists(tempDir)) { From c5f060d269875e1521ef2b7abea5c3628ccb7eee Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 19 Mar 2026 14:48:07 +0700 Subject: [PATCH 212/274] dev add violation notion --- .../soune/nocopy/dto/file/SimilarFileDTO.java | 1 + .../nocopy/service/FileSimilarityService.java | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/dto/file/SimilarFileDTO.java b/src/main/java/ru/soune/nocopy/dto/file/SimilarFileDTO.java index 44d8495..971bbd5 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/SimilarFileDTO.java +++ b/src/main/java/ru/soune/nocopy/dto/file/SimilarFileDTO.java @@ -19,4 +19,5 @@ public class SimilarFileDTO { ProtectionStatus status; LocalDateTime uploadDate; String url; + Boolean owner; } diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 3b036ee..2a5ea95 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -155,19 +155,21 @@ public class FileSimilarityService { .orElseThrow(() -> new RuntimeException("Hash not found for file: " + fileId)); List candidates; - if (authToken == null || "all".equals(authToken)) { + List userIds; +// if (authToken == null || "all".equals(authToken)) { candidates = repository.findCandidates(fileId); - } else { +// } else { Long userId = authTokenRepository.findUserIdByToken(authToken); User user = userRepository.findById(userId).orElseThrow(() -> new RuntimeException("User not found")); Company company = user.getCompany(); if (company != null) { - Set userIds = company.getUsers().stream().map(User::getId).collect(Collectors.toSet()); - candidates = repository.findCandidatesFromUserFiles(fileId, userIds); + userIds = company.getUsers().stream().map(User::getId).collect(Collectors.toList()); +// candidates = repository.findCandidatesFromUserFiles(fileId, userIds); } else { - candidates = repository.findCandidatesFromUserFiles(fileId, userId); + userIds = List.of(userId); +// candidates = repository.findCandidatesFromUserFiles(fileId, userId); } - } +// } List levels = (similarityLevels != null && !similarityLevels.isEmpty()) ? similarityLevels @@ -176,7 +178,7 @@ public class FileSimilarityService { List allResults = candidates.stream() .map(c -> createSimilarFileResponse(c, imageHashEntity.getHash64Hi(), - imageHashEntity.getHash64Lo())) + imageHashEntity.getHash64Lo(), userIds)) .filter(dto -> levels.contains(dto.getSimilarityLevel())) .sorted(Comparator.comparingInt(SimilarFileDTO::getHammingDistance)) .collect(Collectors.toList()); @@ -211,7 +213,7 @@ public class FileSimilarityService { } private SimilarFileDTO createSimilarFileResponse(SimilarImageProjection similarImageProjection, - Long hash64Hi, Long hash64Lo) { + Long hash64Hi, Long hash64Lo, List userIds) { Long imageProjectionHash64Hi = similarImageProjection.getHash64Hi(); Long similarImageProjectionHash64Lo = similarImageProjection.getHash64Lo(); int hamming = PerceptualHashHelper.INSTANCE.hammingDistance(new PHash(hash64Hi, hash64Lo), @@ -238,6 +240,7 @@ public class FileSimilarityService { .supportId(similarImageProjection.getSupportId()) .uploadDate(similarImageProjection.getUploadDate()) .status(similarImageProjection.getProtectionStatus()) + .owner(userIds.contains(similarImageProjection.getUserId())) // .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId()) .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId() + "/thumbnail") .build(); From 795b2556133d1d0b5bcf8193628f1fbea85d5354 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 19 Mar 2026 18:03:44 +0700 Subject: [PATCH 213/274] dev add violation notion --- .../nocopy/dto/file/FileEntityRequest.java | 6 + .../nocopy/dto/file/FileListResponse.java | 6 + .../ru/soune/nocopy/dto/file/SortOrder.java | 25 ++ .../nocopy/handler/FileEntityHandler.java | 261 +++++++++++++++++- .../service/search/SearchImageService.java | 6 +- 5 files changed, 286 insertions(+), 18 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/file/SortOrder.java diff --git a/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java b/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java index 560fec3..50fadd5 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java @@ -34,4 +34,10 @@ public class FileEntityRequest { @JsonProperty("token") private String token; + + @JsonProperty("sort_by") + private String sortBy; + + @JsonProperty("sort_order") + private String sortOrder; } diff --git a/src/main/java/ru/soune/nocopy/dto/file/FileListResponse.java b/src/main/java/ru/soune/nocopy/dto/file/FileListResponse.java index bbc5a5f..d8c3a0d 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/FileListResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/file/FileListResponse.java @@ -30,4 +30,10 @@ public class FileListResponse { @JsonProperty("page_size") private Integer pageSize; + + @JsonProperty("sort_by") + private String sortBy; + + @JsonProperty("sort_order") + private String sortOrder; } diff --git a/src/main/java/ru/soune/nocopy/dto/file/SortOrder.java b/src/main/java/ru/soune/nocopy/dto/file/SortOrder.java new file mode 100644 index 0000000..81ba77f --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/file/SortOrder.java @@ -0,0 +1,25 @@ +package ru.soune.nocopy.dto.file; + +import lombok.Getter; + +@Getter +public enum SortOrder { + ASC("asc"), + DESC("desc"); + + private final String value; + + SortOrder(String value) { + this.value = value; + } + + public static SortOrder fromString(String value) { + if (value == null) return ASC; + for (SortOrder order : SortOrder.values()) { + if (order.value.equalsIgnoreCase(value)) { + return order; + } + } + return ASC; + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java index 3a1f56e..b6e9202 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java @@ -16,9 +16,8 @@ import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileStatsService; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; +import java.util.*; +import java.util.stream.Collectors; @Slf4j @Component @@ -183,53 +182,285 @@ public class FileEntityHandler implements RequestHandler { } } +// private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) { +// try { +// Long userId = authService.useUserAuthToken(fileRequest.getToken()); +// int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1; +// int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20; +// +// if (fileRequest.getQuery() == null || fileRequest.getQuery().isBlank()) { +// return handleGetUserFiles(request, fileRequest); +// } +// +// FileResponse allFiles = fileEntityService.getUserFiles(userId, 1, 1000, request.getVersion()); +// +// var filteredFiles = allFiles.getFiles().stream() +// .filter(f -> (f.getOriginalFileName() + f.getFileExtension()).toLowerCase() +// .contains(fileRequest.getQuery().toLowerCase())) +// .toList(); +// +// int start = (page - 1) * pageSize; +// int end = Math.min(start + pageSize, filteredFiles.size()); +// List fileEntityResponses = filteredFiles.subList(start, Math.min(end, filteredFiles.size())); +// long sumSize = fileEntityResponses.stream() +// .mapToLong(FileEntityResponse::getFileSize) +// .sum(); +// +// FileListResponse response = FileListResponse.builder() +// .files(fileEntityResponses) +// .totalCount(filteredFiles.size()) +// .totalSize(sumSize) +// .formattedTotalSize(fileEntityService.formatFileSize(sumSize)) +// .page(page) +// .pageSize(pageSize) +// .build(); +// +// return new BaseResponse(request.getMsgId(), +// MessageCode.SUCCESS.getCode(), +// MessageCode.SUCCESS.getDescription(), +// response); +// } catch (NotFoundAuthToken e) { +// return new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(), +// "Authentication required", null); +// } catch (Exception e) { +// log.error("Error searching files", e); +// return new BaseResponse(request.getMsgId(), MessageCode.FILE_UPLOAD_ERROR.getCode(), +// "Failed to search files: " + e.getMessage(), null); +// } +// } + private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) { try { Long userId = authService.useUserAuthToken(fileRequest.getToken()); int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1; int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20; + String sortBy = fileRequest.getSortBy() != null ? fileRequest.getSortBy() : "fileName"; + SortOrder sortOrder = SortOrder.fromString(fileRequest.getSortOrder()); + if (fileRequest.getQuery() == null || fileRequest.getQuery().isBlank()) { - return handleGetUserFiles(request, fileRequest); + return handleGetUserFilesWithSort(request, fileRequest, sortBy, sortOrder); } FileResponse allFiles = fileEntityService.getUserFiles(userId, 1, 1000, request.getVersion()); - var filteredFiles = allFiles.getFiles().stream() - .filter(f -> (f.getOriginalFileName() + f.getFileExtension()).toLowerCase() - .contains(fileRequest.getQuery().toLowerCase())) - .toList(); + String searchQuery = fileRequest.getQuery().toLowerCase().trim(); + String[] searchTerms = searchQuery.split("\\s+"); + + List filteredFiles = allFiles.getFiles().stream() + .filter(f -> matchesSearch(f, searchTerms, searchQuery)) + .collect(Collectors.toList()); + + Comparator comparator = getComparator(sortBy, sortOrder); + filteredFiles.sort(comparator); int start = (page - 1) * pageSize; int end = Math.min(start + pageSize, filteredFiles.size()); - List fileEntityResponses = filteredFiles.subList(start, Math.min(end, filteredFiles.size())); - long sumSize = fileEntityResponses.stream() + + if (start >= filteredFiles.size()) { + return createEmptyResponse(request, page, pageSize, sortBy, sortOrder.getValue()); + } + + List paginatedFiles = filteredFiles.subList(start, end); + long totalSize = paginatedFiles.stream() .mapToLong(FileEntityResponse::getFileSize) .sum(); FileListResponse response = FileListResponse.builder() - .files(fileEntityResponses) + .files(paginatedFiles) .totalCount(filteredFiles.size()) - .totalSize(sumSize) - .formattedTotalSize(fileEntityService.formatFileSize(sumSize)) + .totalSize(totalSize) + .formattedTotalSize(fileEntityService.formatFileSize(totalSize)) .page(page) .pageSize(pageSize) + .sortBy(sortBy) + .sortOrder(sortOrder.getValue()) .build(); return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), MessageCode.SUCCESS.getDescription(), response); + } catch (NotFoundAuthToken e) { - return new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(), + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_TOKEN.getCode(), "Authentication required", null); } catch (Exception e) { log.error("Error searching files", e); - return new BaseResponse(request.getMsgId(), MessageCode.FILE_UPLOAD_ERROR.getCode(), + return new BaseResponse(request.getMsgId(), + MessageCode.FILE_UPLOAD_ERROR.getCode(), "Failed to search files: " + e.getMessage(), null); } } + private boolean matchesSearch(FileEntityResponse file, String[] searchTerms, String fullQuery) { + String fileName = getSafeString(file.getFileName()); + String originalFileName = getSafeString(file.getOriginalFileName()); + String fileFormat = getSafeString(file.getFileFormat()); + String fullFileName = fileName + " " + originalFileName; + + if (fullFileName.toLowerCase().contains(fullQuery)) { + return true; + } + + for (String term : searchTerms) { + if (term.length() < 2) continue; + + if (fullFileName.toLowerCase().contains(term)) { + return true; + } + } + + if (fileFormat.toLowerCase().contains(fullQuery)) { + return true; + } + + String fileNameWithExtension = fileName; + if (!fileFormat.isEmpty() && !fileName.toLowerCase().endsWith("." + fileFormat.toLowerCase())) { + fileNameWithExtension = fileName + "." + fileFormat; + if (fileNameWithExtension.toLowerCase().contains(fullQuery)) { + return true; + } + } + + return false; + } + + private String getSafeString(String value) { + return value != null ? value : ""; + } + + private Comparator getComparator(String sortBy, SortOrder sortOrder) { + Comparator comparator; + + switch (sortBy) { + case "fileSize": + comparator = Comparator.comparingLong( + FileEntityResponse::getFileSize + ); + break; + + case "createdAt": + comparator = Comparator.comparing( + FileEntityResponse::getCreatedAt, + Comparator.nullsLast(Comparator.naturalOrder()) + ); + break; + + case "updatedAt": + comparator = Comparator.comparing( + FileEntityResponse::getUpdatedAt, + Comparator.nullsLast(Comparator.naturalOrder()) + ); + break; + + case "fileName": + comparator = Comparator.comparing( + f -> getSafeString(f.getFileName()).toLowerCase(), + Comparator.nullsLast(Comparator.naturalOrder()) + ); + break; + + case "protectStatus": + comparator = Comparator.comparing( + f -> getSafeString(f.getProtectStatus()).toLowerCase(), + Comparator.nullsLast(Comparator.naturalOrder()) + ); + break; + + case "supportId": + comparator = Comparator.comparing( + FileEntityResponse::getSupportId, + Comparator.nullsLast(Comparator.naturalOrder()) + ); + break; + + default: + comparator = Comparator.comparing( + f -> getSafeString(f.getFileName()).toLowerCase(), + Comparator.nullsLast(Comparator.naturalOrder()) + ); + break; + } + + return sortOrder == SortOrder.DESC ? comparator.reversed() : comparator; + } + + private BaseResponse createEmptyResponse(BaseRequest request, int page, int pageSize, + String sortBy, String sortOrder) { + FileListResponse emptyResponse = FileListResponse.builder() + .files(Collections.emptyList()) + .totalCount(0) + .totalSize(0L) + .formattedTotalSize("0 B") + .page(page) + .pageSize(pageSize) + .sortBy(sortBy) + .sortOrder(sortOrder) + .build(); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + emptyResponse); + } + + private BaseResponse handleGetUserFilesWithSort(BaseRequest request, FileEntityRequest fileRequest, + String sortBy, SortOrder sortOrder) { + try { + Long userId = authService.useUserAuthToken(fileRequest.getToken()); + int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1; + int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20; + + FileResponse allFiles = fileEntityService.getUserFiles(userId, 1, 1000, request.getVersion()); + + List files = new ArrayList<>(allFiles.getFiles()); + + Comparator comparator = getComparator(sortBy, sortOrder); + files.sort(comparator); + + int start = (page - 1) * pageSize; + int end = Math.min(start + pageSize, files.size()); + + if (start >= files.size()) { + return createEmptyResponse(request, page, pageSize, sortBy, sortOrder.getValue()); + } + + List paginatedFiles = files.subList(start, end); + long totalSize = paginatedFiles.stream() + .mapToLong(FileEntityResponse::getFileSize) + .sum(); + + FileListResponse response = FileListResponse.builder() + .files(paginatedFiles) + .totalCount(files.size()) + .totalSize(totalSize) + .formattedTotalSize(fileEntityService.formatFileSize(totalSize)) + .page(page) + .pageSize(pageSize) + .sortBy(sortBy) + .sortOrder(sortOrder.getValue()) + .build(); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + response); + + } catch (NotFoundAuthToken e) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_TOKEN.getCode(), + "Authentication required", null); + } catch (Exception e) { + log.error("Error getting user files with sort", e); + return new BaseResponse(request.getMsgId(), + MessageCode.FILE_UPLOAD_ERROR.getCode(), + "Failed to get files: " + e.getMessage(), + null); + } + } + private BaseResponse handleGetStorageUsage(BaseRequest request, FileEntityRequest fileRequest) { try { Long userId = authService.useUserAuthToken(fileRequest.getToken()); diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index c9ad65f..cb203a0 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -32,11 +32,11 @@ public class SearchImageService { private final OkHttpClient googleHttpClient; public SearchImageService() { - Proxy proxy = new Proxy(Proxy.Type.HTTP, - new InetSocketAddress("193.46.217.94", 3128)); +// Proxy proxy = new Proxy(Proxy.Type.HTTP, +// new InetSocketAddress("193.46.217.94", 3128)); this.yandexHttpClient = new OkHttpClient.Builder() - .proxy(proxy) +// .proxy(proxy) .connectTimeout(35, TimeUnit.SECONDS) .writeTimeout(35, TimeUnit.SECONDS) .readTimeout(35, TimeUnit.SECONDS) From ff9c959600611d470565d2c95eb81dbefcab8c03 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 19 Mar 2026 18:11:56 +0700 Subject: [PATCH 214/274] dev add violation notion --- .../ru/soune/nocopy/service/search/SearchImageService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index cb203a0..c9ad65f 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -32,11 +32,11 @@ public class SearchImageService { private final OkHttpClient googleHttpClient; public SearchImageService() { -// Proxy proxy = new Proxy(Proxy.Type.HTTP, -// new InetSocketAddress("193.46.217.94", 3128)); + Proxy proxy = new Proxy(Proxy.Type.HTTP, + new InetSocketAddress("193.46.217.94", 3128)); this.yandexHttpClient = new OkHttpClient.Builder() -// .proxy(proxy) + .proxy(proxy) .connectTimeout(35, TimeUnit.SECONDS) .writeTimeout(35, TimeUnit.SECONDS) .readTimeout(35, TimeUnit.SECONDS) From 037445dcd591deb4cdeb09751eb18d8872cc282a Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Mar 2026 12:59:03 +0700 Subject: [PATCH 215/274] dev add violation notion --- .../nocopy/dto/file/FileEntityRequest.java | 6 + .../nocopy/handler/FileEntityHandler.java | 139 +++++++++--------- 2 files changed, 77 insertions(+), 68 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java b/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java index 50fadd5..c21f7b4 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java @@ -40,4 +40,10 @@ public class FileEntityRequest { @JsonProperty("sort_order") private String sortOrder; + + @JsonProperty("type") + private String type; + + @JsonProperty("date_filter") + private String dateFilter; } diff --git a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java index b6e9202..210b5c6 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java @@ -16,6 +16,7 @@ import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileStatsService; +import java.time.LocalDate; import java.util.*; import java.util.stream.Collectors; @@ -182,53 +183,6 @@ public class FileEntityHandler implements RequestHandler { } } -// private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) { -// try { -// Long userId = authService.useUserAuthToken(fileRequest.getToken()); -// int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1; -// int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20; -// -// if (fileRequest.getQuery() == null || fileRequest.getQuery().isBlank()) { -// return handleGetUserFiles(request, fileRequest); -// } -// -// FileResponse allFiles = fileEntityService.getUserFiles(userId, 1, 1000, request.getVersion()); -// -// var filteredFiles = allFiles.getFiles().stream() -// .filter(f -> (f.getOriginalFileName() + f.getFileExtension()).toLowerCase() -// .contains(fileRequest.getQuery().toLowerCase())) -// .toList(); -// -// int start = (page - 1) * pageSize; -// int end = Math.min(start + pageSize, filteredFiles.size()); -// List fileEntityResponses = filteredFiles.subList(start, Math.min(end, filteredFiles.size())); -// long sumSize = fileEntityResponses.stream() -// .mapToLong(FileEntityResponse::getFileSize) -// .sum(); -// -// FileListResponse response = FileListResponse.builder() -// .files(fileEntityResponses) -// .totalCount(filteredFiles.size()) -// .totalSize(sumSize) -// .formattedTotalSize(fileEntityService.formatFileSize(sumSize)) -// .page(page) -// .pageSize(pageSize) -// .build(); -// -// return new BaseResponse(request.getMsgId(), -// MessageCode.SUCCESS.getCode(), -// MessageCode.SUCCESS.getDescription(), -// response); -// } catch (NotFoundAuthToken e) { -// return new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(), -// "Authentication required", null); -// } catch (Exception e) { -// log.error("Error searching files", e); -// return new BaseResponse(request.getMsgId(), MessageCode.FILE_UPLOAD_ERROR.getCode(), -// "Failed to search files: " + e.getMessage(), null); -// } -// } - private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) { try { Long userId = authService.useUserAuthToken(fileRequest.getToken()); @@ -238,17 +192,16 @@ public class FileEntityHandler implements RequestHandler { String sortBy = fileRequest.getSortBy() != null ? fileRequest.getSortBy() : "fileName"; SortOrder sortOrder = SortOrder.fromString(fileRequest.getSortOrder()); - if (fileRequest.getQuery() == null || fileRequest.getQuery().isBlank()) { - return handleGetUserFilesWithSort(request, fileRequest, sortBy, sortOrder); - } - FileResponse allFiles = fileEntityService.getUserFiles(userId, 1, 1000, request.getVersion()); - String searchQuery = fileRequest.getQuery().toLowerCase().trim(); + String searchQuery = fileRequest.getQuery() != null ? fileRequest.getQuery().toLowerCase().trim() : ""; String[] searchTerms = searchQuery.split("\\s+"); + String filterType = fileRequest.getType(); + String dateFilter = fileRequest.getDateFilter(); List filteredFiles = allFiles.getFiles().stream() - .filter(f -> matchesSearch(f, searchTerms, searchQuery)) + .filter(f -> matchesSearch(f, searchTerms, searchQuery, filterType)) + .filter(f -> matchesDateFilter(f, dateFilter)) .collect(Collectors.toList()); Comparator comparator = getComparator(sortBy, sortOrder); @@ -294,37 +247,82 @@ public class FileEntityHandler implements RequestHandler { } } - private boolean matchesSearch(FileEntityResponse file, String[] searchTerms, String fullQuery) { + private boolean matchesDateFilter(FileEntityResponse file, String dateFilter) { + if (dateFilter == null || dateFilter.isEmpty()) { + return true; + } + + LocalDate fileDate = file.getCreatedAt().toLocalDate(); + LocalDate now = LocalDate.now(); + + switch (dateFilter.toLowerCase()) { + case "today": + return fileDate.equals(now); + case "week": + return fileDate.isAfter(now.minusWeeks(1)) && !fileDate.isAfter(now); + case "month": + return fileDate.isAfter(now.minusMonths(1)) && !fileDate.isAfter(now); + default: + return true; + } + } + + private boolean matchesSearch(FileEntityResponse file, String[] searchTerms, String fullQuery, String type) { String fileName = getSafeString(file.getFileName()); String originalFileName = getSafeString(file.getOriginalFileName()); + String mimeType = getSafeString(file.getMimeType()); String fileFormat = getSafeString(file.getFileFormat()); String fullFileName = fileName + " " + originalFileName; - if (fullFileName.toLowerCase().contains(fullQuery)) { - return true; + boolean matchesType = true; + if (type != null && !type.isEmpty()) { + matchesType = matchesMimeType(mimeType, type); } - for (String term : searchTerms) { - if (term.length() < 2) continue; + if (!matchesType) { + return false; + } - if (fullFileName.toLowerCase().contains(term)) { + if (fullQuery != null && !fullQuery.isEmpty()) { + if (fullFileName.toLowerCase().contains(fullQuery)) { return true; } - } - if (fileFormat.toLowerCase().contains(fullQuery)) { - return true; - } + for (String term : searchTerms) { + if (term.length() < 2) continue; + if (fullFileName.toLowerCase().contains(term)) { + return true; + } + } - String fileNameWithExtension = fileName; - if (!fileFormat.isEmpty() && !fileName.toLowerCase().endsWith("." + fileFormat.toLowerCase())) { - fileNameWithExtension = fileName + "." + fileFormat; - if (fileNameWithExtension.toLowerCase().contains(fullQuery)) { + if (fileFormat.toLowerCase().contains(fullQuery)) { return true; } + + if (mimeType.toLowerCase().contains(fullQuery)) { + return true; + } + + if (!fileFormat.isEmpty() && !fileName.toLowerCase().endsWith("." + fileFormat.toLowerCase())) { + String fileNameWithExtension = fileName + "." + fileFormat; + if (fileNameWithExtension.toLowerCase().contains(fullQuery)) { + return true; + } + } + + return false; } - return false; + return true; + } + + private boolean matchesMimeType(String mimeType, String filterType) { + if (filterType == null || filterType.isEmpty()) return true; + + String mimeTypeLower = mimeType.toLowerCase(); + String filterTypeLower = filterType.toLowerCase().trim(); + + return mimeTypeLower.equals(filterTypeLower); } private String getSafeString(String value) { @@ -341,6 +339,11 @@ public class FileEntityHandler implements RequestHandler { ); break; + case "fileType": + comparator = Comparator.comparing( + FileEntityResponse::getMimeType); + break; + case "createdAt": comparator = Comparator.comparing( FileEntityResponse::getCreatedAt, From 4ff5c5ec1327a466b62e96901d2eb9c194004438 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Mar 2026 15:26:20 +0700 Subject: [PATCH 216/274] dev add violation notion --- .../search/SearchProperties.java | 26 ++++ .../controller/SearchSettingsController.java | 58 ++++++++ .../dto/search/config/SearchSettingsDto.java | 11 ++ .../handler/ImageFoundRequestHandler.java | 102 ++++++++------ .../service/file/FileCleanupService.java | 15 --- .../monitoring/MonitoringSearchService.java | 93 +++++++++---- .../search/GlobalSearchAsyncProcessor.java | 125 +++++++++++------- .../service/search/SearchImageService.java | 58 ++++---- src/main/resources/application.yaml | 11 ++ 9 files changed, 348 insertions(+), 151 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/configuration/search/SearchProperties.java create mode 100644 src/main/java/ru/soune/nocopy/controller/SearchSettingsController.java create mode 100644 src/main/java/ru/soune/nocopy/dto/search/config/SearchSettingsDto.java diff --git a/src/main/java/ru/soune/nocopy/configuration/search/SearchProperties.java b/src/main/java/ru/soune/nocopy/configuration/search/SearchProperties.java new file mode 100644 index 0000000..3ca91ff --- /dev/null +++ b/src/main/java/ru/soune/nocopy/configuration/search/SearchProperties.java @@ -0,0 +1,26 @@ +package ru.soune.nocopy.configuration.search; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import java.util.Map; + +@Data +@Component +@ConfigurationProperties(prefix = "search") +public class SearchProperties { + private Map engines; + private ProxyConfig proxy; + + @Data + public static class EngineConfig { + private boolean enabled; + } + + @Data + public static class ProxyConfig { + private boolean enabled; + private String host; + private int port; + } +} diff --git a/src/main/java/ru/soune/nocopy/controller/SearchSettingsController.java b/src/main/java/ru/soune/nocopy/controller/SearchSettingsController.java new file mode 100644 index 0000000..d18596a --- /dev/null +++ b/src/main/java/ru/soune/nocopy/controller/SearchSettingsController.java @@ -0,0 +1,58 @@ +package ru.soune.nocopy.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.soune.nocopy.configuration.search.SearchProperties; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.search.config.SearchSettingsDto; + +import java.util.Map; + + +@Slf4j +@RestController +@RequestMapping("/api/search/settings") +@RequiredArgsConstructor +public class SearchSettingsController { + + private final SearchProperties searchProperties; + + @GetMapping + public ResponseEntity getSettings() { + SearchSettingsDto dto = new SearchSettingsDto(); + + dto.setEngines(Map.of( + "yandex", searchProperties.getEngines().get("yandex").isEnabled(), + "google", searchProperties.getEngines().get("google").isEnabled())); + + dto.setProxyEnabled(searchProperties.getProxy().isEnabled()); + + return ResponseEntity.ok().body(BaseResponse.builder() + .messageDesc("Success") + .messageBody(dto) + .build()); + } + + @PutMapping + public ResponseEntity updateSettings(@RequestBody SearchSettingsDto settings) { + if (settings.getEngines() != null) { + settings.getEngines().forEach((key, value) -> { + if (searchProperties.getEngines().containsKey(key)) { + searchProperties.getEngines().get(key).setEnabled(value); + } + }); + } + + if (settings.getProxyEnabled() != null) { + searchProperties.getProxy().setEnabled(settings.getProxyEnabled()); + } + + log.info("Settings updated: {}", settings); + + return ResponseEntity.ok().body(BaseResponse.builder() + .messageDesc("Settings updated") + .build()); + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/search/config/SearchSettingsDto.java b/src/main/java/ru/soune/nocopy/dto/search/config/SearchSettingsDto.java new file mode 100644 index 0000000..ffacfa0 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/search/config/SearchSettingsDto.java @@ -0,0 +1,11 @@ +package ru.soune.nocopy.dto.search.config; + + +import lombok.Data; +import java.util.Map; + +@Data +public class SearchSettingsDto { + private Map engines; + private Boolean proxyEnabled; +} diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 2f42428..51bb0fa 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import ru.soune.nocopy.configuration.search.SearchProperties; import ru.soune.nocopy.dto.BaseRequest; import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.MessageCode; @@ -36,6 +37,8 @@ public class ImageFoundRequestHandler implements RequestHandler { private final TariffInfoService tariffInfoService; + private final SearchProperties searchProperties; + @Override public BaseResponse handle(BaseRequest request) throws Exception { ImageSearchRequest imageSearchRequest = objectMapper.convertValue(request.getMessageBody(), @@ -47,47 +50,72 @@ public class ImageFoundRequestHandler implements RequestHandler { .orElseThrow(() -> { throw new NotValidFieldException("File not found", new BaseResponse(20007, MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(), - Map.of("fileId",fileId))); - }); + Map.of("fileId", fileId)));}); - String searchResponseYandex; -// String searchResponseGoogle; - List allYandexImages = new ArrayList<>(); -// List allGoogleImages = new ArrayList<>(); - List allUniqueImages; + List allYandexImages = new ArrayList<>(); + List allGoogleImages = new ArrayList<>(); boolean hasTimeout = false; -// -// try { -// searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", -// "exact_matches"); -// log.info("searchResponseGoogle OK,{}", searchResponseGoogle); -// allGoogleImages = searchImageService.getAllImagesWithoutPagination( -// searchResponseGoogle, "exact_matches"); -// -// } catch (TimeoutException e) { -// log.warn("Google search timeout for file {}", fileId); -// hasTimeout = true; -// } catch (IOException e) { -// log.error("Google search failed for file {}", fileId, e); -// } - try { - searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", - "visual_matches"); - log.info("searchResponseYandex OK"); - allYandexImages = searchImageService.getAllImagesWithoutPagination( - searchResponseYandex, "visual_matches"); - } catch (TimeoutException e) { - log.warn("Yandex search timeout for file {}", fileId); - hasTimeout = true; - } catch (IOException e) { - log.error("Yandex search failed for file {}", fileId, e); - } + boolean useGoogle = searchProperties.getEngines().getOrDefault("google", + new SearchProperties.EngineConfig()).isEnabled(); + boolean useYandex = searchProperties.getEngines().getOrDefault("yandex", + new SearchProperties.EngineConfig()).isEnabled(); -// allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); -// allUniqueImages = allGoogleImages; - allUniqueImages = allYandexImages; - log.info("allUniqueImages OK: {} images", allUniqueImages.size()); + log.info("Search settings: useGoogle={}, useYandex={}", useGoogle, useYandex); + + if (useGoogle) { + try { + String searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", + "exact_matches"); + log.info("searchResponseGoogle OK"); + allGoogleImages = searchImageService.getAllImagesWithoutPagination( + searchResponseGoogle, "exact_matches"); + } catch (TimeoutException e) { + log.warn("Google search timeout for file {}", fileId); + hasTimeout = true; + } catch (IOException e) { + log.error("Google search failed for file {}", fileId, e); + } + } else { + log.info("Google search is disabled by settings"); + } + + if (useYandex) { + try { + String searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", + "visual_matches"); + log.info("searchResponseYandex OK"); + allYandexImages = searchImageService.getAllImagesWithoutPagination( + searchResponseYandex, "visual_matches"); + } catch (TimeoutException e) { + log.warn("Yandex search timeout for file {}", fileId); + hasTimeout = true; + } catch (IOException e) { + log.error("Yandex search failed for file {}", fileId, e); + } + } else { + log.info("Yandex search is disabled by settings"); + } + + if (allYandexImages.isEmpty() && allGoogleImages.isEmpty()) { + log.warn("No results from any search engine"); + String errorMessage = useGoogle || useYandex ? "No results found" : "All search engines are disabled"; + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + errorMessage, new YandexSearchResponse()); + } + + List allUniqueImages; + if (useGoogle && useYandex) { + allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); + log.info("Merged results from both engines: {} unique images", allUniqueImages.size()); + } else if (useGoogle) { + allUniqueImages = allGoogleImages; + log.info("Results only from Google: {} images", allUniqueImages.size()); + } else { + allUniqueImages = allYandexImages; + log.info("Results only from Yandex: {} images", allUniqueImages.size()); + } tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); diff --git a/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java b/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java index 18bf37c..7d90acb 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java @@ -34,8 +34,6 @@ public class FileCleanupService { public void cleanupExpiredFiles() { log.info("Starting cleanup of expired temporary files"); -// cleanupOldPeriods(); - Path tempDir = Paths.get(basePath, "temp"); if (!Files.exists(tempDir)) { return; @@ -85,17 +83,4 @@ public class FileCleanupService { log.error("Error during cleanup", e); } } - - private void cleanupOldPeriods() { - LocalDate thirtyDaysAgo = LocalDate.now().minusDays(30); - - List checks = protectedFileCheckRepository.findByPeriodStartDateBefore(thirtyDaysAgo); - - for (ProtectedFileCheck check : checks) { - check.cleanupOldData(); - } - - protectedFileCheckRepository.saveAll(checks); - log.info("Cleaned up {} old protection usage records", checks.size()); - } } diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java index 0b11018..93f43f3 100644 --- a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java @@ -5,11 +5,11 @@ 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.search.GlobalSearchResult; import ru.soune.nocopy.entity.tarif.TariffInfo; import ru.soune.nocopy.entity.tarif.TariffType; import ru.soune.nocopy.entity.user.User; @@ -24,6 +24,7 @@ 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; @@ -45,6 +46,8 @@ public class MonitoringSearchService { private final ViolationService violationService; + private final SearchProperties searchProperties; + @Transactional public void processFileSearch(FileMonitoringEntity monitoring) throws MessagingException, IOException { monitoring.setLastRun(LocalDateTime.now()); @@ -53,36 +56,78 @@ public class MonitoringSearchService { MonitoringType monitoringType = monitoring.getMonitoringType(); TariffDTO tariffMonitoring = tariffService.getTariffByType(TariffType.valueOf(monitoringType.name())); + try { tariffInfoService.writeOffTokens(monitoring.getUserId(), tariffMonitoring.getTokens()); - String yandexResponse = searchImageService.searchReverseByPublicUrl( - monitoring.getFile(), "yandex_reverse_image", "visual_matches"); + boolean useYandex = searchProperties.getEngines().getOrDefault("yandex", + new SearchProperties.EngineConfig()).isEnabled(); + boolean useGoogle = searchProperties.getEngines().getOrDefault("google", + new SearchProperties.EngineConfig()).isEnabled(); - List yandexImages = - searchImageService.getAllImagesWithoutPagination(yandexResponse, "visual_matches"); + log.info("Monitoring search settings: useYandex={}, useGoogle={}", useYandex, useGoogle); - for (YandexSearchResponse.ImageResult imageResult : yandexImages) { + List allResults = new ArrayList<>(); + + if (useYandex) { + try { + String yandexResponse = searchImageService.searchReverseByPublicUrl( + monitoring.getFile(), "yandex_reverse_image", "visual_matches"); + + List 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 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 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); } -// try { -// String googleResponse = searchImageService.searchReverseByPublicUrl( -// monitoring.getFile(), "google_lens", "exact_matches"); -// -// List googleImages = -// searchImageService.getAllImagesWithoutPagination(googleResponse, "exact_matches"); -// -// -// for (YandexSearchResponse.ImageResult imageResult : googleImages) { -// violationService.processViolation(imageResult, monitoring.getFile(), null); -// } -// -// } catch (TimeoutException | IOException e) { -// log.warn("Google search failed"); -// } - 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(); @@ -91,10 +136,12 @@ public class MonitoringSearchService { 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()); + } finally { updateNextRun(monitoring); monitoringRepository.save(monitoring); diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java index 9f202c4..19b1644 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -1,21 +1,18 @@ package ru.soune.nocopy.service.search; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.vrt.fileprotection.image.ImageCheckResult; 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.entity.violation.Violation; import ru.soune.nocopy.repository.GlobalSearchResultRepository; import ru.soune.nocopy.repository.GlobalSearchTaskRepository; -import ru.soune.nocopy.repository.ViolationRepository; import ru.soune.nocopy.service.tariff.TariffConstants; import ru.soune.nocopy.service.tariff.TariffInfoService; import ru.soune.nocopy.service.violation.ViolationService; @@ -36,16 +33,14 @@ public class GlobalSearchAsyncProcessor { private final SearchImageService searchImageService; - private final ObjectMapper objectMapper; - private final TariffInfoService tariffInfoService; private final GlobalSearchTaskRepository globalSearchTaskRepository; - private final ViolationRepository violationRepository; - private final ViolationService violationService; + private final SearchProperties searchProperties; + @Async @Transactional public void processFilesAsync(String taskId, List filesToProcess, Long userId) { @@ -85,52 +80,85 @@ public class GlobalSearchAsyncProcessor { result = globalSearchResultRepository.save(result); -// List googleImages = new ArrayList<>(); + 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 googleImages = new ArrayList<>(); List yandexImages = new ArrayList<>(); - List allUniqueImages = new ArrayList<>(); + List allResults = new ArrayList<>(); boolean hasTimeout = false; -// try { -// String searchResponseGoogle = searchImageService.searchReverseByPublicUrl( -// file, "google_lens", "exact_matches"); -// -// googleImages = searchImageService.getAllImagesWithoutPagination(searchResponseGoogle, -// "exact_matches"); -// -// allUniqueImages.addAll(googleImages); -// log.info("Google search OK for file {}", file.getId()); -// -// } 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); -// } + if (useGoogle) { + try { + String searchResponseGoogle = searchImageService.searchReverseByPublicUrl( + file, "google_lens", "exact_matches"); - try { - String searchResponseYandex = searchImageService.searchReverseByPublicUrl( - file, "yandex_reverse_image", "visual_matches"); + googleImages = searchImageService.getAllImagesWithoutPagination(searchResponseGoogle, + "exact_matches"); - yandexImages = - searchImageService.getAllImagesWithoutPagination(searchResponseYandex, "visual_matches"); + allResults.addAll(googleImages); + log.info("Google search OK for file {}, found {} results", file.getId(), googleImages.size()); - allUniqueImages.addAll(yandexImages); - log.info("Yandex search OK for file {}", file.getId()); - - } 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); + } 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()); } -// allUniqueImages = searchImageService.removeDuplicateUrls( -// allUniqueImages.stream() -// .filter(img -> img.getUrl() != null) -// .collect(Collectors.toList()), -// new ArrayList<>()); + if (useYandex) { + try { + String searchResponseYandex = searchImageService.searchReverseByPublicUrl( + file, "yandex_reverse_image", "visual_matches"); - allUniqueImages = yandexImages; + 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 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); @@ -138,10 +166,15 @@ public class GlobalSearchAsyncProcessor { 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()); - tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH); + if (!allUniqueImages.isEmpty()) { + tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH); + } } return result; diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index c9ad65f..2aa67dd 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -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; @@ -23,40 +24,15 @@ import java.util.concurrent.*; @Slf4j @Service + public class SearchImageService { private final ObjectMapper objectMapper; - private final OkHttpClient yandexHttpClient; - - private final OkHttpClient googleHttpClient; - - public SearchImageService() { - Proxy proxy = new Proxy(Proxy.Type.HTTP, - new InetSocketAddress("193.46.217.94", 3128)); - - this.yandexHttpClient = new OkHttpClient.Builder() - .proxy(proxy) - .connectTimeout(35, TimeUnit.SECONDS) - .writeTimeout(35, TimeUnit.SECONDS) - .readTimeout(35, TimeUnit.SECONDS) - .callTimeout(35, TimeUnit.SECONDS) - .followRedirects(true) - .followSslRedirects(true) - .retryOnConnectionFailure(true) - .build(); - - this.googleHttpClient = new OkHttpClient.Builder() -// .proxy(proxy) - .connectTimeout(35, TimeUnit.SECONDS) - .writeTimeout(35, TimeUnit.SECONDS) - .readTimeout(35, TimeUnit.SECONDS) - .callTimeout(35, TimeUnit.SECONDS) - .followRedirects(true) - .followSslRedirects(true) - .retryOnConnectionFailure(true) - .build(); + private final SearchProperties searchProperties; + public SearchImageService(SearchProperties searchProperties) { + this.searchProperties = searchProperties; this.objectMapper = new ObjectMapper(); } @@ -113,12 +89,34 @@ public class SearchImageService { return mimeType != null && mimeType.startsWith("image"); } + private OkHttpClient createHttpClient(boolean useProxy) { + 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"); } - OkHttpClient client = engine.contains("yandex_reverse_image") ? yandexHttpClient : googleHttpClient; + boolean useProxy = searchProperties.getProxy().isEnabled(); + + OkHttpClient client = createHttpClient(useProxy); HttpUrl url = HttpUrl.parse("https://www.searchapi.io/api/v1/search") .newBuilder() diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index a0d6d5a..43f3b9a 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -107,6 +107,17 @@ searchapi: api-key: ${SEARCHAPI_API_KEY:5jyYZC8jSaxhZTwjMUhwtAXi} reverse-image-url: "https://searchapi.io/api/v1/search" +search: + engines: + yandex: + enabled: true + google: + enabled: false + proxy: + enabled: true + host: "193.46.217.94" + port: 3128 + yookassa: secret-key: test_0Yns_0NHV5GJf6ypJ5HC4NSfnLO8SJkw-1PwrVWsDl4 shop-id: 1276731 From b6656961de9dc8dc60a34fd7a31eb6f2c7874766 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Mar 2026 15:55:58 +0700 Subject: [PATCH 217/274] dev add violation notion --- .../service/search/SearchImageService.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 2aa67dd..9d77047 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -89,7 +89,7 @@ public class SearchImageService { return mimeType != null && mimeType.startsWith("image"); } - private OkHttpClient createHttpClient(boolean useProxy) { + private OkHttpClient createHttpClient() { OkHttpClient.Builder builder = new OkHttpClient.Builder() .connectTimeout(35, TimeUnit.SECONDS) .writeTimeout(35, TimeUnit.SECONDS) @@ -99,12 +99,12 @@ public class SearchImageService { .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); - } +// 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(); } @@ -114,9 +114,10 @@ public class SearchImageService { throw new IllegalStateException("SearchAPI key not configured"); } - boolean useProxy = searchProperties.getProxy().isEnabled(); +// boolean useProxy = searchProperties.getProxy().isEnabled(); - OkHttpClient client = createHttpClient(useProxy); +// OkHttpClient client = createHttpClient(useProxy); + OkHttpClient client = createHttpClient(); HttpUrl url = HttpUrl.parse("https://www.searchapi.io/api/v1/search") .newBuilder() From a9b7b7ca6ec6e8627d08d8a1d10b6acf7aa7bff1 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Mar 2026 16:02:10 +0700 Subject: [PATCH 218/274] Revert "dev add violation notion" This reverts commit b6656961de9dc8dc60a34fd7a31eb6f2c7874766. --- .../service/search/SearchImageService.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 9d77047..2aa67dd 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -89,7 +89,7 @@ public class SearchImageService { return mimeType != null && mimeType.startsWith("image"); } - private OkHttpClient createHttpClient() { + private OkHttpClient createHttpClient(boolean useProxy) { OkHttpClient.Builder builder = new OkHttpClient.Builder() .connectTimeout(35, TimeUnit.SECONDS) .writeTimeout(35, TimeUnit.SECONDS) @@ -99,12 +99,12 @@ public class SearchImageService { .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); -// } + 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(); } @@ -114,10 +114,9 @@ public class SearchImageService { throw new IllegalStateException("SearchAPI key not configured"); } -// boolean useProxy = searchProperties.getProxy().isEnabled(); + boolean useProxy = searchProperties.getProxy().isEnabled(); -// OkHttpClient client = createHttpClient(useProxy); - OkHttpClient client = createHttpClient(); + OkHttpClient client = createHttpClient(useProxy); HttpUrl url = HttpUrl.parse("https://www.searchapi.io/api/v1/search") .newBuilder() From 726da25ddf1d9ec657bd5c260f0e471478270cf4 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Mar 2026 16:02:11 +0700 Subject: [PATCH 219/274] Revert "dev add violation notion" This reverts commit 4ff5c5ec1327a466b62e96901d2eb9c194004438. --- .../search/SearchProperties.java | 26 ---- .../controller/SearchSettingsController.java | 58 -------- .../dto/search/config/SearchSettingsDto.java | 11 -- .../handler/ImageFoundRequestHandler.java | 106 ++++++--------- .../service/file/FileCleanupService.java | 15 +++ .../monitoring/MonitoringSearchService.java | 93 ++++--------- .../search/GlobalSearchAsyncProcessor.java | 125 +++++++----------- .../service/search/SearchImageService.java | 58 ++++---- src/main/resources/application.yaml | 11 -- 9 files changed, 153 insertions(+), 350 deletions(-) delete mode 100644 src/main/java/ru/soune/nocopy/configuration/search/SearchProperties.java delete mode 100644 src/main/java/ru/soune/nocopy/controller/SearchSettingsController.java delete mode 100644 src/main/java/ru/soune/nocopy/dto/search/config/SearchSettingsDto.java diff --git a/src/main/java/ru/soune/nocopy/configuration/search/SearchProperties.java b/src/main/java/ru/soune/nocopy/configuration/search/SearchProperties.java deleted file mode 100644 index 3ca91ff..0000000 --- a/src/main/java/ru/soune/nocopy/configuration/search/SearchProperties.java +++ /dev/null @@ -1,26 +0,0 @@ -package ru.soune.nocopy.configuration.search; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; -import java.util.Map; - -@Data -@Component -@ConfigurationProperties(prefix = "search") -public class SearchProperties { - private Map engines; - private ProxyConfig proxy; - - @Data - public static class EngineConfig { - private boolean enabled; - } - - @Data - public static class ProxyConfig { - private boolean enabled; - private String host; - private int port; - } -} diff --git a/src/main/java/ru/soune/nocopy/controller/SearchSettingsController.java b/src/main/java/ru/soune/nocopy/controller/SearchSettingsController.java deleted file mode 100644 index d18596a..0000000 --- a/src/main/java/ru/soune/nocopy/controller/SearchSettingsController.java +++ /dev/null @@ -1,58 +0,0 @@ -package ru.soune.nocopy.controller; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import ru.soune.nocopy.configuration.search.SearchProperties; -import ru.soune.nocopy.dto.BaseResponse; -import ru.soune.nocopy.dto.search.config.SearchSettingsDto; - -import java.util.Map; - - -@Slf4j -@RestController -@RequestMapping("/api/search/settings") -@RequiredArgsConstructor -public class SearchSettingsController { - - private final SearchProperties searchProperties; - - @GetMapping - public ResponseEntity getSettings() { - SearchSettingsDto dto = new SearchSettingsDto(); - - dto.setEngines(Map.of( - "yandex", searchProperties.getEngines().get("yandex").isEnabled(), - "google", searchProperties.getEngines().get("google").isEnabled())); - - dto.setProxyEnabled(searchProperties.getProxy().isEnabled()); - - return ResponseEntity.ok().body(BaseResponse.builder() - .messageDesc("Success") - .messageBody(dto) - .build()); - } - - @PutMapping - public ResponseEntity updateSettings(@RequestBody SearchSettingsDto settings) { - if (settings.getEngines() != null) { - settings.getEngines().forEach((key, value) -> { - if (searchProperties.getEngines().containsKey(key)) { - searchProperties.getEngines().get(key).setEnabled(value); - } - }); - } - - if (settings.getProxyEnabled() != null) { - searchProperties.getProxy().setEnabled(settings.getProxyEnabled()); - } - - log.info("Settings updated: {}", settings); - - return ResponseEntity.ok().body(BaseResponse.builder() - .messageDesc("Settings updated") - .build()); - } -} diff --git a/src/main/java/ru/soune/nocopy/dto/search/config/SearchSettingsDto.java b/src/main/java/ru/soune/nocopy/dto/search/config/SearchSettingsDto.java deleted file mode 100644 index ffacfa0..0000000 --- a/src/main/java/ru/soune/nocopy/dto/search/config/SearchSettingsDto.java +++ /dev/null @@ -1,11 +0,0 @@ -package ru.soune.nocopy.dto.search.config; - - -import lombok.Data; -import java.util.Map; - -@Data -public class SearchSettingsDto { - private Map engines; - private Boolean proxyEnabled; -} diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 51bb0fa..2f42428 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import ru.soune.nocopy.configuration.search.SearchProperties; import ru.soune.nocopy.dto.BaseRequest; import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.MessageCode; @@ -37,8 +36,6 @@ public class ImageFoundRequestHandler implements RequestHandler { private final TariffInfoService tariffInfoService; - private final SearchProperties searchProperties; - @Override public BaseResponse handle(BaseRequest request) throws Exception { ImageSearchRequest imageSearchRequest = objectMapper.convertValue(request.getMessageBody(), @@ -50,72 +47,47 @@ public class ImageFoundRequestHandler implements RequestHandler { .orElseThrow(() -> { throw new NotValidFieldException("File not found", new BaseResponse(20007, MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(), - Map.of("fileId", fileId)));}); - - List allYandexImages = new ArrayList<>(); - List allGoogleImages = new ArrayList<>(); - boolean hasTimeout = false; - - boolean useGoogle = searchProperties.getEngines().getOrDefault("google", - new SearchProperties.EngineConfig()).isEnabled(); - boolean useYandex = searchProperties.getEngines().getOrDefault("yandex", - new SearchProperties.EngineConfig()).isEnabled(); - - log.info("Search settings: useGoogle={}, useYandex={}", useGoogle, useYandex); - - if (useGoogle) { - try { - String searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", - "exact_matches"); - log.info("searchResponseGoogle OK"); - allGoogleImages = searchImageService.getAllImagesWithoutPagination( - searchResponseGoogle, "exact_matches"); - } catch (TimeoutException e) { - log.warn("Google search timeout for file {}", fileId); - hasTimeout = true; - } catch (IOException e) { - log.error("Google search failed for file {}", fileId, e); - } - } else { - log.info("Google search is disabled by settings"); - } - - if (useYandex) { - try { - String searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", - "visual_matches"); - log.info("searchResponseYandex OK"); - allYandexImages = searchImageService.getAllImagesWithoutPagination( - searchResponseYandex, "visual_matches"); - } catch (TimeoutException e) { - log.warn("Yandex search timeout for file {}", fileId); - hasTimeout = true; - } catch (IOException e) { - log.error("Yandex search failed for file {}", fileId, e); - } - } else { - log.info("Yandex search is disabled by settings"); - } - - if (allYandexImages.isEmpty() && allGoogleImages.isEmpty()) { - log.warn("No results from any search engine"); - String errorMessage = useGoogle || useYandex ? "No results found" : "All search engines are disabled"; - - return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), - errorMessage, new YandexSearchResponse()); - } + Map.of("fileId",fileId))); + }); + String searchResponseYandex; +// String searchResponseGoogle; + List allYandexImages = new ArrayList<>(); +// List allGoogleImages = new ArrayList<>(); List allUniqueImages; - if (useGoogle && useYandex) { - allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); - log.info("Merged results from both engines: {} unique images", allUniqueImages.size()); - } else if (useGoogle) { - allUniqueImages = allGoogleImages; - log.info("Results only from Google: {} images", allUniqueImages.size()); - } else { - allUniqueImages = allYandexImages; - log.info("Results only from Yandex: {} images", allUniqueImages.size()); - } + boolean hasTimeout = false; +// +// try { +// searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", +// "exact_matches"); +// log.info("searchResponseGoogle OK,{}", searchResponseGoogle); +// allGoogleImages = searchImageService.getAllImagesWithoutPagination( +// searchResponseGoogle, "exact_matches"); +// +// } catch (TimeoutException e) { +// log.warn("Google search timeout for file {}", fileId); +// hasTimeout = true; +// } catch (IOException e) { +// log.error("Google search failed for file {}", fileId, e); +// } + + try { + searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", + "visual_matches"); + log.info("searchResponseYandex OK"); + allYandexImages = searchImageService.getAllImagesWithoutPagination( + searchResponseYandex, "visual_matches"); + } catch (TimeoutException e) { + log.warn("Yandex search timeout for file {}", fileId); + hasTimeout = true; + } catch (IOException e) { + log.error("Yandex search failed for file {}", fileId, e); + } + +// allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); +// allUniqueImages = allGoogleImages; + allUniqueImages = allYandexImages; + log.info("allUniqueImages OK: {} images", allUniqueImages.size()); tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); diff --git a/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java b/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java index 7d90acb..18bf37c 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java @@ -34,6 +34,8 @@ public class FileCleanupService { public void cleanupExpiredFiles() { log.info("Starting cleanup of expired temporary files"); +// cleanupOldPeriods(); + Path tempDir = Paths.get(basePath, "temp"); if (!Files.exists(tempDir)) { return; @@ -83,4 +85,17 @@ public class FileCleanupService { log.error("Error during cleanup", e); } } + + private void cleanupOldPeriods() { + LocalDate thirtyDaysAgo = LocalDate.now().minusDays(30); + + List checks = protectedFileCheckRepository.findByPeriodStartDateBefore(thirtyDaysAgo); + + for (ProtectedFileCheck check : checks) { + check.cleanupOldData(); + } + + protectedFileCheckRepository.saveAll(checks); + log.info("Cleaned up {} old protection usage records", checks.size()); + } } diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java index 93f43f3..0b11018 100644 --- a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java @@ -5,11 +5,11 @@ 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.search.GlobalSearchResult; import ru.soune.nocopy.entity.tarif.TariffInfo; import ru.soune.nocopy.entity.tarif.TariffType; import ru.soune.nocopy.entity.user.User; @@ -24,7 +24,6 @@ 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; @@ -46,8 +45,6 @@ public class MonitoringSearchService { private final ViolationService violationService; - private final SearchProperties searchProperties; - @Transactional public void processFileSearch(FileMonitoringEntity monitoring) throws MessagingException, IOException { monitoring.setLastRun(LocalDateTime.now()); @@ -56,78 +53,36 @@ public class MonitoringSearchService { 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(); + String yandexResponse = searchImageService.searchReverseByPublicUrl( + monitoring.getFile(), "yandex_reverse_image", "visual_matches"); - log.info("Monitoring search settings: useYandex={}, useGoogle={}", useYandex, useGoogle); + List yandexImages = + searchImageService.getAllImagesWithoutPagination(yandexResponse, "visual_matches"); - List allResults = new ArrayList<>(); - - if (useYandex) { - try { - String yandexResponse = searchImageService.searchReverseByPublicUrl( - monitoring.getFile(), "yandex_reverse_image", "visual_matches"); - - List 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 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 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) { + for (YandexSearchResponse.ImageResult imageResult : yandexImages) { violationService.processViolation(imageResult, monitoring.getFile(), null); } +// try { +// String googleResponse = searchImageService.searchReverseByPublicUrl( +// monitoring.getFile(), "google_lens", "exact_matches"); +// +// List googleImages = +// searchImageService.getAllImagesWithoutPagination(googleResponse, "exact_matches"); +// +// +// for (YandexSearchResponse.ImageResult imageResult : googleImages) { +// violationService.processViolation(imageResult, monitoring.getFile(), null); +// } +// +// } catch (TimeoutException | IOException e) { +// log.warn("Google search failed"); +// } + 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(); @@ -136,12 +91,10 @@ public class MonitoringSearchService { 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()); - } finally { updateNextRun(monitoring); monitoringRepository.save(monitoring); diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java index 19b1644..9f202c4 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -1,18 +1,21 @@ package ru.soune.nocopy.service.search; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.vrt.fileprotection.image.ImageCheckResult; 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.entity.violation.Violation; import ru.soune.nocopy.repository.GlobalSearchResultRepository; import ru.soune.nocopy.repository.GlobalSearchTaskRepository; +import ru.soune.nocopy.repository.ViolationRepository; import ru.soune.nocopy.service.tariff.TariffConstants; import ru.soune.nocopy.service.tariff.TariffInfoService; import ru.soune.nocopy.service.violation.ViolationService; @@ -33,13 +36,15 @@ public class GlobalSearchAsyncProcessor { private final SearchImageService searchImageService; + private final ObjectMapper objectMapper; + private final TariffInfoService tariffInfoService; private final GlobalSearchTaskRepository globalSearchTaskRepository; - private final ViolationService violationService; + private final ViolationRepository violationRepository; - private final SearchProperties searchProperties; + private final ViolationService violationService; @Async @Transactional @@ -80,85 +85,52 @@ public class GlobalSearchAsyncProcessor { 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 googleImages = new ArrayList<>(); +// List googleImages = new ArrayList<>(); List yandexImages = new ArrayList<>(); - List allResults = new ArrayList<>(); + List allUniqueImages = new ArrayList<>(); boolean hasTimeout = false; - if (useGoogle) { - try { - String searchResponseGoogle = searchImageService.searchReverseByPublicUrl( - file, "google_lens", "exact_matches"); +// try { +// String searchResponseGoogle = searchImageService.searchReverseByPublicUrl( +// file, "google_lens", "exact_matches"); +// +// googleImages = searchImageService.getAllImagesWithoutPagination(searchResponseGoogle, +// "exact_matches"); +// +// allUniqueImages.addAll(googleImages); +// log.info("Google search OK for file {}", file.getId()); +// +// } 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); +// } - googleImages = searchImageService.getAllImagesWithoutPagination(searchResponseGoogle, - "exact_matches"); + try { + String searchResponseYandex = searchImageService.searchReverseByPublicUrl( + file, "yandex_reverse_image", "visual_matches"); - allResults.addAll(googleImages); - log.info("Google search OK for file {}, found {} results", file.getId(), googleImages.size()); + yandexImages = + searchImageService.getAllImagesWithoutPagination(searchResponseYandex, "visual_matches"); - } 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()); + allUniqueImages.addAll(yandexImages); + log.info("Yandex search OK for file {}", file.getId()); + + } 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); } - if (useYandex) { - try { - String searchResponseYandex = searchImageService.searchReverseByPublicUrl( - file, "yandex_reverse_image", "visual_matches"); +// allUniqueImages = searchImageService.removeDuplicateUrls( +// allUniqueImages.stream() +// .filter(img -> img.getUrl() != null) +// .collect(Collectors.toList()), +// new ArrayList<>()); - 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 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()); - } + allUniqueImages = yandexImages; for (YandexSearchResponse.ImageResult imageResult : allUniqueImages) { violationService.processViolation(imageResult, file, result); @@ -166,15 +138,10 @@ public class GlobalSearchAsyncProcessor { 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()); - if (!allUniqueImages.isEmpty()) { - tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH); - } + tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH); } return result; diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 2aa67dd..c9ad65f 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -8,7 +8,6 @@ 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; @@ -24,15 +23,40 @@ import java.util.concurrent.*; @Slf4j @Service - public class SearchImageService { private final ObjectMapper objectMapper; - private final SearchProperties searchProperties; + private final OkHttpClient yandexHttpClient; + + private final OkHttpClient googleHttpClient; + + public SearchImageService() { + Proxy proxy = new Proxy(Proxy.Type.HTTP, + new InetSocketAddress("193.46.217.94", 3128)); + + this.yandexHttpClient = new OkHttpClient.Builder() + .proxy(proxy) + .connectTimeout(35, TimeUnit.SECONDS) + .writeTimeout(35, TimeUnit.SECONDS) + .readTimeout(35, TimeUnit.SECONDS) + .callTimeout(35, TimeUnit.SECONDS) + .followRedirects(true) + .followSslRedirects(true) + .retryOnConnectionFailure(true) + .build(); + + this.googleHttpClient = new OkHttpClient.Builder() +// .proxy(proxy) + .connectTimeout(35, TimeUnit.SECONDS) + .writeTimeout(35, TimeUnit.SECONDS) + .readTimeout(35, TimeUnit.SECONDS) + .callTimeout(35, TimeUnit.SECONDS) + .followRedirects(true) + .followSslRedirects(true) + .retryOnConnectionFailure(true) + .build(); - public SearchImageService(SearchProperties searchProperties) { - this.searchProperties = searchProperties; this.objectMapper = new ObjectMapper(); } @@ -89,34 +113,12 @@ public class SearchImageService { return mimeType != null && mimeType.startsWith("image"); } - private OkHttpClient createHttpClient(boolean useProxy) { - 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"); } - boolean useProxy = searchProperties.getProxy().isEnabled(); - - OkHttpClient client = createHttpClient(useProxy); + OkHttpClient client = engine.contains("yandex_reverse_image") ? yandexHttpClient : googleHttpClient; HttpUrl url = HttpUrl.parse("https://www.searchapi.io/api/v1/search") .newBuilder() diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 43f3b9a..a0d6d5a 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -107,17 +107,6 @@ searchapi: api-key: ${SEARCHAPI_API_KEY:5jyYZC8jSaxhZTwjMUhwtAXi} reverse-image-url: "https://searchapi.io/api/v1/search" -search: - engines: - yandex: - enabled: true - google: - enabled: false - proxy: - enabled: true - host: "193.46.217.94" - port: 3128 - yookassa: secret-key: test_0Yns_0NHV5GJf6ypJ5HC4NSfnLO8SJkw-1PwrVWsDl4 shop-id: 1276731 From 4930a737627a369e8a5c3f509bd57ee8139743f5 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Mar 2026 16:08:32 +0700 Subject: [PATCH 220/274] dev add violation notion --- .../ru/soune/nocopy/service/search/SearchImageService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index c9ad65f..cb203a0 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -32,11 +32,11 @@ public class SearchImageService { private final OkHttpClient googleHttpClient; public SearchImageService() { - Proxy proxy = new Proxy(Proxy.Type.HTTP, - new InetSocketAddress("193.46.217.94", 3128)); +// Proxy proxy = new Proxy(Proxy.Type.HTTP, +// new InetSocketAddress("193.46.217.94", 3128)); this.yandexHttpClient = new OkHttpClient.Builder() - .proxy(proxy) +// .proxy(proxy) .connectTimeout(35, TimeUnit.SECONDS) .writeTimeout(35, TimeUnit.SECONDS) .readTimeout(35, TimeUnit.SECONDS) From 46c75deacb191228790580da94a22c3e798e2b76 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Mar 2026 16:21:47 +0700 Subject: [PATCH 221/274] dev add violation notion --- .../soune/nocopy/service/search/SearchImageService.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index cb203a0..8e5e4b4 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -36,11 +36,10 @@ public class SearchImageService { // new InetSocketAddress("193.46.217.94", 3128)); this.yandexHttpClient = new OkHttpClient.Builder() -// .proxy(proxy) - .connectTimeout(35, TimeUnit.SECONDS) - .writeTimeout(35, TimeUnit.SECONDS) - .readTimeout(35, TimeUnit.SECONDS) - .callTimeout(35, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .callTimeout(60, TimeUnit.SECONDS) .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) From a42f3e8ed8bf736106f8231860d76e95eee10ae2 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Mar 2026 16:28:23 +0700 Subject: [PATCH 222/274] dev add violation notion --- .../nocopy/service/search/SearchImageService.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 8e5e4b4..e896d8a 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -141,17 +141,18 @@ public class SearchImageService { log.info("SearchAPI response code={}, duration={}ms, engine={}", response.code(), duration, engine); - if (!response.isSuccessful()) { - 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"); } - return body.string(); + String responseBody = body.string(); + + if (!response.isSuccessful()) { + throw new IOException("API error " + response.code() + ": " + responseBody); + } + + return responseBody; } } From 97d6c8317e012b92fb831cb4b75f7fd7088f9155 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Mar 2026 16:35:35 +0700 Subject: [PATCH 223/274] dev add violation notion --- .../soune/nocopy/service/search/SearchImageService.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index e896d8a..401b866 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -32,10 +32,11 @@ public class SearchImageService { private final OkHttpClient googleHttpClient; public SearchImageService() { -// Proxy proxy = new Proxy(Proxy.Type.HTTP, -// new InetSocketAddress("193.46.217.94", 3128)); + Proxy proxy = new Proxy(Proxy.Type.HTTP, + new InetSocketAddress("193.46.217.94", 3128)); this.yandexHttpClient = new OkHttpClient.Builder() +// .proxy(proxy) .connectTimeout(60, TimeUnit.SECONDS) .writeTimeout(60, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) @@ -148,6 +149,10 @@ public class SearchImageService { String responseBody = body.string(); + log.info("responseBody: " + responseBody); + log.info("responseBody: " + responseBody); + log.info("responseBody: " + responseBody); + if (!response.isSuccessful()) { throw new IOException("API error " + response.code() + ": " + responseBody); } From 31aa1c0545ec3bdf1bfad10143672fb12e925a9d Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Mar 2026 16:43:11 +0700 Subject: [PATCH 224/274] dev add violation notion --- .../service/search/SearchImageService.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 401b866..83d93c2 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -114,7 +114,12 @@ public class SearchImageService { } private String callReverseImageApiByUrl(String imageUrl, String engine, String searchType) throws IOException { + log.info("=== START SEARCH API CALL ==="); + log.info("Engine: {}, SearchType: {}", engine, searchType); + log.info("ImageUrl: {}", imageUrl); + if (searchApiKey == null || searchApiKey.isBlank()) { + log.error("SearchAPI key not configured"); throw new IllegalStateException("SearchAPI key not configured"); } @@ -129,6 +134,8 @@ public class SearchImageService { .addQueryParameter("t_", String.valueOf(System.currentTimeMillis())) .build(); + log.info("Request URL: {}", url); + Request request = new Request.Builder() .url(url) .header("Accept", "application/json") @@ -144,20 +151,25 @@ public class SearchImageService { ResponseBody body = response.body(); if (body == null) { + log.error("Response body is null"); throw new IOException("Empty response body"); } String responseBody = body.string(); - log.info("responseBody: " + responseBody); - log.info("responseBody: " + responseBody); - log.info("responseBody: " + responseBody); + log.info("=== RESPONSE BODY LENGTH: {} ===", responseBody.length()); + log.info("=== RESPONSE BODY START: {} ===", + responseBody.length() > 200 ? responseBody.substring(0, 200) : responseBody); if (!response.isSuccessful()) { + log.error("Response not successful: {}", response.code()); throw new IOException("API error " + response.code() + ": " + responseBody); } return responseBody; + } catch (Exception e) { + log.error("Exception in search API call: {}", e.getMessage(), e); + throw e; } } From 655e8f0844cf50dfa9b30167a853da59b1b2d549 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Mar 2026 17:13:22 +0700 Subject: [PATCH 225/274] dev add violation notion --- .../service/search/SearchImageService.java | 452 ++++++++++++++++-- 1 file changed, 407 insertions(+), 45 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 83d93c2..ed334a4 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -37,10 +37,10 @@ public class SearchImageService { this.yandexHttpClient = new OkHttpClient.Builder() // .proxy(proxy) - .connectTimeout(60, TimeUnit.SECONDS) - .writeTimeout(60, TimeUnit.SECONDS) - .readTimeout(60, TimeUnit.SECONDS) - .callTimeout(60, TimeUnit.SECONDS) + .connectTimeout(120, TimeUnit.SECONDS) + .writeTimeout(120, TimeUnit.SECONDS) + .readTimeout(120, TimeUnit.SECONDS) + .callTimeout(120, TimeUnit.SECONDS) .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) @@ -48,10 +48,10 @@ public class SearchImageService { this.googleHttpClient = new OkHttpClient.Builder() // .proxy(proxy) - .connectTimeout(35, TimeUnit.SECONDS) - .writeTimeout(35, TimeUnit.SECONDS) - .readTimeout(35, TimeUnit.SECONDS) - .callTimeout(35, TimeUnit.SECONDS) + .connectTimeout(120, TimeUnit.SECONDS) + .writeTimeout(120, TimeUnit.SECONDS) + .readTimeout(120, TimeUnit.SECONDS) + .callTimeout(120, TimeUnit.SECONDS) .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) @@ -103,7 +103,7 @@ public class SearchImageService { try { return callReverseImageApiByUrl(publicUrl, engine, searchType); } catch (SocketTimeoutException e) { - log.error("Yandex search timeout after {}", fileEntity.getId()); + log.error("Search timeout for file: {}", fileEntity.getId()); throw new TimeoutException("Search timeout"); } } @@ -131,6 +131,7 @@ public class SearchImageService { .addQueryParameter("api_key", searchApiKey) .addQueryParameter("url", imageUrl) .addQueryParameter("search_type", searchType) + .addQueryParameter("num", "10") .addQueryParameter("t_", String.valueOf(System.currentTimeMillis())) .build(); @@ -155,62 +156,129 @@ public class SearchImageService { throw new IOException("Empty response body"); } - String responseBody = body.string(); + String responseBody = readWithTimeout(body, 60000); - log.info("=== RESPONSE BODY LENGTH: {} ===", responseBody.length()); - log.info("=== RESPONSE BODY START: {} ===", - responseBody.length() > 200 ? responseBody.substring(0, 200) : responseBody); + log.info("=== RESPONSE BODY LENGTH: {} bytes received ===", responseBody.length()); + + if (responseBody.length() < 1000) { + log.info("=== RESPONSE BODY: {} ===", responseBody); + } if (!response.isSuccessful()) { log.error("Response not successful: {}", response.code()); - throw new IOException("API error " + response.code() + ": " + responseBody); + throw new IOException("API error " + response.code() + ": " + + (responseBody.length() > 200 ? responseBody.substring(0, 200) : responseBody)); + } + + if (responseBody.length() > 0 && !responseBody.endsWith("}")) { + responseBody = fixTruncatedJson(responseBody); + log.info("Fixed truncated JSON, new length: {}", responseBody.length()); } return responseBody; + } catch (InterruptedIOException e) { + log.warn("Timeout/Interrupted during reading for engine: {}", engine); + throw new IOException("Partial response received", e); } catch (Exception e) { log.error("Exception in search API call: {}", e.getMessage(), e); throw e; } } - public List getAllImagesWithoutPagination(String searchApiJson, String findType) - throws IOException { - JsonNode root = objectMapper.readTree(searchApiJson); - JsonNode matches = root.path(findType); + private String readWithTimeout(ResponseBody body, long timeoutMs) throws IOException { + StringBuilder result = new StringBuilder(); + long startTime = System.currentTimeMillis(); - List allImages = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(body.charStream())) { + char[] buffer = new char[8192]; + int read; - if (matches.isArray()) { - for (JsonNode match : matches) { - YandexSearchResponse.ImageResult result = mapImageResult(match); + while ((read = reader.read(buffer)) != -1) { + result.append(buffer, 0, read); - if ("exact_matches".equals(findType)) { - JsonNode thumbnail = match.path("thumbnail"); - 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"); - if (!imageNode.isMissingNode()) { - String directUrl = imageNode.path("link").asText(); - if (directUrl != null && !directUrl.isBlank()) { - result.setUrl(directUrl); - } - } - } - - if (result.getUrl() != null && !result.getUrl().isBlank()) { - allImages.add(result); + if (System.currentTimeMillis() - startTime > timeoutMs) { + log.warn("Partial read timeout after {} ms, received {} bytes", + timeoutMs, result.length()); + break; } } } - return allImages; + return result.toString(); + } + + private String fixTruncatedJson(String partialJson) { + if (partialJson == null || partialJson.isEmpty()) { + return "{}"; + } + + String[] matchTypes = {"\"visual_matches\":", "\"exact_matches\":"}; + + for (String matchType : matchTypes) { + int matchesStart = partialJson.indexOf(matchType); + if (matchesStart > 0) { + int lastCompleteMatch = partialJson.lastIndexOf("},"); + int arrayEnd = partialJson.lastIndexOf("]"); + + if (lastCompleteMatch > matchesStart && arrayEnd < lastCompleteMatch) { + return partialJson.substring(0, lastCompleteMatch + 1) + "]}"; + } + break; + } + } + + if (!partialJson.endsWith("}")) { + return partialJson + "}"; + } + + return partialJson; + } + + public List getAllImagesWithoutPagination(String searchApiJson, String findType) + throws IOException { + try { + JsonNode root = objectMapper.readTree(searchApiJson); + JsonNode matches = root.path(findType); + + List allImages = new ArrayList<>(); + + if (matches.isArray()) { + for (JsonNode match : matches) { + YandexSearchResponse.ImageResult result = mapImageResult(match); + + if ("exact_matches".equals(findType)) { + JsonNode thumbnail = match.path("thumbnail"); + 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"); + if (!imageNode.isMissingNode()) { + String directUrl = imageNode.path("link").asText(); + if (directUrl != null && !directUrl.isBlank()) { + result.setUrl(directUrl); + } + } + } + + if (result.getUrl() != null && !result.getUrl().isBlank()) { + allImages.add(result); + } + } + } + + log.info("Parsed {} images from response (findType: {})", allImages.size(), findType); + return allImages; + + } catch (Exception e) { + log.error("Failed to parse JSON response: {}", e.getMessage()); + return new ArrayList<>(); + } } public List removeDuplicateUrls( @@ -315,3 +383,297 @@ public class SearchImageService { return source; } } +//@Slf4j +//@Service +//public class SearchImageService { +// +// private final ObjectMapper objectMapper; +// +// private final OkHttpClient yandexHttpClient; +// +// private final OkHttpClient googleHttpClient; +// +// public SearchImageService() { +// Proxy proxy = new Proxy(Proxy.Type.HTTP, +// new InetSocketAddress("193.46.217.94", 3128)); +// +// this.yandexHttpClient = new OkHttpClient.Builder() +//// .proxy(proxy) +// .connectTimeout(60, TimeUnit.SECONDS) +// .writeTimeout(60, TimeUnit.SECONDS) +// .readTimeout(60, TimeUnit.SECONDS) +// .callTimeout(60, TimeUnit.SECONDS) +// .followRedirects(true) +// .followSslRedirects(true) +// .retryOnConnectionFailure(true) +// .build(); +// +// this.googleHttpClient = new OkHttpClient.Builder() +//// .proxy(proxy) +// .connectTimeout(35, TimeUnit.SECONDS) +// .writeTimeout(35, TimeUnit.SECONDS) +// .readTimeout(35, TimeUnit.SECONDS) +// .callTimeout(35, TimeUnit.SECONDS) +// .followRedirects(true) +// .followSslRedirects(true) +// .retryOnConnectionFailure(true) +// .build(); +// +// this.objectMapper = new ObjectMapper(); +// } +// +// @Value("${yandex.api-key}") +// private String apiKey; +// +// @Value("${yandex.folder-id}") +// private String folderId; +// +// @Value("${yandex.search-url}") +// private String searchUrl; +// +// @Value("${searchapi.api-key:}") +// private String searchApiKey; +// +// @Value("${server.baseurl}") +// private String appBaseUrl; +// +// @PostConstruct +// public void init() { +// objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); +// objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); +// } +// +// public String searchReverseByPublicUrl(FileEntity fileEntity, String engine, String searchType) +// throws IOException, TimeoutException { +// +// if (!isImageFile(fileEntity)) { +// throw new NotValidFieldException( +// "File not image", +// new BaseResponse( +// 20007, +// MessageCode.INVALID_FIELD.getCode(), +// MessageCode.INVALID_FIELD.getDescription(), +// Map.of("file_type", fileEntity.getMimeType()) +// ) +// ); +// } +// +// String publicUrl = String.format("%s/api/files/public/%s", appBaseUrl, fileEntity.getId()); +// +// log.info("Searching reverse for image: {}", publicUrl); +// +// try { +// return callReverseImageApiByUrl(publicUrl, engine, searchType); +// } catch (SocketTimeoutException e) { +// log.error("Yandex search timeout after {}", fileEntity.getId()); +// throw new TimeoutException("Search timeout"); +// } +// } +// +// private boolean isImageFile(FileEntity fileEntity) { +// String mimeType = fileEntity.getMimeType(); +// return mimeType != null && mimeType.startsWith("image"); +// } +// +// private String callReverseImageApiByUrl(String imageUrl, String engine, String searchType) throws IOException { +// log.info("=== START SEARCH API CALL ==="); +// log.info("Engine: {}, SearchType: {}", engine, searchType); +// log.info("ImageUrl: {}", imageUrl); +// +// if (searchApiKey == null || searchApiKey.isBlank()) { +// log.error("SearchAPI key not configured"); +// throw new IllegalStateException("SearchAPI key not configured"); +// } +// +// OkHttpClient client = engine.contains("yandex_reverse_image") ? yandexHttpClient : googleHttpClient; +// +// 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("t_", String.valueOf(System.currentTimeMillis())) +// .build(); +// +// log.info("Request URL: {}", url); +// +// Request request = new Request.Builder() +// .url(url) +// .header("Accept", "application/json") +// .header("User-Agent", "Mozilla/5.0") +// .build(); +// +// long start = System.currentTimeMillis(); +// +// try (Response response = client.newCall(request).execute()) { +// long duration = System.currentTimeMillis() - start; +// log.info("SearchAPI response code={}, duration={}ms, engine={}", +// response.code(), duration, engine); +// +// ResponseBody body = response.body(); +// if (body == null) { +// log.error("Response body is null"); +// throw new IOException("Empty response body"); +// } +// +// String responseBody = body.string(); +// +// log.info("=== RESPONSE BODY LENGTH: {} ===", responseBody.length()); +// log.info("=== RESPONSE BODY START: {} ===", +// responseBody.length() > 200 ? responseBody.substring(0, 200) : responseBody); +// +// if (!response.isSuccessful()) { +// log.error("Response not successful: {}", response.code()); +// throw new IOException("API error " + response.code() + ": " + responseBody); +// } +// +// return responseBody; +// } catch (Exception e) { +// log.error("Exception in search API call: {}", e.getMessage(), e); +// throw e; +// } +// } +// +// public List getAllImagesWithoutPagination(String searchApiJson, String findType) +// throws IOException { +// JsonNode root = objectMapper.readTree(searchApiJson); +// JsonNode matches = root.path(findType); +// +// List allImages = new ArrayList<>(); +// +// if (matches.isArray()) { +// for (JsonNode match : matches) { +// YandexSearchResponse.ImageResult result = mapImageResult(match); +// +// if ("exact_matches".equals(findType)) { +// JsonNode thumbnail = match.path("thumbnail"); +// 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"); +// if (!imageNode.isMissingNode()) { +// String directUrl = imageNode.path("link").asText(); +// if (directUrl != null && !directUrl.isBlank()) { +// result.setUrl(directUrl); +// } +// } +// } +// +// if (result.getUrl() != null && !result.getUrl().isBlank()) { +// allImages.add(result); +// } +// } +// } +// +// return allImages; +// } +// +// public List removeDuplicateUrls( +// List yandexImages, +// List googleImages) { +// +// Map uniqueByUrl = new LinkedHashMap<>(); +// +// for (YandexSearchResponse.ImageResult image : googleImages) { +// String normalizedUrl = normalizeUrl(image.getUrl()); +// uniqueByUrl.putIfAbsent(normalizedUrl, image); +// } +// +// for (YandexSearchResponse.ImageResult image : yandexImages) { +// String normalizedUrl = normalizeUrl(image.getUrl()); +// uniqueByUrl.putIfAbsent(normalizedUrl, image); +// } +// +// return new ArrayList<>(uniqueByUrl.values()); +// } +// +// public List paginateResults( +// List allResults, +// int page, +// int pageSize) { +// +// if (allResults.isEmpty()) { +// return new ArrayList<>(); +// } +// +// int totalPages = (int) Math.ceil((double) allResults.size() / pageSize); +// +// if (page > totalPages) { +// return new ArrayList<>(); +// } +// +// int startIndex = (page - 1) * pageSize; +// int endIndex = Math.min(startIndex + pageSize, allResults.size()); +// +// return new ArrayList<>(allResults.subList(startIndex, endIndex)); +// } +// +// private String normalizeUrl(String url) { +// if (url == null) return null; +// +// try { +// if (url.startsWith("data:image")) { +// return url.length() > 100 ? url.substring(0, 100) : url; +// } +// +// java.net.URI uri = new java.net.URI(url); +// String host = uri.getHost(); +// if (host != null) { +// host = host.toLowerCase().replaceFirst("^www\\.", ""); +// } +// +// String path = uri.getPath(); +// if (path != null && path.length() > 1 && path.endsWith("/")) { +// path = path.substring(0, path.length() - 1); +// } +// +// return (host != null ? host : "") + (path != null ? path : ""); +// } catch (Exception e) { +// return url.toLowerCase() +// .replaceFirst("^(https?://)?(www\\.)?", "") +// .replaceFirst("/$", ""); +// } +// } +// +// private YandexSearchResponse.ImageResult mapImageResult(JsonNode match) { +// +// YandexSearchResponse.ImageResult result = +// new YandexSearchResponse.ImageResult(); +// +// JsonNode imageNode = match.path("image"); +// if (imageNode.isObject()) { +// result.setUrl(imageNode.path("link").asText()); +// result.setWidth(imageNode.path("width").asInt(0)); +// result.setHeight(imageNode.path("height").asInt(0)); +// } +// +// result.setPageUrl(match.path("link").asText()); +// result.setPageTitle(match.path("title").asText()); +// +// String source = match.path("source").asText(); +// result.setHost(extractHostFromSource(source)); +// +// return result; +// } +// +// private String extractHostFromSource(String source) { +// if (source == null || source.isEmpty()) { +// return ""; +// } +// source = source.replaceFirst("^(https?://)?(www\\.)?", ""); +// +// int slashIndex = source.indexOf('/'); +// if (slashIndex > 0) { +// return source.substring(0, slashIndex); +// } +// +// return source; +// } +//} From 2bf5e99fbbdb0b07f676c2c322ec8f5349b16886 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Mar 2026 17:33:09 +0700 Subject: [PATCH 226/274] dev add violation notion --- .../service/search/SearchImageService.java | 442 ++---------------- 1 file changed, 40 insertions(+), 402 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index ed334a4..83d93c2 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -37,10 +37,10 @@ public class SearchImageService { this.yandexHttpClient = new OkHttpClient.Builder() // .proxy(proxy) - .connectTimeout(120, TimeUnit.SECONDS) - .writeTimeout(120, TimeUnit.SECONDS) - .readTimeout(120, TimeUnit.SECONDS) - .callTimeout(120, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .callTimeout(60, TimeUnit.SECONDS) .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) @@ -48,10 +48,10 @@ public class SearchImageService { this.googleHttpClient = new OkHttpClient.Builder() // .proxy(proxy) - .connectTimeout(120, TimeUnit.SECONDS) - .writeTimeout(120, TimeUnit.SECONDS) - .readTimeout(120, TimeUnit.SECONDS) - .callTimeout(120, TimeUnit.SECONDS) + .connectTimeout(35, TimeUnit.SECONDS) + .writeTimeout(35, TimeUnit.SECONDS) + .readTimeout(35, TimeUnit.SECONDS) + .callTimeout(35, TimeUnit.SECONDS) .followRedirects(true) .followSslRedirects(true) .retryOnConnectionFailure(true) @@ -103,7 +103,7 @@ public class SearchImageService { try { return callReverseImageApiByUrl(publicUrl, engine, searchType); } catch (SocketTimeoutException e) { - log.error("Search timeout for file: {}", fileEntity.getId()); + log.error("Yandex search timeout after {}", fileEntity.getId()); throw new TimeoutException("Search timeout"); } } @@ -131,7 +131,6 @@ public class SearchImageService { .addQueryParameter("api_key", searchApiKey) .addQueryParameter("url", imageUrl) .addQueryParameter("search_type", searchType) - .addQueryParameter("num", "10") .addQueryParameter("t_", String.valueOf(System.currentTimeMillis())) .build(); @@ -156,129 +155,62 @@ public class SearchImageService { throw new IOException("Empty response body"); } - String responseBody = readWithTimeout(body, 60000); + String responseBody = body.string(); - log.info("=== RESPONSE BODY LENGTH: {} bytes received ===", responseBody.length()); - - if (responseBody.length() < 1000) { - log.info("=== RESPONSE BODY: {} ===", responseBody); - } + log.info("=== RESPONSE BODY LENGTH: {} ===", responseBody.length()); + log.info("=== RESPONSE BODY START: {} ===", + responseBody.length() > 200 ? responseBody.substring(0, 200) : responseBody); if (!response.isSuccessful()) { log.error("Response not successful: {}", response.code()); - throw new IOException("API error " + response.code() + ": " + - (responseBody.length() > 200 ? responseBody.substring(0, 200) : responseBody)); - } - - if (responseBody.length() > 0 && !responseBody.endsWith("}")) { - responseBody = fixTruncatedJson(responseBody); - log.info("Fixed truncated JSON, new length: {}", responseBody.length()); + throw new IOException("API error " + response.code() + ": " + responseBody); } return responseBody; - } catch (InterruptedIOException e) { - log.warn("Timeout/Interrupted during reading for engine: {}", engine); - throw new IOException("Partial response received", e); } catch (Exception e) { log.error("Exception in search API call: {}", e.getMessage(), e); throw e; } } - private String readWithTimeout(ResponseBody body, long timeoutMs) throws IOException { - StringBuilder result = new StringBuilder(); - long startTime = System.currentTimeMillis(); - - try (BufferedReader reader = new BufferedReader(body.charStream())) { - char[] buffer = new char[8192]; - int read; - - while ((read = reader.read(buffer)) != -1) { - result.append(buffer, 0, read); - - if (System.currentTimeMillis() - startTime > timeoutMs) { - log.warn("Partial read timeout after {} ms, received {} bytes", - timeoutMs, result.length()); - break; - } - } - } - - return result.toString(); - } - - private String fixTruncatedJson(String partialJson) { - if (partialJson == null || partialJson.isEmpty()) { - return "{}"; - } - - String[] matchTypes = {"\"visual_matches\":", "\"exact_matches\":"}; - - for (String matchType : matchTypes) { - int matchesStart = partialJson.indexOf(matchType); - if (matchesStart > 0) { - int lastCompleteMatch = partialJson.lastIndexOf("},"); - int arrayEnd = partialJson.lastIndexOf("]"); - - if (lastCompleteMatch > matchesStart && arrayEnd < lastCompleteMatch) { - return partialJson.substring(0, lastCompleteMatch + 1) + "]}"; - } - break; - } - } - - if (!partialJson.endsWith("}")) { - return partialJson + "}"; - } - - return partialJson; - } - public List getAllImagesWithoutPagination(String searchApiJson, String findType) throws IOException { - try { - JsonNode root = objectMapper.readTree(searchApiJson); - JsonNode matches = root.path(findType); + JsonNode root = objectMapper.readTree(searchApiJson); + JsonNode matches = root.path(findType); - List allImages = new ArrayList<>(); + List allImages = new ArrayList<>(); - if (matches.isArray()) { - for (JsonNode match : matches) { - YandexSearchResponse.ImageResult result = mapImageResult(match); + if (matches.isArray()) { + for (JsonNode match : matches) { + YandexSearchResponse.ImageResult result = mapImageResult(match); - if ("exact_matches".equals(findType)) { - JsonNode thumbnail = match.path("thumbnail"); - 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"); - if (!imageNode.isMissingNode()) { - String directUrl = imageNode.path("link").asText(); - if (directUrl != null && !directUrl.isBlank()) { - result.setUrl(directUrl); - } + if ("exact_matches".equals(findType)) { + JsonNode thumbnail = match.path("thumbnail"); + if (!thumbnail.isMissingNode()) { + String thumbnailUrl = thumbnail.asText(); + if (thumbnailUrl.startsWith("data:image")) { + result.setUrl(thumbnailUrl); + } else if (!thumbnailUrl.isBlank()) { + result.setUrl(thumbnailUrl); } } - if (result.getUrl() != null && !result.getUrl().isBlank()) { - allImages.add(result); + JsonNode imageNode = match.path("image"); + if (!imageNode.isMissingNode()) { + String directUrl = imageNode.path("link").asText(); + if (directUrl != null && !directUrl.isBlank()) { + result.setUrl(directUrl); + } } } + + if (result.getUrl() != null && !result.getUrl().isBlank()) { + allImages.add(result); + } } - - log.info("Parsed {} images from response (findType: {})", allImages.size(), findType); - return allImages; - - } catch (Exception e) { - log.error("Failed to parse JSON response: {}", e.getMessage()); - return new ArrayList<>(); } + + return allImages; } public List removeDuplicateUrls( @@ -383,297 +315,3 @@ public class SearchImageService { return source; } } -//@Slf4j -//@Service -//public class SearchImageService { -// -// private final ObjectMapper objectMapper; -// -// private final OkHttpClient yandexHttpClient; -// -// private final OkHttpClient googleHttpClient; -// -// public SearchImageService() { -// Proxy proxy = new Proxy(Proxy.Type.HTTP, -// new InetSocketAddress("193.46.217.94", 3128)); -// -// this.yandexHttpClient = new OkHttpClient.Builder() -//// .proxy(proxy) -// .connectTimeout(60, TimeUnit.SECONDS) -// .writeTimeout(60, TimeUnit.SECONDS) -// .readTimeout(60, TimeUnit.SECONDS) -// .callTimeout(60, TimeUnit.SECONDS) -// .followRedirects(true) -// .followSslRedirects(true) -// .retryOnConnectionFailure(true) -// .build(); -// -// this.googleHttpClient = new OkHttpClient.Builder() -//// .proxy(proxy) -// .connectTimeout(35, TimeUnit.SECONDS) -// .writeTimeout(35, TimeUnit.SECONDS) -// .readTimeout(35, TimeUnit.SECONDS) -// .callTimeout(35, TimeUnit.SECONDS) -// .followRedirects(true) -// .followSslRedirects(true) -// .retryOnConnectionFailure(true) -// .build(); -// -// this.objectMapper = new ObjectMapper(); -// } -// -// @Value("${yandex.api-key}") -// private String apiKey; -// -// @Value("${yandex.folder-id}") -// private String folderId; -// -// @Value("${yandex.search-url}") -// private String searchUrl; -// -// @Value("${searchapi.api-key:}") -// private String searchApiKey; -// -// @Value("${server.baseurl}") -// private String appBaseUrl; -// -// @PostConstruct -// public void init() { -// objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); -// objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); -// } -// -// public String searchReverseByPublicUrl(FileEntity fileEntity, String engine, String searchType) -// throws IOException, TimeoutException { -// -// if (!isImageFile(fileEntity)) { -// throw new NotValidFieldException( -// "File not image", -// new BaseResponse( -// 20007, -// MessageCode.INVALID_FIELD.getCode(), -// MessageCode.INVALID_FIELD.getDescription(), -// Map.of("file_type", fileEntity.getMimeType()) -// ) -// ); -// } -// -// String publicUrl = String.format("%s/api/files/public/%s", appBaseUrl, fileEntity.getId()); -// -// log.info("Searching reverse for image: {}", publicUrl); -// -// try { -// return callReverseImageApiByUrl(publicUrl, engine, searchType); -// } catch (SocketTimeoutException e) { -// log.error("Yandex search timeout after {}", fileEntity.getId()); -// throw new TimeoutException("Search timeout"); -// } -// } -// -// private boolean isImageFile(FileEntity fileEntity) { -// String mimeType = fileEntity.getMimeType(); -// return mimeType != null && mimeType.startsWith("image"); -// } -// -// private String callReverseImageApiByUrl(String imageUrl, String engine, String searchType) throws IOException { -// log.info("=== START SEARCH API CALL ==="); -// log.info("Engine: {}, SearchType: {}", engine, searchType); -// log.info("ImageUrl: {}", imageUrl); -// -// if (searchApiKey == null || searchApiKey.isBlank()) { -// log.error("SearchAPI key not configured"); -// throw new IllegalStateException("SearchAPI key not configured"); -// } -// -// OkHttpClient client = engine.contains("yandex_reverse_image") ? yandexHttpClient : googleHttpClient; -// -// 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("t_", String.valueOf(System.currentTimeMillis())) -// .build(); -// -// log.info("Request URL: {}", url); -// -// Request request = new Request.Builder() -// .url(url) -// .header("Accept", "application/json") -// .header("User-Agent", "Mozilla/5.0") -// .build(); -// -// long start = System.currentTimeMillis(); -// -// try (Response response = client.newCall(request).execute()) { -// long duration = System.currentTimeMillis() - start; -// log.info("SearchAPI response code={}, duration={}ms, engine={}", -// response.code(), duration, engine); -// -// ResponseBody body = response.body(); -// if (body == null) { -// log.error("Response body is null"); -// throw new IOException("Empty response body"); -// } -// -// String responseBody = body.string(); -// -// log.info("=== RESPONSE BODY LENGTH: {} ===", responseBody.length()); -// log.info("=== RESPONSE BODY START: {} ===", -// responseBody.length() > 200 ? responseBody.substring(0, 200) : responseBody); -// -// if (!response.isSuccessful()) { -// log.error("Response not successful: {}", response.code()); -// throw new IOException("API error " + response.code() + ": " + responseBody); -// } -// -// return responseBody; -// } catch (Exception e) { -// log.error("Exception in search API call: {}", e.getMessage(), e); -// throw e; -// } -// } -// -// public List getAllImagesWithoutPagination(String searchApiJson, String findType) -// throws IOException { -// JsonNode root = objectMapper.readTree(searchApiJson); -// JsonNode matches = root.path(findType); -// -// List allImages = new ArrayList<>(); -// -// if (matches.isArray()) { -// for (JsonNode match : matches) { -// YandexSearchResponse.ImageResult result = mapImageResult(match); -// -// if ("exact_matches".equals(findType)) { -// JsonNode thumbnail = match.path("thumbnail"); -// 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"); -// if (!imageNode.isMissingNode()) { -// String directUrl = imageNode.path("link").asText(); -// if (directUrl != null && !directUrl.isBlank()) { -// result.setUrl(directUrl); -// } -// } -// } -// -// if (result.getUrl() != null && !result.getUrl().isBlank()) { -// allImages.add(result); -// } -// } -// } -// -// return allImages; -// } -// -// public List removeDuplicateUrls( -// List yandexImages, -// List googleImages) { -// -// Map uniqueByUrl = new LinkedHashMap<>(); -// -// for (YandexSearchResponse.ImageResult image : googleImages) { -// String normalizedUrl = normalizeUrl(image.getUrl()); -// uniqueByUrl.putIfAbsent(normalizedUrl, image); -// } -// -// for (YandexSearchResponse.ImageResult image : yandexImages) { -// String normalizedUrl = normalizeUrl(image.getUrl()); -// uniqueByUrl.putIfAbsent(normalizedUrl, image); -// } -// -// return new ArrayList<>(uniqueByUrl.values()); -// } -// -// public List paginateResults( -// List allResults, -// int page, -// int pageSize) { -// -// if (allResults.isEmpty()) { -// return new ArrayList<>(); -// } -// -// int totalPages = (int) Math.ceil((double) allResults.size() / pageSize); -// -// if (page > totalPages) { -// return new ArrayList<>(); -// } -// -// int startIndex = (page - 1) * pageSize; -// int endIndex = Math.min(startIndex + pageSize, allResults.size()); -// -// return new ArrayList<>(allResults.subList(startIndex, endIndex)); -// } -// -// private String normalizeUrl(String url) { -// if (url == null) return null; -// -// try { -// if (url.startsWith("data:image")) { -// return url.length() > 100 ? url.substring(0, 100) : url; -// } -// -// java.net.URI uri = new java.net.URI(url); -// String host = uri.getHost(); -// if (host != null) { -// host = host.toLowerCase().replaceFirst("^www\\.", ""); -// } -// -// String path = uri.getPath(); -// if (path != null && path.length() > 1 && path.endsWith("/")) { -// path = path.substring(0, path.length() - 1); -// } -// -// return (host != null ? host : "") + (path != null ? path : ""); -// } catch (Exception e) { -// return url.toLowerCase() -// .replaceFirst("^(https?://)?(www\\.)?", "") -// .replaceFirst("/$", ""); -// } -// } -// -// private YandexSearchResponse.ImageResult mapImageResult(JsonNode match) { -// -// YandexSearchResponse.ImageResult result = -// new YandexSearchResponse.ImageResult(); -// -// JsonNode imageNode = match.path("image"); -// if (imageNode.isObject()) { -// result.setUrl(imageNode.path("link").asText()); -// result.setWidth(imageNode.path("width").asInt(0)); -// result.setHeight(imageNode.path("height").asInt(0)); -// } -// -// result.setPageUrl(match.path("link").asText()); -// result.setPageTitle(match.path("title").asText()); -// -// String source = match.path("source").asText(); -// result.setHost(extractHostFromSource(source)); -// -// return result; -// } -// -// private String extractHostFromSource(String source) { -// if (source == null || source.isEmpty()) { -// return ""; -// } -// source = source.replaceFirst("^(https?://)?(www\\.)?", ""); -// -// int slashIndex = source.indexOf('/'); -// if (slashIndex > 0) { -// return source.substring(0, slashIndex); -// } -// -// return source; -// } -//} From 29cdbb887f7c7224df8fdee4d2bdcf95eaa5369b Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Mar 2026 15:26:20 +0700 Subject: [PATCH 227/274] dev add violation notion --- .../search/SearchProperties.java | 26 ++++ .../controller/SearchSettingsController.java | 58 ++++++++ .../dto/search/config/SearchSettingsDto.java | 11 ++ .../handler/ImageFoundRequestHandler.java | 102 ++++++++------ .../service/file/FileCleanupService.java | 15 --- .../monitoring/MonitoringSearchService.java | 93 +++++++++---- .../search/GlobalSearchAsyncProcessor.java | 125 +++++++++++------- .../service/search/SearchImageService.java | 87 +++++------- src/main/resources/application.yaml | 11 ++ 9 files changed, 354 insertions(+), 174 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/configuration/search/SearchProperties.java create mode 100644 src/main/java/ru/soune/nocopy/controller/SearchSettingsController.java create mode 100644 src/main/java/ru/soune/nocopy/dto/search/config/SearchSettingsDto.java diff --git a/src/main/java/ru/soune/nocopy/configuration/search/SearchProperties.java b/src/main/java/ru/soune/nocopy/configuration/search/SearchProperties.java new file mode 100644 index 0000000..3ca91ff --- /dev/null +++ b/src/main/java/ru/soune/nocopy/configuration/search/SearchProperties.java @@ -0,0 +1,26 @@ +package ru.soune.nocopy.configuration.search; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import java.util.Map; + +@Data +@Component +@ConfigurationProperties(prefix = "search") +public class SearchProperties { + private Map engines; + private ProxyConfig proxy; + + @Data + public static class EngineConfig { + private boolean enabled; + } + + @Data + public static class ProxyConfig { + private boolean enabled; + private String host; + private int port; + } +} diff --git a/src/main/java/ru/soune/nocopy/controller/SearchSettingsController.java b/src/main/java/ru/soune/nocopy/controller/SearchSettingsController.java new file mode 100644 index 0000000..d18596a --- /dev/null +++ b/src/main/java/ru/soune/nocopy/controller/SearchSettingsController.java @@ -0,0 +1,58 @@ +package ru.soune.nocopy.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.soune.nocopy.configuration.search.SearchProperties; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.search.config.SearchSettingsDto; + +import java.util.Map; + + +@Slf4j +@RestController +@RequestMapping("/api/search/settings") +@RequiredArgsConstructor +public class SearchSettingsController { + + private final SearchProperties searchProperties; + + @GetMapping + public ResponseEntity getSettings() { + SearchSettingsDto dto = new SearchSettingsDto(); + + dto.setEngines(Map.of( + "yandex", searchProperties.getEngines().get("yandex").isEnabled(), + "google", searchProperties.getEngines().get("google").isEnabled())); + + dto.setProxyEnabled(searchProperties.getProxy().isEnabled()); + + return ResponseEntity.ok().body(BaseResponse.builder() + .messageDesc("Success") + .messageBody(dto) + .build()); + } + + @PutMapping + public ResponseEntity updateSettings(@RequestBody SearchSettingsDto settings) { + if (settings.getEngines() != null) { + settings.getEngines().forEach((key, value) -> { + if (searchProperties.getEngines().containsKey(key)) { + searchProperties.getEngines().get(key).setEnabled(value); + } + }); + } + + if (settings.getProxyEnabled() != null) { + searchProperties.getProxy().setEnabled(settings.getProxyEnabled()); + } + + log.info("Settings updated: {}", settings); + + return ResponseEntity.ok().body(BaseResponse.builder() + .messageDesc("Settings updated") + .build()); + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/search/config/SearchSettingsDto.java b/src/main/java/ru/soune/nocopy/dto/search/config/SearchSettingsDto.java new file mode 100644 index 0000000..ffacfa0 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/search/config/SearchSettingsDto.java @@ -0,0 +1,11 @@ +package ru.soune.nocopy.dto.search.config; + + +import lombok.Data; +import java.util.Map; + +@Data +public class SearchSettingsDto { + private Map engines; + private Boolean proxyEnabled; +} diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 2f42428..51bb0fa 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import ru.soune.nocopy.configuration.search.SearchProperties; import ru.soune.nocopy.dto.BaseRequest; import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.MessageCode; @@ -36,6 +37,8 @@ public class ImageFoundRequestHandler implements RequestHandler { private final TariffInfoService tariffInfoService; + private final SearchProperties searchProperties; + @Override public BaseResponse handle(BaseRequest request) throws Exception { ImageSearchRequest imageSearchRequest = objectMapper.convertValue(request.getMessageBody(), @@ -47,47 +50,72 @@ public class ImageFoundRequestHandler implements RequestHandler { .orElseThrow(() -> { throw new NotValidFieldException("File not found", new BaseResponse(20007, MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(), - Map.of("fileId",fileId))); - }); + Map.of("fileId", fileId)));}); - String searchResponseYandex; -// String searchResponseGoogle; - List allYandexImages = new ArrayList<>(); -// List allGoogleImages = new ArrayList<>(); - List allUniqueImages; + List allYandexImages = new ArrayList<>(); + List allGoogleImages = new ArrayList<>(); boolean hasTimeout = false; -// -// try { -// searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", -// "exact_matches"); -// log.info("searchResponseGoogle OK,{}", searchResponseGoogle); -// allGoogleImages = searchImageService.getAllImagesWithoutPagination( -// searchResponseGoogle, "exact_matches"); -// -// } catch (TimeoutException e) { -// log.warn("Google search timeout for file {}", fileId); -// hasTimeout = true; -// } catch (IOException e) { -// log.error("Google search failed for file {}", fileId, e); -// } - try { - searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", - "visual_matches"); - log.info("searchResponseYandex OK"); - allYandexImages = searchImageService.getAllImagesWithoutPagination( - searchResponseYandex, "visual_matches"); - } catch (TimeoutException e) { - log.warn("Yandex search timeout for file {}", fileId); - hasTimeout = true; - } catch (IOException e) { - log.error("Yandex search failed for file {}", fileId, e); - } + boolean useGoogle = searchProperties.getEngines().getOrDefault("google", + new SearchProperties.EngineConfig()).isEnabled(); + boolean useYandex = searchProperties.getEngines().getOrDefault("yandex", + new SearchProperties.EngineConfig()).isEnabled(); -// allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); -// allUniqueImages = allGoogleImages; - allUniqueImages = allYandexImages; - log.info("allUniqueImages OK: {} images", allUniqueImages.size()); + log.info("Search settings: useGoogle={}, useYandex={}", useGoogle, useYandex); + + if (useGoogle) { + try { + String searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", + "exact_matches"); + log.info("searchResponseGoogle OK"); + allGoogleImages = searchImageService.getAllImagesWithoutPagination( + searchResponseGoogle, "exact_matches"); + } catch (TimeoutException e) { + log.warn("Google search timeout for file {}", fileId); + hasTimeout = true; + } catch (IOException e) { + log.error("Google search failed for file {}", fileId, e); + } + } else { + log.info("Google search is disabled by settings"); + } + + if (useYandex) { + try { + String searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", + "visual_matches"); + log.info("searchResponseYandex OK"); + allYandexImages = searchImageService.getAllImagesWithoutPagination( + searchResponseYandex, "visual_matches"); + } catch (TimeoutException e) { + log.warn("Yandex search timeout for file {}", fileId); + hasTimeout = true; + } catch (IOException e) { + log.error("Yandex search failed for file {}", fileId, e); + } + } else { + log.info("Yandex search is disabled by settings"); + } + + if (allYandexImages.isEmpty() && allGoogleImages.isEmpty()) { + log.warn("No results from any search engine"); + String errorMessage = useGoogle || useYandex ? "No results found" : "All search engines are disabled"; + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + errorMessage, new YandexSearchResponse()); + } + + List allUniqueImages; + if (useGoogle && useYandex) { + allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); + log.info("Merged results from both engines: {} unique images", allUniqueImages.size()); + } else if (useGoogle) { + allUniqueImages = allGoogleImages; + log.info("Results only from Google: {} images", allUniqueImages.size()); + } else { + allUniqueImages = allYandexImages; + log.info("Results only from Yandex: {} images", allUniqueImages.size()); + } tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); diff --git a/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java b/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java index 18bf37c..7d90acb 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java @@ -34,8 +34,6 @@ public class FileCleanupService { public void cleanupExpiredFiles() { log.info("Starting cleanup of expired temporary files"); -// cleanupOldPeriods(); - Path tempDir = Paths.get(basePath, "temp"); if (!Files.exists(tempDir)) { return; @@ -85,17 +83,4 @@ public class FileCleanupService { log.error("Error during cleanup", e); } } - - private void cleanupOldPeriods() { - LocalDate thirtyDaysAgo = LocalDate.now().minusDays(30); - - List checks = protectedFileCheckRepository.findByPeriodStartDateBefore(thirtyDaysAgo); - - for (ProtectedFileCheck check : checks) { - check.cleanupOldData(); - } - - protectedFileCheckRepository.saveAll(checks); - log.info("Cleaned up {} old protection usage records", checks.size()); - } } diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java index 0b11018..93f43f3 100644 --- a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java @@ -5,11 +5,11 @@ 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.search.GlobalSearchResult; import ru.soune.nocopy.entity.tarif.TariffInfo; import ru.soune.nocopy.entity.tarif.TariffType; import ru.soune.nocopy.entity.user.User; @@ -24,6 +24,7 @@ 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; @@ -45,6 +46,8 @@ public class MonitoringSearchService { private final ViolationService violationService; + private final SearchProperties searchProperties; + @Transactional public void processFileSearch(FileMonitoringEntity monitoring) throws MessagingException, IOException { monitoring.setLastRun(LocalDateTime.now()); @@ -53,36 +56,78 @@ public class MonitoringSearchService { MonitoringType monitoringType = monitoring.getMonitoringType(); TariffDTO tariffMonitoring = tariffService.getTariffByType(TariffType.valueOf(monitoringType.name())); + try { tariffInfoService.writeOffTokens(monitoring.getUserId(), tariffMonitoring.getTokens()); - String yandexResponse = searchImageService.searchReverseByPublicUrl( - monitoring.getFile(), "yandex_reverse_image", "visual_matches"); + boolean useYandex = searchProperties.getEngines().getOrDefault("yandex", + new SearchProperties.EngineConfig()).isEnabled(); + boolean useGoogle = searchProperties.getEngines().getOrDefault("google", + new SearchProperties.EngineConfig()).isEnabled(); - List yandexImages = - searchImageService.getAllImagesWithoutPagination(yandexResponse, "visual_matches"); + log.info("Monitoring search settings: useYandex={}, useGoogle={}", useYandex, useGoogle); - for (YandexSearchResponse.ImageResult imageResult : yandexImages) { + List allResults = new ArrayList<>(); + + if (useYandex) { + try { + String yandexResponse = searchImageService.searchReverseByPublicUrl( + monitoring.getFile(), "yandex_reverse_image", "visual_matches"); + + List 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 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 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); } -// try { -// String googleResponse = searchImageService.searchReverseByPublicUrl( -// monitoring.getFile(), "google_lens", "exact_matches"); -// -// List googleImages = -// searchImageService.getAllImagesWithoutPagination(googleResponse, "exact_matches"); -// -// -// for (YandexSearchResponse.ImageResult imageResult : googleImages) { -// violationService.processViolation(imageResult, monitoring.getFile(), null); -// } -// -// } catch (TimeoutException | IOException e) { -// log.warn("Google search failed"); -// } - 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(); @@ -91,10 +136,12 @@ public class MonitoringSearchService { 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()); + } finally { updateNextRun(monitoring); monitoringRepository.save(monitoring); diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java index 9f202c4..19b1644 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -1,21 +1,18 @@ package ru.soune.nocopy.service.search; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.vrt.fileprotection.image.ImageCheckResult; 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.entity.violation.Violation; import ru.soune.nocopy.repository.GlobalSearchResultRepository; import ru.soune.nocopy.repository.GlobalSearchTaskRepository; -import ru.soune.nocopy.repository.ViolationRepository; import ru.soune.nocopy.service.tariff.TariffConstants; import ru.soune.nocopy.service.tariff.TariffInfoService; import ru.soune.nocopy.service.violation.ViolationService; @@ -36,16 +33,14 @@ public class GlobalSearchAsyncProcessor { private final SearchImageService searchImageService; - private final ObjectMapper objectMapper; - private final TariffInfoService tariffInfoService; private final GlobalSearchTaskRepository globalSearchTaskRepository; - private final ViolationRepository violationRepository; - private final ViolationService violationService; + private final SearchProperties searchProperties; + @Async @Transactional public void processFilesAsync(String taskId, List filesToProcess, Long userId) { @@ -85,52 +80,85 @@ public class GlobalSearchAsyncProcessor { result = globalSearchResultRepository.save(result); -// List googleImages = new ArrayList<>(); + 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 googleImages = new ArrayList<>(); List yandexImages = new ArrayList<>(); - List allUniqueImages = new ArrayList<>(); + List allResults = new ArrayList<>(); boolean hasTimeout = false; -// try { -// String searchResponseGoogle = searchImageService.searchReverseByPublicUrl( -// file, "google_lens", "exact_matches"); -// -// googleImages = searchImageService.getAllImagesWithoutPagination(searchResponseGoogle, -// "exact_matches"); -// -// allUniqueImages.addAll(googleImages); -// log.info("Google search OK for file {}", file.getId()); -// -// } 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); -// } + if (useGoogle) { + try { + String searchResponseGoogle = searchImageService.searchReverseByPublicUrl( + file, "google_lens", "exact_matches"); - try { - String searchResponseYandex = searchImageService.searchReverseByPublicUrl( - file, "yandex_reverse_image", "visual_matches"); + googleImages = searchImageService.getAllImagesWithoutPagination(searchResponseGoogle, + "exact_matches"); - yandexImages = - searchImageService.getAllImagesWithoutPagination(searchResponseYandex, "visual_matches"); + allResults.addAll(googleImages); + log.info("Google search OK for file {}, found {} results", file.getId(), googleImages.size()); - allUniqueImages.addAll(yandexImages); - log.info("Yandex search OK for file {}", file.getId()); - - } 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); + } 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()); } -// allUniqueImages = searchImageService.removeDuplicateUrls( -// allUniqueImages.stream() -// .filter(img -> img.getUrl() != null) -// .collect(Collectors.toList()), -// new ArrayList<>()); + if (useYandex) { + try { + String searchResponseYandex = searchImageService.searchReverseByPublicUrl( + file, "yandex_reverse_image", "visual_matches"); - allUniqueImages = yandexImages; + 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 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); @@ -138,10 +166,15 @@ public class GlobalSearchAsyncProcessor { 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()); - tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH); + if (!allUniqueImages.isEmpty()) { + tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH); + } } return result; diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 83d93c2..2aa67dd 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -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; @@ -23,40 +24,15 @@ import java.util.concurrent.*; @Slf4j @Service + public class SearchImageService { private final ObjectMapper objectMapper; - private final OkHttpClient yandexHttpClient; - - private final OkHttpClient googleHttpClient; - - public SearchImageService() { - Proxy proxy = new Proxy(Proxy.Type.HTTP, - new InetSocketAddress("193.46.217.94", 3128)); - - this.yandexHttpClient = new OkHttpClient.Builder() -// .proxy(proxy) - .connectTimeout(60, TimeUnit.SECONDS) - .writeTimeout(60, TimeUnit.SECONDS) - .readTimeout(60, TimeUnit.SECONDS) - .callTimeout(60, TimeUnit.SECONDS) - .followRedirects(true) - .followSslRedirects(true) - .retryOnConnectionFailure(true) - .build(); - - this.googleHttpClient = new OkHttpClient.Builder() -// .proxy(proxy) - .connectTimeout(35, TimeUnit.SECONDS) - .writeTimeout(35, TimeUnit.SECONDS) - .readTimeout(35, TimeUnit.SECONDS) - .callTimeout(35, TimeUnit.SECONDS) - .followRedirects(true) - .followSslRedirects(true) - .retryOnConnectionFailure(true) - .build(); + private final SearchProperties searchProperties; + public SearchImageService(SearchProperties searchProperties) { + this.searchProperties = searchProperties; this.objectMapper = new ObjectMapper(); } @@ -113,17 +89,34 @@ public class SearchImageService { return mimeType != null && mimeType.startsWith("image"); } - private String callReverseImageApiByUrl(String imageUrl, String engine, String searchType) throws IOException { - log.info("=== START SEARCH API CALL ==="); - log.info("Engine: {}, SearchType: {}", engine, searchType); - log.info("ImageUrl: {}", imageUrl); + private OkHttpClient createHttpClient(boolean useProxy) { + 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()) { - log.error("SearchAPI key not configured"); throw new IllegalStateException("SearchAPI key not configured"); } - OkHttpClient client = engine.contains("yandex_reverse_image") ? yandexHttpClient : googleHttpClient; + boolean useProxy = searchProperties.getProxy().isEnabled(); + + OkHttpClient client = createHttpClient(useProxy); HttpUrl url = HttpUrl.parse("https://www.searchapi.io/api/v1/search") .newBuilder() @@ -134,8 +127,6 @@ public class SearchImageService { .addQueryParameter("t_", String.valueOf(System.currentTimeMillis())) .build(); - log.info("Request URL: {}", url); - Request request = new Request.Builder() .url(url) .header("Accept", "application/json") @@ -149,27 +140,17 @@ public class SearchImageService { log.info("SearchAPI response code={}, duration={}ms, engine={}", response.code(), duration, engine); + if (!response.isSuccessful()) { + String errorBody = response.body() != null ? response.body().string() : "null"; + throw new IOException("API error " + response.code() + ": " + errorBody); + } + ResponseBody body = response.body(); if (body == null) { - log.error("Response body is null"); throw new IOException("Empty response body"); } - String responseBody = body.string(); - - log.info("=== RESPONSE BODY LENGTH: {} ===", responseBody.length()); - log.info("=== RESPONSE BODY START: {} ===", - responseBody.length() > 200 ? responseBody.substring(0, 200) : responseBody); - - if (!response.isSuccessful()) { - log.error("Response not successful: {}", response.code()); - throw new IOException("API error " + response.code() + ": " + responseBody); - } - - return responseBody; - } catch (Exception e) { - log.error("Exception in search API call: {}", e.getMessage(), e); - throw e; + return body.string(); } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index a0d6d5a..43f3b9a 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -107,6 +107,17 @@ searchapi: api-key: ${SEARCHAPI_API_KEY:5jyYZC8jSaxhZTwjMUhwtAXi} reverse-image-url: "https://searchapi.io/api/v1/search" +search: + engines: + yandex: + enabled: true + google: + enabled: false + proxy: + enabled: true + host: "193.46.217.94" + port: 3128 + yookassa: secret-key: test_0Yns_0NHV5GJf6ypJ5HC4NSfnLO8SJkw-1PwrVWsDl4 shop-id: 1276731 From 0fbe725eb9433819337f3b4c0596b828282a30fb Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Mar 2026 15:55:58 +0700 Subject: [PATCH 228/274] dev add violation notion --- .../service/search/SearchImageService.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 2aa67dd..9d77047 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -89,7 +89,7 @@ public class SearchImageService { return mimeType != null && mimeType.startsWith("image"); } - private OkHttpClient createHttpClient(boolean useProxy) { + private OkHttpClient createHttpClient() { OkHttpClient.Builder builder = new OkHttpClient.Builder() .connectTimeout(35, TimeUnit.SECONDS) .writeTimeout(35, TimeUnit.SECONDS) @@ -99,12 +99,12 @@ public class SearchImageService { .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); - } +// 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(); } @@ -114,9 +114,10 @@ public class SearchImageService { throw new IllegalStateException("SearchAPI key not configured"); } - boolean useProxy = searchProperties.getProxy().isEnabled(); +// boolean useProxy = searchProperties.getProxy().isEnabled(); - OkHttpClient client = createHttpClient(useProxy); +// OkHttpClient client = createHttpClient(useProxy); + OkHttpClient client = createHttpClient(); HttpUrl url = HttpUrl.parse("https://www.searchapi.io/api/v1/search") .newBuilder() From 0b2ef108ca774f3914f872cc488e6e8789843aeb Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Mar 2026 18:23:53 +0700 Subject: [PATCH 229/274] dev add violation notion --- .../service/search/SearchImageService.java | 82 +++++++++++++++---- 1 file changed, 67 insertions(+), 15 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 9d77047..dcd79a7 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -90,6 +90,7 @@ public class SearchImageService { } private OkHttpClient createHttpClient() { + boolean useProxy = searchProperties.getProxy().isEnabled(); OkHttpClient.Builder builder = new OkHttpClient.Builder() .connectTimeout(35, TimeUnit.SECONDS) .writeTimeout(35, TimeUnit.SECONDS) @@ -99,12 +100,12 @@ public class SearchImageService { .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); -// } + 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(); } @@ -114,9 +115,6 @@ public class SearchImageService { throw new IllegalStateException("SearchAPI key not configured"); } -// boolean useProxy = searchProperties.getProxy().isEnabled(); - -// OkHttpClient client = createHttpClient(useProxy); OkHttpClient client = createHttpClient(); HttpUrl url = HttpUrl.parse("https://www.searchapi.io/api/v1/search") @@ -136,25 +134,79 @@ public class SearchImageService { long start = System.currentTimeMillis(); +// try (Response response = client.newCall(request).execute()) { +// long duration = System.currentTimeMillis() - start; +// log.info("SearchAPI response code={}, duration={}ms, engine={}", +// response.code(), duration, engine); +// +// if (!response.isSuccessful()) { +// 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"); +// } +// +// return body.string(); +// } + try (Response response = client.newCall(request).execute()) { long duration = System.currentTimeMillis() - start; log.info("SearchAPI response code={}, duration={}ms, engine={}", response.code(), duration, engine); - if (!response.isSuccessful()) { - 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"); } - return body.string(); + String responseBody = readWithTimeout(body, 30000); + + log.info("Read {} bytes (may be partial)", responseBody.length()); + + if (responseBody.length() > 0) { + String preview = responseBody.length() > 500 ? + responseBody.substring(0, 500) + "..." : responseBody; + log.info("Response preview: {}", preview); + } + + if (!response.isSuccessful()) { + throw new IOException("API error " + response.code() + ": " + + (responseBody.length() > 200 ? responseBody.substring(0, 200) : responseBody)); + } + + return responseBody; + + } catch (InterruptedIOException e) { + log.error("Timeout during reading, partial data may be lost"); + throw e; } } + private String readWithTimeout(ResponseBody body, long timeoutMs) throws IOException { + StringBuilder result = new StringBuilder(); + long startTime = System.currentTimeMillis(); + + try (BufferedReader reader = new BufferedReader(body.charStream())) { + char[] buffer = new char[8192]; + int read; + + while ((read = reader.read(buffer)) != -1) { + result.append(buffer, 0, read); + + if (System.currentTimeMillis() - startTime > timeoutMs) { + log.warn("Partial read timeout after {} ms, received {} bytes", + timeoutMs, result.length()); + break; + } + } + } + + return result.toString(); + } + public List getAllImagesWithoutPagination(String searchApiJson, String findType) throws IOException { JsonNode root = objectMapper.readTree(searchApiJson); From 86db70caf4be053f0d85eb61d5dde6297aafc207 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Mar 2026 18:36:54 +0700 Subject: [PATCH 230/274] dev add violation notion --- .../service/search/SearchImageService.java | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index dcd79a7..4fd3ea5 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -134,54 +134,54 @@ public class SearchImageService { long start = System.currentTimeMillis(); -// try (Response response = client.newCall(request).execute()) { + try (Response response = client.newCall(request).execute()) { + ResponseBody body = response.body(); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); + log.info("Search reverse response body: {}", body); // long duration = System.currentTimeMillis() - start; // log.info("SearchAPI response code={}, duration={}ms, engine={}", // response.code(), duration, engine); -// + // if (!response.isSuccessful()) { // 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"); -// } -// -// return body.string(); -// } - - try (Response response = client.newCall(request).execute()) { - long duration = System.currentTimeMillis() - start; - log.info("SearchAPI response code={}, duration={}ms, engine={}", - response.code(), duration, engine); - - ResponseBody body = response.body(); if (body == null) { throw new IOException("Empty response body"); } - String responseBody = readWithTimeout(body, 30000); - - log.info("Read {} bytes (may be partial)", responseBody.length()); - - if (responseBody.length() > 0) { - String preview = responseBody.length() > 500 ? - responseBody.substring(0, 500) + "..." : responseBody; - log.info("Response preview: {}", preview); - } - - if (!response.isSuccessful()) { - throw new IOException("API error " + response.code() + ": " + - (responseBody.length() > 200 ? responseBody.substring(0, 200) : responseBody)); - } - - return responseBody; - - } catch (InterruptedIOException e) { - log.error("Timeout during reading, partial data may be lost"); - throw e; + return body.string(); } } From 9ca116856ca4796353c5ad28e18f993649f8ecb7 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Mar 2026 18:44:03 +0700 Subject: [PATCH 231/274] dev add violation notion --- .../service/search/SearchImageService.java | 46 ++++--------------- 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 4fd3ea5..ff60d11 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -135,38 +135,13 @@ public class SearchImageService { long start = System.currentTimeMillis(); try (Response response = client.newCall(request).execute()) { - ResponseBody body = response.body(); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); - log.info("Search reverse response body: {}", body); + ResponseBody body = response.body(); + + log.info("START READ BODY: "); + String responseBody = readWithTimeout(body, 30000); + log.info("Search reverse response body length: {} bytes", responseBody.length()); + log.info("Response preview: {}", responseBody.substring(0, Math.min(500, responseBody.length()))); + log.info("END READ BODY: "); // long duration = System.currentTimeMillis() - start; // log.info("SearchAPI response code={}, duration={}ms, engine={}", // response.code(), duration, engine); @@ -176,10 +151,9 @@ public class SearchImageService { // throw new IOException("API error " + response.code() + ": " + errorBody); // } -// ResponseBody body = response.body(); - if (body == null) { - throw new IOException("Empty response body"); - } +// if (body == null) { +// throw new IOException("Empty response body"); +// } return body.string(); } From 610415951dff24b401467e002b0f38e59f2ad298 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 20 Mar 2026 19:27:13 +0700 Subject: [PATCH 232/274] dev add violation notion --- .../service/search/SearchImageService.java | 48 ++++--------------- 1 file changed, 10 insertions(+), 38 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index ff60d11..32ac438 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -136,51 +136,23 @@ public class SearchImageService { try (Response response = client.newCall(request).execute()) { ResponseBody body = response.body(); + long duration = System.currentTimeMillis() - start; + log.info("SearchAPI response code={}, duration={}ms, engine={}", + response.code(), duration, engine); - log.info("START READ BODY: "); - String responseBody = readWithTimeout(body, 30000); - log.info("Search reverse response body length: {} bytes", responseBody.length()); - log.info("Response preview: {}", responseBody.substring(0, Math.min(500, responseBody.length()))); - log.info("END READ BODY: "); -// long duration = System.currentTimeMillis() - start; -// log.info("SearchAPI response code={}, duration={}ms, engine={}", -// response.code(), duration, engine); + if (!response.isSuccessful()) { + String errorBody = response.body() != null ? response.body().string() : "null"; + throw new IOException("API error " + response.code() + ": " + errorBody); + } -// if (!response.isSuccessful()) { -// String errorBody = response.body() != null ? response.body().string() : "null"; -// throw new IOException("API error " + response.code() + ": " + errorBody); -// } - -// if (body == null) { -// throw new IOException("Empty response body"); -// } + if (body == null) { + throw new IOException("Empty response body"); + } return body.string(); } } - private String readWithTimeout(ResponseBody body, long timeoutMs) throws IOException { - StringBuilder result = new StringBuilder(); - long startTime = System.currentTimeMillis(); - - try (BufferedReader reader = new BufferedReader(body.charStream())) { - char[] buffer = new char[8192]; - int read; - - while ((read = reader.read(buffer)) != -1) { - result.append(buffer, 0, read); - - if (System.currentTimeMillis() - startTime > timeoutMs) { - log.warn("Partial read timeout after {} ms, received {} bytes", - timeoutMs, result.length()); - break; - } - } - } - - return result.toString(); - } - public List getAllImagesWithoutPagination(String searchApiJson, String findType) throws IOException { JsonNode root = objectMapper.readTree(searchApiJson); From 69c3d876ae3de0b26e8763626e8c7ab93273677d Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 23 Mar 2026 15:17:42 +0700 Subject: [PATCH 233/274] dev add complaint entity --- .../nocopy/configuration/HandlerConfig.java | 4 +- .../java/ru/soune/nocopy/dto/MessageCode.java | 1 + .../dto/complaint/ComplaintRequest.java | 41 +++++ .../dto/complaint/ComplaintResponse.java | 39 +++++ .../entity/complaint/ComplaintEntity.java | 58 +++++++ .../entity/complaint/ComplaintStatus.java | 5 + .../nocopy/entity/complaint/LawCase.java | 17 ++ .../exception/ComplaintNotFoundException.java | 7 + .../DuplicateComplaintException.java | 7 + .../handler/ComplaintEntityHandler.java | 137 ++++++++++++++++ .../repository/ComplaintEntityRepository.java | 28 ++++ .../complaint/ComplaintEntityService.java | 148 ++++++++++++++++++ 12 files changed, 491 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/complaint/ComplaintRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/complaint/ComplaintResponse.java create mode 100644 src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java create mode 100644 src/main/java/ru/soune/nocopy/entity/complaint/ComplaintStatus.java create mode 100644 src/main/java/ru/soune/nocopy/entity/complaint/LawCase.java create mode 100644 src/main/java/ru/soune/nocopy/exception/ComplaintNotFoundException.java create mode 100644 src/main/java/ru/soune/nocopy/exception/DuplicateComplaintException.java create mode 100644 src/main/java/ru/soune/nocopy/handler/ComplaintEntityHandler.java create mode 100644 src/main/java/ru/soune/nocopy/repository/ComplaintEntityRepository.java create mode 100644 src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index 4c3baff..1309a1e 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -32,7 +32,8 @@ public class HandlerConfig { ViolationHandler violationHandler, ViolationStatisticsHandler violationStatisticsHandler, GlobalSearchHandler globalSearchHandler, - ViolationNotionHandler violationNotionHandler + ViolationNotionHandler violationNotionHandler, + ComplaintEntityHandler complaintEntityHandler ) { Map map = new HashMap<>(); map.put(20001, login); @@ -56,6 +57,7 @@ public class HandlerConfig { map.put(30010, violationStatisticsHandler); map.put(30011, globalSearchHandler); map.put(30012, violationNotionHandler); + map.put(30013, complaintEntityHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index ec273ba..886c0b3 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -55,6 +55,7 @@ public enum MessageCode { "more than max valid"), USER_NOT_ACTIVE(2, "User not active"), NOTION_NOT_FOUND(4, "Notion not found"), + NOT_FOUND(4, "Notion not found"), MESSAGE_IS_REQUIRED_FOR_NOTION(4, "Message is required for notion"); private final Integer code; diff --git a/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintRequest.java b/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintRequest.java new file mode 100644 index 0000000..7c7d695 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintRequest.java @@ -0,0 +1,41 @@ +package ru.soune.nocopy.dto.complaint; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import java.util.List; + +@Data +public class ComplaintRequest { + @JsonProperty("action") + private String action; + + @JsonProperty("id") + private Long id; + + @JsonProperty("violation_id") + private Long violationId; + + @JsonProperty("text") + private String complaintText; + + @JsonProperty("email") + private String email; + + @JsonProperty("links") + private List links; + + @JsonProperty("status") + private String status; + + @JsonProperty("page") + private Integer page; + + @JsonProperty("size") + private Integer size; + + @JsonProperty("sort_by") + private String sortBy; + + @JsonProperty("sort_direction") + private String sortDirection; +} diff --git a/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintResponse.java b/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintResponse.java new file mode 100644 index 0000000..4b86313 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintResponse.java @@ -0,0 +1,39 @@ +package ru.soune.nocopy.dto.complaint; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ComplaintResponse { + @JsonProperty("id") + private Long id; + + @JsonProperty("status") + private String status; + + @JsonProperty("complaint_text") + private String complaintText; + + @JsonProperty("violation_id") + private Long violationId; + + @JsonProperty("email") + private String email; + + @JsonProperty("links") + private List links; + + @JsonProperty("created_at") + private String createdAt; + + @JsonProperty("updated_at") + private String updatedAt; +} diff --git a/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java new file mode 100644 index 0000000..d09a433 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java @@ -0,0 +1,58 @@ +package ru.soune.nocopy.entity.complaint; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import ru.soune.nocopy.entity.violation.Violation; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "complaint") +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter @Setter +@EntityListeners(AuditingEntityListener.class) +public class ComplaintEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "status") + @Enumerated(EnumType.STRING) + private ComplaintStatus status; + + @Column(name = "complaint_text", columnDefinition = "TEXT") + private String complaintText; + + @Column(name = "email") + private String email; + + @Column(name = "links", columnDefinition = "TEXT") + private String links; + + @OneToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "violation_id") + @ToString.Exclude + @JsonIgnore + private Violation violation; + + @OneToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "law_case_id") + @ToString.Exclude + @JsonIgnore + private LawCase lawCase; + + @Column(name = "created_at", updatable = false) + @CreatedDate + private LocalDateTime createdAt; + + @Column(name = "updated_at") + @LastModifiedDate + private LocalDateTime updatedAt; +} diff --git a/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintStatus.java b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintStatus.java new file mode 100644 index 0000000..e65a83c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintStatus.java @@ -0,0 +1,5 @@ +package ru.soune.nocopy.entity.complaint; + +public enum ComplaintStatus { + CREATED, IN_WORK, COMPLETED, CANCELLED +} diff --git a/src/main/java/ru/soune/nocopy/entity/complaint/LawCase.java b/src/main/java/ru/soune/nocopy/entity/complaint/LawCase.java new file mode 100644 index 0000000..b9ca6e6 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/complaint/LawCase.java @@ -0,0 +1,17 @@ +package ru.soune.nocopy.entity.complaint; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter @Setter +@Table(name = "law_case") +public class LawCase { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; +} diff --git a/src/main/java/ru/soune/nocopy/exception/ComplaintNotFoundException.java b/src/main/java/ru/soune/nocopy/exception/ComplaintNotFoundException.java new file mode 100644 index 0000000..fce3a9f --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/ComplaintNotFoundException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class ComplaintNotFoundException extends RuntimeException { + public ComplaintNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/DuplicateComplaintException.java b/src/main/java/ru/soune/nocopy/exception/DuplicateComplaintException.java new file mode 100644 index 0000000..9ce0be5 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/DuplicateComplaintException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class DuplicateComplaintException extends RuntimeException { + public DuplicateComplaintException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/ComplaintEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/ComplaintEntityHandler.java new file mode 100644 index 0000000..a046205 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/ComplaintEntityHandler.java @@ -0,0 +1,137 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.complaint.ComplaintRequest; +import ru.soune.nocopy.dto.complaint.ComplaintResponse; +import ru.soune.nocopy.entity.complaint.ComplaintStatus; +import ru.soune.nocopy.service.complaint.ComplaintEntityService; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ComplaintEntityHandler implements RequestHandler { + private final ComplaintEntityService complaintService; + + private final ObjectMapper objectMapper; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + Integer msgId = request.getMsgId(); + ComplaintRequest complaintRequest = objectMapper.convertValue(request.getMessageBody(), ComplaintRequest.class); + String action = complaintRequest.getAction(); + + try { + switch (action.toLowerCase()) { + case "create": + return handleCreate(msgId, complaintRequest); + case "get": + return handleGet(msgId, complaintRequest); + case "get_all": + return handleGetAll(msgId, complaintRequest); + case "get_by_violation": + return handleGetByViolation(msgId, complaintRequest); + case "update_status": + return handleUpdateStatus(msgId, complaintRequest); + case "update": + return handleUpdate(msgId, complaintRequest); + case "delete": + return handleDelete(msgId, complaintRequest); + default: + return errorResponse(msgId, MessageCode.MSG_ID_NOT_FOUND.getCode(), "Unknown action: " + action); + } + } catch (Exception e) { + log.error("Error handling complaint request", e); + return errorResponse(msgId, MessageCode.INVALID_JSON_BODY.getCode(), e.getMessage()); + } + } + + private BaseResponse handleCreate(Integer msgId, ComplaintRequest req) { + if (req.getViolationId() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), + "violation_id is required"); + if (req.getComplaintText() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), + "text is required"); + + ComplaintResponse response = complaintService.createComplaint(req); + return successResponse(msgId, "Complaint created successfully", response); + } + + private BaseResponse handleGet(Integer msgId, ComplaintRequest req) { + if (req.getId() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), "id is required"); + + ComplaintResponse response = complaintService.getComplaintById(req.getId()); + return successResponse(msgId, "Complaint retrieved successfully", response); + } + + private BaseResponse handleGetAll(Integer msgId, ComplaintRequest req) { + Pageable pageable = PageRequest.of( + req.getPage() != null ? req.getPage() : 0, + req.getSize() != null ? req.getSize() : 20, + Sort.by(Sort.Direction.fromString(req.getSortDirection() != null ? req.getSortDirection() : "desc"), + req.getSortBy() != null ? req.getSortBy() : "createdAt") + ); + + var page = complaintService.getAllComplaints(pageable); + Map data = new HashMap<>(); + data.put("content", page.getContent()); + data.put("page", page.getNumber()); + data.put("size", page.getSize()); + data.put("totalElements", page.getTotalElements()); + data.put("totalPages", page.getTotalPages()); + + return successResponse(msgId, "Complaints retrieved successfully", data); + } + + private BaseResponse handleGetByViolation(Integer msgId, ComplaintRequest req) { + if (req.getViolationId() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), + "violation_id is required"); + + var complaints = complaintService.getComplaintsByViolationId(req.getViolationId()); + Map data = new HashMap<>(); + data.put("content", complaints); + data.put("totalElements", complaints.size()); + + return successResponse(msgId, "Complaints retrieved successfully", data); + } + + private BaseResponse handleUpdateStatus(Integer msgId, ComplaintRequest req) { + if (req.getId() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), "id is required"); + if (req.getStatus() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), "status is required"); + + ComplaintStatus status = ComplaintStatus.valueOf(req.getStatus().toUpperCase()); + ComplaintResponse response = complaintService.updateComplaintStatus(req.getId(), status); + return successResponse(msgId, "Status updated successfully", response); + } + + private BaseResponse handleUpdate(Integer msgId, ComplaintRequest req) { + if (req.getId() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), "id is required"); + + ComplaintResponse response = complaintService.updateComplaint(req.getId(), req); + return successResponse(msgId, "Complaint updated successfully", response); + } + + private BaseResponse handleDelete(Integer msgId, ComplaintRequest req) { + if (req.getId() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), "id is required"); + + ComplaintResponse response = complaintService.deleteComplaint(req.getId()); + return successResponse(msgId, "Complaint deleted successfully", response); + } + + private BaseResponse successResponse(Integer msgId, String message, Object data) { + return new BaseResponse(msgId, MessageCode.SUCCESS.getCode(), message, data); + } + + private BaseResponse errorResponse(Integer msgId, Integer code, String message) { + return new BaseResponse(msgId, code, message, new HashMap<>()); + } +} diff --git a/src/main/java/ru/soune/nocopy/repository/ComplaintEntityRepository.java b/src/main/java/ru/soune/nocopy/repository/ComplaintEntityRepository.java new file mode 100644 index 0000000..159d388 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/ComplaintEntityRepository.java @@ -0,0 +1,28 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.entity.complaint.ComplaintEntity; +import ru.soune.nocopy.entity.complaint.ComplaintStatus; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ComplaintEntityRepository extends JpaRepository { + Optional findById(Long id); + Page findAll(Pageable pageable); + Page findByStatus(ComplaintStatus status, Pageable pageable); + List findByViolationId(Long violationId); + boolean existsByViolationId(Long violationId); + + @Modifying + @Transactional + @Query("UPDATE ComplaintEntity c SET c.status = :status WHERE c.id = :id") + int updateStatus(Long id, ComplaintStatus status); +} diff --git a/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java b/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java new file mode 100644 index 0000000..fafc277 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java @@ -0,0 +1,148 @@ +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.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 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 ObjectMapper objectMapper; + + 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()); + } + + String linksJson = null; + if (request.getLinks() != null && !request.getLinks().isEmpty()) { + try { + linksJson = objectMapper.writeValueAsString(request.getLinks()); + } catch (JsonProcessingException e) { + linksJson = request.getLinks().toString(); + } + } + + ComplaintEntity complaint = ComplaintEntity.builder() + .status(ComplaintStatus.CREATED) + .complaintText(request.getComplaintText()) + .email(request.getEmail()) + .links(linksJson) + .violation(violation) + .build(); + + 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 getAllComplaints(Pageable pageable) { + return complaintRepository.findAll(pageable).map(this::mapToResponse); + } + + @Transactional(readOnly = true) + public List 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.getLinks() != null) { + try { + complaint.setLinks(objectMapper.writeValueAsString(request.getLinks())); + } catch (JsonProcessingException e) { + complaint.setLinks(request.getLinks().toString()); + } + } + + 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) { + List linksList = null; + if (entity.getLinks() != null) { + try { + linksList = objectMapper.readValue(entity.getLinks(), + objectMapper.getTypeFactory().constructCollectionType(List.class, String.class)); + } catch (JsonProcessingException e) { + log.warn("Failed to parse links JSON: {}", entity.getLinks()); + } + } + + return ComplaintResponse.builder() + .id(entity.getId()) + .status(entity.getStatus() != null ? entity.getStatus().name() : null) + .complaintText(entity.getComplaintText()) + .email(entity.getEmail()) + .links(linksList) + .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) + .build(); + } +} From 40f8effb319263069fb8a2b43f25719e1425643a Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 24 Mar 2026 11:37:25 +0700 Subject: [PATCH 234/274] dev add additional info user --- .../nocopy/configuration/HandlerConfig.java | 4 +- .../soune/nocopy/dto/register/RegRequest.java | 6 + .../dto/user/UserAdditionalInfoBody.java | 20 ++ .../entity/user/UserAdditionalInfo.java | 34 +++ .../entity/user/UserAdditionalInfoAnswer.java | 31 +++ .../user/UserAdditionalInfoListAnswer.java | 22 ++ ...gistrationAdditionalInfoNullException.java | 7 + .../exception/UserAdditionalInfoNotFound.java | 7 + .../nocopy/handler/RegRequestHandler.java | 6 +- .../soune/nocopy/handler/UserInfoHandler.java | 214 ++++++++++++++++++ .../validator/RegRequestValidator.java | 17 ++ .../UserAdditionalInfoRepository.java | 9 + .../service/user/AdditionalInfoService.java | 54 +++++ 13 files changed, 429 insertions(+), 2 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/user/UserAdditionalInfoBody.java create mode 100644 src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfo.java create mode 100644 src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoAnswer.java create mode 100644 src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoListAnswer.java create mode 100644 src/main/java/ru/soune/nocopy/exception/RegistrationAdditionalInfoNullException.java create mode 100644 src/main/java/ru/soune/nocopy/exception/UserAdditionalInfoNotFound.java create mode 100644 src/main/java/ru/soune/nocopy/handler/UserInfoHandler.java create mode 100644 src/main/java/ru/soune/nocopy/repository/UserAdditionalInfoRepository.java create mode 100644 src/main/java/ru/soune/nocopy/service/user/AdditionalInfoService.java diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index 1309a1e..1bdd94a 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -33,7 +33,8 @@ public class HandlerConfig { ViolationStatisticsHandler violationStatisticsHandler, GlobalSearchHandler globalSearchHandler, ViolationNotionHandler violationNotionHandler, - ComplaintEntityHandler complaintEntityHandler + ComplaintEntityHandler complaintEntityHandler, + UserInfoHandler userInfoHandler ) { Map map = new HashMap<>(); map.put(20001, login); @@ -58,6 +59,7 @@ public class HandlerConfig { map.put(30011, globalSearchHandler); map.put(30012, violationNotionHandler); map.put(30013, complaintEntityHandler); + map.put(30014, userInfoHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java b/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java index ef19795..70f7d2c 100644 --- a/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java @@ -37,4 +37,10 @@ public class RegRequest { @JsonProperty("mail_verified") private String mailVerified; + + @JsonProperty("ip") + private String ip; + + @JsonProperty("user_agent") + private String userAgent; } diff --git a/src/main/java/ru/soune/nocopy/dto/user/UserAdditionalInfoBody.java b/src/main/java/ru/soune/nocopy/dto/user/UserAdditionalInfoBody.java new file mode 100644 index 0000000..12fa859 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/user/UserAdditionalInfoBody.java @@ -0,0 +1,20 @@ +package ru.soune.nocopy.dto.user; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class UserAdditionalInfoBody { + + @JsonProperty("action") + private String action; + + @JsonProperty("user_id") + private Long userId; + + @JsonProperty("ip_address") + private String ipAddress; + + @JsonProperty("user_agent") + private String userAgent; +} diff --git a/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfo.java b/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfo.java new file mode 100644 index 0000000..ea1c040 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfo.java @@ -0,0 +1,34 @@ +package ru.soune.nocopy.entity.user; + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; + +import java.sql.Timestamp; + +@Entity +@NoArgsConstructor +@AllArgsConstructor +@Getter @Setter +@Builder +@Table(name = "user_additional_info") +public class UserAdditionalInfo { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, name = "register_date") + @CreationTimestamp + private Timestamp registrationDate; + + @Column(nullable = false, name = "ip") + private String ipAddress; + + @Column(nullable = false, name = "user_agent") + private String userAgent; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", unique = true, nullable = false) + private User user; +} diff --git a/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoAnswer.java b/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoAnswer.java new file mode 100644 index 0000000..3adf2e5 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoAnswer.java @@ -0,0 +1,31 @@ +package ru.soune.nocopy.entity.user; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Timestamp; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserAdditionalInfoAnswer { + + @JsonProperty("id") + private Long id; + + @JsonProperty("user_id") + private Long userId; + + @JsonProperty("ip_address") + private String ipAddress; + + @JsonProperty("user_agent") + private String userAgent; + + @JsonProperty("registration_date") + private Timestamp registrationDate; +} diff --git a/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoListAnswer.java b/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoListAnswer.java new file mode 100644 index 0000000..89917a9 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoListAnswer.java @@ -0,0 +1,22 @@ +package ru.soune.nocopy.entity.user; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserAdditionalInfoListAnswer { + + @JsonProperty("items") + private List items; + + @JsonProperty("count") + private int count; +} diff --git a/src/main/java/ru/soune/nocopy/exception/RegistrationAdditionalInfoNullException.java b/src/main/java/ru/soune/nocopy/exception/RegistrationAdditionalInfoNullException.java new file mode 100644 index 0000000..61f5cda --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/RegistrationAdditionalInfoNullException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class RegistrationAdditionalInfoNullException extends RuntimeException { + public RegistrationAdditionalInfoNullException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/UserAdditionalInfoNotFound.java b/src/main/java/ru/soune/nocopy/exception/UserAdditionalInfoNotFound.java new file mode 100644 index 0000000..ce16c63 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/UserAdditionalInfoNotFound.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class UserAdditionalInfoNotFound extends RuntimeException { + public UserAdditionalInfoNotFound(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java index 6e60a96..77efad5 100644 --- a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java @@ -21,6 +21,7 @@ import ru.soune.nocopy.repository.ReferralJpaRepository; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.mail.EmailService; +import ru.soune.nocopy.service.user.AdditionalInfoService; import java.util.Arrays; import java.util.Map; @@ -43,6 +44,8 @@ public class RegRequestHandler implements RequestHandler { private final ReferralJpaRepository referralJpaRepository; + private final AdditionalInfoService additionalInfoService; + @Override public BaseResponse handle(BaseRequest request) throws ValidationException { RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class); @@ -124,9 +127,10 @@ public class RegRequestHandler implements RequestHandler { referralService.onRegister(authToken.getUser().getId(), regRequest.getReferralLink()); } + additionalInfoService.additionalInfo(regRequest.getIp(), regRequest.getUserAgent()); + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), MessageCode.SUCCESS.getDescription(), regAnswer); - } catch (CompanyAlreadyExist e) { return new BaseResponse(request.getMsgId(), MessageCode.COMPANY_ALREADY_EXISTS.getCode(), MessageCode.COMPANY_ALREADY_EXISTS.getDescription(), Map.of( diff --git a/src/main/java/ru/soune/nocopy/handler/UserInfoHandler.java b/src/main/java/ru/soune/nocopy/handler/UserInfoHandler.java new file mode 100644 index 0000000..07edf10 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/UserInfoHandler.java @@ -0,0 +1,214 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; + +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.user.UserAdditionalInfoBody; + +import ru.soune.nocopy.entity.user.UserAdditionalInfo; +import ru.soune.nocopy.entity.user.UserAdditionalInfoAnswer; +import ru.soune.nocopy.entity.user.UserAdditionalInfoListAnswer; +import ru.soune.nocopy.exception.UserAdditionalInfoNotFound; +import ru.soune.nocopy.service.user.AdditionalInfoService; + + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@Component +@Slf4j +@RequiredArgsConstructor +public class UserInfoHandler implements RequestHandler { + + private final AdditionalInfoService additionalInfoService; + private final ObjectMapper objectMapper; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + try { + UserAdditionalInfoBody body = objectMapper.convertValue(request.getMessageBody(), + UserAdditionalInfoBody.class); + + log.info("Processing UserInfoHandler with action: {}", body.getAction()); + + return switch (body.getAction()) { + case "getAll" -> getAllAdditionalInfos(request); + case "getById" -> getAdditionalInfoById(request, body.getUserId()); + case "update" -> updateAdditionalInfo(request, body); + case "delete" -> deleteAdditionalInfo(request, body.getUserId()); + default -> new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_ACTION.getCode(), + "Invalid action: " + body.getAction(), + null + ); + }; + } catch (IllegalArgumentException e) { + log.error("Validation error in UserInfoHandler", e); + return new BaseResponse( + request.getMsgId(), + MessageCode.VALIDATION_ERROR.getCode(), + e.getMessage(), + null); + } catch (UserAdditionalInfoNotFound e) { + log.error("User additional info not found", e); + return new BaseResponse( + request.getMsgId(), + MessageCode.NOT_FOUND.getCode(), + e.getMessage(), + null); + } catch (Exception e) { + log.error("Unexpected error in UserInfoHandler", e); + return new BaseResponse( + request.getMsgId(), + MessageCode.INTERNAL_ERROR.getCode(), + "Internal server error: " + e.getMessage(), + null); + } + } + + private BaseResponse getAllAdditionalInfos(BaseRequest request) { + log.info("Getting all additional infos"); + + List infos = additionalInfoService.getAllAdditionalInfos(); + + List answerItems = infos.stream() + .map(this::convertToAnswer) + .collect(Collectors.toList()); + + UserAdditionalInfoListAnswer listAnswer = UserAdditionalInfoListAnswer.builder() + .items(answerItems) + .count(answerItems.size()) + .build(); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + listAnswer + ); + } + + private BaseResponse getAdditionalInfoById(BaseRequest request, Long userId) { + log.info("Getting additional info for user: {}", userId); + + if (userId == null) { + throw new IllegalArgumentException("User ID is required for getById action"); + } + + UserAdditionalInfo info = additionalInfoService.getAdditionalInfoByUser(userId); + + if (info == null) { + throw new UserAdditionalInfoNotFound( + String.format("User additional info not found for userId: %d", userId) + ); + } + + UserAdditionalInfoAnswer answer = convertToAnswer(info); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + answer + ); + } + + private BaseResponse updateAdditionalInfo(BaseRequest request, UserAdditionalInfoBody body) { + log.info("Updating additional info for user: {}", body.getUserId()); + + if (body.getUserId() == null) { + throw new IllegalArgumentException("User ID is required for update action"); + } + + validateIpAddress(body.getIpAddress()); + validateUserAgent(body.getUserAgent()); + + Optional updatedInfo = additionalInfoService.updateAdditionalInfo( + body.getUserId(), + body.getIpAddress(), + body.getUserAgent() + ); + + if (updatedInfo.isEmpty()) { + throw new UserAdditionalInfoNotFound( + String.format("User additional info not found for userId: %d", body.getUserId()) + ); + } + + UserAdditionalInfoAnswer answer = convertToAnswer(updatedInfo.get()); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + answer + ); + } + + private BaseResponse deleteAdditionalInfo(BaseRequest request, Long userId) { + log.info("Deleting additional info for user: {}", userId); + + if (userId == null) { + throw new IllegalArgumentException("User ID is required for delete action"); + } + + UserAdditionalInfo existingInfo = additionalInfoService.getAdditionalInfoByUser(userId); + + if (existingInfo == null) { + throw new UserAdditionalInfoNotFound( + String.format("User additional info not found for userId: %d", userId) + ); + } + + additionalInfoService.deleteAdditionalInfo(userId); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + Map.of("deleted", true, "user_id", userId) + ); + } + + private UserAdditionalInfoAnswer convertToAnswer(UserAdditionalInfo info) { + return UserAdditionalInfoAnswer.builder() + .id(info.getId()) + .userId(info.getUser() != null ? info.getUser().getId() : null) + .ipAddress(info.getIpAddress()) + .userAgent(info.getUserAgent()) + .registrationDate(info.getRegistrationDate()) + .build(); + } + + private void validateIpAddress(String ip) { + if (ip == null || ip.trim().isEmpty()) { + throw new IllegalArgumentException("IP address is required for update action"); + } + + String ipv4Pattern = + "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"; + + if (!ip.trim().matches(ipv4Pattern)) { + log.warn("Invalid IP address format: {}", ip); + throw new IllegalArgumentException("Invalid IP address format"); + } + } + + private void validateUserAgent(String userAgent) { + if (userAgent == null || userAgent.trim().isEmpty()) { + throw new IllegalArgumentException("User agent is required for update action"); + } + + if (userAgent.length() > 500) { + throw new IllegalArgumentException("User agent must not exceed 500 characters"); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/validator/RegRequestValidator.java b/src/main/java/ru/soune/nocopy/handler/validator/RegRequestValidator.java index b57c7fb..84b708a 100644 --- a/src/main/java/ru/soune/nocopy/handler/validator/RegRequestValidator.java +++ b/src/main/java/ru/soune/nocopy/handler/validator/RegRequestValidator.java @@ -1,6 +1,7 @@ package ru.soune.nocopy.handler.validator; import org.apache.commons.validator.routines.EmailValidator; +import org.apache.commons.validator.routines.InetAddressValidator; import org.springframework.stereotype.Component; import org.springframework.validation.Errors; import org.springframework.validation.Validator; @@ -28,6 +29,7 @@ public class RegRequestValidator implements Validator { validateEmail(request.getEmail(), errors); validateCompanyName(request.getCompanyName(), errors); validateFullName(request.getFullName(), errors); + validateAdditionalInfo(request.getIp(), request.getUserAgent(), errors); } private void validateFullName(String fullName, Errors errors) { @@ -73,6 +75,21 @@ public class RegRequestValidator implements Validator { } } + public void validateAdditionalInfo(String ip, String userAgent, Errors errors) { + if (ip == null || ip.trim().isEmpty()) { + errors.rejectValue("ip", "ip.required", "IP address is required"); + } else if (!InetAddressValidator.getInstance().isValid(ip.trim())) { + errors.rejectValue("ip", "ip.invalid", "Invalid IP address format"); + } + + if (userAgent == null || userAgent.trim().isEmpty()) { + errors.rejectValue("userAgent", "userAgent.required", "User agent is required"); + } else if (userAgent.length() > 500) { + errors.rejectValue("userAgent", "userAgent.tooLong", "User agent must not exceed " + + "500 characters"); + } + } + public void validateEmail(String email, Errors errors) { if (email == null || email.trim().isEmpty()) { errors.rejectValue("email", "email.is.empty", "Email must not be empty"); diff --git a/src/main/java/ru/soune/nocopy/repository/UserAdditionalInfoRepository.java b/src/main/java/ru/soune/nocopy/repository/UserAdditionalInfoRepository.java new file mode 100644 index 0000000..b45d0ca --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/UserAdditionalInfoRepository.java @@ -0,0 +1,9 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.user.UserAdditionalInfo; + +@Repository +public interface UserAdditionalInfoRepository extends JpaRepository { +} diff --git a/src/main/java/ru/soune/nocopy/service/user/AdditionalInfoService.java b/src/main/java/ru/soune/nocopy/service/user/AdditionalInfoService.java new file mode 100644 index 0000000..f0016cc --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/user/AdditionalInfoService.java @@ -0,0 +1,54 @@ +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.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) { + if (ip == null || userAgent == null) { + throw new RegistrationAdditionalInfoNullException("Ip address or user agent is null"); + } + + UserAdditionalInfo userAdditionalInfo = UserAdditionalInfo.builder() + .ipAddress(ip) + .userAgent(userAgent) + .build(); + + additionalInfoRepository.save(userAdditionalInfo); + } + + public UserAdditionalInfo getAdditionalInfoByUser(Long userId) { + return additionalInfoRepository.findById(userId).orElse(null); + } + + public List getAllAdditionalInfos() { + return additionalInfoRepository.findAll(); + } + + public void deleteAdditionalInfo(Long userId) { + additionalInfoRepository.deleteById(userId); + } + + @Transactional + public Optional updateAdditionalInfo(Long userId, String ip, String userAgent) { + return additionalInfoRepository.findById(userId) + .map(info -> { + info.setIpAddress(ip); + info.setUserAgent(userAgent); + return additionalInfoRepository.save(info); + }); + } +} From 12e001211bc90298baed77b7e9a93520692d195c Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 24 Mar 2026 16:16:16 +0700 Subject: [PATCH 235/274] dev add additional info user --- .../nocopy/controller/FileController.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/main/java/ru/soune/nocopy/controller/FileController.java b/src/main/java/ru/soune/nocopy/controller/FileController.java index b044427..6beca5a 100644 --- a/src/main/java/ru/soune/nocopy/controller/FileController.java +++ b/src/main/java/ru/soune/nocopy/controller/FileController.java @@ -1,5 +1,7 @@ package ru.soune.nocopy.controller; +import com.vrt.NoCopyFileService; +import com.vrt.fileprotection.FileProtector; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; @@ -15,6 +17,7 @@ import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.file.FileStorageService; import ru.soune.nocopy.service.file.ImageResizeService; +import ru.soune.nocopy.util.FileUtil; import java.io.FileNotFoundException; import java.net.URLEncoder; @@ -45,6 +48,12 @@ public class FileController { @Autowired private ImageResizeService imageResizeService; + @Autowired + private NoCopyFileService noCopyFileService; + + @Autowired + private FileUtil fileUtil; + @GetMapping("/public/{fileId}") public ResponseEntity getPublicFile(@PathVariable String fileId) { try { @@ -145,4 +154,16 @@ public class FileController { .ok() .body(protectedFileCheck); } + + @PostMapping("protect/{fileId}/{convertTo}") + public ResponseEntity convert(@PathVariable String fileId, @PathVariable String convertTo) { + FileEntity file = fileRepository.findByFileId(fileId); + FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(file, convertTo); + + noCopyFileService.addFile(fileInfo); + + return ResponseEntity + .ok() + .body(fileInfo); + } } From e68ad3d3d3ee770768853b50f8d866565c883039 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 25 Mar 2026 11:52:01 +0700 Subject: [PATCH 236/274] dev add additional info user --- .../nocopy/dto/complaint/ComplaintRequest.java | 3 --- .../dto/complaint/ComplaintResponse.java | 3 --- .../entity/complaint/ComplaintEntity.java | 3 --- .../entity/complaint/ComplaintStatus.java | 3 ++- .../complaint/ComplaintEntityService.java | 18 ------------------ 5 files changed, 2 insertions(+), 28 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintRequest.java b/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintRequest.java index 7c7d695..0de3cce 100644 --- a/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintRequest.java @@ -21,9 +21,6 @@ public class ComplaintRequest { @JsonProperty("email") private String email; - @JsonProperty("links") - private List links; - @JsonProperty("status") private String status; diff --git a/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintResponse.java b/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintResponse.java index 4b86313..55efa91 100644 --- a/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintResponse.java @@ -28,9 +28,6 @@ public class ComplaintResponse { @JsonProperty("email") private String email; - @JsonProperty("links") - private List links; - @JsonProperty("created_at") private String createdAt; diff --git a/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java index d09a433..ab2b573 100644 --- a/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java +++ b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java @@ -33,9 +33,6 @@ public class ComplaintEntity { @Column(name = "email") private String email; - @Column(name = "links", columnDefinition = "TEXT") - private String links; - @OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "violation_id") @ToString.Exclude diff --git a/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintStatus.java b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintStatus.java index e65a83c..ca5deed 100644 --- a/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintStatus.java +++ b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintStatus.java @@ -1,5 +1,6 @@ package ru.soune.nocopy.entity.complaint; public enum ComplaintStatus { - CREATED, IN_WORK, COMPLETED, CANCELLED + //NEW , SHOWED, LEGAL_IN_WORK, ЖАЛОБА, 3 + 4 , САНКЦАНИРОВАННОЕ ИСПОЛЬЗОВАНИЕ + CREATED, SHOWED, IN_WORK, COMPLETED, CANCELLED } diff --git a/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java b/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java index fafc277..94aad5d 100644 --- a/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java @@ -57,7 +57,6 @@ public class ComplaintEntityService { .status(ComplaintStatus.CREATED) .complaintText(request.getComplaintText()) .email(request.getEmail()) - .links(linksJson) .violation(violation) .build(); @@ -105,13 +104,6 @@ public class ComplaintEntityService { } if (request.getComplaintText() != null) complaint.setComplaintText(request.getComplaintText()); if (request.getEmail() != null) complaint.setEmail(request.getEmail()); - if (request.getLinks() != null) { - try { - complaint.setLinks(objectMapper.writeValueAsString(request.getLinks())); - } catch (JsonProcessingException e) { - complaint.setLinks(request.getLinks().toString()); - } - } return mapToResponse(complaintRepository.save(complaint)); } @@ -124,22 +116,12 @@ public class ComplaintEntityService { } private ComplaintResponse mapToResponse(ComplaintEntity entity) { - List linksList = null; - if (entity.getLinks() != null) { - try { - linksList = objectMapper.readValue(entity.getLinks(), - objectMapper.getTypeFactory().constructCollectionType(List.class, String.class)); - } catch (JsonProcessingException e) { - log.warn("Failed to parse links JSON: {}", entity.getLinks()); - } - } return ComplaintResponse.builder() .id(entity.getId()) .status(entity.getStatus() != null ? entity.getStatus().name() : null) .complaintText(entity.getComplaintText()) .email(entity.getEmail()) - .links(linksList) .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) From ebcb68135bfdcaae513fcab37a1bfd9c4217e338 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 25 Mar 2026 12:00:04 +0700 Subject: [PATCH 237/274] dev add additional info user --- .../nocopy/service/complaint/ComplaintEntityService.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java b/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java index 94aad5d..0e9b959 100644 --- a/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java @@ -44,15 +44,6 @@ public class ComplaintEntityService { throw new DuplicateComplaintException("Complaint already exists for violation: " + request.getViolationId()); } - String linksJson = null; - if (request.getLinks() != null && !request.getLinks().isEmpty()) { - try { - linksJson = objectMapper.writeValueAsString(request.getLinks()); - } catch (JsonProcessingException e) { - linksJson = request.getLinks().toString(); - } - } - ComplaintEntity complaint = ComplaintEntity.builder() .status(ComplaintStatus.CREATED) .complaintText(request.getComplaintText()) From 46cf252699d266261c8973ca8d6afcc10374c4e2 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 25 Mar 2026 12:11:25 +0700 Subject: [PATCH 238/274] dev add additional info user --- src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java | 2 +- .../ru/soune/nocopy/service/user/AdditionalInfoService.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java index 77efad5..d9a54bd 100644 --- a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java @@ -127,7 +127,7 @@ public class RegRequestHandler implements RequestHandler { referralService.onRegister(authToken.getUser().getId(), regRequest.getReferralLink()); } - additionalInfoService.additionalInfo(regRequest.getIp(), regRequest.getUserAgent()); + additionalInfoService.additionalInfo(regRequest.getIp(), regRequest.getUserAgent(), authToken.getUser()); return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), MessageCode.SUCCESS.getDescription(), regAnswer); diff --git a/src/main/java/ru/soune/nocopy/service/user/AdditionalInfoService.java b/src/main/java/ru/soune/nocopy/service/user/AdditionalInfoService.java index f0016cc..3ca53d2 100644 --- a/src/main/java/ru/soune/nocopy/service/user/AdditionalInfoService.java +++ b/src/main/java/ru/soune/nocopy/service/user/AdditionalInfoService.java @@ -3,6 +3,7 @@ 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; @@ -17,7 +18,7 @@ public class AdditionalInfoService { private final UserAdditionalInfoRepository additionalInfoRepository; @Transactional - public void additionalInfo(String ip, String userAgent) { + public void additionalInfo(String ip, String userAgent, User user) { if (ip == null || userAgent == null) { throw new RegistrationAdditionalInfoNullException("Ip address or user agent is null"); } @@ -25,6 +26,7 @@ public class AdditionalInfoService { UserAdditionalInfo userAdditionalInfo = UserAdditionalInfo.builder() .ipAddress(ip) .userAgent(userAgent) + .user(user) .build(); additionalInfoRepository.save(userAdditionalInfo); From c1782cf59b4562cd8974bd09c52286db0a848487 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 25 Mar 2026 12:46:33 +0700 Subject: [PATCH 239/274] dev add additional info user --- .../soune/nocopy/dto/tarriff/TariffDTO.java | 1 + .../nocopy/dto/tarriff/TariffRequest.java | 3 + .../ru/soune/nocopy/entity/tarif/Tariff.java | 4 + .../nocopy/entity/tarif/TariffTimeTerm.java | 15 +++ .../soune/nocopy/entity/tarif/TariffType.java | 3 +- .../soune/nocopy/handler/TariffHandler.java | 6 +- .../nocopy/handler/TariffInfoHandler.java | 8 +- .../nocopy/repository/TariffRepository.java | 2 + .../service/tariff/TariffInfoService.java | 4 +- .../tariff/TariffInitializationService.java | 91 ++++++++++++++++++- .../nocopy/service/tariff/TariffService.java | 17 +++- 11 files changed, 142 insertions(+), 12 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/entity/tarif/TariffTimeTerm.java diff --git a/src/main/java/ru/soune/nocopy/dto/tarriff/TariffDTO.java b/src/main/java/ru/soune/nocopy/dto/tarriff/TariffDTO.java index 0748a9f..3c7627a 100644 --- a/src/main/java/ru/soune/nocopy/dto/tarriff/TariffDTO.java +++ b/src/main/java/ru/soune/nocopy/dto/tarriff/TariffDTO.java @@ -16,4 +16,5 @@ public class TariffDTO { private int maxFilesCount; private Long diskSize; private Long maxUsers; + private String tariffTerm; } diff --git a/src/main/java/ru/soune/nocopy/dto/tarriff/TariffRequest.java b/src/main/java/ru/soune/nocopy/dto/tarriff/TariffRequest.java index b623e6a..524cccf 100644 --- a/src/main/java/ru/soune/nocopy/dto/tarriff/TariffRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/tarriff/TariffRequest.java @@ -49,4 +49,7 @@ public class TariffRequest { @JsonProperty("user_token") private String userToken; + + @JsonProperty("tariff_term") + private String tariffTerm; } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/entity/tarif/Tariff.java b/src/main/java/ru/soune/nocopy/entity/tarif/Tariff.java index abf1395..3e6d3f6 100644 --- a/src/main/java/ru/soune/nocopy/entity/tarif/Tariff.java +++ b/src/main/java/ru/soune/nocopy/entity/tarif/Tariff.java @@ -40,4 +40,8 @@ public class Tariff { @Column(name = "tariffAccountType") private String tariffAccountType; + + @Column(name = "tariff_term") + @Enumerated(EnumType.STRING) + private TariffTimeTerm tariffTerm; } diff --git a/src/main/java/ru/soune/nocopy/entity/tarif/TariffTimeTerm.java b/src/main/java/ru/soune/nocopy/entity/tarif/TariffTimeTerm.java new file mode 100644 index 0000000..be0c078 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/tarif/TariffTimeTerm.java @@ -0,0 +1,15 @@ +package ru.soune.nocopy.entity.tarif; + +public enum TariffTimeTerm { + MONTHLY(30),YEAR(365); + + private final Integer days; + + TariffTimeTerm(Integer days) { + this.days = days; + } + + public Integer getDays() { + return days; + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java b/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java index aa3eb2b..f48cbb9 100644 --- a/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java +++ b/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java @@ -2,6 +2,7 @@ package ru.soune.nocopy.entity.tarif; public enum TariffType { - START, BASIC, PRO, ENTERPRISE, DEMO, BUISNES, CORPORAT, STUDIO, FREE, TOKEN_100, TOKEN_500, TOKEN_1000, TOKEN_5000, + START, START_YEAR, BASIC, BASIC_YEAR, PRO, PRO_YEAR, ENTERPRISE, ENTERPRISE_YEAR, DEMO, BUISNES, BUISNES_YEAR, + CORPORAT, CORPORAT_YEAR, STUDIO, STUDIO_YEAR, FREE, TOKEN_100, TOKEN_500, TOKEN_1000, TOKEN_5000, MONITORING_DAILY, MONITORING_WEEKLY, MONITORING_MONTHLY; } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/handler/TariffHandler.java b/src/main/java/ru/soune/nocopy/handler/TariffHandler.java index 89d24f0..036bc2a 100644 --- a/src/main/java/ru/soune/nocopy/handler/TariffHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/TariffHandler.java @@ -10,6 +10,7 @@ import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.tarriff.TariffDTO; import ru.soune.nocopy.dto.tarriff.TariffRequest; import ru.soune.nocopy.dto.tarriff.TariffResponse; +import ru.soune.nocopy.entity.tarif.TariffTimeTerm; import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.repository.AuthTokenRepository; @@ -64,7 +65,7 @@ public class TariffHandler implements RequestHandler { private BaseResponse handleAddTariff(BaseRequest request, TariffRequest tariffRequest) { tariffService.addTariff(tariffRequest.getName(), tariffRequest.getPrice(), tariffRequest.getType(), tariffRequest.getTokens(), tariffRequest.getDiskSize(), tariffRequest.getMaxUsers(), - tariffRequest.getMaxFilesCount()); + tariffRequest.getMaxFilesCount(), tariffRequest.getTariffTerm()); TariffResponse tariffResponse = TariffResponse.builder() .tariffName(tariffRequest.getName()) @@ -122,7 +123,8 @@ public class TariffHandler implements RequestHandler { private BaseResponse handleGetTariffs(BaseRequest request, TariffRequest tariffRequest) { AuthToken authToken = authTokenRepository.findByToken(tariffRequest.getUserToken()).orElseThrow(); User user = authToken.getUser(); - List allTariffs = tariffService.getTariffByAccountType(user); + List allTariffs = tariffService.getTariffByAccountTypeAndTariffTerm(user, + TariffTimeTerm.valueOf(tariffRequest.getTariffTerm().toUpperCase())); return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), MessageCode.SUCCESS.getDescription(), diff --git a/src/main/java/ru/soune/nocopy/handler/TariffInfoHandler.java b/src/main/java/ru/soune/nocopy/handler/TariffInfoHandler.java index 7c53c9f..c56dd12 100644 --- a/src/main/java/ru/soune/nocopy/handler/TariffInfoHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/TariffInfoHandler.java @@ -12,6 +12,7 @@ import ru.soune.nocopy.dto.tarriff.TariffInfoRequest; import ru.soune.nocopy.dto.tarriff.TariffInfoResponse; import ru.soune.nocopy.entity.tarif.Tariff; import ru.soune.nocopy.entity.tarif.TariffStatus; +import ru.soune.nocopy.entity.tarif.TariffTimeTerm; import ru.soune.nocopy.entity.tarif.TariffType; import ru.soune.nocopy.repository.TariffRepository; import ru.soune.nocopy.service.tariff.TariffInfoService; @@ -75,13 +76,14 @@ public class TariffInfoHandler implements RequestHandler { try { Long tariffId = tariffInfoRequest.getTariffId(); Tariff tariff= tariffRepository.findById(tariffId).orElseThrow(); + TariffTimeTerm tariffTerm = tariff.getTariffTerm(); + LocalDateTime endDateTime = LocalDateTime.now().plusDays(tariffTerm.getDays()); TariffInfoDTO tariffInfoDTO = new TariffInfoDTO(null, tariffInfoRequest.getStatus() != null ? tariffInfoRequest.getStatus() : TariffStatus.ACTIVE.name(), tariffInfoRequest.getStartTariff() != null ? tariffInfoRequest.getStartTariff() : LocalDateTime.now(), - tariffInfoRequest.getEndTariff() != null ? tariffInfoRequest.getEndTariff() : LocalDateTime.now().plusDays(30), - tariffInfoRequest.getTariffId(), tariff.getName(), tariff.getTokens(), tariff.getDiskSize(), 0L, - 0, tariff.getMaxFilesCount()); + endDateTime, tariffInfoRequest.getTariffId(), tariff.getName(), tariff.getTokens(), + tariff.getDiskSize(), 0L, 0, tariff.getMaxFilesCount()); TariffInfoDTO createdTariffInfo = tariffInfoService.createTariffInfo(tariffInfoDTO); diff --git a/src/main/java/ru/soune/nocopy/repository/TariffRepository.java b/src/main/java/ru/soune/nocopy/repository/TariffRepository.java index a0e3bc9..b39e48a 100644 --- a/src/main/java/ru/soune/nocopy/repository/TariffRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/TariffRepository.java @@ -3,6 +3,7 @@ package ru.soune.nocopy.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import ru.soune.nocopy.entity.tarif.Tariff; +import ru.soune.nocopy.entity.tarif.TariffTimeTerm; import ru.soune.nocopy.entity.tarif.TariffType; import java.util.List; @@ -13,5 +14,6 @@ public interface TariffRepository extends JpaRepository { Optional findByType(String type); List findAllByOrderByPriceAsc(); List findByTariffAccountType(String typeAccount); + List findByTariffAccountTypeAndTariffTerm(String typeAccount, TariffTimeTerm tariffTerm); Tariff findByName(String name); } diff --git a/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java b/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java index 8e2054c..426a6c6 100644 --- a/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java +++ b/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java @@ -75,8 +75,8 @@ 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); diff --git a/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java b/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java index 72244d5..47c743d 100644 --- a/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java +++ b/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java @@ -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,7 @@ public class TariffInitializationService { tariff.setDiskSize(1024L * 1024 * 100); tariff.setMaxUsers(1L); tariff.setTariffAccountType("b2c"); + tariff.setTariffTerm(TariffTimeTerm.MONTHLY); break; case START: @@ -119,6 +119,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 +141,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 +164,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 +186,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 +208,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 +230,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,8 +253,21 @@ 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); diff --git a/src/main/java/ru/soune/nocopy/service/tariff/TariffService.java b/src/main/java/ru/soune/nocopy/service/tariff/TariffService.java index 2149b08..be2c083 100644 --- a/src/main/java/ru/soune/nocopy/service/tariff/TariffService.java +++ b/src/main/java/ru/soune/nocopy/service/tariff/TariffService.java @@ -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,15 @@ public class TariffService { .collect(Collectors.toList()); } + public List 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 getTariffByType(String type) { return tariffRepository.findByTariffAccountType(type) .stream() @@ -99,7 +110,8 @@ public class TariffService { tariff.getTokens(), tariff.getMaxFilesCount(), tariff.getDiskSize(), - tariff.getMaxUsers() + tariff.getMaxUsers(), + tariff.getTariffTerm().name() ); } @@ -111,5 +123,6 @@ public class TariffService { tariff.setMaxFilesCount(dto.getMaxFilesCount()); tariff.setDiskSize(dto.getDiskSize()); tariff.setMaxUsers(dto.getMaxUsers()); + tariff.setTariffTerm(TariffTimeTerm.valueOf(dto.getTariffTerm().toUpperCase())); } } From 8eb07721966a20a106be9b0e92b0ca52d5064d25 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 25 Mar 2026 13:08:10 +0700 Subject: [PATCH 240/274] dev add additional info user --- .../java/ru/soune/nocopy/service/tariff/TariffService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/tariff/TariffService.java b/src/main/java/ru/soune/nocopy/service/tariff/TariffService.java index be2c083..c8309a3 100644 --- a/src/main/java/ru/soune/nocopy/service/tariff/TariffService.java +++ b/src/main/java/ru/soune/nocopy/service/tariff/TariffService.java @@ -102,6 +102,8 @@ public class TariffService { } private TariffDTO convertToDTO(Tariff tariff) { + TariffTimeTerm tariffTerm = tariff.getTariffTerm(); + return new TariffDTO( tariff.getId(), tariff.getType(), @@ -111,7 +113,7 @@ public class TariffService { tariff.getMaxFilesCount(), tariff.getDiskSize(), tariff.getMaxUsers(), - tariff.getTariffTerm().name() + tariffTerm == null ? "" : tariffTerm.name() ); } From 17aa52d4ac13f972167211f36cc274333bd113af Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 25 Mar 2026 15:55:16 +0700 Subject: [PATCH 241/274] dev add new status fo file --- .../nocopy/controller/ApiController.java | 12 +++++++ .../java/ru/soune/nocopy/dto/MessageCode.java | 1 + .../dto/complaint/ComplaintResponse.java | 3 ++ .../soune/nocopy/dto/file/SimilarFileDTO.java | 1 + .../entity/complaint/ComplaintEntity.java | 3 ++ .../soune/nocopy/entity/file/FileEntity.java | 3 ++ .../soune/nocopy/entity/file/FileStatus.java | 5 ++- .../file/moderation/ModerationReason.java | 34 +++++++++++++++++++ .../handler/ComplaintEntityHandler.java | 2 ++ .../repository/ImageSimilarityRepository.java | 1 + .../repository/SimilarImageProjection.java | 1 + .../nocopy/service/FileSimilarityService.java | 30 ++++++++-------- .../complaint/ComplaintEntityService.java | 5 +-- 13 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationReason.java diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 4026dfe..73c505c 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -391,6 +391,18 @@ public class ApiController { Long userId = authService.useUserAuthToken(tokenHeader); FileEntityResponse entityResponse = fileEntityService.getById(fileId, version); + FileStatus status = entityResponse.getStatus(); + + if (status.equals(FileStatus.BLOCKED) || status.equals(FileStatus.DELETED) || + status.equals(FileStatus.REMOVED)) { + Map errorData = new HashMap<>(); + errorData.put("fileId", entityResponse.getId()); + + return ResponseEntity.ok().body(new BaseResponse(20004, + MessageCode.FILE_IS_BLOCKED.getCode(), + MessageCode.FILE_IS_BLOCKED.getDescription(), + errorData)); + } User fileUser = userRepository.findById(entityResponse.getUserId()).orElseThrow(); User user = userRepository.findById(userId).orElseThrow(); diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index 886c0b3..522dca1 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -5,6 +5,7 @@ public enum MessageCode { REG_EMAIL_EXISTS(1, "Email already registered"), REG_EMAIL_OR_PHONE_EXISTS(1, "Email or phone already registered"), REFERRAL_LINK_IS_NOT_EXIST(1, "Refferal link is not exist"), + FILE_IS_BLOCKED(1, "File is blocked,deleted or removed"), INVALID_FIELD(2, "Invalid field"), MAIL_VERIFIED_NULL(2, "Mail verified null"), INVALID_TOKEN(2, "Token not found or time expired"), diff --git a/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintResponse.java b/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintResponse.java index 55efa91..1fe00ac 100644 --- a/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintResponse.java @@ -33,4 +33,7 @@ public class ComplaintResponse { @JsonProperty("updated_at") private String updatedAt; + + @JsonProperty("not_moderated") + private Boolean notModerated; } diff --git a/src/main/java/ru/soune/nocopy/dto/file/SimilarFileDTO.java b/src/main/java/ru/soune/nocopy/dto/file/SimilarFileDTO.java index 971bbd5..d94b0c2 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/SimilarFileDTO.java +++ b/src/main/java/ru/soune/nocopy/dto/file/SimilarFileDTO.java @@ -20,4 +20,5 @@ public class SimilarFileDTO { LocalDateTime uploadDate; String url; Boolean owner; + String fileStatus; } diff --git a/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java index ab2b573..acb19e9 100644 --- a/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java +++ b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java @@ -52,4 +52,7 @@ public class ComplaintEntity { @Column(name = "updated_at") @LastModifiedDate private LocalDateTime updatedAt; + + @Column(name = "not_moderated_file") + private Boolean not_moderated_file; } diff --git a/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java b/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java index 02104c7..a02e1cc 100644 --- a/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java +++ b/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java @@ -87,6 +87,9 @@ public class FileEntity { @Column(name = "medium_path") private String mediumPath; + @Column(name = "moderation_reason_id") + private Long moderationReasonId; + @OneToOne(mappedBy = "file", cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnore private ImageHashEntity imageHash; diff --git a/src/main/java/ru/soune/nocopy/entity/file/FileStatus.java b/src/main/java/ru/soune/nocopy/entity/file/FileStatus.java index cdd4fc1..a23ead9 100644 --- a/src/main/java/ru/soune/nocopy/entity/file/FileStatus.java +++ b/src/main/java/ru/soune/nocopy/entity/file/FileStatus.java @@ -7,5 +7,8 @@ public enum FileStatus { VIOLATION, CHECKED, ERROR, - TEMP + TEMP, + REMOVED, + MODERATION, + BLOCKED } diff --git a/src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationReason.java b/src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationReason.java new file mode 100644 index 0000000..e62c302 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationReason.java @@ -0,0 +1,34 @@ +package ru.soune.nocopy.entity.file.moderation; + +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "moderation_reasons") +@EntityListeners(AuditingEntityListener.class) +public class ModerationReason { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "reason_text", nullable = false, columnDefinition = "TEXT") + private String reasonText; + + @CreatedDate + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at") + private LocalDateTime updatedAt; +} diff --git a/src/main/java/ru/soune/nocopy/handler/ComplaintEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/ComplaintEntityHandler.java index a046205..c0ebc7b 100644 --- a/src/main/java/ru/soune/nocopy/handler/ComplaintEntityHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ComplaintEntityHandler.java @@ -63,6 +63,7 @@ public class ComplaintEntityHandler implements RequestHandler { "text is required"); ComplaintResponse response = complaintService.createComplaint(req); + return successResponse(msgId, "Complaint created successfully", response); } @@ -117,6 +118,7 @@ public class ComplaintEntityHandler implements RequestHandler { if (req.getId() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), "id is required"); ComplaintResponse response = complaintService.updateComplaint(req.getId(), req); + return successResponse(msgId, "Complaint updated successfully", response); } diff --git a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java index 4144760..1d78c71 100644 --- a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java @@ -22,6 +22,7 @@ public interface ImageSimilarityRepository f.created_at AS uploadDate, f.protection_status AS protectionStatus, f.file_extension AS extension, + f.status AS status, h.hash64_hi AS hash64Hi, h.hash64_lo AS hash64Lo FROM image_hashes ref diff --git a/src/main/java/ru/soune/nocopy/repository/SimilarImageProjection.java b/src/main/java/ru/soune/nocopy/repository/SimilarImageProjection.java index 4d3f47f..cf3fe4b 100644 --- a/src/main/java/ru/soune/nocopy/repository/SimilarImageProjection.java +++ b/src/main/java/ru/soune/nocopy/repository/SimilarImageProjection.java @@ -15,4 +15,5 @@ public interface SimilarImageProjection { LocalDateTime getUploadDate(); ProtectionStatus getProtectionStatus(); String getExtension(); + String getFileStatus(); } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 2a5ea95..0db4e92 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -12,6 +12,7 @@ import org.springframework.stereotype.Service; import ru.soune.nocopy.dto.file.SimilarFileDTO; 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.entity.user.User; import ru.soune.nocopy.exception.DuplicateImageException; @@ -154,22 +155,19 @@ public class FileSimilarityService { var imageHashEntity = hashRepository.findById(fileId) .orElseThrow(() -> new RuntimeException("Hash not found for file: " + fileId)); - List candidates; + List userIds; -// if (authToken == null || "all".equals(authToken)) { - candidates = repository.findCandidates(fileId); -// } else { - 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()); -// candidates = repository.findCandidatesFromUserFiles(fileId, userIds); - } else { - userIds = List.of(userId); -// candidates = repository.findCandidatesFromUserFiles(fileId, userId); - } -// } + List 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 levels = (similarityLevels != null && !similarityLevels.isEmpty()) ? similarityLevels @@ -180,6 +178,7 @@ public class FileSimilarityService { imageHashEntity.getHash64Hi(), imageHashEntity.getHash64Lo(), userIds)) .filter(dto -> levels.contains(dto.getSimilarityLevel())) + .filter(dto -> dto.getFileStatus().equals(FileStatus.ACTIVE.name())) .sorted(Comparator.comparingInt(SimilarFileDTO::getHammingDistance)) .collect(Collectors.toList()); @@ -243,6 +242,7 @@ public class FileSimilarityService { .owner(userIds.contains(similarImageProjection.getUserId())) // .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId()) .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId() + "/thumbnail") + .fileStatus(similarImageProjection.getFileStatus()) .build(); } diff --git a/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java b/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java index 0e9b959..bf6f509 100644 --- a/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java @@ -12,6 +12,7 @@ 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; @@ -32,8 +33,6 @@ public class ComplaintEntityService { private final ViolationRepository violationRepository; - private final ObjectMapper objectMapper; - private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); public ComplaintResponse createComplaint(ComplaintRequest request) { @@ -49,6 +48,7 @@ public class ComplaintEntityService { .complaintText(request.getComplaintText()) .email(request.getEmail()) .violation(violation) + .not_moderated_file(violation.getFileEntity().getStatus().equals(FileStatus.MODERATION)) .build(); return mapToResponse(complaintRepository.save(complaint)); @@ -116,6 +116,7 @@ public class ComplaintEntityService { .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(); } } From 2f38a7c455be6511bfe30fef65bc8a0fd95a4c91 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 25 Mar 2026 16:12:26 +0700 Subject: [PATCH 242/274] dev add new status fo file --- src/main/java/ru/soune/nocopy/service/FileSimilarityService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 0db4e92..66f0167 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -255,6 +255,7 @@ public class FileSimilarityService { "." + fileEntity.getFileExtension()) .fileSize(fileEntity.getFileSize()) .uploadDate(fileEntity.getCreatedAt()) + .fileStatus(fileEntity.getStatus().name()) .status(fileEntity.getProtectionStatus()) .supportId(Long.valueOf(fileEntity.getSupportId())) .build(); From a3e6e0091af7be4d5c7d7815c4ae510495c41afe Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 25 Mar 2026 16:24:19 +0700 Subject: [PATCH 243/274] dev add new status fo file --- .../service/tariff/TariffInitializationService.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java b/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java index 47c743d..f5ae48e 100644 --- a/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java +++ b/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java @@ -111,6 +111,17 @@ public class TariffInitializationService { 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: tariff.setName("start"); tariff.setPrice(1200); From 95ebcf0241728a54b992af7761a548fa264523ea Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 25 Mar 2026 16:31:21 +0700 Subject: [PATCH 244/274] dev add new status fo file --- .../java/ru/soune/nocopy/entity/tarif/TariffTimeTerm.java | 6 +++--- .../soune/nocopy/repository/ImageSimilarityRepository.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/entity/tarif/TariffTimeTerm.java b/src/main/java/ru/soune/nocopy/entity/tarif/TariffTimeTerm.java index be0c078..b3a9441 100644 --- a/src/main/java/ru/soune/nocopy/entity/tarif/TariffTimeTerm.java +++ b/src/main/java/ru/soune/nocopy/entity/tarif/TariffTimeTerm.java @@ -1,5 +1,8 @@ package ru.soune.nocopy.entity.tarif; +import lombok.Getter; + +@Getter public enum TariffTimeTerm { MONTHLY(30),YEAR(365); @@ -9,7 +12,4 @@ public enum TariffTimeTerm { this.days = days; } - public Integer getDays() { - return days; - } } diff --git a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java index 1d78c71..6c686f3 100644 --- a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java @@ -22,7 +22,7 @@ public interface ImageSimilarityRepository f.created_at AS uploadDate, f.protection_status AS protectionStatus, f.file_extension AS extension, - f.status AS status, + f.status AS fileStatus, h.hash64_hi AS hash64Hi, h.hash64_lo AS hash64Lo FROM image_hashes ref From a21eb899b33ae973410abc9107ac2e38d7b2f06c Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 26 Mar 2026 11:40:32 +0700 Subject: [PATCH 245/274] dev add new status fo file --- .../nocopy/controller/PayoutController.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/PayoutController.java b/src/main/java/ru/soune/nocopy/controller/PayoutController.java index ba8b827..39246f5 100644 --- a/src/main/java/ru/soune/nocopy/controller/PayoutController.java +++ b/src/main/java/ru/soune/nocopy/controller/PayoutController.java @@ -14,13 +14,16 @@ import ru.soune.nocopy.entity.payout.PayoutRequest; import ru.soune.nocopy.entity.payout.PayoutType; import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.exception.InsufficientFundsException; import ru.soune.nocopy.exception.NotFoundAuthToken; +import ru.soune.nocopy.exception.PendingPayoutExistsException; import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.service.payout.PayoutMethodService; import ru.soune.nocopy.service.payout.PayoutRequestService; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -40,8 +43,8 @@ public class PayoutController { Create payout */ @PostMapping("/create-request") - public ResponseEntity createPayoutRequest(@RequestHeader("Authorization") String tokenHeader, - @Valid @RequestBody PayoutRequestDTO request) { + public ResponseEntity createPayoutRequest(@RequestHeader("Authorization") String tokenHeader, + @Valid @RequestBody PayoutRequestDTO request) { String token = tokenHeader.replace("Bearer ", ""); Optional tokenOptional = authTokenRepository.findByToken(token); @@ -50,9 +53,12 @@ public class PayoutController { User user = authToken.getUser(); - PayoutRequest payoutRequest = payoutRequestService.createPayoutRequest(user.getId(), request); - - return ResponseEntity.status(HttpStatus.CREATED).body(payoutRequest); + try { + PayoutRequest payoutRequest = payoutRequestService.createPayoutRequest(user.getId(), request); + return ResponseEntity.status(HttpStatus.CREATED).body(payoutRequest); + } catch (InsufficientFundsException | PendingPayoutExistsException e) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(e.getMessage()); + } } /* From 8965ffdd1fc252a313c5484c2fa84fc3b5d72639 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 26 Mar 2026 14:34:34 +0700 Subject: [PATCH 246/274] dev add new status fo file --- .../nocopy/dto/violation/ViolationRequest.java | 3 +++ .../nocopy/entity/complaint/ComplaintStatus.java | 1 - .../ru/soune/nocopy/handler/ViolationHandler.java | 13 +++++++++++++ .../service/complaint/ComplaintEntityService.java | 7 +++++++ .../nocopy/service/violation/ViolationService.java | 11 ++++++++++- .../nocopy/service/violation/ViolationStatus.java | 5 +++++ 6 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/service/violation/ViolationStatus.java diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationRequest.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationRequest.java index 5c7a2d5..1edf968 100644 --- a/src/main/java/ru/soune/nocopy/dto/violation/ViolationRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationRequest.java @@ -10,6 +10,9 @@ public class ViolationRequest { @JsonProperty("file_id") private String fileId; + @JsonProperty("violation_id") + private Long violationId; + @JsonProperty("page") private Integer page = 0; diff --git a/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintStatus.java b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintStatus.java index ca5deed..7be6b2e 100644 --- a/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintStatus.java +++ b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintStatus.java @@ -1,6 +1,5 @@ package ru.soune.nocopy.entity.complaint; public enum ComplaintStatus { - //NEW , SHOWED, LEGAL_IN_WORK, ЖАЛОБА, 3 + 4 , САНКЦАНИРОВАННОЕ ИСПОЛЬЗОВАНИЕ CREATED, SHOWED, IN_WORK, COMPLETED, CANCELLED } diff --git a/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java b/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java index 9bff5a2..8b658b4 100644 --- a/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java @@ -15,6 +15,7 @@ import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.violation.ViolationService; +import ru.soune.nocopy.service.violation.ViolationStatus; import java.io.FileNotFoundException; import java.time.LocalDateTime; @@ -66,6 +67,18 @@ public class ViolationHandler implements RequestHandler { endDate = parseDate(violationRequest.getEndDate()); } + if ("updateStatus".equals(violationRequest.getAction())) { + String status = violationRequest.getStatus(); + Long violationId = violationRequest.getViolationId(); + + violationService.changeStatus(ViolationStatus.valueOf(status), violationId); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(Map.of("status", status)) + .build(); + } + if ("group".equalsIgnoreCase(violationRequest.getAction())) { Map groupedData = violationService.getGroupedViolations( targetFiles, diff --git a/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java b/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java index bf6f509..3287a0b 100644 --- a/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java @@ -19,6 +19,8 @@ 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; @@ -33,6 +35,8 @@ public class ComplaintEntityService { 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) { @@ -51,6 +55,8 @@ public class ComplaintEntityService { .not_moderated_file(violation.getFileEntity().getStatus().equals(FileStatus.MODERATION)) .build(); + violationService.changeStatus(ViolationStatus.COMPLAINT_IN_WORK, request.getViolationId()); + return mapToResponse(complaintRepository.save(complaint)); } @@ -95,6 +101,7 @@ public class ComplaintEntityService { } 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)); } diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java index 6174d01..57f9af5 100644 --- a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java @@ -7,6 +7,7 @@ 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.ViolationInfoDTO; @@ -51,7 +52,7 @@ public class ViolationService { violation.setPageUrl(imageResult.getPageUrl()); violation.setPageTitle(imageResult.getPageTitle()); violation.setFileEntity(file); - violation.setStatus("CREATED"); + violation.setStatus(ViolationStatus.NEW.name()); violationRepository.save(violation); } @@ -94,6 +95,14 @@ public class ViolationService { .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 getViolationsByFiles(List 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); diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationStatus.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationStatus.java new file mode 100644 index 0000000..6deac03 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationStatus.java @@ -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 +} From a01adfc848cd30e222d922ee021b19c19bee577a Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 26 Mar 2026 14:42:38 +0700 Subject: [PATCH 247/274] dev add new status fo file --- .../service/violation/ViolationService.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java index 57f9af5..8f8fad3 100644 --- a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java @@ -10,12 +10,10 @@ 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.ViolationInfoDTO; 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.exception.ViolationNotFoundException; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.ViolationRepository; import ru.soune.nocopy.service.file.FileEntityService; @@ -192,22 +190,23 @@ public class ViolationService { violations = violationRepository.findByFileEntityIn(userFiles); } + Set 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 -> "CREATED".equalsIgnoreCase(v.getStatus()) || - "NEW".equalsIgnoreCase(v.getStatus())) + .filter(v -> v.getStatus().equals(ViolationStatus.NEW.name())) .count(); long inProgressViolations = violations.stream() - .filter(v -> "IN_PROGRESS".equalsIgnoreCase(v.getStatus()) || - "PROCESSING".equalsIgnoreCase(v.getStatus()) || - "IN_WORK".equalsIgnoreCase(v.getStatus())) + .filter(v -> inProgressStatuses.contains(v.getStatus())) .count(); long resolvedViolations = violations.stream() - .filter(v -> "RESOLVED".equalsIgnoreCase(v.getStatus()) || - "COMPLETED".equalsIgnoreCase(v.getStatus()) || - "SOLVED".equalsIgnoreCase(v.getStatus()) || - "DONE".equalsIgnoreCase(v.getStatus())) + .filter(v -> v.getStatus().equals(ViolationStatus.AUTHORIZED_USE.name())) .count(); return ViolationStatisticsResponse.builder() From 57dcc12cf2482219ddedb7e953509e2384cbac3a Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 27 Mar 2026 13:37:37 +0700 Subject: [PATCH 248/274] dev add new status fo file --- .../ru/soune/nocopy/dto/file/AppealInfo.java | 14 + .../soune/nocopy/dto/file/AppealRequest.java | 13 + .../soune/nocopy/dto/file/AppealResponse.java | 19 + .../nocopy/dto/file/FileEntityRequest.java | 22 ++ .../nocopy/dto/file/FileModerationInfo.java | 21 + .../dto/file/ModerationActionRequest.java | 22 ++ .../nocopy/entity/file/AppealStatus.java | 9 + .../soune/nocopy/entity/file/FileAppeal.java | 84 ++++ .../entity/file/moderation/ModerationLog.java | 56 +++ .../exception/InvalidAppealException.java | 7 + .../nocopy/handler/FileEntityHandler.java | 369 ++++++++++++++++++ .../repository/FileAppealRepository.java | 28 ++ .../repository/FileEntityRepository.java | 6 + .../repository/ModerationLogRepository.java | 13 + .../service/file/FileEntityService.java | 11 +- .../impl/NoCopyProcessingListenerImpl.java | 8 +- .../file/moderation/ModerationService.java | 236 +++++++++++ 17 files changed, 932 insertions(+), 6 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/file/AppealInfo.java create mode 100644 src/main/java/ru/soune/nocopy/dto/file/AppealRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/file/AppealResponse.java create mode 100644 src/main/java/ru/soune/nocopy/dto/file/FileModerationInfo.java create mode 100644 src/main/java/ru/soune/nocopy/dto/file/ModerationActionRequest.java create mode 100644 src/main/java/ru/soune/nocopy/entity/file/AppealStatus.java create mode 100644 src/main/java/ru/soune/nocopy/entity/file/FileAppeal.java create mode 100644 src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationLog.java create mode 100644 src/main/java/ru/soune/nocopy/exception/InvalidAppealException.java create mode 100644 src/main/java/ru/soune/nocopy/repository/FileAppealRepository.java create mode 100644 src/main/java/ru/soune/nocopy/repository/ModerationLogRepository.java create mode 100644 src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java diff --git a/src/main/java/ru/soune/nocopy/dto/file/AppealInfo.java b/src/main/java/ru/soune/nocopy/dto/file/AppealInfo.java new file mode 100644 index 0000000..6c2bc7c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/file/AppealInfo.java @@ -0,0 +1,14 @@ +package ru.soune.nocopy.dto.file; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@Builder +public class AppealInfo { + private String appealId; + private String status; + private LocalDateTime createdAt; +} diff --git a/src/main/java/ru/soune/nocopy/dto/file/AppealRequest.java b/src/main/java/ru/soune/nocopy/dto/file/AppealRequest.java new file mode 100644 index 0000000..1590820 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/file/AppealRequest.java @@ -0,0 +1,13 @@ +package ru.soune.nocopy.dto.file; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class AppealRequest { + private String fileId; + private String appealReason; + private String additionalInfo; + private String token; +} diff --git a/src/main/java/ru/soune/nocopy/dto/file/AppealResponse.java b/src/main/java/ru/soune/nocopy/dto/file/AppealResponse.java new file mode 100644 index 0000000..60739b6 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/file/AppealResponse.java @@ -0,0 +1,19 @@ +package ru.soune.nocopy.dto.file; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@Builder +public class AppealResponse { + private String appealId; + private String fileId; + private String appealReason; + private String additionalInfo; + private String status; + private String adminComment; + private LocalDateTime createdAt; + private LocalDateTime resolvedAt; +} diff --git a/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java b/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java index c21f7b4..8159ceb 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java @@ -5,6 +5,7 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import ru.soune.nocopy.entity.file.FileStatus; @Data @Builder @@ -44,6 +45,27 @@ public class FileEntityRequest { @JsonProperty("type") private String type; + @JsonProperty("file_status") + private FileStatus fileStatus; + @JsonProperty("date_filter") private String dateFilter; + + @JsonProperty("moderation_reason_id") + private Long moderationReasonId; + + @JsonProperty("comment") + private String comment; + + @JsonProperty("appeal_reason") + private String appealReason; + + @JsonProperty("additional_info") + private String additionalInfo; + + @JsonProperty("appeal_id") + private String appealId; + + @JsonProperty("approve") + private Boolean approve; } diff --git a/src/main/java/ru/soune/nocopy/dto/file/FileModerationInfo.java b/src/main/java/ru/soune/nocopy/dto/file/FileModerationInfo.java new file mode 100644 index 0000000..fbe409c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/file/FileModerationInfo.java @@ -0,0 +1,21 @@ +package ru.soune.nocopy.dto.file; + +import lombok.Builder; +import lombok.Data; +import ru.soune.nocopy.entity.file.FileStatus; + +import java.time.LocalDateTime; + +@Data +@Builder +public class FileModerationInfo { + private String fileId; + private String fileName; + private String ownerName; + private String ownerEmail; + private FileStatus currentStatus; + private LocalDateTime uploadDate; + private String moderationReason; + private Boolean hasActiveAppeal; + private AppealInfo activeAppeal; +} diff --git a/src/main/java/ru/soune/nocopy/dto/file/ModerationActionRequest.java b/src/main/java/ru/soune/nocopy/dto/file/ModerationActionRequest.java new file mode 100644 index 0000000..ad6f6b1 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/file/ModerationActionRequest.java @@ -0,0 +1,22 @@ +package ru.soune.nocopy.dto.file; + +import lombok.Builder; +import lombok.Data; +import ru.soune.nocopy.entity.file.FileStatus; + +@Data +@Builder +public class ModerationActionRequest { + private String fileId; + private FileStatus newStatus; + private Long moderationReasonId; + private String comment; + private String token; +} + + + + + + + diff --git a/src/main/java/ru/soune/nocopy/entity/file/AppealStatus.java b/src/main/java/ru/soune/nocopy/entity/file/AppealStatus.java new file mode 100644 index 0000000..01018fb --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/file/AppealStatus.java @@ -0,0 +1,9 @@ +package ru.soune.nocopy.entity.file; + +public enum AppealStatus { + PENDING, + IN_REVIEW, + APPROVED, + REJECTED, + EXPIRED +} diff --git a/src/main/java/ru/soune/nocopy/entity/file/FileAppeal.java b/src/main/java/ru/soune/nocopy/entity/file/FileAppeal.java new file mode 100644 index 0000000..36a5553 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/file/FileAppeal.java @@ -0,0 +1,84 @@ +package ru.soune.nocopy.entity.file; + +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "file_appeals") +@EntityListeners(AuditingEntityListener.class) +public class FileAppeal { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; + + @Column(name = "file_id", nullable = false) + private String fileId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "file_id", insertable = false, updatable = false) + @ToString.Exclude + private FileEntity file; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "appeal_reason", nullable = false, columnDefinition = "TEXT") + private String appealReason; + + @Column(name = "additional_info", columnDefinition = "TEXT") + private String additionalInfo; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + private AppealStatus status; + + @Column(name = "admin_comment", columnDefinition = "TEXT") + private String adminComment; + + @Column(name = "moderator_id") + private Long moderatorId; + + @Column(name = "moderation_before_status") + @Enumerated(EnumType.STRING) + private FileStatus moderationBeforeStatus; + + @Column(name = "moderation_reason_id") + private Long moderationReasonId; + + @Column(name = "resolved_at") + private LocalDateTime resolvedAt; + + @CreatedDate + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @PrePersist + public void prePersist() { + if (this.status == null) { + this.status = AppealStatus.PENDING; + } + if (this.createdAt == null) { + this.createdAt = LocalDateTime.now(); + } + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + public void preUpdate() { + this.updatedAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationLog.java b/src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationLog.java new file mode 100644 index 0000000..04249a7 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationLog.java @@ -0,0 +1,56 @@ +package ru.soune.nocopy.entity.file.moderation; + +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.file.FileStatus; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "moderation_logs") +@EntityListeners(AuditingEntityListener.class) +public class ModerationLog { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; + + @Column(name = "file_id", nullable = false) + private String fileId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "file_id", insertable = false, updatable = false) + @ToString.Exclude + private FileEntity file; + + @Column(name = "moderator_id", nullable = false) + private Long moderatorId; + + @Enumerated(EnumType.STRING) + @Column(name = "old_status", nullable = false) + private FileStatus oldStatus; + + @Enumerated(EnumType.STRING) + @Column(name = "new_status", nullable = false) + private FileStatus newStatus; + + @Column(name = "reason", columnDefinition = "TEXT") + private String reason; + + @Column(name = "moderation_reason_id") + private Long moderationReasonId; + + @Column(name = "comment", columnDefinition = "TEXT") + private String comment; + + @CreatedDate + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; +} diff --git a/src/main/java/ru/soune/nocopy/exception/InvalidAppealException.java b/src/main/java/ru/soune/nocopy/exception/InvalidAppealException.java new file mode 100644 index 0000000..b2ba888 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/InvalidAppealException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class InvalidAppealException extends RuntimeException { + public InvalidAppealException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java index 210b5c6..0fc33b2 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java @@ -3,15 +3,22 @@ package ru.soune.nocopy.handler; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; import org.springframework.stereotype.Component; import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.file.*; +import ru.soune.nocopy.entity.file.FileAppeal; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.ProtectionStatus; +import ru.soune.nocopy.entity.file.moderation.ModerationLog; import ru.soune.nocopy.exception.FileEntityNotFoundException; +import ru.soune.nocopy.exception.InvalidAppealException; import ru.soune.nocopy.exception.NotFoundAuthToken; +import ru.soune.nocopy.repository.FileAppealRepository; import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.repository.ModerationLogRepository; +import ru.soune.nocopy.service.file.moderation.ModerationService; import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileStatsService; @@ -35,6 +42,12 @@ public class FileEntityHandler implements RequestHandler { private final FileEntityRepository fileEntityRepository; + private final ModerationService moderationService; + + private final FileAppealRepository fileAppealRepository; + + private final ModerationLogRepository moderationLogRepository; + @Override public BaseResponse handle(BaseRequest request) { try { @@ -60,6 +73,18 @@ public class FileEntityHandler implements RequestHandler { return handleProtectFile(request, fileRequest); case "delete_file": return handleDeleteFile(request, fileRequest); + case "change_file_status": + return handleChangeStatus(request, fileRequest); + case "moderate_file": + return handleModerateFile(request, fileRequest); + case "submit_appeal": + return handleSubmitAppeal(request, fileRequest); + case "review_appeal": + return handleReviewAppeal(request, fileRequest); + case "user_appeals": + return handleGetUserAppeals(request, fileRequest); + case "files_for_moderation": + return handleGetFilesForModeration(request, fileRequest); default: ActionResponse response = ActionResponse.builder() .action(action) @@ -183,6 +208,23 @@ public class FileEntityHandler implements RequestHandler { } } + private BaseResponse handleChangeStatus(BaseRequest request, FileEntityRequest fileRequest) { + FileStatus newStatus = fileRequest.getFileStatus(); + try { + fileEntityService.changeFileStatus(newStatus ,fileRequest.getFileId()); + } catch (FileEntityNotFoundException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.FILE_NOT_FOUND.getCode(), + MessageCode.FILE_NOT_FOUND.getDescription(), + null); + } + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + Map.of("new_status", newStatus)); + } + private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) { try { Long userId = authService.useUserAuthToken(fileRequest.getToken()); @@ -568,6 +610,333 @@ public class FileEntityHandler implements RequestHandler { } } + private BaseResponse handleModerateFile(BaseRequest request, FileEntityRequest fileRequest) { + try { + //TODO добавить проверку на админа +// Long moderatorId = authService.useAdminAuthToken(fileRequest.getToken()); + + ModerationActionRequest moderationRequest = ModerationActionRequest.builder() + .fileId(fileRequest.getFileId()) + .newStatus(fileRequest.getFileStatus()) + .moderationReasonId(fileRequest.getModerationReasonId()) + .comment(fileRequest.getComment()) + .build(); + + //TODO set moderator ID + moderationService.moderateFile(moderationRequest, 0L); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + "File moderated successfully", + null); + + } catch (SecurityException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.ACCESS_DENIED.getCode(), + e.getMessage(), + null); + } catch (Exception e) { + log.error("Error moderating file", e); + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "Failed to moderate file: " + e.getMessage(), + null); + } + } + + private BaseResponse handleSubmitAppeal(BaseRequest request, FileEntityRequest fileRequest) { + try { + Long userId = authService.useUserAuthToken(fileRequest.getToken()); + + AppealRequest appealRequest = AppealRequest.builder() + .fileId(fileRequest.getFileId()) + .appealReason(fileRequest.getAppealReason()) + .additionalInfo(fileRequest.getAdditionalInfo()) + .build(); + + AppealResponse response = moderationService.submitAppeal(appealRequest, userId); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + "Appeal submitted successfully", + response); + + } catch (InvalidAppealException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + e.getMessage(), + null); + } catch (Exception e) { + log.error("Error submitting appeal", e); + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "Failed to submit appeal: " + e.getMessage(), + null); + } + } + + private BaseResponse handleReviewAppeal(BaseRequest request, FileEntityRequest fileRequest) { + try { + // TODO +// Long moderatorId = authService.useAdminAuthToken(fileRequest.getToken()); + + String appealId = fileRequest.getAppealId(); + Boolean approve = fileRequest.getApprove(); + String comment = fileRequest.getComment(); + + if (appealId == null || approve == null) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "appealId and approve are required", + null); + } + //TODO + moderationService.reviewAppeal(appealId, approve, comment, 0L); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + "Appeal reviewed successfully", + Map.of( + "appealId", appealId, + "approved", approve, + "comment", comment + )); + + } catch (SecurityException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.ACCESS_DENIED.getCode(), + "Admin access required", + null); + } catch (IllegalStateException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + e.getMessage(), + null); + } catch (Exception e) { + log.error("Error reviewing appeal", e); + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "Failed to review appeal: " + e.getMessage(), + null); + } + } + + /** + * Получение списка апелляций пользователя + */ + private BaseResponse handleGetUserAppeals(BaseRequest request, FileEntityRequest fileRequest) { + try { + Long userId = authService.useUserAuthToken(fileRequest.getToken()); + + int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1; + int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20; + + Page appeals = moderationService.getUserAppeals(userId, page - 1, pageSize); + + Map response = Map.of( + "appeals", appeals.getContent(), + "totalCount", appeals.getTotalElements(), + "totalPages", appeals.getTotalPages(), + "currentPage", page, + "pageSize", pageSize); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + response); + + } catch (SecurityException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_TOKEN.getCode(), + "Authentication required", + null); + } catch (Exception e) { + log.error("Error getting user appeals", e); + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "Failed to get appeals: " + e.getMessage(), + null); + } + } + + /** + * Получение списка файлов на модерации + */ + private BaseResponse handleGetFilesForModeration(BaseRequest request, FileEntityRequest fileRequest) { + try { + //TODO +// authService.useAdminAuthToken(fileRequest.getToken()); + + int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1; + int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20; + + Page files = moderationService.getFilesForModeration(page - 1, pageSize); + + List> filesWithInfo = files.getContent().stream() + .map(file -> { + FileModerationInfo info = moderationService.getFileModerationInfo(file.getId()); + return Map.of( + "fileId", file.getId(), + "fileName", file.getOriginalFileName(), + "userId", file.getUserId(), + "status", file.getStatus().name(), + "createdAt", file.getCreatedAt(), + "fileSize", file.getFileSize(), + "formattedSize", fileEntityService.formatFileSize(file.getFileSize()), + "moderationInfo", Map.of( + "hasActiveAppeal", info.getHasActiveAppeal(), + "appealInfo", info.getActiveAppeal() != null ? + Map.of( + "appealId", info.getActiveAppeal().getAppealId(), + "status", info.getActiveAppeal().getStatus(), + "createdAt", info.getActiveAppeal().getCreatedAt() + ) : null + ) + ); + }) + .collect(Collectors.toList()); + + Map response = Map.of( + "files", filesWithInfo, + "totalCount", files.getTotalElements(), + "totalPages", files.getTotalPages(), + "currentPage", page, + "pageSize", pageSize + ); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + response); + + } catch (SecurityException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.ACCESS_DENIED.getCode(), + "Admin access required", + null); + } catch (Exception e) { + log.error("Error getting files for moderation", e); + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "Failed to get files: " + e.getMessage(), + null); + } + } + + /** + * Получение детальной информации о файле для модерации + */ + private BaseResponse handleGetFileModerationInfo(BaseRequest request, FileEntityRequest fileRequest) { + try { + // TODO + // authService.useAdminAuthToken(fileRequest.getToken()); + + String fileId = fileRequest.getFileId(); + if (fileId == null) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "fileId is required", + null); + } + + FileModerationInfo moderationInfo = moderationService.getFileModerationInfo(fileId); + + FileEntity file = fileEntityRepository.findById(fileId) + .orElseThrow(() -> new FileEntityNotFoundException(fileId)); + + Map fileInfoMap = new HashMap<>(); + fileInfoMap.put("id", file.getId()); + fileInfoMap.put("fileName", file.getOriginalFileName()); + fileInfoMap.put("storedFileName", file.getStoredFileName()); + fileInfoMap.put("fileSize", file.getFileSize()); + fileInfoMap.put("formattedSize", fileEntityService.formatFileSize(file.getFileSize())); + fileInfoMap.put("mimeType", file.getMimeType()); + fileInfoMap.put("fileExtension", file.getFileExtension()); + fileInfoMap.put("checksum", file.getChecksum()); + fileInfoMap.put("status", file.getStatus() != null ? file.getStatus().name() : null); + fileInfoMap.put("createdAt", file.getCreatedAt()); + fileInfoMap.put("updatedAt", file.getUpdatedAt()); + + Map response = new HashMap<>(); + response.put("fileInfo", fileInfoMap); + response.put("moderationInfo", moderationInfo != null ? moderationInfo : new HashMap<>()); + response.put("moderationHistory", getModerationHistory(fileId)); + response.put("appeals", getFileAppeals(fileId)); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + response); + + } catch (SecurityException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.ACCESS_DENIED.getCode(), + "Admin access required", + null); + } catch (FileEntityNotFoundException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.FILE_NOT_FOUND.getCode(), + e.getMessage(), + null); + } catch (Exception e) { + log.error("Error getting file moderation info", e); + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "Failed to get file info: " + e.getMessage(), + null); + } + } + + /** + * Получение истории модерации файла + */ + private List> getModerationHistory(String fileId) { + List logs = moderationLogRepository.findByFileIdOrderByCreatedAtDesc(fileId); + if (logs == null || logs.isEmpty()) { + return Collections.emptyList(); + } + + return logs.stream() + .map(log -> { + Map logMap = new HashMap<>(); + logMap.put("id", log.getId()); + logMap.put("moderatorId", log.getModeratorId()); + logMap.put("oldStatus", log.getOldStatus() != null ? log.getOldStatus().name() : null); + logMap.put("newStatus", log.getNewStatus() != null ? log.getNewStatus().name() : null); + logMap.put("reason", log.getReason()); + logMap.put("comment", log.getComment()); + logMap.put("createdAt", log.getCreatedAt()); + return logMap; + }) + .collect(Collectors.toList()); + } + + /** + * Получение всех апелляций файла + */ + private List> getFileAppeals(String fileId) { + List appeals = fileAppealRepository.findByFileId(fileId); + if (appeals == null || appeals.isEmpty()) { + return Collections.emptyList(); + } + + return appeals.stream() + .map(appeal -> { + Map appealMap = new HashMap<>(); + appealMap.put("id", appeal.getId()); + appealMap.put("userId", appeal.getUserId()); + appealMap.put("appealReason", appeal.getAppealReason()); + appealMap.put("additionalInfo", appeal.getAdditionalInfo()); + appealMap.put("status", appeal.getStatus() != null ? appeal.getStatus().name() : null); + appealMap.put("adminComment", appeal.getAdminComment()); + appealMap.put("moderatorId", appeal.getModeratorId()); + appealMap.put("createdAt", appeal.getCreatedAt()); + appealMap.put("resolvedAt", appeal.getResolvedAt()); + return appealMap; + }) + .collect(Collectors.toList()); + } + private String formatFileSize(long size) { if (size < 1024) return size + " B"; int exp = (int) (Math.log(size) / Math.log(1024)); diff --git a/src/main/java/ru/soune/nocopy/repository/FileAppealRepository.java b/src/main/java/ru/soune/nocopy/repository/FileAppealRepository.java new file mode 100644 index 0000000..c645496 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/FileAppealRepository.java @@ -0,0 +1,28 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import ru.soune.nocopy.entity.file.FileAppeal; +import ru.soune.nocopy.entity.file.AppealStatus; +import java.util.List; +import java.util.Optional; + +public interface FileAppealRepository extends JpaRepository { + + Optional findByFileIdAndStatus(String fileId, AppealStatus status); + + List findByFileId(String fileId); + + Page findByStatus(AppealStatus status, Pageable pageable); + + Page findByUserId(Long userId, Pageable pageable); + + @Query("SELECT fa FROM FileAppeal fa WHERE fa.status = :status ORDER BY fa.createdAt ASC") + Page findPendingAppeals(@Param("status") AppealStatus status, Pageable pageable); + + boolean existsByFileIdAndStatusIn(String fileId, List statuses); +} + diff --git a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java index 234cbc4..0e5e014 100644 --- a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java @@ -1,5 +1,7 @@ package ru.soune.nocopy.repository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -66,4 +68,8 @@ public interface FileEntityRepository extends JpaRepository Integer findMaxSupportId(); List findByThumbnailPathIsNull(); + + Page findByStatusIn(List statuses, Pageable pageable); + + List findByUserIdAndStatusIn(Long userId, List statuses); } diff --git a/src/main/java/ru/soune/nocopy/repository/ModerationLogRepository.java b/src/main/java/ru/soune/nocopy/repository/ModerationLogRepository.java new file mode 100644 index 0000000..f407060 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/ModerationLogRepository.java @@ -0,0 +1,13 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.soune.nocopy.entity.file.moderation.ModerationLog; + +import java.util.List; + +public interface ModerationLogRepository extends JpaRepository { + + List findByFileIdOrderByCreatedAtDesc(String fileId); + + List findByModeratorId(Long moderatorId); +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index 3131eac..16e2e3b 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -16,7 +16,6 @@ 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.handler.MonitoringHandler; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileMonitoringRepository; import ru.soune.nocopy.repository.SimilarImageProjection; @@ -252,7 +251,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); @@ -260,6 +259,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 void writeProtectedFile(String id, byte[] data, String fileExt) throws IOException { FileEntity fileEntity = fileEntityRepository.findById(id) diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/NoCopyProcessingListenerImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/NoCopyProcessingListenerImpl.java index 257cf65..0a7e9cf 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/NoCopyProcessingListenerImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/NoCopyProcessingListenerImpl.java @@ -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());; } } diff --git a/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java new file mode 100644 index 0000000..5ee458e --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java @@ -0,0 +1,236 @@ +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.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 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()) + .moderationReasonId(request.getModerationReasonId()) + .comment(request.getComment()) + .build(); + moderationLogRepository.save(log); + + file.setStatus(request.getNewStatus()); + file.setModerationReasonId(request.getModerationReasonId()); + 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("Апелляция отклонена в процессе модерации"); + appeal.setResolvedAt(LocalDateTime.now()); + fileAppealRepository.save(appeal); + }); + } + } + + /** + * Подача апелляции пользователем + */ + @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()) + .moderationReasonId(file.getModerationReasonId()) + .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); + file.setModerationReasonId(null); + } + + 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) { + if (file.getStatus() != FileStatus.BLOCKED) { + return false; + } + + return true; + } + + /** + * Проверка наличия активной апелляции + */ + 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 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 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(); + } +} From 4e0579c9527ba796cd9837f729368e7fed09d7b9 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 27 Mar 2026 14:20:09 +0700 Subject: [PATCH 249/274] dev add reason entity --- .../nocopy/dto/file/FileEntityRequest.java | 6 + .../dto/file/ModerationReasonRequest.java | 16 ++ .../dto/file/ModerationReasonResponse.java | 15 ++ .../ModerationReasonInUseException.java | 17 ++ .../ModerationReasonNotFoundException.java | 7 + .../nocopy/handler/FileEntityHandler.java | 223 +++++++++++++++++- .../repository/FileEntityRepository.java | 4 + .../ModerationReasonRepository.java | 22 ++ .../moderation/ModerationReasonService.java | 187 +++++++++++++++ 9 files changed, 494 insertions(+), 3 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/file/ModerationReasonRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/file/ModerationReasonResponse.java create mode 100644 src/main/java/ru/soune/nocopy/exception/ModerationReasonInUseException.java create mode 100644 src/main/java/ru/soune/nocopy/exception/ModerationReasonNotFoundException.java create mode 100644 src/main/java/ru/soune/nocopy/repository/ModerationReasonRepository.java create mode 100644 src/main/java/ru/soune/nocopy/service/file/moderation/ModerationReasonService.java diff --git a/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java b/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java index 8159ceb..9d5811d 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java @@ -68,4 +68,10 @@ public class FileEntityRequest { @JsonProperty("approve") private Boolean approve; + + @JsonProperty("reason_text") + private String reasonText; + + @JsonProperty("reason_text") + private String searchQuery; } diff --git a/src/main/java/ru/soune/nocopy/dto/file/ModerationReasonRequest.java b/src/main/java/ru/soune/nocopy/dto/file/ModerationReasonRequest.java new file mode 100644 index 0000000..3e475c5 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/file/ModerationReasonRequest.java @@ -0,0 +1,16 @@ +package ru.soune.nocopy.dto.file; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ModerationReasonRequest { + private String action; + private String token; + private Long reasonId; + private String reasonText; + private Integer page; + private Integer pageSize; + private String searchQuery; +} diff --git a/src/main/java/ru/soune/nocopy/dto/file/ModerationReasonResponse.java b/src/main/java/ru/soune/nocopy/dto/file/ModerationReasonResponse.java new file mode 100644 index 0000000..a5bbaa8 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/file/ModerationReasonResponse.java @@ -0,0 +1,15 @@ +package ru.soune.nocopy.dto.file; +import lombok.Builder; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@Builder +public class ModerationReasonResponse { + private Long id; + private String reasonText; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private Boolean isUsed; + private Long usageCount; +} diff --git a/src/main/java/ru/soune/nocopy/exception/ModerationReasonInUseException.java b/src/main/java/ru/soune/nocopy/exception/ModerationReasonInUseException.java new file mode 100644 index 0000000..21525ba --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/ModerationReasonInUseException.java @@ -0,0 +1,17 @@ +package ru.soune.nocopy.exception; + +import lombok.Getter; + +@Getter +public class ModerationReasonInUseException extends RuntimeException { + private final Long reasonId; + private final Long usageCount; + + public ModerationReasonInUseException(Long reasonId, Long usageCount) { + super(String.format("Cannot delete moderation reason %d because it is used by %d files", + reasonId, usageCount)); + this.reasonId = reasonId; + this.usageCount = usageCount; + } + +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/exception/ModerationReasonNotFoundException.java b/src/main/java/ru/soune/nocopy/exception/ModerationReasonNotFoundException.java new file mode 100644 index 0000000..a829628 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/ModerationReasonNotFoundException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class ModerationReasonNotFoundException extends RuntimeException { + public ModerationReasonNotFoundException(Long id) { + super("Moderation reason not found with id: " + id); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java index 0fc33b2..09bcdbe 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java @@ -12,12 +12,11 @@ import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.ProtectionStatus; import ru.soune.nocopy.entity.file.moderation.ModerationLog; -import ru.soune.nocopy.exception.FileEntityNotFoundException; -import ru.soune.nocopy.exception.InvalidAppealException; -import ru.soune.nocopy.exception.NotFoundAuthToken; +import ru.soune.nocopy.exception.*; import ru.soune.nocopy.repository.FileAppealRepository; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.ModerationLogRepository; +import ru.soune.nocopy.service.file.moderation.ModerationReasonService; import ru.soune.nocopy.service.file.moderation.ModerationService; import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.file.FileEntityService; @@ -48,6 +47,8 @@ public class FileEntityHandler implements RequestHandler { private final ModerationLogRepository moderationLogRepository; + private final ModerationReasonService moderationReasonService; + @Override public BaseResponse handle(BaseRequest request) { try { @@ -85,6 +86,14 @@ public class FileEntityHandler implements RequestHandler { return handleGetUserAppeals(request, fileRequest); case "files_for_moderation": return handleGetFilesForModeration(request, fileRequest); + case "get_moderation_reason": + return handleGetModerationReason(request, fileRequest); + case "create_moderation_reason": + return handleCreateModerationReason(request, fileRequest); + case "update_moderation_reason": + return handleUpdateModerationReason(request, fileRequest); + case "delete_moderation_reason": + return handleDeleteModerationReason(request, fileRequest); default: ActionResponse response = ActionResponse.builder() .action(action) @@ -937,6 +946,214 @@ public class FileEntityHandler implements RequestHandler { .collect(Collectors.toList()); } + /** + * Получение списка причин модерации + */ + private BaseResponse handleGetModerationReasons(BaseRequest request, FileEntityRequest fileRequest) { + try { + // TODO + // authService.useAdminAuthToken(fileRequest.getToken()); + + int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1; + int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20; + String searchQuery = fileRequest.getSearchQuery(); + + Page reasons = moderationReasonService.getAllReasons( + page - 1, pageSize, searchQuery); + + Map response = new HashMap<>(); + response.put("reasons", reasons.getContent()); + response.put("totalCount", reasons.getTotalElements()); + response.put("totalPages", reasons.getTotalPages()); + response.put("currentPage", page); + response.put("pageSize", pageSize); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + response); + + } catch (SecurityException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.ACCESS_DENIED.getCode(), + "Admin access required", + null); + } catch (Exception e) { + log.error("Error getting moderation reasons", e); + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "Failed to get reasons: " + e.getMessage(), + null); + } + } + + /** + * Получение конкретной причины модерации + */ + private BaseResponse handleGetModerationReason(BaseRequest request, FileEntityRequest fileRequest) { + try { + // TODO + // authService.useAdminAuthToken(fileRequest.getToken()); + + Long reasonId = fileRequest.getModerationReasonId(); + if (reasonId == null) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "reasonId is required", + null); + } + + ModerationReasonResponse reason = moderationReasonService.getReasonById(reasonId); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + reason); + + } catch (ModerationReasonNotFoundException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.NOT_FOUND.getCode(), + e.getMessage(), + null); + } catch (Exception e) { + log.error("Error getting moderation reason", e); + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "Failed to get reason: " + e.getMessage(), + null); + } + } + + /** + * Создание новой причины модерации + */ + private BaseResponse handleCreateModerationReason(BaseRequest request, FileEntityRequest fileRequest) { + try { + // TODO + // authService.useAdminAuthToken(fileRequest.getToken()); + + String reasonText = fileRequest.getReasonText(); + if (reasonText == null || reasonText.trim().isEmpty()) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "reasonText is required", + null); + } + + ModerationReasonResponse reason = moderationReasonService.createReason(reasonText); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + "Moderation reason created successfully", + reason); + + } catch (IllegalArgumentException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + e.getMessage(), + null); + } catch (Exception e) { + log.error("Error creating moderation reason", e); + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "Failed to create reason: " + e.getMessage(), + null); + } + } + + /** + * Обновление причины модерации + */ + private BaseResponse handleUpdateModerationReason(BaseRequest request, FileEntityRequest fileRequest) { + try { + // TODO + // authService.useAdminAuthToken(fileRequest.getToken()); + + Long reasonId = fileRequest.getModerationReasonId(); + String reasonText = fileRequest.getReasonText(); + + if (reasonId == null) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "reasonId is required", + null); + } + + if (reasonText == null || reasonText.trim().isEmpty()) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "reasonText is required", + null); + } + + ModerationReasonResponse reason = moderationReasonService.updateReason(reasonId, reasonText); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + "Moderation reason updated successfully", + reason); + + } catch (ModerationReasonNotFoundException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.NOT_FOUND.getCode(), + e.getMessage(), + null); + } catch (IllegalArgumentException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + e.getMessage(), + null); + } catch (Exception e) { + log.error("Error updating moderation reason", e); + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "Failed to update reason: " + e.getMessage(), + null); + } + } + + /** + * Удаление причины модерации + */ + private BaseResponse handleDeleteModerationReason(BaseRequest request, FileEntityRequest fileRequest) { + try { + // TODO + // authService.useAdminAuthToken(fileRequest.getToken()); + + Long reasonId = fileRequest.getModerationReasonId(); + if (reasonId == null) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "reasonId is required", + null); + } + + moderationReasonService.deleteReason(reasonId); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + "Moderation reason deleted successfully", + Map.of("reasonId", reasonId)); + + } catch (ModerationReasonNotFoundException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.NOT_FOUND.getCode(), + e.getMessage(), + null); + } catch (ModerationReasonInUseException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + e.getMessage(), + Map.of("usageCount", e.getUsageCount())); + } catch (Exception e) { + log.error("Error deleting moderation reason", e); + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "Failed to delete reason: " + e.getMessage(), + null); + } + } + private String formatFileSize(long size) { if (size < 1024) return size + " B"; int exp = (int) (Math.log(size) / Math.log(1024)); diff --git a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java index 0e5e014..3b98025 100644 --- a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java @@ -72,4 +72,8 @@ public interface FileEntityRepository extends JpaRepository Page findByStatusIn(List statuses, Pageable pageable); List findByUserIdAndStatusIn(Long userId, List statuses); + + @Query("SELECT COUNT(f) FROM FileEntity f WHERE f.moderationReasonId = :reasonId AND f.status IN :statuses") + long countByModerationReasonIdAndStatusIn(@Param("reasonId") Long reasonId, + @Param("statuses") List statuses); } diff --git a/src/main/java/ru/soune/nocopy/repository/ModerationReasonRepository.java b/src/main/java/ru/soune/nocopy/repository/ModerationReasonRepository.java new file mode 100644 index 0000000..81ebce4 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/ModerationReasonRepository.java @@ -0,0 +1,22 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import ru.soune.nocopy.entity.file.moderation.ModerationReason; + +import java.util.Optional; + +public interface ModerationReasonRepository extends JpaRepository { + Optional findByReasonText(String reasonText); + + Page findAll(Pageable pageable); + + @Query("SELECT mr FROM ModerationReason mr WHERE mr.reasonText LIKE %:search%") + Page searchByText(@Param("search") String search, Pageable pageable); + + @Query("SELECT COUNT(mr) FROM ModerationReason mr") + long countAllReasons(); +} diff --git a/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationReasonService.java b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationReasonService.java new file mode 100644 index 0000000..d9515af --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationReasonService.java @@ -0,0 +1,187 @@ +package ru.soune.nocopy.service.file.moderation; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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.ModerationReasonResponse; +import ru.soune.nocopy.entity.file.FileStatus; +import ru.soune.nocopy.entity.file.moderation.ModerationReason; +import ru.soune.nocopy.exception.ModerationReasonInUseException; +import ru.soune.nocopy.exception.ModerationReasonNotFoundException; +import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.repository.ModerationReasonRepository; + +import java.util.Arrays; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ModerationReasonService { + + private final ModerationReasonRepository moderationReasonRepository; + private final FileEntityRepository fileEntityRepository; + + private static final List BLOCKED_STATUSES = Arrays.asList( + FileStatus.BLOCKED, + FileStatus.VIOLATION, + FileStatus.MODERATION + ); + + /** + * Создание новой причины модерации + */ + @Transactional + public ModerationReasonResponse createReason(String reasonText) { + if (reasonText == null || reasonText.trim().isEmpty()) { + throw new IllegalArgumentException("Reason text cannot be empty"); + } + + // Проверяем на дубликат + if (moderationReasonRepository.findByReasonText(reasonText.trim()).isPresent()) { + throw new IllegalArgumentException("Reason with this text already exists"); + } + + ModerationReason reason = ModerationReason.builder() + .reasonText(reasonText.trim()) + .build(); + + ModerationReason saved = moderationReasonRepository.save(reason); + + log.info("Created new moderation reason: id={}, text={}", saved.getId(), saved.getReasonText()); + + return convertToResponse(saved, false, 0L); + } + + /** + * Обновление причины модерации + */ + @Transactional + public ModerationReasonResponse updateReason(Long reasonId, String newReasonText) { + ModerationReason reason = moderationReasonRepository.findById(reasonId) + .orElseThrow(() -> new ModerationReasonNotFoundException(reasonId)); + + if (newReasonText == null || newReasonText.trim().isEmpty()) { + throw new IllegalArgumentException("Reason text cannot be empty"); + } + + // Проверяем на дубликат (исключая текущую запись) + moderationReasonRepository.findByReasonText(newReasonText.trim()) + .ifPresent(existingReason -> { + if (!existingReason.getId().equals(reasonId)) { + throw new IllegalArgumentException("Reason with this text already exists"); + } + }); + + String oldText = reason.getReasonText(); + reason.setReasonText(newReasonText.trim()); + + ModerationReason updated = moderationReasonRepository.save(reason); + + log.info("Updated moderation reason: id={}, oldText={}, newText={}", + reasonId, oldText, newReasonText); + + long usageCount = getReasonUsageCount(reasonId); + + return convertToResponse(updated, usageCount > 0, usageCount); + } + + /** + * Удаление причины модерации + */ + @Transactional + public void deleteReason(Long reasonId) { + ModerationReason reason = moderationReasonRepository.findById(reasonId) + .orElseThrow(() -> new ModerationReasonNotFoundException(reasonId)); + + // Проверяем, используется ли причина в активных блокировках + long usageCount = getReasonUsageCount(reasonId); + + if (usageCount > 0) { + throw new ModerationReasonInUseException(reasonId, usageCount); + } + + moderationReasonRepository.delete(reason); + + log.info("Deleted moderation reason: id={}, text={}", reasonId, reason.getReasonText()); + } + + /** + * Получение причины по ID + */ + @Transactional(readOnly = true) + public ModerationReasonResponse getReasonById(Long reasonId) { + ModerationReason reason = moderationReasonRepository.findById(reasonId) + .orElseThrow(() -> new ModerationReasonNotFoundException(reasonId)); + + long usageCount = getReasonUsageCount(reasonId); + + return convertToResponse(reason, usageCount > 0, usageCount); + } + + /** + * Получение всех причин с пагинацией + */ + @Transactional(readOnly = true) + public Page getAllReasons(int page, int size, String searchQuery) { + Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); + + Page reasonsPage; + + if (searchQuery != null && !searchQuery.trim().isEmpty()) { + reasonsPage = moderationReasonRepository.searchByText(searchQuery.trim(), pageable); + } else { + reasonsPage = moderationReasonRepository.findAll(pageable); + } + + return reasonsPage.map(reason -> { + long usageCount = getReasonUsageCount(reason.getId()); + return convertToResponse(reason, usageCount > 0, usageCount); + }); + } + + /** + * Получение списка всех причин (без пагинации, для выпадающих списков) + */ + @Transactional(readOnly = true) + public List getAllReasonsList() { + List reasons = moderationReasonRepository.findAll(Sort.by("reasonText").ascending()); + + return reasons.stream() + .map(reason -> { + long usageCount = getReasonUsageCount(reason.getId()); + return convertToResponse(reason, usageCount > 0, usageCount); + }) + .collect(java.util.stream.Collectors.toList()); + } + + /** + * Подсчет количества файлов, заблокированных по данной причине + */ + private long getReasonUsageCount(Long reasonId) { + return fileEntityRepository.countByModerationReasonIdAndStatusIn(reasonId, BLOCKED_STATUSES); + } + + /** + * Проверка, используется ли причина в активных блокировках + */ + public boolean isReasonInUse(Long reasonId) { + return getReasonUsageCount(reasonId) > 0; + } + + private ModerationReasonResponse convertToResponse(ModerationReason reason, boolean isUsed, long usageCount) { + return ModerationReasonResponse.builder() + .id(reason.getId()) + .reasonText(reason.getReasonText()) + .createdAt(reason.getCreatedAt()) + .updatedAt(reason.getUpdatedAt()) + .isUsed(isUsed) + .usageCount(usageCount) + .build(); + } +} From 78b97b48c9f114342fe6ab77d932934b63e9153a Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 27 Mar 2026 14:31:49 +0700 Subject: [PATCH 250/274] dev add reason entity --- src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java b/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java index 9d5811d..8d749b5 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java @@ -72,6 +72,6 @@ public class FileEntityRequest { @JsonProperty("reason_text") private String reasonText; - @JsonProperty("reason_text") + @JsonProperty("search_query") private String searchQuery; } From 547e70ecdd3f1d0d6b0e777905a36f29831666c0 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 27 Mar 2026 15:31:45 +0700 Subject: [PATCH 251/274] dev add reason entity --- .../nocopy/handler/FileEntityHandler.java | 44 +++++++++++-------- .../moderation/ModerationReasonService.java | 5 +-- .../file/moderation/ModerationService.java | 2 +- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java index 09bcdbe..3d68059 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java @@ -784,24 +784,32 @@ public class FileEntityHandler implements RequestHandler { List> filesWithInfo = files.getContent().stream() .map(file -> { FileModerationInfo info = moderationService.getFileModerationInfo(file.getId()); - return Map.of( - "fileId", file.getId(), - "fileName", file.getOriginalFileName(), - "userId", file.getUserId(), - "status", file.getStatus().name(), - "createdAt", file.getCreatedAt(), - "fileSize", file.getFileSize(), - "formattedSize", fileEntityService.formatFileSize(file.getFileSize()), - "moderationInfo", Map.of( - "hasActiveAppeal", info.getHasActiveAppeal(), - "appealInfo", info.getActiveAppeal() != null ? - Map.of( - "appealId", info.getActiveAppeal().getAppealId(), - "status", info.getActiveAppeal().getStatus(), - "createdAt", info.getActiveAppeal().getCreatedAt() - ) : null - ) - ); + + Map fileMap = new HashMap<>(); + fileMap.put("fileId", file.getId()); + fileMap.put("fileName", file.getOriginalFileName()); + fileMap.put("userId", file.getUserId()); + fileMap.put("status", file.getStatus() != null ? file.getStatus().name() : null); + fileMap.put("createdAt", file.getCreatedAt()); + fileMap.put("fileSize", file.getFileSize()); + fileMap.put("formattedSize", fileEntityService.formatFileSize(file.getFileSize())); + + Map moderationInfoMap = new HashMap<>(); + moderationInfoMap.put("hasActiveAppeal", info != null && info.getHasActiveAppeal()); + + if (info != null && info.getActiveAppeal() != null) { + Map appealInfoMap = new HashMap<>(); + appealInfoMap.put("appealId", info.getActiveAppeal().getAppealId()); + appealInfoMap.put("status", info.getActiveAppeal().getStatus()); + appealInfoMap.put("createdAt", info.getActiveAppeal().getCreatedAt()); + moderationInfoMap.put("appealInfo", appealInfoMap); + } else { + moderationInfoMap.put("appealInfo", null); + } + + fileMap.put("moderationInfo", moderationInfoMap); + + return fileMap; }) .collect(Collectors.toList()); diff --git a/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationReasonService.java b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationReasonService.java index d9515af..0140d4b 100644 --- a/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationReasonService.java +++ b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationReasonService.java @@ -42,7 +42,6 @@ public class ModerationReasonService { throw new IllegalArgumentException("Reason text cannot be empty"); } - // Проверяем на дубликат if (moderationReasonRepository.findByReasonText(reasonText.trim()).isPresent()) { throw new IllegalArgumentException("Reason with this text already exists"); } @@ -70,7 +69,6 @@ public class ModerationReasonService { throw new IllegalArgumentException("Reason text cannot be empty"); } - // Проверяем на дубликат (исключая текущую запись) moderationReasonRepository.findByReasonText(newReasonText.trim()) .ifPresent(existingReason -> { if (!existingReason.getId().equals(reasonId)) { @@ -99,14 +97,13 @@ public class ModerationReasonService { ModerationReason reason = moderationReasonRepository.findById(reasonId) .orElseThrow(() -> new ModerationReasonNotFoundException(reasonId)); - // Проверяем, используется ли причина в активных блокировках long usageCount = getReasonUsageCount(reasonId); if (usageCount > 0) { throw new ModerationReasonInUseException(reasonId, usageCount); } - moderationReasonRepository.delete(reason); + moderationReasonRepository.deleteById(reasonId); log.info("Deleted moderation reason: id={}, text={}", reasonId, reason.getReasonText()); } diff --git a/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java index 5ee458e..188888c 100644 --- a/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java +++ b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java @@ -72,7 +72,7 @@ public class ModerationService { fileAppealRepository.findByFileIdAndStatus(file.getId(), AppealStatus.PENDING) .ifPresent(appeal -> { appeal.setStatus(AppealStatus.REJECTED); - appeal.setAdminComment("Апелляция отклонена в процессе модерации"); + appeal.setAdminComment(request.getComment()); appeal.setResolvedAt(LocalDateTime.now()); fileAppealRepository.save(appeal); }); From 964910ec691ed0cba8e5591fe99f2193830067f0 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 27 Mar 2026 16:37:20 +0700 Subject: [PATCH 252/274] dev add reason entity --- .../java/ru/soune/nocopy/handler/FileEntityHandler.java | 3 --- .../service/file/moderation/ModerationService.java | 9 +++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java index 3d68059..6cf8112 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java @@ -790,9 +790,6 @@ public class FileEntityHandler implements RequestHandler { fileMap.put("fileName", file.getOriginalFileName()); fileMap.put("userId", file.getUserId()); fileMap.put("status", file.getStatus() != null ? file.getStatus().name() : null); - fileMap.put("createdAt", file.getCreatedAt()); - fileMap.put("fileSize", file.getFileSize()); - fileMap.put("formattedSize", fileEntityService.formatFileSize(file.getFileSize())); Map moderationInfoMap = new HashMap<>(); moderationInfoMap.put("hasActiveAppeal", info != null && info.getHasActiveAppeal()); diff --git a/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java index 188888c..8e3cfec 100644 --- a/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java +++ b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java @@ -60,6 +60,7 @@ public class ModerationService { .moderationReasonId(request.getModerationReasonId()) .comment(request.getComment()) .build(); + moderationLogRepository.save(log); file.setStatus(request.getNewStatus()); @@ -91,7 +92,7 @@ public class ModerationService { throw new SecurityException("User is not the owner of this file"); } - if (!canAppeal(file)) { + if (canAppeal(file)) { throw new InvalidAppealException("Cannot appeal this file. Current status: " + file.getStatus()); } @@ -156,11 +157,7 @@ public class ModerationService { * Проверка, можно ли подать апелляцию на файл */ public boolean canAppeal(FileEntity file) { - if (file.getStatus() != FileStatus.BLOCKED) { - return false; - } - - return true; + return file.getStatus() == FileStatus.BLOCKED; } /** From 9a8a4a72f4d23c06089ebd9563656a497c982332 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 27 Mar 2026 17:31:06 +0700 Subject: [PATCH 253/274] dev add reason entity --- .../java/ru/soune/nocopy/dto/MessageCode.java | 2 ++ .../nocopy/handler/FileEntityHandler.java | 34 +++++++++++-------- .../file/moderation/ModerationService.java | 3 +- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index 522dca1..34e903d 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -2,6 +2,7 @@ package ru.soune.nocopy.dto; public enum MessageCode { SUCCESS(0, "Operation successful"), + SUCCESS_MODERATION(0, "File moderated successfully"), REG_EMAIL_EXISTS(1, "Email already registered"), REG_EMAIL_OR_PHONE_EXISTS(1, "Email or phone already registered"), REFERRAL_LINK_IS_NOT_EXIST(1, "Refferal link is not exist"), @@ -52,6 +53,7 @@ public enum MessageCode { MONITORING_TYPE_NOT_FOUND(4, "Monitoring type not found"), ERROR_TARIFF_INFO(2, "Erorr with tariff info"), FILE_FOR_SEARCH_NOT_VALID(2, "File for search unsupported"), + ILLEGAL_STATE(2, "File is not in moderation state"), NOT_VALID_FILE_TYPE_OR_COUNT_FILE(2, "Cost for file type not found, count is negative or files count" + "more than max valid"), USER_NOT_ACTIVE(2, "User not active"), diff --git a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java index 6cf8112..cb1feb5 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java @@ -219,19 +219,21 @@ public class FileEntityHandler implements RequestHandler { private BaseResponse handleChangeStatus(BaseRequest request, FileEntityRequest fileRequest) { FileStatus newStatus = fileRequest.getFileStatus(); + String fileId = fileRequest.getFileId(); + try { fileEntityService.changeFileStatus(newStatus ,fileRequest.getFileId()); } catch (FileEntityNotFoundException e) { return new BaseResponse(request.getMsgId(), MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(), - null); + FileEntityResponse.builder().id(fileId)); } return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), MessageCode.SUCCESS.getDescription(), - Map.of("new_status", newStatus)); + FileEntityResponse.builder().status(newStatus)); } private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) { @@ -635,21 +637,27 @@ public class FileEntityHandler implements RequestHandler { moderationService.moderateFile(moderationRequest, 0L); return new BaseResponse(request.getMsgId(), - MessageCode.SUCCESS.getCode(), - "File moderated successfully", - null); + MessageCode.SUCCESS_MODERATION.getCode(), + MessageCode.SUCCESS_MODERATION.getDescription(), + FileEntityResponse.builder().id(fileRequest.getFileId())); } catch (SecurityException e) { return new BaseResponse(request.getMsgId(), MessageCode.ACCESS_DENIED.getCode(), - e.getMessage(), - null); - } catch (Exception e) { - log.error("Error moderating file", e); + MessageCode.ACCESS_DENIED.getDescription(), + FileEntityResponse.builder().id(fileRequest.getFileId())); + } catch (FileEntityNotFoundException e) { return new BaseResponse(request.getMsgId(), - MessageCode.INVALID_FIELD.getCode(), - "Failed to moderate file: " + e.getMessage(), - null); + MessageCode.FILE_NOT_FOUND.getCode(), + MessageCode.FILE_NOT_FOUND.getDescription(), + FileEntityResponse.builder() + .id(fileRequest.getFileId())); + } catch (IllegalStateException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.ILLEGAL_STATE.getCode(), + MessageCode.ILLEGAL_STATE.getDescription(), + FileEntityResponse.builder() + .status(fileRequest.getFileStatus())); } } @@ -669,14 +677,12 @@ public class FileEntityHandler implements RequestHandler { MessageCode.SUCCESS.getCode(), "Appeal submitted successfully", response); - } catch (InvalidAppealException e) { return new BaseResponse(request.getMsgId(), MessageCode.INVALID_FIELD.getCode(), e.getMessage(), null); } catch (Exception e) { - log.error("Error submitting appeal", e); return new BaseResponse(request.getMsgId(), MessageCode.INVALID_FIELD.getCode(), "Failed to submit appeal: " + e.getMessage(), diff --git a/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java index 8e3cfec..8a7b4d8 100644 --- a/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java +++ b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java @@ -92,7 +92,7 @@ public class ModerationService { throw new SecurityException("User is not the owner of this file"); } - if (canAppeal(file)) { + if (!canAppeal(file)) { throw new InvalidAppealException("Cannot appeal this file. Current status: " + file.getStatus()); } @@ -150,6 +150,7 @@ public class ModerationService { moderationLogRepository.save(log); fileEntityRepository.save(file); + fileAppealRepository.save(appeal); } From 067b967a1775b7c692b0cd18102c2f80b4c07260 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 27 Mar 2026 17:54:04 +0700 Subject: [PATCH 254/274] dev add reason entity --- .../nocopy/dto/file/FileEntityRequest.java | 3 - .../nocopy/dto/file/FileModerationInfo.java | 1 - .../dto/file/ModerationActionRequest.java | 1 - .../dto/file/ModerationReasonRequest.java | 16 -- .../dto/file/ModerationReasonResponse.java | 15 -- .../soune/nocopy/entity/file/FileAppeal.java | 3 - .../soune/nocopy/entity/file/FileEntity.java | 3 - .../entity/file/moderation/ModerationLog.java | 3 - .../file/moderation/ModerationReason.java | 34 --- .../ModerationReasonInUseException.java | 17 -- .../ModerationReasonNotFoundException.java | 7 - .../nocopy/handler/FileEntityHandler.java | 235 ------------------ .../repository/FileEntityRepository.java | 4 - .../ModerationReasonRepository.java | 22 -- .../moderation/ModerationReasonService.java | 184 -------------- .../file/moderation/ModerationService.java | 5 +- 16 files changed, 1 insertion(+), 552 deletions(-) delete mode 100644 src/main/java/ru/soune/nocopy/dto/file/ModerationReasonRequest.java delete mode 100644 src/main/java/ru/soune/nocopy/dto/file/ModerationReasonResponse.java delete mode 100644 src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationReason.java delete mode 100644 src/main/java/ru/soune/nocopy/exception/ModerationReasonInUseException.java delete mode 100644 src/main/java/ru/soune/nocopy/exception/ModerationReasonNotFoundException.java delete mode 100644 src/main/java/ru/soune/nocopy/repository/ModerationReasonRepository.java delete mode 100644 src/main/java/ru/soune/nocopy/service/file/moderation/ModerationReasonService.java diff --git a/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java b/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java index 8d749b5..68df125 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java @@ -51,9 +51,6 @@ public class FileEntityRequest { @JsonProperty("date_filter") private String dateFilter; - @JsonProperty("moderation_reason_id") - private Long moderationReasonId; - @JsonProperty("comment") private String comment; diff --git a/src/main/java/ru/soune/nocopy/dto/file/FileModerationInfo.java b/src/main/java/ru/soune/nocopy/dto/file/FileModerationInfo.java index fbe409c..8ffc794 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/FileModerationInfo.java +++ b/src/main/java/ru/soune/nocopy/dto/file/FileModerationInfo.java @@ -15,7 +15,6 @@ public class FileModerationInfo { private String ownerEmail; private FileStatus currentStatus; private LocalDateTime uploadDate; - private String moderationReason; private Boolean hasActiveAppeal; private AppealInfo activeAppeal; } diff --git a/src/main/java/ru/soune/nocopy/dto/file/ModerationActionRequest.java b/src/main/java/ru/soune/nocopy/dto/file/ModerationActionRequest.java index ad6f6b1..58a227c 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/ModerationActionRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/file/ModerationActionRequest.java @@ -9,7 +9,6 @@ import ru.soune.nocopy.entity.file.FileStatus; public class ModerationActionRequest { private String fileId; private FileStatus newStatus; - private Long moderationReasonId; private String comment; private String token; } diff --git a/src/main/java/ru/soune/nocopy/dto/file/ModerationReasonRequest.java b/src/main/java/ru/soune/nocopy/dto/file/ModerationReasonRequest.java deleted file mode 100644 index 3e475c5..0000000 --- a/src/main/java/ru/soune/nocopy/dto/file/ModerationReasonRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package ru.soune.nocopy.dto.file; - -import lombok.Builder; -import lombok.Data; - -@Data -@Builder -public class ModerationReasonRequest { - private String action; - private String token; - private Long reasonId; - private String reasonText; - private Integer page; - private Integer pageSize; - private String searchQuery; -} diff --git a/src/main/java/ru/soune/nocopy/dto/file/ModerationReasonResponse.java b/src/main/java/ru/soune/nocopy/dto/file/ModerationReasonResponse.java deleted file mode 100644 index a5bbaa8..0000000 --- a/src/main/java/ru/soune/nocopy/dto/file/ModerationReasonResponse.java +++ /dev/null @@ -1,15 +0,0 @@ -package ru.soune.nocopy.dto.file; -import lombok.Builder; -import lombok.Data; -import java.time.LocalDateTime; - -@Data -@Builder -public class ModerationReasonResponse { - private Long id; - private String reasonText; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; - private Boolean isUsed; - private Long usageCount; -} diff --git a/src/main/java/ru/soune/nocopy/entity/file/FileAppeal.java b/src/main/java/ru/soune/nocopy/entity/file/FileAppeal.java index 36a5553..26b28a6 100644 --- a/src/main/java/ru/soune/nocopy/entity/file/FileAppeal.java +++ b/src/main/java/ru/soune/nocopy/entity/file/FileAppeal.java @@ -52,9 +52,6 @@ public class FileAppeal { @Enumerated(EnumType.STRING) private FileStatus moderationBeforeStatus; - @Column(name = "moderation_reason_id") - private Long moderationReasonId; - @Column(name = "resolved_at") private LocalDateTime resolvedAt; diff --git a/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java b/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java index a02e1cc..02104c7 100644 --- a/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java +++ b/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java @@ -87,9 +87,6 @@ public class FileEntity { @Column(name = "medium_path") private String mediumPath; - @Column(name = "moderation_reason_id") - private Long moderationReasonId; - @OneToOne(mappedBy = "file", cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnore private ImageHashEntity imageHash; diff --git a/src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationLog.java b/src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationLog.java index 04249a7..5196134 100644 --- a/src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationLog.java +++ b/src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationLog.java @@ -44,9 +44,6 @@ public class ModerationLog { @Column(name = "reason", columnDefinition = "TEXT") private String reason; - @Column(name = "moderation_reason_id") - private Long moderationReasonId; - @Column(name = "comment", columnDefinition = "TEXT") private String comment; diff --git a/src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationReason.java b/src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationReason.java deleted file mode 100644 index e62c302..0000000 --- a/src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationReason.java +++ /dev/null @@ -1,34 +0,0 @@ -package ru.soune.nocopy.entity.file.moderation; - -import jakarta.persistence.*; -import lombok.*; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -import java.time.LocalDateTime; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Entity -@Table(name = "moderation_reasons") -@EntityListeners(AuditingEntityListener.class) -public class ModerationReason { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "reason_text", nullable = false, columnDefinition = "TEXT") - private String reasonText; - - @CreatedDate - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - - @LastModifiedDate - @Column(name = "updated_at") - private LocalDateTime updatedAt; -} diff --git a/src/main/java/ru/soune/nocopy/exception/ModerationReasonInUseException.java b/src/main/java/ru/soune/nocopy/exception/ModerationReasonInUseException.java deleted file mode 100644 index 21525ba..0000000 --- a/src/main/java/ru/soune/nocopy/exception/ModerationReasonInUseException.java +++ /dev/null @@ -1,17 +0,0 @@ -package ru.soune.nocopy.exception; - -import lombok.Getter; - -@Getter -public class ModerationReasonInUseException extends RuntimeException { - private final Long reasonId; - private final Long usageCount; - - public ModerationReasonInUseException(Long reasonId, Long usageCount) { - super(String.format("Cannot delete moderation reason %d because it is used by %d files", - reasonId, usageCount)); - this.reasonId = reasonId; - this.usageCount = usageCount; - } - -} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/exception/ModerationReasonNotFoundException.java b/src/main/java/ru/soune/nocopy/exception/ModerationReasonNotFoundException.java deleted file mode 100644 index a829628..0000000 --- a/src/main/java/ru/soune/nocopy/exception/ModerationReasonNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.soune.nocopy.exception; - -public class ModerationReasonNotFoundException extends RuntimeException { - public ModerationReasonNotFoundException(Long id) { - super("Moderation reason not found with id: " + id); - } -} diff --git a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java index cb1feb5..a254184 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java @@ -16,7 +16,6 @@ import ru.soune.nocopy.exception.*; import ru.soune.nocopy.repository.FileAppealRepository; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.ModerationLogRepository; -import ru.soune.nocopy.service.file.moderation.ModerationReasonService; import ru.soune.nocopy.service.file.moderation.ModerationService; import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.file.FileEntityService; @@ -47,8 +46,6 @@ public class FileEntityHandler implements RequestHandler { private final ModerationLogRepository moderationLogRepository; - private final ModerationReasonService moderationReasonService; - @Override public BaseResponse handle(BaseRequest request) { try { @@ -86,14 +83,6 @@ public class FileEntityHandler implements RequestHandler { return handleGetUserAppeals(request, fileRequest); case "files_for_moderation": return handleGetFilesForModeration(request, fileRequest); - case "get_moderation_reason": - return handleGetModerationReason(request, fileRequest); - case "create_moderation_reason": - return handleCreateModerationReason(request, fileRequest); - case "update_moderation_reason": - return handleUpdateModerationReason(request, fileRequest); - case "delete_moderation_reason": - return handleDeleteModerationReason(request, fileRequest); default: ActionResponse response = ActionResponse.builder() .action(action) @@ -629,7 +618,6 @@ public class FileEntityHandler implements RequestHandler { ModerationActionRequest moderationRequest = ModerationActionRequest.builder() .fileId(fileRequest.getFileId()) .newStatus(fileRequest.getFileStatus()) - .moderationReasonId(fileRequest.getModerationReasonId()) .comment(fileRequest.getComment()) .build(); @@ -736,9 +724,6 @@ public class FileEntityHandler implements RequestHandler { } } - /** - * Получение списка апелляций пользователя - */ private BaseResponse handleGetUserAppeals(BaseRequest request, FileEntityRequest fileRequest) { try { Long userId = authService.useUserAuthToken(fileRequest.getToken()); @@ -774,9 +759,6 @@ public class FileEntityHandler implements RequestHandler { } } - /** - * Получение списка файлов на модерации - */ private BaseResponse handleGetFilesForModeration(BaseRequest request, FileEntityRequest fileRequest) { try { //TODO @@ -843,9 +825,6 @@ public class FileEntityHandler implements RequestHandler { } } - /** - * Получение детальной информации о файле для модерации - */ private BaseResponse handleGetFileModerationInfo(BaseRequest request, FileEntityRequest fileRequest) { try { // TODO @@ -907,9 +886,6 @@ public class FileEntityHandler implements RequestHandler { } } - /** - * Получение истории модерации файла - */ private List> getModerationHistory(String fileId) { List logs = moderationLogRepository.findByFileIdOrderByCreatedAtDesc(fileId); if (logs == null || logs.isEmpty()) { @@ -931,9 +907,6 @@ public class FileEntityHandler implements RequestHandler { .collect(Collectors.toList()); } - /** - * Получение всех апелляций файла - */ private List> getFileAppeals(String fileId) { List appeals = fileAppealRepository.findByFileId(fileId); if (appeals == null || appeals.isEmpty()) { @@ -957,214 +930,6 @@ public class FileEntityHandler implements RequestHandler { .collect(Collectors.toList()); } - /** - * Получение списка причин модерации - */ - private BaseResponse handleGetModerationReasons(BaseRequest request, FileEntityRequest fileRequest) { - try { - // TODO - // authService.useAdminAuthToken(fileRequest.getToken()); - - int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1; - int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20; - String searchQuery = fileRequest.getSearchQuery(); - - Page reasons = moderationReasonService.getAllReasons( - page - 1, pageSize, searchQuery); - - Map response = new HashMap<>(); - response.put("reasons", reasons.getContent()); - response.put("totalCount", reasons.getTotalElements()); - response.put("totalPages", reasons.getTotalPages()); - response.put("currentPage", page); - response.put("pageSize", pageSize); - - return new BaseResponse(request.getMsgId(), - MessageCode.SUCCESS.getCode(), - MessageCode.SUCCESS.getDescription(), - response); - - } catch (SecurityException e) { - return new BaseResponse(request.getMsgId(), - MessageCode.ACCESS_DENIED.getCode(), - "Admin access required", - null); - } catch (Exception e) { - log.error("Error getting moderation reasons", e); - return new BaseResponse(request.getMsgId(), - MessageCode.INVALID_FIELD.getCode(), - "Failed to get reasons: " + e.getMessage(), - null); - } - } - - /** - * Получение конкретной причины модерации - */ - private BaseResponse handleGetModerationReason(BaseRequest request, FileEntityRequest fileRequest) { - try { - // TODO - // authService.useAdminAuthToken(fileRequest.getToken()); - - Long reasonId = fileRequest.getModerationReasonId(); - if (reasonId == null) { - return new BaseResponse(request.getMsgId(), - MessageCode.INVALID_FIELD.getCode(), - "reasonId is required", - null); - } - - ModerationReasonResponse reason = moderationReasonService.getReasonById(reasonId); - - return new BaseResponse(request.getMsgId(), - MessageCode.SUCCESS.getCode(), - MessageCode.SUCCESS.getDescription(), - reason); - - } catch (ModerationReasonNotFoundException e) { - return new BaseResponse(request.getMsgId(), - MessageCode.NOT_FOUND.getCode(), - e.getMessage(), - null); - } catch (Exception e) { - log.error("Error getting moderation reason", e); - return new BaseResponse(request.getMsgId(), - MessageCode.INVALID_FIELD.getCode(), - "Failed to get reason: " + e.getMessage(), - null); - } - } - - /** - * Создание новой причины модерации - */ - private BaseResponse handleCreateModerationReason(BaseRequest request, FileEntityRequest fileRequest) { - try { - // TODO - // authService.useAdminAuthToken(fileRequest.getToken()); - - String reasonText = fileRequest.getReasonText(); - if (reasonText == null || reasonText.trim().isEmpty()) { - return new BaseResponse(request.getMsgId(), - MessageCode.INVALID_FIELD.getCode(), - "reasonText is required", - null); - } - - ModerationReasonResponse reason = moderationReasonService.createReason(reasonText); - - return new BaseResponse(request.getMsgId(), - MessageCode.SUCCESS.getCode(), - "Moderation reason created successfully", - reason); - - } catch (IllegalArgumentException e) { - return new BaseResponse(request.getMsgId(), - MessageCode.INVALID_FIELD.getCode(), - e.getMessage(), - null); - } catch (Exception e) { - log.error("Error creating moderation reason", e); - return new BaseResponse(request.getMsgId(), - MessageCode.INVALID_FIELD.getCode(), - "Failed to create reason: " + e.getMessage(), - null); - } - } - - /** - * Обновление причины модерации - */ - private BaseResponse handleUpdateModerationReason(BaseRequest request, FileEntityRequest fileRequest) { - try { - // TODO - // authService.useAdminAuthToken(fileRequest.getToken()); - - Long reasonId = fileRequest.getModerationReasonId(); - String reasonText = fileRequest.getReasonText(); - - if (reasonId == null) { - return new BaseResponse(request.getMsgId(), - MessageCode.INVALID_FIELD.getCode(), - "reasonId is required", - null); - } - - if (reasonText == null || reasonText.trim().isEmpty()) { - return new BaseResponse(request.getMsgId(), - MessageCode.INVALID_FIELD.getCode(), - "reasonText is required", - null); - } - - ModerationReasonResponse reason = moderationReasonService.updateReason(reasonId, reasonText); - - return new BaseResponse(request.getMsgId(), - MessageCode.SUCCESS.getCode(), - "Moderation reason updated successfully", - reason); - - } catch (ModerationReasonNotFoundException e) { - return new BaseResponse(request.getMsgId(), - MessageCode.NOT_FOUND.getCode(), - e.getMessage(), - null); - } catch (IllegalArgumentException e) { - return new BaseResponse(request.getMsgId(), - MessageCode.INVALID_FIELD.getCode(), - e.getMessage(), - null); - } catch (Exception e) { - log.error("Error updating moderation reason", e); - return new BaseResponse(request.getMsgId(), - MessageCode.INVALID_FIELD.getCode(), - "Failed to update reason: " + e.getMessage(), - null); - } - } - - /** - * Удаление причины модерации - */ - private BaseResponse handleDeleteModerationReason(BaseRequest request, FileEntityRequest fileRequest) { - try { - // TODO - // authService.useAdminAuthToken(fileRequest.getToken()); - - Long reasonId = fileRequest.getModerationReasonId(); - if (reasonId == null) { - return new BaseResponse(request.getMsgId(), - MessageCode.INVALID_FIELD.getCode(), - "reasonId is required", - null); - } - - moderationReasonService.deleteReason(reasonId); - - return new BaseResponse(request.getMsgId(), - MessageCode.SUCCESS.getCode(), - "Moderation reason deleted successfully", - Map.of("reasonId", reasonId)); - - } catch (ModerationReasonNotFoundException e) { - return new BaseResponse(request.getMsgId(), - MessageCode.NOT_FOUND.getCode(), - e.getMessage(), - null); - } catch (ModerationReasonInUseException e) { - return new BaseResponse(request.getMsgId(), - MessageCode.INVALID_FIELD.getCode(), - e.getMessage(), - Map.of("usageCount", e.getUsageCount())); - } catch (Exception e) { - log.error("Error deleting moderation reason", e); - return new BaseResponse(request.getMsgId(), - MessageCode.INVALID_FIELD.getCode(), - "Failed to delete reason: " + e.getMessage(), - null); - } - } - private String formatFileSize(long size) { if (size < 1024) return size + " B"; int exp = (int) (Math.log(size) / Math.log(1024)); diff --git a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java index 3b98025..0e5e014 100644 --- a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java @@ -72,8 +72,4 @@ public interface FileEntityRepository extends JpaRepository Page findByStatusIn(List statuses, Pageable pageable); List findByUserIdAndStatusIn(Long userId, List statuses); - - @Query("SELECT COUNT(f) FROM FileEntity f WHERE f.moderationReasonId = :reasonId AND f.status IN :statuses") - long countByModerationReasonIdAndStatusIn(@Param("reasonId") Long reasonId, - @Param("statuses") List statuses); } diff --git a/src/main/java/ru/soune/nocopy/repository/ModerationReasonRepository.java b/src/main/java/ru/soune/nocopy/repository/ModerationReasonRepository.java deleted file mode 100644 index 81ebce4..0000000 --- a/src/main/java/ru/soune/nocopy/repository/ModerationReasonRepository.java +++ /dev/null @@ -1,22 +0,0 @@ -package ru.soune.nocopy.repository; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import ru.soune.nocopy.entity.file.moderation.ModerationReason; - -import java.util.Optional; - -public interface ModerationReasonRepository extends JpaRepository { - Optional findByReasonText(String reasonText); - - Page findAll(Pageable pageable); - - @Query("SELECT mr FROM ModerationReason mr WHERE mr.reasonText LIKE %:search%") - Page searchByText(@Param("search") String search, Pageable pageable); - - @Query("SELECT COUNT(mr) FROM ModerationReason mr") - long countAllReasons(); -} diff --git a/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationReasonService.java b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationReasonService.java deleted file mode 100644 index 0140d4b..0000000 --- a/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationReasonService.java +++ /dev/null @@ -1,184 +0,0 @@ -package ru.soune.nocopy.service.file.moderation; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -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.ModerationReasonResponse; -import ru.soune.nocopy.entity.file.FileStatus; -import ru.soune.nocopy.entity.file.moderation.ModerationReason; -import ru.soune.nocopy.exception.ModerationReasonInUseException; -import ru.soune.nocopy.exception.ModerationReasonNotFoundException; -import ru.soune.nocopy.repository.FileEntityRepository; -import ru.soune.nocopy.repository.ModerationReasonRepository; - -import java.util.Arrays; -import java.util.List; - -@Slf4j -@Service -@RequiredArgsConstructor -public class ModerationReasonService { - - private final ModerationReasonRepository moderationReasonRepository; - private final FileEntityRepository fileEntityRepository; - - private static final List BLOCKED_STATUSES = Arrays.asList( - FileStatus.BLOCKED, - FileStatus.VIOLATION, - FileStatus.MODERATION - ); - - /** - * Создание новой причины модерации - */ - @Transactional - public ModerationReasonResponse createReason(String reasonText) { - if (reasonText == null || reasonText.trim().isEmpty()) { - throw new IllegalArgumentException("Reason text cannot be empty"); - } - - if (moderationReasonRepository.findByReasonText(reasonText.trim()).isPresent()) { - throw new IllegalArgumentException("Reason with this text already exists"); - } - - ModerationReason reason = ModerationReason.builder() - .reasonText(reasonText.trim()) - .build(); - - ModerationReason saved = moderationReasonRepository.save(reason); - - log.info("Created new moderation reason: id={}, text={}", saved.getId(), saved.getReasonText()); - - return convertToResponse(saved, false, 0L); - } - - /** - * Обновление причины модерации - */ - @Transactional - public ModerationReasonResponse updateReason(Long reasonId, String newReasonText) { - ModerationReason reason = moderationReasonRepository.findById(reasonId) - .orElseThrow(() -> new ModerationReasonNotFoundException(reasonId)); - - if (newReasonText == null || newReasonText.trim().isEmpty()) { - throw new IllegalArgumentException("Reason text cannot be empty"); - } - - moderationReasonRepository.findByReasonText(newReasonText.trim()) - .ifPresent(existingReason -> { - if (!existingReason.getId().equals(reasonId)) { - throw new IllegalArgumentException("Reason with this text already exists"); - } - }); - - String oldText = reason.getReasonText(); - reason.setReasonText(newReasonText.trim()); - - ModerationReason updated = moderationReasonRepository.save(reason); - - log.info("Updated moderation reason: id={}, oldText={}, newText={}", - reasonId, oldText, newReasonText); - - long usageCount = getReasonUsageCount(reasonId); - - return convertToResponse(updated, usageCount > 0, usageCount); - } - - /** - * Удаление причины модерации - */ - @Transactional - public void deleteReason(Long reasonId) { - ModerationReason reason = moderationReasonRepository.findById(reasonId) - .orElseThrow(() -> new ModerationReasonNotFoundException(reasonId)); - - long usageCount = getReasonUsageCount(reasonId); - - if (usageCount > 0) { - throw new ModerationReasonInUseException(reasonId, usageCount); - } - - moderationReasonRepository.deleteById(reasonId); - - log.info("Deleted moderation reason: id={}, text={}", reasonId, reason.getReasonText()); - } - - /** - * Получение причины по ID - */ - @Transactional(readOnly = true) - public ModerationReasonResponse getReasonById(Long reasonId) { - ModerationReason reason = moderationReasonRepository.findById(reasonId) - .orElseThrow(() -> new ModerationReasonNotFoundException(reasonId)); - - long usageCount = getReasonUsageCount(reasonId); - - return convertToResponse(reason, usageCount > 0, usageCount); - } - - /** - * Получение всех причин с пагинацией - */ - @Transactional(readOnly = true) - public Page getAllReasons(int page, int size, String searchQuery) { - Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); - - Page reasonsPage; - - if (searchQuery != null && !searchQuery.trim().isEmpty()) { - reasonsPage = moderationReasonRepository.searchByText(searchQuery.trim(), pageable); - } else { - reasonsPage = moderationReasonRepository.findAll(pageable); - } - - return reasonsPage.map(reason -> { - long usageCount = getReasonUsageCount(reason.getId()); - return convertToResponse(reason, usageCount > 0, usageCount); - }); - } - - /** - * Получение списка всех причин (без пагинации, для выпадающих списков) - */ - @Transactional(readOnly = true) - public List getAllReasonsList() { - List reasons = moderationReasonRepository.findAll(Sort.by("reasonText").ascending()); - - return reasons.stream() - .map(reason -> { - long usageCount = getReasonUsageCount(reason.getId()); - return convertToResponse(reason, usageCount > 0, usageCount); - }) - .collect(java.util.stream.Collectors.toList()); - } - - /** - * Подсчет количества файлов, заблокированных по данной причине - */ - private long getReasonUsageCount(Long reasonId) { - return fileEntityRepository.countByModerationReasonIdAndStatusIn(reasonId, BLOCKED_STATUSES); - } - - /** - * Проверка, используется ли причина в активных блокировках - */ - public boolean isReasonInUse(Long reasonId) { - return getReasonUsageCount(reasonId) > 0; - } - - private ModerationReasonResponse convertToResponse(ModerationReason reason, boolean isUsed, long usageCount) { - return ModerationReasonResponse.builder() - .id(reason.getId()) - .reasonText(reason.getReasonText()) - .createdAt(reason.getCreatedAt()) - .updatedAt(reason.getUpdatedAt()) - .isUsed(isUsed) - .usageCount(usageCount) - .build(); - } -} diff --git a/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java index 8a7b4d8..289f194 100644 --- a/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java +++ b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java @@ -57,14 +57,13 @@ public class ModerationService { .oldStatus(file.getStatus()) .newStatus(request.getNewStatus()) .reason(request.getComment()) - .moderationReasonId(request.getModerationReasonId()) .comment(request.getComment()) .build(); moderationLogRepository.save(log); file.setStatus(request.getNewStatus()); - file.setModerationReasonId(request.getModerationReasonId()); + fileEntityRepository.save(file); if (request.getNewStatus() == FileStatus.BLOCKED || @@ -107,7 +106,6 @@ public class ModerationService { .additionalInfo(request.getAdditionalInfo()) .status(AppealStatus.PENDING) .moderationBeforeStatus(file.getStatus()) - .moderationReasonId(file.getModerationReasonId()) .build(); FileAppeal saved = fileAppealRepository.save(appeal); @@ -137,7 +135,6 @@ public class ModerationService { if (approve) { file.setStatus(FileStatus.ACTIVE); - file.setModerationReasonId(null); } ModerationLog log = ModerationLog.builder() From 0aa7f5c86588282e4b50769740ec8124a776b801 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 27 Mar 2026 19:08:15 +0700 Subject: [PATCH 255/274] dev add reason entity --- .../soune/nocopy/entity/file/FileEntity.java | 2 +- .../nocopy/handler/FileEntityHandler.java | 12 ++-- .../repository/FileEntityRepository.java | 3 + .../service/file/FileEntityService.java | 70 +++---------------- .../file/impl/FileUploadServiceImpl.java | 2 +- 5 files changed, 19 insertions(+), 70 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java b/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java index 02104c7..9182674 100644 --- a/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java +++ b/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java @@ -98,7 +98,7 @@ public class FileEntity { } if (this.status == null) { - this.status = FileStatus.ACTIVE; + this.status = FileStatus.MODERATION; } if (this.protectionStatus == null) { diff --git a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java index a254184..660ee7c 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java @@ -216,13 +216,13 @@ public class FileEntityHandler implements RequestHandler { return new BaseResponse(request.getMsgId(), MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(), - FileEntityResponse.builder().id(fileId)); + FileEntityResponse.builder().id(fileId).build()); } return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), MessageCode.SUCCESS.getDescription(), - FileEntityResponse.builder().status(newStatus)); + FileEntityResponse.builder().status(newStatus).build()); } private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) { @@ -627,25 +627,25 @@ public class FileEntityHandler implements RequestHandler { return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS_MODERATION.getCode(), MessageCode.SUCCESS_MODERATION.getDescription(), - FileEntityResponse.builder().id(fileRequest.getFileId())); + FileEntityResponse.builder().id(fileRequest.getFileId()).build()); } catch (SecurityException e) { return new BaseResponse(request.getMsgId(), MessageCode.ACCESS_DENIED.getCode(), MessageCode.ACCESS_DENIED.getDescription(), - FileEntityResponse.builder().id(fileRequest.getFileId())); + FileEntityResponse.builder().id(fileRequest.getFileId()).build()); } catch (FileEntityNotFoundException e) { return new BaseResponse(request.getMsgId(), MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(), FileEntityResponse.builder() - .id(fileRequest.getFileId())); + .id(fileRequest.getFileId()).build()); } catch (IllegalStateException e) { return new BaseResponse(request.getMsgId(), MessageCode.ILLEGAL_STATE.getCode(), MessageCode.ILLEGAL_STATE.getDescription(), FileEntityResponse.builder() - .status(fileRequest.getFileStatus())); + .status(fileRequest.getFileStatus()).build()); } } diff --git a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java index 0e5e014..9d9cb0f 100644 --- a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java @@ -10,6 +10,7 @@ import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.ProtectionStatus; +import java.util.Collection; import java.util.List; import java.util.Optional; @@ -23,6 +24,8 @@ public interface FileEntityRepository extends JpaRepository List findByUserIdAndStatus(Long userId, FileStatus status); + List findByUserIdAndStatusIn(Long userId, Collection statuses); + Optional findByFilePath(String filePath); Optional findByUploadSessionId(String uploadSessionId); diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index 16e2e3b..cebce6c 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -53,63 +53,6 @@ public class FileEntityService { @Value("${server.baseurl}") private String baseUrl; - @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 imageHash = Map.of(); - - if (session.getFileType().startsWith("image")) { - imageHash = imageHashService.calculateHash(filePath); - List 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); - } - } - @Transactional(readOnly = true) public FileEntityResponse getById(String fileId, int version) { FileEntity fileEntity = fileEntityRepository.findById(fileId) @@ -144,10 +87,12 @@ 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, FileStatus.MODERATION))); } } else { - allFiles = fileEntityRepository.findByUserIdAndStatus(userId, FileStatus.ACTIVE); + fileEntityRepository.findByUserIdAndStatusIn(userId, + List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION)); } return allFiles; @@ -162,12 +107,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, FileStatus.MODERATION))); } } else { - allFiles = fileEntityRepository.findByUserIdAndStatus( - userId, FileStatus.ACTIVE); + allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId, + List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION)); } int start = (page - 1) * pageSize; diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index f0a8050..a0016e3 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -364,7 +364,7 @@ public class FileUploadServiceImpl implements FileUploadService { 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()); From fdea57d9479281e925be13ae4463a6ca235436bb Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 27 Mar 2026 19:29:54 +0700 Subject: [PATCH 256/274] dev add reason entity --- .../soune/nocopy/service/file/FileEntityService.java | 10 +++++----- .../service/file/moderation/ModerationService.java | 7 +++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index cebce6c..5ef91f3 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -168,13 +168,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); } diff --git a/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java index 289f194..4e8b01f 100644 --- a/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java +++ b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java @@ -19,6 +19,7 @@ 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; @@ -76,6 +77,12 @@ public class ModerationService { appeal.setResolvedAt(LocalDateTime.now()); fileAppealRepository.save(appeal); }); + } else if (request.getNewStatus() == FileStatus.REMOVED) { + try { + fileEntityService.softDeleteFileWithHash(file); + } catch (IOException e) { + throw new RuntimeException(e); + } } } From 535be69840d3fdd244ece7103c4f8b097d8500c5 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 27 Mar 2026 19:35:55 +0700 Subject: [PATCH 257/274] dev add reason entity --- src/main/java/ru/soune/nocopy/controller/PayoutController.java | 3 ++- .../ru/soune/nocopy/service/payout/PayoutRequestService.java | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/PayoutController.java b/src/main/java/ru/soune/nocopy/controller/PayoutController.java index 39246f5..1b57c74 100644 --- a/src/main/java/ru/soune/nocopy/controller/PayoutController.java +++ b/src/main/java/ru/soune/nocopy/controller/PayoutController.java @@ -15,6 +15,7 @@ import ru.soune.nocopy.entity.payout.PayoutType; import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.InsufficientFundsException; +import ru.soune.nocopy.exception.InvalidPayoutMethodException; import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.exception.PendingPayoutExistsException; import ru.soune.nocopy.repository.AuthTokenRepository; @@ -56,7 +57,7 @@ public class PayoutController { try { PayoutRequest payoutRequest = payoutRequestService.createPayoutRequest(user.getId(), request); return ResponseEntity.status(HttpStatus.CREATED).body(payoutRequest); - } catch (InsufficientFundsException | PendingPayoutExistsException e) { + } catch (InsufficientFundsException | PendingPayoutExistsException | InvalidPayoutMethodException e) { return ResponseEntity.status(HttpStatus.CONFLICT).body(e.getMessage()); } } diff --git a/src/main/java/ru/soune/nocopy/service/payout/PayoutRequestService.java b/src/main/java/ru/soune/nocopy/service/payout/PayoutRequestService.java index 1db44cf..f8bc0be 100644 --- a/src/main/java/ru/soune/nocopy/service/payout/PayoutRequestService.java +++ b/src/main/java/ru/soune/nocopy/service/payout/PayoutRequestService.java @@ -75,9 +75,6 @@ public class PayoutRequestService { balanceService.freezeAmount(user, request.getAmount()); - log.info("Payout requset created: ID={}, user={}, sum={}, status={}", - savedRequest.getId(), user.getId(), request.getAmount(), savedRequest.getStatus()); - return savedRequest; } From 6d6721fb0a40d4caa71273246fbe65b29dc632e5 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 27 Mar 2026 21:54:36 +0700 Subject: [PATCH 258/274] dev add reason entity --- referral/src/main/kotlin/Referral.kt | 1 + referral/src/main/kotlin/ReferralService.kt | 4 +++- referral/src/main/kotlin/ReferralStat.kt | 1 + .../soune/nocopy/service/referral/ReferralRepoImpl.java | 8 ++++++-- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/referral/src/main/kotlin/Referral.kt b/referral/src/main/kotlin/Referral.kt index 4b65d8a..cb0e89f 100644 --- a/referral/src/main/kotlin/Referral.kt +++ b/referral/src/main/kotlin/Referral.kt @@ -9,5 +9,6 @@ interface Referral { val totalIncome: Int val availableIncome: Int val holdBalance: Int + val spentBalance: Int val active: Boolean } \ No newline at end of file diff --git a/referral/src/main/kotlin/ReferralService.kt b/referral/src/main/kotlin/ReferralService.kt index 489e149..d36534e 100644 --- a/referral/src/main/kotlin/ReferralService.kt +++ b/referral/src/main/kotlin/ReferralService.kt @@ -18,6 +18,7 @@ class ReferralService( override val totalIncome = 0 override val availableIncome = 0 override val holdBalance: Int = 0 + override val spentBalance: Int = 0 override val active: Boolean = false } @@ -59,7 +60,8 @@ class ReferralService( totalIncome = referral.totalIncome, availableIncome = referral.availableIncome, holdBalance = referral.holdBalance, - referralLink = referral.referralLink + referralLink = referral.referralLink, + spentBalance = referral.spentBalance, ) } diff --git a/referral/src/main/kotlin/ReferralStat.kt b/referral/src/main/kotlin/ReferralStat.kt index d9e6ae5..fa05122 100644 --- a/referral/src/main/kotlin/ReferralStat.kt +++ b/referral/src/main/kotlin/ReferralStat.kt @@ -7,4 +7,5 @@ data class ReferralStat( val availableIncome: Int, val holdBalance: Int, val referralLink: String, + val spentBalance: Int, ) \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/referral/ReferralRepoImpl.java b/src/main/java/ru/soune/nocopy/service/referral/ReferralRepoImpl.java index c210ed3..b02d0b4 100644 --- a/src/main/java/ru/soune/nocopy/service/referral/ReferralRepoImpl.java +++ b/src/main/java/ru/soune/nocopy/service/referral/ReferralRepoImpl.java @@ -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(); } }; From fa1892585a5429c714699ad01f9264f65d76b818 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 27 Mar 2026 22:26:22 +0700 Subject: [PATCH 259/274] dev add reason entity --- .../repository/FileEntityRepository.java | 3 +++ .../service/file/FileEntityService.java | 25 +------------------ 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java index 9d9cb0f..e54e23b 100644 --- a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java @@ -75,4 +75,7 @@ public interface FileEntityRepository extends JpaRepository Page findByStatusIn(List statuses, Pageable pageable); List findByUserIdAndStatusIn(Long userId, List statuses); + + @Query("SELECT f FROM FileEntity f WHERE f.userId = :userId AND f.status IN :statuses") + List findByStatuses(Long userId, @Param("statuses") List statuses); } diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index 5ef91f3..bb70f14 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -9,19 +9,14 @@ 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.FileMonitoringRepository; -import ru.soune.nocopy.repository.SimilarImageProjection; import ru.soune.nocopy.repository.UserRepository; -import ru.soune.nocopy.service.FileSimilarityService; -import ru.soune.nocopy.service.ImageHashService; import java.io.File; import java.io.IOException; @@ -31,7 +26,6 @@ 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; @@ -42,10 +36,6 @@ public class FileEntityService { private final FileEntityRepository fileEntityRepository; - private final ImageHashService imageHashService; - - private final FileSimilarityService fileSimilarityService; - private final UserRepository userRepository; private final ImageResizeService imageResizeService; @@ -91,7 +81,7 @@ public class FileEntityService { List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION))); } } else { - fileEntityRepository.findByUserIdAndStatusIn(userId, + allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId, List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION)); } @@ -150,19 +140,6 @@ public class FileEntityService { .build(); } - @Transactional - public FileEntity markAsDeleted(FileEntity fileEntity) throws IOException { - fileEntity.setStatus(FileStatus.DELETED); - fileEntity.setUpdatedAt(LocalDateTime.now()); - fileEntity.setProtectionStatus(ProtectionStatus.NOT_PROTECTED); - - Path path = Paths.get(fileEntity.getProtectedFilePath()); - Files.deleteIfExists(path); - fileEntity.setProtectedFilePath(""); - - return fileEntityRepository.save(fileEntity); - } - public void softDeleteFileWithHash(FileEntity fileEntity) throws IOException { if (fileEntity.getImageHash() != null) { fileEntity.setImageHash(null); From 8ce07c6b8160bd9e5ba71326952d72dd397baac5 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 30 Mar 2026 11:31:31 +0700 Subject: [PATCH 260/274] dev add reason entity --- .../java/ru/soune/nocopy/scheduler/MonitoringScheduler.java | 1 - .../nocopy/service/monitoring/MonitoringSearchService.java | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/scheduler/MonitoringScheduler.java b/src/main/java/ru/soune/nocopy/scheduler/MonitoringScheduler.java index 4a92b57..442bcbe 100644 --- a/src/main/java/ru/soune/nocopy/scheduler/MonitoringScheduler.java +++ b/src/main/java/ru/soune/nocopy/scheduler/MonitoringScheduler.java @@ -9,7 +9,6 @@ import org.springframework.transaction.annotation.Transactional; import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; import ru.soune.nocopy.repository.FileMonitoringRepository; import ru.soune.nocopy.service.monitoring.MonitoringSearchService; -import ru.soune.nocopy.service.tariff.TariffInfoService; import java.time.LocalDateTime; import java.util.List; diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java index 93f43f3..bcfdabb 100644 --- a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java @@ -48,7 +48,7 @@ public class MonitoringSearchService { private final SearchProperties searchProperties; - @Transactional + @Transactional(noRollbackFor = TariffNotFoundException.class) public void processFileSearch(FileMonitoringEntity monitoring) throws MessagingException, IOException { monitoring.setLastRun(LocalDateTime.now()); monitoring.setLastRunStatus("IN_PROGRESS"); @@ -141,8 +141,6 @@ public class MonitoringSearchService { } catch (Exception e) { log.error("Error processing monitoring search", e); monitoring.setLastRunStatus("ERROR: " + e.getMessage()); - - } finally { updateNextRun(monitoring); monitoringRepository.save(monitoring); } From 7e1877ffc3e2565809be15e5b0f9731fff11cfe4 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 30 Mar 2026 12:14:06 +0700 Subject: [PATCH 261/274] dev email send when tokens not found --- .../search/GlobalSearchAsyncProcessor.java | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java index 19b1644..9f12527 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -1,5 +1,6 @@ package ru.soune.nocopy.service.search; +import jakarta.mail.MessagingException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; @@ -11,8 +12,14 @@ 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.entity.tarif.TariffInfo; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.exception.TariffNotFoundException; +import ru.soune.nocopy.exception.TokenNotFoundException; import ru.soune.nocopy.repository.GlobalSearchResultRepository; import ru.soune.nocopy.repository.GlobalSearchTaskRepository; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.service.mail.EmailService; import ru.soune.nocopy.service.tariff.TariffConstants; import ru.soune.nocopy.service.tariff.TariffInfoService; import ru.soune.nocopy.service.violation.ViolationService; @@ -41,6 +48,10 @@ public class GlobalSearchAsyncProcessor { private final SearchProperties searchProperties; + private final UserRepository userRepository; + + private final EmailService emailService; + @Async @Transactional public void processFilesAsync(String taskId, List filesToProcess, Long userId) { @@ -72,12 +83,26 @@ public class GlobalSearchAsyncProcessor { } } - private GlobalSearchResult processFile(FileEntity file, String taskId, Long userId) { + private GlobalSearchResult processFile(FileEntity file, String taskId, Long userId) throws MessagingException, IOException { GlobalSearchResult result = new GlobalSearchResult(); result.setTaskId(taskId); result.setFileId(file.getId()); result.setFileName(file.getOriginalFileName()); + try { + tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH); + } catch (TariffNotFoundException e) { + User user = userRepository.findById(file.getUserId()).orElseThrow(); + TariffInfo activeTariffInfo = user.getActiveTariffInfo(); + int currentTokens = activeTariffInfo.getTokens() + activeTariffInfo.getBoughtTokens(); + + emailService.sendTokensNotFoundEmail(user, currentTokens, TariffConstants.TOKEN_VALUE_FOR_SEARCH); + + result.setFileStatus(SearchStatus.FAILED.name()); + + return globalSearchResultRepository.save(result); + } + result = globalSearchResultRepository.save(result); boolean useGoogle = searchProperties.getEngines().getOrDefault("google", @@ -171,10 +196,6 @@ public class GlobalSearchAsyncProcessor { log.warn("No results for file {} because all engines are disabled", file.getId()); } else { result.setFileStatus(SearchStatus.SUCCESS.name()); - - if (!allUniqueImages.isEmpty()) { - tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH); - } } return result; From dbef82d8966e6c484f79263480311426348e552b Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 30 Mar 2026 12:29:53 +0700 Subject: [PATCH 262/274] dev email send when tokens not found --- .../nocopy/service/search/GlobalSearchAsyncProcessor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java index 9f12527..aba3591 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -99,8 +99,9 @@ public class GlobalSearchAsyncProcessor { emailService.sendTokensNotFoundEmail(user, currentTokens, TariffConstants.TOKEN_VALUE_FOR_SEARCH); result.setFileStatus(SearchStatus.FAILED.name()); + globalSearchResultRepository.save(result); - return globalSearchResultRepository.save(result); + throw new TariffNotFoundException("Tariff not found"); } result = globalSearchResultRepository.save(result); From 976d1f751eaa15d430803831b8f7febde4da3643 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 30 Mar 2026 12:48:11 +0700 Subject: [PATCH 263/274] dev email send when tokens not found --- .../InsufficientTokensException.java | 7 ++ .../exception/SearchFailedException.java | 7 ++ .../search/GlobalSearchAsyncProcessor.java | 68 ++++++++++++++++--- 3 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/exception/InsufficientTokensException.java create mode 100644 src/main/java/ru/soune/nocopy/exception/SearchFailedException.java diff --git a/src/main/java/ru/soune/nocopy/exception/InsufficientTokensException.java b/src/main/java/ru/soune/nocopy/exception/InsufficientTokensException.java new file mode 100644 index 0000000..1b6dcb4 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/InsufficientTokensException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class InsufficientTokensException extends RuntimeException { + public InsufficientTokensException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/SearchFailedException.java b/src/main/java/ru/soune/nocopy/exception/SearchFailedException.java new file mode 100644 index 0000000..9b505dd --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/SearchFailedException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class SearchFailedException extends RuntimeException { + public SearchFailedException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java index aba3591..5c7538f 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -14,8 +14,9 @@ import ru.soune.nocopy.entity.search.GlobalSearchTask; import ru.soune.nocopy.entity.search.SearchStatus; import ru.soune.nocopy.entity.tarif.TariffInfo; import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.exception.InsufficientTokensException; +import ru.soune.nocopy.exception.SearchFailedException; import ru.soune.nocopy.exception.TariffNotFoundException; -import ru.soune.nocopy.exception.TokenNotFoundException; import ru.soune.nocopy.repository.GlobalSearchResultRepository; import ru.soune.nocopy.repository.GlobalSearchTaskRepository; import ru.soune.nocopy.repository.UserRepository; @@ -52,6 +53,42 @@ public class GlobalSearchAsyncProcessor { private final EmailService emailService; +// @Async +// @Transactional +// public void processFilesAsync(String taskId, List filesToProcess, Long userId) { +// try { +// for (int i = 0; i < filesToProcess.size(); i++) { +// FileEntity file = filesToProcess.get(i); +// +// GlobalSearchResult result = processFile(file, taskId, userId); +// globalSearchResultRepository.save(result); +// +// if (result.getFileStatus().equals(SearchStatus.FAILED.name())) { +// break; +// } +// +// 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); +// } +// } + + @Async @Transactional public void processFilesAsync(String taskId, List filesToProcess, Long userId) { @@ -59,13 +96,27 @@ public class GlobalSearchAsyncProcessor { for (int i = 0; i < filesToProcess.size(); i++) { FileEntity file = filesToProcess.get(i); - GlobalSearchResult result = processFile(file, taskId, userId); - globalSearchResultRepository.save(result); + try { + 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); + if (result.getFileStatus().equals(SearchStatus.FAILED.name()) || + result.getFileStatus().equals(SearchStatus.TIMEOUT.name())) { + throw new SearchFailedException("File processing failed for file: " + file.getId()); + } + + GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow(); + task.setProcessedFiles(i + 1); + task.setUpdatedAt(LocalDateTime.now()); + globalSearchTaskRepository.save(task); + + } catch (InsufficientTokensException e) { + log.warn("Insufficient tokens for user {}, stopping task: {}", userId, taskId); + throw e; + } catch (Exception e) { + log.error("Error processing file {} in task {}", file.getId(), taskId, e); + throw new SearchFailedException("Failed to process file: " + file.getId()); + } } GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow(); @@ -99,9 +150,10 @@ public class GlobalSearchAsyncProcessor { emailService.sendTokensNotFoundEmail(user, currentTokens, TariffConstants.TOKEN_VALUE_FOR_SEARCH); result.setFileStatus(SearchStatus.FAILED.name()); + globalSearchResultRepository.save(result); - throw new TariffNotFoundException("Tariff not found"); + throw new InsufficientTokensException("Insufficient tokens for user: " + userId); } result = globalSearchResultRepository.save(result); From 7af909c4f2782029220cf29a5dd944ed9e564a9e Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 30 Mar 2026 13:21:11 +0700 Subject: [PATCH 264/274] dev email send when tokens not found --- .../search/GlobalSearchAsyncProcessor.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java index 5c7538f..e0a71f8 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -111,7 +111,7 @@ public class GlobalSearchAsyncProcessor { globalSearchTaskRepository.save(task); } catch (InsufficientTokensException e) { - log.warn("Insufficient tokens for user {}, stopping task: {}", userId, taskId); + log.warn("Insufficient tokens for user {}, stopping task: {}", userId, taskId, e); throw e; } catch (Exception e) { log.error("Error processing file {} in task {}", file.getId(), taskId, e); @@ -124,6 +124,14 @@ public class GlobalSearchAsyncProcessor { task.setUpdatedAt(LocalDateTime.now()); globalSearchTaskRepository.save(task); + } catch (InsufficientTokensException e) { + log.error("Task {} failed due to insufficient tokens for user {}", taskId, userId, e); + + GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow(); + task.setStatus(SearchStatus.FAILED.name()); + task.setUpdatedAt(LocalDateTime.now()); + globalSearchTaskRepository.save(task); + } catch (Exception e) { log.error("Global search failed for task: {}", taskId, e); @@ -134,7 +142,8 @@ public class GlobalSearchAsyncProcessor { } } - private GlobalSearchResult processFile(FileEntity file, String taskId, Long userId) throws MessagingException, IOException { + private GlobalSearchResult processFile(FileEntity file, String taskId, Long userId) throws MessagingException, + IOException, InsufficientTokensException { GlobalSearchResult result = new GlobalSearchResult(); result.setTaskId(taskId); result.setFileId(file.getId()); @@ -147,8 +156,9 @@ public class GlobalSearchAsyncProcessor { TariffInfo activeTariffInfo = user.getActiveTariffInfo(); int currentTokens = activeTariffInfo.getTokens() + activeTariffInfo.getBoughtTokens(); + log.info("Before sending email for user: {}", user.getEmail()); emailService.sendTokensNotFoundEmail(user, currentTokens, TariffConstants.TOKEN_VALUE_FOR_SEARCH); - + log.info("Email sent successfully to user: {}", user.getEmail()); result.setFileStatus(SearchStatus.FAILED.name()); globalSearchResultRepository.save(result); From 41ac8933e9ef213449bfc81f4e02ee0d9c623376 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 30 Mar 2026 13:36:57 +0700 Subject: [PATCH 265/274] dev email send when tokens not found --- .../search/GlobalSearchAsyncProcessor.java | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java index e0a71f8..e7080a2 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -17,6 +17,7 @@ import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.InsufficientTokensException; import ru.soune.nocopy.exception.SearchFailedException; import ru.soune.nocopy.exception.TariffNotFoundException; +import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.GlobalSearchResultRepository; import ru.soune.nocopy.repository.GlobalSearchTaskRepository; import ru.soune.nocopy.repository.UserRepository; @@ -53,6 +54,8 @@ public class GlobalSearchAsyncProcessor { private final EmailService emailService; + private final FileEntityRepository fileEntityRepository; + // @Async // @Transactional // public void processFilesAsync(String taskId, List filesToProcess, Long userId) { @@ -97,7 +100,7 @@ public class GlobalSearchAsyncProcessor { FileEntity file = filesToProcess.get(i); try { - GlobalSearchResult result = processFile(file, taskId, userId); + GlobalSearchResult result = processFile(file.getId(), taskId, userId); globalSearchResultRepository.save(result); if (result.getFileStatus().equals(SearchStatus.FAILED.name()) || @@ -109,7 +112,6 @@ public class GlobalSearchAsyncProcessor { task.setProcessedFiles(i + 1); task.setUpdatedAt(LocalDateTime.now()); globalSearchTaskRepository.save(task); - } catch (InsufficientTokensException e) { log.warn("Insufficient tokens for user {}, stopping task: {}", userId, taskId, e); throw e; @@ -124,7 +126,7 @@ public class GlobalSearchAsyncProcessor { task.setUpdatedAt(LocalDateTime.now()); globalSearchTaskRepository.save(task); - } catch (InsufficientTokensException e) { + } catch (InsufficientTokensException e) { log.error("Task {} failed due to insufficient tokens for user {}", taskId, userId, e); GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow(); @@ -132,18 +134,13 @@ public class GlobalSearchAsyncProcessor { 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) throws MessagingException, + private GlobalSearchResult processFile(String fileId, String taskId, Long userId) throws MessagingException, IOException, InsufficientTokensException { + FileEntity file = fileEntityRepository.findById(fileId).orElseThrow(); + GlobalSearchResult result = new GlobalSearchResult(); result.setTaskId(taskId); result.setFileId(file.getId()); From 2445b70c7a003221fd50f12ae8882f2bd0ec439c Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 30 Mar 2026 13:48:10 +0700 Subject: [PATCH 266/274] dev email send when tokens not found --- .../search/GlobalSearchAsyncProcessor.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java index e7080a2..03f5442 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import ru.soune.nocopy.configuration.search.SearchProperties; import ru.soune.nocopy.dto.file.YandexSearchResponse; @@ -113,11 +114,14 @@ public class GlobalSearchAsyncProcessor { task.setUpdatedAt(LocalDateTime.now()); globalSearchTaskRepository.save(task); } catch (InsufficientTokensException e) { - log.warn("Insufficient tokens for user {}, stopping task: {}", userId, taskId, e); - throw e; + log.error("Task {} failed due to insufficient tokens for user {}", taskId, userId, e); + + updateTaskStatus(taskId, SearchStatus.FAILED.name()); + } catch (Exception e) { - log.error("Error processing file {} in task {}", file.getId(), taskId, e); - throw new SearchFailedException("Failed to process file: " + file.getId()); + log.error("Global search failed for task: {}", taskId, e); + + updateTaskStatus(taskId, SearchStatus.FAILED.name()); } } @@ -137,6 +141,18 @@ public class GlobalSearchAsyncProcessor { } } + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void updateTaskStatus(String taskId, String status) { + try { + GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow(); + task.setStatus(status); + task.setUpdatedAt(LocalDateTime.now()); + globalSearchTaskRepository.save(task); + } catch (Exception e) { + log.error("Failed to update task status for task: {}", taskId, e); + } + } + private GlobalSearchResult processFile(String fileId, String taskId, Long userId) throws MessagingException, IOException, InsufficientTokensException { FileEntity file = fileEntityRepository.findById(fileId).orElseThrow(); From e78f8824176d0f84a9cd55030a35490f3c5efecd Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 30 Mar 2026 14:28:56 +0700 Subject: [PATCH 267/274] dev email send when tokens not found --- .../search/GlobalSearchAsyncProcessor.java | 121 ++---------------- .../service/tariff/TariffInfoService.java | 14 +- 2 files changed, 22 insertions(+), 113 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java index 03f5442..c155366 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -1,11 +1,9 @@ package ru.soune.nocopy.service.search; -import jakarta.mail.MessagingException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import ru.soune.nocopy.configuration.search.SearchProperties; import ru.soune.nocopy.dto.file.YandexSearchResponse; @@ -13,16 +11,8 @@ 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.entity.tarif.TariffInfo; -import ru.soune.nocopy.entity.user.User; -import ru.soune.nocopy.exception.InsufficientTokensException; -import ru.soune.nocopy.exception.SearchFailedException; -import ru.soune.nocopy.exception.TariffNotFoundException; -import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.GlobalSearchResultRepository; import ru.soune.nocopy.repository.GlobalSearchTaskRepository; -import ru.soune.nocopy.repository.UserRepository; -import ru.soune.nocopy.service.mail.EmailService; import ru.soune.nocopy.service.tariff.TariffConstants; import ru.soune.nocopy.service.tariff.TariffInfoService; import ru.soune.nocopy.service.violation.ViolationService; @@ -51,78 +41,20 @@ public class GlobalSearchAsyncProcessor { private final SearchProperties searchProperties; - private final UserRepository userRepository; - - private final EmailService emailService; - - private final FileEntityRepository fileEntityRepository; - -// @Async -// @Transactional -// public void processFilesAsync(String taskId, List filesToProcess, Long userId) { -// try { -// for (int i = 0; i < filesToProcess.size(); i++) { -// FileEntity file = filesToProcess.get(i); -// -// GlobalSearchResult result = processFile(file, taskId, userId); -// globalSearchResultRepository.save(result); -// -// if (result.getFileStatus().equals(SearchStatus.FAILED.name())) { -// break; -// } -// -// 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); -// } -// } - - @Async @Transactional public void processFilesAsync(String taskId, List 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); - try { - GlobalSearchResult result = processFile(file.getId(), taskId, userId); - globalSearchResultRepository.save(result); - - if (result.getFileStatus().equals(SearchStatus.FAILED.name()) || - result.getFileStatus().equals(SearchStatus.TIMEOUT.name())) { - throw new SearchFailedException("File processing failed for file: " + file.getId()); - } - - GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow(); - task.setProcessedFiles(i + 1); - task.setUpdatedAt(LocalDateTime.now()); - globalSearchTaskRepository.save(task); - } catch (InsufficientTokensException e) { - log.error("Task {} failed due to insufficient tokens for user {}", taskId, userId, e); - - updateTaskStatus(taskId, SearchStatus.FAILED.name()); - - } catch (Exception e) { - log.error("Global search failed for task: {}", taskId, e); - - updateTaskStatus(taskId, SearchStatus.FAILED.name()); - } + GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow(); + task.setProcessedFiles(i + 1); + task.setUpdatedAt(LocalDateTime.now()); + globalSearchTaskRepository.save(task); } GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow(); @@ -130,55 +62,22 @@ public class GlobalSearchAsyncProcessor { task.setUpdatedAt(LocalDateTime.now()); globalSearchTaskRepository.save(task); - } catch (InsufficientTokensException e) { - log.error("Task {} failed due to insufficient tokens for user {}", taskId, userId, e); + } 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); - } } - @Transactional(propagation = Propagation.REQUIRES_NEW) - public void updateTaskStatus(String taskId, String status) { - try { - GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow(); - task.setStatus(status); - task.setUpdatedAt(LocalDateTime.now()); - globalSearchTaskRepository.save(task); - } catch (Exception e) { - log.error("Failed to update task status for task: {}", taskId, e); - } - } - - private GlobalSearchResult processFile(String fileId, String taskId, Long userId) throws MessagingException, - IOException, InsufficientTokensException { - FileEntity file = fileEntityRepository.findById(fileId).orElseThrow(); - + private GlobalSearchResult processFile(FileEntity file, String taskId, Long userId) { GlobalSearchResult result = new GlobalSearchResult(); result.setTaskId(taskId); result.setFileId(file.getId()); result.setFileName(file.getOriginalFileName()); - try { - tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH); - } catch (TariffNotFoundException e) { - User user = userRepository.findById(file.getUserId()).orElseThrow(); - TariffInfo activeTariffInfo = user.getActiveTariffInfo(); - int currentTokens = activeTariffInfo.getTokens() + activeTariffInfo.getBoughtTokens(); - - log.info("Before sending email for user: {}", user.getEmail()); - emailService.sendTokensNotFoundEmail(user, currentTokens, TariffConstants.TOKEN_VALUE_FOR_SEARCH); - log.info("Email sent successfully to user: {}", user.getEmail()); - result.setFileStatus(SearchStatus.FAILED.name()); - - globalSearchResultRepository.save(result); - - throw new InsufficientTokensException("Insufficient tokens for user: " + userId); - } - result = globalSearchResultRepository.save(result); boolean useGoogle = searchProperties.getEngines().getOrDefault("google", diff --git a/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java b/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java index 426a6c6..d288b70 100644 --- a/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java +++ b/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java @@ -1,5 +1,6 @@ package ru.soune.nocopy.service.tariff; +import jakarta.mail.MessagingException; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -13,7 +14,9 @@ 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; @@ -28,6 +31,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(); @@ -41,7 +46,7 @@ public class TariffInfoService { } @Transactional - public void writeOffTokens(long userId, Integer value) { + public void writeOffTokens(long userId, Integer value) throws MessagingException, IOException { User user = userRepository.findById(userId).orElseThrow(); TariffInfo activeTariffInfo; @@ -55,7 +60,7 @@ public class TariffInfoService { writeOffTokensForTariff(activeTariffInfo, value); } - public void writeOffTokensForTariff(TariffInfo activeTariffInfo, Integer value) { + public void writeOffTokensForTariff(TariffInfo activeTariffInfo, Integer value) throws MessagingException, IOException { Integer tokens = activeTariffInfo.getTokens(); Integer boughtTokens = activeTariffInfo.getBoughtTokens(); @@ -68,6 +73,11 @@ public class TariffInfoService { tariffInfoRepository.save(activeTariffInfo); } else { + int needTokens = boughtTokens - value; + User user = userRepository.findByPersonalTariffInfo(activeTariffInfo); + + emailService.sendTokensNotFoundEmail(user, tokens, Math.abs(needTokens)); + throw new TariffNotFoundException("User not have tokens for protect"); } } From a7b9f229395b7e08966a37c399fd082c6268cb3f Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 30 Mar 2026 14:42:51 +0700 Subject: [PATCH 268/274] dev email send when tokens not found --- .../ru/soune/nocopy/service/tariff/TariffInfoService.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java b/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java index d288b70..50fba93 100644 --- a/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java +++ b/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java @@ -2,6 +2,7 @@ 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; @@ -23,6 +24,7 @@ import java.util.stream.Collectors; @Service @AllArgsConstructor +@Slf4j public class TariffInfoService { private TariffInfoRepository tariffInfoRepository; @@ -45,7 +47,7 @@ public class TariffInfoService { return tariffInfoRepository.save(tariffInfo); } - @Transactional +// @Transactional public void writeOffTokens(long userId, Integer value) throws MessagingException, IOException { User user = userRepository.findById(userId).orElseThrow(); @@ -77,6 +79,7 @@ public class TariffInfoService { 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"); } From 3f375d3b126e0c7876d2bf29dea69b8c6544f9b6 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 31 Mar 2026 11:24:43 +0700 Subject: [PATCH 269/274] dev email send when tokens not found --- .../ru/soune/nocopy/service/search/GlobalSearchService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchService.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchService.java index 87a2230..d132923 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchService.java @@ -6,6 +6,7 @@ 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; @@ -61,6 +62,7 @@ public class GlobalSearchService { } else { List fileIds = request.getFileIds().isEmpty() ? fileEntityService.getAllUserFiles(userId) .stream() + .filter(f -> f.getMimeType().equals(FileType.IMAGE.name())) .map(FileEntity::getId) .toList(): request.getFileIds(); From dfb1fca8cdbb14c8d36b95effbc58bce811d42b4 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 31 Mar 2026 11:34:41 +0700 Subject: [PATCH 270/274] dev email send when tokens not found --- .../ru/soune/nocopy/service/search/GlobalSearchService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchService.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchService.java index d132923..e5c29a6 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchService.java @@ -62,7 +62,7 @@ public class GlobalSearchService { } else { List fileIds = request.getFileIds().isEmpty() ? fileEntityService.getAllUserFiles(userId) .stream() - .filter(f -> f.getMimeType().equals(FileType.IMAGE.name())) + .filter(f -> f.getMimeType().equals(FileType.IMAGE.getDisplayName())) .map(FileEntity::getId) .toList(): request.getFileIds(); From d775802a9951ec7939c103da85a1d98989a803d4 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 31 Mar 2026 11:35:19 +0700 Subject: [PATCH 271/274] dev email send when tokens not found --- src/main/resources/application.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 43f3b9a..668dda5 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -114,7 +114,7 @@ search: google: enabled: false proxy: - enabled: true + enabled: false host: "193.46.217.94" port: 3128 From 51170891cef2a3826e50d2fb78cb0a988bef90d4 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 31 Mar 2026 11:52:39 +0700 Subject: [PATCH 272/274] dev email send when tokens not found --- .../ru/soune/nocopy/service/file/FileEntityService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index bb70f14..757d693 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -78,11 +78,11 @@ public class FileEntityService { .map(User::getId) .toList()) { allFiles.addAll(fileEntityRepository.findByUserIdAndStatusIn(uId, - List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION))); + List.of(FileStatus.ACTIVE, FileStatus.BLOCKED))); } } else { allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId, - List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION)); + List.of(FileStatus.ACTIVE, FileStatus.BLOCKED)); } return allFiles; @@ -98,12 +98,12 @@ public class FileEntityService { .map(User::getId) .toList()) { allFiles.addAll(fileEntityRepository.findByUserIdAndStatusIn(uId, - List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION))); + List.of(FileStatus.ACTIVE, FileStatus.BLOCKED))); } } else { allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId, - List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION)); + List.of(FileStatus.ACTIVE, FileStatus.BLOCKED)); } int start = (page - 1) * pageSize; From 3d2c7ecb058c83ec54903822a0decb92745ab87f Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 31 Mar 2026 13:06:27 +0700 Subject: [PATCH 273/274] dev email send when tokens not found --- .../configuration/ApplicationConfig.java | 23 +++++++++++++++++-- .../soune/nocopy/entity/file/FileEntity.java | 3 +++ .../repository/ImageSimilarityRepository.java | 2 +- .../nocopy/service/FileSimilarityService.java | 8 +++++-- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/configuration/ApplicationConfig.java b/src/main/java/ru/soune/nocopy/configuration/ApplicationConfig.java index df4b7e1..92598e7 100644 --- a/src/main/java/ru/soune/nocopy/configuration/ApplicationConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/ApplicationConfig.java @@ -12,14 +12,22 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +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.Collections; +import java.util.List; @Configuration @EnableAutoConfiguration @AllArgsConstructor public class ApplicationConfig { + private final FileEntityRepository fileEntityRepository; + + private final FileUtil fileUtil; + @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); @@ -36,8 +44,11 @@ public class ApplicationConfig { AudioFilePathProvider audioFilePathProvider, DocumentLocalSearch documentLocalSearch) { + List initialFiles = loadProcessingFile(); + + //TODO инициализация файлов при создании return new com.vrt.NoCopyFileService( - Collections.emptyList(), + initialFiles, fileProvider, processingListener, imageUniqueCheck, @@ -47,5 +58,13 @@ public class ApplicationConfig { documentLocalSearch ); } + + private List loadProcessingFile() { + List byProtectionStatus = + fileEntityRepository.findByProtectionStatus(ProtectionStatus.PROCESSING); + + return byProtectionStatus.stream().map(fileEntity -> + fileUtil.createFileInfo(fileEntity, null)).toList(); + } } diff --git a/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java b/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java index 9182674..5872a3a 100644 --- a/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java +++ b/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java @@ -87,6 +87,9 @@ public class FileEntity { @Column(name = "medium_path") private String mediumPath; + @Column + private String newExtension; + @OneToOne(mappedBy = "file", cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnore private ImageHashEntity imageHash; diff --git a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java index 6c686f3..0a68a2a 100644 --- a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java @@ -31,7 +31,7 @@ public interface ImageSimilarityRepository JOIN file_entities f ON f.id = h.file_id WHERE ref.file_id = :fileId - AND f.status = 'ACTIVE' + AND f.status IN ('ACTIVE', 'MODERATION') """, nativeQuery = true) List findCandidates( diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 66f0167..bc7c66d 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -178,7 +178,8 @@ public class FileSimilarityService { imageHashEntity.getHash64Hi(), imageHashEntity.getHash64Lo(), userIds)) .filter(dto -> levels.contains(dto.getSimilarityLevel())) - .filter(dto -> dto.getFileStatus().equals(FileStatus.ACTIVE.name())) + .filter(dto -> !dto.getOwner() && + dto.getFileStatus().equals(FileStatus.MODERATION.name())) .sorted(Comparator.comparingInt(SimilarFileDTO::getHammingDistance)) .collect(Collectors.toList()); @@ -227,6 +228,9 @@ public class FileSimilarityService { level = "DIFFERENT"; } + boolean owner = userIds.contains(similarImageProjection.getUserId()); + + return SimilarFileDTO.builder() .fileId(similarImageProjection.getId()) .ownerId(similarImageProjection.getUserId()) @@ -239,7 +243,7 @@ public class FileSimilarityService { .supportId(similarImageProjection.getSupportId()) .uploadDate(similarImageProjection.getUploadDate()) .status(similarImageProjection.getProtectionStatus()) - .owner(userIds.contains(similarImageProjection.getUserId())) + .owner(owner) // .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId()) .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId() + "/thumbnail") .fileStatus(similarImageProjection.getFileStatus()) From ad4850414a39d393c35371505a3e30c859be0964 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 31 Mar 2026 13:31:26 +0700 Subject: [PATCH 274/274] dev email send when tokens not found --- .../java/ru/soune/nocopy/service/FileSimilarityService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index bc7c66d..cb7334c 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -178,8 +178,7 @@ public class FileSimilarityService { imageHashEntity.getHash64Hi(), imageHashEntity.getHash64Lo(), userIds)) .filter(dto -> levels.contains(dto.getSimilarityLevel())) - .filter(dto -> !dto.getOwner() && - dto.getFileStatus().equals(FileStatus.MODERATION.name())) + .filter(dto -> !(dto.getFileStatus().equals(FileStatus.MODERATION.name()) && !dto.getOwner())) .sorted(Comparator.comparingInt(SimilarFileDTO::getHammingDistance)) .collect(Collectors.toList());