From aa6c0c1b94a05de572b7ec14e2b30166d62c4240 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 10 Apr 2026 20:13:59 +0700 Subject: [PATCH] dev add token spent logic --- README.md | 2 +- .../nocopy/configuration/HandlerConfig.java | 4 +- .../nocopy/controller/PaymentController.java | 2 +- .../java/ru/soune/nocopy/dto/MessageCode.java | 1 + .../tokenoperation/TokenOperationPageDto.java | 34 ++ .../tokenoperation/TokenOperationRequest.java | 46 ++ .../TokenOperationResponse.java | 37 ++ .../entity/tokenoperation/OperationType.java | 12 + .../entity/tokenoperation/TokenOperation.java | 32 ++ .../handler/ImageFoundRequestHandler.java | 4 +- .../soune/nocopy/handler/LawCaseHandler.java | 3 +- .../nocopy/handler/TokenOperationHandler.java | 472 ++++++++++++++++++ .../repository/TokenOperationRepository.java | 41 ++ .../file/impl/FileUploadServiceImpl.java | 3 +- .../monitoring/MonitoringSearchService.java | 3 +- .../search/GlobalSearchAsyncProcessor.java | 3 +- .../service/tariff/TariffInfoService.java | 8 +- .../tokenoperation/TokenOperationService.java | 122 +++++ 18 files changed, 820 insertions(+), 9 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/tokenoperation/TokenOperationPageDto.java create mode 100644 src/main/java/ru/soune/nocopy/dto/tokenoperation/TokenOperationRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/tokenoperation/TokenOperationResponse.java create mode 100644 src/main/java/ru/soune/nocopy/entity/tokenoperation/OperationType.java create mode 100644 src/main/java/ru/soune/nocopy/entity/tokenoperation/TokenOperation.java create mode 100644 src/main/java/ru/soune/nocopy/handler/TokenOperationHandler.java create mode 100644 src/main/java/ru/soune/nocopy/repository/TokenOperationRepository.java create mode 100644 src/main/java/ru/soune/nocopy/service/tokenoperation/TokenOperationService.java diff --git a/README.md b/README.md index 7e64986..ffac0f8 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/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index 7a66c3f..dbb0477 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -37,7 +37,8 @@ public class HandlerConfig { UserInfoHandler userInfoHandler, NotificationHandler notificationHandler, UserVerificationHandler userVerificationHandler, - LawCaseHandler lawCaseHandler + LawCaseHandler lawCaseHandler, + TokenOperationHandler tokenOperationHandler ) { Map map = new HashMap<>(); map.put(20001, login); @@ -66,6 +67,7 @@ public class HandlerConfig { map.put(30015, notificationHandler); map.put(30016, userVerificationHandler); map.put(30017, lawCaseHandler); + map.put(30018, tokenOperationHandler); return map; } 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/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index b69e47b..dae6f55 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -48,6 +48,7 @@ public enum MessageCode { 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"), 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/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/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 index fe0e112..e6e49c5 100644 --- a/src/main/java/ru/soune/nocopy/handler/LawCaseHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/LawCaseHandler.java @@ -15,6 +15,7 @@ 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; @@ -114,7 +115,7 @@ public class LawCaseHandler implements RequestHandler { private BaseResponse createLawCase(Long userId, LawCaseRequest lawCaseRequest, BaseRequest request) { try { - tariffInfoService.writeOffTokens(userId, TariffConstants.LEGAL_COST); + 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(), 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/repository/TokenOperationRepository.java b/src/main/java/ru/soune/nocopy/repository/TokenOperationRepository.java new file mode 100644 index 0000000..0a39c38 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/TokenOperationRepository.java @@ -0,0 +1,41 @@ +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); +} 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 7c489f4..85751cd 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 @@ -18,6 +18,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; @@ -441,7 +442,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/monitoring/MonitoringSearchService.java b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java index 755c83d..c93ac5b 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; @@ -66,7 +67,7 @@ public class MonitoringSearchService { TariffDTO tariffMonitoring = tariffService.getTariffByType(TariffType.valueOf(monitoringType.name())); 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(); 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/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