implemented new DB core, referal system
This commit is contained in:
7
pom.xml
7
pom.xml
@@ -72,6 +72,13 @@
|
|||||||
<artifactId>flyway-mysql</artifactId>
|
<artifactId>flyway-mysql</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MaxMind GeoIP2 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.maxmind.geoip2</groupId>
|
||||||
|
<artifactId>geoip2</artifactId>
|
||||||
|
<version>4.2.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -2,17 +2,16 @@ package com.honey.honey.controller;
|
|||||||
|
|
||||||
import com.honey.honey.dto.CreateSessionRequest;
|
import com.honey.honey.dto.CreateSessionRequest;
|
||||||
import com.honey.honey.dto.CreateSessionResponse;
|
import com.honey.honey.dto.CreateSessionResponse;
|
||||||
import com.honey.honey.model.User;
|
import com.honey.honey.model.UserA;
|
||||||
import com.honey.honey.repository.UserRepository;
|
|
||||||
import com.honey.honey.service.SessionService;
|
import com.honey.honey.service.SessionService;
|
||||||
import com.honey.honey.service.TelegramAuthService;
|
import com.honey.honey.service.TelegramAuthService;
|
||||||
import lombok.Data;
|
import com.honey.honey.service.UserService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -23,33 +22,33 @@ public class AuthController {
|
|||||||
|
|
||||||
private final TelegramAuthService telegramAuthService;
|
private final TelegramAuthService telegramAuthService;
|
||||||
private final SessionService sessionService;
|
private final SessionService sessionService;
|
||||||
private final UserRepository userRepository;
|
private final UserService userService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a session by validating Telegram initData.
|
* Creates a session by validating Telegram initData.
|
||||||
* This is the only endpoint that accepts initData.
|
* This is the only endpoint that accepts initData.
|
||||||
|
* Handles user registration/login and referral system.
|
||||||
*/
|
*/
|
||||||
@PostMapping("/tma/session")
|
@PostMapping("/tma/session")
|
||||||
public CreateSessionResponse createSession(@RequestBody CreateSessionRequest request) {
|
public CreateSessionResponse createSession(
|
||||||
|
@RequestBody CreateSessionRequest request,
|
||||||
|
HttpServletRequest httpRequest) {
|
||||||
String initData = request.getInitData();
|
String initData = request.getInitData();
|
||||||
|
|
||||||
if (initData == null || initData.isBlank()) {
|
if (initData == null || initData.isBlank()) {
|
||||||
throw new IllegalArgumentException("initData is required");
|
throw new IllegalArgumentException("initData is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Telegram initData signature
|
// Validate Telegram initData signature and parse data
|
||||||
Map<String, Object> tgUser = telegramAuthService.validateAndParseInitData(initData);
|
Map<String, Object> tgUserData = telegramAuthService.validateAndParseInitData(initData);
|
||||||
|
|
||||||
Long telegramId = ((Number) tgUser.get("id")).longValue();
|
// Get or create user (handles registration, login update, and referral system)
|
||||||
String username = (String) tgUser.get("username");
|
UserA user = userService.getOrCreateUser(tgUserData, httpRequest);
|
||||||
|
|
||||||
// Get or create user
|
|
||||||
User user = getOrCreateUser(telegramId, username);
|
|
||||||
|
|
||||||
// Create session
|
// Create session
|
||||||
String sessionId = sessionService.createSession(user);
|
String sessionId = sessionService.createSession(user);
|
||||||
|
|
||||||
log.info("Session created for userId={}, telegramId={}", user.getId(), telegramId);
|
log.info("Session created for userId={}, telegramId={}", user.getId(), user.getTelegramId());
|
||||||
|
|
||||||
return CreateSessionResponse.builder()
|
return CreateSessionResponse.builder()
|
||||||
.access_token(sessionId)
|
.access_token(sessionId)
|
||||||
@@ -76,33 +75,6 @@ public class AuthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets existing user or creates a new one.
|
|
||||||
*/
|
|
||||||
private User getOrCreateUser(Long telegramId, String username) {
|
|
||||||
var existingUser = userRepository.findByTelegramId(telegramId);
|
|
||||||
|
|
||||||
if (existingUser.isPresent()) {
|
|
||||||
User user = existingUser.get();
|
|
||||||
// Update username if it changed
|
|
||||||
if (username != null && !username.equals(user.getUsername())) {
|
|
||||||
user.setUsername(username);
|
|
||||||
userRepository.save(user);
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new user
|
|
||||||
log.info("Creating new user for telegramId={}, username={}", telegramId, username);
|
|
||||||
return userRepository.save(
|
|
||||||
User.builder()
|
|
||||||
.telegramId(telegramId)
|
|
||||||
.username(username)
|
|
||||||
.createdAt(LocalDateTime.now())
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts Bearer token from Authorization header.
|
* Extracts Bearer token from Authorization header.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package com.honey.honey.controller;
|
package com.honey.honey.controller;
|
||||||
|
|
||||||
import com.honey.honey.dto.UserDto;
|
import com.honey.honey.dto.UserDto;
|
||||||
import com.honey.honey.model.User;
|
import com.honey.honey.model.UserA;
|
||||||
import com.honey.honey.security.UserContext;
|
import com.honey.honey.security.UserContext;
|
||||||
|
import com.honey.honey.service.UserService;
|
||||||
|
import lombok.Data;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@@ -15,14 +16,31 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UserController {
|
public class UserController {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
@GetMapping("/current")
|
@GetMapping("/current")
|
||||||
public UserDto getCurrentUser() {
|
public UserDto getCurrentUser() {
|
||||||
User user = UserContext.get();
|
UserA user = UserContext.get();
|
||||||
|
|
||||||
return UserDto.builder()
|
return UserDto.builder()
|
||||||
.telegram_id(user.getTelegramId())
|
.telegram_id(user.getTelegramId())
|
||||||
.username(user.getUsername())
|
.username(user.getTelegramName())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates user's language code.
|
||||||
|
* Called when user changes language in app header.
|
||||||
|
*/
|
||||||
|
@PutMapping("/language")
|
||||||
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||||
|
public void updateLanguage(@RequestBody UpdateLanguageRequest request) {
|
||||||
|
UserA user = UserContext.get();
|
||||||
|
userService.updateLanguageCode(user.getId(), request.getLanguageCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class UpdateLanguageRequest {
|
||||||
|
private String languageCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,9 +21,8 @@ public class Session {
|
|||||||
@Column(name = "session_id_hash", unique = true, nullable = false, length = 255)
|
@Column(name = "session_id_hash", unique = true, nullable = false, length = 255)
|
||||||
private String sessionIdHash;
|
private String sessionIdHash;
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.EAGER)
|
@Column(name = "user_id", nullable = false)
|
||||||
@JoinColumn(name = "user_id", nullable = false)
|
private Integer userId;
|
||||||
private User user;
|
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false)
|
@Column(name = "created_at", nullable = false)
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.honey.honey.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.*;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "users")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class User {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(name = "telegram_id", unique = true, nullable = false)
|
|
||||||
private Long telegramId;
|
|
||||||
|
|
||||||
@Column(name = "username")
|
|
||||||
private String username;
|
|
||||||
|
|
||||||
@Column(name = "created_at")
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
|
|
||||||
@PrePersist
|
|
||||||
public void prePersist() {
|
|
||||||
if (createdAt == null) {
|
|
||||||
createdAt = LocalDateTime.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
62
src/main/java/com/honey/honey/model/UserA.java
Normal file
62
src/main/java/com/honey/honey/model/UserA.java
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package com.honey.honey.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "db_users_a")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class UserA {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "id")
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@Column(name = "screen_name", nullable = false, length = 75)
|
||||||
|
@Builder.Default
|
||||||
|
private String screenName = "-";
|
||||||
|
|
||||||
|
@Column(name = "telegram_id", unique = true)
|
||||||
|
private Long telegramId;
|
||||||
|
|
||||||
|
@Column(name = "telegram_name", nullable = false, length = 33)
|
||||||
|
@Builder.Default
|
||||||
|
private String telegramName = "-";
|
||||||
|
|
||||||
|
@Column(name = "is_premium", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer isPremium = 0;
|
||||||
|
|
||||||
|
@Column(name = "language_code", nullable = false, length = 2)
|
||||||
|
@Builder.Default
|
||||||
|
private String languageCode = "XX";
|
||||||
|
|
||||||
|
@Column(name = "country_code", nullable = false, length = 2)
|
||||||
|
@Builder.Default
|
||||||
|
private String countryCode = "XX";
|
||||||
|
|
||||||
|
@Column(name = "device_code", nullable = false, length = 5)
|
||||||
|
@Builder.Default
|
||||||
|
private String deviceCode = "XX";
|
||||||
|
|
||||||
|
@Column(name = "ip", columnDefinition = "VARBINARY(16)")
|
||||||
|
private byte[] ip;
|
||||||
|
|
||||||
|
@Column(name = "date_reg", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer dateReg = 0;
|
||||||
|
|
||||||
|
@Column(name = "date_login", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer dateLogin = 0;
|
||||||
|
|
||||||
|
@Column(name = "banned", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer banned = 0;
|
||||||
|
}
|
||||||
|
|
||||||
43
src/main/java/com/honey/honey/model/UserB.java
Normal file
43
src/main/java/com/honey/honey/model/UserB.java
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package com.honey.honey.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "db_users_b")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class UserB {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "id")
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@Column(name = "balance_a", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Long balanceA = 0L;
|
||||||
|
|
||||||
|
@Column(name = "balance_b", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Long balanceB = 0L;
|
||||||
|
|
||||||
|
@Column(name = "deposit_total", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Long depositTotal = 0L;
|
||||||
|
|
||||||
|
@Column(name = "deposit_count", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer depositCount = 0;
|
||||||
|
|
||||||
|
@Column(name = "withdraw_total", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Long withdrawTotal = 0L;
|
||||||
|
|
||||||
|
@Column(name = "withdraw_count", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer withdrawCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
103
src/main/java/com/honey/honey/model/UserD.java
Normal file
103
src/main/java/com/honey/honey/model/UserD.java
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package com.honey.honey.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "db_users_d")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class UserD {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "id")
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@Column(name = "referer_id_1", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer refererId1 = 0;
|
||||||
|
|
||||||
|
@Column(name = "referer_id_2", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer refererId2 = 0;
|
||||||
|
|
||||||
|
@Column(name = "referer_id_3", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer refererId3 = 0;
|
||||||
|
|
||||||
|
@Column(name = "referer_id_4", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer refererId4 = 0;
|
||||||
|
|
||||||
|
@Column(name = "referer_id_5", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer refererId5 = 0;
|
||||||
|
|
||||||
|
@Column(name = "master_id", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer masterId = 0;
|
||||||
|
|
||||||
|
@Column(name = "referals_1", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer referals1 = 0;
|
||||||
|
|
||||||
|
@Column(name = "referals_2", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer referals2 = 0;
|
||||||
|
|
||||||
|
@Column(name = "referals_3", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer referals3 = 0;
|
||||||
|
|
||||||
|
@Column(name = "referals_4", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer referals4 = 0;
|
||||||
|
|
||||||
|
@Column(name = "referals_5", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer referals5 = 0;
|
||||||
|
|
||||||
|
@Column(name = "from_referals_1", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Long fromReferals1 = 0L;
|
||||||
|
|
||||||
|
@Column(name = "from_referals_2", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Long fromReferals2 = 0L;
|
||||||
|
|
||||||
|
@Column(name = "from_referals_3", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Long fromReferals3 = 0L;
|
||||||
|
|
||||||
|
@Column(name = "from_referals_4", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Long fromReferals4 = 0L;
|
||||||
|
|
||||||
|
@Column(name = "from_referals_5", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Long fromReferals5 = 0L;
|
||||||
|
|
||||||
|
@Column(name = "to_referer_1", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Long toReferer1 = 0L;
|
||||||
|
|
||||||
|
@Column(name = "to_referer_2", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Long toReferer2 = 0L;
|
||||||
|
|
||||||
|
@Column(name = "to_referer_3", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Long toReferer3 = 0L;
|
||||||
|
|
||||||
|
@Column(name = "to_referer_4", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Long toReferer4 = 0L;
|
||||||
|
|
||||||
|
@Column(name = "to_referer_5", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Long toReferer5 = 0L;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -19,15 +19,15 @@ public interface SessionRepository extends JpaRepository<Session, Long> {
|
|||||||
/**
|
/**
|
||||||
* Counts active (non-expired) sessions for a user.
|
* Counts active (non-expired) sessions for a user.
|
||||||
*/
|
*/
|
||||||
@Query("SELECT COUNT(s) FROM Session s WHERE s.user.id = :userId AND s.expiresAt > :now")
|
@Query("SELECT COUNT(s) FROM Session s WHERE s.userId = :userId AND s.expiresAt > :now")
|
||||||
long countActiveSessionsByUserId(@Param("userId") Long userId, @Param("now") LocalDateTime now);
|
long countActiveSessionsByUserId(@Param("userId") Integer userId, @Param("now") LocalDateTime now);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds oldest active sessions for a user, ordered by created_at ASC.
|
* Finds oldest active sessions for a user, ordered by created_at ASC.
|
||||||
* Used to delete oldest sessions when max limit is exceeded.
|
* Used to delete oldest sessions when max limit is exceeded.
|
||||||
*/
|
*/
|
||||||
@Query("SELECT s FROM Session s WHERE s.user.id = :userId AND s.expiresAt > :now ORDER BY s.createdAt ASC")
|
@Query("SELECT s FROM Session s WHERE s.userId = :userId AND s.expiresAt > :now ORDER BY s.createdAt ASC")
|
||||||
List<Session> findOldestActiveSessionsByUserId(@Param("userId") Long userId, @Param("now") LocalDateTime now, Pageable pageable);
|
List<Session> findOldestActiveSessionsByUserId(@Param("userId") Integer userId, @Param("now") LocalDateTime now, Pageable pageable);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Batch deletes expired sessions (up to batchSize).
|
* Batch deletes expired sessions (up to batchSize).
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.honey.honey.repository;
|
||||||
|
|
||||||
|
import com.honey.honey.model.UserA;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface UserARepository extends JpaRepository<UserA, Integer> {
|
||||||
|
Optional<UserA> findByTelegramId(Long telegramId);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.honey.honey.repository;
|
||||||
|
|
||||||
|
import com.honey.honey.model.UserB;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface UserBRepository extends JpaRepository<UserB, Integer> {
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.honey.honey.repository;
|
||||||
|
|
||||||
|
import com.honey.honey.model.UserD;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface UserDRepository extends JpaRepository<UserD, Integer> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments referals_1 for a user.
|
||||||
|
*/
|
||||||
|
@Modifying
|
||||||
|
@Query("UPDATE UserD u SET u.referals1 = u.referals1 + 1 WHERE u.id = :userId")
|
||||||
|
void incrementReferals1(@Param("userId") Integer userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments referals_2 for a user.
|
||||||
|
*/
|
||||||
|
@Modifying
|
||||||
|
@Query("UPDATE UserD u SET u.referals2 = u.referals2 + 1 WHERE u.id = :userId")
|
||||||
|
void incrementReferals2(@Param("userId") Integer userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments referals_3 for a user.
|
||||||
|
*/
|
||||||
|
@Modifying
|
||||||
|
@Query("UPDATE UserD u SET u.referals3 = u.referals3 + 1 WHERE u.id = :userId")
|
||||||
|
void incrementReferals3(@Param("userId") Integer userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments referals_4 for a user.
|
||||||
|
*/
|
||||||
|
@Modifying
|
||||||
|
@Query("UPDATE UserD u SET u.referals4 = u.referals4 + 1 WHERE u.id = :userId")
|
||||||
|
void incrementReferals4(@Param("userId") Integer userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments referals_5 for a user.
|
||||||
|
*/
|
||||||
|
@Modifying
|
||||||
|
@Query("UPDATE UserD u SET u.referals5 = u.referals5 + 1 WHERE u.id = :userId")
|
||||||
|
void incrementReferals5(@Param("userId") Integer userId);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.honey.honey.repository;
|
|
||||||
|
|
||||||
import com.honey.honey.model.User;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
public interface UserRepository extends JpaRepository<User, Long> {
|
|
||||||
List<User> findAllByTelegramId(Long telegramId);
|
|
||||||
Optional<User> findByTelegramId(Long telegramId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.honey.honey.security;
|
package com.honey.honey.security;
|
||||||
|
|
||||||
import com.honey.honey.model.User;
|
import com.honey.honey.model.UserA;
|
||||||
import com.honey.honey.service.SessionService;
|
import com.honey.honey.service.SessionService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -38,7 +38,7 @@ public class AuthInterceptor implements HandlerInterceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate session and get user
|
// Validate session and get user
|
||||||
Optional<User> userOpt = sessionService.getUserBySession(sessionId);
|
Optional<UserA> userOpt = sessionService.getUserBySession(sessionId);
|
||||||
|
|
||||||
if (userOpt.isEmpty()) {
|
if (userOpt.isEmpty()) {
|
||||||
log.warn("❌ Invalid or expired session: {}", maskSessionId(sessionId));
|
log.warn("❌ Invalid or expired session: {}", maskSessionId(sessionId));
|
||||||
@@ -47,7 +47,7 @@ public class AuthInterceptor implements HandlerInterceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Put user in context
|
// Put user in context
|
||||||
User user = userOpt.get();
|
UserA user = userOpt.get();
|
||||||
UserContext.set(user);
|
UserContext.set(user);
|
||||||
log.debug("🔑 Authenticated userId={} via session", user.getId());
|
log.debug("🔑 Authenticated userId={} via session", user.getId());
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
package com.honey.honey.security;
|
package com.honey.honey.security;
|
||||||
|
|
||||||
import com.honey.honey.model.User;
|
import com.honey.honey.model.UserA;
|
||||||
|
|
||||||
public class UserContext {
|
public class UserContext {
|
||||||
|
|
||||||
private static final ThreadLocal<User> current = new ThreadLocal<>();
|
private static final ThreadLocal<UserA> current = new ThreadLocal<>();
|
||||||
|
|
||||||
public static void set(User user) {
|
public static void set(UserA user) {
|
||||||
current.set(user);
|
current.set(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static User get() {
|
public static UserA get() {
|
||||||
return current.get();
|
return current.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
156
src/main/java/com/honey/honey/service/CountryCodeService.java
Normal file
156
src/main/java/com/honey/honey/service/CountryCodeService.java
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package com.honey.honey.service;
|
||||||
|
|
||||||
|
import com.maxmind.db.CHMCache;
|
||||||
|
import com.maxmind.geoip2.DatabaseReader;
|
||||||
|
import com.maxmind.geoip2.exception.GeoIp2Exception;
|
||||||
|
import com.maxmind.geoip2.model.CountryResponse;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.ResourceLoader;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for determining country code from IP address using MaxMind GeoLite2 database.
|
||||||
|
* Thread-safe singleton that maintains a single DatabaseReader instance.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class CountryCodeService {
|
||||||
|
|
||||||
|
private final String dbPath;
|
||||||
|
private final ResourceLoader resourceLoader;
|
||||||
|
private DatabaseReader reader;
|
||||||
|
|
||||||
|
public CountryCodeService(
|
||||||
|
@Value("${geoip.db-path:}") String dbPath,
|
||||||
|
ResourceLoader resourceLoader) {
|
||||||
|
this.dbPath = dbPath;
|
||||||
|
this.resourceLoader = resourceLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() throws IOException {
|
||||||
|
File dbFile = null;
|
||||||
|
|
||||||
|
// Try external file path first (from config/env)
|
||||||
|
if (dbPath != null && !dbPath.isBlank()) {
|
||||||
|
dbFile = new File(dbPath);
|
||||||
|
if (dbFile.exists() && dbFile.isFile()) {
|
||||||
|
log.info("Loading GeoIP database from external path: {}", dbFile.getAbsolutePath());
|
||||||
|
} else {
|
||||||
|
log.warn("GeoIP database file not found at configured path: {}, falling back to resources", dbFile.getAbsolutePath());
|
||||||
|
dbFile = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to resources directory
|
||||||
|
if (dbFile == null) {
|
||||||
|
try {
|
||||||
|
Resource resource = resourceLoader.getResource("classpath:geoip/GeoLite2-Country.mmdb");
|
||||||
|
if (resource.exists() && resource.isReadable()) {
|
||||||
|
// Try to get file path first (works if resource is a file system resource)
|
||||||
|
try {
|
||||||
|
dbFile = resource.getFile();
|
||||||
|
log.info("Loading GeoIP database from resources: {}", dbFile.getAbsolutePath());
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Resource is in JAR, need to extract to temp file
|
||||||
|
log.debug("GeoIP database is in JAR, extracting to temp file");
|
||||||
|
InputStream inputStream = resource.getInputStream();
|
||||||
|
File tempFile = File.createTempFile("GeoLite2-Country", ".mmdb");
|
||||||
|
tempFile.deleteOnExit();
|
||||||
|
|
||||||
|
try (inputStream) {
|
||||||
|
java.nio.file.Files.copy(inputStream, tempFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
|
||||||
|
dbFile = tempFile;
|
||||||
|
log.info("Loading GeoIP database from JAR resources (extracted to temp file)");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("GeoIP database not found in resources: classpath:geoip/GeoLite2-Country.mmdb");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("Failed to load GeoIP database from resources: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize DatabaseReader with cache for better performance
|
||||||
|
try {
|
||||||
|
this.reader = new DatabaseReader.Builder(dbFile)
|
||||||
|
.withCache(new CHMCache())
|
||||||
|
.build();
|
||||||
|
log.info("GeoIP database loaded successfully");
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Failed to initialize GeoIP database reader", e);
|
||||||
|
throw new IllegalStateException("Failed to initialize GeoIP database reader", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void destroy() {
|
||||||
|
if (reader != null) {
|
||||||
|
try {
|
||||||
|
reader.close();
|
||||||
|
log.info("GeoIP database reader closed");
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Error closing GeoIP database reader", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines country code from IP address using MaxMind GeoLite2 database.
|
||||||
|
*
|
||||||
|
* @param ipAddress IP address as string (IPv4 or IPv6)
|
||||||
|
* @return ISO 3166-1 alpha-2 country code (e.g., "UA", "PL", "DE"), or "XX" if unknown/invalid/private
|
||||||
|
*/
|
||||||
|
public String getCountryCode(String ipAddress) {
|
||||||
|
if (ipAddress == null || ipAddress.isBlank()) {
|
||||||
|
return "XX";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader == null) {
|
||||||
|
log.warn("GeoIP database reader not initialized, returning 'XX' for IP: {}", ipAddress);
|
||||||
|
return "XX";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
InetAddress addr = InetAddress.getByName(ipAddress);
|
||||||
|
|
||||||
|
// Check if it's a private/local IP (won't be in GeoIP database)
|
||||||
|
if (addr.isLoopbackAddress() || addr.isLinkLocalAddress() || addr.isSiteLocalAddress()) {
|
||||||
|
log.debug("Private/local IP address detected: {}, returning 'XX'", ipAddress);
|
||||||
|
return "XX";
|
||||||
|
}
|
||||||
|
|
||||||
|
CountryResponse response = reader.country(addr);
|
||||||
|
String iso = response.getCountry().getIsoCode();
|
||||||
|
|
||||||
|
if (iso != null && iso.length() == 2) {
|
||||||
|
return iso.toUpperCase(); // Ensure uppercase (ISO codes should be uppercase)
|
||||||
|
} else {
|
||||||
|
log.debug("Invalid ISO code returned for IP {}: {}", ipAddress, iso);
|
||||||
|
return "XX";
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.debug("Failed to resolve IP address: {}", ipAddress, e);
|
||||||
|
return "XX";
|
||||||
|
} catch (GeoIp2Exception e) {
|
||||||
|
log.debug("GeoIP lookup failed for IP {}: {}", ipAddress, e.getMessage());
|
||||||
|
return "XX";
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.debug("Invalid IP address format: {}", ipAddress, e);
|
||||||
|
return "XX";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.honey.honey.service;
|
package com.honey.honey.service;
|
||||||
|
|
||||||
import com.honey.honey.model.Session;
|
import com.honey.honey.model.Session;
|
||||||
import com.honey.honey.model.User;
|
import com.honey.honey.model.UserA;
|
||||||
import com.honey.honey.repository.SessionRepository;
|
import com.honey.honey.repository.SessionRepository;
|
||||||
|
import com.honey.honey.repository.UserARepository;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -24,9 +26,15 @@ import java.util.Optional;
|
|||||||
public class SessionService {
|
public class SessionService {
|
||||||
|
|
||||||
private final SessionRepository sessionRepository;
|
private final SessionRepository sessionRepository;
|
||||||
|
private final UserARepository userARepository;
|
||||||
private static final int SESSION_TTL_HOURS = 24; // 1 day
|
private static final int SESSION_TTL_HOURS = 24; // 1 day
|
||||||
private static final SecureRandom secureRandom = new SecureRandom();
|
private static final SecureRandom secureRandom = new SecureRandom();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -- GETTER --
|
||||||
|
* Gets max active sessions per user.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
@Value("${app.session.max-active-per-user:5}")
|
@Value("${app.session.max-active-per-user:5}")
|
||||||
private int maxActiveSessionsPerUser;
|
private int maxActiveSessionsPerUser;
|
||||||
|
|
||||||
@@ -36,7 +44,7 @@ public class SessionService {
|
|||||||
* Returns the raw session ID (to be sent to frontend) and stores the hash in DB.
|
* Returns the raw session ID (to be sent to frontend) and stores the hash in DB.
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public String createSession(User user) {
|
public String createSession(UserA user) {
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
// Generate cryptographically random session ID
|
// Generate cryptographically random session ID
|
||||||
@@ -56,7 +64,7 @@ public class SessionService {
|
|||||||
// Create and save session
|
// Create and save session
|
||||||
Session session = Session.builder()
|
Session session = Session.builder()
|
||||||
.sessionIdHash(sessionIdHash)
|
.sessionIdHash(sessionIdHash)
|
||||||
.user(user)
|
.userId(user.getId())
|
||||||
.createdAt(now)
|
.createdAt(now)
|
||||||
.expiresAt(expiresAt)
|
.expiresAt(expiresAt)
|
||||||
.build();
|
.build();
|
||||||
@@ -70,7 +78,7 @@ public class SessionService {
|
|||||||
/**
|
/**
|
||||||
* Enforces MAX_ACTIVE_SESSIONS_PER_USER by deleting oldest active sessions if limit exceeded.
|
* Enforces MAX_ACTIVE_SESSIONS_PER_USER by deleting oldest active sessions if limit exceeded.
|
||||||
*/
|
*/
|
||||||
private void enforceMaxActiveSessions(Long userId, LocalDateTime now) {
|
private void enforceMaxActiveSessions(Integer userId, LocalDateTime now) {
|
||||||
long activeCount = sessionRepository.countActiveSessionsByUserId(userId, now);
|
long activeCount = sessionRepository.countActiveSessionsByUserId(userId, now);
|
||||||
|
|
||||||
if (activeCount >= maxActiveSessionsPerUser) {
|
if (activeCount >= maxActiveSessionsPerUser) {
|
||||||
@@ -98,7 +106,7 @@ public class SessionService {
|
|||||||
* Returns empty if session is invalid or expired.
|
* Returns empty if session is invalid or expired.
|
||||||
*/
|
*/
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Optional<User> getUserBySession(String sessionId) {
|
public Optional<UserA> getUserBySession(String sessionId) {
|
||||||
if (sessionId == null || sessionId.isBlank()) {
|
if (sessionId == null || sessionId.isBlank()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
@@ -120,12 +128,8 @@ public class SessionService {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Access user properties while still in transaction to initialize lazy proxy
|
// Load user by ID
|
||||||
User user = session.getUser();
|
return userARepository.findById(session.getUserId());
|
||||||
// Force initialization by accessing a property
|
|
||||||
user.getTelegramId();
|
|
||||||
|
|
||||||
return Optional.of(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -173,11 +177,5 @@ public class SessionService {
|
|||||||
return SESSION_TTL_HOURS * 3600;
|
return SESSION_TTL_HOURS * 3600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets max active sessions per user.
|
|
||||||
*/
|
|
||||||
public int getMaxActiveSessionsPerUser() {
|
|
||||||
return maxActiveSessionsPerUser;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ public class TelegramAuthService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates and parses Telegram initData string.
|
* Validates and parses Telegram initData string.
|
||||||
|
* Returns a map containing:
|
||||||
|
* - "user": parsed user data (Map)
|
||||||
|
* - "start": referral start parameter from URL (e.g., "774876" from /honey?start=774876) (String, can be null)
|
||||||
*/
|
*/
|
||||||
public Map<String, Object> validateAndParseInitData(String initData) {
|
public Map<String, Object> validateAndParseInitData(String initData) {
|
||||||
|
|
||||||
@@ -60,16 +63,21 @@ public class TelegramAuthService {
|
|||||||
throw new UnauthorizedException("Invalid Telegram signature");
|
throw new UnauthorizedException("Invalid Telegram signature");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 5. Extract the user JSON from initData.
|
// Step 5. Extract the user JSON and start parameter from initData.
|
||||||
Map<String, String> decoded = decodeQueryParams(initData);
|
Map<String, String> decoded = decodeQueryParams(initData);
|
||||||
String userJson = decoded.get("user");
|
String userJson = decoded.get("user");
|
||||||
|
String start = decoded.get("start"); // Referral parameter from URL: /honey?start=774876
|
||||||
|
|
||||||
if (userJson == null) {
|
if (userJson == null) {
|
||||||
throw new UnauthorizedException("initData does not contain 'user' field");
|
throw new UnauthorizedException("initData does not contain 'user' field");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 6. Parse JSON into map.
|
// Step 6. Parse JSON into map and add start parameter.
|
||||||
return objectMapper.readValue(userJson, Map.class);
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("user", objectMapper.readValue(userJson, Map.class));
|
||||||
|
result.put("start", start);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
} catch (UnauthorizedException ex) {
|
} catch (UnauthorizedException ex) {
|
||||||
throw ex;
|
throw ex;
|
||||||
|
|||||||
317
src/main/java/com/honey/honey/service/UserService.java
Normal file
317
src/main/java/com/honey/honey/service/UserService.java
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
package com.honey.honey.service;
|
||||||
|
|
||||||
|
import com.honey.honey.model.UserA;
|
||||||
|
import com.honey.honey.model.UserB;
|
||||||
|
import com.honey.honey.model.UserD;
|
||||||
|
import com.honey.honey.repository.UserARepository;
|
||||||
|
import com.honey.honey.repository.UserBRepository;
|
||||||
|
import com.honey.honey.repository.UserDRepository;
|
||||||
|
import com.honey.honey.util.IpUtils;
|
||||||
|
import com.honey.honey.util.TimeProvider;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for user management with sharded tables.
|
||||||
|
* Handles registration, login, and referral system.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserService {
|
||||||
|
|
||||||
|
private final UserARepository userARepository;
|
||||||
|
private final UserBRepository userBRepository;
|
||||||
|
private final UserDRepository userDRepository;
|
||||||
|
private final CountryCodeService countryCodeService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets or creates a user based on Telegram initData.
|
||||||
|
* Updates user data on each login.
|
||||||
|
* Handles referral system if start parameter is present.
|
||||||
|
*
|
||||||
|
* @param tgUserData Parsed Telegram data from initData (contains "user" map and "start" string)
|
||||||
|
* @param request HTTP request for IP extraction
|
||||||
|
* @return UserA entity
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public UserA getOrCreateUser(Map<String, Object> tgUserData, HttpServletRequest request) {
|
||||||
|
// Extract user data and start parameter (from URL: /honey?start=774876)
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> tgUser = (Map<String, Object>) tgUserData.get("user");
|
||||||
|
String start = (String) tgUserData.get("start");
|
||||||
|
Long telegramId = ((Number) tgUser.get("id")).longValue();
|
||||||
|
String firstName = (String) tgUser.get("first_name");
|
||||||
|
String lastName = (String) tgUser.get("last_name");
|
||||||
|
String username = (String) tgUser.get("username");
|
||||||
|
Boolean isPremium = (Boolean) tgUser.get("is_premium");
|
||||||
|
String languageCode = (String) tgUser.get("language_code");
|
||||||
|
|
||||||
|
// Build screen_name from first_name and last_name
|
||||||
|
String screenName = buildScreenName(firstName, lastName);
|
||||||
|
|
||||||
|
// device_code should be language_code from initData
|
||||||
|
String deviceCode = languageCode != null ? languageCode : "XX";
|
||||||
|
|
||||||
|
// Get client IP and convert to bytes
|
||||||
|
String clientIp = IpUtils.getClientIp(request);
|
||||||
|
byte[] ipBytes = IpUtils.ipToBytes(clientIp);
|
||||||
|
|
||||||
|
// Get country code from IP
|
||||||
|
String countryCode = countryCodeService.getCountryCode(clientIp);
|
||||||
|
|
||||||
|
// Get current timestamp
|
||||||
|
long nowSeconds = TimeProvider.nowSeconds();
|
||||||
|
|
||||||
|
// Check if user exists
|
||||||
|
Optional<UserA> existingUserOpt = userARepository.findByTelegramId(telegramId);
|
||||||
|
|
||||||
|
if (existingUserOpt.isPresent()) {
|
||||||
|
// User exists - update login data
|
||||||
|
UserA userA = existingUserOpt.get();
|
||||||
|
updateUserOnLogin(userA, screenName, username, isPremium, languageCode, countryCode, deviceCode, ipBytes, nowSeconds);
|
||||||
|
return userA;
|
||||||
|
} else {
|
||||||
|
// New user - create in all 3 tables
|
||||||
|
return createNewUser(telegramId, screenName, username, isPremium, languageCode, countryCode, deviceCode, ipBytes, nowSeconds, start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates user data on login (when session is created).
|
||||||
|
* Note: language_code is NOT updated here - it should be updated via separate endpoint when user changes language in app.
|
||||||
|
*/
|
||||||
|
private void updateUserOnLogin(UserA userA, String screenName, String username, Boolean isPremium,
|
||||||
|
String languageCode, String countryCode, String deviceCode,
|
||||||
|
byte[] ipBytes, long nowSeconds) {
|
||||||
|
userA.setScreenName(screenName);
|
||||||
|
userA.setTelegramName(username != null ? username : "-");
|
||||||
|
userA.setIsPremium(isPremium != null && isPremium ? 1 : 0);
|
||||||
|
userA.setCountryCode(countryCode);
|
||||||
|
userA.setDeviceCode(deviceCode != null ? deviceCode : "XX");
|
||||||
|
userA.setIp(ipBytes);
|
||||||
|
userA.setDateLogin((int) nowSeconds);
|
||||||
|
// language_code is NOT updated here - it's updated via separate endpoint when user changes language
|
||||||
|
|
||||||
|
userARepository.save(userA);
|
||||||
|
log.debug("Updated user data on login: userId={}", userA.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates user's language code (called when user changes language in app header).
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void updateLanguageCode(Integer userId, String languageCode) {
|
||||||
|
Optional<UserA> userOpt = userARepository.findById(userId);
|
||||||
|
if (userOpt.isPresent()) {
|
||||||
|
UserA user = userOpt.get();
|
||||||
|
user.setLanguageCode(languageCode != null && languageCode.length() == 2 ? languageCode.toUpperCase() : "XX");
|
||||||
|
userARepository.save(user);
|
||||||
|
log.debug("Updated language_code for userId={}: {}", userId, user.getLanguageCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new user in all 3 tables with referral handling.
|
||||||
|
*/
|
||||||
|
private UserA createNewUser(Long telegramId, String screenName, String username, Boolean isPremium,
|
||||||
|
String languageCode, String countryCode, String deviceCode,
|
||||||
|
byte[] ipBytes, long nowSeconds, String start) {
|
||||||
|
|
||||||
|
// Create UserA
|
||||||
|
UserA userA = UserA.builder()
|
||||||
|
.screenName(screenName)
|
||||||
|
.telegramId(telegramId)
|
||||||
|
.telegramName(username != null ? username : "-")
|
||||||
|
.isPremium(isPremium != null && isPremium ? 1 : 0)
|
||||||
|
.languageCode(languageCode != null ? languageCode : "XX")
|
||||||
|
.countryCode(countryCode)
|
||||||
|
.deviceCode(deviceCode != null ? deviceCode : "XX")
|
||||||
|
.ip(ipBytes)
|
||||||
|
.dateReg((int) nowSeconds)
|
||||||
|
.dateLogin((int) nowSeconds)
|
||||||
|
.banned(0)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
userA = userARepository.save(userA);
|
||||||
|
Integer userId = userA.getId();
|
||||||
|
|
||||||
|
log.info("Created new user: userId={}, telegramId={}", userId, telegramId);
|
||||||
|
|
||||||
|
// Create UserB with same ID
|
||||||
|
UserB userB = UserB.builder()
|
||||||
|
.id(userId)
|
||||||
|
.balanceA(0L)
|
||||||
|
.balanceB(0L)
|
||||||
|
.depositTotal(0L)
|
||||||
|
.depositCount(0)
|
||||||
|
.withdrawTotal(0L)
|
||||||
|
.withdrawCount(0)
|
||||||
|
.build();
|
||||||
|
userBRepository.save(userB);
|
||||||
|
|
||||||
|
// Create UserD with referral handling
|
||||||
|
UserD userD = createUserDWithReferral(userId, start);
|
||||||
|
userDRepository.save(userD);
|
||||||
|
|
||||||
|
return userA;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates UserD entity with referral chain setup.
|
||||||
|
* @param userId New user's ID
|
||||||
|
* @param start Referral parameter from URL (e.g., "774876" from /honey?start=774876)
|
||||||
|
*/
|
||||||
|
private UserD createUserDWithReferral(Integer userId, String start) {
|
||||||
|
UserD.UserDBuilder builder = UserD.builder()
|
||||||
|
.id(userId)
|
||||||
|
.refererId1(0)
|
||||||
|
.refererId2(0)
|
||||||
|
.refererId3(0)
|
||||||
|
.refererId4(0)
|
||||||
|
.refererId5(0)
|
||||||
|
.masterId(1) // Default master_id = 1
|
||||||
|
.referals1(0)
|
||||||
|
.referals2(0)
|
||||||
|
.referals3(0)
|
||||||
|
.referals4(0)
|
||||||
|
.referals5(0)
|
||||||
|
.fromReferals1(0L)
|
||||||
|
.fromReferals2(0L)
|
||||||
|
.fromReferals3(0L)
|
||||||
|
.fromReferals4(0L)
|
||||||
|
.fromReferals5(0L)
|
||||||
|
.toReferer1(0L)
|
||||||
|
.toReferer2(0L)
|
||||||
|
.toReferer3(0L)
|
||||||
|
.toReferer4(0L)
|
||||||
|
.toReferer5(0L);
|
||||||
|
|
||||||
|
if (start != null && !start.isEmpty()) {
|
||||||
|
try {
|
||||||
|
Integer refererId = Integer.parseInt(start);
|
||||||
|
Optional<UserD> refererUserDOpt = userDRepository.findById(refererId);
|
||||||
|
|
||||||
|
if (refererUserDOpt.isPresent()) {
|
||||||
|
UserD refererUserD = refererUserDOpt.get();
|
||||||
|
|
||||||
|
// Set referral chain: shift referer's chain down by 1 level
|
||||||
|
builder.refererId1(refererId)
|
||||||
|
.masterId(refererUserD.getMasterId())
|
||||||
|
.refererId2(refererUserD.getRefererId1())
|
||||||
|
.refererId3(refererUserD.getRefererId2())
|
||||||
|
.refererId4(refererUserD.getRefererId3())
|
||||||
|
.refererId5(refererUserD.getRefererId4());
|
||||||
|
|
||||||
|
// Increment referal counts for all 5 levels up the chain
|
||||||
|
setupReferralChain(userId, refererId);
|
||||||
|
} else {
|
||||||
|
// Referer doesn't exist, just set referer_id_1
|
||||||
|
log.warn("Referer with id {} not found, setting only referer_id_1", refererId);
|
||||||
|
builder.refererId1(refererId);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("Invalid start parameter format: {}", start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up referral chain and increments referal counts for all 5 levels.
|
||||||
|
* Example: If user F registers with referer E, increments:
|
||||||
|
* - referals_1 for E
|
||||||
|
* - referals_2 for D (E's referer_id_1)
|
||||||
|
* - referals_3 for C (D's referer_id_1)
|
||||||
|
* - referals_4 for B (C's referer_id_1)
|
||||||
|
* - referals_5 for A (B's referer_id_1)
|
||||||
|
*/
|
||||||
|
private void setupReferralChain(Integer newUserId, Integer refererId) {
|
||||||
|
// Level 1: Direct referer
|
||||||
|
userDRepository.incrementReferals1(refererId);
|
||||||
|
|
||||||
|
Optional<UserD> level1Opt = userDRepository.findById(refererId);
|
||||||
|
if (level1Opt.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserD level1 = level1Opt.get();
|
||||||
|
|
||||||
|
// Level 2
|
||||||
|
if (level1.getRefererId1() > 0) {
|
||||||
|
userDRepository.incrementReferals2(level1.getRefererId1());
|
||||||
|
|
||||||
|
Optional<UserD> level2Opt = userDRepository.findById(level1.getRefererId1());
|
||||||
|
if (level2Opt.isPresent()) {
|
||||||
|
UserD level2 = level2Opt.get();
|
||||||
|
|
||||||
|
// Level 3
|
||||||
|
if (level2.getRefererId1() > 0) {
|
||||||
|
userDRepository.incrementReferals3(level2.getRefererId1());
|
||||||
|
|
||||||
|
Optional<UserD> level3Opt = userDRepository.findById(level2.getRefererId1());
|
||||||
|
if (level3Opt.isPresent()) {
|
||||||
|
UserD level3 = level3Opt.get();
|
||||||
|
|
||||||
|
// Level 4
|
||||||
|
if (level3.getRefererId1() > 0) {
|
||||||
|
userDRepository.incrementReferals4(level3.getRefererId1());
|
||||||
|
|
||||||
|
Optional<UserD> level4Opt = userDRepository.findById(level3.getRefererId1());
|
||||||
|
if (level4Opt.isPresent()) {
|
||||||
|
UserD level4 = level4Opt.get();
|
||||||
|
|
||||||
|
// Level 5
|
||||||
|
if (level4.getRefererId1() > 0) {
|
||||||
|
userDRepository.incrementReferals5(level4.getRefererId1());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Referral chain setup completed: newUserId={}, refererId={}", newUserId, refererId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds screen_name from first_name and last_name.
|
||||||
|
*/
|
||||||
|
private String buildScreenName(String firstName, String lastName) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (firstName != null && !firstName.isEmpty()) {
|
||||||
|
sb.append(firstName);
|
||||||
|
}
|
||||||
|
if (lastName != null && !lastName.isEmpty()) {
|
||||||
|
if (sb.length() > 0) {
|
||||||
|
sb.append(" ");
|
||||||
|
}
|
||||||
|
sb.append(lastName);
|
||||||
|
}
|
||||||
|
String result = sb.toString().trim();
|
||||||
|
return result.isEmpty() ? "-" : (result.length() > 75 ? result.substring(0, 75) : result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets user by ID.
|
||||||
|
*/
|
||||||
|
public Optional<UserA> getUserById(Integer userId) {
|
||||||
|
return userARepository.findById(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets user by Telegram ID.
|
||||||
|
*/
|
||||||
|
public Optional<UserA> getUserByTelegramId(Long telegramId) {
|
||||||
|
return userARepository.findByTelegramId(telegramId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
177
src/main/java/com/honey/honey/util/IpUtils.java
Normal file
177
src/main/java/com/honey/honey/util/IpUtils.java
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package com.honey.honey.util;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for IP address handling with reverse proxy support.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class IpUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts client IP address from request, handling reverse proxies.
|
||||||
|
* Priority:
|
||||||
|
* 1. X-Forwarded-For header (first IP in chain)
|
||||||
|
* 2. Forwarded header (RFC 7239)
|
||||||
|
* 3. X-Real-IP header
|
||||||
|
* 4. Remote address (fallback)
|
||||||
|
*
|
||||||
|
* @param request HTTP request
|
||||||
|
* @return Client IP address as string (normalized), or null if not available
|
||||||
|
*/
|
||||||
|
public static String getClientIp(HttpServletRequest request) {
|
||||||
|
// 1) Standard proxy header (most common)
|
||||||
|
String xff = request.getHeader("X-Forwarded-For");
|
||||||
|
if (xff != null && !xff.isBlank() && !"unknown".equalsIgnoreCase(xff)) {
|
||||||
|
// Format: "client, proxy1, proxy2" - take first IP
|
||||||
|
String first = xff.split(",")[0].trim();
|
||||||
|
if (!first.isEmpty()) {
|
||||||
|
return normalize(first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) RFC 7239 Forwarded header (optional, more complex)
|
||||||
|
String forwarded = request.getHeader("Forwarded");
|
||||||
|
if (forwarded != null && !forwarded.isBlank()) {
|
||||||
|
String ip = extractFromForwardedHeader(forwarded);
|
||||||
|
if (ip != null) {
|
||||||
|
return normalize(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) X-Real-IP header (some proxies use this)
|
||||||
|
String xRealIp = request.getHeader("X-Real-IP");
|
||||||
|
if (xRealIp != null && !xRealIp.isBlank() && !"unknown".equalsIgnoreCase(xRealIp)) {
|
||||||
|
return normalize(xRealIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Fallback to remote address
|
||||||
|
return normalize(request.getRemoteAddr());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts IP from RFC 7239 Forwarded header.
|
||||||
|
* Format: "for=192.0.2.60;proto=http;by=203.0.113.43"
|
||||||
|
*/
|
||||||
|
private static String extractFromForwardedHeader(String forwarded) {
|
||||||
|
try {
|
||||||
|
// Look for "for=" pattern
|
||||||
|
int forIndex = forwarded.toLowerCase().indexOf("for=");
|
||||||
|
if (forIndex == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract value after "for="
|
||||||
|
String afterFor = forwarded.substring(forIndex + 4);
|
||||||
|
// Value can be quoted or unquoted, and may contain port
|
||||||
|
// Stop at semicolon, comma, quote, or space
|
||||||
|
StringBuilder ipBuilder = new StringBuilder();
|
||||||
|
for (int i = 0; i < afterFor.length(); i++) {
|
||||||
|
char c = afterFor.charAt(i);
|
||||||
|
if (c == ';' || c == ',' || c == ' ' || c == '"') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ipBuilder.append(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
String candidate = ipBuilder.toString().trim();
|
||||||
|
return candidate.isEmpty() ? null : candidate;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Failed to parse Forwarded header: {}", forwarded, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes IP address string:
|
||||||
|
* - Removes brackets from IPv6 (e.g., "[2001:db8::1]" -> "2001:db8::1")
|
||||||
|
* - Removes port number if present (e.g., "192.168.1.1:8080" -> "192.168.1.1")
|
||||||
|
* - Trims whitespace
|
||||||
|
*
|
||||||
|
* @param ip IP address string (may contain brackets, port, etc.)
|
||||||
|
* @return Normalized IP address, or null if input is null
|
||||||
|
*/
|
||||||
|
private static String normalize(String ip) {
|
||||||
|
if (ip == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ip = ip.trim();
|
||||||
|
if (ip.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle IPv6 in brackets, e.g., "[2001:db8::1]"
|
||||||
|
if (ip.startsWith("[") && ip.endsWith("]")) {
|
||||||
|
ip = ip.substring(1, ip.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle "ip:port" formats
|
||||||
|
// For IPv4: "192.168.1.1:8080" -> "192.168.1.1"
|
||||||
|
// For IPv6: "2001:db8::1:8080" is ambiguous, but we try to detect port
|
||||||
|
int lastColon = ip.lastIndexOf(':');
|
||||||
|
if (lastColon > 0) {
|
||||||
|
// Check if part after last colon looks like a port number
|
||||||
|
String afterColon = ip.substring(lastColon + 1);
|
||||||
|
if (afterColon.chars().allMatch(Character::isDigit)) {
|
||||||
|
// Likely a port number
|
||||||
|
String beforeColon = ip.substring(0, lastColon);
|
||||||
|
// For IPv4, we can safely remove port
|
||||||
|
if (beforeColon.contains(".") && !beforeColon.contains("::")) {
|
||||||
|
ip = beforeColon;
|
||||||
|
}
|
||||||
|
// For IPv6, we need to be more careful - only remove if it's clearly a port
|
||||||
|
// IPv6 addresses can contain colons, so we check if it's a valid IPv6 format
|
||||||
|
// For simplicity, if it contains "::" or more than 2 colons, assume it's IPv6 and don't remove
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts IP address string to varbinary(16) format for database storage.
|
||||||
|
* Supports both IPv4 and IPv6.
|
||||||
|
*
|
||||||
|
* @param ipAddress IP address as string
|
||||||
|
* @return IP address as byte array (4 bytes for IPv4, 16 bytes for IPv6), or null if invalid
|
||||||
|
*/
|
||||||
|
public static byte[] ipToBytes(String ipAddress) {
|
||||||
|
if (ipAddress == null || ipAddress.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
InetAddress inetAddress = InetAddress.getByName(ipAddress);
|
||||||
|
return inetAddress.getAddress();
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
log.warn("Failed to parse IP address: {}", ipAddress, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts IP address from varbinary(16) format to string.
|
||||||
|
*
|
||||||
|
* @param ipBytes IP address as byte array
|
||||||
|
* @return IP address as string, or null if invalid
|
||||||
|
*/
|
||||||
|
public static String bytesToIp(byte[] ipBytes) {
|
||||||
|
if (ipBytes == null || ipBytes.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
InetAddress inetAddress = InetAddress.getByAddress(ipBytes);
|
||||||
|
return inetAddress.getHostAddress();
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
log.warn("Failed to convert IP bytes to string", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
27
src/main/java/com/honey/honey/util/TimeProvider.java
Normal file
27
src/main/java/com/honey/honey/util/TimeProvider.java
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package com.honey.honey.util;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized time provider for consistent time handling across the application.
|
||||||
|
* All time-related operations should use this provider.
|
||||||
|
*/
|
||||||
|
public class TimeProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets current Unix timestamp in seconds.
|
||||||
|
* @return Current epoch seconds
|
||||||
|
*/
|
||||||
|
public static long nowSeconds() {
|
||||||
|
return Instant.now().getEpochSecond();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets current Unix timestamp in milliseconds.
|
||||||
|
* @return Current epoch milliseconds
|
||||||
|
*/
|
||||||
|
public static long nowMillis() {
|
||||||
|
return Instant.now().toEpochMilli();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -46,6 +46,12 @@ app:
|
|||||||
# Maximum number of batches to process per cleanup run
|
# Maximum number of batches to process per cleanup run
|
||||||
max-batches-per-run: ${APP_SESSION_CLEANUP_MAX_BATCHES:20}
|
max-batches-per-run: ${APP_SESSION_CLEANUP_MAX_BATCHES:20}
|
||||||
|
|
||||||
|
# GeoIP configuration
|
||||||
|
# Set GEOIP_DB_PATH environment variable to use external file (recommended for production)
|
||||||
|
# If not set, falls back to classpath:geoip/GeoLite2-Country.mmdb
|
||||||
|
geoip:
|
||||||
|
db-path: ${GEOIP_DB_PATH:}
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
root: INFO
|
root: INFO
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
-- Drop foreign key constraint from sessions table if it exists
|
||||||
|
-- Find the foreign key name dynamically (MySQL auto-generates names like sessions_ibfk_1)
|
||||||
|
SET @fk_name = NULL;
|
||||||
|
SELECT CONSTRAINT_NAME INTO @fk_name
|
||||||
|
FROM information_schema.TABLE_CONSTRAINTS
|
||||||
|
WHERE CONSTRAINT_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'sessions'
|
||||||
|
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
SET @sql = IF(@fk_name IS NOT NULL,
|
||||||
|
CONCAT('ALTER TABLE sessions DROP FOREIGN KEY ', @fk_name),
|
||||||
|
'DO 1');
|
||||||
|
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- Drop old users table
|
||||||
|
DROP TABLE IF EXISTS users;
|
||||||
|
|
||||||
|
-- Create db_users_a table
|
||||||
|
CREATE TABLE `db_users_a` (
|
||||||
|
`id` int NOT NULL,
|
||||||
|
`screen_name` varchar(75) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '-',
|
||||||
|
`telegram_id` bigint UNSIGNED DEFAULT NULL,
|
||||||
|
`telegram_name` varchar(33) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '-',
|
||||||
|
`is_premium` int NOT NULL DEFAULT '0',
|
||||||
|
`language_code` varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'XX',
|
||||||
|
`country_code` varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'XX',
|
||||||
|
`device_code` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'XX',
|
||||||
|
`ip` varbinary(16) DEFAULT NULL,
|
||||||
|
`date_reg` int NOT NULL DEFAULT '0',
|
||||||
|
`date_login` int NOT NULL DEFAULT '0',
|
||||||
|
`banned` int NOT NULL DEFAULT '0'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
|
-- Create db_users_b table
|
||||||
|
CREATE TABLE `db_users_b` (
|
||||||
|
`id` int NOT NULL DEFAULT '0',
|
||||||
|
`balance_a` bigint UNSIGNED NOT NULL DEFAULT '0',
|
||||||
|
`balance_b` bigint UNSIGNED NOT NULL DEFAULT '0',
|
||||||
|
`deposit_total` bigint NOT NULL DEFAULT '0',
|
||||||
|
`deposit_count` int NOT NULL DEFAULT '0',
|
||||||
|
`withdraw_total` bigint NOT NULL DEFAULT '0',
|
||||||
|
`withdraw_count` int NOT NULL DEFAULT '0'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
|
-- Create db_users_d table
|
||||||
|
CREATE TABLE `db_users_d` (
|
||||||
|
`id` int NOT NULL DEFAULT '0',
|
||||||
|
`referer_id_1` int NOT NULL DEFAULT '0',
|
||||||
|
`referer_id_2` int NOT NULL DEFAULT '0',
|
||||||
|
`referer_id_3` int NOT NULL DEFAULT '0',
|
||||||
|
`referer_id_4` int NOT NULL DEFAULT '0',
|
||||||
|
`referer_id_5` int NOT NULL DEFAULT '0',
|
||||||
|
`master_id` int NOT NULL DEFAULT '0',
|
||||||
|
`referals_1` int NOT NULL DEFAULT '0',
|
||||||
|
`referals_2` int NOT NULL DEFAULT '0',
|
||||||
|
`referals_3` int NOT NULL DEFAULT '0',
|
||||||
|
`referals_4` int NOT NULL DEFAULT '0',
|
||||||
|
`referals_5` int NOT NULL DEFAULT '0',
|
||||||
|
`from_referals_1` bigint NOT NULL DEFAULT '0',
|
||||||
|
`from_referals_2` bigint NOT NULL DEFAULT '0',
|
||||||
|
`from_referals_3` bigint NOT NULL DEFAULT '0',
|
||||||
|
`from_referals_4` bigint NOT NULL DEFAULT '0',
|
||||||
|
`from_referals_5` bigint NOT NULL DEFAULT '0',
|
||||||
|
`to_referer_1` bigint NOT NULL DEFAULT '0',
|
||||||
|
`to_referer_2` bigint NOT NULL DEFAULT '0',
|
||||||
|
`to_referer_3` bigint NOT NULL DEFAULT '0',
|
||||||
|
`to_referer_4` bigint NOT NULL DEFAULT '0',
|
||||||
|
`to_referer_5` bigint NOT NULL DEFAULT '0'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
|
-- Add indexes for db_users_a
|
||||||
|
ALTER TABLE `db_users_a`
|
||||||
|
ADD PRIMARY KEY (`id`),
|
||||||
|
ADD UNIQUE KEY `telegram_id` (`telegram_id`),
|
||||||
|
ADD KEY `telegram_name` (`telegram_name`),
|
||||||
|
ADD KEY `ip` (`ip`);
|
||||||
|
|
||||||
|
-- Add indexes for db_users_b
|
||||||
|
ALTER TABLE `db_users_b`
|
||||||
|
ADD KEY `id` (`id`);
|
||||||
|
|
||||||
|
-- Add indexes for db_users_d
|
||||||
|
ALTER TABLE `db_users_d`
|
||||||
|
ADD KEY `id` (`id`),
|
||||||
|
ADD KEY `referer_id_1` (`referer_id_1`),
|
||||||
|
ADD KEY `referer_id_2` (`referer_id_2`),
|
||||||
|
ADD KEY `referer_id_3` (`referer_id_3`),
|
||||||
|
ADD KEY `referer_id_4` (`referer_id_4`),
|
||||||
|
ADD KEY `referer_id_5` (`referer_id_5`),
|
||||||
|
ADD KEY `master_id` (`master_id`);
|
||||||
|
|
||||||
|
-- Set auto increment for db_users_a
|
||||||
|
ALTER TABLE `db_users_a`
|
||||||
|
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||||
|
|
||||||
|
-- Update sessions table: change user_id from BIGINT to INT to match db_users_a.id
|
||||||
|
ALTER TABLE `sessions`
|
||||||
|
MODIFY `user_id` int NOT NULL;
|
||||||
|
|
||||||
|
-- Add foreign key constraint from sessions to db_users_a
|
||||||
|
ALTER TABLE `sessions`
|
||||||
|
ADD CONSTRAINT `fk_sessions_user_id` FOREIGN KEY (`user_id`) REFERENCES `db_users_a`(`id`) ON DELETE CASCADE;
|
||||||
|
|
||||||
BIN
src/main/resources/geoip/GeoLite2-Country.mmdb
Normal file
BIN
src/main/resources/geoip/GeoLite2-Country.mmdb
Normal file
Binary file not shown.
Reference in New Issue
Block a user