From 42a53680031fa54836a339b958f2487addbcf4f5 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 16 Apr 2026 00:28:27 +0700 Subject: [PATCH] dev add statistic files protected --- .../nocopy/configuration/HandlerConfig.java | 4 +- .../statistic/SubscriberStatisticRequest.java | 7 + .../SubscriberStatisticResponse.java | 42 ++++++ .../soune/nocopy/entity/tarif/TariffInfo.java | 1 + .../handler/StatisticSubscriberHandler.java | 29 +++++ .../repository/TariffInfoRepository.java | 16 +++ .../nocopy/repository/UserRepository.java | 5 + .../statistic/SubscriberStatisticService.java | 121 ++++++++++++++++++ 8 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/statistic/SubscriberStatisticRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/statistic/SubscriberStatisticResponse.java create mode 100644 src/main/java/ru/soune/nocopy/handler/StatisticSubscriberHandler.java create mode 100644 src/main/java/ru/soune/nocopy/service/statistic/SubscriberStatisticService.java diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index c7cfec7..8b85370 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -43,7 +43,8 @@ public class HandlerConfig { StatisticTariffInfoFileHandler statisticTariffInfoFileHandler, StatisticUserDynamicHandler statisticUserDynamicHandler, StatisticProtectedFilesHandler statisticProtectedFilesHandler, - StatisticViolationHandler statisticViolationHandler + StatisticViolationHandler statisticViolationHandler, + StatisticSubscriberHandler statisticSubscriberHandler ) { Map map = new HashMap<>(); @@ -79,6 +80,7 @@ public class HandlerConfig { map.put(30021, statisticUserDynamicHandler); map.put(30022, statisticProtectedFilesHandler); map.put(30023, statisticViolationHandler); + map.put(30024, statisticSubscriberHandler); return map; } 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/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/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/repository/TariffInfoRepository.java b/src/main/java/ru/soune/nocopy/repository/TariffInfoRepository.java index f9a256d..a03da52 100644 --- a/src/main/java/ru/soune/nocopy/repository/TariffInfoRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/TariffInfoRepository.java @@ -50,4 +50,20 @@ public interface TariffInfoRepository extends JpaRepository 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") String term); + + @Query(""" + SELECT ti + FROM TariffInfo ti + WHERE (:term IS NULL OR ti.tariff.tariffTerm = :term) + ORDER BY ti.startTariff + """) + List getAllTariffInfosForAnalysis(@Param("term") String term); } diff --git a/src/main/java/ru/soune/nocopy/repository/UserRepository.java b/src/main/java/ru/soune/nocopy/repository/UserRepository.java index c5aec5c..df28e66 100644 --- a/src/main/java/ru/soune/nocopy/repository/UserRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/UserRepository.java @@ -3,10 +3,12 @@ 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); @@ -40,4 +42,7 @@ public interface UserRepository extends JpaRepository { 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/service/statistic/SubscriberStatisticService.java b/src/main/java/ru/soune/nocopy/service/statistic/SubscriberStatisticService.java new file mode 100644 index 0000000..86c6622 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/statistic/SubscriberStatisticService.java @@ -0,0 +1,121 @@ +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.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; + + private static final String TERM_MONTHLY = "MONTHLY"; + + private static final String TERM_YEARLY = "YEAR"; + + public SubscriberStatisticResponse getSubscriberStatistics() { + return SubscriberStatisticResponse.builder() + .monthly(buildPeriodStatistics(TERM_MONTHLY)) + .yearly(buildPeriodStatistics(TERM_YEARLY)) + .build(); + } + + private SubscriberStatisticResponse.PeriodStatistics buildPeriodStatistics(String 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; + } + } +}