This commit is contained in:
@@ -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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user