From 2d80b40b270e4e53abe1d716bf3eadf8c485041e Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 15 Apr 2026 18:12:32 +0700 Subject: [PATCH] dev add statistic files protected --- .../nocopy/configuration/HandlerConfig.java | 7 +- .../ProtectedFilesStatisticRequest.java | 7 ++ .../ProtectedFilesStatisticResponse.java | 66 ++++++++++ .../UserDynamicStatisticRequest.java | 7 ++ .../UserDynamicStatisticResponse.java | 33 +++++ .../StatisticProtectedFilesHandler.java | 28 +++++ .../handler/StatisticUserDynamicHandler.java | 29 +++++ .../repository/FileEntityRepository.java | 21 ++++ .../nocopy/repository/UserRepository.java | 26 ++++ .../ProtectedFilesStatisticService.java | 118 ++++++++++++++++++ .../UserDynamicStatisticService.java | 46 +++++++ .../nocopy/util/PercentileCalculator.java | 80 ++++++++++++ 12 files changed, 467 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/statistic/ProtectedFilesStatisticRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/statistic/ProtectedFilesStatisticResponse.java create mode 100644 src/main/java/ru/soune/nocopy/dto/statistic/UserDynamicStatisticRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/statistic/UserDynamicStatisticResponse.java create mode 100644 src/main/java/ru/soune/nocopy/handler/StatisticProtectedFilesHandler.java create mode 100644 src/main/java/ru/soune/nocopy/handler/StatisticUserDynamicHandler.java create mode 100644 src/main/java/ru/soune/nocopy/service/statistic/ProtectedFilesStatisticService.java create mode 100644 src/main/java/ru/soune/nocopy/service/statistic/UserDynamicStatisticService.java create mode 100644 src/main/java/ru/soune/nocopy/util/PercentileCalculator.java diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index 343fd03..a582f44 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -40,7 +40,10 @@ public class HandlerConfig { LawCaseHandler lawCaseHandler, TokenOperationHandler tokenOperationHandler, StatisticUserFilesHandler statisticUserHandler, - StatisticTariffInfoFileHandler statisticTariffInfoFileHandler + StatisticTariffInfoFileHandler statisticTariffInfoFileHandler, + StatisticUserDynamicHandler statisticUserDynamicHandler, + StatisticProtectedFilesHandler statisticProtectedFilesHandler + ) { Map map = new HashMap<>(); map.put(20001, login); @@ -72,6 +75,8 @@ public class HandlerConfig { map.put(30018, tokenOperationHandler); map.put(30019, statisticUserHandler); map.put(30020, statisticTariffInfoFileHandler); + map.put(30021, statisticUserDynamicHandler); + map.put(30022, statisticProtectedFilesHandler); return map; } 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/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/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/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/repository/FileEntityRepository.java b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java index d0cdce6..2476b5f 100644 --- a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java @@ -116,4 +116,25 @@ public interface FileEntityRepository extends JpaRepository @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/UserRepository.java b/src/main/java/ru/soune/nocopy/repository/UserRepository.java index 8c7e31c..c5aec5c 100644 --- a/src/main/java/ru/soune/nocopy/repository/UserRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/UserRepository.java @@ -2,6 +2,7 @@ package ru.soune.nocopy.repository; import jakarta.validation.constraints.Size; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import ru.soune.nocopy.entity.tarif.TariffInfo; import ru.soune.nocopy.entity.user.User; @@ -14,4 +15,29 @@ 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(); } 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/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/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