diff --git a/README.md b/README.md index ad40cf0..fe72727 100644 --- a/README.md +++ b/README.md @@ -290,7 +290,7 @@ server_name dev-admin.not-copy.com; ssl_certificate_key /etc/letsencrypt/live/dev-admin.not-copy.com/privkey.pem; location /api/ { - proxy_pass http://localhost:3001; + proxy_pass http://localhost:8082; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/build.gradle b/build.gradle index d3ccf62..3bc912f 100644 --- a/build.gradle +++ b/build.gradle @@ -62,6 +62,7 @@ dependencies { implementation name: 'testlib-fat-0.3.1-all' implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.12.0' + implementation 'com.maxmind.geoip2:geoip2:4.2.0' implementation project(':referral') diff --git a/docker-compose.yaml b/docker-compose.yaml index ade6746..f9d857d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -216,6 +216,33 @@ services: networks: - app-network + nexus: + image: sonatype/nexus3:latest + container_name: nexus + restart: unless-stopped + ports: + - "9000:8081" + volumes: + - nexus-data:/nexus-data + environment: + - INSTALL4J_ADD_VM_PARAMS=-Xms512m -Xmx1024m -XX:MaxDirectMemorySize=512m + networks: + - app-network + + mongodb: + image: mongo:latest + container_name: mongo_ncp + restart: unless-stopped + ports: + - "27017:27017" + environment: + MONGO_INITDB_ROOT_USERNAME: mongo_db + MONGO_INITDB_ROOT_PASSWORD: mongo_ncp + volumes: + - mongodb_data:/data/db + networks: + - app-network + tempo: image: grafana/tempo:2.4.1 container_name: tempo @@ -267,6 +294,8 @@ services: volumes: pgdata: + mongodb_data: + name: mongodb_data artifacts: tempo_data: loki_data: @@ -275,6 +304,8 @@ volumes: loki_rules: uploads_data: prometheus_data: + nexus-data: + driver: local # postfix-data: networks: app-network: diff --git a/infrastructure/jenkins/deploy-backend.groovy b/infrastructure/jenkins/deploy-backend.groovy index 78e2105..efab9a0 100644 --- a/infrastructure/jenkins/deploy-backend.groovy +++ b/infrastructure/jenkins/deploy-backend.groovy @@ -86,10 +86,11 @@ pipeline { passwordVariable: 'SSH_PASS' ), string(credentialsId: 'DB_USER', variable: 'DB_USER'), - string(credentialsId: 'DB_PASSWORD', variable: 'DB_PASSWORD') + string(credentialsId: 'DB_PASSWORD', variable: 'DB_PASSWORD'), + string(credentialsId: 'MONGO_USER', variable: 'MONGO_USER'), + string(credentialsId: 'MONGO_PASSWORD', variable: 'MONGO_PASSWORD') ]) { sh """ - # Тестируем подключение сначала echo "Testing SSH connection..." if sshpass -p '${SSH_PASS}' ssh -o StrictHostKeyChecking=no ${SSH_USER}@${params.SERVER} 'echo "SSH connection successful"'; then echo "SSH connection OK" @@ -98,7 +99,6 @@ pipeline { exit 1 fi - # Основная команда 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} @@ -118,6 +118,8 @@ pipeline { docker rm postgres-${params.PORT} 2>/dev/null || echo 'Контейнер БД не найден' docker stop file-storage-${params.PORT} 2>/dev/null || echo 'Контейнер storage не найден' docker rm file-storage-${params.PORT} 2>/dev/null || echo 'Контейнер storage не найден' + docker stop mongodb-${params.PORT} 2>/dev/null || echo 'Контейнер MongoDB не найден' + docker rm mongodb-${params.PORT} 2>/dev/null || echo 'Контейнер MongoDB не найден' echo '3. Запускаем БД...' docker run -d \\ @@ -134,20 +136,31 @@ pipeline { -e BUILD_TIME_BACK='${buildTime}' \\ postgres:17.7 - echo '4. Запускаем storage...' + echo '4. Запускаем MongoDB...' + docker run -d \\ + --name mongodb-${params.PORT} \\ + --network ${params.NETWORK} \\ + --network-alias mongodb-${params.PORT} \\ + -p ${params.PORT}1:27017 \\ + -e MONGO_INITDB_ROOT_USERNAME=${MONGO_USER} \\ + -e MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD} \\ + -v mongodb_data_${params.PORT}:/data/db \\ + mongo:7.0 + + echo '5. Запускаем storage...' docker run -d \\ --name file-storage-${params.PORT} \\ --network ${params.NETWORK} \\ -v uploads_data_${params.PORT}:/storage:rw \\ alpine:latest tail -f /dev/null - echo '5. Ждем БД...' + echo '6. Ждем БД...' sleep 15 - echo '6. Собираем приложение...' + echo '7. Собираем приложение...' docker build --no-cache -t app-backend-${params.PORT}:latest . - echo '7. Запускаем приложение...' + echo '8. Запускаем приложение...' docker run -d \\ --name app-backend-${params.PORT} \\ --network ${params.NETWORK} \\ @@ -162,9 +175,11 @@ pipeline { -e POSTGRES_HOST=no_copy_${params.PORT} \\ -e FILE_STORAGE_PATH=/data/uploads \\ -e SPRING_PROFILES_ACTIVE=${springProfile} \\ + -e MONGO_URI=mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongodb-${params.PORT}:27017 \\ + -e MONGO_DATABASE=no_copy \\ app-backend-${params.PORT}:latest - echo '8. Проверка...' + echo '9. Проверка...' sleep 10 if curl -s -f http://localhost:${params.PORT}/health > /dev/null 2>&1; then diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index 926fa69..a845008 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -35,7 +35,19 @@ public class HandlerConfig { ViolationNotionHandler violationNotionHandler, ComplaintEntityHandler complaintEntityHandler, UserInfoHandler userInfoHandler, - NotificationHandler notificationHandler + NotificationHandler notificationHandler, + UserVerificationHandler userVerificationHandler, + LawCaseHandler lawCaseHandler, + TokenOperationHandler tokenOperationHandler, + StatisticUserFilesHandler statisticUserHandler, + StatisticTariffInfoFileHandler statisticTariffInfoFileHandler, + StatisticUserDynamicHandler statisticUserDynamicHandler, + StatisticProtectedFilesHandler statisticProtectedFilesHandler, + StatisticViolationHandler statisticViolationHandler, + StatisticSubscriberHandler statisticSubscriberHandler, + StatisticTokenHandler statisticTokenHandler, + StatisticIncomeHandler statisticIncomeHandler + ) { Map map = new HashMap<>(); map.put(20001, login); @@ -62,6 +74,17 @@ public class HandlerConfig { map.put(30013, complaintEntityHandler); map.put(30014, userInfoHandler); map.put(30015, notificationHandler); + map.put(30016, userVerificationHandler); + map.put(30017, lawCaseHandler); + map.put(30018, tokenOperationHandler); + map.put(30019, statisticUserHandler); + map.put(30020, statisticTariffInfoFileHandler); + map.put(30021, statisticUserDynamicHandler); + map.put(30022, statisticProtectedFilesHandler); + map.put(30023, statisticViolationHandler); + map.put(30024, statisticSubscriberHandler); + map.put(30025, statisticTokenHandler); + map.put(30026, statisticIncomeHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 07a03ec..2544f33 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -34,12 +34,12 @@ 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.*; import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.file.ProtectionsLimitService; import ru.soune.nocopy.service.file.cloud.CloudStorageService; import ru.soune.nocopy.service.register.AuthService; -import ru.soune.nocopy.service.file.FileEntityService; -import ru.soune.nocopy.service.file.FileUploadService; +import ru.soune.nocopy.service.user.moderation.UserVerificationService; import ru.soune.nocopy.util.FileUtil; import java.io.File; @@ -81,6 +81,10 @@ public class ApiController { private final CloudStorageService cloudStorageService; + private final ZipService zipService; + + private final UserVerificationService userVerificationService; + @PostMapping("/v{version}/data") public ResponseEntity handlePostRequest(@RequestBody BaseRequest request, @PathVariable("version") int version) { @@ -169,6 +173,56 @@ public class ApiController { } } + @PostMapping("/v{version}/private/files/chunk") + public ResponseEntity uploadChunk( + @RequestParam(value = "upload_id", required = false) String uploadId, + @RequestParam(value = "chunk_number", required = false) Integer chunkNumber, + @RequestParam(value = "chunk", required = false) MultipartFile chunk, + @RequestParam(value = "last_file", required = false) Boolean last_file, + @RequestParam(value = "token") String token) { + try { + if (chunk == null || chunk.isEmpty()) { + return buildErrorResponse(uploadId, chunkNumber, "Chunk file null or empty"); + } + if (uploadId == null || uploadId.isBlank()) { + return buildErrorResponse(uploadId, chunkNumber, "Upload Id is required"); + } + if (chunkNumber == null || chunkNumber < 0) { + return buildErrorResponse(uploadId, chunkNumber, "Valid chunk number is required"); + } + + Long userId = authService.useUserAuthToken(token); + + UploadProgressResponse uploadProgressResponse = fileUploadService.uploadPassportChunk(uploadId, chunkNumber, + chunk, userId); + + if (last_file) { + zipService.createAndSplitZip(userId, + fileEntityService.getAllUserFiles(userId, List.of(FileStatus.PRIVATE))); + } + + return buildSuccessResponse(uploadId, chunkNumber, chunk, + fileEntityService.findFileIdByPath(uploadProgressResponse.getFilePath())); + }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()); + } + } + + @GetMapping("/check/{fileId}") + public ResponseEntity check(@PathVariable String fileId) { + FileEntity fileEntity = fileEntityRepository.findByFileId(fileId); + + File file = new File(fileEntity.getFilePath()); + NoCopyCheckResult checkResult = noCopyFileService.checkFile(file, FileProtector.Type.IMAGE); + log.info("checkResult: {}", checkResult); + + + return ResponseEntity.ok(new BaseResponse(1, 1, "1", checkResult)); + } + @GetMapping("/v{version}/files/{fileId}/similar") public ResponseEntity findSimilarFiles( @PathVariable("version") int version, diff --git a/src/main/java/ru/soune/nocopy/controller/FileController.java b/src/main/java/ru/soune/nocopy/controller/FileController.java index 6beca5a..38cf43e 100644 --- a/src/main/java/ru/soune/nocopy/controller/FileController.java +++ b/src/main/java/ru/soune/nocopy/controller/FileController.java @@ -2,8 +2,8 @@ package ru.soune.nocopy.controller; import com.vrt.NoCopyFileService; import com.vrt.fileprotection.FileProtector; +import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.http.*; import org.springframework.web.bind.annotation.*; @@ -17,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.service.file.ZipService; import ru.soune.nocopy.util.FileUtil; import java.io.FileNotFoundException; @@ -31,29 +32,25 @@ import java.nio.file.Paths; @RestController @RequestMapping("/api/files") @Slf4j +@AllArgsConstructor public class FileController { - @Autowired private FileStorageService fileStorageService; - @Autowired private FileEntityRepository fileRepository; - @Autowired private CheckCounterService checkCounterService; - @Autowired private UserRepository userRepository; - @Autowired private ImageResizeService imageResizeService; - @Autowired private NoCopyFileService noCopyFileService; - @Autowired private FileUtil fileUtil; + private ZipService zipService; + @GetMapping("/public/{fileId}") public ResponseEntity getPublicFile(@PathVariable String fileId) { try { @@ -166,4 +163,21 @@ public class FileController { .ok() .body(fileInfo); } + + @GetMapping("/download/user-passport/archive/{userId}") + public ResponseEntity downloadPassportZip(@PathVariable Long userId) { + try { + byte[] zipBytes = zipService.assembleZipFromSplitFiles(userId); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=passport_" + userId + ".zip") + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .contentLength(zipBytes.length) + .body(zipBytes); + } catch (IOException e) { + log.error("Failed to assemble passport for user {}", userId, e); + return ResponseEntity.internalServerError().build(); + } + } } diff --git a/src/main/java/ru/soune/nocopy/controller/PaymentController.java b/src/main/java/ru/soune/nocopy/controller/PaymentController.java index 47407b5..8450452 100644 --- a/src/main/java/ru/soune/nocopy/controller/PaymentController.java +++ b/src/main/java/ru/soune/nocopy/controller/PaymentController.java @@ -70,7 +70,7 @@ public class PaymentController { } paymentService.changeAutoRenewal(authService.useUserAuthToken(tokenHeader), renewal); - return ResponseEntity.ok(Map.of("message", "Auto-renewal disabled")); + return ResponseEntity.ok(Map.of("message", "Auto-renewal changed:" + renewal)); } catch (UserNotFoundException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(Map.of("error", e.getMessage())); diff --git a/src/main/java/ru/soune/nocopy/controller/UserController.java b/src/main/java/ru/soune/nocopy/controller/UserController.java index e16d722..56674cc 100644 --- a/src/main/java/ru/soune/nocopy/controller/UserController.java +++ b/src/main/java/ru/soune/nocopy/controller/UserController.java @@ -59,7 +59,7 @@ public class UserController { .map(u -> new UserDTO(u.getFullName(), u.getCompanyName(), u.getEmail(), u.isActive(), u.getPhone(), u.getGenderType(), u.getBirthday(), u.getCreatedAt(), u.getSubscriptionType(), tariffService.getAllTariffs(), - null, null)) + null, null, u.getVerificationStatus())) .toList(); return ResponseEntity.ok(allUsers); @@ -79,6 +79,7 @@ public class UserController { UserDTO userDTO = userMapper.toDTO(user); userDTO.setEmail(email); userDTO.setFullName(user.getFullName()); + userDTO.setVerifiedStatus(user.getVerificationStatus()); if (user.getCompany() != null) { userDTO.setCompany(user.getCompany().getCompanyName()); diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index 34e903d..dae6f55 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -19,6 +19,7 @@ public enum MessageCode { FILE_DELETE(2, "File was deleted"), USER_NOT_HAD_PERMISSION(2, "User not have permission for file"), USER_NOT_VERIFIED(2, "User not verified"), + LAW_CASE_NOT_FOUND(4, "Law case not found"), PERMISSION_NOT_FOUND(2, "Permission not found"), USER_NOT_FOUND(2, "User not found"), FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD(2, "Not correct field"), @@ -26,8 +27,10 @@ public enum MessageCode { IMAGE_FOUND_ERROR(2, "Image found error"), INVALID_JSON_BODY(2, "Invalid fields in JSON object"), INCOMPLETE_UPLOAD(2, "Not load all chunks"), + USER_NOT_HAVE_TOKEN(2, "Not have tokens"), MSG_ID_NOT_FOUND(4, "Message id not found"), FILE_ENTITY_ERROR(2, "File entity error"), + NOT_VALID_FIELD(2, "Not valid field"), ACCESS_DENIED(2, "Access denied"), AUTH_EMAIL_NOT_FOUND(4, "Email not found"), AUTH_EMAIL_OR_TOKEN_NOT_FOUND(4, "Email or Token not found "), @@ -45,6 +48,7 @@ public enum MessageCode { TARIFF_IS_NOT_FOUND(0, "Tariff is not found"), VALIDATION_ERROR(2, "Validation error"), RESOURCE_NOT_FOUND(4, "Resource not found"), + OPERATION_NOT_FOUND(4, "Operation not found"), INTERNAL_ERROR(4, "Internal server error"), COMPANY_NOT_FOUND(4, "Company not found"), PAYMENT_NOT_FOUND(4, "Payment not found"), @@ -59,7 +63,9 @@ public enum MessageCode { 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"); + MESSAGE_IS_REQUIRED_FOR_NOTION(4, "Message is required for notion"), + USER_VERIFICATIONS_NOT_FOUND(4, "Verifications not found"), + ADMIN_USER_NOT_FOUND(4, "Verifications ,user or admin not found"); private final Integer code; diff --git a/src/main/java/ru/soune/nocopy/dto/complaint/BaseResponse.java b/src/main/java/ru/soune/nocopy/dto/complaint/BaseResponse.java new file mode 100644 index 0000000..9506a69 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/complaint/BaseResponse.java @@ -0,0 +1,35 @@ +package ru.soune.nocopy.dto.complaint; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class BaseResponse { + @JsonProperty("status") + private String status; + + @JsonProperty("message") + private String message; + + @JsonProperty("data") + private Object data; + + @JsonProperty("error_code") + private String errorCode; + + public static BaseResponse success(Object data) { + return BaseResponse.builder() + .status("SUCCESS") + .data(data) + .build(); + } + + public static BaseResponse error(String message, String errorCode) { + return BaseResponse.builder() + .status("ERROR") + .message(message) + .errorCode(errorCode) + .build(); + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/complaint/LawCasePageResponse.java b/src/main/java/ru/soune/nocopy/dto/complaint/LawCasePageResponse.java new file mode 100644 index 0000000..5b4e847 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/complaint/LawCasePageResponse.java @@ -0,0 +1,17 @@ +package ru.soune.nocopy.dto.complaint; + +import lombok.Builder; +import lombok.Data; +import java.util.List; + +@Data +@Builder +public class LawCasePageResponse { + private List content; + private int pageNumber; + private int pageSize; + private long totalElements; + private int totalPages; + private boolean last; + private boolean first; +} diff --git a/src/main/java/ru/soune/nocopy/dto/complaint/LawCaseRequest.java b/src/main/java/ru/soune/nocopy/dto/complaint/LawCaseRequest.java new file mode 100644 index 0000000..45db607 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/complaint/LawCaseRequest.java @@ -0,0 +1,33 @@ +package ru.soune.nocopy.dto.complaint; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import ru.soune.nocopy.entity.complaint.LawCasePriority; +import ru.soune.nocopy.entity.complaint.LawCaseType; + +import java.math.BigDecimal; + +@Data +public class LawCaseRequest { + private Long id; + //required + private String name; + //required + private String description; + private BigDecimal amount; + private String priority = LawCasePriority.HIGH.toString(); + private String type; + private String lawyer; + private String token; + private Integer pageSize = 5; + private Integer pageNumber = 0; + private String sortBy = "updatedAt"; + private String sortDir = "desc"; + private String action; + private LawCaseType filterType; + private String filterLawyer; + private LawCasePriority filterPriority; + @JsonProperty("violation_id") + private Long violationId; + +} diff --git a/src/main/java/ru/soune/nocopy/dto/complaint/LawCaseResponse.java b/src/main/java/ru/soune/nocopy/dto/complaint/LawCaseResponse.java new file mode 100644 index 0000000..af3f066 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/complaint/LawCaseResponse.java @@ -0,0 +1,46 @@ +package ru.soune.nocopy.dto.complaint; + +import lombok.Builder; +import lombok.Data; +import ru.soune.nocopy.entity.complaint.LawCase; +import ru.soune.nocopy.entity.complaint.LawCasePriority; +import ru.soune.nocopy.entity.complaint.LawCaseType; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Data +@Builder +public class LawCaseResponse { + private Long id; + private String name; + private String description; + private BigDecimal amount; + private LawCasePriority priority; + private LawCaseType type; + private String lawyer; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private int pageNumber; + private int pageSize; + private long totalElements; + private int totalPages; + private long violationId; + private List content; + + public static LawCaseResponse fromEntity(LawCase lawCase) { + return LawCaseResponse.builder() + .id(lawCase.getId()) + .name(lawCase.getName()) + .description(lawCase.getDescription()) + .amount(lawCase.getAmount()) + .priority(lawCase.getPriority()) + .type(lawCase.getType()) + .lawyer(lawCase.getLawyer()) + .createdAt(lawCase.getCreatedAt()) + .updatedAt(lawCase.getUpdatedAt()) + .violationId(lawCase.getViolationId()) + .build(); + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/complaint/PaginatedResponse.java b/src/main/java/ru/soune/nocopy/dto/complaint/PaginatedResponse.java new file mode 100644 index 0000000..1e3a6e6 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/complaint/PaginatedResponse.java @@ -0,0 +1,17 @@ +package ru.soune.nocopy.dto.complaint; + +import lombok.AllArgsConstructor; +import lombok.Data; +import java.util.List; + +@Data +@AllArgsConstructor +public class PaginatedResponse { + private List content; + private long totalElements; + private int totalPages; + private int pageNumber; + private int pageSize; + private boolean first; + private boolean last; +} diff --git a/src/main/java/ru/soune/nocopy/dto/statistic/IncomeStatisticRequest.java b/src/main/java/ru/soune/nocopy/dto/statistic/IncomeStatisticRequest.java new file mode 100644 index 0000000..1cbe6a9 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/statistic/IncomeStatisticRequest.java @@ -0,0 +1,6 @@ +package ru.soune.nocopy.dto.statistic; +import lombok.Data; + +@Data +public class IncomeStatisticRequest { +} diff --git a/src/main/java/ru/soune/nocopy/dto/statistic/IncomeStatisticResponse.java b/src/main/java/ru/soune/nocopy/dto/statistic/IncomeStatisticResponse.java new file mode 100644 index 0000000..12fea04 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/statistic/IncomeStatisticResponse.java @@ -0,0 +1,40 @@ +package ru.soune.nocopy.dto.statistic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class IncomeStatisticResponse { + + @JsonProperty("total_income") + private Long totalIncome; + + @JsonProperty("total_available") + private Long totalAvailable; + + @JsonProperty("total_hold") + private Long totalHold; + + @JsonProperty("income_per_user") + private Percentiles incomePerUser; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Percentiles { + @JsonProperty("p5") + private Long p5; + + @JsonProperty("p50") + private Long p50; + + @JsonProperty("p95") + private Long p95; + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/statistic/ProtectedFilesStatisticRequest.java b/src/main/java/ru/soune/nocopy/dto/statistic/ProtectedFilesStatisticRequest.java new file mode 100644 index 0000000..514a91f --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/statistic/ProtectedFilesStatisticRequest.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.dto.statistic; + +import lombok.Data; + +@Data +public class ProtectedFilesStatisticRequest { +} diff --git a/src/main/java/ru/soune/nocopy/dto/statistic/ProtectedFilesStatisticResponse.java b/src/main/java/ru/soune/nocopy/dto/statistic/ProtectedFilesStatisticResponse.java new file mode 100644 index 0000000..eed1b21 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/statistic/ProtectedFilesStatisticResponse.java @@ -0,0 +1,66 @@ +package ru.soune.nocopy.dto.statistic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ProtectedFilesStatisticResponse { + + @JsonProperty("total_files") + private Long totalFiles; + + @JsonProperty("total_size_bytes") + private Long totalSizeBytes; + + @JsonProperty("total_size_gb") + private Double totalSizeGb; + + @JsonProperty("files_per_user_percentiles") + private Percentiles filesPerUser; + + @JsonProperty("size_per_user_percentiles") + private SizePercentiles sizePerUser; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Percentiles { + @JsonProperty("p5") + private Long p5; + + @JsonProperty("p50") + private Long p50; + + @JsonProperty("p95") + private Long p95; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class SizePercentiles { + @JsonProperty("p5_bytes") + private Long p5; + + @JsonProperty("p50_bytes") + private Long p50; + + @JsonProperty("p95_bytes") + private Long p95; + + @JsonProperty("p5_mb") + private Double p5Mb; + + @JsonProperty("p50_mb") + private Double p50Mb; + + @JsonProperty("p95_mb") + private Double p95Mb; + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/statistic/SubscriberStatisticRequest.java b/src/main/java/ru/soune/nocopy/dto/statistic/SubscriberStatisticRequest.java new file mode 100644 index 0000000..b12cf52 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/statistic/SubscriberStatisticRequest.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.dto.statistic; + +import lombok.Data; + +@Data +public class SubscriberStatisticRequest { +} diff --git a/src/main/java/ru/soune/nocopy/dto/statistic/SubscriberStatisticResponse.java b/src/main/java/ru/soune/nocopy/dto/statistic/SubscriberStatisticResponse.java new file mode 100644 index 0000000..a37d30d --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/statistic/SubscriberStatisticResponse.java @@ -0,0 +1,42 @@ +package ru.soune.nocopy.dto.statistic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SubscriberStatisticResponse { + + @JsonProperty("monthly") + private PeriodStatistics monthly; + + @JsonProperty("yearly") + private PeriodStatistics yearly; + + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class PeriodStatistics { + + @JsonProperty("current_active_subscriptions") + private Long currentActiveSubscriptions; + + @JsonProperty("first_time_subscriptions") + private Long firstTimeSubscriptions; + + @JsonProperty("renewals") + private Long renewals; + + @JsonProperty("upgrades") + private Long upgrades; + + @JsonProperty("downgrades") + private Long downgrades; + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/statistic/TariffStatisticFullResponse.java b/src/main/java/ru/soune/nocopy/dto/statistic/TariffStatisticFullResponse.java new file mode 100644 index 0000000..48f5419 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/statistic/TariffStatisticFullResponse.java @@ -0,0 +1,17 @@ +package ru.soune.nocopy.dto.statistic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TariffStatisticFullResponse { + + @JsonProperty("tariffs") + private List tariffs; +} diff --git a/src/main/java/ru/soune/nocopy/dto/statistic/TariffStatisticRequest.java b/src/main/java/ru/soune/nocopy/dto/statistic/TariffStatisticRequest.java new file mode 100644 index 0000000..3b2d036 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/statistic/TariffStatisticRequest.java @@ -0,0 +1,14 @@ +package ru.soune.nocopy.dto.statistic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class TariffStatisticRequest { + + @JsonProperty("company_only") + private Boolean companyOnly = false; + + @JsonProperty("active_only") + private Boolean activeOnly = true; +} diff --git a/src/main/java/ru/soune/nocopy/dto/statistic/TariffStatisticResponse.java b/src/main/java/ru/soune/nocopy/dto/statistic/TariffStatisticResponse.java new file mode 100644 index 0000000..7b7ebe8 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/statistic/TariffStatisticResponse.java @@ -0,0 +1,21 @@ +package ru.soune.nocopy.dto.statistic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TariffStatisticResponse { + + @JsonProperty("tariff_name") + private String tariffName; + + @JsonProperty("user_count") + private Long userCount; + + @JsonProperty("usage_percent") + private Double usagePercent; +} diff --git a/src/main/java/ru/soune/nocopy/dto/statistic/TokenStatisticRequest.java b/src/main/java/ru/soune/nocopy/dto/statistic/TokenStatisticRequest.java new file mode 100644 index 0000000..3623860 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/statistic/TokenStatisticRequest.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.dto.statistic; + +import lombok.Data; + +@Data +public class TokenStatisticRequest { +} diff --git a/src/main/java/ru/soune/nocopy/dto/statistic/TokenStatisticResponse.java b/src/main/java/ru/soune/nocopy/dto/statistic/TokenStatisticResponse.java new file mode 100644 index 0000000..1a2474c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/statistic/TokenStatisticResponse.java @@ -0,0 +1,40 @@ +package ru.soune.nocopy.dto.statistic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TokenStatisticResponse { + + @JsonProperty("total_bought") + private Long totalBought; + + @JsonProperty("total_spent") + private Long totalSpent; + + @JsonProperty("bought_per_user") + private Percentiles boughtPerUser; + + @JsonProperty("spent_per_user") + private Percentiles spentPerUser; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Percentiles { + @JsonProperty("p5") + private Long p5; + + @JsonProperty("p50") + private Long p50; + + @JsonProperty("p95") + private Long p95; + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/statistic/UserDynamicStatisticRequest.java b/src/main/java/ru/soune/nocopy/dto/statistic/UserDynamicStatisticRequest.java new file mode 100644 index 0000000..110bc36 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/statistic/UserDynamicStatisticRequest.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.dto.statistic; + +import lombok.Data; + +@Data +public class UserDynamicStatisticRequest { +} diff --git a/src/main/java/ru/soune/nocopy/dto/statistic/UserDynamicStatisticResponse.java b/src/main/java/ru/soune/nocopy/dto/statistic/UserDynamicStatisticResponse.java new file mode 100644 index 0000000..43cef64 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/statistic/UserDynamicStatisticResponse.java @@ -0,0 +1,33 @@ +package ru.soune.nocopy.dto.statistic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class UserDynamicStatisticResponse { + + @JsonProperty("total_users") + private Long totalUsers; + + @JsonProperty("new_last_30_days") + private Long newLast30Days; + + @JsonProperty("new_previous_30_days") + private Long newPrevious30Days; + + @JsonProperty("growth_percent") + private Double growthPercent; + + @JsonProperty("new_current_month") + private Long newCurrentMonth; + + @JsonProperty("new_previous_month") + private Long newPreviousMonth; + + @JsonProperty("month_growth_percent") + private Double monthGrowthPercent; +} diff --git a/src/main/java/ru/soune/nocopy/dto/statistic/UserFilesStatisticRequest.java b/src/main/java/ru/soune/nocopy/dto/statistic/UserFilesStatisticRequest.java new file mode 100644 index 0000000..ad2bc38 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/statistic/UserFilesStatisticRequest.java @@ -0,0 +1,13 @@ +package ru.soune.nocopy.dto.statistic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class UserFilesStatisticRequest { + @JsonProperty("top") + int topUsers; + + @JsonProperty("type") + String fileMimeType; +} diff --git a/src/main/java/ru/soune/nocopy/dto/statistic/UserFilesStatisticResponse.java b/src/main/java/ru/soune/nocopy/dto/statistic/UserFilesStatisticResponse.java new file mode 100644 index 0000000..82c2ec5 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/statistic/UserFilesStatisticResponse.java @@ -0,0 +1,19 @@ +package ru.soune.nocopy.dto.statistic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class UserFilesStatisticResponse { + @JsonProperty("full_name") + private final String userName; + + @JsonProperty("user_id") + private final long userId; + + @JsonProperty("count") + private final int countFiles; + + @JsonProperty("files_size") + private final long fileSize; +} diff --git a/src/main/java/ru/soune/nocopy/dto/statistic/ViolationStatisticRequest.java b/src/main/java/ru/soune/nocopy/dto/statistic/ViolationStatisticRequest.java new file mode 100644 index 0000000..9cbd1ab --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/statistic/ViolationStatisticRequest.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.dto.statistic; + +import lombok.Data; + +@Data +public class ViolationStatisticRequest { +} diff --git a/src/main/java/ru/soune/nocopy/dto/statistic/ViolationStatisticResponse.java b/src/main/java/ru/soune/nocopy/dto/statistic/ViolationStatisticResponse.java new file mode 100644 index 0000000..f0e7dce --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/statistic/ViolationStatisticResponse.java @@ -0,0 +1,32 @@ +package ru.soune.nocopy.dto.statistic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ViolationStatisticResponse { + + @JsonProperty("total_violations") + private Long totalViolations; + + @JsonProperty("total_complaints") + private Long totalComplaints; + + @JsonProperty("total_law_cases") + private Long totalLawCases; + + @JsonProperty("complaints_without_case") + private Long complaintsWithoutCase; + + @JsonProperty("violations_with_complaint") + private Long violationsWithComplaint; + + @JsonProperty("violations_without_complaint") + private Long violationsWithoutComplaint; +} diff --git a/src/main/java/ru/soune/nocopy/dto/tokenoperation/TokenOperationPageDto.java b/src/main/java/ru/soune/nocopy/dto/tokenoperation/TokenOperationPageDto.java new file mode 100644 index 0000000..1455393 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/tokenoperation/TokenOperationPageDto.java @@ -0,0 +1,34 @@ +package ru.soune.nocopy.dto.tokenoperation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import ru.soune.nocopy.entity.tokenoperation.TokenOperation; + +import java.util.List; + +@Data +@Builder +public class TokenOperationPageDto { + + @JsonProperty("content") + private List content; + + @JsonProperty("page_number") + private int pageNumber; + + @JsonProperty("page_size") + private int pageSize; + + @JsonProperty("total_elements") + private long totalElements; + + @JsonProperty("total_pages") + private int totalPages; + + @JsonProperty("is_last") + private boolean last; + + @JsonProperty("is_first") + private boolean first; +} diff --git a/src/main/java/ru/soune/nocopy/dto/tokenoperation/TokenOperationRequest.java b/src/main/java/ru/soune/nocopy/dto/tokenoperation/TokenOperationRequest.java new file mode 100644 index 0000000..1d3d993 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/tokenoperation/TokenOperationRequest.java @@ -0,0 +1,46 @@ +package ru.soune.nocopy.dto.tokenoperation; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import ru.soune.nocopy.entity.tokenoperation.OperationType; + +@Data +public class TokenOperationRequest { + + @JsonProperty("action") + private String action; + + @JsonProperty("token") + private String token; + + @JsonProperty("operation_id") + private Long operationId; + + @JsonProperty("operation_type") + private OperationType operationType; + + @JsonProperty("spent") + private Long spent; + + @JsonProperty("page") + private Integer page = 0; + + @JsonProperty("size") + private Integer size = 20; + + @JsonProperty("sort_by") + private String sortBy = "createdAt"; + + @JsonProperty("sort_direction") + private String sortDirection = "desc"; + + @JsonProperty("min_spent") + private Long minSpent; + + @JsonProperty("max_spent") + private Long maxSpent; + + @JsonProperty("updates") + private Object updates; +} diff --git a/src/main/java/ru/soune/nocopy/dto/tokenoperation/TokenOperationResponse.java b/src/main/java/ru/soune/nocopy/dto/tokenoperation/TokenOperationResponse.java new file mode 100644 index 0000000..75a02d8 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/tokenoperation/TokenOperationResponse.java @@ -0,0 +1,37 @@ +package ru.soune.nocopy.dto.tokenoperation; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; + +import java.util.Map; + +@Data +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TokenOperationResponse { + + @JsonProperty("success") + private Boolean success; + + @JsonProperty("message") + private String message; + + @JsonProperty("data") + private Object data; + + @JsonProperty("total_elements") + private Long totalElements; + + @JsonProperty("total_pages") + private Integer totalPages; + + @JsonProperty("current_page") + private Integer currentPage; + + @JsonProperty("page_size") + private Integer pageSize; + + @JsonProperty("statistics") + private Map statistics; +} diff --git a/src/main/java/ru/soune/nocopy/dto/user/UserDTO.java b/src/main/java/ru/soune/nocopy/dto/user/UserDTO.java index 58a0824..fd07cfc 100644 --- a/src/main/java/ru/soune/nocopy/dto/user/UserDTO.java +++ b/src/main/java/ru/soune/nocopy/dto/user/UserDTO.java @@ -7,6 +7,7 @@ import lombok.NoArgsConstructor; import ru.soune.nocopy.dto.tarriff.TariffDTO; import ru.soune.nocopy.dto.tarriff.TariffInfoDTO; import ru.soune.nocopy.entity.user.GenderType; +import ru.soune.nocopy.entity.user.ModerationStatus; import ru.soune.nocopy.entity.user.SubscriptionType; import java.time.LocalDate; @@ -30,4 +31,5 @@ public class UserDTO { private List tariffs; private TariffInfoDTO tariffInfo; private Long permission; + private ModerationStatus verifiedStatus; } diff --git a/src/main/java/ru/soune/nocopy/dto/user/moderation/UserVerificationRequest.java b/src/main/java/ru/soune/nocopy/dto/user/moderation/UserVerificationRequest.java new file mode 100644 index 0000000..8749f54 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/user/moderation/UserVerificationRequest.java @@ -0,0 +1,22 @@ +package ru.soune.nocopy.dto.user.moderation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class UserVerificationRequest { + @JsonProperty("message") + private String message; + + @JsonProperty("user_id") + private Long userId; + + @JsonProperty("verified") + private Boolean verified; + + @JsonProperty("admin_id") + private Long adminId; + + @JsonProperty("action") + private String action; +} diff --git a/src/main/java/ru/soune/nocopy/dto/user/moderation/UserVerificationResponse.java b/src/main/java/ru/soune/nocopy/dto/user/moderation/UserVerificationResponse.java new file mode 100644 index 0000000..7247038 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/user/moderation/UserVerificationResponse.java @@ -0,0 +1,15 @@ +package ru.soune.nocopy.dto.user.moderation; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@Builder +public class UserVerificationResponse { + private Long userVerificationId; + private Long userId; + private LocalDateTime updateTime; + private String moderationStatus; +} diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationResponse.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationResponse.java index 94f1321..8a588c6 100644 --- a/src/main/java/ru/soune/nocopy/dto/violation/ViolationResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationResponse.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; import lombok.Data; import ru.soune.nocopy.entity.violation.Violation; +import ru.soune.nocopy.service.geo.GeoCountryService; import java.time.LocalDateTime; import java.util.List; @@ -12,6 +13,8 @@ import java.util.List; @Builder public class ViolationResponse { + private final GeoCountryService geoCountryService; + @JsonProperty("violations") private List violations; @@ -58,6 +61,12 @@ public class ViolationResponse { @JsonProperty("created_date") private LocalDateTime createdDate; + @JsonProperty("country") + private String country; + + @JsonProperty("country_code") + private String countryCode; + @JsonProperty("file_id") private String fileId; 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 acb19e9..37d4e3a 100644 --- a/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java +++ b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java @@ -39,12 +39,6 @@ public class ComplaintEntity { @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; @@ -55,4 +49,12 @@ public class ComplaintEntity { @Column(name = "not_moderated_file") private Boolean not_moderated_file; + + @PrePersist + public void prePersist() { + if (this.createdAt == null) { + this.createdAt = LocalDateTime.now(); + } + this.updatedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/ru/soune/nocopy/entity/complaint/LawCase.java b/src/main/java/ru/soune/nocopy/entity/complaint/LawCase.java index b9ca6e6..d4e73f8 100644 --- a/src/main/java/ru/soune/nocopy/entity/complaint/LawCase.java +++ b/src/main/java/ru/soune/nocopy/entity/complaint/LawCase.java @@ -2,6 +2,12 @@ package ru.soune.nocopy.entity.complaint; import jakarta.persistence.*; import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import ru.soune.nocopy.entity.user.User; + +import java.math.BigDecimal; +import java.time.LocalDateTime; @Entity @AllArgsConstructor @@ -14,4 +20,48 @@ public class LawCase { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @ToString.Exclude + private User user; + + @Column(name = "name") + private String name; + + @Column(length = 500, name = "description") + private String description; + + @Column(name = "amount") + private BigDecimal amount; + + @Column(name = "priority") + @Enumerated(EnumType.STRING) + private LawCasePriority priority; + + @Column(name = "type") + @Enumerated(EnumType.STRING) + private LawCaseType type = LawCaseType.ACTIVE; + + @CreatedDate + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @Column(name = "lawyer") + private String lawyer; + + @Column(name = "violation_id") + private Long violationId; + + @PrePersist + public void prePersist() { + if (this.createdAt == null) { + this.createdAt = LocalDateTime.now(); + } + this.updatedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/ru/soune/nocopy/entity/complaint/LawCasePriority.java b/src/main/java/ru/soune/nocopy/entity/complaint/LawCasePriority.java new file mode 100644 index 0000000..2b0bd3c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/complaint/LawCasePriority.java @@ -0,0 +1,12 @@ +package ru.soune.nocopy.entity.complaint; + +public enum LawCasePriority { + CRITICAL("critical"), HIGH("high"), + MIDDLE("middle"), LOW("low"); + + private String name; + + private LawCasePriority(String name) { + this.name = name; + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/complaint/LawCaseType.java b/src/main/java/ru/soune/nocopy/entity/complaint/LawCaseType.java new file mode 100644 index 0000000..12a42a9 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/complaint/LawCaseType.java @@ -0,0 +1,15 @@ +package ru.soune.nocopy.entity.complaint; + +import lombok.Getter; + +@Getter +public enum LawCaseType { + ACTIVE("active"), CLOSED("closed"), + COMPLETED("completed"); + + private String name; + + private LawCaseType(String name) { + this.name = name; + } +} 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 5872a3a..6a0137f 100644 --- a/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java +++ b/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java @@ -6,8 +6,10 @@ 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.monitoring.FileMonitoringEntity; import java.time.LocalDateTime; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @Data 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 a23ead9..48d5611 100644 --- a/src/main/java/ru/soune/nocopy/entity/file/FileStatus.java +++ b/src/main/java/ru/soune/nocopy/entity/file/FileStatus.java @@ -10,5 +10,6 @@ public enum FileStatus { TEMP, REMOVED, MODERATION, + PRIVATE, BLOCKED } diff --git a/src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationPassportFile.java b/src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationPassportFile.java new file mode 100644 index 0000000..5556410 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationPassportFile.java @@ -0,0 +1,28 @@ +package ru.soune.nocopy.entity.file.moderation; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Builder +@Table(name = "moderation_file") +@Getter @Setter +@AllArgsConstructor +@NoArgsConstructor +public class ModerationPassportFile { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "path", nullable = false) + private String path; + + @Column(name = "part") + private Integer part; + + private String status; +} diff --git a/src/main/java/ru/soune/nocopy/entity/file/moderation/PassportModerationFileStatus.java b/src/main/java/ru/soune/nocopy/entity/file/moderation/PassportModerationFileStatus.java new file mode 100644 index 0000000..8cc8beb --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/file/moderation/PassportModerationFileStatus.java @@ -0,0 +1,17 @@ +package ru.soune.nocopy.entity.file.moderation; + +import lombok.Getter; + +@Getter +public enum PassportModerationFileStatus { + + DOWNLOAD("download"), + ON_MODERATION("on_moderation"), + MODERATED("moderated"); + + private final String name; + + PassportModerationFileStatus(String name) { + this.name = name; + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/notification/Notification.java b/src/main/java/ru/soune/nocopy/entity/notification/Notification.java index 31035cb..87b772b 100644 --- a/src/main/java/ru/soune/nocopy/entity/notification/Notification.java +++ b/src/main/java/ru/soune/nocopy/entity/notification/Notification.java @@ -26,7 +26,7 @@ public class Notification { @Column(name = "created_at", updatable = false) @CreatedDate - private LocalDateTime createdAt; + private LocalDateTime createdAt = LocalDateTime.now(); @LastModifiedDate @Column(name = "updated_at") @@ -46,4 +46,13 @@ public class Notification { @JoinColumn(name = "user_id") @JsonIgnore private User user; + + @PrePersist + public void prePersist() { + if (this.createdAt == null) { + this.createdAt = LocalDateTime.now(); + } + + this.updatedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/ru/soune/nocopy/entity/notification/NotificationMessage.java b/src/main/java/ru/soune/nocopy/entity/notification/NotificationMessage.java index 0085299..2fb71ba 100644 --- a/src/main/java/ru/soune/nocopy/entity/notification/NotificationMessage.java +++ b/src/main/java/ru/soune/nocopy/entity/notification/NotificationMessage.java @@ -4,7 +4,7 @@ import lombok.Getter; @Getter public enum NotificationMessage { - FILE_SEARCH("notification.file.search"); + FILE_SEARCH("notification-file-search"); private final String messageKey; diff --git a/src/main/java/ru/soune/nocopy/entity/notification/NotificationType.java b/src/main/java/ru/soune/nocopy/entity/notification/NotificationType.java index 159fe35..bb38f20 100644 --- a/src/main/java/ru/soune/nocopy/entity/notification/NotificationType.java +++ b/src/main/java/ru/soune/nocopy/entity/notification/NotificationType.java @@ -4,24 +4,26 @@ import lombok.Getter; @Getter public enum NotificationType { - SEARCH_RESULT("notification.search.result"), - MONITORING_RESULT("notification.monitoring.result"), - OPERATION_IMPOSSIBLE("notification.operation.impossible"), - START_FAILED_NOW("notification.start.failed.now"), - START_FAILED_NEXT("notification.start.failed.next"), - PAYOUT_RESULT("notification.payout.result"), - PAYMENT_RESULT("notification.payment.result"), - REFERRAL_DEPOSIT("notification.referral.deposit"), - REFERRAL_REGISTERED("notification.referral.registered"), - REFERRAL_ACTIVATED("notification.referral.activated"), - COMPLAINT_STATUS_CHANGED("notification.complaint.status"), - CASE_STATUS_CHANGED("notification.case.status"), - INCOMING_MESSAGE("notification.incoming.message"), - TARIFF_EXPIRING("notification.tariff.expiring"), - TOKEN_NOT_FOUND("notification.token.not.found"), - FILE_MODERATION_EVENT("notification.file.moderation"), - FILE_ADDED_TO_SYSTEM("notification.file.added"), - SEARCH_OPERATION_FAILED("notification.search.operation.filed"); + SEARCH_RESULT("notification-search-result"), + MONITORING_RESULT("notification-monitoring-result"), + OPERATION_IMPOSSIBLE("notification-operation-impossible"), + START_FAILED_NOW("notification-start-failed-now"), + START_FAILED_NEXT("notification-start-failed-next"), + PAYOUT_RESULT("notification-payout-result"), + PAYMENT_RESULT("notification-payment-result"), + REFERRAL_DEPOSIT("notification-referral-deposit"), + REFERRAL_REGISTERED("notification-referral-registered"), + USER_VERIFIED("notification-user-verified"), + USER_NOT_VERIFIED("notification-user-not-verified"), + REFERRAL_ACTIVATED("notification-referral-activated"), + COMPLAINT_STATUS_CHANGED("notification-complaint-status"), + CASE_STATUS_CHANGED("notification-case-status"), + INCOMING_MESSAGE("notification-incoming-message"), + TARIFF_EXPIRING("notification-tariff-expiring"), + TOKEN_NOT_FOUND("notification-token-not-found"), + FILE_MODERATION_EVENT("notification-file-moderation"), + FILE_ADDED_TO_SYSTEM("notification-file-added"), + SEARCH_OPERATION_FAILED("notification-search-operation-filed"); private final String messageKey; 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 3646fbc..cb164eb 100644 --- a/src/main/java/ru/soune/nocopy/entity/payment/Payment.java +++ b/src/main/java/ru/soune/nocopy/entity/payment/Payment.java @@ -61,4 +61,13 @@ public class Payment { @JoinColumn(name = "tariff_id") @JsonIgnore private Tariff tariff; + + + @PrePersist + public void prePersist() { + if (this.createdAt == null) { + this.createdAt = LocalDateTime.now(); + } + this.updatedAt = LocalDateTime.now(); + } } \ No newline at end of file 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 9c71022..c83337f 100644 --- a/src/main/java/ru/soune/nocopy/entity/tarif/TariffInfo.java +++ b/src/main/java/ru/soune/nocopy/entity/tarif/TariffInfo.java @@ -20,6 +20,7 @@ public class TariffInfo { private String id; @Column(name = "status", nullable = false) + @Enumerated(EnumType.STRING) private TariffStatus status; @Column(name = "start_tariff") diff --git a/src/main/java/ru/soune/nocopy/entity/tokenoperation/OperationType.java b/src/main/java/ru/soune/nocopy/entity/tokenoperation/OperationType.java new file mode 100644 index 0000000..95d5104 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/tokenoperation/OperationType.java @@ -0,0 +1,12 @@ +package ru.soune.nocopy.entity.tokenoperation; + +public enum OperationType { + SEARCH("search"), MONITORING("monitoring"), CREATE_LEGAL_CASE("legal_case"), + FILE_UPLOAD("file_upload"), GLOBAL_SEARCH("global_search"); + + private final String name; + + private OperationType(String name) { + this.name = name; + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/tokenoperation/TokenOperation.java b/src/main/java/ru/soune/nocopy/entity/tokenoperation/TokenOperation.java new file mode 100644 index 0000000..f737157 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/tokenoperation/TokenOperation.java @@ -0,0 +1,32 @@ +package ru.soune.nocopy.entity.tokenoperation; + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Table(name = "token_operation") +@Entity +@Setter @Getter +@NoArgsConstructor +public class TokenOperation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(name = "operation_type") + private OperationType operationType; + + @Column(name = "user_id") + private Long userId; + + @Column(name = "spent") + private Long spent; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; +} diff --git a/src/main/java/ru/soune/nocopy/entity/user/ModerationStatus.java b/src/main/java/ru/soune/nocopy/entity/user/ModerationStatus.java new file mode 100644 index 0000000..be75cbb --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/user/ModerationStatus.java @@ -0,0 +1,15 @@ +package ru.soune.nocopy.entity.user; + +import lombok.Getter; + +@Getter +public enum ModerationStatus { + NOT_VERIFIED("not_verified") , VERIFICATION_IN_PROGRESS("verification_in_progress"), + VERIFIED("verified"), VERIFICATION_FAILED("verification_failed"); + + private final String value; + + private ModerationStatus(String value) { + this.value = value; + } +} 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 0c58638..0a7faad 100644 --- a/src/main/java/ru/soune/nocopy/entity/user/User.java +++ b/src/main/java/ru/soune/nocopy/entity/user/User.java @@ -106,6 +106,10 @@ public class User { @ToString.Exclude private UserNotActive userNotActive; + @Column(name = "verification_status") + @Enumerated(EnumType.STRING) + private ModerationStatus verificationStatus = ModerationStatus.NOT_VERIFIED; + public TariffInfo getActiveTariffInfo() { if (company != null && company.getTariffInfo() != null) { return company.getTariffInfo(); diff --git a/src/main/java/ru/soune/nocopy/entity/user/moderation/UserVerification.java b/src/main/java/ru/soune/nocopy/entity/user/moderation/UserVerification.java new file mode 100644 index 0000000..16d70b3 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/user/moderation/UserVerification.java @@ -0,0 +1,52 @@ +package ru.soune.nocopy.entity.user.moderation; + +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import ru.soune.nocopy.entity.user.ModerationStatus; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "user_verification") +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@Setter +public class UserVerification { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id", unique = true, nullable = false) + private Long userId; + + @CreatedDate + @Column(name = "created_at", updatable = false, nullable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @Column(name = "message") + private String message; + + @Column(name = "admin_id") + private Long adminId; + + @Enumerated(EnumType.STRING) + @Column(name = "status") + private ModerationStatus moderationStatus; + + @PrePersist + public void prePersist() { + if (this.createdAt == null) { + this.createdAt = LocalDateTime.now(); + } + this.updatedAt = LocalDateTime.now(); + } +} 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 0c361bc..06a397c 100644 --- a/src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java +++ b/src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java @@ -43,4 +43,12 @@ public class ViolationNotion { @Column(name = "message") private String message; + + @PrePersist + public void prePersist() { + if (this.createdAt == null) { + this.createdAt = LocalDateTime.now(); + } + this.updatedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/ru/soune/nocopy/handler/ComplaintEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/ComplaintEntityHandler.java index c0ebc7b..1ec55e8 100644 --- a/src/main/java/ru/soune/nocopy/handler/ComplaintEntityHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ComplaintEntityHandler.java @@ -77,10 +77,9 @@ public class ComplaintEntityHandler implements RequestHandler { private BaseResponse handleGetAll(Integer msgId, ComplaintRequest req) { Pageable pageable = PageRequest.of( req.getPage() != null ? req.getPage() : 0, - req.getSize() != null ? req.getSize() : 20, + req.getSize() != null ? req.getSize() : 5, Sort.by(Sort.Direction.fromString(req.getSortDirection() != null ? req.getSortDirection() : "desc"), - req.getSortBy() != null ? req.getSortBy() : "createdAt") - ); + req.getSortBy() != null ? req.getSortBy() : "updatedAt")); var page = complaintService.getAllComplaints(pageable); Map data = new HashMap<>(); diff --git a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java index 61d77a5..5c04a99 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java @@ -4,6 +4,9 @@ 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.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.file.*; @@ -225,9 +228,55 @@ public class FileEntityHandler implements RequestHandler { return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), MessageCode.SUCCESS.getDescription(), - FileEntityResponse.builder().status(newStatus).build()); + Map.of("file_status", newStatus, "file_id", fileId)); } +// 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 fileRequestSortBy = fileRequest.getSortBy(); +// String sortBy = !fileRequestSortBy.equals("") ? fileRequest.getSortBy(): "createdAt"; +// sortBy = sortBy.equals("fileName") ? "originalFileName" : sortBy; +// +// String sortOrder = fileRequest.getSortOrder(); +// Sort.Direction direction = (sortOrder != null && sortOrder.equalsIgnoreCase("desc")) +// ? Sort.Direction.DESC +// : Sort.Direction.ASC; +// Sort sort = Sort.by(direction, sortBy); +// +// Pageable pageable = PageRequest.of(page, pageSize, sort); +// FileResponse userFiles = fileEntityService.getUserFiles(userId, pageable, request.getVersion()); +// +// FileListResponse response = FileListResponse.builder() +// .files(userFiles.getFiles()) +// .totalCount(userFiles.getTotalCount()) +// .totalSize(userFiles.getTotalSize()) +// .formattedTotalSize(fileEntityService.formatFileSize(userFiles.getTotalSize())) +// .page(page) +// .pageSize(pageSize) +// .sortBy(sortBy) +// .sortOrder(sortOrder) +// .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()); @@ -292,6 +341,73 @@ 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; + * + * String sortBy = fileRequest.getSortBy() != null ? fileRequest.getSortBy() : "fileName"; + * SortOrder sortOrder = SortOrder.fromString(fileRequest.getSortOrder()); + * + * FileResponse allFiles = fileEntityService.getUserFiles(userId, 1, 1000, request.getVersion()); + * + * 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, filterType)) + * .filter(f -> matchesDateFilter(f, dateFilter)) + * .collect(Collectors.toList()); + * + * Comparator comparator = getComparator(sortBy, sortOrder); + * filteredFiles.sort(comparator); + * + * int start = (page - 1) * pageSize; + * int end = Math.min(start + pageSize, filteredFiles.size()); + * + * 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(paginatedFiles) + * .totalCount(filteredFiles.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 searching files", e); + * return new BaseResponse(request.getMsgId(), + * MessageCode.FILE_UPLOAD_ERROR.getCode(), + * "Failed to search files: " + e.getMessage(), null); + * } + * } + */ + private boolean matchesDateFilter(FileEntityResponse file, String dateFilter) { if (dateFilter == null || dateFilter.isEmpty()) { return true; @@ -423,6 +539,12 @@ public class FileEntityHandler implements RequestHandler { Comparator.nullsLast(Comparator.naturalOrder()) ); break; + case "monitoring": + comparator = Comparator.comparing( + FileEntityResponse::getMonitoring, + Comparator.nullsLast(Comparator.naturalOrder()) + ); + break; default: comparator = Comparator.comparing( diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 51bb0fa..741bf3e 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -11,6 +11,7 @@ import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.file.ImageSearchRequest; import ru.soune.nocopy.dto.file.YandexSearchResponse; import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.tokenoperation.OperationType; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.service.file.CheckCounterService; @@ -117,7 +118,8 @@ public class ImageFoundRequestHandler implements RequestHandler { log.info("Results only from Yandex: {} images", allUniqueImages.size()); } - tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); + tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH, + OperationType.SEARCH); checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); int page = imageSearchRequest.getPage() != null ? imageSearchRequest.getPage() : 1; diff --git a/src/main/java/ru/soune/nocopy/handler/LawCaseHandler.java b/src/main/java/ru/soune/nocopy/handler/LawCaseHandler.java new file mode 100644 index 0000000..e6e49c5 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/LawCaseHandler.java @@ -0,0 +1,200 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.mail.MessagingException; +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.MessageCode; +import ru.soune.nocopy.dto.complaint.LawCaseRequest; +import ru.soune.nocopy.dto.complaint.LawCaseResponse; +import ru.soune.nocopy.dto.complaint.PaginatedResponse; +import ru.soune.nocopy.entity.complaint.LawCase; +import ru.soune.nocopy.entity.complaint.LawCasePriority; +import ru.soune.nocopy.entity.complaint.LawCaseType; +import ru.soune.nocopy.entity.tokenoperation.OperationType; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.exception.NotValidFieldException; +import ru.soune.nocopy.exception.TariffNotFoundException; +import ru.soune.nocopy.exception.UserNotFoundException; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.service.complaint.LawCaseService; +import ru.soune.nocopy.service.register.AuthService; +import ru.soune.nocopy.service.tariff.TariffConstants; +import ru.soune.nocopy.service.tariff.TariffInfoService; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Component +@RequiredArgsConstructor +public class LawCaseHandler implements RequestHandler { + + private final LawCaseService lawCaseService; + + private final AuthService authService; + + private final ObjectMapper objectMapper; + + private final UserRepository userRepository; + + private final TariffInfoService tariffInfoService; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + + LawCaseRequest lawCaseRequest = objectMapper.convertValue(request.getMessageBody(), + LawCaseRequest.class); + Long userId = authService.useUserAuthToken(lawCaseRequest.getToken()); + String action = lawCaseRequest.getAction(); + + return switch (action) { + case "get_all" -> getAllUserLawCases(userId, lawCaseRequest, request); + case "get_byId" -> getLawCaseById(lawCaseRequest.getId(), request); + case "create" -> createLawCase(userId, lawCaseRequest, request); + case "update_priority" -> updatePriority(lawCaseRequest, request); + case "update_type" -> updateType(lawCaseRequest, request); + case "update_amount" -> updateAmount(lawCaseRequest, request); + case "delete" -> deleteLawCase(lawCaseRequest.getId(), request); + default -> new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_ACTION.getCode(), + MessageCode.INVALID_ACTION.getDescription(), + Map.of("actions", "get_all, get_byId, create, update_priority, update_type, " + + "update_amount, delete") + ); + }; + } + + private BaseResponse getAllUserLawCases(Long userId, LawCaseRequest lawCaseRequest, BaseRequest request) { + Page lawCases = lawCaseService.getAllUserLawCases( + userId, + lawCaseRequest.getPageSize(), + lawCaseRequest.getPageNumber(), + lawCaseRequest.getFilterType(), + lawCaseRequest.getFilterLawyer(), + lawCaseRequest.getFilterPriority(), + lawCaseRequest.getViolationId(), + lawCaseRequest.getSortBy(), + lawCaseRequest.getSortDir()); + + List responses = lawCases.getContent().stream() + .map(LawCaseResponse::fromEntity) + .collect(Collectors.toList()); + + PaginatedResponse paginatedResponse = new PaginatedResponse<>( + responses, + lawCases.getTotalElements(), + lawCases.getTotalPages(), + lawCases.getNumber(), + lawCases.getSize(), + lawCases.isFirst(), + lawCases.isLast()); + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), paginatedResponse); + } + + private BaseResponse getLawCaseById(Long id, BaseRequest request) { + LawCase lawCase = lawCaseService.getById(id); + + if (lawCase == null) { + return new BaseResponse(request.getMsgId(), MessageCode.LAW_CASE_NOT_FOUND.getCode(), + MessageCode.LAW_CASE_NOT_FOUND.getDescription(), Map.of("id", id)); + } + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), LawCaseResponse.fromEntity(lawCase)); + } + + private BaseResponse createLawCase(Long userId, LawCaseRequest lawCaseRequest, BaseRequest request) { + try { + tariffInfoService.writeOffTokens(userId, TariffConstants.LEGAL_COST, OperationType.CREATE_LEGAL_CASE); + } catch (TariffNotFoundException e) { + return new BaseResponse(request.getMsgId(), MessageCode.USER_NOT_HAVE_TOKEN.getCode(), + MessageCode.USER_NOT_HAVE_TOKEN.getDescription(), + Map.of("error", "Tariff not found", "message", e.getMessage())); + } catch (MessagingException | IOException e) { + return new BaseResponse(request.getMsgId(), MessageCode.INVALID_FIELD.getCode(), + MessageCode.INVALID_FIELD.getDescription(), Map.of("id", lawCaseRequest.getId())); + } + + validateRequiredParameters(lawCaseRequest, request); + + User user = userRepository.findById(userId).orElseThrow(() -> new UserNotFoundException("User not found")); + + LawCase lawCase = lawCaseService.addLawCase( + lawCaseRequest.getAmount(), + lawCaseRequest.getDescription(), + lawCaseRequest.getName(), + LawCasePriority.valueOf(lawCaseRequest.getPriority()), + lawCaseRequest.getViolationId(), user); + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), LawCaseResponse.fromEntity(lawCase)); + } + + private BaseResponse updatePriority(LawCaseRequest lawCaseRequest, BaseRequest request) { + LawCase lawCase = lawCaseService.changePriority( + lawCaseRequest.getId(), + LawCasePriority.valueOf(lawCaseRequest.getPriority())); + + if (lawCase == null) { + return new BaseResponse(request.getMsgId(), MessageCode.LAW_CASE_NOT_FOUND.getCode(), + MessageCode.LAW_CASE_NOT_FOUND.getDescription(), Map.of("id", lawCaseRequest.getId())); + } + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), LawCaseResponse.fromEntity(lawCase)); + } + + private BaseResponse updateType(LawCaseRequest lawCaseRequest, BaseRequest request) { + LawCase lawCase = lawCaseService.changeStatus( + lawCaseRequest.getId(), + LawCaseType.valueOf(lawCaseRequest.getType())); + + if (lawCase == null) { + return new BaseResponse(request.getMsgId(), MessageCode.LAW_CASE_NOT_FOUND.getCode(), + MessageCode.LAW_CASE_NOT_FOUND.getDescription(), Map.of("id", lawCaseRequest.getId())); + } + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), LawCaseResponse.fromEntity(lawCase)); + } + + private BaseResponse updateAmount(LawCaseRequest lawCaseRequest, BaseRequest request) { + LawCase lawCase = lawCaseService.changeDamage( + lawCaseRequest.getId(), + lawCaseRequest.getAmount()); + + if (lawCase == null) { + return new BaseResponse(request.getMsgId(), MessageCode.LAW_CASE_NOT_FOUND.getCode(), + MessageCode.LAW_CASE_NOT_FOUND.getDescription(), Map.of("id", lawCaseRequest.getId())); + } + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), LawCaseResponse.fromEntity(lawCase)); + } + + private BaseResponse deleteLawCase(Long id, BaseRequest request) { + lawCaseService.deleteById(id); + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), Map.of("id", id)); + } + + private void validateRequiredParameters(LawCaseRequest lawCaseRequest, BaseRequest request) { + if (lawCaseRequest.getName() == null) throw new NotValidFieldException("Name is required", + new BaseResponse(request.getMsgId(), MessageCode.NOT_VALID_FIELD.getCode(), + MessageCode.NOTION_NOT_FOUND.getDescription(), Map.of("fields", "name"))); + + if (lawCaseRequest.getDescription() == null) throw new NotValidFieldException("Description is required", + new BaseResponse(request.getMsgId(), MessageCode.NOT_VALID_FIELD.getCode(), + MessageCode.NOTION_NOT_FOUND.getDescription(), Map.of("fields", "description"))); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/NotificationHandler.java b/src/main/java/ru/soune/nocopy/handler/NotificationHandler.java index 6db1c55..82a148d 100644 --- a/src/main/java/ru/soune/nocopy/handler/NotificationHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/NotificationHandler.java @@ -122,10 +122,27 @@ public class NotificationHandler implements RequestHandler { response); } + case "delete": { + MarkAsReadRequest markRequest = objectMapper.convertValue(request.getMessageBody(), + MarkAsReadRequest.class); + + int delete = notificationService.delete(markRequest.getNotificationIds()); + + MarkAsReadResponse response = MarkAsReadResponse.builder() + .success(true) + .updatedCount(delete) + .build(); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + response); + } + default: ActionResponse response = ActionResponse.builder() .action(action) - .availableActions(Arrays.asList("list", "active", "markRead")) + .availableActions(Arrays.asList("list", "active", "markRead", "delete")) .build(); return new BaseResponse(request.getMsgId(), diff --git a/src/main/java/ru/soune/nocopy/handler/StatisticIncomeHandler.java b/src/main/java/ru/soune/nocopy/handler/StatisticIncomeHandler.java new file mode 100644 index 0000000..13d0e5c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/StatisticIncomeHandler.java @@ -0,0 +1,30 @@ +package ru.soune.nocopy.handler; + +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.statistic.IncomeStatisticResponse; +import ru.soune.nocopy.service.statistic.IncomeStatisticService; + +@Component +@RequiredArgsConstructor +@Slf4j +public class StatisticIncomeHandler implements RequestHandler { + + private final IncomeStatisticService statisticService; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + IncomeStatisticResponse statistics = statisticService.getIncomeStatistics(); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + statistics + ); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/StatisticProtectedFilesHandler.java b/src/main/java/ru/soune/nocopy/handler/StatisticProtectedFilesHandler.java new file mode 100644 index 0000000..5a79c22 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/StatisticProtectedFilesHandler.java @@ -0,0 +1,28 @@ +package ru.soune.nocopy.handler; + +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.statistic.ProtectedFilesStatisticResponse; +import ru.soune.nocopy.service.statistic.ProtectedFilesStatisticService; + +@Component +@RequiredArgsConstructor +@Slf4j +public class StatisticProtectedFilesHandler implements RequestHandler { + private final ProtectedFilesStatisticService statisticService; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + ProtectedFilesStatisticResponse statistics = statisticService.getProtectedFilesStatistics(); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + statistics); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/StatisticSubscriberHandler.java b/src/main/java/ru/soune/nocopy/handler/StatisticSubscriberHandler.java new file mode 100644 index 0000000..9ec0a97 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/StatisticSubscriberHandler.java @@ -0,0 +1,29 @@ +package ru.soune.nocopy.handler; + +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.statistic.SubscriberStatisticResponse; +import ru.soune.nocopy.service.statistic.SubscriberStatisticService; + +@Component +@RequiredArgsConstructor +@Slf4j +public class StatisticSubscriberHandler implements RequestHandler { + private final SubscriberStatisticService statisticService; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + SubscriberStatisticResponse statistics = statisticService.getSubscriberStatistics(); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + statistics + ); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/StatisticTariffInfoFileHandler.java b/src/main/java/ru/soune/nocopy/handler/StatisticTariffInfoFileHandler.java new file mode 100644 index 0000000..66a5933 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/StatisticTariffInfoFileHandler.java @@ -0,0 +1,48 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +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.statistic.TariffStatisticFullResponse; +import ru.soune.nocopy.dto.statistic.TariffStatisticRequest; +import ru.soune.nocopy.dto.statistic.TariffStatisticResponse; +import ru.soune.nocopy.repository.TariffInfoRepository; + +import java.util.List; + +@Component +@AllArgsConstructor +public class StatisticTariffInfoFileHandler implements RequestHandler { + + private ObjectMapper objectMapper; + + private TariffInfoRepository tariffInfoRepository; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + TariffStatisticRequest tariffRequest = + objectMapper.convertValue(request.getMessageBody(), TariffStatisticRequest.class); + + boolean activeOnly = tariffRequest.getActiveOnly() != null ? tariffRequest.getActiveOnly() : true; + List results = tariffInfoRepository.getTariffStatisticsNative(activeOnly); + + List tariffs = results.stream() + .map(row -> new TariffStatisticResponse( + (String) row[0], + ((Number) row[1]).longValue(), + ((Number) row[2]).doubleValue() + )) + .toList(); + + TariffStatisticFullResponse statistics = new TariffStatisticFullResponse(tariffs); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + statistics); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/StatisticTokenHandler.java b/src/main/java/ru/soune/nocopy/handler/StatisticTokenHandler.java new file mode 100644 index 0000000..90232e3 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/StatisticTokenHandler.java @@ -0,0 +1,29 @@ +package ru.soune.nocopy.handler; + +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.statistic.TokenStatisticResponse; +import ru.soune.nocopy.service.statistic.TokenStatisticService; + +@Component +@RequiredArgsConstructor +@Slf4j +public class StatisticTokenHandler implements RequestHandler { + private final TokenStatisticService statisticService; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + TokenStatisticResponse statistics = statisticService.getTokenStatistics(); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + statistics + ); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/StatisticUserDynamicHandler.java b/src/main/java/ru/soune/nocopy/handler/StatisticUserDynamicHandler.java new file mode 100644 index 0000000..6665ef4 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/StatisticUserDynamicHandler.java @@ -0,0 +1,29 @@ +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.statistic.UserDynamicStatisticResponse; +import ru.soune.nocopy.service.statistic.UserDynamicStatisticService; + +@Component +@RequiredArgsConstructor +@Slf4j +public class StatisticUserDynamicHandler implements RequestHandler { + private final UserDynamicStatisticService statisticService; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + UserDynamicStatisticResponse statistics = statisticService.getUserDynamicStatistics(); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + statistics); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/StatisticUserFilesHandler.java b/src/main/java/ru/soune/nocopy/handler/StatisticUserFilesHandler.java new file mode 100644 index 0000000..6eca048 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/StatisticUserFilesHandler.java @@ -0,0 +1,38 @@ +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.statistic.UserFilesStatisticRequest; +import ru.soune.nocopy.dto.statistic.UserFilesStatisticResponse; +import ru.soune.nocopy.repository.FileEntityRepository; + +import java.util.List; + +@Component +@RequiredArgsConstructor +@Slf4j +public class StatisticUserFilesHandler implements RequestHandler { + + private final ObjectMapper objectMapper; + + private final FileEntityRepository fileRepository; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + UserFilesStatisticRequest userFilesStatisticRequest = + objectMapper.convertValue(request.getMessageBody(), UserFilesStatisticRequest.class); + + int topUsers = userFilesStatisticRequest.getTopUsers(); + String fileMimeType = userFilesStatisticRequest.getFileMimeType(); + + List files = fileRepository.findTopUsers(topUsers, fileMimeType); + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), files); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/StatisticViolationHandler.java b/src/main/java/ru/soune/nocopy/handler/StatisticViolationHandler.java new file mode 100644 index 0000000..194fe61 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/StatisticViolationHandler.java @@ -0,0 +1,34 @@ +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.statistic.ViolationStatisticResponse; +import ru.soune.nocopy.service.statistic.ViolationStatisticService; + +@Component +@RequiredArgsConstructor +@Slf4j +public class StatisticViolationHandler implements RequestHandler { + + private final ObjectMapper objectMapper; + private final ViolationStatisticService statisticService; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + log.debug("Handling violation statistic request, msgId: {}", request.getMsgId()); + + ViolationStatisticResponse statistics = statisticService.getViolationStatistics(); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + statistics + ); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/TokenOperationHandler.java b/src/main/java/ru/soune/nocopy/handler/TokenOperationHandler.java new file mode 100644 index 0000000..f80e093 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/TokenOperationHandler.java @@ -0,0 +1,472 @@ +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.MessageCode; +import ru.soune.nocopy.dto.tokenoperation.TokenOperationPageDto; +import ru.soune.nocopy.dto.tokenoperation.TokenOperationRequest; +import ru.soune.nocopy.entity.tokenoperation.OperationType; +import ru.soune.nocopy.entity.tokenoperation.TokenOperation; +import ru.soune.nocopy.service.register.AuthService; +import ru.soune.nocopy.service.tokenoperation.TokenOperationService; + +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class TokenOperationHandler implements RequestHandler { + private final TokenOperationService tokenOperationService; + + private final ObjectMapper objectMapper; + + private final AuthService authService; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + log.info("Handling TokenOperation request: msg_id={}, version={}", + request.getMsgId(), request.getVersion()); + + try { + TokenOperationRequest tokenRequest = objectMapper.convertValue( + request.getMessageBody(), + TokenOperationRequest.class + ); + + String action = tokenRequest.getAction(); + if (action == null) { + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_ACTION.getCode(), + MessageCode.INVALID_ACTION.getDescription(), + Map.of("error", "Action is required") + ); + } + + switch (action.toLowerCase()) { + case "get_list": + return handleGetList(request.getMsgId(), tokenRequest); + case "get_by_id": + return handleGetById(request.getMsgId(), tokenRequest); + case "get_by_user": + return handleGetByUser(request.getMsgId(), tokenRequest); + case "create": + return handleCreate(request.getMsgId(), tokenRequest); + case "update": + return handleUpdate(request.getMsgId(), tokenRequest); + case "delete": + return handleDelete(request.getMsgId(), tokenRequest); + case "delete_all_by_user": + return handleDeleteAllByUser(request.getMsgId(), tokenRequest); + case "get_stats": + return handleGetStats(request.getMsgId(), tokenRequest); + case "filter": + return handleFilter(request.getMsgId(), tokenRequest); + default: + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_ACTION.getCode(), + MessageCode.INVALID_ACTION.getDescription(), + Map.of("available_actions", + "get_list, get_by_id, get_by_user, create, update, " + + "delete, delete_all_by_user, get_stats, filter") + ); + } + + } catch (Exception e) { + log.error("Error handling TokenOperation request", e); + return new BaseResponse( + request.getMsgId(), + MessageCode.INTERNAL_ERROR.getCode(), + MessageCode.INTERNAL_ERROR.getDescription(), + Map.of("error", e.getMessage()) + ); + } + } + + private BaseResponse handleGetList(Integer msgId, TokenOperationRequest request) { + if (request.getToken() == null) { + return new BaseResponse( + msgId, + MessageCode.USER_NOT_FOUND.getCode(), + MessageCode.USER_NOT_FOUND.getDescription(), + Map.of("error", "User ID is required") + ); + } + + Long userId = authService.useUserAuthToken(request.getToken()); + + try { + Page page = tokenOperationService.findByUserId( + userId, + request.getPage() != null ? request.getPage() : 0, + request.getSize() != null ? request.getSize() : 20 + ); + + TokenOperationPageDto pageDto = TokenOperationPageDto.builder() + .content(page.getContent()) + .pageNumber(page.getNumber()) + .pageSize(page.getSize()) + .totalElements(page.getTotalElements()) + .totalPages(page.getTotalPages()) + .last(page.isLast()) + .first(page.isFirst()) + .build(); + + Map responseData = new HashMap<>(); + responseData.put("operations", pageDto); + responseData.put("total_elements", page.getTotalElements()); + responseData.put("total_pages", page.getTotalPages()); + responseData.put("current_page", page.getNumber()); + responseData.put("page_size", page.getSize()); + + return new BaseResponse( + msgId, + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + responseData + ); + + } catch (Exception e) { + log.error("Error getting token operations list", e); + return new BaseResponse( + msgId, + MessageCode.INTERNAL_ERROR.getCode(), + MessageCode.INTERNAL_ERROR.getDescription(), + Map.of("error", "Error getting operations: " + e.getMessage()) + ); + } + } + + private BaseResponse handleGetById(Integer msgId, TokenOperationRequest request) { + log.info("Getting token operation by id: {}", request.getOperationId()); + + if (request.getOperationId() == null) { + return new BaseResponse( + msgId, + MessageCode.OPERATION_NOT_FOUND.getCode(), + MessageCode.OPERATION_NOT_FOUND.getDescription(), + Map.of("error", "Operation ID is required") + ); + } + + try { + TokenOperation operation = tokenOperationService.findById(request.getOperationId()); + + return new BaseResponse( + msgId, + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + operation + ); + + } catch (jakarta.persistence.EntityNotFoundException e) { + return new BaseResponse( + msgId, + MessageCode.NOT_FOUND.getCode(), + MessageCode.NOT_FOUND.getDescription(), + Map.of("error", "Operation not found with id: " + request.getOperationId()) + ); + } catch (Exception e) { + log.error("Error getting token operation by id", e); + return new BaseResponse( + msgId, + MessageCode.INTERNAL_ERROR.getCode(), + MessageCode.INTERNAL_ERROR.getDescription(), + Map.of("error", e.getMessage()) + ); + } + } + + private BaseResponse handleGetByUser(Integer msgId, TokenOperationRequest request) { + return handleGetList(msgId, request); + } + + private BaseResponse handleCreate(Integer msgId, TokenOperationRequest request) { + if (request.getToken() == null) { + return new BaseResponse( + msgId, + MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), + MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), + Map.of("error", "User ID is required") + ); + } + + Long userId = authService.useUserAuthToken(request.getToken()); + + try { + TokenOperation operation = tokenOperationService.create( + request.getOperationType(), + userId, + request.getSpent() + ); + + return new BaseResponse( + msgId, + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + operation + ); + + } catch (Exception e) { + log.error("Error creating token operation", e); + return new BaseResponse( + msgId, + MessageCode.INTERNAL_ERROR.getCode(), + MessageCode.INTERNAL_ERROR.getDescription(), + Map.of("error", "Error creating operation: " + e.getMessage()) + ); + } + } + + private BaseResponse handleUpdate(Integer msgId, TokenOperationRequest request) { + log.info("Updating token operation: {}", request.getOperationId()); + + if (request.getOperationId() == null) { + return new BaseResponse( + msgId, + MessageCode.OPERATION_NOT_FOUND.getCode(), + MessageCode.OPERATION_NOT_FOUND.getDescription(), + Map.of("error", "Operation ID is required") + ); + } + + try { + TokenOperation existing = tokenOperationService.findById(request.getOperationId()); + + if (request.getOperationType() != null) { + existing.setOperationType(request.getOperationType()); + } + if (request.getSpent() != null) { + existing.setSpent(request.getSpent()); + } + + TokenOperation updated = tokenOperationService.update(request.getOperationId(), existing); + + return new BaseResponse( + msgId, + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + updated + ); + + } catch (jakarta.persistence.EntityNotFoundException e) { + return new BaseResponse( + msgId, + MessageCode.NOT_FOUND.getCode(), + MessageCode.NOT_FOUND.getDescription(), + Map.of("error", "Operation not found with id: " + request.getOperationId()) + ); + } catch (Exception e) { + log.error("Error updating token operation", e); + return new BaseResponse( + msgId, + MessageCode.INTERNAL_ERROR.getCode(), + MessageCode.INTERNAL_ERROR.getDescription(), + Map.of("error", "Error updating operation: " + e.getMessage()) + ); + } + } + + private BaseResponse handleDelete(Integer msgId, TokenOperationRequest request) { + log.info("Deleting token operation: {}", request.getOperationId()); + + if (request.getOperationId() == null) { + return new BaseResponse( + msgId, + MessageCode.OPERATION_NOT_FOUND.getCode(), + MessageCode.OPERATION_NOT_FOUND.getDescription(), + Map.of("error", "Operation ID is required") + ); + } + + try { + tokenOperationService.deleteById(request.getOperationId()); + + return new BaseResponse( + msgId, + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + Map.of("message", "Operation deleted successfully", + "operation_id", request.getOperationId()) + ); + + } catch (jakarta.persistence.EntityNotFoundException e) { + return new BaseResponse( + msgId, + MessageCode.NOT_FOUND.getCode(), + MessageCode.NOT_FOUND.getDescription(), + Map.of("error", "Operation not found with id: " + request.getOperationId()) + ); + } catch (Exception e) { + log.error("Error deleting token operation", e); + return new BaseResponse( + msgId, + MessageCode.INTERNAL_ERROR.getCode(), + MessageCode.INTERNAL_ERROR.getDescription(), + Map.of("error", "Error deleting operation: " + e.getMessage()) + ); + } + } + + private BaseResponse handleDeleteAllByUser(Integer msgId, TokenOperationRequest request) { + if (request.getToken() == null) { + return new BaseResponse( + msgId, + MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), + MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), + Map.of("error", "User ID is required") + ); + } + + Long userId = authService.useUserAuthToken(request.getToken()); + + try { + tokenOperationService.deleteByUserId(userId); + + return new BaseResponse( + msgId, + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + Map.of("message", "All operations deleted successfully for user", + "user_id", userId) + ); + + } catch (Exception e) { + log.error("Error deleting all operations for user", e); + return new BaseResponse( + msgId, + MessageCode.INTERNAL_ERROR.getCode(), + MessageCode.INTERNAL_ERROR.getDescription(), + Map.of("error", "Error deleting operations: " + e.getMessage()) + ); + } + } + + private BaseResponse handleGetStats(Integer msgId, TokenOperationRequest request) { + if (request.getToken() == null) { + return new BaseResponse( + msgId, + MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), + MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), + Map.of("error", "User ID is required") + ); + } + + Long userId = authService.useUserAuthToken(request.getToken()); + + if (userId == null) { + return new BaseResponse( + msgId, + MessageCode.USER_NOT_FOUND.getCode(), + MessageCode.USER_NOT_FOUND.getDescription(), + Map.of("error", "User ID is required") + ); + } + + try { + Map statsByType = new HashMap<>(); + long totalSpent = 0L; + + for (OperationType type : OperationType.values()) { + Long spent = tokenOperationService.getTotalSpentByUserAndType(userId, type); + statsByType.put(type.name().toLowerCase(), spent != null ? spent : 0L); + totalSpent += spent != null ? spent : 0L; + } + + Map statistics = new HashMap<>(); + statistics.put("user_id", userId); + statistics.put("total_spent", totalSpent); + statistics.put("total_operations", tokenOperationService.countByUserId(userId)); + statistics.put("stats_by_type", statsByType); + + return new BaseResponse( + msgId, + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + statistics + ); + + } catch (Exception e) { + log.error("Error getting statistics", e); + return new BaseResponse( + msgId, + MessageCode.INTERNAL_ERROR.getCode(), + MessageCode.INTERNAL_ERROR.getDescription(), + Map.of("error", "Error getting statistics: " + e.getMessage()) + ); + } + } + + private BaseResponse handleFilter(Integer msgId, TokenOperationRequest request) { + if (request.getToken() == null) { + return new BaseResponse( + msgId, + MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), + MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), + Map.of("error", "User ID is required") + ); + } + + Long userId = authService.useUserAuthToken(request.getToken()); + + try { + Page page = tokenOperationService.findByFilters( + userId, + request.getOperationType(), + request.getMinSpent(), + request.getMaxSpent(), + request.getPage() != null ? request.getPage() : 0, + request.getSize() != null ? request.getSize() : 20, + request.getSortBy() != null ? request.getSortBy() : "createdAt", + request.getSortDirection() != null ? request.getSortDirection() : "desc" + ); + + TokenOperationPageDto pageDto = TokenOperationPageDto.builder() + .content(page.getContent()) + .pageNumber(page.getNumber()) + .pageSize(page.getSize()) + .totalElements(page.getTotalElements()) + .totalPages(page.getTotalPages()) + .last(page.isLast()) + .first(page.isFirst()) + .build(); + + Map responseData = new HashMap<>(); + responseData.put("operations", pageDto); + responseData.put("total_elements", page.getTotalElements()); + responseData.put("total_pages", page.getTotalPages()); + responseData.put("current_page", page.getNumber()); + responseData.put("page_size", page.getSize()); + + responseData.put("applied_filters", Map.of( + "user_id",userId != null ? userId : "all", + "operation_type", request.getOperationType() != null ? request.getOperationType() : "all", + "min_spent", request.getMinSpent() != null ? request.getMinSpent() : "none", + "max_spent", request.getMaxSpent() != null ? request.getMaxSpent() : "none" + )); + + return new BaseResponse( + msgId, + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + responseData + ); + + } catch (Exception e) { + log.error("Error filtering operations", e); + return new BaseResponse( + msgId, + MessageCode.INTERNAL_ERROR.getCode(), + MessageCode.INTERNAL_ERROR.getDescription(), + Map.of("error", "Error filtering operations: " + e.getMessage()) + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/handler/UserVerificationHandler.java b/src/main/java/ru/soune/nocopy/handler/UserVerificationHandler.java new file mode 100644 index 0000000..a09036d --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/UserVerificationHandler.java @@ -0,0 +1,108 @@ +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.file.ActionResponse; +import ru.soune.nocopy.dto.user.moderation.UserVerificationRequest; +import ru.soune.nocopy.dto.user.moderation.UserVerificationResponse; +import ru.soune.nocopy.entity.notification.NotificationType; +import ru.soune.nocopy.entity.user.ModerationStatus; +import ru.soune.nocopy.entity.user.moderation.UserVerification; +import ru.soune.nocopy.service.notification.NotificationService; +import ru.soune.nocopy.service.user.moderation.UserVerificationService; + +import java.util.Arrays; +import java.util.List; + +@Component +@RequiredArgsConstructor +@Slf4j +public class UserVerificationHandler implements RequestHandler { + + private final UserVerificationService userVerificationService; + + private final ObjectMapper objectMapper; + + private final NotificationService notificationService; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + UserVerificationRequest userVerificationRequest = objectMapper.convertValue(request.getMessageBody(), + UserVerificationRequest.class); + String action = userVerificationRequest.getAction(); + + switch (action) { + case "get_all_verifications": + return handleVerifications(request, userVerificationRequest); + case "verified": + return handleVerified(request, userVerificationRequest); + default: + ActionResponse response = ActionResponse.builder() + .action(action) + .availableActions(Arrays.asList( + "get_all_verifications", "verified")) + .build(); + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_ACTION.getCode(), + "Invalid action: " + action, + response); + } + } + + private BaseResponse handleVerifications(BaseRequest request, UserVerificationRequest userVerificationRequest) throws Exception { + List verifications = userVerificationService.verifications(); + + if (verifications.isEmpty()) { + return new BaseResponse(request.getMsgId(), MessageCode.USER_VERIFICATIONS_NOT_FOUND.getCode(), + MessageCode.USER_VERIFICATIONS_NOT_FOUND.getDescription(), null); + } + + List verificationResponses = verifications.stream().map(this::convertDto).toList(); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(verificationResponses) + .build(); + } + + private BaseResponse handleVerified(BaseRequest request, UserVerificationRequest userVerificationRequest) throws Exception { + UserVerification userVerification = userVerificationService.verifyUser(userVerificationRequest.getUserId(), userVerificationRequest.getMessage(), + userVerificationRequest.getAdminId(), userVerificationRequest.getVerified()); + + if (userVerification == null) { + return new BaseResponse(request.getMsgId(), + MessageCode.ADMIN_USER_NOT_FOUND.getCode(), + MessageCode.ADMIN_USER_NOT_FOUND.getDescription(), + null); + } + + NotificationType notificationType = userVerification.getModerationStatus() == ModerationStatus.VERIFIED? + NotificationType.USER_VERIFIED: NotificationType.USER_NOT_VERIFIED; + + notificationService.addNotification(notificationType, userVerification.getUserId()); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(UserVerificationResponse.builder() + .userVerificationId(userVerification.getId()) + .moderationStatus(userVerification.getModerationStatus().toString()) + .userId(userVerification.getUserId())) + .build(); + } + + + private UserVerificationResponse convertDto(UserVerification userVerification) { + return UserVerificationResponse.builder() + .userVerificationId(userVerification.getId()) + .userId(userVerification.getUserId()) + .moderationStatus(userVerification.getModerationStatus().name()) + .updateTime(userVerification.getUpdatedAt()) + .build(); + + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java b/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java index 8b658b4..5344fd2 100644 --- a/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java @@ -13,6 +13,7 @@ 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.geo.GeoCountryService; import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.violation.ViolationService; import ru.soune.nocopy.service.violation.ViolationStatus; @@ -21,6 +22,7 @@ import java.io.FileNotFoundException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -38,6 +40,8 @@ public class ViolationHandler implements RequestHandler { private final FileEntityService fileEntityService; + private final GeoCountryService geoCountryService; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @Override @@ -112,10 +116,16 @@ public class ViolationHandler implements RequestHandler { ); } - List violationDtos = violationPage.getContent() - .stream() - .map(ViolationResponse.ViolationDto::fromEntity) - .toList(); + List content = violationPage.getContent(); + List violationDtos = new ArrayList<>(); + + for (Violation violation : content) { + ViolationResponse.ViolationDto violationDto = ViolationResponse.ViolationDto.fromEntity(violation); + violationDto.setCountry(geoCountryService.getCountryName(violation.getPageUrl())); + violationDto.setCountryCode(geoCountryService.getCountryCode(violation.getPageUrl())); + + violationDtos.add(violationDto); + } ViolationResponse response = ViolationResponse.builder() .violations(violationDtos) diff --git a/src/main/java/ru/soune/nocopy/repository/ComplaintEntityRepository.java b/src/main/java/ru/soune/nocopy/repository/ComplaintEntityRepository.java index 159d388..3a31d59 100644 --- a/src/main/java/ru/soune/nocopy/repository/ComplaintEntityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ComplaintEntityRepository.java @@ -25,4 +25,16 @@ public interface ComplaintEntityRepository 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); + + @Query("SELECT DISTINCT f FROM FileEntity f " + + "LEFT JOIN FileMonitoringEntity m ON f.id = m.file.id " + + "WHERE f.userId IN :userIds " + + "AND f.status IN :statuses") + Page findFiles( + @Param("userIds") List userIds, + @Param("statuses") List statuses, + Pageable pageable); + + @Query("SELECT SUM(f.fileSize) FROM FileEntity f " + + "WHERE f.userId IN :userIds " + + "AND f.status IN :statuses") + Long sumFileSize( + @Param("userIds") List userIds, + @Param("statuses") List statuses); + + @Query(""" + SELECT new ru.soune.nocopy.dto.statistic.UserFilesStatisticResponse( + u.fullName, + u.Id, + CAST(COUNT(f.id) AS int), + COALESCE(SUM(f.fileSize), 0L) + ) + FROM FileEntity f + JOIN User u ON f.userId = u.Id + WHERE + (:mimeType IS NULL OR :mimeType = '' OR f.mimeType LIKE CONCAT(:mimeType, '%')) + GROUP BY u.Id, u.fullName + ORDER BY COUNT(f.id) DESC + LIMIT :topUsers + """) + List findTopUsers( + @Param("topUsers") int topUsers, + @Param("mimeType") String mimeType + ); + + @Query(""" + SELECT + COUNT(f.id), + COALESCE(SUM(f.fileSize), 0) + FROM FileEntity f + WHERE f.protectionStatus = 'PROTECTED' + """) + List getProtectedFilesTotalStats(); + + @Query(value = """ + SELECT + user_id, + COUNT(*) as file_count, + SUM(file_size) as total_size + FROM file_entities + WHERE protection_status = 'PROTECTED' + GROUP BY user_id + ORDER BY file_count + """, nativeQuery = true) + List getProtectedFilesPerUserStats(); } diff --git a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java index 0a68a2a..e934173 100644 --- a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java @@ -56,7 +56,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' + AND (f.status = 'ACTIVE' OR f.status = 'MODERATION') """, nativeQuery = true) List findCandidatesFromUserFiles( @@ -82,7 +82,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' + AND (f.status = 'ACTIVE' OR f.status = 'MODERATION') """, nativeQuery = true) List findCandidatesFromUserFiles( @@ -108,7 +108,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' + AND (f.status = 'ACTIVE' OR f.status = 'MODERATION') """, nativeQuery = true) List findExactDuplicates( diff --git a/src/main/java/ru/soune/nocopy/repository/LawCaseRepository.java b/src/main/java/ru/soune/nocopy/repository/LawCaseRepository.java new file mode 100644 index 0000000..822dc32 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/LawCaseRepository.java @@ -0,0 +1,45 @@ +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.complaint.LawCase; +import ru.soune.nocopy.entity.complaint.LawCasePriority; +import ru.soune.nocopy.entity.complaint.LawCaseType; + +import java.util.List; + +@Repository +public interface LawCaseRepository extends JpaRepository { + + @Query("SELECT l FROM LawCase l WHERE l.user.Id = :userId") + List getUserLawCases(@Param("userId") Long userId, Pageable pageable); + +// @Query("SELECT l FROM LawCase l WHERE l.user.Id = :userId " + +// "AND (:type IS NULL OR l.type = :type) " + +// "AND (:lawyer IS NULL OR l.lawyer = :lawyer) " + +// "AND (:priority IS NULL OR l.priority = :priority)") +// List getUserLawCases(@Param("userId") Long userId, +// @Param("type") LawCaseType type, +// @Param("lawyer") String lawyer, +// @Param("priority") LawCasePriority priority, +// Pageable pageable); + + @Query("SELECT l FROM LawCase l WHERE l.user.Id = :userId " + + "AND (:type IS NULL OR l.type = :type) " + + "AND (:lawyer IS NULL OR l.lawyer = :lawyer) " + + "AND (:violationId IS NULL OR l.violationId = :violationId) " + + "AND (:priority IS NULL OR l.priority = :priority)") + Page getUserLawCases(@Param("userId") Long userId, + @Param("type") LawCaseType type, + @Param("lawyer") String lawyer, + @Param("priority") LawCasePriority priority, + @Param("violationId") Long violationId, + Pageable pageable); + + @Query("SELECT COUNT(lc.id) FROM LawCase lc") + Long getTotalLawCasesCount(); +} diff --git a/src/main/java/ru/soune/nocopy/repository/ModerationFileRepository.java b/src/main/java/ru/soune/nocopy/repository/ModerationFileRepository.java new file mode 100644 index 0000000..6ad4167 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/ModerationFileRepository.java @@ -0,0 +1,13 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.file.moderation.ModerationPassportFile; + +import java.util.List; + +@Repository +public interface ModerationFileRepository extends CrudRepository { + + public List findByStatus(String status); +} diff --git a/src/main/java/ru/soune/nocopy/repository/NotificationRepository.java b/src/main/java/ru/soune/nocopy/repository/NotificationRepository.java index 50e3762..3b6af62 100644 --- a/src/main/java/ru/soune/nocopy/repository/NotificationRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/NotificationRepository.java @@ -11,6 +11,7 @@ import ru.soune.nocopy.entity.notification.NotificationStatus; import ru.soune.nocopy.entity.notification.NotificationType; import ru.soune.nocopy.entity.user.User; +import java.time.LocalDateTime; import java.util.List; public interface NotificationRepository extends JpaRepository { @@ -29,12 +30,13 @@ public interface NotificationRepository extends JpaRepository ids, @Param("user") User user, @Param("oldStatus") NotificationStatus oldStatus, - @Param("newStatus") NotificationStatus newStatus); + @Param("newStatus") NotificationStatus newStatus, + @Param("updateTime") LocalDateTime updateTime); Page getNotificationsByUser(User user, Pageable pageable); } diff --git a/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java b/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java index d599c4b..93456d7 100644 --- a/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java @@ -1,6 +1,7 @@ package ru.soune.nocopy.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import ru.soune.nocopy.entity.payment.Payment; @@ -11,4 +12,23 @@ import java.util.Optional; public interface PaymentRepository extends JpaRepository { Optional findByPaymentUuid(String paymentUuid); List findByUserId(Long userId); + + @Query(""" + SELECT COALESCE(SUM(p.tariff.tokens), 0) + FROM Payment p + WHERE p.operationType = 'TOKEN' + AND p.status = 'SUCCEEDED' + """) + Long getTotalTokensBought(); + + @Query(""" + SELECT + p.user.Id, + COALESCE(SUM(p.tariff.tokens), 0) + FROM Payment p + WHERE p.operationType = 'TOKEN' + AND p.status = 'SUCCEEDED' + GROUP BY p.user.Id + """) + List getTokensBoughtPerUser(); } diff --git a/src/main/java/ru/soune/nocopy/repository/ReferralJpaRepository.java b/src/main/java/ru/soune/nocopy/repository/ReferralJpaRepository.java index c90eba9..232bfeb 100644 --- a/src/main/java/ru/soune/nocopy/repository/ReferralJpaRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ReferralJpaRepository.java @@ -43,4 +43,22 @@ public interface ReferralJpaRepository extends JpaRepository { @Query("SELECT COUNT(r) FROM Referral r WHERE r.inviterId = :userId") int countTotalInvitees(@Param("userId") Long userId); + + @Query("SELECT COALESCE(SUM(r.totalIncome), 0) FROM Referral r") + Long getTotalIncome(); + + @Query("SELECT COALESCE(SUM(r.availableIncome), 0) FROM Referral r") + Long getTotalAvailableIncome(); + + @Query("SELECT COALESCE(SUM(r.holdBalance), 0) FROM Referral r") + Long getTotalHoldBalance(); + + @Query(""" + SELECT + r.userId, + r.totalIncome + FROM Referral r + WHERE r.totalIncome > 0 + """) + List getIncomePerUser(); } diff --git a/src/main/java/ru/soune/nocopy/repository/TariffInfoRepository.java b/src/main/java/ru/soune/nocopy/repository/TariffInfoRepository.java index 6982080..2be839d 100644 --- a/src/main/java/ru/soune/nocopy/repository/TariffInfoRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/TariffInfoRepository.java @@ -5,6 +5,7 @@ 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 ru.soune.nocopy.entity.tarif.TariffTimeTerm; import java.time.LocalDateTime; import java.util.List; @@ -13,4 +14,57 @@ import java.util.List; 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); + + @Query(""" + SELECT COUNT(u.Id) + FROM User u + WHERE (:activeOnly = false OR u.isActive = true) + AND (:companyOnly = false OR u.company IS NOT NULL) + """) + Long getTotalUsersCount( + @Param("activeOnly") boolean activeOnly, + @Param("companyOnly") boolean companyOnly + ); + + @Query(""" + SELECT COUNT(u.Id) + FROM User u + WHERE (:activeOnly = false OR u.isActive = true) + """) + Long getTotalUsersCount(@Param("activeOnly") boolean activeOnly); + + @Query(value = """ + SELECT + t.tariff_name, + COUNT(u.id) as user_count, + (COUNT(u.id) * 100.0) / NULLIF( + (SELECT COUNT(*) FROM users u2 + WHERE (:activeOnly = false OR u2.is_active = true) + AND u2.personal_tariff_info_id IS NOT NULL + ), 0 + ) as usage_percent + FROM users u + INNER JOIN tariff_info pti ON u.personal_tariff_info_id = pti.id + INNER JOIN tariff t ON pti.tariff_id = t.id + WHERE (:activeOnly = false OR u.is_active = true) + GROUP BY t.id, t.tariff_name + ORDER BY user_count DESC +""", nativeQuery = true) + List getTariffStatisticsNative(@Param("activeOnly") boolean activeOnly); + + @Query(""" + SELECT COUNT(ti.id) + FROM TariffInfo ti + WHERE ti.endTariff >= CURRENT_TIMESTAMP + AND (:term IS NULL OR ti.tariff.tariffTerm = :term) + """) + Long getCurrentActiveSubscriptions(@Param("term") TariffTimeTerm term); + + @Query(""" + SELECT ti + FROM TariffInfo ti + WHERE (:term IS NULL OR ti.tariff.tariffTerm = :term) + ORDER BY ti.startTariff + """) + List getAllTariffInfosForAnalysis(@Param("term") TariffTimeTerm term); } diff --git a/src/main/java/ru/soune/nocopy/repository/TokenOperationRepository.java b/src/main/java/ru/soune/nocopy/repository/TokenOperationRepository.java new file mode 100644 index 0000000..8319718 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/TokenOperationRepository.java @@ -0,0 +1,56 @@ +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.tokenoperation.OperationType; +import ru.soune.nocopy.entity.tokenoperation.TokenOperation; + +import java.util.List; + +@Repository +public interface TokenOperationRepository extends JpaRepository { + Page findAll(Pageable pageable); + + List findByUserId(Long userId); + + Page findByUserId(Long userId, Pageable pageable); + + Page findByOperationType(OperationType operationType, Pageable pageable); + + @Query("SELECT t FROM TokenOperation t WHERE " + + "(:userId IS NULL OR t.userId = :userId) AND " + + "(:operationType IS NULL OR t.operationType = :operationType) AND " + + "(:minSpent IS NULL OR t.spent >= :minSpent) AND " + + "(:maxSpent IS NULL OR t.spent <= :maxSpent)") + Page findByFilters( + @Param("userId") Long userId, + @Param("operationType") OperationType operationType, + @Param("minSpent") Long minSpent, + @Param("maxSpent") Long maxSpent, + Pageable pageable + ); + + @Query("SELECT SUM(t.spent) FROM TokenOperation t WHERE t.userId = :userId AND t.operationType = :operationType") + Long sumSpentByUserAndType(@Param("userId") Long userId, @Param("operationType") OperationType operationType); + + Long countByUserId(Long userId); + + @Query(""" + SELECT COALESCE(SUM(t.spent), 0) + FROM TokenOperation t + """) + Long getTotalTokensSpent(); + + @Query(""" + SELECT + t.userId, + COALESCE(SUM(t.spent), 0) + FROM TokenOperation t + GROUP BY t.userId + """) + List getTokensSpentPerUser(); +} diff --git a/src/main/java/ru/soune/nocopy/repository/UserRepository.java b/src/main/java/ru/soune/nocopy/repository/UserRepository.java index 8c7e31c..df28e66 100644 --- a/src/main/java/ru/soune/nocopy/repository/UserRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/UserRepository.java @@ -2,10 +2,13 @@ package ru.soune.nocopy.repository; import jakarta.validation.constraints.Size; 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.tarif.TariffInfo; import ru.soune.nocopy.entity.user.User; import java.util.List; +import java.util.Optional; public interface UserRepository extends JpaRepository { User findByEmail(String email); @@ -14,4 +17,32 @@ public interface UserRepository extends JpaRepository { List findByCompanyId(String companyId); long countByCompanyId(String companyId); User findByPersonalTariffInfo(TariffInfo tariffInfo); + + @Query("SELECT COUNT(u.Id) FROM User u") + Long getTotalUsersCount(); + + @Query(value = """ + SELECT COUNT(*) + FROM users + WHERE created_at >= NOW() - INTERVAL '1 day' * :days + """, nativeQuery = true) + Long getNewUsersCount(int days); + + @Query(value = """ + SELECT COUNT(*) + FROM users + WHERE created_at >= DATE_TRUNC('month', NOW()) + """, nativeQuery = true) + Long getNewUsersCurrentMonth(); + + @Query(value = """ + SELECT COUNT(*) + FROM users + WHERE created_at >= DATE_TRUNC('month', NOW() - INTERVAL '1 month') + AND created_at < DATE_TRUNC('month', NOW()) + """, nativeQuery = true) + Long getNewUsersPreviousMonth(); + + @Query("SELECT u FROM User u WHERE u.personalTariffInfo.id = :tariffInfoId") + Optional findByPersonalTariffInfoId(@Param("tariffInfoId") String tariffInfoId); } diff --git a/src/main/java/ru/soune/nocopy/repository/UserVerificationRepository.java b/src/main/java/ru/soune/nocopy/repository/UserVerificationRepository.java new file mode 100644 index 0000000..b491506 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/UserVerificationRepository.java @@ -0,0 +1,12 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.soune.nocopy.entity.user.ModerationStatus; +import ru.soune.nocopy.entity.user.moderation.UserVerification; + +import java.util.List; + +public interface UserVerificationRepository extends JpaRepository { + List findByUserId(Long userId); + ListfindByUserIdAndModerationStatus(Long userId, ModerationStatus moderationStatus); +} diff --git a/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java b/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java index 0834914..250f50d 100644 --- a/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java @@ -74,4 +74,10 @@ public interface ViolationRepository extends JpaRepository { List findByFileEntityInAndStatusAndCreatedDateBetween(List files, String status, LocalDateTime startDate, LocalDateTime endDate, Sort sort); + + @Query("SELECT COUNT(v.id) FROM Violation v") + Long getTotalViolationsCount(); + + @Query("SELECT COUNT(DISTINCT c.violation.id) FROM ComplaintEntity c WHERE c.violation IS NOT NULL") + Long getViolationsWithComplaintCount(); } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/complaint/LawCaseService.java b/src/main/java/ru/soune/nocopy/service/complaint/LawCaseService.java new file mode 100644 index 0000000..740ed9a --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/complaint/LawCaseService.java @@ -0,0 +1,90 @@ +package ru.soune.nocopy.service.complaint; + +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 ru.soune.nocopy.entity.complaint.LawCase; +import ru.soune.nocopy.entity.complaint.LawCasePriority; +import ru.soune.nocopy.entity.complaint.LawCaseType; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.repository.LawCaseRepository; + +import java.math.BigDecimal; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class LawCaseService { + + private final LawCaseRepository lawCaseRepository; + + public LawCase addLawCase(BigDecimal amount, String description, String name, + LawCasePriority priority, Long violationId, User user) { + LawCase lawCase = new LawCase(); + + BigDecimal damage = amount == null ? BigDecimal.ZERO : amount; + + lawCase.setAmount(damage); + lawCase.setName(name); + lawCase.setDescription(description); + lawCase.setPriority(priority); + lawCase.setViolationId(violationId); + lawCase.setUser(user); + + return lawCaseRepository.save(lawCase); + } + + public LawCase getById(Long id) { + return lawCaseRepository.findById(id).orElse(null); + } + + public LawCase changeStatus(Long id, LawCaseType type) { + LawCase lawCase = lawCaseRepository.findById(id).orElse(null); + if (lawCase == null) { + return null; + } + + lawCase.setType(type); + return lawCaseRepository.save(lawCase); + } + + public LawCase changePriority(Long id, LawCasePriority priority) { + LawCase lawCase = lawCaseRepository.findById(id).orElse(null); + if (lawCase == null) { + return null; + } + + lawCase.setPriority(priority); + return lawCaseRepository.save(lawCase); + } + + public LawCase changeDamage(Long id, BigDecimal damage) { + LawCase lawCase = lawCaseRepository.findById(id).orElse(null); + if (lawCase == null) { + return null; + } + lawCase.setAmount(damage); + return lawCaseRepository.save(lawCase); + } + + public Page getAllUserLawCases(Long userId, int pageSize, int pageNumber, + LawCaseType lawCaseType, String lawyer, + LawCasePriority lawCasePriority, + Long violationId, + String propertySort, String ascDesc) { + Sort.Direction direction = ascDesc.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC; + Sort sort = Sort.by(direction, propertySort); + Pageable pageable = PageRequest.of(pageNumber, pageSize, sort); + + return lawCaseRepository.getUserLawCases(userId, lawCaseType, lawyer, lawCasePriority, violationId, pageable); + } + + public void deleteById(Long id) { + lawCaseRepository.deleteById(id); + } + +} + 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 5c720bb..fe480bb 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -3,6 +3,8 @@ package ru.soune.nocopy.service.file; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +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.file.FileEntityResponse; @@ -81,17 +83,61 @@ public class FileEntityService { for (Long uId : userRepository.findByCompanyId(user.getCompany().getId()).stream() .map(User::getId) .toList()) { - allFiles.addAll(fileEntityRepository.findByUserIdAndStatusIn(uId, - List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION))); + allFiles.addAll(getAllUserFiles(uId, List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, + FileStatus.MODERATION))); } } else { - allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId, - List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION)); + allFiles = getAllUserFiles(userId, List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION)); } return allFiles; } + public List getAllUserFiles(Long userId, List statuses) { + User user = userRepository.findById(userId).orElseThrow(); + List allFiles = new ArrayList<>(); + + if (user.getCompany() != null) { + for (Long uId : userRepository.findByCompanyId(user.getCompany().getId()).stream() + .map(User::getId) + .toList()) { + allFiles.addAll(fileEntityRepository.findByUserIdAndStatusIn(uId, statuses)); + } + } else { + allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId, statuses); + } + + return allFiles; + } + + @Transactional(readOnly = true) + public FileResponse getUserFiles(Long userId, Pageable pageable, int version) { + User user = userRepository.findById(userId).orElseThrow(); + List users = user.getCompany() != null ? + userRepository.findByCompanyId(user.getCompany().getId()) : List.of(user); + List statusesForSearch = List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION); + + Page files = fileEntityRepository.findFiles(users.stream().map(User::getId).toList(), + statusesForSearch, pageable); + + Long totalSize = fileEntityRepository.sumFileSize( + users.stream().map(User::getId).toList(), + statusesForSearch); + + List filesToResponse = files.stream() + .map(file -> convertToResponse(file, version)) + .toList(); + + return FileResponse.builder() + .files(filesToResponse) + .totalCount((int) files.getTotalElements()) + .totalSize(totalSize != null ? totalSize : 0L) + .formattedTotalSize(formatFileSize(totalSize != null ? totalSize : 0L)) + .page(pageable.getPageNumber()) + .pageSize(pageable.getPageSize()) + .build(); + } + @Transactional(readOnly = true) public FileResponse getUserFiles(Long userId, int page, int pageSize, int version) { User user = userRepository.findById(userId).orElseThrow(); diff --git a/src/main/java/ru/soune/nocopy/service/file/FileStatsService.java b/src/main/java/ru/soune/nocopy/service/file/FileStatsService.java index 8407ad1..f8994e7 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileStatsService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileStatsService.java @@ -25,8 +25,12 @@ public class FileStatsService { private final UserRepository userRepository; + private final FileEntityService fileEntityService; + public FileInfoUserResponse getUserFileStats(Long userId) { - List userFiles = fileEntityRepository.findByUserId(userId); + List userFiles = fileEntityService.getAllUserFiles(userId, List.of(FileStatus.ACTIVE, + FileStatus.BLOCKED, FileStatus.MODERATION)); + return calculateStats(userFiles); } @@ -34,32 +38,24 @@ public class FileStatsService { return FileInfoUserResponse.builder() .allFileSize(calculateTotalSize(files)) .fileCount(calculateTotalCount(files)) - .filesCheck(calculateByStatus(files, FileStatus.CHECKED)) - .filesViolation(calculateByStatus(files, FileStatus.VIOLATION)) +// .filesCheck(calculateByStatus(files, FileStatus.CHECKED)) +// .filesViolation(calculateByStatus(files, FileStatus.VIOLATION)) .protectedFilesCount(protectedUserFiles(files)) .imagesSize(calculateMediaSize(files, FileType.IMAGE)) .imagesCount(calculateMediaCount(files, FileType.IMAGE)) - .imagesCheck(calculateMediaByStatus(files, FileType.IMAGE, FileStatus.CHECKED)) - .imagesViolations(calculateMediaByStatus(files, FileType.IMAGE, FileStatus.VIOLATION)) .protectedImageFilesCount(protectedUserFiles(files, FileType.IMAGE)) .videosSize(calculateMediaSize(files, FileType.VIDEO)) .videosCount(calculateMediaCount(files, FileType.VIDEO)) - .videosCheck(calculateMediaByStatus(files, FileType.VIDEO, FileStatus.CHECKED)) - .videosViolations(calculateMediaByStatus(files, FileType.VIDEO, FileStatus.VIOLATION)) .protectedVideoFilesCount(protectedUserFiles(files, FileType.VIDEO)) .audiosSize(calculateMediaSize(files, FileType.AUDIO)) .audiosCount(calculateMediaCount(files, FileType.AUDIO)) - .audiosCheck(calculateMediaByStatus(files, FileType.AUDIO, FileStatus.CHECKED)) - .audiosViolations(calculateMediaByStatus(files, FileType.AUDIO, FileStatus.VIOLATION)) .protectedAudioFilesCount(protectedUserFiles(files, FileType.AUDIO)) .documentSize(calculateMediaSize(files, FileType.DOCUMENT)) .documentCount(calculateMediaCount(files, FileType.DOCUMENT)) - .documentCheck(calculateMediaByStatus(files, FileType.DOCUMENT, FileStatus.CHECKED)) - .documentViolations(calculateMediaByStatus(files, FileType.DOCUMENT, FileStatus.VIOLATION)) .protectedDocumentFilesCount(protectedUserFiles(files, FileType.DOCUMENT)) .build(); } 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 28dad71..ac7bc06 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileUploadService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileUploadService.java @@ -16,6 +16,9 @@ public interface FileUploadService { UploadProgressResponse uploadChunk(String uploadId, Integer chunkNumber, MultipartFile chunkFile, Integer findSimilar); + UploadProgressResponse uploadPassportChunk(String uploadId, Integer chunkNumber, MultipartFile chunkFile, + Long userId); + UploadProgressResponse getUploadProgress(String uploadId); FileEntity completeFileProcessing(FileUploadSession session, FileStatus status); diff --git a/src/main/java/ru/soune/nocopy/service/file/ZipService.java b/src/main/java/ru/soune/nocopy/service/file/ZipService.java new file mode 100644 index 0000000..abc1a07 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/file/ZipService.java @@ -0,0 +1,146 @@ +package ru.soune.nocopy.service.file; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.file.moderation.ModerationPassportFile; +import ru.soune.nocopy.service.file.moderation.ModerationFileService; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +@Service +@Slf4j +@RequiredArgsConstructor +public class ZipService { + + private final ModerationFileService moderationFileService; + + private static final int MAX_SIZE_MB = 20; + + private static final int FIRST_PART = 1; + + private static final int SECOND_PART = 2; + + public void createAndSplitZip(Long userId, List files) throws IOException { + byte[] zipBytes = createZip(files); + String base64String = Base64.getEncoder().encodeToString(zipBytes); + String[] parts = splitBase64(base64String); + saveSplitFiles(userId, parts[0], parts[1]); + } + + public byte[] assembleZipFromSplitFiles(Long userId) throws IOException { + List activeFiles = moderationFileService.getActiveFiles(); + + String part1Content = null; + String part2Content = null; + + for (ModerationPassportFile file : activeFiles) { + Path filePath = Paths.get(file.getPath()); + String content = Files.readString(filePath); + + if (file.getPart() == FIRST_PART) { + part1Content = content; + } else if (file.getPart() == SECOND_PART) { + part2Content = content; + } + } + + if (part1Content == null || part2Content == null) { + throw new IOException("Missing part 1 or part 2 for user: " + userId); + } + + String base64String = mergeBase64(part1Content, part2Content); + + return Base64.getDecoder().decode(base64String); + } + + private byte[] createZip(List files) throws IOException { + long totalSize = files.stream() + .map(FileEntity::getFileSize) + .reduce(0L, Long::sum); + + + if (totalSize > MAX_SIZE_MB * 1024 * 1024) { + throw new IllegalArgumentException( + String.format("Total size %.2f MB exceeds %d MB limit", + totalSize / (1024.0 * 1024.0), MAX_SIZE_MB) + ); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ZipOutputStream zos = new ZipOutputStream(baos)) { + + for (FileEntity file: files) { + String filePath = file.getFilePath(); + String fileName = file.getStoredFileName(); + + Path path = Paths.get(filePath); + ZipEntry zipEntry = new ZipEntry(fileName); + zos.putNextEntry(zipEntry); + Files.copy(path, zos); + zos.closeEntry(); + + log.info("Added file {} to ZIP", fileName); + } + } + + return baos.toByteArray(); + } + + private String[] splitBase64(String base64) { + StringBuilder part1 = new StringBuilder(); + StringBuilder part2 = new StringBuilder(); + + for (int i = 0; i < base64.length(); i++) { + if (i % 2 == 0) { + part1.append(base64.charAt(i)); + } else { + part2.append(base64.charAt(i)); + } + } + + return new String[]{part1.toString(), part2.toString()}; + } + + private void saveSplitFiles(Long userId, String part1, String part2) throws IOException { + Path userDir = Paths.get("/uploads/passport", String.valueOf(userId)); + Files.createDirectories(userDir); + + Path file1 = userDir.resolve(userId + "___1.txt"); + Files.writeString(file1, part1); + Path file2 = userDir.resolve(userId + "___2.txt"); + Files.writeString(file2, part2); + Map files = Map.of(FIRST_PART, file1.toString(), SECOND_PART, file2.toString()); + + moderationFileService.addFiles(userId, files); + + log.info("Saved split files: {} and {}", file1, file2); + } + + private String mergeBase64(String part1, String part2) { + StringBuilder merged = new StringBuilder(); + + int maxLength = Math.max(part1.length(), part2.length()); + + for (int i = 0; i < maxLength; i++) { + if (i < part1.length()) { + merged.append(part1.charAt(i)); + } + if (i < part2.length()) { + merged.append(part2.charAt(i)); + } + } + + return merged.toString(); + } +} 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 5833a97..517d587 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 @@ -19,6 +19,7 @@ import org.springframework.web.multipart.MultipartFile; import ru.soune.nocopy.dto.file.UploadProgressResponse; import ru.soune.nocopy.entity.file.*; import ru.soune.nocopy.entity.notification.NotificationType; +import ru.soune.nocopy.entity.tokenoperation.OperationType; import ru.soune.nocopy.exception.*; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileUploadSessionRepository; @@ -30,7 +31,9 @@ import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileUploadService; import ru.soune.nocopy.service.file.cloud.CloudStorageService; import ru.soune.nocopy.service.tariff.TariffConstants; +import ru.soune.nocopy.service.file.moderation.ModerationFileService; import ru.soune.nocopy.service.notification.NotificationService; +import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.tariff.TariffInfoService; import ru.soune.nocopy.util.FileUtil; @@ -91,6 +94,10 @@ public class FileUploadServiceImpl implements FileUploadService { private final NotificationService notificationService; + private final AuthService authService; + + private final ModerationFileService moderationFileService; + private final CloudStorageService cloudStorageService; @PostConstruct @@ -189,35 +196,28 @@ public class FileUploadServiceImpl implements FileUploadService { @Override public UploadProgressResponse uploadChunk(String uploadId, Integer chunkNumber, - MultipartFile chunkFile, Integer findSimilar) { + MultipartFile chunkFile, Integer findSimilar) { FileUploadSession session = sessionRepository.findById(uploadId) .orElseThrow(() -> new UploadSessionNotFoundException(uploadId)); - validateSession(session); - - if (session.getExpiresAt().isBefore(LocalDateTime.now())) { - handleExpiredSession(session); - throw new FileUploadException("Upload session expired"); - } - - if (chunkNumber < 0 || chunkNumber >= session.getTotalChunks()) { - throw new FileUploadException( - String.format("Invalid chunk number %d. Expected number: %d", - chunkNumber, session.getTotalChunks() - 1)); - } - - if (chunkNumber == 0 && chunkFile.getSize() > chunkSize || chunkNumber + 1 == (session.getTotalChunks()) - && chunkFile.getSize() > chunkSize) { - throw new ChunkSizeExceededException(chunkFile.getSize(), chunkSize); - } - - if (chunkNumber > 0 && chunkNumber + 1 < session.getTotalChunks() && chunkFile.getSize() != chunkSize) { - throw new ChunkSizeExceededException(chunkFile.getSize(), chunkSize); - } + validateSession(session, chunkNumber, chunkFile); return processChunk(session, chunkNumber, chunkFile, findSimilar); } + @Override + public UploadProgressResponse uploadPassportChunk(String uploadId, Integer chunkNumber, MultipartFile chunkFile, + Long userId) { + FileUploadSession session = sessionRepository.findById(uploadId).orElseThrow(() -> + new UploadSessionNotFoundException(uploadId)); + + validateSession(session, chunkNumber, chunkFile); + + UploadProgressResponse uploadProgressResponse = processChunk(session, chunkNumber, chunkFile); + + return uploadProgressResponse; + } + @Override public UploadProgressResponse getUploadProgress(String uploadId) { FileUploadSession session = sessionRepository.findById(uploadId) @@ -243,20 +243,23 @@ public class FileUploadServiceImpl implements FileUploadService { .fileExtension(session.getExtension()) .checksum(checksum) .uploadSessionId(session.getUploadId()) - .protectionStatus(ProtectionStatus.PROCESSING) .status(status) .build(); + if (status != FileStatus.TEMP) { + fileEntity.setProtectionStatus(ProtectionStatus.PROCESSING); + } + FileEntity saved = fileEntityRepository.save(fileEntity); - if (session.getFileType().equals("image")) { + if (session.getFileType().equals("image") && status != FileStatus.PRIVATE) { Map hash = imageHashService.calculateHash(filePath); imageHashService.create(saved, hash); } cleanupSessionFiles(session); - if (status != FileStatus.TEMP) { + if (status != FileStatus.TEMP && status != FileStatus.PRIVATE) { FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity, session.getConvertTo()); noCopyFileService.addFile(fileInfo); } @@ -271,7 +274,8 @@ public class FileUploadServiceImpl implements FileUploadService { } } - private void validateSession(FileUploadSession session) { + private void validateSession(FileUploadSession session, Integer chunkNumber, + MultipartFile chunkFile) { UploadStatus status = session.getStatus(); if (status == UploadStatus.FAILED) { @@ -298,6 +302,67 @@ public class FileUploadServiceImpl implements FileUploadService { session.setStatus(UploadStatus.UPLOADING); sessionRepository.save(session); } + + if (session.getExpiresAt().isBefore(LocalDateTime.now())) { + handleExpiredSession(session); + throw new FileUploadException("Upload session expired"); + } + + if (chunkNumber < 0 || chunkNumber >= session.getTotalChunks()) { + throw new FileUploadException( + String.format("Invalid chunk number %d. Expected number: %d", + chunkNumber, session.getTotalChunks() - 1)); + } + + if (chunkNumber == 0 && chunkFile.getSize() > chunkSize || chunkNumber + 1 == (session.getTotalChunks()) + && chunkFile.getSize() > chunkSize) { + throw new ChunkSizeExceededException(chunkFile.getSize(), chunkSize); + } + + if (chunkNumber > 0 && chunkNumber + 1 < session.getTotalChunks() && chunkFile.getSize() != chunkSize) { + throw new ChunkSizeExceededException(chunkFile.getSize(), chunkSize); + } + } + + private UploadProgressResponse processChunk(FileUploadSession session, Integer chunkNumber, MultipartFile chunkFile) { + String chunkPath = null; + + try { + chunkPath = saveChunkWithIntegrityCheck(session, chunkNumber, chunkFile); + + session.getChunkPaths().put(chunkNumber, chunkPath); + session.setChunksUploaded(session.getChunksUploaded() + 1); + session.setStatus(UploadStatus.UPLOADING); + session.setExpiresAt(LocalDateTime.now().plusMinutes(1)); + + boolean isLastChunk = session.getChunksUploaded().equals(session.getTotalChunks()); + + if (isLastChunk) { + String finalFilePath = assembleFileSynchronously(session); + + session.setStatus(UploadStatus.COMPLETED); + session.setFilePath(finalFilePath); + + completeFileProcessingAsync(session, FileStatus.PRIVATE); + } else { + sessionRepository.save(session); + } + + return UploadProgressResponse.fromSession(session); + } catch (Exception e) { + if (chunkPath != null) { + cleanupFailedChunk(chunkPath); + } + + log.error("Failed to process chunk {} for session {}: {}", + chunkNumber, session.getUploadId(), e.getMessage(), e); + + session.setStatus(UploadStatus.FAILED); + session.setLastError(e.getMessage()); + sessionRepository.save(session); + + throw new FileUploadException("Failed to upload chunk: " + e.getMessage(), e); + } } @Autowired @@ -397,7 +462,7 @@ public class FileUploadServiceImpl implements FileUploadService { if (status != FileStatus.TEMP) { String fileType = session.getFileType(); - tariffInfoService.writeOffTokens(session.getUserId(), costService.protectFileCost(fileType)); + tariffInfoService.writeOffTokens(session.getUserId(), costService.protectFileCost(fileType), OperationType.FILE_UPLOAD); } } else { sessionRepository.save(session); diff --git a/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationFileService.java b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationFileService.java new file mode 100644 index 0000000..6a859a0 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationFileService.java @@ -0,0 +1,54 @@ +package ru.soune.nocopy.service.file.moderation; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.soune.nocopy.entity.file.moderation.ModerationPassportFile; +import ru.soune.nocopy.repository.ModerationFileRepository; +import ru.soune.nocopy.service.user.moderation.UserVerificationService; + +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class ModerationFileService { + + private final ModerationFileRepository moderationFileRepository; + + private final UserVerificationService userVerificationService; + + public void addFiles(Long userId, Map files) { + List moderationFileRepositoryByStatus = + moderationFileRepository.findByStatus(ModerationFileStatus.ACTIVE.getName()); + + if (!moderationFileRepositoryByStatus.isEmpty()) { + disActiveDocuments(moderationFileRepositoryByStatus); + } + + for (Map.Entry file : files.entrySet()) { + ModerationPassportFile moderationFile = ModerationPassportFile.builder().userId(userId) + .path(file.getValue()) + .part(file.getKey()) + .userId(userId) + .status(ModerationFileStatus.ACTIVE.getName()) + .build(); + + + moderationFileRepository.save(moderationFile); + } + + userVerificationService.initUserVerification(userId); + } + + public List getActiveFiles() { + return moderationFileRepository.findByStatus(ModerationFileStatus.ACTIVE.getName()); + } + + private void disActiveDocuments(List moderationFileRepositoryByStatus) { + for (ModerationPassportFile moderationFile : moderationFileRepositoryByStatus) { + moderationFile.setStatus(ModerationFileStatus.NOT_ACTIVE.getName()); + + moderationFileRepository.save(moderationFile); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationFileStatus.java b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationFileStatus.java new file mode 100644 index 0000000..8dc3052 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationFileStatus.java @@ -0,0 +1,14 @@ +package ru.soune.nocopy.service.file.moderation; + +import lombok.Getter; + +@Getter +public enum ModerationFileStatus { + ACTIVE("active"), NOT_ACTIVE("not_active"); + + private final String name; + + ModerationFileStatus(String name) { + this.name = name; + } +} diff --git a/src/main/java/ru/soune/nocopy/service/geo/GeoCountryService.java b/src/main/java/ru/soune/nocopy/service/geo/GeoCountryService.java new file mode 100644 index 0000000..68ca2e6 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/geo/GeoCountryService.java @@ -0,0 +1,66 @@ +package ru.soune.nocopy.service.geo; + +import com.maxmind.geoip2.DatabaseReader; +import com.maxmind.geoip2.exception.GeoIp2Exception; +import com.maxmind.geoip2.model.CountryResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.net.InetAddress; + +@Service +@Slf4j +public class GeoCountryService { + + private final DatabaseReader dbReader; + + public GeoCountryService() throws IOException { + Resource resource = new ClassPathResource("geo/GeoLite2-Country.mmdb"); + this.dbReader = new DatabaseReader.Builder(resource.getInputStream()).build(); + } + + public String getCountryName(String input) { + try { + CountryResponse response = extractCountry(input); + + return response.getCountry().getName(); + } catch (Exception e) { + log.error("Failed for input: {}", input, e); + return "UNKNOWN"; + } + } + + public String getCountryCode(String input) { + try { + CountryResponse response = extractCountry(input); + + return response.getCountry().getIsoCode(); + } catch (Exception e) { + log.error("Failed for input: {}", input, e); + return "UNKNOWN"; + } + } + + private CountryResponse extractCountry(String input) throws IOException, GeoIp2Exception { + String domain = extractDomain(input); + + InetAddress ip = InetAddress.getByName(domain); + + return dbReader.country(ip); + } + + private String extractDomain(String input) { + if (input == null || input.isEmpty()) { + throw new IllegalArgumentException("Input is empty"); + } + + String cleaned = input.replaceAll("^https?://", ""); + cleaned = cleaned.split("/")[0]; + cleaned = cleaned.split(":")[0]; + + return cleaned; + } +} \ 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 755c83d..96a64bc 100644 --- a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java @@ -15,6 +15,7 @@ import ru.soune.nocopy.entity.notification.NotificationMessage; import ru.soune.nocopy.entity.notification.NotificationType; import ru.soune.nocopy.entity.tarif.TariffInfo; import ru.soune.nocopy.entity.tarif.TariffType; +import ru.soune.nocopy.entity.tokenoperation.OperationType; import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.TariffNotFoundException; import ru.soune.nocopy.repository.FileMonitoringRepository; @@ -64,9 +65,10 @@ public class MonitoringSearchService { MonitoringType monitoringType = monitoring.getMonitoringType(); TariffDTO tariffMonitoring = tariffService.getTariffByType(TariffType.valueOf(monitoringType.name())); + User user = userRepository.findById(monitoring.getUserId()).orElseThrow(); try { - tariffInfoService.writeOffTokens(monitoring.getUserId(), tariffMonitoring.getTokens()); + tariffInfoService.writeOffTokens(monitoring.getUserId(), tariffMonitoring.getTokens(), OperationType.MONITORING); boolean useYandex = searchProperties.getEngines().getOrDefault("yandex", new SearchProperties.EngineConfig()).isEnabled(); @@ -88,10 +90,14 @@ public class MonitoringSearchService { 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); + } catch (TimeoutException | IOException e) { + log.error("Error processing monitoring search", e); + monitoring.setLastRunStatus("ERROR: " + e.getMessage()); + updateNextRun(monitoring); + + monitoringRepository.save(monitoring); + + notificationService.addNotification(NotificationType.SEARCH_OPERATION_FAILED, user.getId()); } } else { log.info("Yandex search is disabled by settings"); @@ -108,10 +114,14 @@ public class MonitoringSearchService { 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); + } catch (TimeoutException | IOException e) { + log.error("Error processing monitoring search", e); + monitoring.setLastRunStatus("ERROR: " + e.getMessage()); + updateNextRun(monitoring); + + monitoringRepository.save(monitoring); + + notificationService.addNotification(NotificationType.SEARCH_OPERATION_FAILED, user.getId()); } } else { log.info("Google search is disabled by settings"); @@ -138,7 +148,6 @@ public class MonitoringSearchService { } } catch (TariffNotFoundException e) { - User user = userRepository.findById(monitoring.getUserId()).orElseThrow(); TariffInfo activeTariffInfo = user.getActiveTariffInfo(); int currentTokens = activeTariffInfo.getTokens() + activeTariffInfo.getBoughtTokens(); @@ -153,7 +162,6 @@ public class MonitoringSearchService { notificationService.addNotification(NotificationType.TOKEN_NOT_FOUND, user.getId(), NotificationMessage.FILE_SEARCH.getMessageKey()); } catch (Exception e) { - User user = userRepository.findById(monitoring.getUserId()).orElseThrow(); log.error("Error processing monitoring search", e); monitoring.setLastRunStatus("ERROR: " + e.getMessage()); updateNextRun(monitoring); diff --git a/src/main/java/ru/soune/nocopy/service/notification/NotificationService.java b/src/main/java/ru/soune/nocopy/service/notification/NotificationService.java index e820789..d7567b5 100644 --- a/src/main/java/ru/soune/nocopy/service/notification/NotificationService.java +++ b/src/main/java/ru/soune/nocopy/service/notification/NotificationService.java @@ -17,6 +17,7 @@ import ru.soune.nocopy.repository.NotificationRepository; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.user.UserService; +import java.time.LocalDateTime; import java.util.List; import java.util.Locale; @@ -33,15 +34,12 @@ public class NotificationService { @Transactional public void addNotification(NotificationType notificationType, long userId, Object... args) { - Locale locale = Locale.forLanguageTag("ru"); - - String message = messageSource.getMessage(notificationType.getMessageKey(), args, locale); - Notification notification = Notification.builder() .notificationType(notificationType) + .createdAt(LocalDateTime.now()) .user(userRepository.findById(userId).orElseThrow()) .status(NotificationStatus.NEW) - .message(message) + .message(notificationType.getMessageKey()) .build(); notificationRepository.save(notification); @@ -110,7 +108,24 @@ public class NotificationService { if (notificationIds == null || notificationIds.isEmpty()) { return 0; } + return notificationRepository.updateStatus(notificationIds, user, NotificationStatus.NEW, - NotificationStatus.READIED); + NotificationStatus.READIED, LocalDateTime.now()); + } + + @Transactional + public int delete(List notificationIds) { + if (notificationIds == null || notificationIds.isEmpty()) { + return 0; + } + + int count = 0; + + for (Long notificationId : notificationIds) { + notificationRepository.deleteById(notificationId); + count += 1; + } + + return count; } } 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 0d3d832..ac10767 100644 --- a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -12,6 +12,7 @@ import ru.soune.nocopy.entity.notification.NotificationType; 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.tokenoperation.OperationType; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.GlobalSearchResultRepository; import ru.soune.nocopy.repository.GlobalSearchTaskRepository; @@ -59,7 +60,7 @@ public class GlobalSearchAsyncProcessor { FileEntity file = fileEntityRepository.findById(uuid) .orElseThrow(() -> new RuntimeException("File not found: " + uuid)); - tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH); + tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH, OperationType.GLOBAL_SEARCH); GlobalSearchResult result = processFile(file, taskId, userId); globalSearchResultRepository.save(result); diff --git a/src/main/java/ru/soune/nocopy/service/statistic/IncomeStatisticService.java b/src/main/java/ru/soune/nocopy/service/statistic/IncomeStatisticService.java new file mode 100644 index 0000000..d21cd9d --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/statistic/IncomeStatisticService.java @@ -0,0 +1,55 @@ +package ru.soune.nocopy.service.statistic; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.soune.nocopy.dto.statistic.IncomeStatisticResponse; +import ru.soune.nocopy.repository.ReferralJpaRepository; +import ru.soune.nocopy.util.PercentileCalculator; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +public class IncomeStatisticService { + private final ReferralJpaRepository referralRepository; + + private static final int PERCENTILE_5 = 5; + + private static final int PERCENTILE_50 = 50; + + private static final int PERCENTILE_95 = 95; + + public IncomeStatisticResponse getIncomeStatistics() { + Long totalIncome = referralRepository.getTotalIncome(); + Long totalAvailable = referralRepository.getTotalAvailableIncome(); + Long totalHold = referralRepository.getTotalHoldBalance(); + + List incomeData = referralRepository.getIncomePerUser(); + List incomeValues = extractIncomeValues(incomeData); + + PercentileCalculator.PercentilesResult incomePercentiles = + PercentileCalculator.calculatePercentiles(incomeValues, PERCENTILE_5, PERCENTILE_50, + PERCENTILE_95); + + return IncomeStatisticResponse.builder() + .totalIncome(totalIncome != null ? totalIncome : 0L) + .totalAvailable(totalAvailable != null ? totalAvailable : 0L) + .totalHold(totalHold != null ? totalHold : 0L) + .incomePerUser(new IncomeStatisticResponse.Percentiles( + incomePercentiles.getP5() != null ? incomePercentiles.getP5() : 0L, + incomePercentiles.getP50() != null ? incomePercentiles.getP50() : 0L, + incomePercentiles.getP95() != null ? incomePercentiles.getP95() : 0L + )) + .build(); + } + + private List extractIncomeValues(List data) { + return data.stream() + .map(row -> row[1] != null ? ((Number) row[1]).longValue() : 0L) + .filter(value -> value > 0) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/statistic/ProtectedFilesStatisticService.java b/src/main/java/ru/soune/nocopy/service/statistic/ProtectedFilesStatisticService.java new file mode 100644 index 0000000..36c5d49 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/statistic/ProtectedFilesStatisticService.java @@ -0,0 +1,118 @@ +package ru.soune.nocopy.service.statistic; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.soune.nocopy.dto.statistic.ProtectedFilesStatisticResponse; +import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.util.PercentileCalculator; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ProtectedFilesStatisticService { + private static final int PERCENTILE_5 = 5; + + private static final int PERCENTILE_50 = 50; + + private static final int PERCENTILE_95 = 95; + + private static final double BYTES_TO_MB = 1048576.0; + + private static final double BYTES_TO_GB = 1073741824.0; + + private final FileEntityRepository fileEntityRepository; + + public ProtectedFilesStatisticResponse getProtectedFilesStatistics() { + List totalStats = fileEntityRepository.getProtectedFilesTotalStats(); + Long totalFiles = extractTotalFiles(totalStats); + Long totalSize = extractTotalSize(totalStats); + + List userStats = fileEntityRepository.getProtectedFilesPerUserStats(); + + List fileCounts = extractFileCounts(userStats); + List fileSizes = extractFileSizes(userStats); + + PercentileCalculator.PercentilesResult filesPercentiles = + PercentileCalculator.calculatePercentiles(fileCounts, PERCENTILE_5, PERCENTILE_50, PERCENTILE_95); + PercentileCalculator.PercentilesResult sizePercentiles = + PercentileCalculator.calculatePercentiles(fileSizes, PERCENTILE_5, PERCENTILE_50, PERCENTILE_95); + + return buildResponse(totalFiles, totalSize, filesPercentiles, sizePercentiles); + } + + private Long extractTotalFiles(List totalStats) { + if (totalStats.isEmpty() || totalStats.get(0) == null) { + return 0L; + } + Object[] row = totalStats.get(0); + return row[0] != null ? ((Number) row[0]).longValue() : 0L; + } + + private Long extractTotalSize(List totalStats) { + if (totalStats.isEmpty() || totalStats.get(0) == null) { + return 0L; + } + Object[] row = totalStats.get(0); + return row[1] != null ? ((Number) row[1]).longValue() : 0L; + } + + private List extractFileCounts(List userStats) { + return userStats.stream() + .map(row -> row[1] != null ? ((Number) row[1]).longValue() : 0L) + .collect(Collectors.toList()); + } + + private List extractFileSizes(List userStats) { + return userStats.stream() + .map(row -> row[2] != null ? ((Number) row[2]).longValue() : 0L) + .collect(Collectors.toList()); + } + + private ProtectedFilesStatisticResponse buildResponse( + Long totalFiles, + Long totalSize, + PercentileCalculator.PercentilesResult filesPercentiles, + PercentileCalculator.PercentilesResult sizePercentiles) { + + ProtectedFilesStatisticResponse.Percentiles filesPerUser = + new ProtectedFilesStatisticResponse.Percentiles( + filesPercentiles.getP5(), + filesPercentiles.getP50(), + filesPercentiles.getP95()); + + ProtectedFilesStatisticResponse.SizePercentiles sizePerUser = + new ProtectedFilesStatisticResponse.SizePercentiles( + sizePercentiles.getP5(), + sizePercentiles.getP50(), + sizePercentiles.getP95(), + convertBytesToMb(sizePercentiles.getP5()), + convertBytesToMb(sizePercentiles.getP50()), + convertBytesToMb(sizePercentiles.getP95())); + + return ProtectedFilesStatisticResponse.builder() + .totalFiles(totalFiles) + .totalSizeBytes(totalSize) + .totalSizeGb(convertBytesToGb(totalSize)) + .filesPerUser(filesPerUser) + .sizePerUser(sizePerUser) + .build(); + } + + private Double convertBytesToMb(Long bytes) { + if (bytes == null || bytes == 0) { + return 0.0; + } + return bytes / BYTES_TO_MB; + } + + private Double convertBytesToGb(Long bytes) { + if (bytes == null || bytes == 0) { + return 0.0; + } + return bytes / BYTES_TO_GB; + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/statistic/SubscriberStatisticService.java b/src/main/java/ru/soune/nocopy/service/statistic/SubscriberStatisticService.java new file mode 100644 index 0000000..40d94ab --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/statistic/SubscriberStatisticService.java @@ -0,0 +1,118 @@ +package ru.soune.nocopy.service.statistic; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.soune.nocopy.dto.statistic.SubscriberStatisticResponse; +import ru.soune.nocopy.entity.tarif.TariffInfo; +import ru.soune.nocopy.entity.tarif.TariffTimeTerm; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.repository.TariffInfoRepository; +import ru.soune.nocopy.repository.UserRepository; + +import java.util.*; + +@Service +@RequiredArgsConstructor +@Slf4j +public class SubscriberStatisticService { + + private final TariffInfoRepository tariffInfoRepository; + private final UserRepository userRepository; + + public SubscriberStatisticResponse getSubscriberStatistics() { + return SubscriberStatisticResponse.builder() + .monthly(buildPeriodStatistics(TariffTimeTerm.MONTHLY)) + .yearly(buildPeriodStatistics(TariffTimeTerm.YEAR)) + .build(); + } + + private SubscriberStatisticResponse.PeriodStatistics buildPeriodStatistics(TariffTimeTerm term) { + Long currentActive = tariffInfoRepository.getCurrentActiveSubscriptions(term); + + List allTariffs = tariffInfoRepository.getAllTariffInfosForAnalysis(term); + + Map> tariffsByUser = groupTariffsByUser(allTariffs); + + Long firstTime = countFirstTimeSubscriptions(tariffsByUser); + Long renewals = countRenewals(tariffsByUser); + UpDownGradeResult gradeResult = analyzeUpgradesAndDowngrades(tariffsByUser); + + return SubscriberStatisticResponse.PeriodStatistics.builder() + .currentActiveSubscriptions(currentActive != null ? currentActive : 0L) + .firstTimeSubscriptions(firstTime) + .renewals(renewals) + .upgrades(gradeResult.upgrades) + .downgrades(gradeResult.downgrades) + .build(); + } + + private Map> groupTariffsByUser(List tariffs) { + Map> result = new HashMap<>(); + + for (TariffInfo ti : tariffs) { + Optional userOpt = userRepository.findByPersonalTariffInfoId(ti.getId()); + if (userOpt.isPresent()) { + Long userId = userOpt.get().getId(); + result.computeIfAbsent(userId, k -> new ArrayList<>()).add(ti); + } + } + + return result; + } + + private Long countFirstTimeSubscriptions(Map> tariffsByUser) { + return (long) tariffsByUser.size(); + } + + private Long countRenewals(Map> tariffsByUser) { + return tariffsByUser.values().stream() + .filter(list -> list.size() > 1) + .mapToLong(list -> list.size() - 1) + .sum(); + } + + private UpDownGradeResult analyzeUpgradesAndDowngrades(Map> tariffsByUser) { + long upgrades = 0L; + long downgrades = 0L; + + for (List userTariffs : tariffsByUser.values()) { + if (userTariffs.size() < 2) { + continue; + } + + userTariffs.sort(Comparator.comparing(TariffInfo::getStartTariff, + Comparator.nullsLast(Comparator.naturalOrder()))); + + for (int i = 1; i < userTariffs.size(); i++) { + TariffInfo previous = userTariffs.get(i - 1); + TariffInfo current = userTariffs.get(i); + + if (previous.getTariff() == null || current.getTariff() == null) { + continue; + } + + double previousPrice = previous.getTariff().getPrice(); + double currentPrice = current.getTariff().getPrice(); + + if (currentPrice > previousPrice) { + upgrades++; + } else if (currentPrice < previousPrice) { + downgrades++; + } + } + } + + return new UpDownGradeResult(upgrades, downgrades); + } + + private static class UpDownGradeResult { + final Long upgrades; + final Long downgrades; + + UpDownGradeResult(Long upgrades, Long downgrades) { + this.upgrades = upgrades; + this.downgrades = downgrades; + } + } +} diff --git a/src/main/java/ru/soune/nocopy/service/statistic/TokenStatisticService.java b/src/main/java/ru/soune/nocopy/service/statistic/TokenStatisticService.java new file mode 100644 index 0000000..471127f --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/statistic/TokenStatisticService.java @@ -0,0 +1,65 @@ +package ru.soune.nocopy.service.statistic; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.soune.nocopy.dto.statistic.TokenStatisticResponse; +import ru.soune.nocopy.repository.PaymentRepository; +import ru.soune.nocopy.repository.TokenOperationRepository; +import ru.soune.nocopy.util.PercentileCalculator; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +public class TokenStatisticService { + private final PaymentRepository paymentRepository; + + private final TokenOperationRepository tokenOperationRepository; + + private static final int PERCENTILE_5 = 5; + + private static final int PERCENTILE_50 = 50; + + private static final int PERCENTILE_95 = 95; + + public TokenStatisticResponse getTokenStatistics() { + Long totalBought = paymentRepository.getTotalTokensBought(); + Long totalSpent = tokenOperationRepository.getTotalTokensSpent(); + + List boughtData = paymentRepository.getTokensBoughtPerUser(); + List boughtValues = extractTokenValues(boughtData); + + List spentData = tokenOperationRepository.getTokensSpentPerUser(); + List spentValues = extractTokenValues(spentData); + + PercentileCalculator.PercentilesResult boughtPercentiles = + PercentileCalculator.calculatePercentiles(boughtValues, PERCENTILE_5, PERCENTILE_50, PERCENTILE_95); + PercentileCalculator.PercentilesResult spentPercentiles = + PercentileCalculator.calculatePercentiles(spentValues, PERCENTILE_5, PERCENTILE_50, PERCENTILE_95); + + return TokenStatisticResponse.builder() + .totalBought(totalBought != null ? totalBought : 0L) + .totalSpent(totalSpent != null ? totalSpent : 0L) + .boughtPerUser(new TokenStatisticResponse.Percentiles( + boughtPercentiles.getP5(), + boughtPercentiles.getP50(), + boughtPercentiles.getP95() + )) + .spentPerUser(new TokenStatisticResponse.Percentiles( + spentPercentiles.getP5(), + spentPercentiles.getP50(), + spentPercentiles.getP95() + )) + .build(); + } + + private List extractTokenValues(List data) { + return data.stream() + .map(row -> row[1] != null ? ((Number) row[1]).longValue() : 0L) + .filter(value -> value > 0) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/statistic/UserDynamicStatisticService.java b/src/main/java/ru/soune/nocopy/service/statistic/UserDynamicStatisticService.java new file mode 100644 index 0000000..fa711a7 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/statistic/UserDynamicStatisticService.java @@ -0,0 +1,46 @@ +package ru.soune.nocopy.service.statistic; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.soune.nocopy.dto.statistic.UserDynamicStatisticResponse; +import ru.soune.nocopy.repository.UserRepository; + +@Service +@RequiredArgsConstructor +@Slf4j +public class UserDynamicStatisticService { + private final UserRepository userRepository; + + public UserDynamicStatisticResponse getUserDynamicStatistics() { + Long totalUsers = userRepository.getTotalUsersCount(); + Long newLast30Days = userRepository.getNewUsersCount(30); + Long totalUpTo60Days = userRepository.getNewUsersCount(60); + Long newPrevious30Days = totalUpTo60Days - newLast30Days; + Double growthPercent = calculateGrowthPercent(newLast30Days, newPrevious30Days); + Long newCurrentMonth = userRepository.getNewUsersCurrentMonth(); + Long newPreviousMonth = userRepository.getNewUsersPreviousMonth(); + + Double monthGrowthPercent = calculateGrowthPercent(newCurrentMonth, newPreviousMonth); + + return UserDynamicStatisticResponse.builder() + .totalUsers(totalUsers != null ? totalUsers : 0L) + .newLast30Days(newLast30Days != null ? newLast30Days : 0L) + .newPrevious30Days(newPrevious30Days != null ? newPrevious30Days : 0L) + .growthPercent(growthPercent) + .newCurrentMonth(newCurrentMonth != null ? newCurrentMonth : 0L) + .newPreviousMonth(newPreviousMonth != null ? newPreviousMonth : 0L) + .monthGrowthPercent(monthGrowthPercent) + .build(); + } + + private Double calculateGrowthPercent(Long current, Long previous) { + if (previous == null || previous == 0) { + return current != null && current > 0 ? 100.0 : 0.0; + } + + double percent = ((current - previous) * 100.0) / previous; + + return Math.round(percent * 100.0) / 100.0; + } +} diff --git a/src/main/java/ru/soune/nocopy/service/statistic/ViolationStatisticService.java b/src/main/java/ru/soune/nocopy/service/statistic/ViolationStatisticService.java new file mode 100644 index 0000000..e56c995 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/statistic/ViolationStatisticService.java @@ -0,0 +1,40 @@ +package ru.soune.nocopy.service.statistic; + + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.soune.nocopy.dto.statistic.ViolationStatisticResponse; +import ru.soune.nocopy.repository.ComplaintEntityRepository; +import ru.soune.nocopy.repository.LawCaseRepository; +import ru.soune.nocopy.repository.ViolationRepository; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ViolationStatisticService { + private final ViolationRepository violationRepository; + + private final ComplaintEntityRepository complaintRepository; + + private final LawCaseRepository lawCaseRepository; + + public ViolationStatisticResponse getViolationStatistics() { + Long totalViolations = violationRepository.getTotalViolationsCount(); + Long totalComplaints = complaintRepository.getTotalComplaintsCount(); + Long totalLawCases = lawCaseRepository.getTotalLawCasesCount(); + Long complaintsWithoutCase = complaintRepository.getComplaintsWithoutLawCaseCount(); + Long violationsWithComplaint = violationRepository.getViolationsWithComplaintCount(); + + Long violationsWithoutComplaint = totalViolations - violationsWithComplaint; + + return ViolationStatisticResponse.builder() + .totalViolations(totalViolations != null ? totalViolations : 0L) + .totalComplaints(totalComplaints != null ? totalComplaints : 0L) + .totalLawCases(totalLawCases != null ? totalLawCases : 0L) + .complaintsWithoutCase(complaintsWithoutCase != null ? complaintsWithoutCase : 0L) + .violationsWithComplaint(violationsWithComplaint != null ? violationsWithComplaint : 0L) + .violationsWithoutComplaint(violationsWithoutComplaint) + .build(); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/tariff/TariffConstants.java b/src/main/java/ru/soune/nocopy/service/tariff/TariffConstants.java index 14c08c4..6df1fdf 100644 --- a/src/main/java/ru/soune/nocopy/service/tariff/TariffConstants.java +++ b/src/main/java/ru/soune/nocopy/service/tariff/TariffConstants.java @@ -18,4 +18,5 @@ public class TariffConstants { public static final Integer AUDIO_TOKEN_VALUE = 8; public static final Integer PDF_TOKEN_VALUE = 6; public static final Integer TOKEN_VALUE_FOR_SEARCH = 5; + public static final Integer LEGAL_COST = 100; } 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 50fba93..6e91ed5 100644 --- a/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java +++ b/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java @@ -10,12 +10,14 @@ 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.tarif.TariffType; +import ru.soune.nocopy.entity.tokenoperation.OperationType; import ru.soune.nocopy.entity.user.User; 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 ru.soune.nocopy.service.tokenoperation.TokenOperationService; import java.io.IOException; import java.time.LocalDateTime; @@ -35,6 +37,8 @@ public class TariffInfoService { private EmailService emailService; + private TokenOperationService tokenOperationService; + public TariffInfo createTariffInfo(LocalDateTime startTariff, LocalDateTime endTariff, TariffStatus tariffStatus, Integer tokens) { TariffInfo tariffInfo = new TariffInfo(); @@ -48,7 +52,7 @@ public class TariffInfoService { } // @Transactional - public void writeOffTokens(long userId, Integer value) throws MessagingException, IOException { + public void writeOffTokens(long userId, Integer value, OperationType operationType) throws MessagingException, IOException { User user = userRepository.findById(userId).orElseThrow(); TariffInfo activeTariffInfo; @@ -60,6 +64,8 @@ public class TariffInfoService { } writeOffTokensForTariff(activeTariffInfo, value); + + tokenOperationService.create(operationType, userId, Long.valueOf(value)); } public void writeOffTokensForTariff(TariffInfo activeTariffInfo, Integer value) throws MessagingException, IOException { diff --git a/src/main/java/ru/soune/nocopy/service/tokenoperation/TokenOperationService.java b/src/main/java/ru/soune/nocopy/service/tokenoperation/TokenOperationService.java new file mode 100644 index 0000000..f56efab --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/tokenoperation/TokenOperationService.java @@ -0,0 +1,122 @@ +package ru.soune.nocopy.service.tokenoperation; + +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.entity.tokenoperation.OperationType; +import ru.soune.nocopy.entity.tokenoperation.TokenOperation; +import ru.soune.nocopy.repository.TokenOperationRepository; + +import jakarta.persistence.EntityNotFoundException; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class TokenOperationService { + + private final TokenOperationRepository repository; + + @Transactional + public TokenOperation create(OperationType operationType, Long userId, Long spent) { + TokenOperation tokenOperation = new TokenOperation(); + + tokenOperation.setOperationType(operationType); + tokenOperation.setUserId(userId); + tokenOperation.setSpent(spent); + tokenOperation.setCreatedAt(LocalDateTime.now()); + + return repository.save(tokenOperation); + } + + public TokenOperation findById(Long id) { + log.debug("Finding token operation by id: {}", id); + return repository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("TokenOperation not found with id: " + id)); + } + + public Page findAll(int page, int size, String sortBy, String direction) { + log.debug("Finding all operations with pagination: page={}, size={}, sortBy={}, dir={}", + page, size, sortBy, direction); + + Sort sort = direction.equalsIgnoreCase("desc") + ? Sort.by(sortBy).descending() + : Sort.by(sortBy).ascending(); + + Pageable pageable = PageRequest.of(page, size, sort); + return repository.findAll(pageable); + } + + public Page findByFilters( + Long userId, + OperationType operationType, + Long minSpent, + Long maxSpent, + int page, + int size, + String sortBy, + String direction) { + + log.debug("Finding by filters: userId={}, type={}, minSpent={}, maxSpent={}", + userId, operationType, minSpent, maxSpent); + + Sort sort = Sort.by(Sort.Direction.fromString(direction), sortBy); + Pageable pageable = PageRequest.of(page, size, sort); + + return repository.findByFilters(userId, operationType, minSpent, maxSpent, pageable); + } + + public Page findByUserId(Long userId, int page, int size) { + log.debug("Finding operations for user id: {}", userId); + Pageable pageable = PageRequest.of(page, size, Sort.by("id").descending()); + return repository.findByUserId(userId, pageable); + } + + @Transactional + public TokenOperation update(Long id, TokenOperation updatedOperation) { + log.info("Updating token operation with id: {}", id); + + TokenOperation existing = findById(id); + + existing.setOperationType(updatedOperation.getOperationType()); + existing.setUserId(updatedOperation.getUserId()); + existing.setSpent(updatedOperation.getSpent()); + + return repository.save(existing); + } + + @Transactional + public void deleteById(Long id) { + log.info("Deleting token operation with id: {}", id); + + if (!repository.existsById(id)) { + throw new EntityNotFoundException("TokenOperation not found with id: " + id); + } + repository.deleteById(id); + } + + @Transactional + public void deleteByUserId(Long userId) { + log.warn("Deleting all operations for user id: {}", userId); + List userOps = repository.findByUserId(userId); + repository.deleteAll(userOps); + } + + public Long getTotalSpentByUserAndType(Long userId, OperationType type) { + Long sum = repository.sumSpentByUserAndType(userId, type); + return sum != null ? sum : 0L; + } + + public Long countByUserId(Long userId) { + return repository.countByUserId(userId); + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/user/moderation/UserVerificationService.java b/src/main/java/ru/soune/nocopy/service/user/moderation/UserVerificationService.java new file mode 100644 index 0000000..e22655b --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/user/moderation/UserVerificationService.java @@ -0,0 +1,69 @@ +package ru.soune.nocopy.service.user.moderation; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.soune.nocopy.entity.user.ModerationStatus; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.entity.user.moderation.UserVerification; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.repository.UserVerificationRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class UserVerificationService { + + private final UserRepository userRepository; + + private final UserVerificationRepository userVerificationRepository; + + public void initUserVerification(Long userId) { + User user = userRepository.findById(userId).orElse(null); + + if (user == null) return; + + UserVerification userVerification = UserVerification.builder() + .moderationStatus(ModerationStatus.VERIFICATION_IN_PROGRESS) + .userId(user.getId()) + .createdAt(LocalDateTime.now()) + .build(); + + user.setVerificationStatus(ModerationStatus.VERIFICATION_IN_PROGRESS); + + userRepository.save(user); + + userVerificationRepository.save(userVerification); + } + + public UserVerification verifyUser(Long userId, String message, Long adminId, boolean verified) { + User user = userRepository.findById(userId).orElse(null); + + if (user == null) return null; + + UserVerification userVerification = userVerificationRepository.findById(userId).orElse(null); + + if (userVerification == null) return null; + + ModerationStatus moderationStatus = verified ? ModerationStatus.VERIFIED: ModerationStatus.VERIFICATION_FAILED; + + userVerification.setModerationStatus(moderationStatus); + + if (message != null) userVerification.setMessage(message); + + userVerification.setAdminId(adminId); + + UserVerification save = userVerificationRepository.save(userVerification); + + user.setVerificationStatus(moderationStatus); + + userRepository.save(user); + + return save; + } + + public List verifications() { + return userVerificationRepository.findAll(); + } +} 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 8f8fad3..65fc559 100644 --- a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java @@ -191,7 +191,6 @@ public class ViolationService { } Set inProgressStatuses = Set.of( - ViolationStatus.SHOWED.name(), ViolationStatus.LEGAL_IN_WORK.name(), ViolationStatus.COMPLAINT_IN_WORK.name(), ViolationStatus.COMPLAINT_AND_LEGAL_IN_WORK.name() diff --git a/src/main/java/ru/soune/nocopy/util/PercentileCalculator.java b/src/main/java/ru/soune/nocopy/util/PercentileCalculator.java new file mode 100644 index 0000000..9063390 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/util/PercentileCalculator.java @@ -0,0 +1,80 @@ +package ru.soune.nocopy.util; + +import lombok.experimental.UtilityClass; + +import java.util.List; +import java.util.stream.Collectors; + +@UtilityClass +public class PercentileCalculator { + + /** + * Рассчитать несколько перцентилей за один проход + * + * @param values список значений + * @param percentiles массив перцентилей + * @return PercentilesResult с соответствующими значениями + */ + public PercentilesResult calculatePercentiles(List values, int... percentiles) { + if (values == null || values.isEmpty()) { + return new PercentilesResult(0L, 0L, 0L); + } + + List sorted = values.stream() + .sorted() + .collect(Collectors.toList()); + + Long p5 = percentiles.length > 0 ? calculatePercentileFromSorted(sorted, percentiles[0]) : 0L; + Long p50 = percentiles.length > 1 ? calculatePercentileFromSorted(sorted, percentiles[1]) : 0L; + Long p95 = percentiles.length > 2 ? calculatePercentileFromSorted(sorted, percentiles[2]) : 0L; + + return new PercentilesResult(p5, p50, p95); + } + + public PercentilesResult calculateP5P50P95(List values) { + return calculatePercentiles(values, 5, 50, 95); + } + + private Long calculatePercentileFromSorted(List sorted, double percentile) { + if (sorted.isEmpty()) { + return 0L; + } + + if (percentile <= 0) { + return sorted.get(0); + } + if (percentile >= 100) { + return sorted.get(sorted.size() - 1); + } + + double index = (percentile / 100.0) * (sorted.size() - 1); + int lowerIndex = (int) Math.floor(index); + int upperIndex = (int) Math.ceil(index); + + if (lowerIndex == upperIndex) { + return sorted.get(lowerIndex); + } + + double weight = index - lowerIndex; + long lowerValue = sorted.get(lowerIndex); + long upperValue = sorted.get(upperIndex); + + return lowerValue + Math.round(weight * (upperValue - lowerValue)); + } + + public static class PercentilesResult { + private final Long p5; + private final Long p50; + private final Long p95; + + public PercentilesResult(Long p5, Long p50, Long p95) { + this.p5 = p5; + this.p50 = p50; + this.p95 = p95; + } + + public Long getP5() { return p5; } + public Long getP50() { return p50; } + public Long getP95() { return p95; } + } +} \ No newline at end of file diff --git a/src/main/resources/geo/GeoLite2-Country.mmdb b/src/main/resources/geo/GeoLite2-Country.mmdb new file mode 100644 index 0000000..e47ec55 Binary files /dev/null and b/src/main/resources/geo/GeoLite2-Country.mmdb differ diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties deleted file mode 100644 index d776852..0000000 --- a/src/main/resources/messages.properties +++ /dev/null @@ -1,19 +0,0 @@ -notification.search.result=Результат поиска готов -notification.monitoring.result=Результат мониторинга доступен -notification.operation.impossible=Операция невозможна -notification.start.failed.now=Запуск не удался -notification.start.failed.next=Запуск не удался -notification.payout.result=Результат выплаты {0} -notification.payment.result=Результат оплаты {0} -notification.referral.deposit=Реферальный депозит -notification.referral.registered=Новый реферал зарегистрирован -notification.referral.activated=Реферал активирован -notification.complaint.status=Статус жалобы изменён: {0} -notification.case.status=Статус дела изменён -notification.incoming.message=Новое сообщение -notification.tariff.expiring=Тариф скоро истекает -notification.token.not.found=Недостаточно токенов для операции: {0} -notification.file.moderation=Файл проходит модерацию -notification.file.added=Файл {0} добавлен в систему -notification.file.search=Поиск файла -notification.search.operation.filed=Поиск завершился непредвиденной ошибкой diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties deleted file mode 100644 index 7d94e2d..0000000 --- a/src/main/resources/messages_en.properties +++ /dev/null @@ -1,15 +0,0 @@ -notification.search.result=Search result is ready -notification.monitoring.result=Monitoring result is available -notification.operation.impossible=Operation impossible -notification.start.failed.now=Start failed -notification.start.failed.next=Start failed -notification.payout.result=Payout result -notification.referral.deposit=Referral deposit -notification.referral.registered=New referral registered -notification.referral.activated=Referral activated -notification.complaint.status=Complaint status changed -notification.case.status=Case status changed -notification.incoming.message=New message -notification.tariff.expiring=Tariff expiring soon -notification.file.moderation=File is being moderated -notification.file.added=File {0} added to the system \ No newline at end of file