dev add statistic files protected
Test Workflow / test (push) Successful in 3s

This commit is contained in:
vladp
2026-04-15 18:12:32 +07:00
parent 1b921b525a
commit 2d80b40b27
12 changed files with 467 additions and 1 deletions
@@ -40,7 +40,10 @@ public class HandlerConfig {
LawCaseHandler lawCaseHandler,
TokenOperationHandler tokenOperationHandler,
StatisticUserFilesHandler statisticUserHandler,
StatisticTariffInfoFileHandler statisticTariffInfoFileHandler
StatisticTariffInfoFileHandler statisticTariffInfoFileHandler,
StatisticUserDynamicHandler statisticUserDynamicHandler,
StatisticProtectedFilesHandler statisticProtectedFilesHandler
) {
Map<Integer, RequestHandler> 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;
}
@@ -0,0 +1,7 @@
package ru.soune.nocopy.dto.statistic;
import lombok.Data;
@Data
public class ProtectedFilesStatisticRequest {
}
@@ -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;
}
}
@@ -0,0 +1,7 @@
package ru.soune.nocopy.dto.statistic;
import lombok.Data;
@Data
public class UserDynamicStatisticRequest {
}
@@ -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;
}
@@ -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);
}
}
@@ -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);
}
}
@@ -116,4 +116,25 @@ public interface FileEntityRepository extends JpaRepository<FileEntity, String>
@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<Object[]> 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<Object[]> getProtectedFilesPerUserStats();
}
@@ -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<User, Long> {
List<User> 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();
}
@@ -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<Object[]> totalStats = fileEntityRepository.getProtectedFilesTotalStats();
Long totalFiles = extractTotalFiles(totalStats);
Long totalSize = extractTotalSize(totalStats);
List<Object[]> userStats = fileEntityRepository.getProtectedFilesPerUserStats();
List<Long> fileCounts = extractFileCounts(userStats);
List<Long> 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<Object[]> 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<Object[]> 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<Long> extractFileCounts(List<Object[]> userStats) {
return userStats.stream()
.map(row -> row[1] != null ? ((Number) row[1]).longValue() : 0L)
.collect(Collectors.toList());
}
private List<Long> extractFileSizes(List<Object[]> 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;
}
}
@@ -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;
}
}
@@ -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<Long> values, int... percentiles) {
if (values == null || values.isEmpty()) {
return new PercentilesResult(0L, 0L, 0L);
}
List<Long> 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<Long> values) {
return calculatePercentiles(values, 5, 50, 95);
}
private Long calculatePercentileFromSorted(List<Long> 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; }
}
}