diff --git a/src/main/java/com/honey/honey/controller/AdminUserController.java b/src/main/java/com/honey/honey/controller/AdminUserController.java index 0c1eee7..3f0a676 100644 --- a/src/main/java/com/honey/honey/controller/AdminUserController.java +++ b/src/main/java/com/honey/honey/controller/AdminUserController.java @@ -36,7 +36,8 @@ public class AdminUserController { private static final Set 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 DEPOSIT_SORT_FIELDS = Set.of("id", "usdAmount", "status", "orderId", "createdAt", "completedAt"); private static final Set 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 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 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 response = new HashMap<>(); diff --git a/src/main/java/com/honey/honey/dto/AdminUserDetailDto.java b/src/main/java/com/honey/honey/dto/AdminUserDetailDto.java index e2db557..a14b46a 100644 --- a/src/main/java/com/honey/honey/dto/AdminUserDetailDto.java +++ b/src/main/java/com/honey/honey/dto/AdminUserDetailDto.java @@ -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 1–3 for this user. */ + private java.math.BigDecimal totalReferralDepositsUsd; private Integer masterId; private List referralLevels; } diff --git a/src/main/java/com/honey/honey/dto/AdminUserDto.java b/src/main/java/com/honey/honey/dto/AdminUserDto.java index 4ffecdd..527a9b4 100644 --- a/src/main/java/com/honey/honey/dto/AdminUserDto.java +++ b/src/main/java/com/honey/honey/dto/AdminUserDto.java @@ -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 <= now and expires_at >= 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 > 0; otherwise null. */ + private BigDecimal profitPercent; } diff --git a/src/main/java/com/honey/honey/dto/ReferralLevelDto.java b/src/main/java/com/honey/honey/dto/ReferralLevelDto.java index c2ca7a5..4d9a21b 100644 --- a/src/main/java/com/honey/honey/dto/ReferralLevelDto.java +++ b/src/main/java/com/honey/honey/dto/ReferralLevelDto.java @@ -12,14 +12,16 @@ import java.math.BigDecimal; @NoArgsConstructor @AllArgsConstructor public class ReferralLevelDto { - private Integer level; // 1-5 + private Integer level; // 1–3 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; } diff --git a/src/main/java/com/honey/honey/repository/PaymentRepository.java b/src/main/java/com/honey/honey/repository/PaymentRepository.java index 93d4448..a5bd328 100644 --- a/src/main/java/com/honey/honey/repository/PaymentRepository.java +++ b/src/main/java/com/honey/honey/repository/PaymentRepository.java @@ -99,5 +99,15 @@ public interface PaymentRepository extends JpaRepository, 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); } diff --git a/src/main/java/com/honey/honey/repository/SessionRepository.java b/src/main/java/com/honey/honey/repository/SessionRepository.java index d0e5e65..5e20a9e 100644 --- a/src/main/java/com/honey/honey/repository/SessionRepository.java +++ b/src/main/java/com/honey/honey/repository/SessionRepository.java @@ -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 { @@ -46,6 +48,13 @@ public interface SessionRepository extends JpaRepository { * 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 findOnlineUserIdsAmong(@Param("userIds") Collection 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); diff --git a/src/main/java/com/honey/honey/repository/UserARepository.java b/src/main/java/com/honey/honey/repository/UserARepository.java index e8a8160..284363b 100644 --- a/src/main/java/com/honey/honey/repository/UserARepository.java +++ b/src/main/java/com/honey/honey/repository/UserARepository.java @@ -60,6 +60,10 @@ public interface UserARepository extends JpaRepository, JpaSpeci */ @Query("SELECT u FROM UserA u WHERE u.id >= :fromId AND u.id <= :toId AND u.botActive = true ORDER BY u.id") Page 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); } diff --git a/src/main/java/com/honey/honey/service/AdminMasterService.java b/src/main/java/com/honey/honey/service/AdminMasterService.java index b56e621..1aa48c1 100644 --- a/src/main/java/com/honey/honey/service/AdminMasterService.java +++ b/src/main/java/com/honey/honey/service/AdminMasterService.java @@ -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; diff --git a/src/main/java/com/honey/honey/service/AdminUserService.java b/src/main/java/com/honey/honey/service/AdminUserService.java index 55d5577..fcdcb19 100644 --- a/src/main/java/com/honey/honey/service/AdminUserService.java +++ b/src/main/java/com/honey/honey/service/AdminUserService.java @@ -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 getUsers( @@ -69,7 +74,8 @@ public class AdminUserService { Integer depositCountMin, String sortBy, String sortDir, - boolean excludeMasters) { + boolean excludeMasters, + Boolean hideSubAndBanned) { List 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 subMasterSelf = query.subquery(Integer.class); + Root 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 sortRequiresJoin = Set.of("balanceA", "balanceB", "depositTotal", "withdrawTotal", "referralCount", "profit"); + Set sortRequiresJoin = Set.of( + "balanceA", "balanceB", "depositTotal", "withdrawTotal", "referralCount", "profit", "referrerTelegramName"); boolean useJoinSort = sortBy != null && sortRequiresJoin.contains(sortBy); List 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 userDMap = userDRepository.findAllById(userIds).stream() .collect(Collectors.toMap(UserD::getId, ud -> ud)); + LocalDateTime sessionNow = LocalDateTime.now(); + Set onlineIds = userIds.isEmpty() + ? Set.of() + : sessionRepository.findOnlineUserIdsAmong(userIds, sessionNow); + + Set refererIds = userDMap.values().stream() + .map(UserD::getRefererId1) + .filter(id -> id != null && id > 0) + .collect(Collectors.toSet()); + Map 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 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 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 1–3 only) List 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 getUserTransactions(Integer userId, Pageable pageable) { diff --git a/src/main/resources/db/migration/V81__sessions_user_expires_index.sql b/src/main/resources/db/migration/V81__sessions_user_expires_index.sql new file mode 100644 index 0000000..f9c74b3 --- /dev/null +++ b/src/main/resources/db/migration/V81__sessions_user_expires_index.sql @@ -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);