This commit is contained in:
@@ -43,7 +43,8 @@ public class HandlerConfig {
|
|||||||
StatisticTariffInfoFileHandler statisticTariffInfoFileHandler,
|
StatisticTariffInfoFileHandler statisticTariffInfoFileHandler,
|
||||||
StatisticUserDynamicHandler statisticUserDynamicHandler,
|
StatisticUserDynamicHandler statisticUserDynamicHandler,
|
||||||
StatisticProtectedFilesHandler statisticProtectedFilesHandler,
|
StatisticProtectedFilesHandler statisticProtectedFilesHandler,
|
||||||
StatisticViolationHandler statisticViolationHandler
|
StatisticViolationHandler statisticViolationHandler,
|
||||||
|
StatisticSubscriberHandler statisticSubscriberHandler
|
||||||
|
|
||||||
) {
|
) {
|
||||||
Map<Integer, RequestHandler> map = new HashMap<>();
|
Map<Integer, RequestHandler> map = new HashMap<>();
|
||||||
@@ -79,6 +80,7 @@ public class HandlerConfig {
|
|||||||
map.put(30021, statisticUserDynamicHandler);
|
map.put(30021, statisticUserDynamicHandler);
|
||||||
map.put(30022, statisticProtectedFilesHandler);
|
map.put(30022, statisticProtectedFilesHandler);
|
||||||
map.put(30023, statisticViolationHandler);
|
map.put(30023, statisticViolationHandler);
|
||||||
|
map.put(30024, statisticSubscriberHandler);
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ru.soune.nocopy.dto.statistic;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class SubscriberStatisticRequest {
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ public class TariffInfo {
|
|||||||
private String id;
|
private String id;
|
||||||
|
|
||||||
@Column(name = "status", nullable = false)
|
@Column(name = "status", nullable = false)
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
private TariffStatus status;
|
private TariffStatus status;
|
||||||
|
|
||||||
@Column(name = "start_tariff")
|
@Column(name = "start_tariff")
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,4 +50,20 @@ public interface TariffInfoRepository extends JpaRepository<TariffInfo, String>
|
|||||||
ORDER BY user_count DESC
|
ORDER BY user_count DESC
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
List<Object[]> getTariffStatisticsNative(@Param("activeOnly") boolean activeOnly);
|
List<Object[]> 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<TariffInfo> getAllTariffInfosForAnalysis(@Param("term") String term);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package ru.soune.nocopy.repository;
|
|||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
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.tarif.TariffInfo;
|
||||||
import ru.soune.nocopy.entity.user.User;
|
import ru.soune.nocopy.entity.user.User;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface UserRepository extends JpaRepository<User, Long> {
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
User findByEmail(String email);
|
User findByEmail(String email);
|
||||||
@@ -40,4 +42,7 @@ public interface UserRepository extends JpaRepository<User, Long> {
|
|||||||
AND created_at < DATE_TRUNC('month', NOW())
|
AND created_at < DATE_TRUNC('month', NOW())
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
Long getNewUsersPreviousMonth();
|
Long getNewUsersPreviousMonth();
|
||||||
|
|
||||||
|
@Query("SELECT u FROM User u WHERE u.personalTariffInfo.id = :tariffInfoId")
|
||||||
|
Optional<User> findByPersonalTariffInfoId(@Param("tariffInfoId") String tariffInfoId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<TariffInfo> allTariffs = tariffInfoRepository.getAllTariffInfosForAnalysis(term);
|
||||||
|
|
||||||
|
Map<Long, List<TariffInfo>> 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<Long, List<TariffInfo>> groupTariffsByUser(List<TariffInfo> tariffs) {
|
||||||
|
Map<Long, List<TariffInfo>> result = new HashMap<>();
|
||||||
|
|
||||||
|
for (TariffInfo ti : tariffs) {
|
||||||
|
Optional<User> 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<Long, List<TariffInfo>> tariffsByUser) {
|
||||||
|
return (long) tariffsByUser.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long countRenewals(Map<Long, List<TariffInfo>> tariffsByUser) {
|
||||||
|
return tariffsByUser.values().stream()
|
||||||
|
.filter(list -> list.size() > 1)
|
||||||
|
.mapToLong(list -> list.size() - 1)
|
||||||
|
.sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
private UpDownGradeResult analyzeUpgradesAndDowngrades(Map<Long, List<TariffInfo>> tariffsByUser) {
|
||||||
|
long upgrades = 0L;
|
||||||
|
long downgrades = 0L;
|
||||||
|
|
||||||
|
for (List<TariffInfo> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user