admin statistics part4
All checks were successful
Deploy to VPS / deploy (push) Successful in 1m21s

This commit is contained in:
Tihon
2026-03-22 16:43:20 +02:00
parent 26515ab621
commit b2415acdcf
10 changed files with 161 additions and 28 deletions

View File

@@ -36,7 +36,8 @@ public class AdminUserController {
private static final Set<String> SORTABLE_FIELDS = Set.of(
"id", "screenName", "telegramId", "telegramName", "isPremium",
"languageCode", "countryCode", "deviceCode", "dateReg", "dateLogin", "banned",
"balanceA", "depositTotal", "withdrawTotal", "referralCount", "profit"
"balanceA", "balanceB", "depositTotal", "withdrawTotal", "referralCount", "profit",
"referrerTelegramName"
);
private static final Set<String> DEPOSIT_SORT_FIELDS = Set.of("id", "usdAmount", "status", "orderId", "createdAt", "completedAt");
private static final Set<String> WITHDRAWAL_SORT_FIELDS = Set.of("id", "usdAmount", "cryptoName", "amountToSend", "txhash", "status", "paymentId", "createdAt", "resolvedAt");
@@ -68,10 +69,12 @@ public class AdminUserController {
@RequestParam(required = false) Integer referralLevel,
@RequestParam(required = false) String ip,
@RequestParam(required = false) Boolean botActive,
@RequestParam(required = false) Integer depositCountMin) {
@RequestParam(required = false) Integer depositCountMin,
@RequestParam(required = false) Boolean hideSubAndBanned) {
// Build sort. Fields on UserB/UserD (balanceA, depositTotal, withdrawTotal, referralCount) are handled in service via custom query.
Set<String> sortRequiresJoin = Set.of("balanceA", "depositTotal", "withdrawTotal", "referralCount", "profit");
// Build sort. Fields on UserB/UserD (balanceA, balanceB, depositTotal, etc.) are handled in service via custom query.
Set<String> sortRequiresJoin = Set.of(
"balanceA", "balanceB", "depositTotal", "withdrawTotal", "referralCount", "profit", "referrerTelegramName");
String effectiveSortBy = sortBy != null && sortBy.trim().isEmpty() ? null : (sortBy != null ? sortBy.trim() : null);
if (effectiveSortBy != null && sortRequiresJoin.contains(effectiveSortBy)) {
// Pass through; service will use custom ordered query
@@ -113,7 +116,8 @@ public class AdminUserController {
depositCountMin,
effectiveSortBy,
sortDir,
excludeMasters
excludeMasters,
Boolean.TRUE.equals(hideSubAndBanned)
);
Map<String, Object> response = new HashMap<>();

View File

@@ -27,6 +27,8 @@ public class AdminUserDetailDto {
private Integer banned;
/** IP address as string (e.g. xxx.xxx.xxx.xxx), converted from varbinary in DB. */
private String ipAddress;
/** Number of users sharing the same IP (including this user). */
private Long ipAddressUserCount;
// Balance Info
private Long balanceA;
@@ -45,8 +47,10 @@ public class AdminUserDetailDto {
// Referral Info
private Integer referralCount;
private Long totalCommissionsEarned;
/** Total commissions earned in USD (converted from tickets). */
/** Total commissions earned in USD (Honey: bigint / 10_000_000_000). */
private java.math.BigDecimal totalCommissionsEarnedUsd;
/** Sum of COMPLETED payment USD across referral levels 13 for this user. */
private java.math.BigDecimal totalReferralDepositsUsd;
private Integer masterId;
private List<ReferralLevelDto> referralLevels;
}

View File

@@ -31,11 +31,21 @@ public class AdminUserDto {
private Long totalCommissionsEarned; // Total commissions earned from referrals
/** Profit in tickets (bigint): depositTotal - withdrawTotal */
private Long profit;
/** USD from db_users_b: depositTotal (tickets/1000) */
/** USD from db_users_b deposit_total (Honey scale). */
private BigDecimal depositTotalUsd;
/** USD from db_users_b: withdrawTotal (tickets/1000) */
/** USD from db_users_b withdraw_total (Honey scale). */
private BigDecimal withdrawTotalUsd;
/** USD from db_users_b: profit (tickets/1000) */
/** USD from db_users_b: profit (Honey: bigint / 10_000_000_000). */
private BigDecimal profitUsd;
/** True when user has not blocked the bot. */
private Boolean botActive;
/** True when user has a session with created_at &lt;= now and expires_at &gt;= now. */
private Boolean online;
/** Direct referrer (db_users_d.referer_id_1), if any. */
private Integer referrerId;
/** Telegram name of direct referrer; "-" when absent. */
private String referrerTelegramName;
/** (1 - withdrawUsd/depositUsd) * 100 when depositUsd &gt; 0; otherwise null. */
private BigDecimal profitPercent;
}

View File

@@ -12,14 +12,16 @@ import java.math.BigDecimal;
@NoArgsConstructor
@AllArgsConstructor
public class ReferralLevelDto {
private Integer level; // 1-5
private Integer level; // 13 in admin UI; legacy rows may exist in DB
private Integer refererId;
private Integer referralCount;
private Long commissionsEarned;
private Long commissionsPaid;
/** Commissions earned in USD (converted from tickets: 1000 tickets = 1 USD). */
/** Commissions earned in USD (Honey: bigint / 10_000_000_000). */
private BigDecimal commissionsEarnedUsd;
/** Commissions paid in USD (converted from tickets). */
/** Commissions paid in USD (Honey: bigint / 10_000_000_000). */
private BigDecimal commissionsPaidUsd;
/** Sum of COMPLETED payment USD from referrals at this level. */
private BigDecimal depositsUsd;
}

View File

@@ -99,5 +99,15 @@ public interface PaymentRepository extends JpaRepository<Payment, Long>, JpaSpec
@Param("start") Instant start,
@Param("end") Instant end
);
/** Sum COMPLETED payment USD for users whose level-1 referrer is {@code userId}. */
@Query("SELECT COALESCE(SUM(p.usdAmount), 0) FROM Payment p, UserD d WHERE p.userId = d.id AND p.status = 'COMPLETED' AND p.usdAmount IS NOT NULL AND d.refererId1 = :userId")
java.math.BigDecimal sumCompletedUsdForReferralsLevel1(@Param("userId") Integer userId);
@Query("SELECT COALESCE(SUM(p.usdAmount), 0) FROM Payment p, UserD d WHERE p.userId = d.id AND p.status = 'COMPLETED' AND p.usdAmount IS NOT NULL AND d.refererId2 = :userId")
java.math.BigDecimal sumCompletedUsdForReferralsLevel2(@Param("userId") Integer userId);
@Query("SELECT COALESCE(SUM(p.usdAmount), 0) FROM Payment p, UserD d WHERE p.userId = d.id AND p.status = 'COMPLETED' AND p.usdAmount IS NOT NULL AND d.refererId3 = :userId")
java.math.BigDecimal sumCompletedUsdForReferralsLevel3(@Param("userId") Integer userId);
}

View File

@@ -9,8 +9,10 @@ import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@Repository
public interface SessionRepository extends JpaRepository<Session, Long> {
@@ -46,6 +48,13 @@ public interface SessionRepository extends JpaRepository<Session, Long> {
* Returns the number of deleted rows.
* Note: MySQL requires LIMIT to be a literal or bound parameter, so we use a native query.
*/
/**
* User IDs in {@code userIds} that have at least one session valid at {@code now}
* ({@code created_at <= now} and {@code expires_at >= now}).
*/
@Query("SELECT DISTINCT s.userId FROM Session s WHERE s.userId IN :userIds AND s.createdAt <= :now AND s.expiresAt >= :now")
Set<Integer> findOnlineUserIdsAmong(@Param("userIds") Collection<Integer> userIds, @Param("now") LocalDateTime now);
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query(value = "DELETE FROM sessions WHERE expires_at < :now LIMIT :batchSize", nativeQuery = true)
int deleteExpiredSessionsBatch(@Param("now") LocalDateTime now, @Param("batchSize") int batchSize);

View File

@@ -60,6 +60,10 @@ public interface UserARepository extends JpaRepository<UserA, Integer>, JpaSpeci
*/
@Query("SELECT u FROM UserA u WHERE u.id >= :fromId AND u.id <= :toId AND u.botActive = true ORDER BY u.id")
Page<UserA> findByIdBetweenAndBotActiveTrue(@Param("fromId") int fromId, @Param("toId") int toId, Pageable pageable);
/** Count users sharing the same IP (varbinary); returns 0 if {@code ip} is null. */
@Query("SELECT COUNT(u) FROM UserA u WHERE u.ip IS NOT NULL AND u.ip = :ip")
long countByIpEqual(@Param("ip") byte[] ip);
}

View File

@@ -17,7 +17,8 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public class AdminMasterService {
private static final BigDecimal USD_DIVISOR = new BigDecimal("1000000000");
/** Honey admin: balance bigint → USD (same scale as AdminUserService). */
private static final BigDecimal USD_DIVISOR = new BigDecimal("10000000000");
private final UserDRepository userDRepository;

View File

@@ -20,6 +20,7 @@ import jakarta.persistence.criteria.Subquery;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
@@ -36,7 +37,10 @@ public class AdminUserService {
private static final long TICKETS_MULTIPLIER = 1_000_000L;
private static final BigDecimal TICKETS_TO_USD = new BigDecimal("0.001"); // 1000 tickets = 1 USD
/**
* Honey: display balance = DB bigint / 1_000_000; 1 USD = 10_000 display units → USD = DB / 10_000_000_000.
*/
private static final BigDecimal HONEY_DB_UNITS_PER_USD = new BigDecimal("10000000000");
private final UserARepository userARepository;
private final UserBRepository userBRepository;
@@ -46,6 +50,7 @@ public class AdminUserService {
private final PayoutRepository payoutRepository;
private final UserTaskClaimRepository userTaskClaimRepository;
private final TaskRepository taskRepository;
private final SessionRepository sessionRepository;
private final EntityManager entityManager;
public Page<AdminUserDto> getUsers(
@@ -69,7 +74,8 @@ public class AdminUserService {
Integer depositCountMin,
String sortBy,
String sortDir,
boolean excludeMasters) {
boolean excludeMasters,
Boolean hideSubAndBanned) {
List<Integer> masterIds = excludeMasters ? userDRepository.findMasterUserIds() : List.of();
@@ -195,10 +201,22 @@ public class AdminUserService {
predicates.add(cb.in(root.get("id")).value(subDep));
}
if (Boolean.TRUE.equals(hideSubAndBanned)) {
predicates.add(cb.equal(root.get("banned"), 0));
Subquery<Integer> subMasterSelf = query.subquery(Integer.class);
Root<UserD> dm = subMasterSelf.from(UserD.class);
subMasterSelf.select(dm.get("id"));
subMasterSelf.where(cb.and(
cb.equal(dm.get("id"), dm.get("masterId")),
cb.gt(dm.get("masterId"), 0)));
predicates.add(cb.not(root.get("id").in(subMasterSelf)));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
Set<String> sortRequiresJoin = Set.of("balanceA", "balanceB", "depositTotal", "withdrawTotal", "referralCount", "profit");
Set<String> sortRequiresJoin = Set.of(
"balanceA", "balanceB", "depositTotal", "withdrawTotal", "referralCount", "profit", "referrerTelegramName");
boolean useJoinSort = sortBy != null && sortRequiresJoin.contains(sortBy);
List<UserA> userList;
long totalElements;
@@ -210,6 +228,7 @@ public class AdminUserService {
referralCountMin, referralCountMax,
referrerId, referralLevel, ipFilter,
botActive, depositCountMin,
hideSubAndBanned,
sortBy, sortDir != null ? sortDir : "desc",
pageable.getPageSize(), (int) pageable.getOffset(),
masterIds);
@@ -236,6 +255,20 @@ public class AdminUserService {
Map<Integer, UserD> userDMap = userDRepository.findAllById(userIds).stream()
.collect(Collectors.toMap(UserD::getId, ud -> ud));
LocalDateTime sessionNow = LocalDateTime.now();
Set<Integer> onlineIds = userIds.isEmpty()
? Set.of()
: sessionRepository.findOnlineUserIdsAmong(userIds, sessionNow);
Set<Integer> refererIds = userDMap.values().stream()
.map(UserD::getRefererId1)
.filter(id -> id != null && id > 0)
.collect(Collectors.toSet());
Map<Integer, UserA> referrersById = refererIds.isEmpty()
? Map.of()
: userARepository.findAllById(refererIds).stream()
.collect(Collectors.toMap(UserA::getId, u -> u));
// Map to DTOs (filtering is done in DB via specification subqueries)
List<AdminUserDto> pageContent = userList.stream()
.map(userA -> {
@@ -273,6 +306,21 @@ public class AdminUserService {
BigDecimal depositTotalUsd = ticketsToUsd(userB.getDepositTotal());
BigDecimal withdrawTotalUsd = ticketsToUsd(userB.getWithdrawTotal());
BigDecimal profitUsd = ticketsToUsd(profit);
BigDecimal profitPercent = computeProfitPercent(depositTotalUsd, withdrawTotalUsd);
Integer referrerIdVal = null;
String referrerTelegramNameVal = null;
int rid = userD.getRefererId1();
if (rid > 0) {
referrerIdVal = rid;
UserA refA = referrersById.get(rid);
String tn = refA != null ? refA.getTelegramName() : null;
if (tn == null || tn.isBlank() || "-".equals(tn)) {
referrerTelegramNameVal = "-";
} else {
referrerTelegramNameVal = tn;
}
}
return AdminUserDto.builder()
.id(userA.getId())
@@ -296,6 +344,11 @@ public class AdminUserService {
.depositTotalUsd(depositTotalUsd)
.withdrawTotalUsd(withdrawTotalUsd)
.profitUsd(profitUsd)
.profitPercent(profitPercent)
.botActive(userA.isBotActive())
.online(onlineIds.contains(userA.getId()))
.referrerId(referrerIdVal)
.referrerTelegramName(referrerTelegramNameVal)
.build();
})
.collect(Collectors.toList());
@@ -325,6 +378,7 @@ public class AdminUserService {
String ipFilter,
Boolean botActive,
Integer depositCountMin,
Boolean hideSubAndBanned,
String sortBy,
String sortDir,
int limit,
@@ -333,7 +387,8 @@ public class AdminUserService {
StringBuilder sql = new StringBuilder(
"SELECT a.id FROM db_users_a a " +
"INNER JOIN db_users_b b ON a.id = b.id " +
"INNER JOIN db_users_d d ON a.id = d.id WHERE 1=1");
"INNER JOIN db_users_d d ON a.id = d.id " +
"LEFT JOIN db_users_a ref ON d.referer_id_1 = ref.id WHERE 1=1");
List<Object> params = new ArrayList<>();
int paramIndex = 1;
@@ -451,6 +506,10 @@ public class AdminUserService {
params.add(depositCountMin);
paramIndex++;
}
if (Boolean.TRUE.equals(hideSubAndBanned)) {
sql.append(" AND a.banned = 0");
sql.append(" AND NOT (d.id = d.master_id AND d.master_id > 0)");
}
String orderColumn = switch (sortBy != null ? sortBy : "") {
case "balanceA" -> "b.balance_a";
@@ -459,6 +518,7 @@ public class AdminUserService {
case "withdrawTotal" -> "b.withdraw_total";
case "referralCount" -> "(d.referals_1 + d.referals_2 + d.referals_3 + d.referals_4 + d.referals_5)";
case "profit" -> "(b.deposit_total - b.withdraw_total)";
case "referrerTelegramName" -> "ref.telegram_name";
default -> "a.id";
};
String direction = "asc".equalsIgnoreCase(sortDir) ? " ASC" : " DESC";
@@ -551,41 +611,47 @@ public class AdminUserService {
long totalCommissions = userD.getFromReferals1() + userD.getFromReferals2() +
userD.getFromReferals3() + userD.getFromReferals4() + userD.getFromReferals5();
// Build referral levels
BigDecimal refDep1 = paymentRepository.sumCompletedUsdForReferralsLevel1(userId);
BigDecimal refDep2 = paymentRepository.sumCompletedUsdForReferralsLevel2(userId);
BigDecimal refDep3 = paymentRepository.sumCompletedUsdForReferralsLevel3(userId);
if (refDep1 == null) refDep1 = BigDecimal.ZERO;
if (refDep2 == null) refDep2 = BigDecimal.ZERO;
if (refDep3 == null) refDep3 = BigDecimal.ZERO;
BigDecimal totalReferralDepositsUsd = refDep1.add(refDep2).add(refDep3);
// Build referral levels (admin shows 13 only)
List<ReferralLevelDto> referralLevels = new ArrayList<>();
for (int level = 1; level <= 5; level++) {
for (int level = 1; level <= 3; level++) {
int refererId = switch (level) {
case 1 -> userD.getRefererId1();
case 2 -> userD.getRefererId2();
case 3 -> userD.getRefererId3();
case 4 -> userD.getRefererId4();
case 5 -> userD.getRefererId5();
default -> 0;
};
int referralCount = switch (level) {
case 1 -> userD.getReferals1();
case 2 -> userD.getReferals2();
case 3 -> userD.getReferals3();
case 4 -> userD.getReferals4();
case 5 -> userD.getReferals5();
default -> 0;
};
long commissionsEarned = switch (level) {
case 1 -> userD.getFromReferals1();
case 2 -> userD.getFromReferals2();
case 3 -> userD.getFromReferals3();
case 4 -> userD.getFromReferals4();
case 5 -> userD.getFromReferals5();
default -> 0L;
};
long commissionsPaid = switch (level) {
case 1 -> userD.getToReferer1();
case 2 -> userD.getToReferer2();
case 3 -> userD.getToReferer3();
case 4 -> userD.getToReferer4();
case 5 -> userD.getToReferer5();
default -> 0L;
};
BigDecimal depositsUsd = switch (level) {
case 1 -> refDep1;
case 2 -> refDep2;
case 3 -> refDep3;
default -> BigDecimal.ZERO;
};
referralLevels.add(ReferralLevelDto.builder()
.level(level)
@@ -595,6 +661,7 @@ public class AdminUserService {
.commissionsPaid(commissionsPaid)
.commissionsEarnedUsd(ticketsToUsd(commissionsEarned))
.commissionsPaidUsd(ticketsToUsd(commissionsPaid))
.depositsUsd(depositsUsd)
.build());
}
@@ -602,6 +669,11 @@ public class AdminUserService {
BigDecimal withdrawTotalUsd = ticketsToUsd(userB.getWithdrawTotal());
BigDecimal totalCommissionsEarnedUsd = ticketsToUsd(totalCommissions);
long ipAddressUserCount = 0L;
if (userA.getIp() != null) {
ipAddressUserCount = userARepository.countByIpEqual(userA.getIp());
}
return AdminUserDetailDto.builder()
.id(userA.getId())
.screenName(userA.getScreenName())
@@ -616,6 +688,7 @@ public class AdminUserService {
.dateLogin(userA.getDateLogin())
.banned(userA.getBanned())
.ipAddress(IpUtils.bytesToIp(userA.getIp()))
.ipAddressUserCount(ipAddressUserCount)
.balanceA(userB.getBalanceA())
.balanceB(userB.getBalanceB())
.depositTotal(userB.getDepositTotal())
@@ -628,6 +701,7 @@ public class AdminUserService {
.referralCount(totalReferrals)
.totalCommissionsEarned(totalCommissions)
.totalCommissionsEarnedUsd(totalCommissionsEarnedUsd)
.totalReferralDepositsUsd(totalReferralDepositsUsd)
.masterId(userD.getMasterId() > 0 ? userD.getMasterId() : null)
.referralLevels(referralLevels)
.build();
@@ -635,7 +709,20 @@ public class AdminUserService {
private static BigDecimal ticketsToUsd(long ticketsBigint) {
if (ticketsBigint == 0) return BigDecimal.ZERO;
return BigDecimal.valueOf(ticketsBigint).divide(BigDecimal.valueOf(1_000_000L), 6, RoundingMode.HALF_UP).multiply(TICKETS_TO_USD).setScale(2, RoundingMode.HALF_UP);
return BigDecimal.valueOf(ticketsBigint)
.divide(HONEY_DB_UNITS_PER_USD, 8, RoundingMode.HALF_UP)
.setScale(2, RoundingMode.HALF_UP);
}
private static BigDecimal computeProfitPercent(BigDecimal depositUsd, BigDecimal withdrawUsd) {
if (depositUsd == null || depositUsd.compareTo(BigDecimal.ZERO) <= 0) {
return null;
}
BigDecimal w = withdrawUsd != null ? withdrawUsd : BigDecimal.ZERO;
return BigDecimal.ONE
.subtract(w.divide(depositUsd, 8, RoundingMode.HALF_UP))
.multiply(BigDecimal.valueOf(100))
.setScale(2, RoundingMode.HALF_UP);
}
public Page<AdminTransactionDto> getUserTransactions(Integer userId, Pageable pageable) {

View File

@@ -0,0 +1,2 @@
-- Speed up admin "online" batch lookup: sessions active at a point in time by user_id
CREATE INDEX idx_sessions_user_expires ON sessions (user_id, expires_at);